Skip to content

Commit 2109bb2

Browse files
T-GroCopilot
andcommitted
Fix non-deterministic reference assembly MVIDs (#19751)
Replace randomized String.GetHashCode with deterministic FNV-1a 32-bit hash in TypeHashing.hashText and hashILTypeRef. The per-process hash seed in .NET 6+ caused --refout / ProduceReferenceAssembly to emit a different MVID on every build. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bdb847a commit 2109bb2

4 files changed

Lines changed: 61 additions & 2 deletions

File tree

docs/release-notes/.FSharp.Compiler.Service/11.0.100.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
* Fix parser recovery, name resolution, and code completion for unfinished enum patterns ([PR #19708](https://github.com/dotnet/fsharp/pull/19708))
5858
* Parser: fix unexpected diagnostics in debug builds, improve error messages ([PR #19730](https://github.com/dotnet/fsharp/pull/19730))
5959
* 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))
60+
* 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 #TODO](https://github.com/dotnet/fsharp/pull/TODO))
6061

6162
### Added
6263

src/Compiler/Driver/fsc.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -873,6 +873,8 @@ let main3
873873

874874
let observer = if hasIvt then PublicAndInternal else PublicOnly
875875

876+
// `hash` here is on byte[] / int64, neither of which depends on
877+
// String.GetHashCode; safe for deterministic output. See issue #19751.
876878
let optDataHash =
877879
optDataResources
878880
|> List.map (fun ilResource ->

src/Compiler/Utilities/TypeHashing.fs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,18 @@ module internal HashingPrimitives =
1818

1919
type Hash = int
2020

21-
let inline hashText (s: string) : Hash = hash s
21+
/// FNV-1a 32-bit over UTF-16 code units – deterministic across processes
22+
/// (unlike String.GetHashCode which is randomized in .NET 6+).
23+
let hashStableString (s: string) : Hash =
24+
let mutable h = 2166136261u
25+
26+
for c in s do
27+
h <- (h ^^^ uint32 c) * 16777619u
28+
29+
int h
30+
31+
let hashText (s: string) : Hash = hashStableString s
32+
2233
let inline combineHash acc y : Hash = (acc <<< 1) + y + 631
2334
let inline pipeToHash (value: Hash) (acc: Hash) = combineHash acc value
2435
let inline addFullStructuralHash value (acc: Hash) = combineHash acc (hash value)
@@ -91,7 +102,7 @@ module HashIL =
91102
let hashILTypeRef (tref: ILTypeRef) =
92103
tref.Enclosing
93104
|> hashListOrderMatters hashText
94-
|> addFullStructuralHash tref.Name
105+
|> pipeToHash (hashText tref.Name)
95106

96107
let private hashILArrayShape (sh: ILArrayShape) = sh.Rank
97108

tests/FSharp.Compiler.ComponentTests/CompilerOptions/fsc/determinism/determinism.fs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ namespace CompilerOptions.Fsc
44
open Xunit
55
open FSharp.Test
66
open FSharp.Test.Compiler
7+
open FSharp.Test.Utilities
78
open System
89
open System.IO
10+
open System.Reflection.Metadata
11+
open System.Reflection.PortableExecutable
912

1013
module determinism =
1114

@@ -139,3 +142,45 @@ module Determinism
139142
areSame (Path.ChangeExtension(exename1, "pdb")) (Path.ChangeExtension(exename2, "pdb"))
140143
| _ -> raise (new Exception "Pathmap1 and PathMap2 do not match")
141144

145+
/// Compile to ref assembly out-of-process via runFscProcess.
146+
/// Separate processes needed because String.GetHashCode is seeded once per process.
147+
let private compileRefAssembly (workDir: string) (sourceFile: string) : string * string =
148+
Directory.CreateDirectory workDir |> ignore
149+
let outDll = Path.Combine(workDir, "Out.dll")
150+
let outRef = Path.Combine(workDir, "Out.ref.dll")
151+
let defaultOpts = CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions
152+
let result = runFscProcess [
153+
yield "--target:library"
154+
yield "--deterministic+"
155+
yield! (defaultOpts |> Array.toList)
156+
yield $"--refout:{outRef}"
157+
yield $"-o:{outDll}"
158+
yield sourceFile
159+
]
160+
if result.ExitCode <> 0 then
161+
failwithf "fsc exit %d\nstdout:%s\nstderr:%s" result.ExitCode result.StdOut result.StdErr
162+
outDll, outRef
163+
164+
let private readMvid (dll: string) : Guid =
165+
use peReader = new PEReader(File.OpenRead dll)
166+
let reader = peReader.GetMetadataReader()
167+
reader.GetGuid(reader.GetModuleDefinition().Mvid)
168+
169+
// Regression test for https://github.com/dotnet/fsharp/issues/19751
170+
// Two separate fsc processes needed to detect randomized String.GetHashCode seeds.
171+
[<Fact>]
172+
let ``Reference assembly MVID is deterministic across separate fsc invocations`` () =
173+
let tempRoot =
174+
Path.Combine(Path.GetTempPath(), "fsharp-ref-mvid-test-" + Guid.NewGuid().ToString("N"))
175+
try
176+
Directory.CreateDirectory tempRoot |> ignore
177+
let src = Path.Combine(tempRoot, "Foo.fs")
178+
File.WriteAllText(src, "module Foo.Core\n\nlet foo (x: int) : int = x + 1\n")
179+
180+
let dll1, ref1 = compileRefAssembly (Path.Combine(tempRoot, "out1")) src
181+
let dll2, ref2 = compileRefAssembly (Path.Combine(tempRoot, "out2")) src
182+
183+
Assert.Equal(readMvid ref1, readMvid ref2)
184+
Assert.Equal(readMvid dll1, readMvid dll2)
185+
finally
186+
try Directory.Delete(tempRoot, true) with _ -> ()

0 commit comments

Comments
 (0)