@@ -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
0 commit comments