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
Expand Up @@ -57,6 +57,7 @@
* Fix parser recovery, name resolution, and code completion for unfinished enum patterns ([PR #19708](https://github.com/dotnet/fsharp/pull/19708))
* Parser: fix unexpected diagnostics in debug builds, improve error messages ([PR #19730](https://github.com/dotnet/fsharp/pull/19730))
* 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))
* 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))

### Added

Expand Down
2 changes: 2 additions & 0 deletions src/Compiler/Driver/fsc.fs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,8 @@ let main3

let observer = if hasIvt then PublicAndInternal else PublicOnly

// `hash` here is on byte[] / int64, neither of which depends on
// String.GetHashCode; safe for deterministic output. See issue #19751.
let optDataHash =
optDataResources
|> List.map (fun ilResource ->
Expand Down
15 changes: 13 additions & 2 deletions src/Compiler/Utilities/TypeHashing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,18 @@ module internal HashingPrimitives =

type Hash = int

let inline hashText (s: string) : Hash = hash s
/// FNV-1a 32-bit over UTF-16 code units – deterministic across processes
/// (unlike String.GetHashCode which is randomized in .NET 6+).
let hashStableString (s: string) : Hash =
let mutable h = 2166136261u

for c in s do
h <- (h ^^^ uint32 c) * 16777619u

int h

let hashText (s: string) : Hash = hashStableString s

let inline combineHash acc y : Hash = (acc <<< 1) + y + 631
let inline pipeToHash (value: Hash) (acc: Hash) = combineHash acc value
let inline addFullStructuralHash value (acc: Hash) = combineHash acc (hash value)
Expand Down Expand Up @@ -91,7 +102,7 @@ module HashIL =
let hashILTypeRef (tref: ILTypeRef) =
tref.Enclosing
|> hashListOrderMatters hashText
|> addFullStructuralHash tref.Name
|> pipeToHash (hashText tref.Name)

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ namespace CompilerOptions.Fsc
open Xunit
open FSharp.Test
open FSharp.Test.Compiler
open FSharp.Test.Utilities
open System
open System.IO
open System.Reflection.Metadata
open System.Reflection.PortableExecutable

module determinism =

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

/// Compile to ref assembly out-of-process via runFscProcess.
/// Separate processes needed because String.GetHashCode is seeded once per process.
let private compileRefAssembly (workDir: string) (sourceFile: string) : string * string =
Directory.CreateDirectory workDir |> ignore
let outDll = Path.Combine(workDir, "Out.dll")
let outRef = Path.Combine(workDir, "Out.ref.dll")
let defaultOpts = CompilerAssert.DefaultProjectOptions(TargetFramework.Current).OtherOptions
let result = runFscProcess [
yield "--target:library"
yield "--deterministic+"
yield! (defaultOpts |> Array.toList)
yield $"--refout:{outRef}"
yield $"-o:{outDll}"
yield sourceFile
]
if result.ExitCode <> 0 then
failwithf "fsc exit %d\nstdout:%s\nstderr:%s" result.ExitCode result.StdOut result.StdErr
outDll, outRef

let private readMvid (dll: string) : Guid =
use peReader = new PEReader(File.OpenRead dll)
let reader = peReader.GetMetadataReader()
reader.GetGuid(reader.GetModuleDefinition().Mvid)

// Regression test for https://github.com/dotnet/fsharp/issues/19751
// Two separate fsc processes needed to detect randomized String.GetHashCode seeds.
[<FactForNETCOREAPP>]
let ``Reference assembly MVID is deterministic across separate fsc invocations`` () =
let tempRoot =
Path.Combine(Path.GetTempPath(), "fsharp-ref-mvid-test-" + Guid.NewGuid().ToString("N"))
try
Directory.CreateDirectory tempRoot |> ignore
let src = Path.Combine(tempRoot, "Foo.fs")
File.WriteAllText(src, "module Foo.Core\n\nlet foo (x: int) : int = x + 1\n")

let dll1, ref1 = compileRefAssembly (Path.Combine(tempRoot, "out1")) src
let dll2, ref2 = compileRefAssembly (Path.Combine(tempRoot, "out2")) src

Assert.Equal(readMvid ref1, readMvid ref2)
Assert.Equal(readMvid dll1, readMvid dll2)
finally
try Directory.Delete(tempRoot, true) with _ -> ()
Loading