Skip to content
Open
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
@@ -1,5 +1,6 @@
### Fixed

* Improve determinism under parallel optimization: `Optimize/DetupleArgs` and `Optimize/InnerLambdasToTopLevelFuncs` now walk their `Val` sets in stable source-position order before calling into `NiceNameGenerator`, so compiler-generated names no longer vary across builds due to `Val.Stamp` assignment races during parallel type-check. ([Issue #19732](https://github.com/dotnet/fsharp/issues/19732), [PR #19810](https://github.com/dotnet/fsharp/pull/19810))
* Honor `--nowarn` and `--warnaserror` for warnings emitted during command-line option parsing ([Issue #19576](https://github.com/dotnet/fsharp/issues/19576), [PR #19776](https://github.com/dotnet/fsharp/pull/19776))
* Fix `[<return: X>]` prefix attributes being silently dropped on class members, and fix false-positive `AllowMultiple=false` errors when `[<X>]` and `[<return: X>]` are applied to the same binding. ([Issue #17904](https://github.com/dotnet/fsharp/issues/17904), [Issue #19020](https://github.com/dotnet/fsharp/issues/19020), [PR #19738](https://github.com/dotnet/fsharp/pull/19738))
* Fix attributes on return type of unparenthesized tuple methods being silently dropped from IL. ([Issue #462](https://github.com/dotnet/fsharp/issues/462), [PR #19714](https://github.com/dotnet/fsharp/pull/19714))
Expand Down
6 changes: 5 additions & 1 deletion src/Compiler/Driver/OptimizeInputs.fs
Original file line number Diff line number Diff line change
Expand Up @@ -511,8 +511,12 @@ let ApplyAllOptimizations

let results, optEnvFirstLoop =
match tcConfig.optSettings.processingMode with
// Parallel optimization breaks determinism - turn it off in deterministic builds.
| Optimizer.OptimizationProcessingMode.Parallel ->
// Determinism under Parallel mode relies on the per-pass sorts in
// DetupleArgs.determineTransforms and InnerLambdasToTopLevelFuncs.CreateNewValuesForTLR
// (via valSourceOrderKey). Any new pass calling NiceNameGenerator from a
// parallel optimizer phase must sort its Val collection the same way.
// See https://github.com/dotnet/fsharp/issues/19732.
let results, optEnvFirstPhase =
ParallelOptimization.optimizeFilesInParallel optEnv phases implFiles

Expand Down
6 changes: 5 additions & 1 deletion src/Compiler/Optimize/DetupleArgs.fs
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,11 @@ let determineTransforms g (z: Results) =
let callPatterns = sitesCPs sites // callPatterns from sites
decideTransform g z f callPatterns (m, tps, vss, retTy) // make transform (if required)

let vtransforms = Zmap.chooseL selectTransform z.Uses
// See https://github.com/dotnet/fsharp/issues/19732 for why we sort here.
let vtransforms =
Zmap.toList z.Uses
|> List.sortWith (fun (v1, _) (v2, _) -> compare (valSourceOrderKey v1) (valSourceOrderKey v2))
|> List.choose (fun (f, sites) -> selectTransform f sites)
let vtransforms = Zmap.ofList valOrder vtransforms
vtransforms

Expand Down
5 changes: 4 additions & 1 deletion src/Compiler/Optimize/InnerLambdasToTopLevelFuncs.fs
Original file line number Diff line number Diff line change
Expand Up @@ -844,7 +844,10 @@ let CreateNewValuesForTLR g tlrS arityM fclassM envPackM =
let fHat = mkLocalNameTypeArity f.IsCompilerGenerated m fHatName fHatTy (Some fHatArity)
fHat

let fs = Zset.elements tlrS
// See https://github.com/dotnet/fsharp/issues/19732 for why we sort here.
let fs =
Zset.elements tlrS
|> List.sortWith (fun v1 v2 -> compare (valSourceOrderKey v1) (valSourceOrderKey v2))
let ffHats = List.map (fun f -> f, createFHat f) fs
let fHatM = Zmap.ofList valOrder ffHats
fHatM
Expand Down
9 changes: 9 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ module internal ExprConstruction =
member _.Compare(v1, v2) = compareBy v1 v2 _.Stamp
}

// Source-position-derived order key for Vals. Used to walk Val collections
// in a stable, build-independent order before calling NiceNameGenerator
// from parallel optimizer passes. Stamp is the final tiebreaker for
// synthetic Vals at the same location; stamps are fixed within a single
// process so the order is total. See https://github.com/dotnet/fsharp/issues/19732.
let valSourceOrderKey (v: Val) =
let r = v.Range
struct (r.FileIndex, r.StartLine, r.StartColumn, v.LogicalName, v.Stamp)

let tyconOrder =
{ new IComparer<Tycon> with
member _.Compare(tycon1, tycon2) = compareBy tycon1 tycon2 _.Stamp
Expand Down
6 changes: 6 additions & 0 deletions src/Compiler/TypedTree/TypedTreeOps.ExprConstruction.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ module internal ExprConstruction =
/// An ordering for value definitions, based on stamp
val valOrder: IComparer<Val>

/// Stable, source-position-derived key for ordering Vals.
/// Use this before calling NiceNameGenerator from parallel optimizer passes
/// so the generated names do not depend on Val.Stamp assignment race.
/// See https://github.com/dotnet/fsharp/issues/19732.
val valSourceOrderKey: Val -> struct (int * int * int * string * int64)

/// An ordering for type definitions, based on stamp
val tyconOrder: IComparer<Tycon>

Expand Down
66 changes: 66 additions & 0 deletions tests/fsharp/Compiler/CodeGen/EmittedIL/DeterministicTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,72 @@ let inline myFunc x y = x - y"""
else
Assert.NotEqual(mvid1,mvid2)

// https://github.com/dotnet/fsharp/issues/19732
// Multi-file optimized compilation exercises DetupleArgs and TLR (tuple-arg
// functions + nested lambdas). These passes iterate Val sets whose order
// depends on Val.Stamp, which is racy under parallel optimization.
// The fix sorts by source position (valSourceOrderKey) before iterating.
// Note: this in-process test is a regression guard; the full race requires
// large-scale parallel compilation tested by eng/test-determinism.ps1 in Release.
[<Fact>]
let ``Optimized multi-file assembly should be deterministic`` () =
let outputDir = DirectoryInfo(Path.Combine(Path.GetTempPath(), "fsharp-determinism-test"))
if outputDir.Exists then outputDir.Delete(true)
outputDir.Create()

let makeFile i =
FsSourceWithFileName
$"File%d{i}.fs"
$"""
module File%d{i}

let processTuple%d{i} (a: int, b: string) =
let inner x = x + a
(inner 1, b.Length)

let callSite%d{i} () =
let r1 = processTuple%d{i} (42, "hello")
let r2 = processTuple%d{i} (99, "world")
let nested () =
let deep () = fst r1 + fst r2
deep ()
nested ()
"""

let additionalFiles = [ for i in 2..8 -> makeFile i ]

let getMvid () =
FSharp
"""
module File1

let processTuple1 (a: int, b: string) =
let inner x = x + a
(inner 1, b.Length)

let callSite1 () =
let r1 = processTuple1 (42, "hello")
let r2 = processTuple1 (99, "world")
let nested () =
let deep () = fst r1 + fst r2
deep ()
nested ()
"""
|> withAdditionalSourceFiles additionalFiles
|> asLibrary
|> withOptimize
|> withName "DetTest"
|> withOutputDirectory (Some outputDir)
|> withOptions [ "--deterministic" ]
|> compileGuid

let mvids = [| for _ in 1..10 -> getMvid () |]

for i in 1 .. mvids.Length - 1 do
Assert.Equal(mvids.[0], mvids.[i])

outputDir.Delete(true)

[<Fact>]
let ``Reference assemblies MVID must change when literal constant value changes`` () =
let codeWithLiteral42 = """
Expand Down
Loading