diff --git a/src/Fable.AST/Fable.fs b/src/Fable.AST/Fable.fs index 92708c4e9b..dc3699db3f 100644 --- a/src/Fable.AST/Fable.fs +++ b/src/Fable.AST/Fable.fs @@ -479,6 +479,7 @@ type Ident = IsThisArgument: bool IsCompilerGenerated: bool Range: SourceLocation option + IsInlineIfLambda: bool } member x.DisplayName = diff --git a/src/Fable.Cli/CHANGELOG.md b/src/Fable.Cli/CHANGELOG.md index 0277caea5d..58b40f472b 100644 --- a/src/Fable.Cli/CHANGELOG.md +++ b/src/Fable.Cli/CHANGELOG.md @@ -29,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [Beam] Fix non-ASCII characters in string literals being truncated to single bytes — emit `<<"..."/utf8>>` instead of `<<"...">>` (by @dbrattli) * [Beam] Fix `Emit` expressions with `case` leaking variables into surrounding scope — auto-wrap in IIFE for scope isolation (by @dbrattli) +### Added + +[All] Add support for `[]` (by @MangelMaxime) + ## 5.0.0-rc.3 - 2026-03-10 ### Added diff --git a/src/Fable.Compiler/CHANGELOG.md b/src/Fable.Compiler/CHANGELOG.md index 1704948880..252234e980 100644 --- a/src/Fable.Compiler/CHANGELOG.md +++ b/src/Fable.Compiler/CHANGELOG.md @@ -29,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * [Beam] Fix non-ASCII characters in string literals being truncated to single bytes — emit `<<"..."/utf8>>` instead of `<<"...">>` (by @dbrattli) * [Beam] Fix `Emit` expressions with `case` leaking variables into surrounding scope — auto-wrap in IIFE for scope isolation (by @dbrattli) +### Added + +[All] Add support for `[]` (by @MangelMaxime) + ## 5.0.0-rc.10 - 2026-03-10 ### Added diff --git a/src/Fable.Transforms/FSharp2Fable.Util.fs b/src/Fable.Transforms/FSharp2Fable.Util.fs index 6af477616a..c2516a36fa 100644 --- a/src/Fable.Transforms/FSharp2Fable.Util.fs +++ b/src/Fable.Transforms/FSharp2Fable.Util.fs @@ -1827,6 +1827,9 @@ module Identifiers = IsCompilerGenerated = fsRef.IsCompilerGenerated IsMutable = isMutable Range = Some r + IsInlineIfLambda = + fsRef.Attributes + |> Seq.exists (fun attr -> attr.AttributeType.FullName = Atts.inlineIfLambda) } let putIdentInScope com ctx (fsRef: FSharpMemberOrFunctionOrValue) value : Context * Fable.Ident = diff --git a/src/Fable.Transforms/FSharp2Fable.fs b/src/Fable.Transforms/FSharp2Fable.fs index 72348c8a7d..55ff5b16cd 100644 --- a/src/Fable.Transforms/FSharp2Fable.fs +++ b/src/Fable.Transforms/FSharp2Fable.fs @@ -2615,9 +2615,9 @@ type FableCompiler(com: Compiler) = ResolvedIdents = Dictionary() } - let ctx, bindings = - ((ctx, []), foldArgs [] (inExpr.Args, args)) - ||> List.fold (fun (ctx, bindings) (argId, arg) -> + let ctx, bindings, forceInlineMap = + (((ctx, [], Map.empty)), foldArgs [] (inExpr.Args, args)) + ||> List.fold (fun (ctx, bindings, forceInlineMap) (argId, arg) -> let argId = resolveInlineIdent ctx info argId // Change type and mark argId as compiler-generated so Fable also // tries to inline it in DEBUG mode (some patterns depend on this) @@ -2627,9 +2627,101 @@ type FableCompiler(com: Compiler) = IsCompilerGenerated = true } + // If this parameter has [] and the argument is a lambda/delegate, + // force-inline it directly into the body (duplicate at each call site) rather + // than creating a Let binding that beta-reduction may refuse to inline. + let isInlineIfLambda = argId.IsInlineIfLambda + + // Helper: try to find a non-inline module-level declaration by its Fable name and + // reconstruct it as a Lambda/Delegate by binding its FSC args and transforming its body. + let tryLambdaFromDeclarations (name: string) = + let decls = com.GetImplementationFile(com.CurrentFile) + + let rec tryFindInDecls (decls: FSharpImplementationFileDeclaration list) = + decls + |> List.tryPick (fun decl -> + match decl with + | FSharpImplementationFileDeclaration.Entity(_, subDecls) -> tryFindInDecls subDecls + | FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(memb, membArgIds, fsBody) when + not (isInline memb) + -> + let declName, _ = getMemberDeclarationName (this :> Compiler) memb + + if declName = name then + let flatArgs = List.concat membArgIds + + match flatArgs with + | [] -> + // No args: transform body directly (may already be Lambda/Delegate) + let fableBody = (this :> IFableCompiler).Transform(ctx, fsBody) + + match fableBody with + | Fable.Lambda _ + | Fable.Delegate _ -> Some fableBody + | _ -> None + | _ -> + // Has args: bind them and wrap transformed body in a lambda + let bodyCtx, fableArgs = bindMemberArgs this ctx membArgIds + let fableBody = (this :> IFableCompiler).Transform(bodyCtx, fsBody) + + match fableArgs with + | [ singleArg ] -> Some(Fable.Lambda(singleArg, fableBody, None)) + | multiArgs -> + Some(Fable.Delegate(multiArgs, fableBody, None, Fable.Tags.empty)) + else + None + | _ -> None + ) + + tryFindInDecls decls + + let effectiveArg = + match arg with + | Fable.IdentExpr identRef -> + // First try local scope (covers anonymous lambda pre-bound by FSC, Case 1) + let fromScope = + ctx.Scope + |> List.tryPick (fun (_, scopeIdent, scopeValue) -> + if scopeIdent.Name = identRef.Name then + scopeValue + else + None + ) + + match fromScope with + | Some _ -> fromScope |> Option.defaultValue arg + | None -> + // Second, try module-level declarations (IdentExpr case when scope lookup fails) + if isInlineIfLambda then + tryLambdaFromDeclarations identRef.Name |> Option.defaultValue arg + else + arg + | Fable.Lambda(lambdaArg, Fable.Call(Fable.IdentExpr calleeIdent, callInfo, _, _), _) when + isInlineIfLambda + && callInfo.Args.Length = 1 + && ( + match callInfo.Args.[0] with + | Fable.IdentExpr a -> a.Name = lambdaArg.Name + | _ -> false + ) + -> + // FSC may eta-expand a named function: `myAction` → `fun x -> myAction(x)`. + // Resolve the callee's body from declarations so we inline the real body. + tryLambdaFromDeclarations calleeIdent.Name |> Option.defaultValue arg + | _ -> arg + let ctx = { ctx with Scope = (None, argId, Some arg) :: ctx.Scope } - ctx, (argId, arg) :: bindings + let isLambdaOrDelegate = + match effectiveArg with + | Fable.Lambda _ + | Fable.Delegate _ -> true + | _ -> false + + if isInlineIfLambda && isLambdaOrDelegate then + (ctx, bindings, Map.add argId.Name effectiveArg forceInlineMap) + else + (ctx, (argId, arg) :: bindings, forceInlineMap) ) let ctx = @@ -2643,6 +2735,21 @@ type FableCompiler(com: Compiler) = let resolved = resolveInlineExpr this ctx info inExpr.Body + // Substitute [] lambdas directly into the body at each use site + let resolved = + if Map.isEmpty forceInlineMap then + resolved + else + resolved + |> visitFromInsideOut ( + function + | Fable.IdentExpr id as e -> + match Map.tryFind id.Name forceInlineMap with + | Some replacement -> replacement + | None -> e + | e -> e + ) + // Some patterns depend on inlined arguments being captured by "magic" Fable.Core functions like // importValueDynamic. If the value can have side effects, it won't be removed by beta binding // reduction, so we try to eliminate it here. @@ -2747,11 +2854,34 @@ let getInlineExprs fileName (declarations: FSharpImplementationFileDeclaration l ctx, ident :: idents ) + // CurriedParameterGroups is the authoritative source for parameter-level + // attributes like []. The argIds (FSharpMemberOrFunctionOrValue) + // are body-level variables and may not carry parameter attributes on fsRef.Attributes. + // So we zip the built idents with the flattened CurriedParameterGroups and annotate. + // NOTE: for instance members the first ident is the `this` argument, which is NOT + // included in CurriedParameterGroups — skip it when indexing into flatParams. + let flatParams = memb.CurriedParameterGroups |> Seq.collect id |> Seq.toList + + let thisOffset = + if memb.IsInstanceMember then + 1 + else + 0 + + let args = + List.rev idents + |> List.mapi (fun i ident -> + match List.tryItem (i - thisOffset) flatParams with + | Some param when i >= thisOffset && hasAttrib Atts.inlineIfLambda param.Attributes -> + { ident with IsInlineIfLambda = true } + | _ -> ident + ) + // It looks as we don't need memb.DeclaringEntity.GenericParameters here let genArgs = memb.GenericParameters |> Seq.mapToList (genParamName) { - Args = List.rev idents + Args = args Body = com.Transform(ctx, body) FileName = fileName GenericArgs = genArgs diff --git a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs index e4d791e7b1..a4faafa166 100644 --- a/src/Fable.Transforms/Python/Fable2Python.Transforms.fs +++ b/src/Fable.Transforms/Python/Fable2Python.Transforms.fs @@ -653,6 +653,7 @@ let transformObjectExpr IsThisArgument = false IsCompilerGenerated = true Range = r + IsInlineIfLambda = false } | e -> e ) diff --git a/src/Fable.Transforms/Transforms.Util.fs b/src/Fable.Transforms/Transforms.Util.fs index eafdc2e2fc..a5d1822e4b 100644 --- a/src/Fable.Transforms/Transforms.Util.fs +++ b/src/Fable.Transforms/Transforms.Util.fs @@ -37,6 +37,9 @@ module Atts = [] let entryPoint = "Microsoft.FSharp.Core.EntryPointAttribute" // typeof.FullName + [] + let inlineIfLambda = "Microsoft.FSharp.Core.InlineIfLambdaAttribute" // typeof.FullName + [] let sealed_ = "Microsoft.FSharp.Core.SealedAttribute" // typeof.FullName @@ -968,6 +971,7 @@ module AST = IsThisArgument = false IsMutable = false Range = None + IsInlineIfLambda = false } /// ATTENTION: Make sure the ident name is unique diff --git a/tests/Dart/main.dart b/tests/Dart/main.dart index 52baa0b0b8..78cb2f2744 100644 --- a/tests/Dart/main.dart +++ b/tests/Dart/main.dart @@ -7,6 +7,7 @@ import './src/DictionaryTests.dart' as dictionary; import './src/EnumTests.dart' as enum_; import './src/EnumerableTests.dart' as enumerable; import './src/HashSetTests.dart' as hash_set; +import './src/InlineIfLambdaTests.dart' as inline_if_lambda; import './src/ListTests.dart' as list; import './src/MapTests.dart' as map; import './src/MiscTests.dart' as misc; @@ -37,6 +38,7 @@ void main() { enum_.tests(); enumerable.tests(); hash_set.tests(); + inline_if_lambda.tests(); list.tests(); map.tests(); misc.tests(); @@ -56,4 +58,4 @@ void main() { tuple.tests(); type_.tests(); union.tests(); -} \ No newline at end of file +} diff --git a/tests/Dart/src/Fable.Tests.Dart.fsproj b/tests/Dart/src/Fable.Tests.Dart.fsproj index b54bdbb5dc..92442e23f0 100644 --- a/tests/Dart/src/Fable.Tests.Dart.fsproj +++ b/tests/Dart/src/Fable.Tests.Dart.fsproj @@ -36,6 +36,7 @@ + diff --git a/tests/Dart/src/InlineIfLambdaTests.fs b/tests/Dart/src/InlineIfLambdaTests.fs new file mode 100644 index 0000000000..8a4afe8ec9 --- /dev/null +++ b/tests/Dart/src/InlineIfLambdaTests.fs @@ -0,0 +1,94 @@ +module Fable.Tests.Dart.InlineIfLambda + +open Util + +// ── helpers ──────────────────────────────────────────────────────────────────── + +let inline iterateTwice ([] action) (array: 'T[]) = + for i = 0 to array.Length - 1 do + action array[i] + for i = 0 to array.Length - 1 do + action array[i] + +let inline fold ([] folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]) = + let mutable acc = state + for i = 0 to array.Length - 1 do + acc <- folder acc array[i] + acc + +let inline applyToFirst ([] f: 'T -> 'U) (x: 'T) = f x + +let inline applyTwice ([] f: 'T -> 'T) (x: 'T) = + f (f x) + +let inline applyPairTwice ([] f: int -> int -> int) a b = + let first = f a b + f first b + +type TwiceBuilder() = + member inline _.Return(x) = x + member inline _.Delay([] f: unit -> 'T) = + let _ = f () + f () + member inline _.Run(x) = x + +let twice = TwiceBuilder() + +// ── tests ────────────────────────────────────────────────────────────────────── + +let tests () = + testCase "Anonymous lambda is inlined across multiple uses" <| fun () -> + let arr = [| 1..10 |] + let mutable sum = 0 + arr |> iterateTwice (fun x -> sum <- sum + x) + sum |> equal 110 + + testCase "Named local lambda is inlined" <| fun () -> + let arr = [| 1..10 |] + let mutable sum = 0 + let myAction = fun x -> sum <- sum + x + arr |> iterateTwice myAction + sum |> equal 110 + + testCase "InlineIfLambda works with fold-style accumulator" <| fun () -> + let arr = [| 1..5 |] + let sum = fold (fun acc x -> acc + x) 0 arr + sum |> equal 15 + + testCase "InlineIfLambda single application works" <| fun () -> + let result = applyToFirst (fun x -> x * 2) 21 + result |> equal 42 + + testCase "InlineIfLambda applied twice works" <| fun () -> + let result = applyTwice (fun x -> x + 3) 1 + result |> equal 7 + + testCase "InlineIfLambda with two parameters works" <| fun () -> + let result = + applyPairTwice + (fun a b -> a + b) + 10 + 32 + result |> equal 74 + + testCase "InlineIfLambda captures mutable correctly" <| fun () -> + let mutable count = 0 + let arr = [| 1..5 |] + arr |> iterateTwice (fun _ -> count <- count + 1) + count |> equal 10 + + testCase "InlineIfLambda with branching" <| fun () -> + let arr = [| 1..10 |] + let mutable evens = 0 + arr |> iterateTwice (fun x -> if x % 2 = 0 then evens <- evens + 1) + evens |> equal 10 + + testCase "InlineIfLambda on CE builder Delay inlines body at each call site" <| fun () -> + let mutable callCount = 0 + let result = + twice { + callCount <- callCount + 1 + return callCount + } + callCount |> equal 2 + result |> equal 2 diff --git a/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.fs b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.fs new file mode 100644 index 0000000000..42e5be68c3 --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.fs @@ -0,0 +1,13 @@ +module AnonymousLambda + +let inline iterateTwice ([] action) (array: 'T[]) = + for i = 0 to array.Length-1 do + action array[i] + for i = 0 to array.Length-1 do + action array[i] + +let arr = [| 1.. 100 |] +let mutable sum = 0 +arr |> iterateTwice (fun x -> + sum <- sum + x +) diff --git a/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected new file mode 100644 index 0000000000..c439bf61a3 --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/AnonymousLambda.jsx.expected @@ -0,0 +1,22 @@ + +import { toArray } from "./fable_modules/fable-library-js.5.0.0-rc.3/Seq.js"; +import { rangeDouble } from "./fable_modules/fable-library-js.5.0.0-rc.3/Range.js"; +import { createAtom } from "./fable_modules/fable-library-js.5.0.0-rc.3/Util.js"; +import { item } from "./fable_modules/fable-library-js.5.0.0-rc.3/Array.js"; + +export const arr = toArray(rangeDouble(1, 1, 100)); + +export let sum = createAtom(0); + +(function () { + const array_1 = arr; + for (let i = 0; i <= (array_1.length - 1); i++) { + const x = item(i, array_1) | 0; + sum(sum() + x); + } + for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { + const x = item(i_1, array_1) | 0; + sum(sum() + x); + } +})(); + diff --git a/tests/Integration/Integration/data/inlineIfLambda/CEBuilderMethod.fs b/tests/Integration/Integration/data/inlineIfLambda/CEBuilderMethod.fs new file mode 100644 index 0000000000..f66cef7930 --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/CEBuilderMethod.fs @@ -0,0 +1,17 @@ +module CEBuilderMethod + +type TwiceBuilder() = + member inline _.Return(x) = x + member inline _.Delay([] f: unit -> 'T) = + let _ = f () // first call: side-effect runs here + f () // second call: result comes from here + member inline _.Run(x) = x + +let twice = TwiceBuilder() + +let mutable callCount = 0 +let result = + twice { + callCount <- callCount + 1 + return callCount + } diff --git a/tests/Integration/Integration/data/inlineIfLambda/CEBuilderMethod.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/CEBuilderMethod.jsx.expected new file mode 100644 index 0000000000..e72d692649 --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/CEBuilderMethod.jsx.expected @@ -0,0 +1,23 @@ + +import { class_type } from "./fable_modules/fable-library-js.5.0.0-rc.3/Reflection.js"; +import { createAtom } from "./fable_modules/fable-library-js.5.0.0-rc.3/Util.js"; + +export class TwiceBuilder { + constructor() { + } +} + +export function TwiceBuilder_$reflection() { + return class_type("CEBuilderMethod.TwiceBuilder", undefined, TwiceBuilder); +} + +export function TwiceBuilder_$ctor() { + return new TwiceBuilder(); +} + +export const twice = TwiceBuilder_$ctor(); + +export let callCount = createAtom(0); + +export const result = ((callCount(callCount() + 1), callCount()), (callCount(callCount() + 1), callCount())); + diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.fs b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.fs new file mode 100644 index 0000000000..fc0926bd07 --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.fs @@ -0,0 +1,13 @@ +module NamedFunction + +let inline iterateTwice ([] action) (array: 'T[]) = + for i = 0 to array.Length-1 do + action array[i] + for i = 0 to array.Length-1 do + action array[i] + +let arr = [| 1.. 100 |] +let mutable sum = 0 + +let addToSum x = sum <- sum + x +arr |> iterateTwice addToSum diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected new file mode 100644 index 0000000000..6cef943fe9 --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedFunction.jsx.expected @@ -0,0 +1,26 @@ + +import { toArray } from "./fable_modules/fable-library-js.5.0.0-rc.3/Seq.js"; +import { rangeDouble } from "./fable_modules/fable-library-js.5.0.0-rc.3/Range.js"; +import { createAtom } from "./fable_modules/fable-library-js.5.0.0-rc.3/Util.js"; +import { item } from "./fable_modules/fable-library-js.5.0.0-rc.3/Array.js"; + +export const arr = toArray(rangeDouble(1, 1, 100)); + +export let sum = createAtom(0); + +export function addToSum(x) { + sum(sum() + x); +} + +(function () { + const array_1 = arr; + for (let i = 0; i <= (array_1.length - 1); i++) { + const x_1 = item(i, array_1) | 0; + sum(sum() + x_1); + } + for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { + const x_1 = item(i_1, array_1) | 0; + sum(sum() + x_1); + } +})(); + diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.fs b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.fs new file mode 100644 index 0000000000..59fc1d7abb --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.fs @@ -0,0 +1,13 @@ +module NamedLambdaValue + +let inline iterateTwice ([] action) (array: 'T[]) = + for i = 0 to array.Length-1 do + action array[i] + for i = 0 to array.Length-1 do + action array[i] + +let arr = [| 1.. 100 |] +let mutable sum = 0 + +let myAction = fun x -> sum <- sum + x +arr |> iterateTwice myAction diff --git a/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected new file mode 100644 index 0000000000..08f3dd85bc --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/NamedLambdaValue.jsx.expected @@ -0,0 +1,26 @@ + +import { toArray } from "./fable_modules/fable-library-js.5.0.0-rc.3/Seq.js"; +import { rangeDouble } from "./fable_modules/fable-library-js.5.0.0-rc.3/Range.js"; +import { createAtom } from "./fable_modules/fable-library-js.5.0.0-rc.3/Util.js"; +import { item } from "./fable_modules/fable-library-js.5.0.0-rc.3/Array.js"; + +export const arr = toArray(rangeDouble(1, 1, 100)); + +export let sum = createAtom(0); + +export function myAction(x) { + sum(sum() + x); +} + +(function () { + const array_1 = arr; + for (let i = 0; i <= (array_1.length - 1); i++) { + const x_1 = item(i, array_1) | 0; + sum(sum() + x_1); + } + for (let i_1 = 0; i_1 <= (array_1.length - 1); i_1++) { + const x_1 = item(i_1, array_1) | 0; + sum(sum() + x_1); + } +})(); + diff --git a/tests/Integration/Integration/data/inlineIfLambda/TwoParameters.fs b/tests/Integration/Integration/data/inlineIfLambda/TwoParameters.fs new file mode 100644 index 0000000000..570e70265b --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/TwoParameters.fs @@ -0,0 +1,16 @@ +module TwoParameters + +let inline applyPairTwice ([] f: int -> int -> int) a b = + let first = f a b + f first b + +let result = + applyPairTwice + (fun a b -> + printfn "Adding %d and %d" a b + a + b + ) + 10 + 32 + +printfn "result: %d" result diff --git a/tests/Integration/Integration/data/inlineIfLambda/TwoParameters.jsx.expected b/tests/Integration/Integration/data/inlineIfLambda/TwoParameters.jsx.expected new file mode 100644 index 0000000000..15765c069c --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/TwoParameters.jsx.expected @@ -0,0 +1,12 @@ + +import { printf, toConsole } from "./fable_modules/fable-library-js.5.0.0-rc.3/String.js"; + +export const result = (() => { + let a; + toConsole(printf("Adding %d and %d"))(10)(32); + a = 42; + toConsole(printf("Adding %d and %d"))(a)(32); + return (a + 32) | 0; +})(); + +toConsole(printf("result: %d"))(result); diff --git a/tests/Integration/Integration/data/inlineIfLambda/inlineIfLambda.fsproj b/tests/Integration/Integration/data/inlineIfLambda/inlineIfLambda.fsproj new file mode 100644 index 0000000000..585b7e8c0e --- /dev/null +++ b/tests/Integration/Integration/data/inlineIfLambda/inlineIfLambda.fsproj @@ -0,0 +1,21 @@ + + + + net10.0 + Major + true + + + + + + + + + + + + + + + diff --git a/tests/Js/Main/Fable.Tests.fsproj b/tests/Js/Main/Fable.Tests.fsproj index 059a764e6e..4959ece88c 100644 --- a/tests/Js/Main/Fable.Tests.fsproj +++ b/tests/Js/Main/Fable.Tests.fsproj @@ -56,6 +56,7 @@ + diff --git a/tests/Js/Main/InlineIfLambdaTests.fs b/tests/Js/Main/InlineIfLambdaTests.fs new file mode 100644 index 0000000000..f2799ed330 --- /dev/null +++ b/tests/Js/Main/InlineIfLambdaTests.fs @@ -0,0 +1,118 @@ +module Fable.Tests.InlineIfLambda + +open Util.Testing + +// ── helpers used by the tests ──────────────────────────────────────────────── + +let inline iterateTwice ([] action) (array: 'T[]) = + for i = 0 to array.Length - 1 do + action array[i] + for i = 0 to array.Length - 1 do + action array[i] + +let inline fold ([] folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]) = + let mutable acc = state + for i = 0 to array.Length - 1 do + acc <- folder acc array[i] + acc + +let inline applyToFirst ([] f: 'T -> 'U) (x: 'T) = f x + +let inline applyTwice ([] f: 'T -> 'T) (x: 'T) = + f (f x) + +// multi-arg InlineIfLambda +let inline applyPairTwice ([] f: int -> int -> int) a b = + let first = f a b + f first b + +// ── CE builder that demonstrates [] on instance methods ────── +// +// `Delay` calls its thunk *twice* — once as a dry-run, once for the real result. +// Without [] the thunk would become a closure variable because +// `canInlineArg` blocks inlining when refCount > 1. With [] the +// body is substituted at each call site so the side-effect (incrementing `log`) +// runs twice as expected and no closure is allocated. +type TwiceBuilder() = + member inline _.Return(x) = x + member inline _.Delay([] f: unit -> 'T) = + let _ = f () // first call: side-effect runs here + f () // second call: result comes from here + member inline _.Run(x) = x + +let twice = TwiceBuilder() + +// ── tests ──────────────────────────────────────────────────────────────────── + +let tests = + testList "InlineIfLambda" [ + + testCase "Anonymous lambda is inlined across multiple uses" <| fun () -> + let arr = [| 1..10 |] + let mutable sum = 0 + arr |> iterateTwice (fun x -> sum <- sum + x) + // each element visited twice → 2 * 55 = 110 + sum |> equal 110 + + testCase "Named module-level lambda is inlined" <| fun () -> + let arr = [| 1..10 |] + let mutable sum = 0 + let myAction = fun x -> sum <- sum + x + arr |> iterateTwice myAction + sum |> equal 110 + + testCase "InlineIfLambda works with fold-style accumulator" <| fun () -> + let arr = [| 1..5 |] + let sum = fold (fun acc x -> acc + x) 0 arr + sum |> equal 15 + + testCase "InlineIfLambda single application works" <| fun () -> + let result = applyToFirst (fun x -> x * 2) 21 + result |> equal 42 + + testCase "InlineIfLambda applied twice works" <| fun () -> + let result = applyTwice (fun x -> x + 3) 1 + result |> equal 7 + + testCase "InlineIfLambda with two parameters works" <| fun () -> + let result = + applyPairTwice + (fun a b -> + printfn "Adding %d and %d" a b + a + b + ) + 10 + 32 + result |> equal 74 + + testCase "InlineIfLambda captures mutable correctly" <| fun () -> + let mutable count = 0 + let arr = [| 1..5 |] + // action captures mutable count + arr |> iterateTwice (fun _ -> count <- count + 1) + // 5 elements * 2 loops = 10 increments + count |> equal 10 + + testCase "InlineIfLambda with early return via exception-free branching" <| fun () -> + let arr = [| 1..10 |] + let mutable evens = 0 + arr |> iterateTwice (fun x -> if x % 2 = 0 then evens <- evens + 1) + // 5 even numbers visited twice + evens |> equal 10 + + testCase "InlineIfLambda on CE builder Delay inlines body at each call site" <| fun () -> + // `twice { ... }` desugars to `twice.Run(twice.Delay(fun () -> ...))`. + // `Delay` calls its thunk twice, so the body runs twice. + // With [] the thunk is inlined; without it `canInlineArg` + // would keep a closure because refCount = 2. + let mutable callCount = 0 + let result = + twice { + callCount <- callCount + 1 + return callCount + } + // Body ran twice: callCount = 2, return value = 2 (second call) + callCount |> equal 2 + result |> equal 2 + + ] diff --git a/tests/Js/Main/Main.fs b/tests/Js/Main/Main.fs index 8891aed675..ca2d7a0244 100644 --- a/tests/Js/Main/Main.fs +++ b/tests/Js/Main/Main.fs @@ -25,6 +25,7 @@ let allTests = Event.tests HashSets.tests Import.tests + InlineIfLambda.tests JsInterop.tests Lists.tests Maps.tests diff --git a/tests/Python/Fable.Tests.Python.fsproj b/tests/Python/Fable.Tests.Python.fsproj index e8c0644094..bc9cfdc216 100644 --- a/tests/Python/Fable.Tests.Python.fsproj +++ b/tests/Python/Fable.Tests.Python.fsproj @@ -82,6 +82,7 @@ + diff --git a/tests/Python/TestInlineIfLambda.fs b/tests/Python/TestInlineIfLambda.fs new file mode 100644 index 0000000000..bdc2b7db7b --- /dev/null +++ b/tests/Python/TestInlineIfLambda.fs @@ -0,0 +1,105 @@ +module Fable.Tests.InlineIfLambda + +open Util.Testing + +// ── helpers ──────────────────────────────────────────────────────────────────── + +let inline iterateTwice ([] action) (array: 'T[]) = + for i = 0 to array.Length - 1 do + action array[i] + for i = 0 to array.Length - 1 do + action array[i] + +let inline fold ([] folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]) = + let mutable acc = state + for i = 0 to array.Length - 1 do + acc <- folder acc array[i] + acc + +let inline applyToFirst ([] f: 'T -> 'U) (x: 'T) = f x + +let inline applyTwice ([] f: 'T -> 'T) (x: 'T) = + f (f x) + +let inline applyPairTwice ([] f: int -> int -> int) a b = + let first = f a b + f first b + +type TwiceBuilder() = + member inline _.Return(x) = x + member inline _.Delay([] f: unit -> 'T) = + let _ = f () + f () + member inline _.Run(x) = x + +let twice = TwiceBuilder() + +// ── tests ────────────────────────────────────────────────────────────────────── + +[] +let ``test Anonymous lambda is inlined across multiple uses`` () = + let arr = [| 1..10 |] + let mutable sum = 0 + arr |> iterateTwice (fun x -> sum <- sum + x) + sum |> equal 110 + +[] +let ``test Named module-level lambda is inlined`` () = + let arr = [| 1..10 |] + let mutable sum = 0 + let myAction = fun x -> sum <- sum + x + arr |> iterateTwice myAction + sum |> equal 110 + +[] +let ``test InlineIfLambda works with fold-style accumulator`` () = + let arr = [| 1..5 |] + let sum = fold (fun acc x -> acc + x) 0 arr + sum |> equal 15 + +[] +let ``test InlineIfLambda single application works`` () = + let result = applyToFirst (fun x -> x * 2) 21 + result |> equal 42 + +[] +let ``test InlineIfLambda applied twice works`` () = + let result = applyTwice (fun x -> x + 3) 1 + result |> equal 7 + +[] +let ``test InlineIfLambda with two parameters works`` () = + let result = + applyPairTwice + (fun a b -> + printfn "Adding %d and %d" a b + a + b + ) + 10 + 32 + result |> equal 74 + +[] +let ``test InlineIfLambda captures mutable correctly`` () = + let mutable count = 0 + let arr = [| 1..5 |] + arr |> iterateTwice (fun _ -> count <- count + 1) + count |> equal 10 + +[] +let ``test InlineIfLambda with early return via exception-free branching`` () = + let arr = [| 1..10 |] + let mutable evens = 0 + arr |> iterateTwice (fun x -> if x % 2 = 0 then evens <- evens + 1) + evens |> equal 10 + +[] +let ``test InlineIfLambda on CE builder Delay inlines body at each call site`` () = + let mutable callCount = 0 + let result = + twice { + callCount <- callCount + 1 + return callCount + } + callCount |> equal 2 + result |> equal 2 diff --git a/tests/Rust/Fable.Tests.Rust.fsproj b/tests/Rust/Fable.Tests.Rust.fsproj index ec9f77be9d..82e529c4ab 100644 --- a/tests/Rust/Fable.Tests.Rust.fsproj +++ b/tests/Rust/Fable.Tests.Rust.fsproj @@ -77,6 +77,7 @@ + diff --git a/tests/Rust/tests/src/InlineIfLambdaTests.fs b/tests/Rust/tests/src/InlineIfLambdaTests.fs new file mode 100644 index 0000000000..a1eeeb4ced --- /dev/null +++ b/tests/Rust/tests/src/InlineIfLambdaTests.fs @@ -0,0 +1,104 @@ +module Fable.Tests.InlineIfLambdaTests + +open Util.Testing + +// ── helpers ──────────────────────────────────────────────────────────────────── + +let inline iterateTwice ([] action) (array: 'T[]) = + for i = 0 to array.Length - 1 do + action array[i] + for i = 0 to array.Length - 1 do + action array[i] + +let inline fold ([] folder: 'State -> 'T -> 'State) (state: 'State) (array: 'T[]) = + let mutable acc = state + for i = 0 to array.Length - 1 do + acc <- folder acc array[i] + acc + +let inline applyToFirst ([] f: 'T -> 'U) (x: 'T) = f x + +let inline applyTwice ([] f: 'T -> 'T) (x: 'T) = + f (f x) + +let inline applyPairTwice ([] f: int -> int -> int) a b = + let first = f a b + f first b + +type TwiceBuilder() = + member inline _.Return(x) = x + member inline _.Delay([] f: unit -> 'T) = + let _ = f () + f () + member inline _.Run(x) = x + +let twice = TwiceBuilder() + +// ── tests ────────────────────────────────────────────────────────────────────── + +[] +let ``Anonymous lambda is inlined across multiple uses`` () = + let arr = [| 1..10 |] + let mutable sum = 0 + arr |> iterateTwice (fun x -> sum <- sum + x) + sum |> equal 110 + +[] +let ``Named module-level lambda is inlined`` () = + let arr = [| 1..10 |] + let mutable sum = 0 + let myAction = fun x -> sum <- sum + x + arr |> iterateTwice myAction + sum |> equal 110 + +[] +let ``InlineIfLambda works with fold-style accumulator`` () = + let arr = [| 1..5 |] + let sum = fold (fun acc x -> acc + x) 0 arr + sum |> equal 15 + +[] +let ``InlineIfLambda single application works`` () = + let result = applyToFirst (fun x -> x * 2) 21 + result |> equal 42 + +[] +let ``InlineIfLambda applied twice works`` () = + let result = applyTwice (fun x -> x + 3) 1 + result |> equal 7 + +[] +let ``InlineIfLambda with two parameters works`` () = + let result = + applyPairTwice + (fun a b -> + a + b + ) + 10 + 32 + result |> equal 74 + +[] +let ``InlineIfLambda captures mutable correctly`` () = + let mutable count = 0 + let arr = [| 1..5 |] + arr |> iterateTwice (fun _ -> count <- count + 1) + count |> equal 10 + +[] +let ``InlineIfLambda with early return via exception-free branching`` () = + let arr = [| 1..10 |] + let mutable evens = 0 + arr |> iterateTwice (fun x -> if x % 2 = 0 then evens <- evens + 1) + evens |> equal 10 + +[] +let ``InlineIfLambda on CE builder Delay inlines body at each call site`` () = + let mutable callCount = 0 + let result = + twice { + callCount <- callCount + 1 + return callCount + } + callCount |> equal 2 + result |> equal 2