Skip to content

Commit 422abf1

Browse files
committed
[All] Add support for [<InlineIfLambda>] (#4401)
1 parent b1ec1bb commit 422abf1

29 files changed

Lines changed: 787 additions & 6 deletions

src/Fable.AST/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Added
11+
12+
* Add `IsInlineIfLambda` to `Fable.AST.Fable.Ident` (by @MangelMaxime)
13+
1014
## 5.0.0-rc.2 - 2026-03-10
1115

1216
### Added

src/Fable.AST/Fable.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ type Ident =
479479
IsThisArgument: bool
480480
IsCompilerGenerated: bool
481481
Range: SourceLocation option
482+
IsInlineIfLambda: bool
482483
}
483484

484485
member x.DisplayName =

src/Fable.Cli/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2929
* [Beam] Fix non-ASCII characters in string literals being truncated to single bytes — emit `<<"..."/utf8>>` instead of `<<"...">>` (by @dbrattli)
3030
* [Beam] Fix `Emit` expressions with `case` leaking variables into surrounding scope — auto-wrap in IIFE for scope isolation (by @dbrattli)
3131

32+
### Added
33+
34+
[All] Add support for `[<InlineIfLambda>]` (by @MangelMaxime)
35+
3236
## 5.0.0-rc.3 - 2026-03-10
3337

3438
### Added

src/Fable.Compiler/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2929
* [Beam] Fix non-ASCII characters in string literals being truncated to single bytes — emit `<<"..."/utf8>>` instead of `<<"...">>` (by @dbrattli)
3030
* [Beam] Fix `Emit` expressions with `case` leaking variables into surrounding scope — auto-wrap in IIFE for scope isolation (by @dbrattli)
3131

32+
### Added
33+
34+
[All] Add support for `[<InlineIfLambda>]` (by @MangelMaxime)
35+
3236
## 5.0.0-rc.10 - 2026-03-10
3337

3438
### Added

src/Fable.Transforms/FSharp2Fable.Util.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1827,6 +1827,9 @@ module Identifiers =
18271827
IsCompilerGenerated = fsRef.IsCompilerGenerated
18281828
IsMutable = isMutable
18291829
Range = Some r
1830+
IsInlineIfLambda =
1831+
fsRef.Attributes
1832+
|> Seq.exists (fun attr -> attr.AttributeType.FullName = Atts.inlineIfLambda)
18301833
}
18311834

18321835
let putIdentInScope com ctx (fsRef: FSharpMemberOrFunctionOrValue) value : Context * Fable.Ident =

src/Fable.Transforms/FSharp2Fable.fs

Lines changed: 135 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2615,9 +2615,9 @@ type FableCompiler(com: Compiler) =
26152615
ResolvedIdents = Dictionary()
26162616
}
26172617

2618-
let ctx, bindings =
2619-
((ctx, []), foldArgs [] (inExpr.Args, args))
2620-
||> List.fold (fun (ctx, bindings) (argId, arg) ->
2618+
let ctx, bindings, forceInlineMap =
2619+
(((ctx, [], Map.empty)), foldArgs [] (inExpr.Args, args))
2620+
||> List.fold (fun (ctx, bindings, forceInlineMap) (argId, arg) ->
26212621
let argId = resolveInlineIdent ctx info argId
26222622
// Change type and mark argId as compiler-generated so Fable also
26232623
// tries to inline it in DEBUG mode (some patterns depend on this)
@@ -2627,9 +2627,101 @@ type FableCompiler(com: Compiler) =
26272627
IsCompilerGenerated = true
26282628
}
26292629

2630+
// If this parameter has [<InlineIfLambda>] and the argument is a lambda/delegate,
2631+
// force-inline it directly into the body (duplicate at each call site) rather
2632+
// than creating a Let binding that beta-reduction may refuse to inline.
2633+
let isInlineIfLambda = argId.IsInlineIfLambda
2634+
2635+
// Helper: try to find a non-inline module-level declaration by its Fable name and
2636+
// reconstruct it as a Lambda/Delegate by binding its FSC args and transforming its body.
2637+
let tryLambdaFromDeclarations (name: string) =
2638+
let decls = com.GetImplementationFile(com.CurrentFile)
2639+
2640+
let rec tryFindInDecls (decls: FSharpImplementationFileDeclaration list) =
2641+
decls
2642+
|> List.tryPick (fun decl ->
2643+
match decl with
2644+
| FSharpImplementationFileDeclaration.Entity(_, subDecls) -> tryFindInDecls subDecls
2645+
| FSharpImplementationFileDeclaration.MemberOrFunctionOrValue(memb, membArgIds, fsBody) when
2646+
not (isInline memb)
2647+
->
2648+
let declName, _ = getMemberDeclarationName (this :> Compiler) memb
2649+
2650+
if declName = name then
2651+
let flatArgs = List.concat membArgIds
2652+
2653+
match flatArgs with
2654+
| [] ->
2655+
// No args: transform body directly (may already be Lambda/Delegate)
2656+
let fableBody = (this :> IFableCompiler).Transform(ctx, fsBody)
2657+
2658+
match fableBody with
2659+
| Fable.Lambda _
2660+
| Fable.Delegate _ -> Some fableBody
2661+
| _ -> None
2662+
| _ ->
2663+
// Has args: bind them and wrap transformed body in a lambda
2664+
let bodyCtx, fableArgs = bindMemberArgs this ctx membArgIds
2665+
let fableBody = (this :> IFableCompiler).Transform(bodyCtx, fsBody)
2666+
2667+
match fableArgs with
2668+
| [ singleArg ] -> Some(Fable.Lambda(singleArg, fableBody, None))
2669+
| multiArgs ->
2670+
Some(Fable.Delegate(multiArgs, fableBody, None, Fable.Tags.empty))
2671+
else
2672+
None
2673+
| _ -> None
2674+
)
2675+
2676+
tryFindInDecls decls
2677+
2678+
let effectiveArg =
2679+
match arg with
2680+
| Fable.IdentExpr identRef ->
2681+
// First try local scope (covers anonymous lambda pre-bound by FSC, Case 1)
2682+
let fromScope =
2683+
ctx.Scope
2684+
|> List.tryPick (fun (_, scopeIdent, scopeValue) ->
2685+
if scopeIdent.Name = identRef.Name then
2686+
scopeValue
2687+
else
2688+
None
2689+
)
2690+
2691+
match fromScope with
2692+
| Some _ -> fromScope |> Option.defaultValue arg
2693+
| None ->
2694+
// Second, try module-level declarations (IdentExpr case when scope lookup fails)
2695+
if isInlineIfLambda then
2696+
tryLambdaFromDeclarations identRef.Name |> Option.defaultValue arg
2697+
else
2698+
arg
2699+
| Fable.Lambda(lambdaArg, Fable.Call(Fable.IdentExpr calleeIdent, callInfo, _, _), _) when
2700+
isInlineIfLambda
2701+
&& callInfo.Args.Length = 1
2702+
&& (
2703+
match callInfo.Args.[0] with
2704+
| Fable.IdentExpr a -> a.Name = lambdaArg.Name
2705+
| _ -> false
2706+
)
2707+
->
2708+
// FSC may eta-expand a named function: `myAction` → `fun x -> myAction(x)`.
2709+
// Resolve the callee's body from declarations so we inline the real body.
2710+
tryLambdaFromDeclarations calleeIdent.Name |> Option.defaultValue arg
2711+
| _ -> arg
2712+
26302713
let ctx = { ctx with Scope = (None, argId, Some arg) :: ctx.Scope }
26312714

2632-
ctx, (argId, arg) :: bindings
2715+
let isLambdaOrDelegate =
2716+
match effectiveArg with
2717+
| Fable.Lambda _
2718+
| Fable.Delegate _ -> true
2719+
| _ -> false
2720+
2721+
if isInlineIfLambda && isLambdaOrDelegate then
2722+
(ctx, bindings, Map.add argId.Name effectiveArg forceInlineMap)
2723+
else
2724+
(ctx, (argId, arg) :: bindings, forceInlineMap)
26332725
)
26342726

26352727
let ctx =
@@ -2643,6 +2735,21 @@ type FableCompiler(com: Compiler) =
26432735

26442736
let resolved = resolveInlineExpr this ctx info inExpr.Body
26452737

2738+
// Substitute [<InlineIfLambda>] lambdas directly into the body at each use site
2739+
let resolved =
2740+
if Map.isEmpty forceInlineMap then
2741+
resolved
2742+
else
2743+
resolved
2744+
|> visitFromInsideOut (
2745+
function
2746+
| Fable.IdentExpr id as e ->
2747+
match Map.tryFind id.Name forceInlineMap with
2748+
| Some replacement -> replacement
2749+
| None -> e
2750+
| e -> e
2751+
)
2752+
26462753
// Some patterns depend on inlined arguments being captured by "magic" Fable.Core functions like
26472754
// importValueDynamic. If the value can have side effects, it won't be removed by beta binding
26482755
// reduction, so we try to eliminate it here.
@@ -2747,11 +2854,34 @@ let getInlineExprs fileName (declarations: FSharpImplementationFileDeclaration l
27472854
ctx, ident :: idents
27482855
)
27492856

2857+
// CurriedParameterGroups is the authoritative source for parameter-level
2858+
// attributes like [<InlineIfLambda>]. The argIds (FSharpMemberOrFunctionOrValue)
2859+
// are body-level variables and may not carry parameter attributes on fsRef.Attributes.
2860+
// So we zip the built idents with the flattened CurriedParameterGroups and annotate.
2861+
// NOTE: for instance members the first ident is the `this` argument, which is NOT
2862+
// included in CurriedParameterGroups — skip it when indexing into flatParams.
2863+
let flatParams = memb.CurriedParameterGroups |> Seq.collect id |> Seq.toList
2864+
2865+
let thisOffset =
2866+
if memb.IsInstanceMember then
2867+
1
2868+
else
2869+
0
2870+
2871+
let args =
2872+
List.rev idents
2873+
|> List.mapi (fun i ident ->
2874+
match List.tryItem (i - thisOffset) flatParams with
2875+
| Some param when i >= thisOffset && hasAttrib Atts.inlineIfLambda param.Attributes ->
2876+
{ ident with IsInlineIfLambda = true }
2877+
| _ -> ident
2878+
)
2879+
27502880
// It looks as we don't need memb.DeclaringEntity.GenericParameters here
27512881
let genArgs = memb.GenericParameters |> Seq.mapToList (genParamName)
27522882

27532883
{
2754-
Args = List.rev idents
2884+
Args = args
27552885
Body = com.Transform(ctx, body)
27562886
FileName = fileName
27572887
GenericArgs = genArgs

src/Fable.Transforms/Python/Fable2Python.Transforms.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,7 @@ let transformObjectExpr
653653
IsThisArgument = false
654654
IsCompilerGenerated = true
655655
Range = r
656+
IsInlineIfLambda = false
656657
}
657658
| e -> e
658659
)

src/Fable.Transforms/Transforms.Util.fs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ module Atts =
3737
[<Literal>]
3838
let entryPoint = "Microsoft.FSharp.Core.EntryPointAttribute" // typeof<Microsoft.FSharp.Core.EntryPointAttribute>.FullName
3939

40+
[<Literal>]
41+
let inlineIfLambda = "Microsoft.FSharp.Core.InlineIfLambdaAttribute" // typeof<Microsoft.FSharp.Core.InlineIfLambdaAttribute>.FullName
42+
4043
[<Literal>]
4144
let sealed_ = "Microsoft.FSharp.Core.SealedAttribute" // typeof<Microsoft.FSharp.Core.SealedAttribute>.FullName
4245

@@ -968,6 +971,7 @@ module AST =
968971
IsThisArgument = false
969972
IsMutable = false
970973
Range = None
974+
IsInlineIfLambda = false
971975
}
972976

973977
/// ATTENTION: Make sure the ident name is unique

tests/Dart/main.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import './src/DictionaryTests.dart' as dictionary;
77
import './src/EnumTests.dart' as enum_;
88
import './src/EnumerableTests.dart' as enumerable;
99
import './src/HashSetTests.dart' as hash_set;
10+
import './src/InlineIfLambdaTests.dart' as inline_if_lambda;
1011
import './src/ListTests.dart' as list;
1112
import './src/MapTests.dart' as map;
1213
import './src/MiscTests.dart' as misc;
@@ -37,6 +38,7 @@ void main() {
3738
enum_.tests();
3839
enumerable.tests();
3940
hash_set.tests();
41+
inline_if_lambda.tests();
4042
list.tests();
4143
map.tests();
4244
misc.tests();
@@ -56,4 +58,4 @@ void main() {
5658
tuple.tests();
5759
type_.tests();
5860
union.tests();
59-
}
61+
}

tests/Dart/src/Fable.Tests.Dart.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<Compile Include="TupleTests.fs" />
3737
<Compile Include="TypeTests.fs" />
3838
<Compile Include="UnionTests.fs" />
39+
<Compile Include="InlineIfLambdaTests.fs" />
3940
<Content Include="..\main.dart" />
4041
</ItemGroup>
4142
</Project>

0 commit comments

Comments
 (0)