Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
f6cad50
Add Roslyn-format EnC CustomDebugInformation codec and portable PDB m…
NatElkins Jul 1, 2026
0d02145
Use ProcessStartInfo.Arguments for net472 compatibility
NatElkins Jul 1, 2026
66a1412
Make the EnC CDI test module public for xunit v3 discovery
NatElkins Jul 2, 2026
c8639f3
Extract stable synthesized-name replay layer
NatElkins Jul 2, 2026
d505ab9
Fix generated-name scope test in stable names slice
NatElkins Jul 8, 2026
7aaacc5
Fix EnC CDI cross-validation test process launch on Desktop and bare …
NatElkins Jul 3, 2026
cd1339b
Merge branch 'refresh/20260717/hotreload-stable-names' into refresh/2…
NatElkins Jul 17, 2026
c23986b
Add hot reload baseline reader state
NatElkins Jul 3, 2026
11e8fa0
Fix synthesized-name snapshot assertions to use the array equality ov…
NatElkins Jul 3, 2026
3e1678d
Validate hot reload baseline metadata inputs
NatElkins Jul 17, 2026
24f4879
Format hot reload compiler sources
NatElkins Jul 17, 2026
4ad2364
Harden baseline metadata parsing
NatElkins Jul 22, 2026
88c0c62
Merge remote-tracking branch 'origin/main' into refresh/20260717/hotr…
NatElkins Jul 22, 2026
9dad496
Handle metadata-only methods in PDB ambiguity checks
NatElkins Jul 22, 2026
e7531a7
Make synthesized name snapshots deterministic
NatElkins Jul 22, 2026
7ffe458
Bound baseline string heap reads
NatElkins Jul 23, 2026
a052db3
Format bounded string heap reader
NatElkins Jul 23, 2026
ead308d
Merge remote-tracking branch 'origin/main' into refresh/20260717/hotr…
NatElkins Jul 24, 2026
e0a38af
Complete baseline reader merge integration
NatElkins Jul 24, 2026
0ad1dce
Place baseline reader release note in current FCS notes
NatElkins Jul 24, 2026
6218916
Merge current main
NatElkins Jul 24, 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
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 @@ -145,6 +145,7 @@
* Implied argument names for function-to-delegate coercions now fall back to the delegate's `Invoke` parameter names when the function has no recoverable names (e.g. a partial application like `System.Func<int, int>((+) 1)`), instead of synthetic `delegateArg0`, `delegateArg1`, … names. ([PR #20001](https://github.com/dotnet/fsharp/pull/20001))
* Add internal `ResetCompilerGeneratedNameState` to `CompilerGlobalState` name generators so warm-checker re-compilation can produce fresh-process-identical generated names. ([PR #20017](https://github.com/dotnet/fsharp/pull/20017))
* Add Roslyn-format EnC CustomDebugInformation codec and portable PDB method CDI emission support to AbstractIL. ([PR #20018](https://github.com/dotnet/fsharp/pull/20018))
* Add internal hot reload baseline reading for recorded EnC state and synthesized-name snapshot PDB data. ([PR #20026](https://github.com/dotnet/fsharp/pull/20026))

### Improved

Expand Down
169 changes: 168 additions & 1 deletion src/Compiler/AbstractIL/EncMethodDebugInformation.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ open System.IO
open System.Reflection.Metadata
open System.Reflection.Metadata.Ecma335
open System.Runtime.InteropServices
open System.Text
open Microsoft.FSharp.NativeInterop

open FSharp.Compiler.AbstractIL.ILPdbWriter

/// Portable-PDB CustomDebugInformation kind GUIDs for the EnC blobs, copied verbatim
/// from roslyn/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs.
[<RequireQualifiedAccess>]
Expand All @@ -47,6 +50,10 @@ module PortableCustomDebugInfoKinds =
/// EnC State Machine State Map CDI kind.
let encStateMachineStateMap = Guid("8B78CD68-2EDE-420B-980B-E15884B8AAA3")

/// F#-owned hot reload synthesized-name snapshot CDI kind. The blob records
/// FSharpSynthesizedTypeMaps.Snapshot bucket arrays in allocation-slot order.
let fsharpSynthesizedNameSnapshot = Guid("49DDB47E-9C74-46EC-8626-0350676571EB")

/// Closure ordinal of a lambda that is lowered to a static (non-capturing) method.
/// Mirrors Roslyn's LambdaDebugInfo.StaticClosureOrdinal.
[<Literal>]
Expand Down Expand Up @@ -167,7 +174,7 @@ let private MaxOccurrenceKey = 0x1FFFFFFD
/// 16-bit segments, least-significant segment = the innermost ordinal; an enclosing
/// ordinal p is stored as (p + 1) shifted left 16 so that depth-1 keys (< 0x10000) and
/// depth-2 keys (>= 0x10000) never collide. Fails closed (None) past the limits: chains
/// deeper than 2, ordinals > 0xFFFF, or keys exceeding the compressed-integer budget
/// deeper than 2, ordinals > 0xFFFF, or keys exceeding the compressed-integer budget,
/// callers must then treat the chain as unmappable, never truncate.
let tryEncodeOccurrenceKey (ordinalChain: int list) : int option =
match ordinalChain with
Expand Down Expand Up @@ -209,6 +216,134 @@ let private invalidData (blobName: string) (offset: int) =
// nullness model, so guard with box (FS3261-safe) rather than dropping the check.
let private isEmpty (blob: byte[]) = isNull (box blob) || blob.Length = 0

// ---------------------------------------------------------------------------
// F# hot reload module CDI: synthesized-name allocation snapshot
// Format:
// compressed(version = 1), compressed(bucket count),
// then buckets sorted by key for deterministic PDB bytes:
// string key, compressed(name count), string name in allocation-slot order.
// Strings are compressed(byte length) followed by UTF-8 bytes.
// ---------------------------------------------------------------------------

[<Literal>]
let private SynthesizedNameSnapshotBlobVersion = 1

let private writeUtf8String (builder: BlobBuilder) (value: string) =
if isNull (box value) then
invalidArg (nameof value) "snapshot strings must be non-null"

let bytes = Encoding.UTF8.GetBytes value
builder.WriteCompressedInteger bytes.Length
builder.WriteBytes bytes

let private readUtf8String (blobName: string) (reader: byref<BlobReader>) =
let length = reader.ReadCompressedInteger()

if length < 0 || length > reader.RemainingBytes then
invalidData blobName reader.Offset

let bytes = reader.ReadBytes length
Encoding.UTF8.GetString(bytes, 0, bytes.Length)

let private materializeSynthesizedNameSnapshot (snapshot: seq<struct (string * string[])>) =
snapshot
|> Seq.map (fun struct (key, names) ->
if isNull (box key) then
invalidArg (nameof snapshot) "snapshot keys must be non-null"

if isNull (box names) then
invalidArg (nameof snapshot) $"snapshot bucket '{key}' must be non-null"

key, Array.copy names)
|> Seq.sortBy fst
|> Seq.toArray

/// Serializes an allocation-ordered synthesized-name snapshot into the F#-owned module
/// CDI blob. An empty snapshot returns an empty blob so no CDI row needs to be emitted.
let serializeSynthesizedNameSnapshot (snapshot: seq<struct (string * string[])>) : byte[] =
let buckets = materializeSynthesizedNameSnapshot snapshot

if buckets.Length = 0 then
Array.empty
else
let builder = BlobBuilder()
builder.WriteCompressedInteger SynthesizedNameSnapshotBlobVersion
builder.WriteCompressedInteger buckets.Length

for key, names in buckets do
writeUtf8String builder key
builder.WriteCompressedInteger names.Length

for name in names do
writeUtf8String builder name

builder.ToArray()

/// Deserializes the F#-owned synthesized-name snapshot CDI blob. Bucket order in the
/// blob is deterministic only; each bucket array is returned exactly in recorded slot order.
let deserializeSynthesizedNameSnapshot (blob: byte[]) : Map<string, string[]> =
if isEmpty blob then
Map.empty
else
let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)

try
let mutable reader =
BlobReader(NativePtr.ofNativeInt<byte> (handle.AddrOfPinnedObject()), blob.Length)

try
let version = reader.ReadCompressedInteger()

if version <> SynthesizedNameSnapshotBlobVersion then
invalidData "synthesized name snapshot" reader.Offset

let bucketCount = reader.ReadCompressedInteger()

if bucketCount <= 0 || bucketCount > reader.RemainingBytes / 2 then
invalidData "synthesized name snapshot" reader.Offset

let buckets = ResizeArray<string * string[]>()

for _ in 1..bucketCount do
let key = readUtf8String "synthesized name snapshot" &reader
let nameCount = reader.ReadCompressedInteger()

// Every serialized name consumes at least one byte for its UTF-8
// length, so this check bounds allocation before Array.zeroCreate.
if nameCount < 0 || nameCount > reader.RemainingBytes then
invalidData "synthesized name snapshot" reader.Offset

let names = Array.zeroCreate nameCount

for i in 0 .. nameCount - 1 do
names[i] <- readUtf8String "synthesized name snapshot" &reader

buckets.Add(key, names)

if reader.RemainingBytes <> 0 then
invalidData "synthesized name snapshot" reader.Offset

buckets |> Seq.map id |> Map.ofSeq
with :? BadImageFormatException ->
invalidData "synthesized name snapshot" reader.Offset
finally
handle.Free()

/// Creates the module-level CustomDebugInformation row for the allocation-ordered
/// synthesized-name snapshot. Empty snapshots emit no row.
let computeSynthesizedNameSnapshotCustomDebugInfoRows (snapshot: seq<struct (string * string[])>) : PdbModuleCustomDebugInfo list =
let blob = serializeSynthesizedNameSnapshot snapshot

if blob.Length = 0 then
[]
else
[
{
KindGuid = PortableCustomDebugInfoKinds.fsharpSynthesizedNameSnapshot
Blob = blob
}
]

// ---------------------------------------------------------------------------
// EnC Local Slot Map
// Format (EditAndContinueMethodDebugInformation.cs, SerializeLocalSlots lines 145-191,
Expand Down Expand Up @@ -555,3 +690,35 @@ let readEncMethodDebugInfoFromPortablePdb (pdbBytes: byte[]) : Map<int, EncMetho
// Not a portable PDB image (or a corrupted one): callers still get an empty
// map instead of a crash.
Map.empty

/// Reads the F#-owned allocation-ordered synthesized-name snapshot from a portable PDB.
/// None means either the record is absent or invalid; callers must fall back to IL
/// reconstruction rather than trusting a partial layout.
let readSynthesizedNameSnapshotFromPortablePdb (pdbBytes: byte[]) : Map<string, string[]> option =
if isEmpty pdbBytes then
None
else
try
use provider =
MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.CreateRange pdbBytes)

let reader = provider.GetMetadataReader()

let blobs =
[
for cdiHandle in reader.CustomDebugInformation do
let cdi = reader.GetCustomDebugInformation cdiHandle

if cdi.Parent.Kind = HandleKind.ModuleDefinition then
let kind = reader.GetGuid cdi.Kind

if kind = PortableCustomDebugInfoKinds.fsharpSynthesizedNameSnapshot then
reader.GetBlobBytes cdi.Value
]

match blobs with
| [ blob ] -> Some(deserializeSynthesizedNameSnapshot blob)
| _ -> None
with
| :? BadImageFormatException
| :? InvalidDataException -> None
21 changes: 21 additions & 0 deletions src/Compiler/AbstractIL/EncMethodDebugInformation.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ module PortableCustomDebugInfoKinds =
/// EnC State Machine State Map CDI kind.
val encStateMachineStateMap: System.Guid

/// F#-owned hot reload synthesized-name snapshot CDI kind.
val fsharpSynthesizedNameSnapshot: System.Guid

/// Closure ordinal of a lambda that is lowered to a static (non-capturing) method.
/// Mirrors Roslyn's LambdaDebugInfo.StaticClosureOrdinal.
[<Literal>]
Expand Down Expand Up @@ -135,6 +138,19 @@ val tryEncodeOccurrenceKey: ordinalChain: int list -> int option
/// root-first ordinal chain.
val decodeOccurrenceKey: key: int -> int list

/// Serializes an allocation-ordered synthesized-name snapshot into the F#-owned module
/// CDI blob. An empty snapshot returns an empty blob so no CDI row needs to be emitted.
val serializeSynthesizedNameSnapshot: snapshot: seq<struct (string * string[])> -> byte[]

/// Deserializes the F#-owned synthesized-name snapshot CDI blob. Bucket order in the
/// blob is deterministic only; each bucket array is returned exactly in recorded slot order.
val deserializeSynthesizedNameSnapshot: blob: byte[] -> Map<string, string[]>

/// Creates the module-level CustomDebugInformation row for the allocation-ordered
/// synthesized-name snapshot. Empty snapshots emit no row.
val computeSynthesizedNameSnapshotCustomDebugInfoRows:
snapshot: seq<struct (string * string[])> -> FSharp.Compiler.AbstractIL.ILPdbWriter.PdbModuleCustomDebugInfo list

/// Serializes the EnC Local Slot Map blob for 'info', byte-for-byte as Roslyn's
/// SerializeLocalSlots. Returns the empty array when there are no slots (no CDI row
/// should be emitted then).
Expand Down Expand Up @@ -176,3 +192,8 @@ val deserialize:
/// Fail safe: a null/empty or non-PDB image yields the empty map, and a method whose
/// blobs do not decode is omitted rather than guessed.
val readEncMethodDebugInfoFromPortablePdb: pdbBytes: byte[] -> Map<int, EncMethodDebugInformation>

/// Reads the F#-owned allocation-ordered synthesized-name snapshot from a portable PDB.
/// None means either the record is absent or invalid; callers must fall back to IL
/// reconstruction rather than trusting a partial layout.
val readSynthesizedNameSnapshotFromPortablePdb: pdbBytes: byte[] -> Map<string, string[]> option
15 changes: 13 additions & 2 deletions src/Compiler/AbstractIL/ilwrite.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3863,8 +3863,11 @@ type options =
referenceAssemblyAttribOpt: ILAttribute option
referenceAssemblySignatureHash : int option
pathMap: PathMap
/// Hot reload baseline side channel: module-level CustomDebugInformation rows for
/// F#-owned records in the portable PDB. Empty for ordinary compiles.
moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list
/// Per-method EnC CustomDebugInformation rows for the portable PDB writer, keyed by
/// IL method name. Empty for ordinary compiles, so flag-off output stays byte-identical.
/// IL method name. Empty for ordinary compiles.
methodCustomDebugInfoRows: Map<string, PdbMethodCustomDebugInfo list> }

let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRefs) =
Expand Down Expand Up @@ -4028,7 +4031,15 @@ let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRe
match options.pdbfile, options.portablePDB with
| Some _, true ->
let pdbInfo =
generatePortablePdb options.embedAllSource options.embedSourceList options.sourceLink options.checksumAlgorithm pdbData options.pathMap options.methodCustomDebugInfoRows
generatePortablePdb
options.embedAllSource
options.embedSourceList
options.sourceLink
options.checksumAlgorithm
pdbData
options.pathMap
options.moduleCustomDebugInfoRows
options.methodCustomDebugInfoRows

if options.embeddedPDB then
let uncompressedLength, contentId, stream, algorithmName, checkSum = pdbInfo
Expand Down
3 changes: 3 additions & 0 deletions src/Compiler/AbstractIL/ilwrite.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type options =
referenceAssemblyAttribOpt: ILAttribute option
referenceAssemblySignatureHash: int option
pathMap: PathMap
/// Hot reload baseline side channel: module-level CustomDebugInformation rows for
/// F#-owned records in the portable PDB. Empty for ordinary compiles.
moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list
/// Per-method EnC CustomDebugInformation rows for the portable PDB writer, keyed by
/// IL method name. Empty for ordinary compiles, so flag-off output stays byte-identical.
methodCustomDebugInfoRows: Map<string, PdbMethodCustomDebugInfo list>
Expand Down
25 changes: 24 additions & 1 deletion src/Compiler/AbstractIL/ilwritepdb.fs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ type PdbMethodData =
/// definition row in the portable PDB.
type PdbMethodCustomDebugInfo = { KindGuid: Guid; Blob: byte[] }

/// A pre-serialized CustomDebugInformation row (kind GUID + blob) to attach to the
/// module definition row in the portable PDB.
type PdbModuleCustomDebugInfo = { KindGuid: Guid; Blob: byte[] }

module SequencePoint =
let orderBySource sp1 sp2 =
let c1 = compare sp1.Document sp2.Document
Expand Down Expand Up @@ -348,6 +352,7 @@ type PortablePdbGenerator
checksumAlgorithm,
info: PdbData,
pathMap: PathMap,
moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list,
methodCustomDebugInfoRows: Map<string, PdbMethodCustomDebugInfo list>
) =

Expand Down Expand Up @@ -484,6 +489,14 @@ type PortablePdbGenerator
)
|> ignore

for cdiRow in moduleCustomDebugInfoRows |> List.sortBy (fun row -> row.KindGuid) do
metadata.AddCustomDebugInformation(
ModuleDefinitionHandle.op_Implicit EntityHandle.ModuleDefinition,
metadata.GetOrAddGuid cdiRow.KindGuid,
metadata.GetOrAddBlob cdiRow.Blob
)
|> ignore

index

let mutable lastLocalVariableHandle = Unchecked.defaultof<LocalVariableHandle>
Expand Down Expand Up @@ -881,10 +894,20 @@ let generatePortablePdb
checksumAlgorithm
(info: PdbData)
(pathMap: PathMap)
(moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list)
(methodCustomDebugInfoRows: Map<string, PdbMethodCustomDebugInfo list>)
=
let generator =
PortablePdbGenerator(embedAllSource, embedSourceList, sourceLink, checksumAlgorithm, info, pathMap, methodCustomDebugInfoRows)
PortablePdbGenerator(
embedAllSource,
embedSourceList,
sourceLink,
checksumAlgorithm,
info,
pathMap,
moduleCustomDebugInfoRows,
methodCustomDebugInfoRows
)

generator.Emit()

Expand Down
6 changes: 6 additions & 0 deletions src/Compiler/AbstractIL/ilwritepdb.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ type PdbMethodData =
/// one method row (fail closed on ambiguity).
type PdbMethodCustomDebugInfo = { KindGuid: System.Guid; Blob: byte[] }

/// A pre-serialized CustomDebugInformation row to attach to the module definition row
/// in the portable PDB (kind GUID + blob). Supplied by hot reload for F#-owned
/// deterministic baseline records.
type PdbModuleCustomDebugInfo = { KindGuid: System.Guid; Blob: byte[] }

[<NoEquality; NoComparison>]
type PdbData =
{
Expand Down Expand Up @@ -115,6 +120,7 @@ val generatePortablePdb:
checksumAlgorithm: HashAlgorithm ->
info: PdbData ->
pathMap: PathMap ->
moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list ->
methodCustomDebugInfoRows: Map<string, PdbMethodCustomDebugInfo list> ->
int64 * BlobContentId * MemoryStream * string * byte[]

Expand Down
Loading
Loading