Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* Fix Release-only (`--optimize+`) `System.InvalidProgramException` from `Seq.collect` / `yield!` over a value-type (struct) collection implementing `seq<'T>` (e.g. `ImmutableArray<_>`) when materialised with `List.ofSeq` / `Seq.toList` / `Seq.toArray` or a list/array comprehension. The collector lowering now boxes a struct sub-collection to `seq<'T>` before calling `AddMany`/`AddManyAndClose` (matching the coercion the type checker already inserts for `yield!`), and uses `unit` as the try/finally result type instead of the body type (removing a spurious `ldnull` store). ([Issue #20203](https://github.com/dotnet/fsharp/issues/20203))
* Fix recursive inline SRTP resolution being truncated by one currying level (e.g. FSharpPlus `memoizeN`), a regression from the function-domain unification order change in [PR #15181](https://github.com/dotnet/fsharp/pull/15181); the contravariant domain now keeps the inference variable that still carries the pending member constraint. ([PR #20247](https://github.com/dotnet/fsharp/pull/20247))
* Fix exponential (2^N) compile time in pattern matching with shared guards and partial active patterns. ([Issue #18425](https://github.com/dotnet/fsharp/issues/18425), [PR #20244](https://github.com/dotnet/fsharp/pull/20244))
* Fix incorrect Debug lowering of inline builders that compose low-level resumable state machines. ([Issue #20466](https://github.com/dotnet/fsharp/issues/20466))
* Fix incorrect `StructLayout(Size = 1)` emission for data-less struct unions where the compiler-generated tag field makes the actual runtime size larger. ([PR #19759](https://github.com/dotnet/fsharp/pull/19759))
* Fix FS0750 "This construct may only be used within computation expressions" incorrectly raised for `let!`/`use!`/`do!` appearing in the right-hand side of a plain `let` binding inside a computation expression. The right-hand side is now desugared as a nested computation of the same builder whose result is bound with `let!`, keeping its bindings correctly scoped. ([Issue #19457](https://github.com/dotnet/fsharp/issues/19457), [PR #19868](https://github.com/dotnet/fsharp/pull/19868))
* Stop leaking a `System.Diagnostics.Metrics.MeterListener` per `Cache` in DEBUG builds. Each cache created a `CacheMetrics.CacheMetricsListener` (which starts a `MeterListener` registered in the process-global metrics registry) and never disposed it, so listeners accumulated for the lifetime of the process. Because every cache hit/miss/add published to all registered listeners, the per-operation cost grew linearly with the number of leaked listeners, so repeated checks (and Debug FCS test runs) slowed down over time. The per-cache `CacheMetricsListener` and the per-instance `cacheId` tag are removed; `DebugDisplay` and tests now read the existing name-aggregated stats populated by the single `ListenToAll` listener, so no per-cache listener is created and no per-operation cost is added. ([PR #19995](https://github.com/dotnet/fsharp/pull/19995))
Expand Down
94 changes: 85 additions & 9 deletions src/Compiler/Optimize/Optimizer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -499,6 +499,9 @@ type IncrementalOptimizationEnv =
/// definition-site replay finds no match; this call site does, letting the correct extension be honored
/// instead of degrading to the throwing dynamic stub. None outside the debug-specialization path.
debugInlineCallSite: range option

/// Inline resumable-code combinators while specializing an enclosing state-machine builder.
resumableCodeContext: bool
}

static member Empty =
Expand All @@ -513,7 +516,8 @@ type IncrementalOptimizationEnv =
methEnv = { pipelineCount = 0 }
referencedCcus = []
earlierImplFileSignatures = []
debugInlineCallSite = None }
debugInlineCallSite = None
resumableCodeContext = false }

override x.ToString() = "<IncrementalOptimizationEnv>"

Expand Down Expand Up @@ -2504,6 +2508,54 @@ and ExprIsFrameLocal cenv env expr =

FoldExpr folder false expr

// A Debug helper method would hide this definition tree from LowerStateMachines.
let HasResumableStateMachineBody cenv env (vref: ValRef) =
let rec hasResumableCodeArgument ty =
let ty = stripTyEqns cenv.g ty

if isFunTy cenv.g ty then
isResumableCodeTy cenv.g (domainOfFunTy cenv.g ty)
|| hasResumableCodeArgument (rangeOfFunTy cenv.g ty)
else
false

let rec containsStateMachineBody visiting expr =
let folder =
{ ExprFolder0 with
exprIntercept =
fun _recurseF noInterceptF acc expr ->
if acc then
acc
else
match expr with
| StructStateMachineExpr cenv.g _ -> true
| Expr.Val (nestedVref, _, _) when nestedVref.ShouldInline ->
hasStateMachineBody visiting nestedVref
| _ -> noInterceptF acc expr }

FoldExpr folder false expr

and hasStateMachineBody visiting (vref: ValRef) =
if List.exists ((=) vref.Stamp) visiting then
false
else
let _, ty = tryDestForallTy cenv.g vref.Type

if not (hasResumableCodeArgument ty) then
false
else
match TryGetInfoForVal cenv env vref |> Option.map (fun info -> stripValue info.ValExprInfo) with
| Some(CurriedLambdaValue (_, _, _, body, _)) ->
containsStateMachineBody (vref.Stamp :: visiting) body
| _ -> false

hasStateMachineBody [] vref

let isResumableInlineInDebug cenv env (vref: ValRef) =
cenv.optimizing &&
(HasResumableStateMachineBody cenv env vref ||
(env.resumableCodeContext && isReturnsResumableCodeTy cenv.g vref.TauType))

let shouldForceInlineInDebug cenv env (vref: ValRef) : bool =
let g = cenv.g

Expand All @@ -2512,6 +2564,8 @@ let shouldForceInlineInDebug cenv env (vref: ValRef) : bool =

(vref.HasDeclaringEntity && shouldForceInlineMembersInDebug g vref.DeclaringEntity) ||

isResumableInlineInDebug cenv env vref ||

HasFrameLocalBody cenv env vref

/// Optimize/analyze an expression
Expand Down Expand Up @@ -3657,9 +3711,12 @@ and TryDevirtualizeApplication cenv env (f, tyargs, args, m) =
/// Attempt to inline an application of a known value at callsites
and TryInlineApplication cenv env finfo (valExpr: Expr) (tyargs: TType list, args: Expr list, m) =
let g = cenv.g

match cenv.settings.alwaysInline, stripExpr valExpr with
| false, Expr.Val(vref, _, _) when vref.ShouldInline && not (shouldForceInlineInDebug cenv env vref) ->
| false, Expr.Val(vref, _, _)
when vref.ShouldInline &&
(not (shouldForceInlineInDebug cenv env vref) || isResumableInlineInDebug cenv env vref) ->
let forceInline = shouldForceInlineInDebug cenv env vref
let hasResumableStateMachineBody = HasResumableStateMachineBody cenv env vref
let hasNoTraits =
let tps, _ = tryDestForallTy g vref.Type
GetTraitConstraintInfosOfTypars g tps |> List.isEmpty
Expand All @@ -3678,17 +3735,25 @@ and TryInlineApplication cenv env finfo (valExpr: Expr) (tyargs: TType list, arg
// so route those through the specialization path which inlines the body.
let isHiddenBySignature = cenv.signatureHidingInfo.HiddenVals.Contains vref.Deref
let canCallDirectly =
not forceInline &&
(cenv.optimizing || (vref.Accessibility.IsPublic && not isHiddenBySignature)) &&
(hasNoTraits || (allTyargsAreBareTypars && vref.ValReprInfo.IsSome))

let argsR = args |> List.map (OptimizeExpr cenv env >> fst)
// Keep nested resumable combinators in the same expression tree as the builder.
let inlineEnv =
if forceInline && hasResumableStateMachineBody then
{ env with resumableCodeContext = true }
else
env

let argsR = args |> List.map (OptimizeExpr cenv inlineEnv >> fst)
let info = { TotalSize = 1; FunctionSize = 1; HasEffect = true; MightMakeCriticalTailcall = false; Info = UnknownValue }

if canCallDirectly then
Some(mkApps g ((exprForValRef m vref, vref.Type), [tyargs], argsR, m), info)
else

let origFinfo = GetInfoForVal cenv env m vref
let origFinfo = GetInfoForVal cenv inlineEnv m vref
match stripValue origFinfo.ValExprInfo with
| CurriedLambdaValue(origLambdaId, _, _, origLambda, origLambdaTy) ->
let f2R = CopyExprForInlining cenv true origLambda m
Expand All @@ -3709,7 +3774,7 @@ and TryInlineApplication cenv env finfo (valExpr: Expr) (tyargs: TType list, arg
// function (e.g. sum<int, int->int> can call sum<int, int> in its body). For
// non-concrete type args, never specialize recursively.
let canSpecialize =
match Map.tryFind origLambdaId env.dontInline with
match Map.tryFind origLambdaId inlineEnv.dontInline with
| Some tys ->
allTyargsAreConcrete &&
not tys.IsEmpty &&
Expand All @@ -3725,15 +3790,26 @@ and TryInlineApplication cenv env finfo (valExpr: Expr) (tyargs: TType list, arg
| Some (_, body) -> copyExpr g CloneAll body
| None ->

let existingTypes = defaultArg (Map.tryFind origLambdaId env.dontInline) []
let env = { env with dontInline = Map.add origLambdaId (specLambdaTy :: existingTypes) env.dontInline; debugInlineCallSite = Some m }
let existingTypes = defaultArg (Map.tryFind origLambdaId inlineEnv.dontInline) []
let currentDontInline = inlineEnv.dontInline
let env = { inlineEnv with dontInline = Map.add origLambdaId (specLambdaTy :: existingTypes) currentDontInline; debugInlineCallSite = Some m }
let specLambdaR, _ = OptimizeExpr cenv env specLambda
cenv.specializedInlineVals.Add(origLambdaId, (specLambdaTy, specLambdaR))
specLambdaR
else
let specLambdaR, _ = OptimizeExpr cenv { env with dontInline = Map.add origLambdaId [] env.dontInline; debugInlineCallSite = Some m } specLambda
let currentDontInline = inlineEnv.dontInline
let specLambdaR, _ = OptimizeExpr cenv { inlineEnv with dontInline = Map.add origLambdaId [] currentDontInline; debugInlineCallSite = Some m } specLambda
specLambdaR

let fullyInlineResumable =
forceInline && isResumableInlineInDebug cenv inlineEnv vref

// A helper method boundary would hide the resumable definitions from lowering.
if fullyInlineResumable then
let reducedExpr = MakeApplicationAndBetaReduce g (specLambdaR, specLambdaTy, [], argsR, m)
Some(OptimizeExpr cenv inlineEnv reducedExpr)
else

// Abstract the specialized lambda over its free typars so IlxGen emits a static
// method with flattened arguments. The alternative closure form (valReprInfo = None)
// wraps args in a reference Tuple<>, which cannot hold byrefs and fails to load at
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1759,6 +1759,39 @@ let main _ =
|> compileAndRun
|> verifySequencePoints

[<Fact>]
let ``Resumable 04 - Builder Run is inlined`` () =
FSharp """
open Microsoft.FSharp.Core.CompilerServices
open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers

#nowarn "3501"
#nowarn "3513"

type Builder() =
member inline _.Run(code: ResumableCode<unit, int>) =
if __useResumableCode then
__stateMachine<unit, int>
(MoveNextMethodImpl<_>(fun sm -> code.Invoke(&sm) |> ignore))
(SetStateMachineMethodImpl<_>(fun _ _ -> ()))
(AfterCode<_, _>(fun _ -> 42))
else
0

let builder = Builder()

[<EntryPoint>]
let main _ =
let code = ResumableCode<unit, int>(fun _ -> true)
let result = builder.Run code
if result = 42 then 0 else 1
"""
|> withDebug
|> withNoOptimize
|> asExe
|> compileAndRun
|> verifySequencePoints

[<Fact>]
let ``InlineIfLambda 01 - Debug`` () =
FSharp """
Expand Down Expand Up @@ -1896,4 +1929,3 @@ let main _ =
|> asExe
|> compileAndRun
|> verifySequencePoints

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
open Microsoft.FSharp.Core.CompilerServices
open Microsoft.FSharp.Core.CompilerServices.StateMachineHelpers

#nowarn "3501"
#nowarn "3513"

type Builder() =
member inline _.Run(code: ResumableCode<unit, int>) =
if __useResumableCode then
__stateMachine<unit, int>
(MoveNextMethodImpl<_>(fun sm -> code.Invoke(&sm) |> ignore))
(SetStateMachineMethodImpl<_>(fun _ _ -> ()))
(AfterCode<_, _>(fun _ -> 42))
else
0

let builder = Builder()

[<EntryPoint>]
let main _ =
let code = ResumableCode<unit, int>(fun _ -> true)
let result = builder.Run code
if result = 42 then 0 else 1
--------------------------------------------------------------------------------

Test::main
(22,5-22,55) let code = ResumableCode<unit, int>(fun _ -> true)
IL_0000: ldc.i4.0
IL_0001: stsfld $Test::init@
IL_0006: ldsfld $Test::init@
IL_000b: pop
IL_000c: ldnull
IL_000d: ldftn code@22::Invoke
IL_0013: newobj .ctor
IL_0018: stloc.0

(23,5-23,34) let result = builder.Run code
IL_0019: ldloca.s 2
IL_001b: initobj result@23
IL_0021: ldloca.s 2
IL_0023: stloc.3
IL_0024: ldc.i4.s 42
IL_0026: stloc.1

(24,5-24,24) if result = 42 then
IL_0027: ldloc.1
IL_0028: ldc.i4.s 42
IL_002a: bne.un.s IL_002e

(24,25-24,26) 0
IL_002c: ldc.i4.0
IL_002d: ret

(24,32-24,33) 1
IL_002e: ldc.i4.1
IL_002f: ret

Test::.cctor
<hidden>
IL_0000: ldc.i4.0
IL_0001: stsfld $Test::init@
IL_0006: ldsfld $Test::init@
IL_000b: pop
IL_000c: ret

Test::staticInitialization@
(18,1-18,24) let builder = Builder()
IL_0000: newobj Builder::.ctor
IL_0005: stsfld Test::builder@18
IL_000a: ret

Builder::.ctor
(8,6-8,13) Builder
IL_0000: ldarg.0
IL_0001: callvirt Object::.ctor
IL_0006: ldarg.0
IL_0007: pop
IL_0008: ret

Builder::Run
(16,13-16,14) 0
IL_0000: ldc.i4.0
IL_0001: ret

code@22::Invoke
(22,50-22,54) true
IL_0000: ldc.i4.1
IL_0001: ret

result@23::MoveNext
<hidden>
IL_0000: ldarg.0
IL_0001: stloc.1

(22,50-22,54) true
IL_0002: ldc.i4.1
IL_0003: stloc.0
IL_0004: ldloc.0
IL_0005: stloc.2
IL_0006: ret
Loading