Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
6c25d66
Add regression repro for parallel cross-assembly inline overloads
majocha Aug 1, 2026
54b5fbe
Add regression test for parallel cross-assembly inline overloads
majocha Aug 1, 2026
3f5f34c
tighten the regression fixture
majocha Aug 1, 2026
fd22df9
add sequential repro
majocha Aug 1, 2026
f7e7e9e
Generalize recursive inline member ordering fix
majocha Aug 1, 2026
a942862
Fix quoted args in fsc subprocess tests
majocha Aug 1, 2026
d4dc94b
Update emitted IL baselines
majocha Aug 1, 2026
78b0011
add comments
majocha Aug 2, 2026
0d8eceb
Revert "Fix quoted args in fsc subprocess tests"
majocha Aug 2, 2026
66fee1d
Simplify recursive inline member regression tests
majocha Aug 2, 2026
1a09b70
simplify tests
majocha Aug 2, 2026
79ce76a
Add recursive inline member regression coverage
majocha Aug 2, 2026
d1b02eb
Handle trait-witness inline regression
majocha Aug 2, 2026
492a850
Fix optimizer ordering regressions
majocha Aug 2, 2026
7a1f5da
Fix inline member optimization ordering
majocha Aug 2, 2026
7eed09a
Restore recursive optimizer dependency ordering
majocha Aug 2, 2026
5d57c2e
Update AOT trimming size baselines
majocha Aug 2, 2026
23213a0
Clarify recursive inline release note
majocha Aug 3, 2026
f1c0776
Merge branch 'main' into fix-20085
majocha Aug 3, 2026
00f9f28
Merge branch 'main' into fix-20085
majocha Aug 4, 2026
7bfd44a
cross assembly repro
majocha Aug 4, 2026
7cddf24
Revert "cross assembly repro"
majocha Aug 4, 2026
65a188e
Remove unrelated cross-assembly regression
majocha Aug 4, 2026
1673a1f
Merge branch 'main' into fix-20085
majocha Aug 4, 2026
0f4d469
Merge branch 'main' into fix-20085
majocha Aug 5, 2026
f41fc0e
Merge branch 'main' into fix-20085
majocha Aug 5, 2026
e941879
Merge branch 'main' into fix-20085
majocha Aug 6, 2026
279591e
merge main
majocha Aug 20, 2026
c446f77
Fix recursive inline dependency ordering
majocha Aug 20, 2026
df3c2a6
Merge remote-tracking branch 'dotnet/main' into majocha-fix-review-fi…
majocha Aug 20, 2026
8091163
Use stack guard for dependency traversal
majocha Aug 20, 2026
317c267
Update AOT trimming size baseline
majocha Aug 20, 2026
8920977
Merge branch 'main' into fix-20085
majocha Aug 20, 2026
d6e7cb7
Merge branch 'main' into fix-20085
majocha Aug 24, 2026
eba8624
Merge branch 'main' into fix-20085
majocha Aug 24, 2026
6bad483
Merge branch 'main' into fix-20085
majocha Aug 24, 2026
0cf8b73
Merge branch 'main' into fix-20085
majocha Aug 26, 2026
b82a38b
Address review: array scatter for source order, extract TopologicalPo…
majocha Aug 26, 2026
69200cc
Factor reorder into OptimizeInDependencyOrder, document optimizing=fa…
majocha Aug 26, 2026
a667944
Merge branch 'main' into fix-20085
majocha Aug 27, 2026
1c260f8
Add failing regression tests: nested module disables rec-group inline…
majocha Aug 27, 2026
6ef18e3
Extend rec-group inline reorder to groups containing nested modules
majocha Aug 27, 2026
7d9255b
add another test
majocha Aug 28, 2026
3930218
Merge branch 'main' into fix-20085
majocha Aug 28, 2026
13375d7
refactor
majocha Aug 28, 2026
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
4 changes: 4 additions & 0 deletions docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@
* Fix signature conformance: overloaded member with unit parameter `M(())` now matches sig `member M: unit -> unit`. ([Issue #19596](https://github.com/dotnet/fsharp/issues/19596), [PR #19615](https://github.com/dotnet/fsharp/pull/19615))
* Fix `--quiet` not suppressing NuGet restore output on stdout in F# Interactive ([Issue #18086](https://github.com/dotnet/fsharp/issues/18086))
* Reference assembly MVIDs are now deterministic across compiler invocations. Previously, `--refout` / `<ProduceReferenceAssembly>true</ProduceReferenceAssembly>` produced a different MVID every build because the implied signature hash used .NET's randomized `String.GetHashCode()`. ([Issue #19751](https://github.com/dotnet/fsharp/issues/19751), [PR #19801](https://github.com/dotnet/fsharp/pull/19801))
* Parser: recover on unfinished if and binary expressions
([PR #19724](https://github.com/dotnet/fsharp/pull/19724))
* Fix recursive inline-member optimization dependencies so inline consumers in recursive groups are resolved reliably without changing static initialization order. ([Issue #20085](https://github.com/dotnet/fsharp/issues/20085), [PR #20111](https://github.com/dotnet/fsharp/pull/20111))
* Parser: recover on unfinished if and binary expressions ([PR #19724](https://github.com/dotnet/fsharp/pull/19724))
* Parser: recover on missing 'when' conditions ([PR ##20071](https://github.com/dotnet/fsharp/pull/#20071))
* Fix `SynExpr.shouldBeParenthesizedInContext` to report parentheses as required around `SynExpr.Sequential` expressions used as record or anonymous-record field values, so the IDE "remove unnecessary parentheses" analyzer no longer breaks code like `{| A = ((); B = 3) |}`. ([Issue #17826](https://github.com/dotnet/fsharp/issues/17826), [PR #19850](https://github.com/dotnet/fsharp/pull/19850))
Expand All @@ -145,6 +148,7 @@
* Import: Don't walk non-F# assemblies when labelling trait constraint sources (PR [#20090](https://github.com/dotnet/fsharp/pull/20090))
* Avoid per-instance lock object in InterruptibleLazy and DelayInitArrayMap (PR [#20088](https://github.com/dotnet/fsharp/pull/20088))
* IL: fix leaking binary view ([PR #20250](https://github.com/dotnet/fsharp/pull/20250))
* Fix dependency ordering and stack safety for recursive inline bindings. ([PR #20111](https://github.com/dotnet/fsharp/pull/20111))

### Added

Expand Down
197 changes: 189 additions & 8 deletions src/Compiler/Optimize/Optimizer.fs
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,10 @@ let GetInfoForLocalValue cenv env (v: Val) m =
match TryGetInfoForLocalValue cenv env v with
| Some vval -> vval
| None ->
if not v.IsDispatchSlot && v.ShouldInline then
// Inside an inline body being prepared for export (not optimizing), a referenced inline val may
// legitimately be absent from the optimization environment here: it is exported as-is and
// resolved when the body gets inlined at the caller site. Only diagnose when optimizing for real.
if cenv.optimizing && not v.IsDispatchSlot && v.ShouldInline then
errorR(Error(FSComp.SR.optValueMarkedInlineButWasNotBoundInTheOptEnv(richTextOfQualifiedValRef (mkLocalValRef v)), m))
UnknownValInfo

Expand Down Expand Up @@ -3316,11 +3319,13 @@ and TryOptimizeVal cenv env (vOpt: ValRef option, shouldInline, inlineIfLambda,
| TupleValue _ | UnionCaseValue _ | RecdValue _ when shouldInline ->
failwith "tuple, union and record values cannot be marked 'inline'"

| UnknownValue when shouldInline && cenv.settings.alwaysInline ->
// No diagnostics when preparing an inline body for export: the reference is exported
// as-is and resolved at the expansion site, where the caller's environment is complete.
| UnknownValue when shouldInline && cenv.settings.alwaysInline && cenv.optimizing ->
warning(Error(FSComp.SR.optValueMarkedInlineHasUnexpectedValue(), m))
None

| _ when shouldInline && cenv.settings.alwaysInline ->
| _ when shouldInline && cenv.settings.alwaysInline && cenv.optimizing ->
warning(Error(FSComp.SR.optValueMarkedInlineCouldNotBeInlined(), m))
None

Expand Down Expand Up @@ -3366,7 +3371,9 @@ and OptimizeVal cenv env expr (v: ValRef, m) =
e, AddValEqualityInfo g m v einfo

| None ->
if cenv.settings.alwaysInline then
// As in GetInfoForLocalValue: a failed inline is only an error when optimizing for this
// site, not when the body is merely being prepared for export.
if cenv.optimizing && cenv.settings.alwaysInline then
if v.ShouldInline then
match valInfoForVal.ValExprInfo with
| UnknownValue -> error(Error(FSComp.SR.optFailedToInlineValue(richTextOfValName g v.Deref), m))
Expand Down Expand Up @@ -4531,6 +4538,8 @@ and OptimizeBinding cenv isRec env (TBind(vref, expr, spBind)) =

let exprOptimized, einfo =
let env = if vref.IsCompilerGenerated && Option.isSome env.latestBoundId then env else {env with latestBoundId=Some vref.Id}
// Bodies of inline bindings are prepared for export (optimized later, at the expansion
// site), which is why diagnostics depending on a complete environment are suppressed.
let cenv = if vref.InlineInfo.ShouldInline then { cenv with optimizing=false} else cenv
let arityInfo = InferValReprInfoOfBinding g AllowTypeDirectedDetupling.No vref expr
let exprOptimized, einfo = OptimizeLambdas (Some vref) cenv env arityInfo expr vref.Type
Expand Down Expand Up @@ -4640,8 +4649,29 @@ and OptimizeBinding cenv isRec env (TBind(vref, expr, spBind)) =
errorRecovery exn vref.Range
raise (ReportedError (Some exn))

/// Optimize a group in the given dependency order, then restore source order.
and OptimizeInDependencyOrder xs order processOne state =
let xsArray = List.toArray xs
let results, state =
(state, order)
||> List.mapFold (fun state idx ->
let result, state = processOne state xsArray[idx]
(idx, result), state)

// The order is a permutation of the indexes, so scatter the results back to source order.
let resultsByIndex = Array.zeroCreate xs.Length

for idx, result in results do
resultsByIndex[idx] <- result

List.ofArray resultsByIndex, state

and OptimizeBindings cenv isRec env xs =
List.mapFold (OptimizeBinding cenv isRec) env xs
if isRec && xs |> List.exists (fun (TBind(vref, _, _)) -> vref.ShouldInline) then
let order = GetBindingOptimizationOrder cenv false true xs
OptimizeInDependencyOrder xs order (OptimizeBinding cenv isRec) env
else
List.mapFold (OptimizeBinding cenv isRec) env xs

and OptimizeModuleExprWithSig cenv env mty def =
let g = cenv.g
Expand Down Expand Up @@ -4731,11 +4761,122 @@ and OptimizeModuleExprWithSig cenv env mty def =
and mkValBind (bind: Binding) info =
(mkLocalValRef bind.Var, info)

/// Cycle-tolerant depth-first post-order over dependency indexes: each node appears exactly
/// once, after all of its dependencies. Nodes are marked on entry, so cyclic back-edges
/// into nodes still being processed are skipped.
and TopologicalPostOrder guard (dependencies: int array array) (roots: int list) =
let ordered = ResizeArray()
let visited = HashSet<int>()

let rec visit idx =
if visited.Add idx then
guard (fun () ->
for depIdx in dependencies[idx] do
if depIdx <> idx then
visit depIdx)

ordered.Add idx

for root in roots do
visit root

List.ofSeq ordered

and GetBindingOptimizationOrder cenv inlineDependenciesOnly preferLowArity (binds: Binding list) =
GetGroupOptimizationOrder cenv inlineDependenciesOnly preferLowArity [
for bind in binds ->
let arity =
bind.Var.ValReprInfo
|> Option.map (fun repr -> repr.TotalArgCount)
|> Option.defaultValue 0

[ bind.Var ], arity, Choice1Of2 bind.Expr
]

/// Compute a dependency-first processing schedule for the elements of a recursive group: each
/// element publishes its vals to the optimization environment only once processed, so a caller
/// optimized before an element it depends on can observe an incomplete optimization environment.
/// Elements are (vals defined, arity, dependency source): a binding defines its own val,
/// a nested module defines every val in its contents.
and GetGroupOptimizationOrder
cenv
inlineDependenciesOnly
preferLowArity
(elements: (Val list * int * Choice<Expr, ModuleOrNamespaceContents>) list)
=
let elemsArray = elements |> List.toArray

let elemIndexByStamp =
elements
|> List.indexed
|> List.collect (fun (idx, (definedVals, _, _)) ->
definedVals |> List.map (fun (v: Val) -> v.Stamp, idx))
|> Map.ofList

let addDependency depIdxs (v: Val) =
match elemIndexByStamp |> Map.tryFind v.Stamp with
| Some depIdx when not inlineDependenciesOnly || v.ShouldInline ->
Set.add depIdx depIdxs
| _ -> depIdxs

let addFreeVars depIdxs (fvs: FreeVars) =
let depIdxs =
(depIdxs, fvs.FreeLocals |> Zset.elements)
||> Seq.fold (fun depIdxs v -> addDependency depIdxs v)

(depIdxs, fvs.FreeTyvars.FreeTraitSolutions |> Zset.elements)
||> Seq.fold (fun depIdxs v -> addDependency depIdxs v)

let rec addBindingDependencies depIdxs expr =
cenv.stackGuard.Guard(fun () ->
let depIdxs =
addFreeVars depIdxs (freeInExpr (CollectLocalsWithStackGuard()) expr)

let folder =
{ ExprFolder0 with
exprIntercept =
(fun _exprF noInterceptF depIdxs expr ->
let depIdxs =
match expr with
// Member-constraint calls can hide the real sibling dependency behind
// a witness expression, so fold over the resolved witness as well.
| Expr.Op(TOp.TraitCall traitInfo, _, args, m) ->
match ConstraintSolver.CodegenWitnessExprForTraitConstraint cenv.TcVal cenv.g cenv.amap m traitInfo args with
Comment thread
majocha marked this conversation as resolved.
| OkResult (_, Some witnessExpr) -> addBindingDependencies depIdxs witnessExpr
| _ -> depIdxs
| _ -> depIdxs

noInterceptF depIdxs expr) }

FoldExpr folder depIdxs expr)

let dependencyIndexes =
elements
|> List.map (fun (_, _, source) ->
(match source with
| Choice1Of2 expr -> addBindingDependencies Set.empty expr
// Note: trait-call witnesses inside nested module contents are not resolved here.
| Choice2Of2 mdef ->
addFreeVars Set.empty (freeInModuleOrNamespace (CollectLocalsWithStackGuard()) mdef))
|> Set.toArray)
|> List.toArray

let rootOrder =
[ 0 .. elements.Length - 1 ]
|> (if preferLowArity then
List.sortBy (fun idx ->
let _, arity, _ = elemsArray[idx]
arity, -idx)
else
id)

TopologicalPostOrder cenv.stackGuard.Guard dependencyIndexes rootOrder

and OptimizeModuleContents cenv (env, bindInfosColl) input =
match input with
| TMDefRec(isRec, opens, tycons, mbinds, m) ->
let env = if isRec then BindInternalValsToUnknown cenv (allValsOfModDef input) env else env
let mbindInfos, (env, bindInfosColl) = OptimizeModuleBindings cenv (env, bindInfosColl) mbinds
let mbindInfos, (env, bindInfosColl) = OptimizeModuleBindings cenv isRec (env, bindInfosColl) mbinds
let mbinds, minfos = List.unzip mbindInfos
let binds = minfos |> List.choose (function Choice1Of2 (x, _) -> Some x | _ -> None)
let binfos = minfos |> List.choose (function Choice1Of2 (_, x) -> Some x | _ -> None)
Expand Down Expand Up @@ -4765,8 +4906,48 @@ and OptimizeModuleContents cenv (env, bindInfosColl) input =
let (defs, info), (env, bindInfosColl) = OptimizeModuleDefs cenv (env, bindInfosColl) defs
(TMDefs defs, info), (env, bindInfosColl)

and OptimizeModuleBindings cenv (env, bindInfosColl) xs =
List.mapFold (OptimizeModuleBinding cenv) (env, bindInfosColl) xs
and OptimizeModuleBindings cenv isRec (env, bindInfosColl) xs =
let (|DependencyOrder|_|) =
function
| [] | [ _ ] -> None
| xs when isRec ->
let elements =
xs
|> List.map (function
| ModuleOrNamespaceBinding.Binding bind ->
let arity =
bind.Var.ValReprInfo
|> Option.map (fun repr -> repr.TotalArgCount)
|> Option.defaultValue 0

[ bind.Var ], arity, Choice1Of2 bind.Expr
| ModuleOrNamespaceBinding.Module(_, def) ->
List.ofSeq (allValsOfModDef def), 0, Choice2Of2 def)

let hasInlineVal =
elements
|> List.exists (fun (definedVals, _, _) ->
definedVals |> List.exists (fun (v: Val) -> v.ShouldInline))

if hasInlineVal then
let preferLowArity =
xs
|> List.forall (function
| ModuleOrNamespaceBinding.Binding bind -> bind.Var.IsMember
| ModuleOrNamespaceBinding.Module _ -> true)

let order = GetGroupOptimizationOrder cenv true preferLowArity elements
Some order
else
None
| _ -> None

match xs with
| DependencyOrder order ->
// Keep the emitted binding list in source order; only the optimization schedule changes.
OptimizeInDependencyOrder xs order (OptimizeModuleBinding cenv) (env, bindInfosColl)
| _ ->
List.mapFold (OptimizeModuleBinding cenv) (env, bindInfosColl) xs

and OptimizeModuleBinding cenv (env, bindInfosColl) x =
match x with
Expand Down
Loading
Loading