diff --git a/FSharp.slnx b/FSharp.slnx
index 50819fbfb6b..c97fdbce36c 100644
--- a/FSharp.slnx
+++ b/FSharp.slnx
@@ -43,6 +43,11 @@
+
+
+
+
+
diff --git a/docs/hot-reload-rude-edits.md b/docs/hot-reload-rude-edits.md
new file mode 100644
index 00000000000..a4a974d537a
--- /dev/null
+++ b/docs/hot-reload-rude-edits.md
@@ -0,0 +1,27 @@
+# F# hot reload rude-edit diagnostics
+
+F# hot reload reports an `FSHRDL` diagnostic when an edit cannot be applied safely to the running process. The application is rebuilt and restarted instead of applying a delta that could leave it in an invalid state.
+
+The diagnostic message identifies the affected declaration and the reason for the restart. These codes are owned by the F# compiler and are separate from Roslyn's `ENC` diagnostic namespace.
+
+| Code | Meaning | What to do |
+| --- | --- | --- |
+| `FSHRDL001` | A member signature changed. | Undo the signature change to apply in place, or allow the rebuild and restart. |
+| `FSHRDL002` | An `inline` annotation changed. | Allow the rebuild and restart. |
+| `FSHRDL003` | A type representation or layout changed. | Allow the rebuild and restart. |
+| `FSHRDL004` | A declaration was added in a shape the runtime cannot add. | Allow the rebuild and restart. |
+| `FSHRDL005` | A declaration was removed. | Allow the rebuild and restart. |
+| `FSHRDL006` | A virtual, abstract, or override member was added. | Allow the rebuild and restart. |
+| `FSHRDL007` | A constructor was added. | Allow the rebuild and restart. |
+| `FSHRDL008` | A user-defined operator was added. | Allow the rebuild and restart. |
+| `FSHRDL009` | An explicit interface implementation was added. | Allow the rebuild and restart. |
+| `FSHRDL010` | A member was added to an interface. | Allow the rebuild and restart. |
+| `FSHRDL011` | A field was added in a shape the runtime cannot add. | Allow the rebuild and restart. |
+| `FSHRDL012` | A lambda's lowered shape changed incompatibly. | Allow the rebuild and restart. |
+| `FSHRDL013` | A state machine's resumable or hoisted layout changed incompatibly. | Keep the existing resume-point and captured-value layout, or allow the rebuild and restart. |
+| `FSHRDL014` | A query expression's lowered shape changed incompatibly. | Allow the rebuild and restart. |
+| `FSHRDL015` | A synthesized compiler declaration changed incompatibly. | Allow the rebuild and restart. |
+| `FSHRDL016` | The runtime did not advertise a capability required by the edit. | Update the runtime if a newer version supports the capability, or allow the rebuild and restart. |
+| `FSHRDL099` | The edit is unsupported for another fail-closed reason. | Follow the detailed message and allow the rebuild and restart. |
+
+These diagnostics are intentionally fail closed. If the compiler cannot prove that an edit is safe, it requests a restart and leaves the running application unchanged.
diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
index 7dd68964cb3..e3a2470dcc0 100644
--- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
+++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
@@ -139,9 +139,12 @@
### Added
+* Add an internal typed-tree diff utility for future F# hot reload edit classification. It is not called by normal compilation. ([PR #20025](https://github.com/dotnet/fsharp/pull/20025))
* Added a "most concrete" tiebreaker for overload resolution (`--langversion:preview`). ([RFC FS-1340](https://github.com/fsharp/fslang-design/pull/834), [PR #19277](https://github.com/dotnet/fsharp/pull/19277))
* Added support for `OverloadResolutionPriorityAttribute` in overload resolution (`--langversion:preview`). ([RFC FS-1338](https://github.com/fsharp/fslang-design/pull/828), [PR #19277](https://github.com/dotnet/fsharp/pull/19277))
* Added internal synthesized-name replay infrastructure for compiler-generated names, preserving normal compilation output while enabling future hot reload name stability work.
+* Added an experimental, internal, flag-gated in-process compile path for hot reload sessions. `FSHARP_HOTRELOAD_INPROCESS_COMPILE` refreshes the output assembly and PDB from the latest checked project before delta emission, while `FSHARP_HOTRELOAD_INCREMENTAL_EMIT` enables a nested per-file optimized-tree cache. ([PR #20031](https://github.com/dotnet/fsharp/pull/20031))
+* Added internal F# hot reload delta emitter and symbol matcher infrastructure with direct emitter test coverage. ([PR #20027](https://github.com/dotnet/fsharp/pull/20027))
* Added `FSharpMemberOrFunctionOrValue.IsPropertyAccessor` convenience property that returns true for compiler-generated property accessors (`get_X` / `set_X`). ([Issue #18157](https://github.com/dotnet/fsharp/issues/18157), [PR #19883](https://github.com/dotnet/fsharp/pull/19883))
* Added warning FS3884 when a function or delegate value is used as an interpolated string argument. ([PR #19289](https://github.com/dotnet/fsharp/pull/19289))
* Symbols: add ObsoleteDiagnosticInfo ([PR #19359](https://github.com/dotnet/fsharp/pull/19359))
@@ -160,6 +163,7 @@
* 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 internal ECMA-335 Edit-and-Continue metadata delta writer to AbstractIL. ([PR #20019](https://github.com/dotnet/fsharp/pull/20019))
* 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 an experimental `FSharpChecker` hot reload session API with per-project baselines, capability-gated delta emission, and `Commit`/`Discard` transaction semantics. Off by default behind `--test:HotReloadDeltas`. ([Issue #11636](https://github.com/dotnet/fsharp/issues/11636), [PR #20030](https://github.com/dotnet/fsharp/pull/20030))
* 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))
* Support for the `` XML documentation tag: at compile time, documentation is copied from an external XML file selected by an XPath query and emitted into the generated documentation file. `` remains unsupported. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19186](https://github.com/dotnet/fsharp/pull/19186))
* Expand `` at tooling time. In IDE tooltips, completion, and signature help, documentation is inherited from base classes, interfaces, overridden members, and constructors (matched by parameter signature). The FCS Symbols API (`FSharpSymbol.XmlDoc`) additionally resolves explicit `cref` targets, but does not expand constructor inheritance. The compiler emits the tag verbatim into generated XML documentation files, matching C#; `` is not implemented. ([Issue #19175](https://github.com/dotnet/fsharp/issues/19175), [PR #19188](https://github.com/dotnet/fsharp/pull/19188))
diff --git a/src/Compiler/AbstractIL/DeltaIndexSizing.fs b/src/Compiler/AbstractIL/DeltaIndexSizing.fs
index 4ca3e280d4b..2e2c979461c 100644
--- a/src/Compiler/AbstractIL/DeltaIndexSizing.fs
+++ b/src/Compiler/AbstractIL/DeltaIndexSizing.fs
@@ -11,9 +11,10 @@ module internal FSharp.Compiler.AbstractIL.DeltaIndexSizing
open FSharp.Compiler.AbstractIL.BinaryConstants
open FSharp.Compiler.AbstractIL.ILDeltaHandles
-open FSharp.Compiler.AbstractIL.ILMetadataHeaps
open FSharp.Compiler.AbstractIL.DeltaMetadataEncoding
+type MetadataHeapSizes = FSharp.Compiler.AbstractIL.ILBinaryWriter.MetadataHeapSizes
+
/// Holds computed "bigness" flags for all coded index types.
/// When true, the index requires 4 bytes; when false, 2 bytes suffice.
type CodedIndexSizes =
diff --git a/src/Compiler/AbstractIL/DeltaMetadataSerializer.fs b/src/Compiler/AbstractIL/DeltaMetadataSerializer.fs
index 7033f74f8a8..c7ad17b5af7 100644
--- a/src/Compiler/AbstractIL/DeltaMetadataSerializer.fs
+++ b/src/Compiler/AbstractIL/DeltaMetadataSerializer.fs
@@ -4,7 +4,7 @@ open System
open System.Collections.Generic
open System.IO
open System.Text
-open FSharp.Compiler.AbstractIL.ILMetadataHeaps
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
open FSharp.Compiler.AbstractIL.BinaryConstants
open FSharp.Compiler.AbstractIL.ILDeltaHandles
open FSharp.Compiler.AbstractIL.DeltaMetadataTables
diff --git a/src/Compiler/AbstractIL/FSharpDeltaMetadataWriter.fs b/src/Compiler/AbstractIL/FSharpDeltaMetadataWriter.fs
index 85ba1c7e823..f4decb82666 100644
--- a/src/Compiler/AbstractIL/FSharpDeltaMetadataWriter.fs
+++ b/src/Compiler/AbstractIL/FSharpDeltaMetadataWriter.fs
@@ -3,7 +3,7 @@ module internal FSharp.Compiler.AbstractIL.FSharpDeltaMetadataWriter
open System
open System.Collections.Generic
open Microsoft.FSharp.Collections
-open FSharp.Compiler.AbstractIL.ILMetadataHeaps
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
open FSharp.Compiler.AbstractIL.BinaryConstants
open FSharp.Compiler.AbstractIL.ILDeltaHandles
open FSharp.Compiler.AbstractIL.IlxDeltaStreams
diff --git a/src/Compiler/AbstractIL/ILBaselineReader.fs b/src/Compiler/AbstractIL/ILBaselineReader.fs
new file mode 100644
index 00000000000..6e0f03bd190
--- /dev/null
+++ b/src/Compiler/AbstractIL/ILBaselineReader.fs
@@ -0,0 +1,1486 @@
+/// Minimal binary reader for baseline metadata extraction.
+/// Replaces SRM MetadataReader dependency for hot reload baseline creation.
+/// Parses PE/CLI metadata headers to extract heap sizes and table row counts.
+///
+/// This module provides a pure F# implementation for reading the minimum metadata
+/// needed to create an FSharpEmitBaseline, without requiring System.Reflection.Metadata.
+///
+/// References:
+/// - ECMA-335 II.24 (Metadata physical layout)
+/// - Roslyn DeltaMetadataWriter.cs for heap offset handling
+module internal FSharp.Compiler.AbstractIL.ILBaselineReader
+
+open System
+open System.Collections.Immutable
+open System.IO
+open System.Reflection.PortableExecutable
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+
+/// Read a little-endian 16-bit integer from bytes at offset.
+let private readUInt16 (bytes: byte[]) (offset: int) =
+ uint16 bytes.[offset] ||| (uint16 bytes.[offset + 1] <<< 8)
+
+/// Read a little-endian 32-bit integer from bytes at offset.
+let private readInt32 (bytes: byte[]) (offset: int) =
+ int bytes.[offset]
+ ||| (int bytes.[offset + 1] <<< 8)
+ ||| (int bytes.[offset + 2] <<< 16)
+ ||| (int bytes.[offset + 3] <<< 24)
+
+/// Read a little-endian unsigned 64-bit integer from bytes at offset.
+let readUInt64 (bytes: byte[]) (offset: int) =
+ uint64 (uint32 (readInt32 bytes offset))
+ ||| (uint64 (uint32 (readInt32 bytes (offset + 4))) <<< 32)
+
+/// Number of metadata tables per ECMA-335.
+let private tableCount = 64
+
+/// Computes the first table-row offset from the table header and parsed row counts.
+let tableDataStart tablesOffset (valid: uint64) (rowCounts: int[]) =
+ if rowCounts.Length <> tableCount then
+ invalidArg (nameof rowCounts) $"metadata table row counts must contain {tableCount} entries"
+
+ let mutable rowCountSize = 0
+
+ for i in 0..63 do
+ // ECMA-335 II.24.2.6 stores one row-count cell for every Valid bit,
+ // including tables whose declared row count is zero.
+ if (valid &&& (1UL <<< i)) <> 0UL then
+ rowCountSize <- rowCountSize + 4
+
+ tablesOffset + 24 + rowCountSize
+
+/// Find the CLI metadata root in PE file bytes.
+/// Returns the offset to the metadata root, or None if not found.
+let private findMetadataRoot (bytes: byte[]) : int option =
+ // Check DOS header magic
+ if bytes.Length < 64 || bytes.[0] <> 0x4Duy || bytes.[1] <> 0x5Auy then
+ None
+ else
+ // e_lfanew at offset 0x3C points to PE signature
+ let peOffset = readInt32 bytes 0x3C
+
+ if peOffset < 0 || peOffset + 24 > bytes.Length then
+ None
+ else if
+ // Check PE signature "PE\0\0"
+ bytes.[peOffset] <> 0x50uy
+ || bytes.[peOffset + 1] <> 0x45uy
+ || bytes.[peOffset + 2] <> 0uy
+ || bytes.[peOffset + 3] <> 0uy
+ then
+ None
+ else
+ // COFF header at peOffset + 4
+ let coffHeader = peOffset + 4
+ let sizeOfOptionalHeader = int (readUInt16 bytes (coffHeader + 16))
+ let optionalHeader = coffHeader + 20
+
+ // PE32 vs PE32+ - check magic
+ let magic = readUInt16 bytes optionalHeader
+ let isPE32Plus = magic = 0x20Bus
+
+ // CLI header RVA is in data directory entry 14 (0-indexed)
+ // PE32: starts at optionalHeader + 96; PE32+: starts at optionalHeader + 112
+ let dataDirectoryStart =
+ if isPE32Plus then
+ optionalHeader + 112
+ else
+ optionalHeader + 96
+
+ let cliHeaderRVA = readInt32 bytes (dataDirectoryStart + 14 * 8)
+
+ if cliHeaderRVA = 0 then
+ None
+ else
+ // Convert RVA to file offset using section headers
+ let numberOfSections = int (readUInt16 bytes (coffHeader + 2))
+ let sectionHeadersStart = optionalHeader + sizeOfOptionalHeader
+
+ let rec findSection sectionIndex =
+ if sectionIndex >= numberOfSections then
+ None
+ else
+ let sectionOffset = sectionHeadersStart + sectionIndex * 40
+ let virtualAddress = readInt32 bytes (sectionOffset + 12)
+ let virtualSize = readInt32 bytes (sectionOffset + 8)
+ let pointerToRawData = readInt32 bytes (sectionOffset + 20)
+
+ if cliHeaderRVA >= virtualAddress && cliHeaderRVA < virtualAddress + virtualSize then
+ let cliHeaderOffset = cliHeaderRVA - virtualAddress + pointerToRawData
+ // CLI header contains MetaData RVA at offset 8
+ let metadataRVA = readInt32 bytes (cliHeaderOffset + 8)
+ // Convert metadata RVA to file offset
+ Some(metadataRVA - virtualAddress + pointerToRawData)
+ else
+ findSection (sectionIndex + 1)
+
+ findSection 0
+
+/// Stream header information.
+type private StreamHeader =
+ { Offset: int; Size: int; Name: string }
+
+/// Parse stream headers from metadata root.
+let private parseStreamHeaders (bytes: byte[]) (metadataRoot: int) : StreamHeader list =
+ // Metadata root signature at offset 0
+ let signature = readInt32 bytes metadataRoot
+
+ if signature <> 0x424A5342 then // "BSJB"
+ []
+ else
+ // Version string length at offset 12
+ let versionLength = readInt32 bytes (metadataRoot + 12)
+ let paddedVersionLength = (versionLength + 3) &&& ~~~3
+
+ // Number of streams at offset 16 + paddedVersionLength + 2
+ let streamsOffset = metadataRoot + 16 + paddedVersionLength
+ let numberOfStreams = int (readUInt16 bytes (streamsOffset + 2))
+
+ // Stream headers start at streamsOffset + 4
+ let mutable currentOffset = streamsOffset + 4
+ let headers = ResizeArray()
+
+ for _ in 1..numberOfStreams do
+ let offset = readInt32 bytes currentOffset
+ let size = readInt32 bytes (currentOffset + 4)
+
+ // Read null-terminated stream name (padded to 4-byte boundary)
+ let mutable nameEnd = currentOffset + 8
+
+ while bytes.[nameEnd] <> 0uy do
+ nameEnd <- nameEnd + 1
+
+ let name =
+ System.Text.Encoding.ASCII.GetString(bytes, currentOffset + 8, nameEnd - currentOffset - 8)
+
+ let paddedNameLength = ((nameEnd - currentOffset - 8 + 1) + 3) &&& ~~~3
+
+ headers.Add(
+ {
+ Offset = metadataRoot + offset
+ Size = size
+ Name = name
+ }
+ )
+
+ currentOffset <- currentOffset + 8 + paddedNameLength
+
+ headers |> Seq.toList
+
+/// Find a stream by name.
+let private findStream (headers: StreamHeader list) (name: string) : StreamHeader option =
+ headers |> List.tryFind (fun h -> h.Name = name)
+
+/// Parse table row counts from the #~ or #- stream.
+/// Returns the heap-size flags, row counts, tables-stream offset, and Valid mask.
+let private parseTablesStream (bytes: byte[]) (tablesStream: StreamHeader) : byte * int[] * int * uint64 =
+ let offset = tablesStream.Offset
+
+ // Header structure:
+ // 0-3: Reserved (0)
+ // 4: MajorVersion
+ // 5: MinorVersion
+ // 6: HeapSizes byte
+ // 7: Reserved
+ // 8-15: Valid (bitmask of present tables)
+ // 16-23: Sorted (bitmask of sorted tables)
+ // 24+: Row counts for present tables
+
+ let heapSizes = bytes.[offset + 6]
+ let valid = readUInt64 bytes (offset + 8)
+
+ let rowCounts = Array.zeroCreate tableCount
+ let mutable rowCountOffset = offset + 24
+
+ for i in 0..63 do
+ if (valid &&& (1UL <<< i)) <> 0UL then
+ let rowCount = readInt32 bytes rowCountOffset
+
+ if rowCount < 0 then
+ invalidArg (nameof bytes) "metadata table row counts must be non-negative"
+
+ rowCounts.[i] <- rowCount
+ rowCountOffset <- rowCountOffset + 4
+
+ heapSizes, rowCounts, offset, valid
+
+/// Extract metadata snapshot from PE file bytes.
+/// This replaces metadataSnapshotFromReader for hot reload baseline creation.
+let metadataSnapshotFromBytes (bytes: byte[]) : MetadataSnapshot option =
+ match findMetadataRoot bytes with
+ | None -> None
+ | Some metadataRoot ->
+ let streamHeaders = parseStreamHeaders bytes metadataRoot
+
+ // Find required streams
+ let stringsStream = findStream streamHeaders "#Strings"
+ let userStringsStream = findStream streamHeaders "#US"
+ let blobStream = findStream streamHeaders "#Blob"
+ let guidStream = findStream streamHeaders "#GUID"
+
+ let tablesStream =
+ findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
+
+ match tablesStream with
+ | None -> None
+ | Some tables ->
+ let _, rowCounts, _, _ = parseTablesStream bytes tables
+
+ // SRM's StringHeap trims the #Strings alignment padding down to a single
+ // terminating zero (StringHeap.TrimEnd), and EnC heap aggregation (runtime,
+ // MetadataAggregator, Roslyn EmitBaseline) places generation-1 strings right
+ // after that TRIMMED size. The baseline snapshot must use the same virtual size
+ // or every delta-heap string reference is shifted by the padding bytes.
+ // #US/#Blob/#GUID are not trimmed by SRM and keep the stream header size.
+ let trimmedStringHeapSize =
+ match stringsStream with
+ | None -> 0
+ | Some stream ->
+ if stream.Size = 0 then
+ 0
+ else
+ let last = stream.Offset + stream.Size - 1
+ let mutable i = last
+
+ while i >= stream.Offset && bytes.[i] = 0uy do
+ i <- i - 1
+
+ if i = last then
+ // No trailing zero: malformed but mirror SRM and keep the raw size.
+ stream.Size
+ else
+ // Keep one terminating zero after the last non-zero byte.
+ i - stream.Offset + 2
+
+ let heapSizeInfo =
+ {
+ StringHeapSize = trimmedStringHeapSize
+ UserStringHeapSize = userStringsStream |> Option.map (fun s -> s.Size) |> Option.defaultValue 0
+ BlobHeapSize = blobStream |> Option.map (fun s -> s.Size) |> Option.defaultValue 0
+ GuidHeapSize = guidStream |> Option.map (fun s -> s.Size) |> Option.defaultValue 0
+ }
+
+ Some
+ {
+ HeapSizes = heapSizeInfo
+ TableRowCounts = rowCounts
+ GuidHeapStart = heapSizeInfo.GuidHeapSize
+ }
+
+/// Read GUID from #GUID stream at 1-based index.
+let readGuidFromBytes (bytes: byte[]) (guidIndex: int) : Guid option =
+ if guidIndex <= 0 then
+ None
+ else
+ match findMetadataRoot bytes with
+ | None -> None
+ | Some metadataRoot ->
+ let streamHeaders = parseStreamHeaders bytes metadataRoot
+
+ match findStream streamHeaders "#GUID" with
+ | None -> None
+ | Some guidStream ->
+ // GUID indices are 1-based; each GUID is 16 bytes
+ let offset = guidStream.Offset + (guidIndex - 1) * 16
+ let streamEnd = int64 guidStream.Offset + int64 guidStream.Size
+ let guidEnd = int64 offset + 16L
+
+ if
+ guidStream.Offset < 0
+ || guidStream.Size < 0
+ || streamEnd > int64 bytes.Length
+ || offset < guidStream.Offset
+ || guidEnd > streamEnd
+ then
+ None
+ else
+ let guidBytes = bytes.[offset .. offset + 15]
+ Some(System.Guid(guidBytes))
+
+/// Reads the portable CodeView content ID embedded in a PE debug directory.
+let readCodeViewContentIdFromBytes (bytes: byte[]) : byte[] option =
+ try
+ use peReader = new PEReader(ImmutableArray.CreateRange bytes)
+
+ peReader.ReadDebugDirectory()
+ |> Seq.tryFind (fun entry -> entry.IsPortableCodeView)
+ |> Option.map (fun entry ->
+ let data = peReader.ReadCodeViewDebugDirectoryData entry
+ let contentId = Array.zeroCreate 20
+ data.Guid.ToByteArray().CopyTo(contentId, 0)
+ BitConverter.GetBytes(entry.Stamp).CopyTo(contentId, 16)
+ contentId)
+ with
+ | :? BadImageFormatException
+ | :? IOException
+ | :? InvalidOperationException -> None
+
+// ============================================================================
+// Table row reading infrastructure
+// ============================================================================
+
+/// Table indices per ECMA-335 II.22
+module private TableIndices =
+ let Module = 0
+ let TypeRef = 1
+ let TypeDef = 2
+ let FieldPtr = 3
+ let Field = 4
+ let MethodPtr = 5
+ let MethodDef = 6
+ let ParamPtr = 7
+ let Param = 8
+ let InterfaceImpl = 9
+ let MemberRef = 10
+ let Constant = 11
+ let CustomAttribute = 12
+ let FieldMarshal = 13
+ let DeclSecurity = 14
+ let ClassLayout = 15
+ let FieldLayout = 16
+ let StandAloneSig = 17
+ let EventMap = 18
+ let EventPtr = 19
+ let Event = 20
+ let PropertyMap = 21
+ let PropertyPtr = 22
+ let Property = 23
+ let MethodSemantics = 24
+ let MethodImpl = 25
+ let ModuleRef = 26
+ let TypeSpec = 27
+ let ImplMap = 28
+ let FieldRVA = 29
+ let Assembly = 32
+ let AssemblyRef = 35
+ let File = 38
+ let ExportedType = 39
+ let ManifestResource = 40
+ let NestedClass = 41
+ let GenericParam = 42
+ let MethodSpec = 43
+ let GenericParamConstraint = 44
+
+/// Parsed metadata context for reading table rows.
+/// Internal (not private): tiny reader members like TypeRefCount get cross-module
+/// inlined in Release builds, and inlined code referencing a module-private type
+/// fails CLR visibility checks at runtime (observed as an access violation on
+/// RowCounts from HotReloadBaseline's state machines).
+type internal MetadataContext =
+ {
+ Bytes: byte[]
+ HeapSizes: byte
+ RowCounts: int[]
+ TablesStart: int
+ StringIndexSize: int
+ GuidIndexSize: int
+ BlobIndexSize: int
+ StringsStreamOffset: int
+ StringsStreamSize: int
+ BlobStreamOffset: int
+ }
+
+/// Calculate index size for a simple table reference (2 if <=65535 rows, else 4).
+let private tableIndexSize (rowCounts: int[]) (tableIndex: int) =
+ if rowCounts.[tableIndex] <= 65535 then 2 else 4
+
+/// Calculate index size for a coded index (multiple possible tables).
+/// The tag takes some bits, so max row must fit in remaining bits.
+let private codedIndexSize (rowCounts: int[]) (tableIndices: int[]) (tagBits: int) =
+ let maxRows =
+ tableIndices
+ |> Array.map (fun i -> if i < 64 then rowCounts.[i] else 0)
+ |> Array.max
+
+ let maxValue = (maxRows <<< tagBits) ||| ((1 <<< tagBits) - 1)
+ if maxValue <= 65535 then 2 else 4
+
+/// ResolutionScope coded index: Module(0), ModuleRef(1), AssemblyRef(2), TypeRef(3) - 2 tag bits
+let private resolutionScopeSize (rowCounts: int[]) =
+ codedIndexSize
+ rowCounts
+ [|
+ TableIndices.Module
+ TableIndices.ModuleRef
+ TableIndices.AssemblyRef
+ TableIndices.TypeRef
+ |]
+ 2
+
+/// TypeDefOrRef coded index: TypeDef(0), TypeRef(1), TypeSpec(2) - 2 tag bits
+let private typeDefOrRefSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.TypeDef; TableIndices.TypeRef; TableIndices.TypeSpec |] 2
+
+/// HasConstant coded index - 2 tag bits
+let private hasConstantSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.Field; TableIndices.Param; TableIndices.Property |] 2
+
+/// HasCustomAttribute coded index - 5 tag bits (22 possible tables, ECMA-335 II.24.2.6)
+let private hasCustomAttributeSize (rowCounts: int[]) =
+ let tables =
+ [|
+ TableIndices.MethodDef
+ TableIndices.Field
+ TableIndices.TypeRef
+ TableIndices.TypeDef
+ TableIndices.Param
+ TableIndices.InterfaceImpl
+ TableIndices.MemberRef
+ TableIndices.Module
+ TableIndices.DeclSecurity
+ TableIndices.Property
+ TableIndices.Event
+ TableIndices.StandAloneSig
+ TableIndices.ModuleRef
+ TableIndices.TypeSpec
+ TableIndices.Assembly
+ TableIndices.AssemblyRef
+ TableIndices.File
+ TableIndices.ExportedType
+ TableIndices.ManifestResource
+ TableIndices.GenericParam
+ TableIndices.GenericParamConstraint
+ TableIndices.MethodSpec
+ |]
+
+ codedIndexSize rowCounts tables 5
+
+/// HasFieldMarshal coded index - 1 tag bit
+let private hasFieldMarshalSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.Field; TableIndices.Param |] 1
+
+/// HasDeclSecurity coded index - 2 tag bits
+let private hasDeclSecuritySize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.TypeDef; TableIndices.MethodDef; TableIndices.Assembly |] 2
+
+/// MemberRefParent coded index - 3 tag bits
+let private memberRefParentSize (rowCounts: int[]) =
+ codedIndexSize
+ rowCounts
+ [|
+ TableIndices.TypeDef
+ TableIndices.TypeRef
+ TableIndices.ModuleRef
+ TableIndices.MethodDef
+ TableIndices.TypeSpec
+ |]
+ 3
+
+/// HasSemantics coded index - 1 tag bit
+let private hasSemanticsSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.Event; TableIndices.Property |] 1
+
+/// MethodDefOrRef coded index - 1 tag bit
+let private methodDefOrRefSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.MethodDef; TableIndices.MemberRef |] 1
+
+/// MemberForwarded coded index - 1 tag bit
+let private memberForwardedSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.Field; TableIndices.MethodDef |] 1
+
+/// Implementation coded index - 2 tag bits
+let private implementationSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.File; TableIndices.AssemblyRef; TableIndices.ExportedType |] 2
+
+/// CustomAttributeType coded index - 3 tag bits
+let private customAttributeTypeSize (rowCounts: int[]) =
+ // Only MethodDef(2) and MemberRef(3) are used
+ codedIndexSize rowCounts [| 0; 0; TableIndices.MethodDef; TableIndices.MemberRef; 0 |] 3
+
+/// TypeOrMethodDef coded index - 1 tag bit
+let private typeOrMethodDefSize (rowCounts: int[]) =
+ codedIndexSize rowCounts [| TableIndices.TypeDef; TableIndices.MethodDef |] 1
+
+/// Calculate row size for each table per ECMA-335 II.22.
+let private calculateTableRowSizes (ctx: MetadataContext) : int[] =
+ let rc = ctx.RowCounts
+ let strIdx = ctx.StringIndexSize
+ let guidIdx = ctx.GuidIndexSize
+ let blobIdx = ctx.BlobIndexSize
+
+ let sizes = Array.zeroCreate tableCount
+
+ // Module: Generation(2) + Name(str) + Mvid(guid) + EncId(guid) + EncBaseId(guid)
+ sizes.[0] <- 2 + strIdx + guidIdx + guidIdx + guidIdx
+
+ // TypeRef: ResolutionScope(coded) + TypeName(str) + TypeNamespace(str)
+ sizes.[1] <- resolutionScopeSize rc + strIdx + strIdx
+
+ // TypeDef: Flags(4) + TypeName(str) + TypeNamespace(str) + Extends(TypeDefOrRef) + FieldList(Field) + MethodList(MethodDef)
+ sizes.[2] <-
+ 4
+ + strIdx
+ + strIdx
+ + typeDefOrRefSize rc
+ + tableIndexSize rc 4
+ + tableIndexSize rc 6
+
+ // Field: Flags(2) + Name(str) + Signature(blob)
+ sizes.[4] <- 2 + strIdx + blobIdx
+
+ // MethodDef: RVA(4) + ImplFlags(2) + Flags(2) + Name(str) + Signature(blob) + ParamList(Param)
+ sizes.[6] <- 4 + 2 + 2 + strIdx + blobIdx + tableIndexSize rc 8
+
+ // Param: Flags(2) + Sequence(2) + Name(str)
+ sizes.[8] <- 2 + 2 + strIdx
+
+ // InterfaceImpl: Class(TypeDef) + Interface(TypeDefOrRef)
+ // Missing this size silently shifted every later table's offset for assemblies with
+ // interface implementations (e.g. anonymous records implementing IEquatable).
+ sizes.[9] <- tableIndexSize rc 2 + typeDefOrRefSize rc
+
+ // MemberRef: Class(MemberRefParent) + Name(str) + Signature(blob)
+ sizes.[10] <- memberRefParentSize rc + strIdx + blobIdx
+
+ // Constant: Type(2) + Parent(HasConstant) + Value(blob)
+ sizes.[11] <- 2 + hasConstantSize rc + blobIdx
+
+ // CustomAttribute: Parent(HasCustomAttribute) + Type(CustomAttributeType) + Value(blob)
+ sizes.[12] <- hasCustomAttributeSize rc + customAttributeTypeSize rc + blobIdx
+
+ // FieldMarshal: Parent(HasFieldMarshal) + NativeType(blob)
+ sizes.[13] <- hasFieldMarshalSize rc + blobIdx
+
+ // DeclSecurity: Action(2) + Parent(HasDeclSecurity) + PermissionSet(blob)
+ sizes.[14] <- 2 + hasDeclSecuritySize rc + blobIdx
+
+ // ClassLayout: PackingSize(2) + ClassSize(4) + Parent(TypeDef)
+ sizes.[15] <- 2 + 4 + tableIndexSize rc 2
+
+ // FieldLayout: Offset(4) + Field(Field)
+ sizes.[16] <- 4 + tableIndexSize rc 4
+
+ // StandAloneSig: Signature(blob)
+ sizes.[17] <- blobIdx
+
+ // EventMap: Parent(TypeDef) + EventList(Event)
+ sizes.[18] <- tableIndexSize rc 2 + tableIndexSize rc 20
+
+ // Event: EventFlags(2) + Name(str) + EventType(TypeDefOrRef)
+ sizes.[20] <- 2 + strIdx + typeDefOrRefSize rc
+
+ // PropertyMap: Parent(TypeDef) + PropertyList(Property)
+ sizes.[21] <- tableIndexSize rc 2 + tableIndexSize rc 23
+
+ // Property: Flags(2) + Name(str) + Type(blob)
+ sizes.[23] <- 2 + strIdx + blobIdx
+
+ // MethodSemantics: Semantics(2) + Method(MethodDef) + Association(HasSemantics)
+ sizes.[24] <- 2 + tableIndexSize rc 6 + hasSemanticsSize rc
+
+ // MethodImpl: Class(TypeDef) + MethodBody(MethodDefOrRef) + MethodDeclaration(MethodDefOrRef)
+ sizes.[25] <- tableIndexSize rc 2 + methodDefOrRefSize rc + methodDefOrRefSize rc
+
+ // ModuleRef: Name(str)
+ sizes.[26] <- strIdx
+
+ // TypeSpec: Signature(blob)
+ sizes.[27] <- blobIdx
+
+ // ImplMap: MappingFlags(2) + MemberForwarded(MemberForwarded) + ImportName(str) + ImportScope(ModuleRef)
+ sizes.[28] <- 2 + memberForwardedSize rc + strIdx + tableIndexSize rc 26
+
+ // FieldRVA: RVA(4) + Field(Field)
+ sizes.[29] <- 4 + tableIndexSize rc 4
+
+ // Assembly: HashAlgId(4) + MajorVersion(2) + MinorVersion(2) + BuildNumber(2) + RevisionNumber(2) +
+ // Flags(4) + PublicKey(blob) + Name(str) + Culture(str)
+ sizes.[32] <- 4 + 2 + 2 + 2 + 2 + 4 + blobIdx + strIdx + strIdx
+
+ // AssemblyRef: MajorVersion(2) + MinorVersion(2) + BuildNumber(2) + RevisionNumber(2) +
+ // Flags(4) + PublicKeyOrToken(blob) + Name(str) + Culture(str) + HashValue(blob)
+ sizes.[35] <- 2 + 2 + 2 + 2 + 4 + blobIdx + strIdx + strIdx + blobIdx
+
+ // File: Flags(4) + Name(str) + HashValue(blob)
+ sizes.[38] <- 4 + strIdx + blobIdx
+
+ // ExportedType: Flags(4) + TypeDefId(4) + TypeName(str) + TypeNamespace(str) + Implementation(Implementation)
+ sizes.[39] <- 4 + 4 + strIdx + strIdx + implementationSize rc
+
+ // ManifestResource: Offset(4) + Flags(4) + Name(str) + Implementation(Implementation)
+ sizes.[40] <- 4 + 4 + strIdx + implementationSize rc
+
+ // NestedClass: NestedClass(TypeDef) + EnclosingClass(TypeDef)
+ sizes.[41] <- tableIndexSize rc 2 + tableIndexSize rc 2
+
+ // GenericParam: Number(2) + Flags(2) + Owner(TypeOrMethodDef) + Name(str)
+ sizes.[42] <- 2 + 2 + typeOrMethodDefSize rc + strIdx
+
+ // MethodSpec: Method(MethodDefOrRef) + Instantiation(blob)
+ sizes.[43] <- methodDefOrRefSize rc + blobIdx
+
+ // GenericParamConstraint: Owner(GenericParam) + Constraint(TypeDefOrRef)
+ sizes.[44] <- tableIndexSize rc 42 + typeDefOrRefSize rc
+
+ sizes
+
+/// Calculate the byte offset where each table starts within the tables stream.
+let private calculateTableOffsets (ctx: MetadataContext) (rowSizes: int[]) : int[] =
+ let offsets = Array.zeroCreate tableCount
+ let mutable currentOffset = ctx.TablesStart
+
+ for i in 0 .. tableCount - 1 do
+ offsets.[i] <- currentOffset
+ currentOffset <- currentOffset + rowSizes.[i] * ctx.RowCounts.[i]
+
+ offsets
+
+/// Read a heap index (2 or 4 bytes) from the given offset.
+let private readHeapIndex (bytes: byte[]) (offset: int) (indexSize: int) =
+ if indexSize = 2 then
+ int (readUInt16 bytes offset)
+ else
+ readInt32 bytes offset
+
+/// Create a metadata context for reading table rows.
+let private createMetadataContext (bytes: byte[]) : MetadataContext option =
+ match findMetadataRoot bytes with
+ | None -> None
+ | Some metadataRoot ->
+ let streamHeaders = parseStreamHeaders bytes metadataRoot
+
+ let tablesStreamOpt =
+ findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
+
+ match tablesStreamOpt with
+ | None -> None
+ | Some tablesStream ->
+ let heapSizes, rowCounts, tablesOffset, valid = parseTablesStream bytes tablesStream
+
+ let pointerTables =
+ [|
+ TableIndices.FieldPtr
+ TableIndices.MethodPtr
+ TableIndices.ParamPtr
+ TableIndices.EventPtr
+ TableIndices.PropertyPtr
+ |]
+
+ // The #- stream permits pointer-table indirection. This reader consumes the
+ // definition tables directly, so accepting a non-empty pointer table would
+ // associate members with the wrong declaring type.
+ if
+ tablesStream.Name = "#-"
+ && pointerTables |> Array.exists (fun table -> rowCounts.[table] <> 0)
+ then
+ None
+ else
+ let stringsBig = (heapSizes &&& 0x01uy) <> 0uy
+ let guidsBig = (heapSizes &&& 0x02uy) <> 0uy
+ let blobsBig = (heapSizes &&& 0x04uy) <> 0uy
+
+ // Calculate where row data starts (after row count array).
+ let tablesStart = tableDataStart tablesOffset valid rowCounts
+
+ let stringsStream =
+ streamHeaders |> List.tryFind (fun header -> header.Name = "#Strings")
+
+ let blobOffset =
+ streamHeaders
+ |> List.tryFind (fun h -> h.Name = "#Blob")
+ |> Option.map (fun h -> h.Offset)
+ |> Option.defaultValue 0
+
+ Some
+ {
+ Bytes = bytes
+ HeapSizes = heapSizes
+ RowCounts = rowCounts
+ TablesStart = tablesStart
+ StringIndexSize = if stringsBig then 4 else 2
+ GuidIndexSize = if guidsBig then 4 else 2
+ BlobIndexSize = if blobsBig then 4 else 2
+ StringsStreamOffset =
+ stringsStream
+ |> Option.map (fun header -> header.Offset)
+ |> Option.defaultValue 0
+ StringsStreamSize = stringsStream |> Option.map (fun header -> header.Size) |> Option.defaultValue 0
+ BlobStreamOffset = blobOffset
+ }
+
+/// Read a null-terminated string from the #Strings heap.
+let private readStringFromHeap (ctx: MetadataContext) (offset: int) : string =
+ if offset = 0 then
+ ""
+ else
+ let streamStart = int64 ctx.StringsStreamOffset
+ let streamSize = int64 ctx.StringsStreamSize
+ let streamEnd = streamStart + streamSize
+ let stringStart = streamStart + int64 offset
+
+ // Metadata indices are scoped to #Strings, not to the containing PE image.
+ // Failing before decoding prevents malformed offsets from reading an adjacent heap.
+ if
+ offset < 0
+ || streamStart < 0L
+ || streamSize < 0L
+ || streamEnd > int64 ctx.Bytes.Length
+ || stringStart < streamStart
+ || stringStart >= streamEnd
+ then
+ raise (BadImageFormatException("String heap index is outside the #Strings stream."))
+
+ let start = int stringStart
+ let streamEnd = int streamEnd
+ let mutable endPos = start
+
+ while endPos < streamEnd && ctx.Bytes.[endPos] <> 0uy do
+ endPos <- endPos + 1
+
+ if endPos = streamEnd then
+ raise (BadImageFormatException("String heap value is not terminated inside the #Strings stream."))
+
+ System.Text.Encoding.UTF8.GetString(ctx.Bytes, start, endPos - start)
+
+// ============================================================================
+// Table row reading functions
+// ============================================================================
+
+/// TypeDef row data needed for byte-only baseline token maps.
+type TypeDefRowData =
+ {
+ Flags: int
+ NameOffset: int
+ NamespaceOffset: int
+ Extends: int
+ FieldList: int
+ MethodList: int
+ }
+
+/// Read a TypeDef row by 1-based row ID.
+let private readTypeDefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : TypeDefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.TypeDef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.TypeDef]
+ let offset = tableOffsets.[TableIndices.TypeDef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let extendsOffset = offset + 4 + ctx.StringIndexSize + ctx.StringIndexSize
+ let fieldListOffset = extendsOffset + typeDefOrRefSize ctx.RowCounts
+
+ let methodListOffset =
+ fieldListOffset + tableIndexSize ctx.RowCounts TableIndices.Field
+
+ Some
+ {
+ Flags = readInt32 bytes offset
+ NameOffset = readHeapIndex bytes (offset + 4) ctx.StringIndexSize
+ NamespaceOffset = readHeapIndex bytes (offset + 4 + ctx.StringIndexSize) ctx.StringIndexSize
+ Extends = readHeapIndex bytes extendsOffset (typeDefOrRefSize ctx.RowCounts)
+ FieldList = readHeapIndex bytes fieldListOffset (tableIndexSize ctx.RowCounts TableIndices.Field)
+ MethodList = readHeapIndex bytes methodListOffset (tableIndexSize ctx.RowCounts TableIndices.MethodDef)
+ }
+
+/// MethodDef row data needed for baseline cache.
+type MethodDefRowData =
+ {
+ RVA: int
+ ImplFlags: int
+ Flags: int
+ NameOffset: int
+ SignatureOffset: int
+ ParamList: int // First Param row ID (1-based)
+ }
+
+/// Read a MethodDef row by 1-based row ID.
+let private readMethodDefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : MethodDefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.MethodDef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.MethodDef]
+ let offset = tableOffsets.[TableIndices.MethodDef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // MethodDef: RVA(4) + ImplFlags(2) + Flags(2) + Name(str) + Signature(blob) + ParamList(Param)
+ let rva = readInt32 bytes offset
+ let implFlags = int (readUInt16 bytes (offset + 4))
+ let flags = int (readUInt16 bytes (offset + 6))
+ let nameOffset = readHeapIndex bytes (offset + 8) ctx.StringIndexSize
+
+ let sigOffset =
+ readHeapIndex bytes (offset + 8 + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ let paramList =
+ readHeapIndex bytes (offset + 8 + ctx.StringIndexSize + ctx.BlobIndexSize) (tableIndexSize ctx.RowCounts TableIndices.Param)
+
+ Some
+ {
+ RVA = rva
+ ImplFlags = implFlags
+ Flags = flags
+ NameOffset = nameOffset
+ SignatureOffset = sigOffset
+ ParamList = paramList
+ }
+
+/// Param row data.
+type ParamRowData =
+ {
+ Flags: int
+ Sequence: int
+ NameOffset: int
+ }
+
+/// Read a Param row by 1-based row ID.
+let private readParamRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : ParamRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.Param] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.Param]
+ let offset = tableOffsets.[TableIndices.Param] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // Param: Flags(2) + Sequence(2) + Name(str)
+ let flags = int (readUInt16 bytes offset)
+ let sequence = int (readUInt16 bytes (offset + 2))
+ let nameOffset = readHeapIndex bytes (offset + 4) ctx.StringIndexSize
+
+ Some
+ {
+ Flags = flags
+ Sequence = sequence
+ NameOffset = nameOffset
+ }
+
+/// Property row data.
+type PropertyRowData =
+ {
+ Flags: int
+ NameOffset: int
+ SignatureOffset: int
+ }
+
+/// Read a Property row by 1-based row ID.
+let private readPropertyRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : PropertyRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.Property] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.Property]
+ let offset = tableOffsets.[TableIndices.Property] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // Property: Flags(2) + Name(str) + Type(blob)
+ let flags = int (readUInt16 bytes offset)
+ let nameOffset = readHeapIndex bytes (offset + 2) ctx.StringIndexSize
+
+ let sigOffset =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ Some
+ {
+ Flags = flags
+ NameOffset = nameOffset
+ SignatureOffset = sigOffset
+ }
+
+/// Event row data.
+type EventRowData =
+ {
+ Flags: int
+ NameOffset: int
+ EventType: int // Coded index (TypeDefOrRef)
+ }
+
+/// Read an Event row by 1-based row ID.
+let private readEventRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : EventRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.Event] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.Event]
+ let offset = tableOffsets.[TableIndices.Event] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // Event: EventFlags(2) + Name(str) + EventType(TypeDefOrRef)
+ let flags = int (readUInt16 bytes offset)
+ let nameOffset = readHeapIndex bytes (offset + 2) ctx.StringIndexSize
+
+ Some
+ {
+ Flags = flags
+ NameOffset = nameOffset
+ EventType = 0
+ }
+
+/// TypeRef row data.
+type TypeRefRowData =
+ {
+ ResolutionScope: int // Coded index
+ NameOffset: int
+ NamespaceOffset: int
+ }
+
+/// Read a TypeRef row by 1-based row ID.
+let private readTypeRefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : TypeRefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.TypeRef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.TypeRef]
+ let offset = tableOffsets.[TableIndices.TypeRef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let resScopeSize = resolutionScopeSize ctx.RowCounts
+
+ // TypeRef: ResolutionScope(coded) + TypeName(str) + TypeNamespace(str)
+ let resScope = readHeapIndex bytes offset resScopeSize
+ let nameOffset = readHeapIndex bytes (offset + resScopeSize) ctx.StringIndexSize
+
+ let nsOffset =
+ readHeapIndex bytes (offset + resScopeSize + ctx.StringIndexSize) ctx.StringIndexSize
+
+ Some
+ {
+ ResolutionScope = resScope
+ NameOffset = nameOffset
+ NamespaceOffset = nsOffset
+ }
+
+/// MemberRef row data.
+type MemberRefRowData =
+ {
+ /// Raw MemberRefParent coded index value (tag bits 0-2, row id above).
+ Parent: int
+ NameOffset: int
+ SignatureOffset: int
+ }
+
+/// Read a MemberRef row by 1-based row ID.
+let private readMemberRefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : MemberRefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.MemberRef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.MemberRef]
+ let offset = tableOffsets.[TableIndices.MemberRef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let parentSize = memberRefParentSize ctx.RowCounts
+
+ // MemberRef: Class(MemberRefParent) + Name(str) + Signature(blob)
+ let parent = readHeapIndex bytes offset parentSize
+ let nameOffset = readHeapIndex bytes (offset + parentSize) ctx.StringIndexSize
+
+ let sigOffset =
+ readHeapIndex bytes (offset + parentSize + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ Some
+ {
+ Parent = parent
+ NameOffset = nameOffset
+ SignatureOffset = sigOffset
+ }
+
+/// CustomAttribute row data.
+type CustomAttributeRowData =
+ {
+ /// Raw HasCustomAttribute coded index value (tag bits 0-4, row id above).
+ Parent: int
+ /// Raw CustomAttributeType coded index value (tag bits 0-2, row id above).
+ Constructor: int
+ ValueOffset: int
+ }
+
+/// Read a CustomAttribute row by 1-based row ID.
+let private readCustomAttributeRow
+ (ctx: MetadataContext)
+ (rowSizes: int[])
+ (tableOffsets: int[])
+ (rowId: int)
+ : CustomAttributeRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.CustomAttribute] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.CustomAttribute]
+ let offset = tableOffsets.[TableIndices.CustomAttribute] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let parentSize = hasCustomAttributeSize ctx.RowCounts
+ let ctorSize = customAttributeTypeSize ctx.RowCounts
+
+ // CustomAttribute: Parent(HasCustomAttribute) + Type(CustomAttributeType) + Value(blob)
+ let parent = readHeapIndex bytes offset parentSize
+ let ctor = readHeapIndex bytes (offset + parentSize) ctorSize
+
+ let valueOffset =
+ readHeapIndex bytes (offset + parentSize + ctorSize) ctx.BlobIndexSize
+
+ Some
+ {
+ Parent = parent
+ Constructor = ctor
+ ValueOffset = valueOffset
+ }
+
+/// Read a TypeSpec row by 1-based row ID; the row is a single #Blob signature column.
+let private readTypeSpecRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : int option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.TypeSpec] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.TypeSpec]
+ let offset = tableOffsets.[TableIndices.TypeSpec] + (rowId - 1) * rowSize
+ Some(readHeapIndex ctx.Bytes offset ctx.BlobIndexSize)
+
+/// Read a length-prefixed blob (ECMA-335 II.24.2.4 compressed length) from the #Blob heap.
+let private readBlobFromHeap (ctx: MetadataContext) (offset: int) : byte[] =
+ if offset <= 0 then
+ Array.empty
+ else
+ let start = ctx.BlobStreamOffset + offset
+ let b0 = int ctx.Bytes.[start]
+
+ let length, headerSize =
+ if b0 &&& 0x80 = 0 then
+ b0, 1
+ elif b0 &&& 0xC0 = 0x80 then
+ (((b0 &&& 0x3F) <<< 8) ||| int ctx.Bytes.[start + 1]), 2
+ else
+ (((b0 &&& 0x1F) <<< 24)
+ ||| (int ctx.Bytes.[start + 1] <<< 16)
+ ||| (int ctx.Bytes.[start + 2] <<< 8)
+ ||| int ctx.Bytes.[start + 3]),
+ 4
+
+ if length = 0 then
+ Array.empty
+ else
+ ctx.Bytes.[start + headerSize .. start + headerSize + length - 1]
+
+/// AssemblyRef row data.
+type AssemblyRefRowData =
+ {
+ MajorVersion: int
+ MinorVersion: int
+ BuildNumber: int
+ RevisionNumber: int
+ Flags: int
+ PublicKeyOrToken: int // Blob offset
+ NameOffset: int
+ Culture: int // String offset
+ HashValue: int // Blob offset
+ }
+
+/// Read an AssemblyRef row by 1-based row ID.
+let private readAssemblyRefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : AssemblyRefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.AssemblyRef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.AssemblyRef]
+ let offset = tableOffsets.[TableIndices.AssemblyRef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // AssemblyRef: MajorVersion(2) + MinorVersion(2) + BuildNumber(2) + RevisionNumber(2) +
+ // Flags(4) + PublicKeyOrToken(blob) + Name(str) + Culture(str) + HashValue(blob)
+ let major = int (readUInt16 bytes offset)
+ let minor = int (readUInt16 bytes (offset + 2))
+ let build = int (readUInt16 bytes (offset + 4))
+ let rev = int (readUInt16 bytes (offset + 6))
+ let flags = readInt32 bytes (offset + 8)
+ let pkOffset = readHeapIndex bytes (offset + 12) ctx.BlobIndexSize
+
+ let nameOffset =
+ readHeapIndex bytes (offset + 12 + ctx.BlobIndexSize) ctx.StringIndexSize
+
+ let cultureOffset =
+ readHeapIndex bytes (offset + 12 + ctx.BlobIndexSize + ctx.StringIndexSize) ctx.StringIndexSize
+
+ let hashOffset =
+ readHeapIndex bytes (offset + 12 + ctx.BlobIndexSize + ctx.StringIndexSize + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ Some
+ {
+ MajorVersion = major
+ MinorVersion = minor
+ BuildNumber = build
+ RevisionNumber = rev
+ Flags = flags
+ PublicKeyOrToken = pkOffset
+ NameOffset = nameOffset
+ Culture = cultureOffset
+ HashValue = hashOffset
+ }
+
+/// Module row data (including name offset).
+type ModuleRowData =
+ {
+ Generation: int
+ NameOffset: int
+ MvidIndex: int
+ EncIdIndex: int
+ EncBaseIdIndex: int
+ }
+
+/// Read the Module row (there's only one, row 1).
+let private readModuleRow (ctx: MetadataContext) (_rowSizes: int[]) (tableOffsets: int[]) : ModuleRowData option =
+ if ctx.RowCounts.[TableIndices.Module] < 1 then
+ None
+ else
+ let offset = tableOffsets.[TableIndices.Module]
+ let bytes = ctx.Bytes
+
+ // Module: Generation(2) + Name(str) + Mvid(guid) + EncId(guid) + EncBaseId(guid)
+ let generation = int (readUInt16 bytes offset)
+ let nameOffset = readHeapIndex bytes (offset + 2) ctx.StringIndexSize
+
+ let mvidIndex =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize) ctx.GuidIndexSize
+
+ let encIdIndex =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize + ctx.GuidIndexSize) ctx.GuidIndexSize
+
+ let encBaseIdIndex =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize + ctx.GuidIndexSize + ctx.GuidIndexSize) ctx.GuidIndexSize
+
+ Some
+ {
+ Generation = generation
+ NameOffset = nameOffset
+ MvidIndex = mvidIndex
+ EncIdIndex = encIdIndex
+ EncBaseIdIndex = encBaseIdIndex
+ }
+
+// ============================================================================
+// Public API for baseline metadata extraction
+// ============================================================================
+
+/// Baseline metadata reader that provides access to table rows without SRM.
+type BaselineMetadataReader private (ctx: MetadataContext, rowSizes: int[], tableOffsets: int[]) =
+
+ /// Create a reader from PE file bytes.
+ static member Create(bytes: byte[]) : BaselineMetadataReader option =
+ match createMetadataContext bytes with
+ | None -> None
+ | Some ctx ->
+ let rowSizes = calculateTableRowSizes ctx
+ let tableOffsets = calculateTableOffsets ctx rowSizes
+ Some(BaselineMetadataReader(ctx, rowSizes, tableOffsets))
+
+ /// Get the table row counts.
+ member _.RowCounts = ctx.RowCounts
+
+ /// Get the TypeDef row count.
+ member _.TypeDefCount = ctx.RowCounts.[TableIndices.TypeDef]
+
+ /// Read a TypeDef row by 1-based row ID.
+ member _.GetTypeDef(rowId: int) =
+ readTypeDefRow ctx rowSizes tableOffsets rowId
+
+ /// Read a MethodDef row by 1-based row ID.
+ member _.GetMethodDef(rowId: int) =
+ readMethodDefRow ctx rowSizes tableOffsets rowId
+
+ /// Get the MethodDef row range owned by a TypeDef row.
+ member this.GetTypeMethodRange(typeRowId: int) : (int * int) option =
+ match this.GetTypeDef(typeRowId) with
+ | None -> None
+ | Some typeDef ->
+ let firstMethod = typeDef.MethodList
+
+ let lastMethod =
+ if typeRowId < ctx.RowCounts.[TableIndices.TypeDef] then
+ match this.GetTypeDef(typeRowId + 1) with
+ | Some next -> next.MethodList - 1
+ | None -> ctx.RowCounts.[TableIndices.MethodDef]
+ else
+ ctx.RowCounts.[TableIndices.MethodDef]
+
+ if firstMethod > lastMethod then
+ None
+ else
+ Some(firstMethod, lastMethod)
+
+ /// Read a Param row by 1-based row ID.
+ member _.GetParam(rowId: int) =
+ readParamRow ctx rowSizes tableOffsets rowId
+
+ /// Get the last param row for a method (based on next method's ParamList or table end).
+ member this.GetMethodParamRange(methodRowId: int) : (int * int) option =
+ match this.GetMethodDef(methodRowId) with
+ | None -> None
+ | Some methodDef ->
+ let firstParam = methodDef.ParamList
+
+ let lastParam =
+ if methodRowId < ctx.RowCounts.[TableIndices.MethodDef] then
+ match this.GetMethodDef(methodRowId + 1) with
+ | Some next -> next.ParamList - 1
+ | None -> ctx.RowCounts.[TableIndices.Param]
+ else
+ ctx.RowCounts.[TableIndices.Param]
+
+ if firstParam > lastParam then
+ None
+ else
+ Some(firstParam, lastParam)
+
+ /// Read a Property row by 1-based row ID.
+ member _.GetProperty(rowId: int) =
+ readPropertyRow ctx rowSizes tableOffsets rowId
+
+ /// Read an Event row by 1-based row ID.
+ member _.GetEvent(rowId: int) =
+ readEventRow ctx rowSizes tableOffsets rowId
+
+ /// Read a TypeRef row by 1-based row ID.
+ member _.GetTypeRef(rowId: int) =
+ readTypeRefRow ctx rowSizes tableOffsets rowId
+
+ /// Read an AssemblyRef row by 1-based row ID.
+ member _.GetAssemblyRef(rowId: int) =
+ readAssemblyRefRow ctx rowSizes tableOffsets rowId
+
+ /// Get the AssemblyRef row count.
+ member _.AssemblyRefCount = ctx.RowCounts.[TableIndices.AssemblyRef]
+
+ /// Get the TypeRef row count.
+ member _.TypeRefCount = ctx.RowCounts.[TableIndices.TypeRef]
+
+ /// Read the Module row.
+ member _.GetModule() = readModuleRow ctx rowSizes tableOffsets
+
+ /// Read a string from the #Strings heap.
+ member _.GetString(offset: int) = readStringFromHeap ctx offset
+
+ /// Read a MemberRef row by 1-based row ID.
+ member _.GetMemberRef(rowId: int) =
+ readMemberRefRow ctx rowSizes tableOffsets rowId
+
+ /// Get the MemberRef row count.
+ member _.MemberRefCount = ctx.RowCounts.[TableIndices.MemberRef]
+
+ /// Read a TypeSpec row's signature blob offset by 1-based row ID.
+ member _.GetTypeSpecSignatureOffset(rowId: int) =
+ readTypeSpecRow ctx rowSizes tableOffsets rowId
+
+ /// Get the TypeSpec row count.
+ member _.TypeSpecCount = ctx.RowCounts.[TableIndices.TypeSpec]
+
+ /// Read a length-prefixed blob from the #Blob heap.
+ member _.GetBlob(offset: int) = readBlobFromHeap ctx offset
+
+ /// Read a CustomAttribute row by 1-based row ID.
+ member _.GetCustomAttributeRow(rowId: int) =
+ readCustomAttributeRow ctx rowSizes tableOffsets rowId
+
+ /// Get the CustomAttribute row count.
+ member _.CustomAttributeCount = ctx.RowCounts.[TableIndices.CustomAttribute]
+
+ /// Decode a HasCustomAttribute coded index to a metadata token.
+ /// Tag bits (5), ECMA-335 II.24.2.6 ordering.
+ member _.DecodeHasCustomAttributeToken(codedIndex: int) : int =
+ let tag = codedIndex &&& 0x1F
+ let rowId = codedIndex >>> 5
+
+ let table =
+ match tag with
+ | 0 -> 0x06 // MethodDef
+ | 1 -> 0x04 // Field
+ | 2 -> 0x01 // TypeRef
+ | 3 -> 0x02 // TypeDef
+ | 4 -> 0x08 // Param
+ | 5 -> 0x09 // InterfaceImpl
+ | 6 -> 0x0A // MemberRef
+ | 7 -> 0x00 // Module
+ | 8 -> 0x0E // DeclSecurity
+ | 9 -> 0x17 // Property
+ | 10 -> 0x14 // Event
+ | 11 -> 0x11 // StandAloneSig
+ | 12 -> 0x1A // ModuleRef
+ | 13 -> 0x1B // TypeSpec
+ | 14 -> 0x20 // Assembly
+ | 15 -> 0x23 // AssemblyRef
+ | 16 -> 0x26 // File
+ | 17 -> 0x27 // ExportedType
+ | 18 -> 0x28 // ManifestResource
+ | 19 -> 0x2A // GenericParam
+ | 20 -> 0x2C // GenericParamConstraint
+ | _ -> 0x2B // MethodSpec
+
+ (table <<< 24) ||| rowId
+
+ /// Decode a CustomAttributeType coded index to a metadata token.
+ /// Tag bits (3): 2=MethodDef, 3=MemberRef.
+ member _.DecodeCustomAttributeTypeToken(codedIndex: int) : int =
+ let tag = codedIndex &&& 0x7
+ let rowId = codedIndex >>> 3
+
+ let table =
+ match tag with
+ | 2 -> 0x06 // MethodDef
+ | _ -> 0x0A // MemberRef
+
+ (table <<< 24) ||| rowId
+
+ /// Decode a MemberRefParent coded index to a metadata token.
+ /// Tag bits (3): 0=TypeDef, 1=TypeRef, 2=ModuleRef, 3=MethodDef, 4=TypeSpec.
+ member _.DecodeMemberRefParentToken(codedIndex: int) : int =
+ let tag = codedIndex &&& 0x7
+ let rowId = codedIndex >>> 3
+
+ let tableIndex =
+ match tag with
+ | 0 -> TableIndices.TypeDef
+ | 1 -> TableIndices.TypeRef
+ | 2 -> TableIndices.ModuleRef
+ | 3 -> TableIndices.MethodDef
+ | 4 -> TableIndices.TypeSpec
+ | _ -> -1
+
+ if tableIndex < 0 then 0 else (tableIndex <<< 24) ||| rowId
+
+ /// Decode ResolutionScope coded index to (table index, row id).
+ /// Tag bits: 0=Module, 1=ModuleRef, 2=AssemblyRef, 3=TypeRef
+ member _.DecodeResolutionScope(codedIndex: int) : (int * int) =
+ let tag = codedIndex &&& 0x3
+ let rowId = codedIndex >>> 2
+
+ let tableIndex =
+ match tag with
+ | 0 -> TableIndices.Module
+ | 1 -> TableIndices.ModuleRef
+ | 2 -> TableIndices.AssemblyRef
+ | 3 -> TableIndices.TypeRef
+ | _ -> -1
+
+ (tableIndex, rowId)
+
+/// Read Module.Mvid GUID from assembly bytes.
+/// Module table row 1 contains the Mvid index.
+let readModuleMvidFromBytes (bytes: byte[]) : System.Guid option =
+ match findMetadataRoot bytes with
+ | None -> None
+ | Some metadataRoot ->
+ let streamHeaders = parseStreamHeaders bytes metadataRoot
+
+ let tablesStreamOpt =
+ findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
+
+ match tablesStreamOpt with
+ | None -> None
+ | Some tablesStream ->
+ let heapSizes, rowCounts, tablesOffset, valid = parseTablesStream bytes tablesStream
+
+ // Check if Module table has at least 1 row
+ if rowCounts.[0] < 1 then
+ None
+ else
+ // Calculate offset to Module row
+ // Module row structure: Generation (2), Name (string), Mvid (guid), EncId (guid), EncBaseId (guid)
+ let stringsBig = (heapSizes &&& 0x01uy) <> 0uy
+ let guidsBig = (heapSizes &&& 0x02uy) <> 0uy
+
+ let stringIndexSize = if stringsBig then 4 else 2
+
+ // Row counts end, then rows start.
+ let tablesStart = tableDataStart tablesOffset valid rowCounts
+
+ // Module table is table 0, so it starts at tablesStart
+ // Module row: Generation (2) + Name (string index) + Mvid (guid index) + EncId (guid index) + EncBaseId (guid index)
+ let mvidOffset = tablesStart + 2 + stringIndexSize
+
+ let mvidIndex =
+ if guidsBig then
+ readInt32 bytes mvidOffset
+ else
+ int (readUInt16 bytes mvidOffset)
+
+ readGuidFromBytes bytes mvidIndex
+
+// ============================================================================
+// Portable PDB Reader
+// ============================================================================
+
+/// Portable PDB table indices (start at 0x30 to avoid collision with ECMA-335 tables)
+module private PdbTableIndices =
+ let Document = 0x30
+ let MethodDebugInformation = 0x31
+ let LocalScope = 0x32
+ let LocalVariable = 0x33
+ let LocalConstant = 0x34
+ let ImportScope = 0x35
+ let StateMachineMethod = 0x36
+ let CustomDebugInformation = 0x37
+
+/// Portable PDB metadata snapshot.
+/// Contains table row counts and entry point info for hot reload baseline.
+type PortablePdbMetadata =
+ {
+ /// Content ID stored in the #Pdb stream.
+ ContentId: byte[]
+ /// Row counts for PDB tables (indexed by PDB table index - 0x30)
+ /// Index 0 = Document, 1 = MethodDebugInformation, etc.
+ TableRowCounts: int[]
+ /// Entry point method token (if present)
+ EntryPointToken: int option
+ }
+
+/// Parse the #Pdb stream to extract PDB-specific info.
+/// The #Pdb stream contains: PdbId (20 bytes), EntryPoint token (4 bytes), ReferencedTypeSystemTables (8 bytes), TypeSystemTableRows (var)
+let private parsePdbStream (bytes: byte[]) (pdbStream: StreamHeader) : int option =
+ if pdbStream.Size < 24 then
+ None
+ else
+ let offset = pdbStream.Offset
+ // PdbId: 20 bytes (GUID + 4 bytes stamp)
+ // EntryPoint: 4 bytes (method def token, or 0 if no entry point)
+ let entryPointToken = readInt32 bytes (offset + 20)
+ if entryPointToken = 0 then None else Some entryPointToken
+
+/// Parse Portable PDB table row counts from the #~ stream.
+/// Portable PDB uses tables 0x30-0x37, but the valid bits are still in position 0x30+.
+let private parsePdbTablesStream (bytes: byte[]) (tablesStream: StreamHeader) : int[] =
+ let offset = tablesStream.Offset
+
+ // Header: Reserved(4) + MajorVersion(1) + MinorVersion(1) + HeapSizes(1) + Reserved(1) + Valid(8) + Sorted(8) + RowCounts(var)
+ let valid = readUInt64 bytes (offset + 8)
+
+ // PDB table row counts (8 tables, indices 0x30-0x37)
+ let pdbRowCounts = Array.zeroCreate 8
+ let mutable rowCountOffset = offset + 24
+
+ for i in 0..63 do
+ if (valid &&& (1UL <<< i)) <> 0UL then
+ let count = readInt32 bytes rowCountOffset
+ // Map table index to PDB array index
+ if i >= 0x30 && i <= 0x37 then
+ pdbRowCounts.[i - 0x30] <- count
+
+ rowCountOffset <- rowCountOffset + 4
+
+ pdbRowCounts
+
+/// Extract metadata from Portable PDB bytes.
+/// This replaces MetadataReaderProvider.FromPortablePdbImage for hot reload baseline creation.
+let readPortablePdbMetadata (pdbBytes: byte[]) : PortablePdbMetadata option =
+ // Portable PDB starts directly with metadata root (no PE header)
+ // Check for BSJB signature at offset 0
+ if pdbBytes.Length < 4 then
+ None
+ else
+ try
+ let signature = readInt32 pdbBytes 0
+
+ if signature <> 0x424A5342 then // "BSJB"
+ None
+ else
+ // Parse from offset 0 (metadata root)
+ let metadataRoot = 0
+ let streamHeaders = parseStreamHeaders pdbBytes metadataRoot
+
+ // Find required streams
+ let tablesStreamOpt =
+ findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
+
+ let pdbStreamOpt = findStream streamHeaders "#Pdb"
+
+ match tablesStreamOpt, pdbStreamOpt with
+ | Some tablesStream, Some pdbStream when pdbStream.Size >= 24 ->
+ let rowCounts = parsePdbTablesStream pdbBytes tablesStream
+ let entryPoint = parsePdbStream pdbBytes pdbStream
+
+ Some
+ {
+ ContentId = pdbBytes.[pdbStream.Offset .. pdbStream.Offset + 19]
+ TableRowCounts = rowCounts
+ EntryPointToken = entryPoint
+ }
+ | _ -> None
+ with
+ | :? System.IndexOutOfRangeException -> None
+ | :? System.ArgumentOutOfRangeException -> None
diff --git a/src/Compiler/AbstractIL/ILMetadataHeaps.fs b/src/Compiler/AbstractIL/ILMetadataHeaps.fs
index 7c6ffe3a86c..5c85fc57489 100644
--- a/src/Compiler/AbstractIL/ILMetadataHeaps.fs
+++ b/src/Compiler/AbstractIL/ILMetadataHeaps.fs
@@ -1,9 +1,8 @@
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
/// Abstractions for metadata heap indexing.
-/// Used by full assembly emission (ilwrite.fs) and intended to also back the delta
-/// emitter tracked in F# hot-reload work (dotnet/fsharp#19941), providing a unified
-/// interface for string, blob, GUID, and user-string heap access.
+/// Used by both full assembly emission (ilwrite.fs) and delta emission (IlxDeltaEmitter.fs)
+/// to provide a unified interface for string, blob, GUID, and user-string heap access.
module internal FSharp.Compiler.AbstractIL.ILMetadataHeaps
/// Abstraction for metadata heap indexing operations.
@@ -33,22 +32,3 @@ module MetadataHeapsExtensions =
match sopt with
| Some s -> this.GetStringHeapIdx s
| None -> 0
-
-///
-/// Records the uncompressed heap sizes produced during metadata emission so that later delta passes
-/// can reason about stream growth.
-///
-///
-/// This type is delta-owned: the full-assembly IL writer (ilwrite.fs) does not currently expose an
-/// equivalent snapshot type on main. Keeping the definition here (rather than growing ilwrite.fsi's
-/// public surface) lets the delta writer stay self-contained; a future PR that wires a baseline
-/// producer into this writer can either reuse this type directly or convert into it at the boundary.
-///
-[]
-type MetadataHeapSizes =
- {
- StringHeapSize: int
- UserStringHeapSize: int
- BlobHeapSize: int
- GuidHeapSize: int
- }
diff --git a/src/Compiler/AbstractIL/ilwrite.fs b/src/Compiler/AbstractIL/ilwrite.fs
index f626e1e56ef..e2e8f918088 100644
--- a/src/Compiler/AbstractIL/ilwrite.fs
+++ b/src/Compiler/AbstractIL/ilwrite.fs
@@ -12,6 +12,7 @@ open FSharp.Compiler.AbstractIL.IL
open FSharp.Compiler.AbstractIL.Diagnostics
open FSharp.Compiler.AbstractIL.BinaryConstants
open FSharp.Compiler.AbstractIL.Support
+open FSharp.Compiler.AbstractIL.ILMetadataHeaps
open Internal.Utilities.Library
open FSharp.Compiler.AbstractIL.StrongNameSign
open FSharp.Compiler.AbstractIL.ILPdbWriter
@@ -163,59 +164,59 @@ module RowElementTags =
let [] Blob = 5
let [] String = 6
let [] SimpleIndexMin = 7
- let SimpleIndex (t : TableName) = assert (t.Index <= 112); SimpleIndexMin + t.Index
+ let SimpleIndex (table: TableName) = assert (table.Index <= 112); SimpleIndexMin + table.Index
let [] SimpleIndexMax = 119
let [] TypeDefOrRefOrSpecMin = 120
- let TypeDefOrRefOrSpec (t: TypeDefOrRefTag) = assert (t.Tag <= 2); TypeDefOrRefOrSpecMin + t.Tag (* + 111 + 1 = 0x70 + 1 = max TableName.Tndex + 1 *)
+ let TypeDefOrRefOrSpec (tag: TypeDefOrRefTag) = assert (tag.Tag <= 2); TypeDefOrRefOrSpecMin + tag.Tag (* + 111 + 1 = 0x70 + 1 = max TableName.Tndex + 1 *)
let [] TypeDefOrRefOrSpecMax = 122
let [] TypeOrMethodDefMin = 123
- let TypeOrMethodDef (t: TypeOrMethodDefTag) = assert (t.Tag <= 1); TypeOrMethodDefMin + t.Tag (* + 2 + 1 = max TypeDefOrRefOrSpec.Tag + 1 *)
+ let TypeOrMethodDef (tag: TypeOrMethodDefTag) = assert (tag.Tag <= 1); TypeOrMethodDefMin + tag.Tag (* + 2 + 1 = max TypeDefOrRefOrSpec.Tag + 1 *)
let [] TypeOrMethodDefMax = 124
let [] HasConstantMin = 125
- let HasConstant (t: HasConstantTag) = assert (t.Tag <= 2); HasConstantMin + t.Tag (* + 1 + 1 = max TypeOrMethodDef.Tag + 1 *)
+ let HasConstant (tag: HasConstantTag) = assert (tag.Tag <= 2); HasConstantMin + tag.Tag (* + 1 + 1 = max TypeOrMethodDef.Tag + 1 *)
let [] HasConstantMax = 127
let [] HasCustomAttributeMin = 128
- let HasCustomAttribute (t: HasCustomAttributeTag) = assert (t.Tag <= 21); HasCustomAttributeMin + t.Tag (* + 2 + 1 = max HasConstant.Tag + 1 *)
+ let HasCustomAttribute (tag: HasCustomAttributeTag) = assert (tag.Tag <= 21); HasCustomAttributeMin + tag.Tag (* + 2 + 1 = max HasConstant.Tag + 1 *)
let [] HasCustomAttributeMax = 149
let [] HasFieldMarshalMin = 150
- let HasFieldMarshal (t: HasFieldMarshalTag) = assert (t.Tag <= 1); HasFieldMarshalMin + t.Tag (* + 21 + 1 = max HasCustomAttribute.Tag + 1 *)
+ let HasFieldMarshal (tag: HasFieldMarshalTag) = assert (tag.Tag <= 1); HasFieldMarshalMin + tag.Tag (* + 21 + 1 = max HasCustomAttribute.Tag + 1 *)
let [] HasFieldMarshalMax = 151
let [] HasDeclSecurityMin = 152
- let HasDeclSecurity (t: HasDeclSecurityTag) = assert (t.Tag <= 2); HasDeclSecurityMin + t.Tag (* + 1 + 1 = max HasFieldMarshal.Tag + 1 *)
+ let HasDeclSecurity (tag: HasDeclSecurityTag) = assert (tag.Tag <= 2); HasDeclSecurityMin + tag.Tag (* + 1 + 1 = max HasFieldMarshal.Tag + 1 *)
let [] HasDeclSecurityMax = 154
let [] MemberRefParentMin = 155
- let MemberRefParent (t: MemberRefParentTag) = assert (t.Tag <= 4); MemberRefParentMin + t.Tag (* + 2 + 1 = max HasDeclSecurity.Tag + 1 *)
+ let MemberRefParent (tag: MemberRefParentTag) = assert (tag.Tag <= 4); MemberRefParentMin + tag.Tag (* + 2 + 1 = max HasDeclSecurity.Tag + 1 *)
let [] MemberRefParentMax = 159
let [] HasSemanticsMin = 160
- let HasSemantics (t: HasSemanticsTag) = assert (t.Tag <= 1); HasSemanticsMin + t.Tag (* + 4 + 1 = max MemberRefParent.Tag + 1 *)
+ let HasSemantics (tag: HasSemanticsTag) = assert (tag.Tag <= 1); HasSemanticsMin + tag.Tag (* + 4 + 1 = max MemberRefParent.Tag + 1 *)
let [] HasSemanticsMax = 161
let [] MethodDefOrRefMin = 162
- let MethodDefOrRef (t: MethodDefOrRefTag) = assert (t.Tag <= 2); MethodDefOrRefMin + t.Tag (* + 1 + 1 = max HasSemantics.Tag + 1 *)
+ let MethodDefOrRef (tag: MethodDefOrRefTag) = assert (tag.Tag <= 2); MethodDefOrRefMin + tag.Tag (* + 1 + 1 = max HasSemantics.Tag + 1 *)
let [] MethodDefOrRefMax = 164
let [] MemberForwardedMin = 165
- let MemberForwarded (t: MemberForwardedTag) = assert (t.Tag <= 1); MemberForwardedMin + t.Tag (* + 2 + 1 = max MethodDefOrRef.Tag + 1 *)
+ let MemberForwarded (tag: MemberForwardedTag) = assert (tag.Tag <= 1); MemberForwardedMin + tag.Tag (* + 2 + 1 = max MethodDefOrRef.Tag + 1 *)
let [] MemberForwardedMax = 166
let [] ImplementationMin = 167
- let Implementation (t: ImplementationTag) = assert (t.Tag <= 2); ImplementationMin + t.Tag (* + 1 + 1 = max MemberForwarded.Tag + 1 *)
+ let Implementation (tag: ImplementationTag) = assert (tag.Tag <= 2); ImplementationMin + tag.Tag (* + 1 + 1 = max MemberForwarded.Tag + 1 *)
let [] ImplementationMax = 169
let [] CustomAttributeTypeMin = 170
- let CustomAttributeType (t: CustomAttributeTypeTag) = assert (t.Tag <= 3); CustomAttributeTypeMin + t.Tag (* + 2 + 1 = max Implementation.Tag + 1 *)
+ let CustomAttributeType (tag: CustomAttributeTypeTag) = assert (tag.Tag <= 3); CustomAttributeTypeMin + tag.Tag (* + 2 + 1 = max Implementation.Tag + 1 *)
let [] CustomAttributeTypeMax = 173
let [] ResolutionScopeMin = 174
- let ResolutionScope (t: ResolutionScopeTag) = assert (t.Tag <= 4); ResolutionScopeMin + t.Tag (* + 3 + 1 = max CustomAttributeType.Tag + 1 *)
+ let ResolutionScope (tag: ResolutionScopeTag) = assert (tag.Tag <= 4); ResolutionScopeMin + tag.Tag (* + 3 + 1 = max CustomAttributeType.Tag + 1 *)
let [] ResolutionScopeMax = 178
[]
@@ -243,33 +244,33 @@ let Blob (x: int) = RowElement(RowElementTags.Blob, x)
let StringE (x: int) = RowElement(RowElementTags.String, x)
/// pos. in some table
-let SimpleIndex (t, x: int) = RowElement(RowElementTags.SimpleIndex t, x)
+let SimpleIndex (table, index: int) = RowElement(RowElementTags.SimpleIndex table, index)
-let TypeDefOrRefOrSpec (t, x: int) = RowElement(RowElementTags.TypeDefOrRefOrSpec t, x)
+let TypeDefOrRefOrSpec (tag, index: int) = RowElement(RowElementTags.TypeDefOrRefOrSpec tag, index)
-let TypeOrMethodDef (t, x: int) = RowElement(RowElementTags.TypeOrMethodDef t, x)
+let TypeOrMethodDef (tag, index: int) = RowElement(RowElementTags.TypeOrMethodDef tag, index)
-let HasConstant (t, x: int) = RowElement(RowElementTags.HasConstant t, x)
+let HasConstant (tag, index: int) = RowElement(RowElementTags.HasConstant tag, index)
-let HasCustomAttribute (t, x: int) = RowElement(RowElementTags.HasCustomAttribute t, x)
+let HasCustomAttribute (tag, index: int) = RowElement(RowElementTags.HasCustomAttribute tag, index)
-let HasFieldMarshal (t, x: int) = RowElement(RowElementTags.HasFieldMarshal t, x)
+let HasFieldMarshal (tag, index: int) = RowElement(RowElementTags.HasFieldMarshal tag, index)
-let HasDeclSecurity (t, x: int) = RowElement(RowElementTags.HasDeclSecurity t, x)
+let HasDeclSecurity (tag, index: int) = RowElement(RowElementTags.HasDeclSecurity tag, index)
-let MemberRefParent (t, x: int) = RowElement(RowElementTags.MemberRefParent t, x)
+let MemberRefParent (tag, index: int) = RowElement(RowElementTags.MemberRefParent tag, index)
-let HasSemantics (t, x: int) = RowElement(RowElementTags.HasSemantics t, x)
+let HasSemantics (tag, index: int) = RowElement(RowElementTags.HasSemantics tag, index)
-let MethodDefOrRef (t, x: int) = RowElement(RowElementTags.MethodDefOrRef t, x)
+let MethodDefOrRef (tag, index: int) = RowElement(RowElementTags.MethodDefOrRef tag, index)
-let MemberForwarded (t, x: int) = RowElement(RowElementTags.MemberForwarded t, x)
+let MemberForwarded (tag, index: int) = RowElement(RowElementTags.MemberForwarded tag, index)
-let Implementation (t, x: int) = RowElement(RowElementTags.Implementation t, x)
+let Implementation (tag, index: int) = RowElement(RowElementTags.Implementation tag, index)
-let CustomAttributeType (t, x: int) = RowElement(RowElementTags.CustomAttributeType t, x)
+let CustomAttributeType (tag, index: int) = RowElement(RowElementTags.CustomAttributeType tag, index)
-let ResolutionScope (t, x: int) = RowElement(RowElementTags.ResolutionScope t, x)
+let ResolutionScope (tag, index: int) = RowElement(RowElementTags.ResolutionScope tag, index)
type BlobIndex = int
@@ -362,57 +363,55 @@ let envForOverrideSpec (ospec: ILOverridesSpec) = { EnclosingTyparCount=ospec.De
// TABLES
//---------------------------------------------------------------------
-[]
-type MetadataTable<'T when 'T:not null> =
- { name: string
- dict: Dictionary<'T, int> // given a row, find its entry number
- mutable rows: ResizeArray<'T> }
+[]
+type MetadataTable<'T when 'T:not null>(name: string, hashEq: IEqualityComparer<'T>) =
+ let dict = Dictionary<'T, int>(100, hashEq)
+ let rows = ResizeArray<'T>()
+
+ member _.Count = rows.Count
+
+ member internal _.Name = name
- member x.Count = x.rows.Count
+ static member New(nm, hashEq) = MetadataTable<'T>(nm, hashEq)
- static member New(nm, hashEq) =
- { name=nm
- dict = Dictionary<_, _>(100, hashEq)
- rows= ResizeArray<_>() }
+ member _.EntriesAsArray = rows |> ResizeArray.toArray
- member tbl.EntriesAsArray =
- tbl.rows |> ResizeArray.toArray
+ member _.Entries = rows |> ResizeArray.toList
- member tbl.Entries =
- tbl.rows |> ResizeArray.toList
+ member internal _.KeyValueSeq = dict :> seq>
- member tbl.AddSharedEntry x =
- let n = tbl.rows.Count + 1
- tbl.dict[x] <- n
- tbl.rows.Add x
+ member _.AddSharedEntry x =
+ let n = rows.Count + 1
+ dict[x] <- n
+ rows.Add x
n
- member tbl.AddUnsharedEntry x =
- let n = tbl.rows.Count + 1
- tbl.rows.Add x
+ member _.AddUnsharedEntry x =
+ let n = rows.Count + 1
+ rows.Add x
n
- member tbl.FindOrAddSharedEntry x =
- match tbl.dict.TryGetValue x with
+ member this.FindOrAddSharedEntry x =
+ match dict.TryGetValue x with
| true, res -> res
- | _ -> tbl.AddSharedEntry x
+ | _ -> this.AddSharedEntry x
- member tbl.Contains x = tbl.dict.ContainsKey x
+ member _.Contains x = dict.ContainsKey x
/// This is only used in one special place - see further below.
- member tbl.SetRowsOfTable t =
- tbl.rows <- ResizeArray.ofArray t
- let h = tbl.dict
- h.Clear()
- t |> Array.iteri (fun i x -> h[x] <- (i+1))
+ member _.SetRowsOfTable(t: 'T[]) =
+ rows.Clear()
+ dict.Clear()
+ t |> Array.iter (fun entry -> rows.Add entry)
+ t |> Array.iteri (fun i entry -> dict[entry] <- i + 1)
- member tbl.AddUniqueEntry nm getter x =
- if tbl.dict.ContainsKey x then failwith ("duplicate entry '"+getter x+"' in "+nm+" table")
- else tbl.AddSharedEntry x
+ member this.AddUniqueEntry nm getter x =
+ if dict.ContainsKey x then failwith ("duplicate entry '" + getter x + "' in " + nm + " table")
+ else this.AddSharedEntry x
- member tbl.GetTableEntry x = tbl.dict[x]
+ member _.GetTableEntry x = dict[x]
- override x.ToString() = "table " + x.name
+ override _.ToString() = "table " + name
//---------------------------------------------------------------------
// Keys into some of the tables
@@ -505,11 +504,11 @@ type TypeDefTableKey = TdKey of string list (* enclosing *) * string (* type nam
type MetadataTable =
| Shared of MetadataTable
| Unshared of MetadataTable
- member t.FindOrAddSharedEntry x = match t with Shared u -> u.FindOrAddSharedEntry x | Unshared u -> failwithf "FindOrAddSharedEntry: incorrect table kind, u.name = %s" u.name
- member t.AddSharedEntry x = match t with | Shared u -> u.AddSharedEntry x | Unshared u -> failwithf "AddSharedEntry: incorrect table kind, u.name = %s" u.name
- member t.AddUnsharedEntry x = match t with Unshared u -> u.AddUnsharedEntry x | Shared u -> failwithf "AddUnsharedEntry: incorrect table kind, u.name = %s" u.name
+ member t.FindOrAddSharedEntry x = match t with Shared u -> u.FindOrAddSharedEntry x | Unshared u -> failwithf "FindOrAddSharedEntry: incorrect table kind, u.Name = %s" u.Name
+ member t.AddSharedEntry x = match t with | Shared u -> u.AddSharedEntry x | Unshared u -> failwithf "AddSharedEntry: incorrect table kind, u.Name = %s" u.Name
+ member t.AddUnsharedEntry x = match t with Unshared u -> u.AddUnsharedEntry x | Shared u -> failwithf "AddUnsharedEntry: incorrect table kind, u.Name = %s" u.Name
member t.GenericRowsOfTable = match t with Unshared u -> u.EntriesAsArray |> Array.map (fun x -> x.GenericRow) | Shared u -> u.EntriesAsArray |> Array.map (fun x -> x.GenericRow)
- member t.SetRowsOfSharedTable rows = match t with Shared u -> u.SetRowsOfTable (Array.map SharedRow rows) | Unshared u -> failwithf "SetRowsOfSharedTable: incorrect table kind, u.name = %s" u.name
+ member t.SetRowsOfSharedTable rows = match t with Shared u -> u.SetRowsOfTable (Array.map SharedRow rows) | Unshared u -> failwithf "SetRowsOfSharedTable: incorrect table kind, u.Name = %s" u.Name
member t.Count = match t with Unshared u -> u.Count | Shared u -> u.Count
@@ -653,6 +652,21 @@ type ILTokenMappings =
PropertyTokenMap: ILTypeDef list * ILTypeDef -> ILPropertyDef -> int32
EventTokenMap: ILTypeDef list * ILTypeDef -> ILEventDef -> int32 }
+[]
+/// Represents the length of each metadata heap emitted for the current module.
+type MetadataHeapSizes =
+ { StringHeapSize: int
+ UserStringHeapSize: int
+ BlobHeapSize: int
+ GuidHeapSize: int }
+
+[]
+/// Snapshot of the metadata state (heap sizes, table row counts, GUID stream offset) used for hot reload baselines.
+type MetadataSnapshot =
+ { HeapSizes: MetadataHeapSizes
+ TableRowCounts: int[]
+ GuidHeapStart: int }
+
let recordRequiredDataFixup (requiredDataFixups: ('T * 'U) list ref) (buf: ByteBuffer) pos lab =
requiredDataFixups.Value <- (pos, lab) :: requiredDataFixups.Value
// Write a special value in that we check later when applying the fixup
@@ -1116,7 +1130,7 @@ let FindMethodDefIdx cenv mdkey =
with :? KeyNotFoundException ->
let typeNameOfIdx i =
match
- (cenv.typeDefs.dict
+ (cenv.typeDefs.KeyValueSeq
|> Seq.fold (fun sofar kvp ->
let tkey2 = kvp.Key
let tidx2 = kvp.Value
@@ -1130,7 +1144,7 @@ let FindMethodDefIdx cenv mdkey =
let (TdKey (tenc, tname)) = typeNameOfIdx mdkey.TypeIdx
dprintn ("The local method '"+(String.concat "." (tenc@[tname]))+"'::'"+mdkey.Name+"' was referenced but not declared")
dprintn ("generic arity: "+string mdkey.GenericArity)
- cenv.methodDefIdxsByKey.dict |> Seq.iter (fun (KeyValue(mdkey2, _)) ->
+ cenv.methodDefIdxsByKey.KeyValueSeq |> Seq.iter (fun (KeyValue(mdkey2, _)) ->
if mdkey2.TypeIdx = mdkey.TypeIdx && mdkey.Name = mdkey2.Name then
let (TdKey (tenc2, tname2)) = typeNameOfIdx mdkey2.TypeIdx
dprintn ("A method in '"+(String.concat "." (tenc2@[tname2]))+"' had the right name but the wrong signature:")
@@ -2478,6 +2492,24 @@ let GenILMethodBody mname cenv env (il: ILMethodBody) =
localToken, (requiredStringFixups', methbuf.AsMemory().ToArray()), seqpoints, scopes
+type EncodedMethodBody =
+ { LocalSignatureToken: int
+ RequiredStringFixupsOffset: int
+ RequiredStringFixups: (int * int) list
+ Code: byte[]
+ SequencePoints: PdbDebugPoint[]
+ RootScope: PdbMethodScope option }
+
+let EncodeMethodBody cenv env mname ilmbody =
+ let localToken, ((offset, fixups), codeBytes), seqpoints, scope = GenILMethodBody mname cenv env ilmbody
+
+ { LocalSignatureToken = localToken
+ RequiredStringFixupsOffset = offset
+ RequiredStringFixups = fixups
+ Code = codeBytes
+ SequencePoints = seqpoints
+ RootScope = if cenv.generatePdb then Some scope else None }
+
// --------------------------------------------------------------------
// ILFieldDef --> FieldDef Row
// --------------------------------------------------------------------
@@ -2673,31 +2705,31 @@ let GenMethodDefAsRow cenv env midx (mdef: ILMethodDef) =
else
ilmbodyLazy.Value
let addr = cenv.nextCodeAddr
- let localToken, code, seqpoints, rootScope = GenILMethodBody mdef.Name cenv env ilmbody
+ let encodedBody = EncodeMethodBody cenv env mdef.Name ilmbody
// Now record the PDB record for this method - we write this out later.
if cenv.generatePdb then
cenv.pdbinfo.Add
- { MethToken=getUncodedToken TableNames.Method midx
- MethName=mdef.Name
- LocalSignatureToken=localToken
- Params= [| |] (* REVIEW *)
- RootScope = Some rootScope
+ { MethToken = getUncodedToken TableNames.Method midx
+ MethName = mdef.Name
+ LocalSignatureToken = encodedBody.LocalSignatureToken
+ Params = [| |] (* REVIEW *)
+ RootScope = encodedBody.RootScope
DebugRange =
match ilmbody.DebugRange with
| Some m when cenv.generatePdb ->
// table indexes are 1-based, document array indexes are 0-based
let doc = (cenv.documents.FindOrAddSharedEntry m.Document) - 1
- Some ({ Document=doc
- Line=m.Line
- Column=m.Column },
- { Document=doc
- Line=m.EndLine
- Column=m.EndColumn })
+ Some ({ Document = doc
+ Line = m.Line
+ Column = m.Column },
+ { Document = doc
+ Line = m.EndLine
+ Column = m.EndColumn })
| _ -> None
- DebugPoints=seqpoints }
- cenv.AddCode code
+ DebugPoints = encodedBody.SequencePoints }
+ cenv.AddCode ((encodedBody.RequiredStringFixupsOffset, encodedBody.RequiredStringFixups), encodedBody.Code)
addr
| MethodBody.Abstract
| MethodBody.PInvoke _
@@ -3276,7 +3308,10 @@ let writeILMetadataAndCode (
allGivenSources,
modul,
cilStartAddress,
- normalizeAssemblyRefs
+ normalizeAssemblyRefs,
+ // Hot reload baseline side channel: when false (the default compilation path) no
+ // MetadataSnapshot is materialized, so flag-off compiles pay no extra allocations.
+ collectMetadataSnapshot: bool
) =
// When we know the real RVAs of the data section we fixup the references for the FieldRVA table.
@@ -3680,7 +3715,22 @@ let writeILMetadataAndCode (
applyFixup32 code locInCode token
reportTime "Fixup Metadata"
- entryPointToken, code, codePadding, metadata, data, resources, requiredDataFixups.Value, pdbData, mappings, guidStart
+ // Hot reload baseline side channel: only materialize the snapshot when a consumer asked
+ // for one (--test:HotReloadDeltas in-memory emission); ordinary compiles skip it entirely.
+ let metadataSnapshotOpt =
+ if collectMetadataSnapshot then
+ Some
+ { HeapSizes =
+ { StringHeapSize = stringsStreamUnpaddedSize
+ UserStringHeapSize = userStringsStreamUnpaddedSize
+ BlobHeapSize = blobsStreamUnpaddedSize
+ GuidHeapSize = guidsStreamUnpaddedSize }
+ TableRowCounts = tables |> Seq.map (fun t -> t.Count) |> Seq.toArray
+ GuidHeapStart = guidStart }
+ else
+ None
+
+ entryPointToken, code, codePadding, metadata, data, resources, requiredDataFixups.Value, pdbData, mappings, guidStart, metadataSnapshotOpt
//---------------------------------------------------------------------
// PHYSICAL METADATA+BLOBS --> PHYSICAL PE FORMAT
@@ -3864,14 +3914,21 @@ 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.
+ // Hot reload baseline side channel: module-level CustomDebugInformation rows for
+ // F#-owned records in the portable PDB. Empty unless a gated hot reload capture
+ // compile needs to persist extra deterministic state.
moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list
- /// Per-method EnC CustomDebugInformation rows for the portable PDB writer, keyed by
- /// IL method name. Empty for ordinary compiles.
+ // Hot reload baseline side channel: per-method EnC CustomDebugInformation rows for
+ // the portable PDB writer, keyed by IL method name. Empty unless the compilation
+ // runs with --test:HotReloadDeltas (flag-off output stays byte-identical).
methodCustomDebugInfoRows: Map }
-let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRefs) =
+///
+/// Core IL writer that emits the PE image and, when is
+/// present, invokes it with the captured metadata snapshot once the metadata streams have been
+/// finalized. When the sink is None (ordinary compilation) no snapshot is constructed.
+///
+let writeBinaryAuxWithSnapshotSink (stream: Stream, options: options, modul, normalizeAssemblyRefs) (metadataSnapshotSink: (MetadataSnapshot -> unit) option) =
// Store the public key from the signer into the manifest. This means it will be written
// to the binary and also acts as an indicator to leave space for delay sign
@@ -3984,22 +4041,28 @@ let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRe
| Some v -> v
| None -> failwith "Expected mscorlib to have a version number"
- let entryPointToken, code, codePadding, metadata, data, resources, requiredDataFixups, pdbData, mappings, guidStart =
+ let entryPointToken, code, codePadding, metadata, data, resources, requiredDataFixups, pdbData, mappings, guidStart, metadataSnapshotOpt =
writeILMetadataAndCode (
options.pdbfile.IsSome,
desiredMetadataVersion,
ilg,
options.emitTailcalls,
- options.deterministic,
+ options.deterministic,
options.referenceAssemblyOnly,
options.referenceAssemblyAttribOpt,
options.allGivenSources,
modul,
next,
- normalizeAssemblyRefs
+ normalizeAssemblyRefs,
+ metadataSnapshotSink.IsSome
)
reportTime "Generated IL and metadata"
+
+ match metadataSnapshotSink, metadataSnapshotOpt with
+ | Some sink, Some metadataSnapshot -> sink metadataSnapshot
+ | _ -> ()
+
let _codeChunk, next = chunk code.Length next
let _codePaddingChunk, next = chunk codePadding.Length next
@@ -4582,6 +4645,9 @@ let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRe
reportTime "Writing Image"
pdbData, pdbInfoOpt, debugDirectoryChunk, debugDataChunk, debugChecksumPdbChunk, debugEmbeddedPdbChunk, debugDeterministicPdbChunk, textV2P, mappings
+let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRefs) =
+ writeBinaryAuxWithSnapshotSink (stream, options, modul, normalizeAssemblyRefs) None
+
let writeBinaryFiles (options: options, modul, normalizeAssemblyRefs) =
let stream =
@@ -4627,12 +4693,20 @@ let writeBinaryFiles (options: options, modul, normalizeAssemblyRefs) =
mappings
-let writeBinaryInMemory (options: options, modul, normalizeAssemblyRefs) =
+let writeBinaryInMemoryWithArtifacts (options: options, modul, normalizeAssemblyRefs) =
let stream = new MemoryStream()
let options = { options with referenceAssemblyOnly = false; referenceAssemblyAttribOpt = None; referenceAssemblySignatureHash = None }
- let pdbData, pdbInfoOpt, debugDirectoryChunk, debugDataChunk, debugChecksumPdbChunk, debugEmbeddedPdbChunk, debugDeterministicPdbChunk, textV2P, _mappings =
- writeBinaryAux(stream, options, modul, normalizeAssemblyRefs)
+ // Capture exactly one metadata snapshot for the emitted module so callers can persist baseline information.
+ let metadataSnapshotRef = ref None
+ let capture snapshot = metadataSnapshotRef := Some snapshot
+ let pdbData, pdbInfoOpt, debugDirectoryChunk, debugDataChunk, debugChecksumPdbChunk, debugEmbeddedPdbChunk, debugDeterministicPdbChunk, textV2P, mappings =
+ writeBinaryAuxWithSnapshotSink (stream, options, modul, normalizeAssemblyRefs) (Some capture)
+
+ let metadataSnapshot =
+ match !metadataSnapshotRef with
+ | Some snapshot -> snapshot
+ | None -> failwith "Metadata snapshot not captured"
let reopenOutput () =
stream.Seek(0, SeekOrigin.Begin) |> ignore
@@ -4658,12 +4732,15 @@ let writeBinaryInMemory (options: options, modul, normalizeAssemblyRefs) =
stream.Close()
- stream.ToArray(), pdbBytes
-
+ stream.ToArray(), pdbBytes, mappings, metadataSnapshot
let WriteILBinaryFile (options: options, inputModule, normalizeAssemblyRefs) =
writeBinaryFiles (options, inputModule, normalizeAssemblyRefs)
|> ignore
+let WriteILBinaryInMemoryWithArtifacts (options: options, inputModule: ILModuleDef, normalizeAssemblyRefs) =
+ writeBinaryInMemoryWithArtifacts (options, inputModule, normalizeAssemblyRefs)
+
let WriteILBinaryInMemory (options: options, inputModule: ILModuleDef, normalizeAssemblyRefs) =
- writeBinaryInMemory (options, inputModule, normalizeAssemblyRefs)
+ let assemblyBytes, pdbBytes, _, _ = writeBinaryInMemoryWithArtifacts (options, inputModule, normalizeAssemblyRefs)
+ assemblyBytes, pdbBytes
diff --git a/src/Compiler/AbstractIL/ilwrite.fsi b/src/Compiler/AbstractIL/ilwrite.fsi
index 40ea015db12..7af65c95c41 100644
--- a/src/Compiler/AbstractIL/ilwrite.fsi
+++ b/src/Compiler/AbstractIL/ilwrite.fsi
@@ -29,13 +29,46 @@ type options =
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.
+ /// F#-owned records in the portable PDB. Empty unless a gated hot reload capture
+ /// compile needs to persist extra deterministic state.
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.
+ /// Hot reload baseline side channel: per-method EnC CustomDebugInformation rows for
+ /// the portable PDB writer, keyed by IL method name. Empty unless the compilation
+ /// runs with --test:HotReloadDeltas (flag-off output stays byte-identical).
methodCustomDebugInfoRows: Map
}
+///
+/// Captures the various metadata token mapping functions produced by the IL writer.
+///
+[]
+type ILTokenMappings =
+ { TypeDefTokenMap: ILTypeDef list * ILTypeDef -> int32
+ FieldDefTokenMap: ILTypeDef list * ILTypeDef -> ILFieldDef -> int32
+ MethodDefTokenMap: ILTypeDef list * ILTypeDef -> ILMethodDef -> int32
+ PropertyTokenMap: ILTypeDef list * ILTypeDef -> ILPropertyDef -> int32
+ EventTokenMap: ILTypeDef list * ILTypeDef -> ILEventDef -> int32 }
+
+///
+/// Records the uncompressed heap sizes produced during metadata emission so that later delta passes
+/// can reason about stream growth.
+///
+[]
+type MetadataHeapSizes =
+ { StringHeapSize: int
+ UserStringHeapSize: int
+ BlobHeapSize: int
+ GuidHeapSize: int }
+
+///
+/// Snapshot of the emitted metadata state that is required to seed hot reload baseline calculations.
+///
+[]
+type MetadataSnapshot =
+ { HeapSizes: MetadataHeapSizes
+ TableRowCounts: int[]
+ GuidHeapStart: int }
+
/// Computes the trailing byte for a user string blob per ECMA-335 II.24.2.4.
/// Returns 1 if any character needs special handling, 0 otherwise.
val markerForUnicodeBytes: b: byte[] -> int
@@ -46,3 +79,8 @@ val WriteILBinaryFile: options: options * inputModule: ILModuleDef * (ILAssembly
/// Write a binary to an array of bytes suitable for dynamic loading.
val WriteILBinaryInMemory:
options: options * inputModule: ILModuleDef * (ILAssemblyRef -> ILAssemblyRef) -> byte[] * byte[] option
+
+/// Write a binary to an array of bytes and capture token and metadata artifacts.
+val WriteILBinaryInMemoryWithArtifacts:
+ options: options * inputModule: ILModuleDef * (ILAssemblyRef -> ILAssemblyRef) ->
+ byte[] * byte[] option * ILTokenMappings * MetadataSnapshot
diff --git a/src/Compiler/AbstractIL/ilwritepdb.fs b/src/Compiler/AbstractIL/ilwritepdb.fs
index bfa9cafef99..b3234e0c25c 100644
--- a/src/Compiler/AbstractIL/ilwritepdb.fs
+++ b/src/Compiler/AbstractIL/ilwritepdb.fs
@@ -171,10 +171,28 @@ type HashAlgorithm =
| Sha1
| Sha256
-// Document checksum algorithms
+// ============================================================================
+// Well-known PDB GUIDs (Portable PDB metadata)
+// ============================================================================
+
+/// Document checksum algorithm: SHA-1 (Portable PDB spec)
let guidSha1 = Guid("ff1816ec-aa5e-4d10-87f7-6f4963833460")
+
+/// Document checksum algorithm: SHA-256 (Portable PDB spec)
let guidSha2 = Guid("8829d00f-11b8-4213-878b-770e8597ac16")
+/// F# language GUID for Portable PDB Document.Language field
+let corSymLanguageTypeFSharp =
+ Guid(0xAB4F38C9u, 0xB6E6us, 0x43baus, 0xBEuy, 0x3Buy, 0x58uy, 0x08uy, 0x0Buy, 0x2Cuy, 0xCCuy, 0xE3uy)
+
+/// Embedded source custom debug information GUID
+let embeddedSourceGuid =
+ Guid(0x0e8a571bu, 0x6926us, 0x466eus, 0xb4uy, 0xaduy, 0x8auy, 0xb0uy, 0x46uy, 0x11uy, 0xf5uy, 0xfeuy)
+
+/// Source link custom debug information GUID
+let sourceLinkGuid =
+ Guid(0xcc110556u, 0xa091us, 0x4d38us, 0x9fuy, 0xecuy, 0x25uy, 0xabuy, 0x9auy, 0x35uy, 0x1auy, 0x6auy)
+
let checkSum (url: string) (checksumAlgorithm: HashAlgorithm) =
try
use file = FileSystem.OpenFileForReadShim(url)
@@ -386,14 +404,9 @@ type PortablePdbGenerator
metadata.GetOrAddBlob writer
- let corSymLanguageTypeId =
- Guid(0xAB4F38C9u, 0xB6E6us, 0x43baus, 0xBEuy, 0x3Buy, 0x58uy, 0x08uy, 0x0Buy, 0x2Cuy, 0xCCuy, 0xE3uy)
-
- let embeddedSourceId =
- Guid(0x0e8a571bu, 0x6926us, 0x466eus, 0xb4uy, 0xaduy, 0x8auy, 0xb0uy, 0x46uy, 0x11uy, 0xf5uy, 0xfeuy)
-
- let sourceLinkId =
- Guid(0xcc110556u, 0xa091us, 0x4d38us, 0x9fuy, 0xecuy, 0x25uy, 0xabuy, 0x9auy, 0x35uy, 0x1auy, 0x6auy)
+ let corSymLanguageTypeId = corSymLanguageTypeFSharp
+ let embeddedSourceId = embeddedSourceGuid
+ let sourceLinkId = sourceLinkGuid
///
/// The maximum number of bytes in to write out uncompressed.
diff --git a/src/Compiler/AbstractIL/ilwritepdb.fsi b/src/Compiler/AbstractIL/ilwritepdb.fsi
index 3aa0679178a..508b892b813 100644
--- a/src/Compiler/AbstractIL/ilwritepdb.fsi
+++ b/src/Compiler/AbstractIL/ilwritepdb.fsi
@@ -68,9 +68,10 @@ type PdbMethodData =
DebugPoints: PdbDebugPoint[] }
/// A pre-serialized CustomDebugInformation row to attach to a method definition row in
-/// the portable PDB (kind GUID + blob). Supplied by the compiler as a side channel keyed
-/// by IL method name. The writer attaches the rows only when the name identifies exactly
-/// one method row (fail closed on ambiguity).
+/// the portable PDB (kind GUID + blob). Supplied by the compiler as a side channel for
+/// hot reload baseline emission (--test:HotReloadDeltas): EnC lambda/closure map blobs
+/// computed from the typed tree, keyed by IL method name. The writer attaches the rows
+/// only when the name identifies exactly 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
diff --git a/src/Compiler/CodeGen/EncMethodDebugInformation.fs b/src/Compiler/CodeGen/EncMethodDebugInformation.fs
new file mode 100644
index 00000000000..c67be11a632
--- /dev/null
+++ b/src/Compiler/CodeGen/EncMethodDebugInformation.fs
@@ -0,0 +1,970 @@
+/// Edit-and-Continue method debug information blobs for hot reload.
+///
+/// This module replicates, byte for byte, the three Portable-PDB CustomDebugInformation
+/// blob formats Roslyn persists per method to support Edit and Continue
+/// (roslyn/src/Compilers/Core/Portable/Emit/EditAndContinueMethodDebugInformation.cs):
+///
+/// - EnC Local Slot Map (kind 755F52A8-91C5-45BE-B4B8-209571E552BD)
+/// - EnC Lambda and Closure Map (kind A643004C-0240-496F-A783-30D64F4979DE)
+/// - EnC State Machine State Map (kind 8B78CD68-2EDE-420B-980B-E15884B8AAA3)
+///
+/// (GUIDs: roslyn/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs.)
+///
+/// All multi-byte integers use the ECMA-335 compressed unsigned/signed encodings via
+/// System.Reflection.Metadata's BlobBuilder.WriteCompressedInteger /
+/// WriteCompressedSignedInteger and BlobReader.ReadCompressedInteger /
+/// ReadCompressedSignedInteger, exactly as Roslyn writes/reads them.
+///
+/// F# semantics of the "syntax offset" slots: Roslyn stores the syntax offset of the
+/// lambda/closure/state-machine-suspension syntax node. The F# typed-tree diff has no
+/// syntax map; instead these integer slots carry OCCURRENCE KEYS — a deterministic
+/// int packed from the occurrence ordinal chain of the lambda occurrence model
+/// (TypedTreeDiff.LambdaOccurrenceId). See tryEncodeOccurrenceKey/decodeOccurrenceKey.
+/// The blob format is identical either way, so mdv/Roslyn tooling can still decode our
+/// maps; only the *meaning* of the integers is F#-specific (debugger-interop
+/// caveat documented in docs/hot-reload-closure-mapping.md).
+module internal FSharp.Compiler.EncMethodDebugInformation
+
+#nowarn "9" // NativePtr: BlobReader only exposes a byte*-based constructor
+
+open System
+open System.Collections.Generic
+open System.Collections.Immutable
+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
+open FSharp.Compiler.TcGlobals
+open FSharp.Compiler.TypedTree
+open FSharp.Compiler.TypedTreeDiff
+
+/// Portable-PDB CustomDebugInformation kind GUIDs for the EnC blobs, copied verbatim
+/// from roslyn/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs.
+[]
+module PortableCustomDebugInfoKinds =
+
+ /// EnC Local Slot Map CDI kind.
+ let encLocalSlotMap = Guid("755F52A8-91C5-45BE-B4B8-209571E552BD")
+
+ /// EnC Lambda and Closure Map CDI kind.
+ let encLambdaAndClosureMap = Guid("A643004C-0240-496F-A783-30D64F4979DE")
+
+ /// 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.
+[]
+let StaticClosureOrdinal = -1
+
+/// Closure ordinal of a lambda closed over the 'this' pointer only.
+/// Mirrors Roslyn's LambdaDebugInfo.ThisOnlyClosureOrdinal.
+[]
+let ThisOnlyClosureOrdinal = -2
+
+/// Smallest valid closure ordinal. Mirrors Roslyn's LambdaDebugInfo.MinClosureOrdinal.
+[]
+let MinClosureOrdinal = ThisOnlyClosureOrdinal
+
+/// Method ordinal of a method that has no lambda map (an empty blob decodes to this).
+/// Mirrors Roslyn's DebugId.UndefinedOrdinal.
+[]
+let UndefinedMethodOrdinal = -1
+
+/// Marker byte introducing the (optional) negative syntax-offset baseline in the
+/// local-slot-map blob. Mirrors Roslyn's SyntaxOffsetBaseline = 0xFF.
+[]
+let private SyntaxOffsetBaselineMarker = 0xFFuy
+
+/// Largest synthesized-local kind serializable in the slot map: the kind is stored as
+/// (kind + 1) in bits 0-6 of the leading byte (bit 7 flags a trailing ordinal), and
+/// Roslyn's reader recovers it with mask 0x3F, so only kinds 0..0x3E round-trip.
+[]
+let MaxSerializableLocalKind = 0x3E
+
+/// One slot in the EnC Local Slot Map: the local variable layout of a method body,
+/// recorded so a later generation can map its locals onto the same slot indices.
+[]
+type EncLocalSlotInfo =
+ /// A short-lived lowering temp: serialized as the single byte 0x00, carrying no
+ /// identity (a later generation never reuses it).
+ | Temp
+
+ /// A long-lived synthesized local.
+ /// kind: synthesized-local kind (Roslyn SynthesizedLocalKind value, 0..MaxSerializableLocalKind;
+ /// 0 = user-defined local).
+ /// syntaxOffset: in F#, the occurrence key of the declaring occurrence
+ /// (Roslyn: syntax offset of the local's declarator).
+ /// ordinal: zero-based disambiguator among slots sharing the same kind and offset (>= 0).
+ | Slot of kind: int * syntaxOffset: int * ordinal: int
+
+/// One closure scope in the EnC Lambda and Closure Map. The closure's ordinal is its
+/// index in EncMethodDebugInformation.Closures; lambdas reference closures by that index.
+/// SyntaxOffset: in F#, the occurrence key of the closure's occurrence.
+type EncClosureInfo =
+ {
+ /// Occurrence key (Roslyn: syntax offset of the scope owning the closure).
+ SyntaxOffset: int
+ }
+
+/// One lambda in the EnC Lambda and Closure Map.
+type EncLambdaInfo =
+ {
+ /// Occurrence key (Roslyn: syntax offset of the lambda body).
+ SyntaxOffset: int
+ /// Index into EncMethodDebugInformation.Closures of the closure holding the
+ /// lambda's captures, or StaticClosureOrdinal / ThisOnlyClosureOrdinal.
+ ClosureOrdinal: int
+ }
+
+/// One suspension point in the EnC State Machine State Map.
+type EncStateMachineStateInfo =
+ {
+ /// State machine state number assigned to the suspension point (may be negative:
+ /// Roslyn uses negative numbers for increasing-iteration finalize states).
+ StateNumber: int
+ /// Occurrence key (Roslyn: syntax offset of the await/yield syntax node).
+ SyntaxOffset: int
+ }
+
+/// Debugging information associated with a method, persisted by the compiler in the
+/// Portable PDB to support Edit and Continue. Mirrors Roslyn's
+/// EditAndContinueMethodDebugInformation.
+type EncMethodDebugInformation =
+ {
+ /// Ordinal of the method within its generation (>= -1; UndefinedMethodOrdinal when absent).
+ MethodOrdinal: int
+ /// Local slot layout, in slot-index order (EnC Local Slot Map).
+ LocalSlots: EncLocalSlotInfo list
+ /// Closure scopes, in ordinal order (EnC Lambda and Closure Map).
+ Closures: EncClosureInfo list
+ /// Lambdas, in ordinal order (EnC Lambda and Closure Map).
+ Lambdas: EncLambdaInfo list
+ /// State machine suspension points (EnC State Machine State Map).
+ StateMachineStates: EncStateMachineStateInfo list
+ }
+
+ /// An empty map (no slots, lambdas, closures or states; undefined method ordinal).
+ static member Empty =
+ {
+ MethodOrdinal = UndefinedMethodOrdinal
+ LocalSlots = []
+ Closures = []
+ Lambdas = []
+ StateMachineStates = []
+ }
+
+// ---------------------------------------------------------------------------
+// Occurrence-key packing
+// ---------------------------------------------------------------------------
+
+/// Maximum encodable occurrence ordinal: each chain segment is 16 bits.
+[]
+let private MaxOccurrenceSegment = 0xFFFF
+
+/// Compressed unsigned integers must lie in [0, 0x1FFFFFFF); after baseline adjustment
+/// the serialized value is (key - baseline) with baseline <= -1, so keys must stay
+/// strictly below 0x1FFFFFFF - 1 to be writable. Cap at 29 bits minus the adjustment.
+[]
+let private MaxOccurrenceKey = 0x1FFFFFFD
+
+/// Packs an occurrence ordinal chain (root-first enclosing-occurrence ordinals,
+/// ending with the occurrence's own ordinal) into the deterministic int carried in the
+/// "syntax offset" blob slots. Packing: 16-bit segments, least-significant segment =
+/// the occurrence's own 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 — callers must then treat the
+/// occurrence as unmappable (rude edit), never truncate.
+let tryEncodeOccurrenceKey (ordinalChain: int list) : int option =
+ match ordinalChain with
+ | [ ordinal ] when ordinal >= 0 && ordinal <= MaxOccurrenceSegment -> Some ordinal
+ | [ parent; ordinal ] when
+ parent >= 0
+ && ordinal >= 0
+ && ordinal <= MaxOccurrenceSegment
+ && parent < MaxOccurrenceSegment
+ ->
+ // Pack in int64: a large parent would wrap negative in int32 and otherwise pass
+ // the upper-bound check, turning an unrepresentable occurrence into a corrupt key.
+ let key = ((int64 parent + 1L) <<< 16) ||| int64 ordinal
+
+ if key <= int64 MaxOccurrenceKey then
+ Some(int key)
+ else
+ None
+ | _ -> None
+
+/// Unpacks an occurrence key produced by tryEncodeOccurrenceKey back into its
+/// root-first ordinal chain.
+let decodeOccurrenceKey (key: int) : int list =
+ if key < 0 then
+ invalidArg (nameof key) $"occurrence key must be non-negative, got %d{key}"
+ elif key <= MaxOccurrenceSegment then
+ [ key ]
+ else
+ [ (key >>> 16) - 1; key &&& MaxOccurrenceSegment ]
+
+// ---------------------------------------------------------------------------
+// Blob helpers
+// ---------------------------------------------------------------------------
+
+let private invalidData (blobName: string) (offset: int) =
+ raise (InvalidDataException $"invalid EnC %s{blobName} blob: unexpected data at offset %d{offset}")
+
+// Absent CDI rows arrive as null at runtime even though the parameter is non-null in the
+// 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.
+// ---------------------------------------------------------------------------
+
+[]
+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) =
+ 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) =
+ 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) : 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 =
+ if isEmpty blob then
+ Map.empty
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (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()
+
+ 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 bound allocation by the remaining payload before creating the array.
+ 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) : 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,
+// UncompressSlotMap lines 92-143): optional baseline record [0xFF, compressed(-baseline)],
+// then one record per slot: 0x00 for a temp, otherwise a leading byte with bits 0-6 =
+// kind + 1 and bit 7 = has-ordinal flag, followed by compressed(syntaxOffset - baseline)
+// and, when flagged, compressed(ordinal).
+// ---------------------------------------------------------------------------
+
+/// 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).
+let serializeLocalSlots (info: EncMethodDebugInformation) : byte[] =
+ match info.LocalSlots with
+ | [] -> Array.empty
+ | slots ->
+ let builder = BlobBuilder()
+
+ // The baseline is the most negative syntax offset, or -1 when none is negative
+ // (Roslyn lines 147-160). Offsets are stored relative to it so the common
+ // all-non-negative case costs no baseline record.
+ let syntaxOffsetBaseline =
+ (-1, slots)
+ ||> List.fold (fun acc slot ->
+ match slot with
+ | EncLocalSlotInfo.Temp -> acc
+ | EncLocalSlotInfo.Slot(_, syntaxOffset, _) -> min acc syntaxOffset)
+
+ if syntaxOffsetBaseline <> -1 then
+ builder.WriteByte SyntaxOffsetBaselineMarker
+ builder.WriteCompressedInteger(-syntaxOffsetBaseline)
+
+ for slot in slots do
+ match slot with
+ | EncLocalSlotInfo.Temp -> builder.WriteByte 0uy
+ | EncLocalSlotInfo.Slot(kind, syntaxOffset, ordinal) ->
+ if kind < 0 || kind > MaxSerializableLocalKind then
+ invalidArg (nameof info) $"local slot kind %d{kind} is outside the serializable range 0..%d{MaxSerializableLocalKind}"
+
+ if ordinal < 0 then
+ invalidArg (nameof info) $"local slot ordinal must be non-negative, got %d{ordinal}"
+
+ let hasOrdinal = ordinal > 0
+ let b = byte (kind + 1) ||| (if hasOrdinal then 0x80uy else 0uy)
+ builder.WriteByte b
+ builder.WriteCompressedInteger(syntaxOffset - syntaxOffsetBaseline)
+
+ if hasOrdinal then
+ builder.WriteCompressedInteger ordinal
+
+ builder.ToArray()
+
+/// Deserializes an EnC Local Slot Map blob, byte-for-byte as Roslyn's UncompressSlotMap.
+/// An empty (or null) blob yields no slots.
+let deserializeLocalSlots (blob: byte[]) : EncLocalSlotInfo list =
+ if isEmpty blob then
+ []
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ let slots = ResizeArray()
+ let mutable syntaxOffsetBaseline = -1
+
+ try
+ while reader.RemainingBytes > 0 do
+ let b = reader.ReadByte()
+
+ if b = SyntaxOffsetBaselineMarker then
+ syntaxOffsetBaseline <- -reader.ReadCompressedInteger()
+ elif b = 0uy then
+ slots.Add EncLocalSlotInfo.Temp
+ else
+ // Roslyn recovers the kind with mask 0x3F (line 126); bit 7 flags
+ // a trailing ordinal, bit 6 is unused by the writer.
+ let kind = int (b &&& 0x3Fuy) - 1
+ let hasOrdinal = b &&& 0x80uy <> 0uy
+ let syntaxOffset = reader.ReadCompressedInteger() + syntaxOffsetBaseline
+ let ordinal = if hasOrdinal then reader.ReadCompressedInteger() else 0
+ slots.Add(EncLocalSlotInfo.Slot(kind, syntaxOffset, ordinal))
+ with :? BadImageFormatException ->
+ invalidData "local slot map" reader.Offset
+
+ List.ofSeq slots
+ finally
+ handle.Free()
+
+// ---------------------------------------------------------------------------
+// EnC Lambda and Closure Map
+// Format (SerializeLambdaMap lines 261-302, UncompressLambdaMap lines 197-259):
+// compressed(methodOrdinal + 1), compressed(-baseline), compressed(closureCount),
+// closureCount * compressed(syntaxOffset - baseline), then until the blob ends:
+// [compressed(syntaxOffset - baseline), compressed(closureOrdinal - MinClosureOrdinal)]
+// per lambda.
+// ---------------------------------------------------------------------------
+
+/// Serializes the EnC Lambda and Closure Map blob for 'info', byte-for-byte as Roslyn's
+/// SerializeLambdaMap. Returns the empty array when there are no lambdas and no closures
+/// (Roslyn's MetadataWriter skips the CDI row in that case; note the method ordinal is
+/// then not persisted and decodes back as UndefinedMethodOrdinal).
+let serializeLambdaMap (info: EncMethodDebugInformation) : byte[] =
+ match info.Closures, info.Lambdas with
+ | [], [] -> Array.empty
+ | closures, lambdas ->
+ if info.MethodOrdinal < -1 then
+ invalidArg (nameof info) $"method ordinal must be >= -1, got %d{info.MethodOrdinal}"
+
+ let builder = BlobBuilder()
+ builder.WriteCompressedInteger(info.MethodOrdinal + 1)
+
+ // Negative offsets are rare (Roslyn: field/property initializers; F#: reserved),
+ // so the baseline is -1 unless a smaller offset exists (Roslyn lines 266-286).
+ let syntaxOffsetBaseline =
+ let closureMin = (-1, closures) ||> List.fold (fun acc c -> min acc c.SyntaxOffset)
+ (closureMin, lambdas) ||> List.fold (fun acc l -> min acc l.SyntaxOffset)
+
+ builder.WriteCompressedInteger(-syntaxOffsetBaseline)
+ builder.WriteCompressedInteger closures.Length
+
+ for closure in closures do
+ builder.WriteCompressedInteger(closure.SyntaxOffset - syntaxOffsetBaseline)
+
+ for lambda in lambdas do
+ if
+ lambda.ClosureOrdinal < MinClosureOrdinal
+ || lambda.ClosureOrdinal >= closures.Length
+ then
+ invalidArg
+ (nameof info)
+ $"lambda closure ordinal %d{lambda.ClosureOrdinal} is outside [%d{MinClosureOrdinal}, %d{closures.Length})"
+
+ builder.WriteCompressedInteger(lambda.SyntaxOffset - syntaxOffsetBaseline)
+ builder.WriteCompressedInteger(lambda.ClosureOrdinal - MinClosureOrdinal)
+
+ builder.ToArray()
+
+/// Deserializes an EnC Lambda and Closure Map blob, byte-for-byte as Roslyn's
+/// UncompressLambdaMap. An empty (or null) blob yields (UndefinedMethodOrdinal, [], []).
+let deserializeLambdaMap (blob: byte[]) : int * EncClosureInfo list * EncLambdaInfo list =
+ if isEmpty blob then
+ UndefinedMethodOrdinal, [], []
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ let closures = ResizeArray()
+ let lambdas = ResizeArray()
+ let mutable methodOrdinal = UndefinedMethodOrdinal
+
+ try
+ methodOrdinal <- reader.ReadCompressedInteger() - 1
+ let syntaxOffsetBaseline = -reader.ReadCompressedInteger()
+ let closureCount = reader.ReadCompressedInteger()
+
+ for _ in 1..closureCount do
+ let syntaxOffset = reader.ReadCompressedInteger() + syntaxOffsetBaseline
+ closures.Add { SyntaxOffset = syntaxOffset }
+
+ while reader.RemainingBytes > 0 do
+ let syntaxOffset = reader.ReadCompressedInteger() + syntaxOffsetBaseline
+ let closureOrdinal = reader.ReadCompressedInteger() + MinClosureOrdinal
+
+ if closureOrdinal >= closureCount then
+ invalidData "lambda map" reader.Offset
+
+ lambdas.Add
+ {
+ SyntaxOffset = syntaxOffset
+ ClosureOrdinal = closureOrdinal
+ }
+ with :? BadImageFormatException ->
+ invalidData "lambda map" reader.Offset
+
+ methodOrdinal, List.ofSeq closures, List.ofSeq lambdas
+ finally
+ handle.Free()
+
+// ---------------------------------------------------------------------------
+// EnC State Machine State Map
+// Format (SerializeStateMachineStates lines 364-381, UncompressStateMachineStates
+// lines 309-362): compressed(count); when count > 0: compressed(-baseline) followed by
+// count * [compressedSigned(stateNumber), compressed(syntaxOffset - baseline)], entries
+// ordered by syntax offset.
+// ---------------------------------------------------------------------------
+
+/// Serializes the EnC State Machine State Map blob for 'info', byte-for-byte as
+/// Roslyn's SerializeStateMachineStates: entries are sorted by syntax offset (stably,
+/// preserving relative order of equal offsets, which encodes the per-offset relative
+/// ordinal). Returns the empty array when there are no states (no CDI row then).
+let serializeStateMachineStates (info: EncMethodDebugInformation) : byte[] =
+ match info.StateMachineStates with
+ | [] -> Array.empty
+ | states ->
+ let builder = BlobBuilder()
+ builder.WriteCompressedInteger states.Length
+
+ // Unlike the other two blobs the baseline here is min(minOffset, 0)
+ // (Roslyn line 372).
+ let syntaxOffsetBaseline =
+ min (states |> List.map (fun s -> s.SyntaxOffset) |> List.min) 0
+
+ builder.WriteCompressedInteger(-syntaxOffsetBaseline)
+
+ // Roslyn's reader rejects more than 256 entries sharing one syntax offset
+ // (relative ordinal must fit a byte, line 344); fail closed at write time.
+ for _, group in states |> List.groupBy (fun s -> s.SyntaxOffset) do
+ if group.Length > 256 then
+ invalidArg (nameof info) $"more than 256 state machine states share syntax offset %d{group.Head.SyntaxOffset}"
+
+ for state in states |> List.sortBy (fun s -> s.SyntaxOffset) do
+ builder.WriteCompressedSignedInteger state.StateNumber
+ builder.WriteCompressedInteger(state.SyntaxOffset - syntaxOffsetBaseline)
+
+ builder.ToArray()
+
+/// Deserializes an EnC State Machine State Map blob, byte-for-byte as Roslyn's
+/// UncompressStateMachineStates (including the ordered-by-offset and <= 256-per-offset
+/// validations). An empty (or null) blob yields no states.
+let deserializeStateMachineStates (blob: byte[]) : EncStateMachineStateInfo list =
+ if isEmpty blob then
+ []
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ let states = ResizeArray()
+
+ try
+ let count = reader.ReadCompressedInteger()
+
+ if count > 0 then
+ let syntaxOffsetBaseline = -reader.ReadCompressedInteger()
+ let mutable lastSyntaxOffset = Int32.MinValue
+ let mutable relativeOrdinal = 0
+
+ for _ in 1..count do
+ let stateNumber = reader.ReadCompressedSignedInteger()
+ let syntaxOffset = syntaxOffsetBaseline + reader.ReadCompressedInteger()
+
+ // Entries must be ordered by syntax offset and at most 256 may
+ // share one offset (Roslyn lines 336-347).
+ if syntaxOffset < lastSyntaxOffset then
+ invalidData "state machine state map" reader.Offset
+
+ relativeOrdinal <-
+ if syntaxOffset = lastSyntaxOffset then
+ relativeOrdinal + 1
+ else
+ 0
+
+ if relativeOrdinal > 255 then
+ invalidData "state machine state map" reader.Offset
+
+ states.Add
+ {
+ StateNumber = stateNumber
+ SyntaxOffset = syntaxOffset
+ }
+
+ lastSyntaxOffset <- syntaxOffset
+ with :? BadImageFormatException ->
+ invalidData "state machine state map" reader.Offset
+
+ List.ofSeq states
+ finally
+ handle.Free()
+
+/// Deserializes EnC method debug information from the three blobs (any of which may be
+/// null or empty). Mirrors Roslyn's EditAndContinueMethodDebugInformation.Create.
+let deserialize (slotMapBlob: byte[]) (lambdaMapBlob: byte[]) (stateMachineStateMapBlob: byte[]) : EncMethodDebugInformation =
+ let methodOrdinal, closures, lambdas = deserializeLambdaMap lambdaMapBlob
+
+ {
+ MethodOrdinal = methodOrdinal
+ LocalSlots = deserializeLocalSlots slotMapBlob
+ Closures = closures
+ Lambdas = lambdas
+ StateMachineStates = deserializeStateMachineStates stateMachineStateMapBlob
+ }
+
+// ---------------------------------------------------------------------------
+// Baseline emission bridge: lambda occurrences -> CDI rows for the
+// portable PDB writer. Computed in the fsc emit path when --test:HotReloadDeltas
+// is on; the rows ride the IL writer options into ilwritepdb keyed by IL method name.
+// ---------------------------------------------------------------------------
+
+/// Root-first ordinal chain of an occurrence: the occurrence id stores enclosing
+/// ordinals nearest-enclosing-first, while the key packing wants root-first with the
+/// occurrence's own ordinal last.
+let private occurrenceOrdinalChain (occurrence: LambdaOccurrence) =
+ List.rev occurrence.Id.ParentChain @ [ occurrence.Id.Ordinal ]
+
+/// Builds the EnC method debug information for one member from its lambda
+/// occurrence sequence. Modeling decisions (documented in
+/// docs/hot-reload-closure-mapping.md, "Baseline CDI emission as implemented"):
+/// - MethodOrdinal stays UndefinedMethodOrdinal: F# needs no Roslyn-style
+/// partial-method/ordinal disambiguation at baseline.
+/// - One closure scope per occurrence, and lambda i references closure i: IlxGen
+/// lowers every lambda occurrence (curried group) to its own closure class, so
+/// unlike C# there is no shared display-class scope to model and no static/this-only
+/// lambdas at the typed-tree level (refinement to Static/ThisOnly ordinals is a
+/// lowering-side concern).
+/// - LocalSlots stays empty: the EnC Local Slot Map describes the lowered local slot
+/// layout, an IlxGen emission artifact that is not trivially derivable from the
+/// typed tree; it is omitted rather than guessed.
+/// Fails closed (None) when any occurrence key is not encodable (chains deeper than 2
+/// or ordinals past the packing limits): a partial map could silently mismatch
+/// occurrences, so the method then gets no lambda map at all.
+let tryCreateFromLambdaOccurrences (occurrences: LambdaOccurrence list) : EncMethodDebugInformation option =
+ let keys =
+ occurrences |> List.map (occurrenceOrdinalChain >> tryEncodeOccurrenceKey)
+
+ if keys |> List.exists Option.isNone then
+ None
+ else
+ let keys = keys |> List.map Option.get
+
+ Some
+ {
+ MethodOrdinal = UndefinedMethodOrdinal
+ LocalSlots = []
+ Closures = keys |> List.map (fun key -> { SyntaxOffset = key })
+ Lambdas =
+ keys
+ |> List.mapi (fun closureOrdinal key ->
+ {
+ SyntaxOffset = key
+ ClosureOrdinal = closureOrdinal
+ })
+ StateMachineStates = []
+ }
+
+/// Computes the per-member EnC method debug information of a flag-on compilation from its
+/// implementation files, keyed by IL method (compiled) name. Keying is fail closed: members
+/// without a compiled name, compiled names claimed by more than one member binding anywhere
+/// in the assembly (overloads, same-named members on different types), and members with
+/// unencodable occurrence chains are omitted, so an entry can never describe the wrong
+/// method. Members without lambda occurrences carry no entry.
+let computeMethodEncDebugInfo (g: TcGlobals) (implFiles: CheckedImplFile list) : Map =
+ let allMembers = implFiles |> List.collect (collectMemberLambdaOccurrences g)
+
+ let ambiguousNames =
+ allMembers
+ |> List.choose (fun (symbol, _) -> symbol.CompiledName)
+ |> List.countBy id
+ |> List.filter (fun (_, count) -> count > 1)
+ |> List.map fst
+ |> Set.ofList
+
+ (Map.empty, allMembers)
+ ||> List.fold (fun acc (symbol: SymbolId, occurrences) ->
+ match symbol.CompiledName, occurrences with
+ | Some methName, _ :: _ when not (Set.contains methName ambiguousNames) ->
+ match tryCreateFromLambdaOccurrences occurrences with
+ | Some info -> Map.add methName info acc
+ | None -> acc
+ | _ -> acc)
+
+/// Computes the per-method EnC CustomDebugInformation side channel for the baseline PDB
+/// writer from the optimized implementation files of a flag-on compilation, keyed by IL
+/// method (compiled) name (fail-closed keying per computeMethodEncDebugInfo) — the writer
+/// additionally drops any name that does not identify exactly one IL method row, so a map
+/// can never attach to the wrong method.
+let computeMethodCustomDebugInfoRows
+ (g: TcGlobals)
+ (implFiles: CheckedImplFile list)
+ (stateMachineResumePointsByStructName: Map)
+ : Map =
+
+ // State machine resume points are recorded by the IlxGen lowering against the
+ // emitted state machine STRUCT's full name ('{member}@hotreload...' nested in the
+ // member's enclosing type); the basic name of the struct's simple name is the
+ // owning member's compiled name, which is this conduit's key. Fail closed on
+ // collisions (two recordings reducing to one basic name: same-named members, or
+ // nested CEs lowering several machines inside one member) — a state map must never
+ // describe the wrong method. The PDB writer additionally drops any name that does
+ // not identify exactly one IL method row.
+ let recordedStateMachineStatesByMethodName =
+ let simpleName (fullName: string) =
+ let separatorIndex = fullName.LastIndexOfAny [| '+'; '.' |]
+
+ if separatorIndex >= 0 then
+ fullName.Substring(separatorIndex + 1)
+ else
+ fullName
+
+ let basicName (name: string) =
+ match name.IndexOf('@') with
+ | atIndex when atIndex > 0 -> name.Substring(0, atIndex)
+ | _ -> name
+
+ stateMachineResumePointsByStructName
+ |> Map.toList
+ |> List.map (fun (structFullName, resumePoints) -> basicName (simpleName structFullName), resumePoints)
+ |> List.groupBy fst
+ |> List.choose (fun (methName, group) ->
+ match group with
+ | [ (_, resumePoints) ] when not resumePoints.IsEmpty ->
+ // SyntaxOffset carries the resume point's ORDINAL (state numbers are
+ // positional in the F# lowering), keeping the occurrence-key
+ // philosophy: deterministic ints, not source offsets.
+ let states =
+ resumePoints
+ |> List.sortBy id
+ |> List.mapi (fun ordinal stateNumber ->
+ {
+ StateNumber = stateNumber
+ SyntaxOffset = ordinal
+ })
+
+ Some(methName, states)
+ | _ -> None)
+ |> Map.ofList
+
+ let derivedStateMachineStatesByMethodName =
+ let memberInputs = implFiles |> List.collect (collectMemberDebugInfoInputs g)
+
+ let ambiguousNames =
+ memberInputs
+ |> List.choose (fun input -> input.Symbol.CompiledName)
+ |> List.countBy id
+ |> List.filter (fun (_, count) -> count > 1)
+ |> List.map fst
+ |> Set.ofList
+
+ let tryDeriveStatesFromContinuations (occurrences: LambdaOccurrence list) =
+ let roots =
+ occurrences
+ |> List.filter (fun occurrence -> List.isEmpty occurrence.Id.ParentChain)
+
+ match roots with
+ | [ root ] ->
+ let sameEnd (occurrence: LambdaOccurrence) =
+ occurrence.Range.EndLine = root.Range.EndLine
+ && occurrence.Range.EndColumn = root.Range.EndColumn
+
+ let continuations =
+ occurrences
+ |> List.filter (fun occurrence -> not (List.isEmpty occurrence.Id.ParentChain) && sameEnd occurrence)
+
+ match continuations with
+ | [] -> None
+ | _ ->
+ continuations
+ |> List.mapi (fun ordinal _ ->
+ {
+ StateNumber = ordinal + 1
+ SyntaxOffset = ordinal
+ })
+ |> Some
+ | _ -> None
+
+ (Map.empty, memberInputs)
+ ||> List.fold (fun acc input ->
+ match input.Symbol.CompiledName, input.HasResumableStateMachine with
+ | Some methName, true when not (Set.contains methName ambiguousNames) ->
+ match tryDeriveStatesFromContinuations input.LambdaOccurrences with
+ | Some states -> Map.add methName states acc
+ | None -> acc
+ | _ -> acc)
+
+ let stateMachineStatesByMethodName =
+ (recordedStateMachineStatesByMethodName, derivedStateMachineStatesByMethodName)
+ ||> Map.fold (fun acc methName states ->
+ if Map.containsKey methName acc then
+ acc
+ else
+ Map.add methName states acc)
+
+ let lambdaRows =
+ (Map.empty, computeMethodEncDebugInfo g implFiles)
+ ||> Map.fold (fun acc methName info ->
+ let lambdaMapBlob = serializeLambdaMap info
+
+ if lambdaMapBlob.Length = 0 then
+ acc
+ else
+ // The EnC Local Slot Map stays omitted (see tryCreateFromLambdaOccurrences).
+ Map.add
+ methName
+ [
+ {
+ KindGuid = PortableCustomDebugInfoKinds.encLambdaAndClosureMap
+ Blob = lambdaMapBlob
+ }
+ ]
+ acc)
+
+ (lambdaRows, stateMachineStatesByMethodName)
+ ||> Map.fold (fun acc methName states ->
+ let stateMapBlob =
+ serializeStateMachineStates
+ { EncMethodDebugInformation.Empty with
+ StateMachineStates = states
+ }
+
+ if stateMapBlob.Length = 0 then
+ acc
+ else
+ let stateRow: PdbMethodCustomDebugInfo =
+ {
+ KindGuid = PortableCustomDebugInfoKinds.encStateMachineStateMap
+ Blob = stateMapBlob
+ }
+
+ match Map.tryFind methName acc with
+ | Some rows -> Map.add methName (rows @ [ stateRow ]) acc
+ | None -> Map.add methName [ stateRow ] acc)
+
+// ---------------------------------------------------------------------------
+// Baseline read bridge: portable-PDB EnC CDI rows -> the per-method map the
+// hot reload session baseline (FSharpEmitBaseline.EncMethodDebugInfos) exposes to the
+// generation-aware closure lowering.
+// ---------------------------------------------------------------------------
+
+/// Decodes every method-level EnC CustomDebugInformation row of a portable PDB image into
+/// per-method EnC debug information, keyed by MethodDef token (0x06xxxxxx). The CDI parent
+/// of the EnC rows is always a MethodDef handle, so token keying is unambiguous here — the
+/// name keying on the write side exists only because the PDB writer lacks tokens.
+/// Fail safe: a null/empty or non-PDB image yields the empty map (back-compat with
+/// baselines compiled without --test:HotReloadDeltas or whose PDBs carry no EnC rows), and a method
+/// whose blobs do not decode is omitted rather than guessed.
+let readEncMethodDebugInfoFromPortablePdb (pdbBytes: byte[]) : Map =
+ if isEmpty pdbBytes then
+ Map.empty
+ else
+ try
+ use provider =
+ MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.CreateRange pdbBytes)
+
+ let reader = provider.GetMetadataReader()
+
+ let slotMapBlobs = Dictionary()
+ let lambdaMapBlobs = Dictionary()
+ let stateMapBlobs = Dictionary()
+
+ for cdiHandle in reader.CustomDebugInformation do
+ let cdi = reader.GetCustomDebugInformation cdiHandle
+
+ if cdi.Parent.Kind = HandleKind.MethodDefinition then
+ let methodToken = MetadataTokens.GetToken cdi.Parent
+ let kind = reader.GetGuid cdi.Kind
+
+ if kind = PortableCustomDebugInfoKinds.encLocalSlotMap then
+ slotMapBlobs[methodToken] <- reader.GetBlobBytes cdi.Value
+ elif kind = PortableCustomDebugInfoKinds.encLambdaAndClosureMap then
+ lambdaMapBlobs[methodToken] <- reader.GetBlobBytes cdi.Value
+ elif kind = PortableCustomDebugInfoKinds.encStateMachineStateMap then
+ stateMapBlobs[methodToken] <- reader.GetBlobBytes cdi.Value
+
+ let methodTokens =
+ Seq.concat [ slotMapBlobs.Keys :> seq; lambdaMapBlobs.Keys; stateMapBlobs.Keys ]
+ |> Seq.distinct
+
+ let tryBlob (blobs: Dictionary) token =
+ match blobs.TryGetValue token with
+ | true, blob -> blob
+ | _ -> Array.empty
+
+ (Map.empty, methodTokens)
+ ||> Seq.fold (fun acc token ->
+ try
+ let info =
+ deserialize (tryBlob slotMapBlobs token) (tryBlob lambdaMapBlobs token) (tryBlob stateMapBlobs token)
+
+ Map.add token info acc
+ with :? InvalidDataException ->
+ // Fail closed per method: an undecodable blob never yields a partial
+ // (and so potentially mismatched) map for its method.
+ acc)
+ with :? BadImageFormatException ->
+ // Not a portable PDB image (or a corrupted one): the session still starts,
+ // with no per-method EnC information.
+ Map.empty
+
+/// Reads the F#-owned allocation-ordered synthesized-name snapshot from a portable PDB.
+/// None means either the record is absent (old baseline / flag-off baseline) or invalid;
+/// callers must then fall back to IL reconstruction rather than trusting a partial layout.
+let readSynthesizedNameSnapshotFromPortablePdb (pdbBytes: byte[]) : Map 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
diff --git a/src/Compiler/CodeGen/FSharpDefinitionIndex.fs b/src/Compiler/CodeGen/FSharpDefinitionIndex.fs
new file mode 100644
index 00000000000..5cc78697473
--- /dev/null
+++ b/src/Compiler/CodeGen/FSharpDefinitionIndex.fs
@@ -0,0 +1,105 @@
+module internal FSharp.Compiler.CodeGen.FSharpDefinitionIndex
+
+open System.Collections.Generic
+
+/// Represents the status of a definition row tracked in the index.
+type private EntryStatus<'T> =
+ | Added of rowId: int * item: 'T
+ | Existing of rowId: int * item: 'T
+
+/// F# analogue of Roslyn's DefinitionIndex
+/// Track row ids for definitions reused from the baseline or added in this generation.
+type DefinitionIndex<'T when 'T: not null and 'T: equality>(getExistingRowId: 'T -> int option, lastRowId: int) =
+ let added = Dictionary<'T, int>()
+ let rows = ResizeArray>()
+ let map = Dictionary()
+ let firstRowId = lastRowId + 1
+ let mutable frozen = false
+
+ let tryGetExistingRowId item =
+ match getExistingRowId item with
+ | Some rowId when rowId > 0 ->
+ map[rowId] <- item
+ Some rowId
+ | _ -> None
+
+ let getRowIdCore item =
+ match added.TryGetValue item with
+ | true, rowId -> rowId
+ | false, _ ->
+ match tryGetExistingRowId item with
+ | Some rowId -> rowId
+ | None -> invalidOp "Row id not found for definition."
+
+ let ensureNotFrozen () =
+ if frozen then
+ invalidOp "Definition index has been frozen."
+
+ let freeze () =
+ if not frozen then
+ frozen <- true
+
+ rows.Sort(fun left right ->
+ let rowId entry =
+ match entry with
+ | Added(rowId, _) -> rowId
+ | Existing(rowId, _) -> rowId
+
+ compare (rowId left) (rowId right))
+
+ member _.Add(item: 'T) =
+ ensureNotFrozen ()
+
+ if added.ContainsKey item then
+ invalidOp "Definition has already been added."
+
+ let rowId = firstRowId + added.Count
+ added.Add(item, rowId)
+ map[rowId] <- item
+ rows.Add(Added(rowId, item))
+ rowId
+
+ member _.AddExisting(item: 'T) =
+ ensureNotFrozen ()
+
+ match tryGetExistingRowId item with
+ | Some rowId -> rows.Add(Existing(rowId, item))
+ | None -> invalidOp "Existing row id not found for definition."
+
+ member _.GetRowId(item: 'T) = getRowIdCore item
+
+ member _.Contains(item: 'T) =
+ match added.TryGetValue item with
+ | true, _ -> true
+ | _ -> Option.isSome (tryGetExistingRowId item)
+
+ member _.IsAdded(item: 'T) = added.ContainsKey item
+
+ member _.TryGetDefinition(rowId: int) =
+ match map.TryGetValue rowId with
+ | true, item -> Some item
+ | _ -> None
+
+ member _.FirstRowId = firstRowId
+
+ member _.NextRowId = firstRowId + added.Count
+
+ member _.IsFrozen = frozen
+
+ member _.Rows =
+ freeze ()
+
+ rows
+ |> Seq.map (fun entry ->
+ match entry with
+ | Added(rowId, item) -> struct (rowId, item, true)
+ | Existing(rowId, item) -> struct (rowId, item, false))
+ |> Seq.toList
+
+ member _.Added =
+ freeze ()
+
+ added
+ |> Seq.map (fun kvp -> struct (kvp.Value, kvp.Key))
+ |> Seq.sortBy (fun struct (rowId, _) -> rowId)
+ |> Seq.toList
diff --git a/src/Compiler/CodeGen/HotReloadBaseline.fs b/src/Compiler/CodeGen/HotReloadBaseline.fs
index f68e3b10419..e86df0c2720 100644
--- a/src/Compiler/CodeGen/HotReloadBaseline.fs
+++ b/src/Compiler/CodeGen/HotReloadBaseline.fs
@@ -3,320 +3,677 @@ module internal FSharp.Compiler.HotReloadBaseline
open System
open System.Collections.Generic
open System.Collections.Immutable
-
-open FSharp.Compiler.AbstractIL.EncMethodDebugInformation
+open System.Reflection
open FSharp.Compiler.AbstractIL.IL
-open FSharp.Compiler.CodeGen
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.AbstractIL.DeltaMetadataTypes
open FSharp.Compiler.CompilerGeneratedNameMapState
+open FSharp.Compiler.EncMethodDebugInformation
open FSharp.Compiler.GeneratedNames
+open FSharp.Compiler.IlxGen
+open FSharp.Compiler.TcGlobals
+open FSharp.Compiler.TypedTree
+
+module ILBaselineReader = FSharp.Compiler.AbstractIL.ILBaselineReader
+module ActiveStatementAnalysis = FSharp.Compiler.HotReload.ActiveStatementAnalysis
+
open FSharp.Compiler.Syntax.PrettyNaming
+open FSharp.Compiler.EnvironmentHelpers
-[]
-type SynthesizedNameSnapshotSource =
- | Recorded
- | Reconstructed
+let private tableCount = DeltaTokens.TableCount
-type PortablePdbSnapshot =
+[]
+let private TraceHeapOffsetsFlagName = "FSHARP_HOTRELOAD_TRACE_HEAP_OFFSETS"
+
+let private traceHeapOffsets = lazy (isEnvVarTruthy TraceHeapOffsetsFlagName)
+
+let private traceClosureNames =
+ lazy (isEnvVarTruthy "FSHARP_HOTRELOAD_TRACE_CLOSURENAMES")
+
+/// Align a size to a 4-byte boundary (stream alignment per ECMA-335).
+/// Used for Blob and UserString heap cumulative tracking, per Roslyn behavior.
+let private align4 value = (value + 3) &&& ~~~3
+
+/// Metadata describing a method body that was added or changed in a delta.
+type AddedOrChangedMethodInfo =
{
- Bytes: byte[]
- TableRowCounts: ImmutableArray
- EntryPointToken: int option
+ MethodToken: int
+ LocalSignatureToken: int
+ CodeOffset: int
+ CodeLength: int
}
-type TypeDefinitionKey =
+/// Stable identifier for a method definition used when correlating baseline tokens.
+type MethodDefinitionKey = FSharp.Compiler.AbstractIL.DeltaMetadataTypes.MethodDefinitionKey
+
+/// Baseline metadata handles reused to keep heap offsets stable across deltas.
+/// Stable identifier for a method parameter (sequence number within a method).
+type ParameterDefinitionKey = FSharp.Compiler.AbstractIL.DeltaMetadataTypes.ParameterDefinitionKey
+
+/// Stable identifier for a field definition in the baseline assembly.
+type FieldDefinitionKey = FSharp.Compiler.AbstractIL.DeltaMetadataTypes.FieldDefinitionKey
+
+/// Stable identifier for a property definition (including indexer parameter shapes).
+type PropertyDefinitionKey = FSharp.Compiler.AbstractIL.DeltaMetadataTypes.PropertyDefinitionKey
+
+/// Stable identifier for an event definition in the baseline assembly.
+type EventDefinitionKey = FSharp.Compiler.AbstractIL.DeltaMetadataTypes.EventDefinitionKey
+
+type MethodDefinitionMetadataHandles =
{
- RowId: int
- Namespace: string
- Name: string
+ NameOffset: StringOffset option
+ SignatureOffset: BlobOffset option
+ FirstParameterRowId: int option
+ Rva: int option
+ Attributes: MethodAttributes option
+ ImplAttributes: MethodImplAttributes option
}
-type MethodDefinitionKey =
+///
+/// Typed identity for a TypeRef resolution scope. Baseline TypeRef tables routinely contain
+/// duplicate type names under different scopes (e.g. two 'Object' rows under different
+/// AssemblyRefs, 'LowPriority' under two namespaces) and nested TypeRefs whose scope is the
+/// enclosing TypeRef row, so a TypeRef can only be matched by its full scope chain - never by
+/// name alone.
+///
+[]
+type AssemblyReferenceKey =
{
- DeclaringType: TypeDefinitionKey
Name: string
- Signature: byte list
+ MajorVersion: int
+ MinorVersion: int
+ BuildNumber: int
+ RevisionNumber: int
+ Culture: string
+ PublicKeyOrToken: byte list
+ Flags: int
}
-type FieldDefinitionKey =
+[]
+type TypeReferenceScope =
+ /// TypeRef resolved against an AssemblyRef row, identified by its complete metadata identity.
+ | Assembly of assembly: AssemblyReferenceKey
+ /// Nested TypeRef whose resolution scope is its enclosing TypeRef.
+ | Nested of enclosing: TypeReferenceKey
+
+and TypeReferenceKey =
{
- DeclaringType: TypeDefinitionKey
+ Scope: TypeReferenceScope
+ Namespace: string
Name: string
- Signature: byte list
}
-type PropertyDefinitionKey =
+type ParameterDefinitionMetadataHandles =
{
- DeclaringType: TypeDefinitionKey
- Name: string
- Signature: byte list
+ NameOffset: StringOffset option
+ /// Baseline parameter name (resolved from the #Strings heap). Param row re-emission
+ /// reuses the baseline name offset only when the fresh compile's name matches;
+ /// a differing name (parameter rename under UpdateParameters) writes the fresh name
+ /// into the delta string heap instead.
+ Name: string option
+ RowId: int option
}
-type EventDefinitionKey =
+type PropertyDefinitionMetadataHandles =
+ {
+ NameOffset: StringOffset option
+ SignatureOffset: BlobOffset option
+ }
+
+type EventDefinitionMetadataHandles = { NameOffset: StringOffset option }
+
+/// Content snapshot of a baseline MemberRef row, used by the delta emitter to VALIDATE
+/// positional token passthrough (the fresh in-memory compile's MemberRef row order can
+/// shift relative to the baseline — e.g. when an added lambda changes the order of first
+/// use — so a row id is only trusted when its content matches the baseline row).
+type BaselineMemberRefRow =
{
- DeclaringType: TypeDefinitionKey
Name: string
- EventType: int
+ /// Decoded MemberRefParent as a metadata token (0x02/0x01/0x1A/0x06/0x1B tables).
+ ParentToken: int
+ /// Signature blob bytes (baseline coordinates).
+ Signature: byte[]
}
-type BaselineTokenMaps =
+/// Content snapshot of a baseline CustomAttribute row. Attribute edits on EXISTING members
+/// pair the fresh compile's attributes against these rows so changed attributes
+/// UPDATE the row in place and removed attributes ZERO it (Roslyn DeltaMetadataWriter
+/// parity, validated against the csharp_enc_reference attr_change/attr_remove templates).
+type BaselineCustomAttributeRow =
{
- TypeTokens: Map
- MethodTokens: Map
- FieldTokens: Map
- PropertyTokens: Map
- EventTokens: Map
+ /// Decoded HasCustomAttribute parent as a metadata token.
+ ParentToken: int
+ /// Decoded CustomAttributeType constructor as a metadata token (0x06/0x0A tables).
+ ConstructorToken: int
+ /// Value blob bytes (baseline coordinates).
+ Value: byte[]
+ }
+
+type BaselineHandleCache =
+ {
+ MethodHandles: Map
+ ParameterHandles: Map
+ PropertyHandles: Map
+ EventHandles: Map
+ }
+
+ static member Empty =
+ {
+ MethodHandles = Map.empty
+ ParameterHandles = Map.empty
+ PropertyHandles = Map.empty
+ EventHandles = Map.empty
+ }
+
+type MethodSemanticsAssociation = FSharp.Compiler.AbstractIL.DeltaMetadataTypes.MethodSemanticsAssociation
+
+type MethodSemanticsEntry =
+ {
+ RowId: int
+ Attributes: MethodSemanticsAttributes
+ Association: MethodSemanticsAssociation
+ }
+
+type SynthesizedTypeShape =
+ {
+ GenericArity: int
+ BaseType: string option
+ InterfaceTypes: string list
+ FieldTypeNames: string list
+ MethodNameAndArities: (string * int) list
+ }
+
+let rec private ilTypeShapeName (ty: ILType) =
+ match ty with
+ | ILType.Void -> "void"
+ | ILType.TypeVar ordinal -> "!" + string ordinal
+ | ILType.Array(ILArrayShape dimensions, elementType) ->
+ ilTypeShapeName elementType
+ + "["
+ + String(',', max 0 (dimensions.Length - 1))
+ + "]"
+ | ILType.Value typeSpec -> "valuetype " + ilTypeSpecShapeName typeSpec
+ | ILType.Boxed typeSpec -> "class " + ilTypeSpecShapeName typeSpec
+ | ILType.Ptr elementType -> ilTypeShapeName elementType + "*"
+ | ILType.Byref elementType -> ilTypeShapeName elementType + "&"
+ | ILType.FunctionPointer signature ->
+ let args = signature.ArgTypes |> List.map ilTypeShapeName |> String.concat ","
+ $"fnptr({args})->{ilTypeShapeName signature.ReturnType}"
+ | ILType.Modified(required, modifier, modifiedType) ->
+ let modifierKind = if required then "modreq" else "modopt"
+ $"{modifierKind}({modifier.QualifiedName}) {ilTypeShapeName modifiedType}"
+
+and private ilTypeSpecShapeName (typeSpec: ILTypeSpec) =
+ if List.isEmpty typeSpec.GenericArgs then
+ typeSpec.TypeRef.QualifiedName
+ else
+ let args = typeSpec.GenericArgs |> List.map ilTypeShapeName |> String.concat ","
+ $"{typeSpec.TypeRef.QualifiedName}<{args}>"
+
+let internal shapeOfSynthesizedTypeDef (typeDef: ILTypeDef) : SynthesizedTypeShape =
+ {
+ GenericArity = typeDef.GenericParams.Length
+ BaseType = typeDef.Extends.Value |> Option.map ilTypeShapeName
+ InterfaceTypes =
+ typeDef.Implements.Value
+ |> List.map (fun implementation -> ilTypeShapeName implementation.Type)
+ |> List.sort
+ FieldTypeNames =
+ typeDef.Fields.AsList()
+ |> List.map (fun fieldDef -> ilTypeShapeName fieldDef.FieldType)
+ |> List.sort
+ MethodNameAndArities =
+ typeDef.Methods.AsList()
+ |> List.map (fun methodDef -> methodDef.Name, methodDef.GenericParams.Length)
+ |> List.distinct
+ |> List.sort
+ }
+
+/// Portable PDB snapshot captured during baseline emission.
+type PortablePdbSnapshot =
+ {
+ Bytes: byte[]
+ TableRowCounts: ImmutableArray
+ EntryPointToken: int option
}
+[]
+type SynthesizedNameSnapshotSource =
+ | Recorded
+ | Reconstructed
+
+///
+/// Represents the captured state of a baseline emission, mirroring Roslyn's EmitBaseline. It stores metadata
+/// snapshots along with stable token maps so delta emission can reuse pre-existing metadata handles.
+///
type FSharpEmitBaseline =
{
ModuleId: Guid
- Metadata: ILBaselineReader.MetadataSnapshot
+ EncId: Guid
+ EncBaseId: Guid
+ NextGeneration: int
+ ModuleNameOffset: StringOffset option
+ Metadata: MetadataSnapshot
+ TokenMappings: ILTokenMappings
+ TypeTokens: Map
+ MethodTokens: Map
+ FieldTokens: Map
+ PropertyTokens: Map
+ EventTokens: Map
+ PropertyMapEntries: Map
+ EventMapEntries: Map
+ MethodSemanticsEntries: Map
+ IlxGenEnvironment: IlxGenEnvSnapshot option
PortablePdb: PortablePdbSnapshot option
- TokenMaps: BaselineTokenMaps
SynthesizedNameSnapshot: Map
SynthesizedNameSnapshotSource: SynthesizedNameSnapshotSource
+ SynthesizedTypeShapes: Map
+ MetadataHandles: BaselineHandleCache
+ TypeReferenceTokens: Map
+ AssemblyReferenceTokens: Map
+ /// Baseline MemberRef row contents keyed by row id, for content-validated token
+ /// passthrough in delta emission (extended with delta-added rows on chaining).
+ /// Empty for baselines whose bytes were unavailable — passthrough then stays
+ /// positional (legacy behavior).
+ MemberReferenceRows: Map
+ /// Baseline TypeSpec signature blobs keyed by row id, for content-validated
+ /// TypeSpec token reuse. An unmatched fresh TypeSpec appends a new delta row;
+ /// appended rows chain into this map for the next generation's content search.
+ TypeSpecSignatures: Map
+ /// Baseline CustomAttribute row contents keyed by row id (decoded parent/ctor
+ /// tokens + value blob). Attribute edits on existing members update/zero these
+ /// rows in place; rows emitted by a delta chain into the map for the next
+ /// generation. Empty for baselines whose bytes were unavailable — CA emission
+ /// then stays append-only (legacy behavior).
+ CustomAttributeRows: Map
+ TableEntriesAdded: int[]
+ StringStreamLengthAdded: int
+ UserStringStreamLengthAdded: int
+ BlobStreamLengthAdded: int
+ GuidStreamLengthAdded: int
+ AddedOrChangedMethods: AddedOrChangedMethodInfo list
+ ///
+ /// Per-method Edit-and-Continue debug information (lambda/closure occurrence maps),
+ /// keyed by MethodDef token (0x06xxxxxx). Decoded from the baseline portable PDB's EnC
+ /// CustomDebugInformation rows when the baseline is captured, and refreshed in memory
+ /// for updated/added methods as each delta is applied (see chainEncMethodDebugInfos).
+ /// A baseline compiled without --test:HotReloadDeltas (or whose PDB carries no EnC rows)
+ /// yields the empty map.
+ ///
EncMethodDebugInfos: Map
+ ///
+ /// Per-method closure-class name tables (occurrence-chain -> emitted closure type
+ /// name), keyed by MethodDef token (0x06xxxxxx) — the companion of
+ /// EncMethodDebugInfos. The Roslyn CDI blob formats carry no name slots, and like
+ /// Roslyn (which recomputes C# names from DebugId alone) F# does not persist
+ /// names: under the occurrence-derived derivation baseline closure names are a pure function of
+ /// occurrence identity ({member}@hotreload#g0_o{chain}), so the tables are
+ /// reconstructed from the decoded EnC CDI occurrence keys — for in-process
+ /// captures and for baselines read back from disk in another process alike (see
+ /// deriveEncClosureNamesFromEncDebugInfos for the fail-closed rules) — and
+ /// chained in memory like EncMethodDebugInfos as deltas allocate
+ /// generation-suffixed names for added occurrences. Empty for flag-off,
+ /// replay-named (non-derivable) and mid-session-recapture baselines: occurrence-keyed naming then stays inert
+ /// and delta compiles keep sequence replay (fail closed).
+ ///
EncClosureNames: Map>
+ ///
+ /// Committed per-method sequence points keyed by MethodDef token (0x06xxxxxx) — the
+ /// debugger's current view of each method's lines. Decoded from the baseline
+ /// portable PDB when the session starts and REPLACED wholesale after every committed delta
+ /// with the fresh compile's sequence points (updated methods get their delta-PDB points;
+ /// unchanged methods get their line-shift-adjusted points, matching the line updates the
+ /// host applied to the debugger). Line-shift detection and active-statement remapping diff
+ /// fresh compiles against this map; empty when the baseline had no portable PDB, which
+ /// keeps the sequence-point/active-statement machinery inert (fail closed).
+ ///
+ SequencePointSnapshots: Map
}
-let private typeDefToken rowId = (0x02 <<< 24) ||| rowId
-let private fieldToken rowId = (0x04 <<< 24) ||| rowId
-let private methodDefToken rowId = (0x06 <<< 24) ||| rowId
-let private eventToken rowId = (0x14 <<< 24) ||| rowId
-let private propertyToken rowId = (0x17 <<< 24) ||| rowId
-
-let private typeFullName (key: TypeDefinitionKey) =
- if String.IsNullOrEmpty key.Namespace then
- key.Name
- else
- key.Namespace + "." + key.Name
-
-let private signatureList (bytes: byte[]) = bytes |> Array.toList
-
-let private buildTypeKeys (reader: ILBaselineReader.BaselineMetadataReader) =
- [
- for rowId in 1 .. reader.TypeDefCount do
- match reader.GetTypeDef rowId with
- | Some row ->
- yield
- rowId,
- {
- RowId = rowId
- Namespace = reader.GetString row.NamespaceOffset
- Name = reader.GetString row.NameOffset
- }
- | None -> ()
- ]
- |> Map.ofList
+type private BaselineMaps =
+ {
+ TypeTokens: Map
+ MethodTokens: Map
+ FieldTokens: Map
+ PropertyTokens: Map
+ EventTokens: Map
+ PropertyMapEntries: Map
+ EventMapEntries: Map
+ SynthesizedTypeShapes: Map
+ }
-let private emptyTokenMaps =
+let private emptyMaps =
{
TypeTokens = Map.empty
MethodTokens = Map.empty
FieldTokens = Map.empty
PropertyTokens = Map.empty
EventTokens = Map.empty
+ PropertyMapEntries = Map.empty
+ EventMapEntries = Map.empty
+ SynthesizedTypeShapes = Map.empty
}
-let private buildTokenMaps (reader: ILBaselineReader.BaselineMetadataReader) =
- let typeKeys = buildTypeKeys reader
-
- let typeTokens: Map =
- typeKeys
- |> Map.toSeq
- |> Seq.map (fun (rowId, key) -> key, typeDefToken rowId)
- |> Map.ofSeq
-
- let methodTokens: Map =
- seq {
- for KeyValue(typeRowId, typeKey) in typeKeys do
- match reader.GetTypeMethodRange typeRowId with
- | None -> ()
- | Some(firstMethod, lastMethod) ->
- for methodRowId in firstMethod..lastMethod do
- match reader.GetMethodDef methodRowId with
- | None -> ()
- | Some methodDef ->
- let key: MethodDefinitionKey =
- {
- DeclaringType = typeKey
- Name = reader.GetString methodDef.NameOffset
- Signature = reader.GetBlob methodDef.SignatureOffset |> signatureList
- }
-
- yield key, methodDefToken methodRowId
- }
- |> Map.ofSeq
-
- let fieldTokens: Map =
- seq {
- for KeyValue(typeRowId, typeKey) in typeKeys do
- match reader.GetTypeFieldRange typeRowId with
- | None -> ()
- | Some(firstField, lastField) ->
- for fieldRowId in firstField..lastField do
- match reader.GetField fieldRowId with
- | None -> ()
- | Some fieldDef ->
- let key: FieldDefinitionKey =
- {
- DeclaringType = typeKey
- Name = reader.GetString fieldDef.NameOffset
- Signature = reader.GetBlob fieldDef.SignatureOffset |> signatureList
- }
-
- yield key, fieldToken fieldRowId
- }
- |> Map.ofSeq
-
- let propertyTokens: Map =
- seq {
- for propertyMapRowId in 1 .. reader.PropertyMapCount do
- match reader.GetPropertyMapRange propertyMapRowId with
- | Some(parentTypeRowId, firstProperty, lastProperty) ->
- match Map.tryFind parentTypeRowId typeKeys with
- | None -> ()
- | Some typeKey ->
- for propertyRowId in firstProperty..lastProperty do
- match reader.GetProperty propertyRowId with
- | None -> ()
- | Some propertyDef ->
- let key: PropertyDefinitionKey =
- {
- DeclaringType = typeKey
- Name = reader.GetString propertyDef.NameOffset
- Signature = reader.GetBlob propertyDef.SignatureOffset |> signatureList
- }
-
- yield key, propertyToken propertyRowId
- | None -> ()
- }
- |> Map.ofSeq
-
- let eventTokens: Map =
- seq {
- for eventMapRowId in 1 .. reader.EventMapCount do
- match reader.GetEventMapRange eventMapRowId with
- | Some(parentTypeRowId, firstEvent, lastEvent) ->
- match Map.tryFind parentTypeRowId typeKeys with
- | None -> ()
- | Some typeKey ->
- for eventRowId in firstEvent..lastEvent do
- match reader.GetEvent eventRowId with
- | None -> ()
- | Some eventDef ->
- let key: EventDefinitionKey =
- {
- DeclaringType = typeKey
- Name = reader.GetString eventDef.NameOffset
- EventType = eventDef.EventType
- }
-
- yield key, eventToken eventRowId
- | None -> ()
- }
- |> Map.ofSeq
+let internal collectSynthesizedNameSnapshot (ilModule: ILModuleDef) =
+ let buckets = Dictionary>(StringComparer.Ordinal)
- {
- TypeTokens = typeTokens
- MethodTokens = methodTokens
- FieldTokens = fieldTokens
- PropertyTokens = propertyTokens
- EventTokens = eventTokens
- }
+ let recordName (name: string) =
+ if not (String.IsNullOrWhiteSpace name) && IsCompilerGeneratedName name then
+ let basicName = GetBasicNameOfPossibleCompilerGeneratedName name
+ let mapKey = GeneratedNames.SynthesizedNameMapKey basicName
+
+ if not (String.IsNullOrWhiteSpace mapKey) then
+ let bucket =
+ match buckets.TryGetValue mapKey with
+ | true, existing -> existing
+ | _ ->
+ let created = ResizeArray()
+ buckets[mapKey] <- created
+ created
+
+ if not (bucket.Contains name) then
+ bucket.Add(name)
+
+ let rec collectTypeDef (typeDef: ILTypeDef) =
+ recordName typeDef.Name
+
+ typeDef.Fields.AsList() |> List.iter (fun fieldDef -> recordName fieldDef.Name)
+
+ typeDef.Methods.AsList()
+ |> List.iter (fun methodDef -> recordName methodDef.Name)
-let private addSynthesizedName (buckets: Dictionary>) (name: string) =
- if not (String.IsNullOrWhiteSpace name) && IsCompilerGeneratedName name then
- let basicName = GetBasicNameOfPossibleCompilerGeneratedName name
- let mapKey = SynthesizedNameMapKey basicName
+ typeDef.Properties.AsList()
+ |> List.iter (fun propertyDef -> recordName propertyDef.Name)
+
+ typeDef.Events.AsList() |> List.iter (fun eventDef -> recordName eventDef.Name)
- if not (String.IsNullOrWhiteSpace mapKey) then
- let bucket =
- match buckets.TryGetValue mapKey with
- | true, existing -> existing
- | _ ->
- let created = ResizeArray()
- buckets[mapKey] <- created
- created
+ typeDef.NestedTypes.AsList() |> List.iter collectTypeDef
- if not (bucket.Contains name) then
- bucket.Add name
+ ilModule.TypeDefs.AsList() |> List.iter collectTypeDef
-let private snapshotFromBuckets (buckets: Dictionary>) =
buckets
|> Seq.map (fun (KeyValue(key, bucket)) -> key, bucket.ToArray())
|> Map.ofSeq
-let internal collectSynthesizedNameSnapshot (ilModule: ILModuleDef) =
- let buckets = Dictionary>(StringComparer.Ordinal)
+/// Captures the allocation-slot snapshot from the synthesized-name map that IlxGen
+/// just used, replacing replay names with the final names IlxGen emitted where the
+/// occurrence-keyed closure allocator overrode them.
+let internal collectRecordedSynthesizedNameSnapshot (compilerGlobalState: obj) (map: ICompilerGeneratedNameMap) =
+ let overrides =
+ FSharp.Compiler.ClosureNameAllocationState.getSynthesizedNameOverrides compilerGlobalState
+
+ map.Snapshot
+ |> FSharp.Compiler.ClosureNameAllocationState.applySynthesizedNameOverrides overrides
+
+///
+/// Populate the baseline token maps by walking type definitions and their nested members.
+///
+let rec private collectType
+ (tokenMappings: ILTokenMappings)
+ (scope: ILScopeRef)
+ (enclosing: ILTypeDef list)
+ (maps: BaselineMaps)
+ (tdef: ILTypeDef)
+ : BaselineMaps =
+ let typeRef = mkRefForNestedILTypeDef scope (enclosing, tdef)
+ let typeName = typeRef.FullName
+ let typeToken = tokenMappings.TypeDefTokenMap(enclosing, tdef)
+
+ let maps =
+ { maps with
+ TypeTokens = maps.TypeTokens |> Map.add typeName typeToken
+ }
- let rec collectTypeDef (typeDef: ILTypeDef) =
- addSynthesizedName buckets typeDef.Name
+ let maps =
+ if IsCompilerGeneratedName tdef.Name then
+ { maps with
+ SynthesizedTypeShapes = maps.SynthesizedTypeShapes |> Map.add typeName (shapeOfSynthesizedTypeDef tdef)
+ }
+ else
+ maps
- typeDef.Fields.AsList()
- |> List.iter (fun fieldDef -> addSynthesizedName buckets fieldDef.Name)
+ let maps =
+ tdef.Methods.AsList()
+ |> List.fold
+ (fun (acc: BaselineMaps) mdef ->
+ let key =
+ {
+ DeclaringType = typeName
+ Name = mdef.Name
+ GenericArity = mdef.GenericParams.Length
+ ParameterTypes = mdef.ParameterTypes
+ ReturnType = mdef.Return.Type
+ }
- typeDef.Methods.AsList()
- |> List.iter (fun methodDef -> addSynthesizedName buckets methodDef.Name)
+ let token = tokenMappings.MethodDefTokenMap (enclosing, tdef) mdef
- typeDef.Properties.AsList()
- |> List.iter (fun propertyDef -> addSynthesizedName buckets propertyDef.Name)
+ { acc with
+ MethodTokens = acc.MethodTokens |> Map.add key token
+ })
+ maps
+
+ let maps =
+ tdef.Fields.AsList()
+ |> List.fold
+ (fun (acc: BaselineMaps) fdef ->
+ let key =
+ {
+ DeclaringType = typeName
+ Name = fdef.Name
+ FieldType = fdef.FieldType
+ }
- typeDef.Events.AsList()
- |> List.iter (fun eventDef -> addSynthesizedName buckets eventDef.Name)
+ let token = tokenMappings.FieldDefTokenMap (enclosing, tdef) fdef
- typeDef.NestedTypes.AsList() |> List.iter collectTypeDef
+ { acc with
+ FieldTokens = acc.FieldTokens |> Map.add key token
+ })
+ maps
- ilModule.TypeDefs.AsList() |> List.iter collectTypeDef
- snapshotFromBuckets buckets
+ let propertyDefs = tdef.Properties.AsList()
-let internal collectRecordedSynthesizedNameSnapshot (_compilerGlobalState: obj) (map: ICompilerGeneratedNameMap) = map.Snapshot
+ let maps =
+ propertyDefs
+ |> List.fold
+ (fun (acc: BaselineMaps) pdef ->
+ let key =
+ {
+ DeclaringType = typeName
+ Name = pdef.Name
+ PropertyType = pdef.PropertyType
+ IndexParameterTypes = List.ofSeq pdef.Args
+ }
-let private collectSynthesizedNameSnapshotFromTokens (tokenMaps: BaselineTokenMaps) =
- let buckets = Dictionary>(StringComparer.Ordinal)
+ let token = tokenMappings.PropertyTokenMap (enclosing, tdef) pdef
- for KeyValue(typeKey, _) in tokenMaps.TypeTokens do
- addSynthesizedName buckets typeKey.Name
+ { acc with
+ PropertyTokens = acc.PropertyTokens |> Map.add key token
+ })
+ maps
- for KeyValue(methodKey, _) in tokenMaps.MethodTokens do
- addSynthesizedName buckets methodKey.Name
+ let maps =
+ match propertyDefs with
+ | first :: _ ->
+ let token = tokenMappings.PropertyTokenMap (enclosing, tdef) first
+ let rowId = token &&& 0x00FFFFFF
- for KeyValue(fieldKey, _) in tokenMaps.FieldTokens do
- addSynthesizedName buckets fieldKey.Name
+ { maps with
+ PropertyMapEntries = maps.PropertyMapEntries |> Map.add typeName rowId
+ }
+ | [] -> maps
- for KeyValue(propertyKey, _) in tokenMaps.PropertyTokens do
- addSynthesizedName buckets propertyKey.Name
+ let eventDefs = tdef.Events.AsList()
- for KeyValue(eventKey, _) in tokenMaps.EventTokens do
- addSynthesizedName buckets eventKey.Name
+ let maps =
+ eventDefs
+ |> List.fold
+ (fun (acc: BaselineMaps) edef ->
+ let key =
+ {
+ DeclaringType = typeName
+ Name = edef.Name
+ EventType = edef.EventType
+ }
- snapshotFromBuckets buckets
+ let token = tokenMappings.EventTokenMap (enclosing, tdef) edef
-let private formatOccurrenceChainKey (ordinalChain: int list) =
- ordinalChain |> List.map string |> String.concat "_"
+ { acc with
+ EventTokens = acc.EventTokens |> Map.add key token
+ })
+ maps
-let private formatGenerationSuffixedClosureName baseName generation ordinalChain =
- CompilerGeneratedNameSuffix baseName $"hotreload#g{generation}_o{formatOccurrenceChainKey ordinalChain}"
+ let maps =
+ match eventDefs with
+ | first :: _ ->
+ let token = tokenMappings.EventTokenMap (enclosing, tdef) first
+ let rowId = token &&& 0x00FFFFFF
-let private cleanUpGeneratedTypeName (name: string) =
- if name.IndexOfAny IllegalCharactersInTypeAndNamespaceNames = -1 then
- name
- else
- (name, IllegalCharactersInTypeAndNamespaceNames)
- ||> Array.fold (fun acc c -> acc.Replace(string c, "-"))
+ { maps with
+ EventMapEntries = maps.EventMapEntries |> Map.add typeName rowId
+ }
+ | [] -> maps
-let private typeDefSimpleNames (tokenMaps: BaselineTokenMaps) =
- tokenMaps.TypeTokens
- |> Map.toSeq
- |> Seq.map (fun (key, _) -> key.Name)
- |> Set.ofSeq
+ tdef.NestedTypes.AsList()
+ |> List.fold (collectType tokenMappings scope (enclosing @ [ tdef ])) maps
-let private methodNamesByToken (methodTokens: Map) =
- methodTokens
- |> Map.toSeq
- |> Seq.map (fun (key, token) -> token, key.Name)
- |> Map.ofSeq
+let private methodKeyFromRef (methodRef: ILMethodRef) =
+ {
+ MethodDefinitionKey.DeclaringType = methodRef.DeclaringTypeRef.FullName
+ Name = methodRef.Name
+ GenericArity = methodRef.GenericArity
+ ParameterTypes = methodRef.ArgTypes |> Seq.toList
+ ReturnType = methodRef.ReturnType
+ }
+
+let collectMethodSemanticsEntries
+ (ilModule: ILModuleDef)
+ (methodTokens: Map)
+ (propertyTokens: Map)
+ (eventTokens: Map)
+ =
+ let entries =
+ Dictionary>(HashIdentity.Structural)
+
+ let mutable nextRowId = 0
+
+ let addEntry methodKey entry =
+ match entries.TryGetValue methodKey with
+ | true, bucket -> bucket.Add entry
+ | _ ->
+ let bucket = ResizeArray()
+ bucket.Add entry
+ entries[methodKey] <- bucket
+
+ let tryAddSemantics association attributes methodRefOpt =
+ match methodRefOpt with
+ | None -> ()
+ | Some methodRef ->
+ let methodKey = methodKeyFromRef methodRef
+
+ if methodTokens.ContainsKey methodKey then
+ nextRowId <- nextRowId + 1
+
+ addEntry
+ methodKey
+ {
+ RowId = nextRowId
+ Attributes = attributes
+ Association = association
+ }
+
+ let rec visitType enclosing (typeDef: ILTypeDef) =
+ let typeRef = mkRefForNestedILTypeDef ILScopeRef.Local (enclosing, typeDef)
+ let typeName = typeRef.FullName
+
+ let buildPropertyKey (prop: ILPropertyDef) =
+ {
+ PropertyDefinitionKey.DeclaringType = typeName
+ Name = prop.Name
+ PropertyType = prop.PropertyType
+ IndexParameterTypes = List.ofSeq prop.Args
+ }
+
+ let buildEventKey (eventDef: ILEventDef) =
+ {
+ EventDefinitionKey.DeclaringType = typeName
+ Name = eventDef.Name
+ EventType = eventDef.EventType
+ }
+
+ for prop in typeDef.Properties.AsList() do
+ let propertyKey = buildPropertyKey prop
+
+ match propertyTokens |> Map.tryFind propertyKey with
+ | Some propertyToken ->
+ let rowId = propertyToken &&& 0x00FFFFFF
+ let association = MethodSemanticsAssociation.PropertyAssociation(propertyKey, rowId)
+ tryAddSemantics association MethodSemanticsAttributes.Setter prop.SetMethod
+ tryAddSemantics association MethodSemanticsAttributes.Getter prop.GetMethod
+ | None -> ()
+
+ for eventDef in typeDef.Events.AsList() do
+ let eventKey = buildEventKey eventDef
+
+ match eventTokens |> Map.tryFind eventKey with
+ | Some eventToken ->
+ let rowId = eventToken &&& 0x00FFFFFF
+ let association = MethodSemanticsAssociation.EventAssociation(eventKey, rowId)
+ tryAddSemantics association MethodSemanticsAttributes.Adder (Some eventDef.AddMethod)
+ tryAddSemantics association MethodSemanticsAttributes.Remover (Some eventDef.RemoveMethod)
+
+ eventDef.FireMethod
+ |> Option.iter (fun fire -> tryAddSemantics association MethodSemanticsAttributes.Raiser (Some fire))
+
+ eventDef.OtherMethods
+ |> List.iter (fun other -> tryAddSemantics association MethodSemanticsAttributes.Other (Some other))
+ | None -> ()
+
+ typeDef.NestedTypes.AsList()
+ |> List.iter (fun nested -> visitType (enclosing @ [ typeDef ]) nested)
+
+ ilModule.TypeDefs.AsList() |> List.iter (visitType [])
+ entries |> Seq.map (fun kvp -> kvp.Key, kvp.Value |> Seq.toList) |> Map.ofSeq
+
+/// Same character cleanup IlxGen applies to closure base names before minting type
+/// names (IlxGen.CleanUpGeneratedTypeName, not exposed through IlxGen.fsi).
+let private cleanUpGeneratedTypeName (nm: string) =
+ if nm.IndexOfAny IllegalCharactersInTypeAndNamespaceNames = -1 then
+ nm
+ else
+ (nm, IllegalCharactersInTypeAndNamespaceNames)
+ ||> Array.fold (fun nm c -> nm.Replace(string c, "-"))
+
+/// Simple (unqualified) names of every TypeDef in the module, nested types included.
+let internal collectTypeDefSimpleNames (ilModule: ILModuleDef) : Set =
+ let names = HashSet(StringComparer.Ordinal)
+
+ let rec visit (typeDef: ILTypeDef) =
+ names.Add typeDef.Name |> ignore
+ typeDef.NestedTypes.AsList() |> List.iter visit
+
+ ilModule.TypeDefs.AsList() |> List.iter visit
+ Set.ofSeq names
+
+///
+/// Reconstructs the per-method occurrence-chain -> closure-class-name tables from the
+/// decoded EnC CDI occurrence keys alone. Under occurrence-derived baseline
+/// naming, a flag-on baseline compile names every mapped closure
+/// {memberCompiledName}@hotreload#g0_o{chain} — a pure function of the identity
+/// the CDI rows persist — so a session started from the on-disk baseline in another
+/// process re-derives exactly the tables the emitting compile installed, with no
+/// in-memory carry-over. Fail closed twice:
+/// - a baseline containing any generation-suffixed TypeDef of generation >= 1 is a
+/// mid-session artifact (a flag-on recapture emitted under an active session, whose
+/// added closures carry their first-allocation generation); its names are NOT
+/// derivable from generation-0 identity, so no table is reconstructed at all;
+/// - per occurrence, a derived name must exist as a baseline TypeDef simple name.
+/// Occurrences that never lowered to a closure class are omitted, while surviving
+/// occurrence-derived closures stay replayable. A reconstructed table can never
+/// claim a name the baseline does not contain.
+///
let deriveEncClosureNamesFromEncDebugInfos
(encMethodDebugInfos: Map)
(methodNamesByToken: Map)
@@ -329,7 +686,7 @@ let deriveEncClosureNamesFromEncDebugInfos
let hasMidSessionClosureNames =
typeDefSimpleNames
|> Set.exists (fun name ->
- match TryGetHotReloadNameGeneration name with
+ match GeneratedNames.TryGetHotReloadNameGeneration name with
| Some generation -> generation >= 1
| None -> false)
@@ -342,7 +699,7 @@ let deriveEncClosureNamesFromEncDebugInfos
typeDefSimpleNames
|> Set.exists (fun name ->
name.StartsWith(prefix, StringComparison.Ordinal)
- && not (IsHotReloadGenerationSuffixedName name))
+ && not (GeneratedNames.IsHotReloadGenerationSuffixedName name))
let derivedRows =
encMethodDebugInfos
@@ -351,106 +708,1026 @@ let deriveEncClosureNamesFromEncDebugInfos
match info.Closures, Map.tryFind methodToken methodNamesByToken with
| [], _
| _, None -> None
- | closures, Some methodName ->
- let nameBase = cleanUpGeneratedTypeName methodName
+ | closures, Some methName ->
+ let nameBase = cleanUpGeneratedTypeName methName
- let rows =
+ let table =
closures
|> List.choose (fun closure ->
let chain = decodeOccurrenceKey closure.SyntaxOffset
- let name = formatGenerationSuffixedClosureName nameBase 0 chain
+ let name = ClosureNameAllocator.formatGenerationSuffixedClosureName nameBase 0 chain
if Set.contains name typeDefSimpleNames then
Some(chain, name)
else
None)
- Some(methodToken, nameBase, rows))
+ Some(methodToken, nameBase, table))
let hasReplayOnlyCdiMethod =
derivedRows
- |> List.exists (fun (_, nameBase, rows) -> List.isEmpty rows && hasReplayNamedTypeDef nameBase)
+ |> List.exists (fun (_, nameBase, table) -> List.isEmpty table && hasReplayNamedTypeDef nameBase)
+
+ let derivedNameBases =
+ derivedRows
+ |> List.choose (fun (_, nameBase, table) -> if List.isEmpty table then None else Some nameBase)
+ |> Set.ofList
+
+ let stateMachineNameBases =
+ encMethodDebugInfos
+ |> Map.toSeq
+ |> Seq.choose (fun (methodToken, info) ->
+ if List.isEmpty info.StateMachineStates then
+ None
+ else
+ Map.tryFind methodToken methodNamesByToken
+ |> Option.map cleanUpGeneratedTypeName)
+ |> Set.ofSeq
+
+ let hasReplayOnlyTypeDef =
+ typeDefSimpleNames
+ |> Set.exists (fun name ->
+ let basicName = GetBasicNameOfPossibleCompilerGeneratedName name
+
+ name.IndexOf("@hotreload", StringComparison.Ordinal) >= 0
+ && not (GeneratedNames.IsHotReloadGenerationSuffixedName name)
+ && not (Set.contains basicName derivedNameBases)
+ && not (Set.contains basicName stateMachineNameBases))
- if hasReplayOnlyCdiMethod then
+ if hasReplayOnlyCdiMethod || hasReplayOnlyTypeDef then
Map.empty
else
- derivedRows
- |> List.choose (fun (methodToken, _, rows) ->
- match rows with
- | [] -> None
- | _ -> Some(methodToken, Map.ofList rows))
- |> Map.ofList
-
-let private toPortablePdbSnapshot (expectedContentId: byte[]) (pdbBytes: byte[]) =
- ILBaselineReader.readPortablePdbMetadata pdbBytes
- |> Option.filter (fun metadata -> metadata.ContentId.AsSpan().SequenceEqual(expectedContentId))
- |> Option.map (fun metadata ->
- {
- Bytes = Array.copy pdbBytes
- TableRowCounts = ImmutableArray.CreateRange metadata.TableRowCounts
- EntryPointToken = metadata.EntryPointToken
- })
+ (Map.empty, derivedRows)
+ ||> List.fold (fun acc (methodToken, _, table) ->
+ if not (List.isEmpty table) then
+ Map.add methodToken (Map.ofList table) acc
+ else
+ acc)
+
+/// Baseline MethodDef names keyed by token, for the CDI-derived closure-name
+/// reconstruction (the CDI write side only ever attaches a map to a method whose name
+/// identifies exactly one MethodDef row, so the name here is unambiguous for any token
+/// that carries EnC debug information).
+let private methodNamesByToken (methodTokens: Map) : Map =
+ methodTokens
+ |> Map.toSeq
+ |> Seq.map (fun (key, token) -> token, key.Name)
+ |> Map.ofSeq
+
+let private createCore
+ (moduleId: Guid)
+ (ilModule: ILModuleDef)
+ (tokenMappings: ILTokenMappings)
+ (metadataSnapshot: MetadataSnapshot)
+ (ilxGenEnvironment: IlxGenEnvSnapshot option)
+ (portablePdbSnapshot: PortablePdbSnapshot option)
+ =
+ let scope = ILScopeRef.Local
+
+ let maps =
+ ilModule.TypeDefs.AsList()
+ |> List.fold (collectType tokenMappings scope []) emptyMaps
+
+ let methodSemanticsEntries =
+ collectMethodSemanticsEntries ilModule maps.MethodTokens maps.PropertyTokens maps.EventTokens
-let private createCore moduleId metadata portablePdb tokenMaps =
- let reconstructedSynthesizedNames =
- collectSynthesizedNameSnapshotFromTokens tokenMaps
+ let reconstructedSynthesizedNames = collectSynthesizedNameSnapshot ilModule
+ // Precedence is explicit: recorded > reconstructed. A recorded snapshot is the
+ // allocation-order ground truth persisted by the flag-on compiler; the IL walk is
+ // only an old-baseline fallback and keeps its previous behavior unchanged.
let synthesizedNames, synthesizedNameSnapshotSource =
match
- portablePdb
+ portablePdbSnapshot
|> Option.bind (fun snapshot -> readSynthesizedNameSnapshotFromPortablePdb snapshot.Bytes)
with
| Some recordedSnapshot -> recordedSnapshot, SynthesizedNameSnapshotSource.Recorded
| None -> reconstructedSynthesizedNames, SynthesizedNameSnapshotSource.Reconstructed
+ if traceClosureNames.Value then
+ let source =
+ match synthesizedNameSnapshotSource with
+ | SynthesizedNameSnapshotSource.Recorded -> "recorded"
+ | SynthesizedNameSnapshotSource.Reconstructed -> "reconstructed"
+
+ printfn "[fsharp-hotreload][closure-names] synthesized-name snapshot source=%s buckets=%d" source (Map.count synthesizedNames)
+
+ // The baseline PDB is already in memory here (captured alongside the emitted
+ // assembly), so the EnC CDI rows are decoded eagerly; flag-off baselines and
+ // PDBs without EnC rows decode to the empty map.
let encMethodDebugInfos =
- portablePdb
+ portablePdbSnapshot
|> Option.map (fun snapshot -> readEncMethodDebugInfoFromPortablePdb snapshot.Bytes)
|> Option.defaultValue Map.empty
+ // Seed the committed sequence-point view from the baseline PDB. No PDB means the
+ // map stays empty and line-shift detection / active-statement remapping stay inert.
+ let sequencePointSnapshots =
+ portablePdbSnapshot
+ |> Option.map (fun snapshot -> ActiveStatementAnalysis.decodeMethodSequencePoints snapshot.Bytes)
+ |> Option.defaultValue Map.empty
+
{
ModuleId = moduleId
- Metadata = metadata
- PortablePdb = portablePdb
- TokenMaps = tokenMaps
+ EncId = System.Guid.Empty
+ EncBaseId = System.Guid.Empty
+ NextGeneration = 1
+ Metadata = metadataSnapshot
+ TokenMappings = tokenMappings
+ TypeTokens = maps.TypeTokens
+ MethodTokens = maps.MethodTokens
+ FieldTokens = maps.FieldTokens
+ PropertyTokens = maps.PropertyTokens
+ EventTokens = maps.EventTokens
+ PropertyMapEntries = maps.PropertyMapEntries
+ EventMapEntries = maps.EventMapEntries
+ MethodSemanticsEntries = methodSemanticsEntries
+ IlxGenEnvironment = ilxGenEnvironment
+ PortablePdb = portablePdbSnapshot
SynthesizedNameSnapshot = synthesizedNames
SynthesizedNameSnapshotSource = synthesizedNameSnapshotSource
+ SynthesizedTypeShapes = maps.SynthesizedTypeShapes
+ MetadataHandles = BaselineHandleCache.Empty
+ TypeReferenceTokens = Map.empty
+ AssemblyReferenceTokens = Map.empty
+ MemberReferenceRows = Map.empty
+ TypeSpecSignatures = Map.empty
+ CustomAttributeRows = Map.empty
+ TableEntriesAdded = Array.zeroCreate tableCount
+ StringStreamLengthAdded = 0
+ UserStringStreamLengthAdded = 0
+ BlobStreamLengthAdded = 0
+ GuidStreamLengthAdded = 0
+ AddedOrChangedMethods = []
EncMethodDebugInfos = encMethodDebugInfos
+ // Closure-name tables are reconstructed from the CDI occurrence keys:
+ // under occurrence-derived baseline naming they are a pure function of the
+ // identity the PDB persists, so this works for baselines read back from disk
+ // in another process exactly as for in-process captures (where the capture
+ // hook additionally validates the reconstruction against the emit-time
+ // stamp -> name recording).
EncClosureNames =
deriveEncClosureNamesFromEncDebugInfos
encMethodDebugInfos
- (methodNamesByToken tokenMaps.MethodTokens)
- (typeDefSimpleNames tokenMaps)
+ (methodNamesByToken maps.MethodTokens)
+ (collectTypeDefSimpleNames ilModule)
+ SequencePointSnapshots = sequencePointSnapshots
+ ModuleNameOffset = None
}
-let tryReadFromAssemblyAndPdbBytes (assemblyBytes: byte[]) (portablePdbBytes: byte[] option) =
- try
- match
- ILBaselineReader.metadataSnapshotFromBytes assemblyBytes,
- ILBaselineReader.BaselineMetadataReader.Create assemblyBytes,
- ILBaselineReader.readModuleMvidFromBytes assemblyBytes
- with
- | Some metadata, Some reader, Some moduleId when moduleId <> Guid.Empty ->
- let portablePdb =
- match ILBaselineReader.readCodeViewContentIdFromBytes assemblyBytes with
- | Some expectedContentId -> portablePdbBytes |> Option.bind (toPortablePdbSnapshot expectedContentId)
+let internal applyDelta
+ (baseline: FSharpEmitBaseline)
+ (deltaTableCounts: int[])
+ (deltaHeapSizes: MetadataHeapSizes)
+ (addedOrChangedMethods: AddedOrChangedMethodInfo list)
+ (encId: Guid)
+ (encBaseId: Guid)
+ (synthesizedSnapshot: Map option)
+ : FSharpEmitBaseline =
+
+ let tableCounts =
+ if deltaTableCounts.Length = tableCount then
+ deltaTableCounts
+ else
+ Array.zeroCreate tableCount
+
+ let updatedTableEntries =
+ Array.init tableCount (fun i ->
+ let previous = baseline.TableEntriesAdded[i]
+ previous + tableCounts.[i])
+
+ let updatedMetadataSnapshot =
+ // Per Roslyn DeltaMetadataWriter.cs: Blob and UserString streams are concatenated
+ // aligned to 4-byte boundaries; String stream is concatenated unaligned.
+ // Each delta #GUID stream already contains the zero-filled cumulative prefix from
+ // prior generations. Replace that prior delta contribution with the newest full
+ // stream instead of adding it again, while retaining the original PE GUID heap.
+ let originalGuidHeapSize =
+ baseline.Metadata.HeapSizes.GuidHeapSize - baseline.GuidStreamLengthAdded
+
+ let updatedHeapSizes =
+ {
+ StringHeapSize = baseline.Metadata.HeapSizes.StringHeapSize + deltaHeapSizes.StringHeapSize
+ UserStringHeapSize =
+ baseline.Metadata.HeapSizes.UserStringHeapSize
+ + align4 deltaHeapSizes.UserStringHeapSize
+ BlobHeapSize = baseline.Metadata.HeapSizes.BlobHeapSize + align4 deltaHeapSizes.BlobHeapSize
+ GuidHeapSize = originalGuidHeapSize + deltaHeapSizes.GuidHeapSize
+ }
+
+ if traceHeapOffsets.Value then
+ printfn "[fsharp-hotreload][heap-offsets] applyDelta: Updating baseline heap sizes"
+ printfn "[fsharp-hotreload][heap-offsets] Before: UserStringHeapSize = %d" baseline.Metadata.HeapSizes.UserStringHeapSize
+
+ printfn
+ "[fsharp-hotreload][heap-offsets] Delta: UserStringHeapSize = %d (aligned = %d)"
+ deltaHeapSizes.UserStringHeapSize
+ (align4 deltaHeapSizes.UserStringHeapSize)
+
+ printfn "[fsharp-hotreload][heap-offsets] After: UserStringHeapSize = %d" updatedHeapSizes.UserStringHeapSize
+ printfn "[fsharp-hotreload][heap-offsets] Generation: %d -> %d" baseline.NextGeneration (baseline.NextGeneration + 1)
+
+ let updatedTableCountsAbsolute =
+ Array.init tableCount (fun i -> baseline.Metadata.TableRowCounts.[i] + tableCounts.[i])
+
+ { baseline.Metadata with
+ HeapSizes = updatedHeapSizes
+ TableRowCounts = updatedTableCountsAbsolute
+ }
+
+ { baseline with
+ EncId = encId
+ EncBaseId = encBaseId
+ NextGeneration = baseline.NextGeneration + 1
+ ModuleNameOffset = baseline.ModuleNameOffset
+ TableEntriesAdded = updatedTableEntries
+ // Per Roslyn DeltaMetadataWriter.cs: String stream is concatenated unaligned,
+ // Blob and UserString streams are concatenated aligned to 4-byte boundaries.
+ StringStreamLengthAdded = baseline.StringStreamLengthAdded + deltaHeapSizes.StringHeapSize
+ UserStringStreamLengthAdded = baseline.UserStringStreamLengthAdded + align4 deltaHeapSizes.UserStringHeapSize
+ BlobStreamLengthAdded = baseline.BlobStreamLengthAdded + align4 deltaHeapSizes.BlobHeapSize
+ GuidStreamLengthAdded = deltaHeapSizes.GuidHeapSize
+ Metadata = updatedMetadataSnapshot
+ SynthesizedNameSnapshot =
+ match synthesizedSnapshot with
+ | Some snapshot -> snapshot
+ | None -> baseline.SynthesizedNameSnapshot
+ MethodSemanticsEntries = baseline.MethodSemanticsEntries
+ AddedOrChangedMethods =
+ (addedOrChangedMethods @ baseline.AddedOrChangedMethods)
+ |> List.distinctBy (fun info -> info.MethodToken)
+ TypeReferenceTokens = baseline.TypeReferenceTokens
+ AssemblyReferenceTokens = baseline.AssemblyReferenceTokens
+ }
+
+///
+/// Carries per-method EnC debug information forward into the next-generation baseline after a
+/// delta, mirroring how AddedOrChangedMethods chains method state: every updated or added
+/// method's entry is replaced by its occurrence data recomputed from the fresh compile, or
+/// dropped when the fresh compile produced none (fail closed — the method's lambdas must then
+/// be treated as unmappable rather than matched against stale data). Unchanged methods keep
+/// their baseline entries.
+///
+let chainEncMethodDebugInfos
+ (baseline: FSharpEmitBaseline)
+ (refreshedEncDebugInfos: Map)
+ (updatedMethodTokens: int list)
+ : FSharpEmitBaseline =
+ let chainedInfos =
+ (baseline.EncMethodDebugInfos, updatedMethodTokens)
+ ||> List.fold (fun acc methodToken ->
+ match Map.tryFind methodToken refreshedEncDebugInfos with
+ | Some info -> Map.add methodToken info acc
+ | None -> Map.remove methodToken acc)
+
+ { baseline with
+ EncMethodDebugInfos = chainedInfos
+ }
+
+///
+/// Recomputes the per-method EnC debug information from the fresh typed tree of an edited
+/// compilation, keyed by baseline MethodDef token, for chaining into the next-generation
+/// baseline (see chainEncMethodDebugInfos). Name-to-token resolution mirrors the fail-closed
+/// write-side keying: only compiled names identifying exactly one baseline MethodDef row
+/// resolve, so an entry can never attach to the wrong method. Methods added by the current
+/// delta have no baseline token yet and carry no entry.
+///
+/// Baseline MethodDef tokens keyed by method name, restricted to names identifying
+/// exactly ONE baseline MethodDef row — the shared fail-closed name -> token resolution
+/// for typed-tree-derived per-method side tables (EnC debug info, closure-name tables).
+let private tokensByUniqueMethodName (baseline: FSharpEmitBaseline) =
+ baseline.MethodTokens
+ |> Map.toSeq
+ |> Seq.groupBy (fun (key, _) -> key.Name)
+ |> Seq.choose (fun (name, entries) ->
+ match entries |> Seq.truncate 2 |> List.ofSeq with
+ | [ (_, token) ] -> Some(name, token)
+ | _ -> None)
+ |> Map.ofSeq
+
+[]
+type ImplementationFileScope =
+ | Full
+ | ReferenceChanged
+
+let private checkedImplFiles (CheckedAssemblyAfterOptimization implFiles) =
+ implFiles |> List.map (fun implFile -> implFile.ImplFile)
+
+let private implementationFileKey (CheckedImplFile(qualifiedNameOfFile = qual)) = qual.Text
+
+let private tryBuildUniqueImplementationFileLookup files =
+ let lookup, duplicateKeys =
+ ((Map.empty, Set.empty), files)
+ ||> List.fold (fun (lookup, duplicateKeys) implFile ->
+ let key = implementationFileKey implFile
+
+ if Map.containsKey key lookup then
+ lookup, Set.add key duplicateKeys
+ else
+ Map.add key implFile lookup, duplicateKeys)
+
+ if Set.isEmpty duplicateKeys then Some lookup else None
+
+let private tryReferenceChangedImplementationFilePairs baselineImplementation freshImplementation =
+ let baselineFiles = checkedImplFiles baselineImplementation
+ let freshFiles = checkedImplFiles freshImplementation
+
+ match tryBuildUniqueImplementationFileLookup baselineFiles, tryBuildUniqueImplementationFileLookup freshFiles with
+ | Some baselineLookup, Some freshLookup ->
+ if
+ baselineLookup
+ |> Map.forall (fun key _ -> Map.containsKey key freshLookup)
+ |> not
+ then
+ None
+ else
+ ((Some [], freshFiles)
+ ||> List.fold (fun changedPairsOpt freshFile ->
+ changedPairsOpt
+ |> Option.bind (fun changedPairs ->
+ match Map.tryFind (implementationFileKey freshFile) baselineLookup with
+ | None -> None
+ | Some baselineFile when obj.ReferenceEquals(baselineFile, freshFile) -> Some changedPairs
+ | Some baselineFile -> Some((baselineFile, freshFile) :: changedPairs))))
+ |> Option.map List.rev
+ | _ -> None
+
+let private scopedFreshImplFiles scope baselineImplementation freshImplementation =
+ match scope, baselineImplementation with
+ | ImplementationFileScope.ReferenceChanged, Some baselineImplementation ->
+ match tryReferenceChangedImplementationFilePairs baselineImplementation freshImplementation with
+ | Some changedPairs -> changedPairs |> List.map snd
+ | None -> checkedImplFiles freshImplementation
+ | _ -> checkedImplFiles freshImplementation
+
+let private scopedBaselineAndFreshImplFiles scope baselineImplementation freshImplementation =
+ match scope with
+ | ImplementationFileScope.ReferenceChanged ->
+ match tryReferenceChangedImplementationFilePairs baselineImplementation freshImplementation with
+ | Some changedPairs -> changedPairs |> List.map fst, changedPairs |> List.map snd
+ | None -> checkedImplFiles baselineImplementation, checkedImplFiles freshImplementation
+ | ImplementationFileScope.Full -> checkedImplFiles baselineImplementation, checkedImplFiles freshImplementation
+
+let computeRefreshedEncMethodDebugInfosWithScope
+ (g: TcGlobals)
+ (baseline: FSharpEmitBaseline)
+ (scope: ImplementationFileScope)
+ (baselineImplementation: CheckedAssemblyAfterOptimization option)
+ (implementationFiles: CheckedAssemblyAfterOptimization)
+ : Map =
+ let infosByName =
+ implementationFiles
+ |> scopedFreshImplFiles scope baselineImplementation
+ |> computeMethodEncDebugInfo g
+
+ if Map.isEmpty infosByName then
+ Map.empty
+ else
+ let tokensByUniqueName = tokensByUniqueMethodName baseline
+
+ (Map.empty, infosByName)
+ ||> Map.fold (fun acc methName info ->
+ match Map.tryFind methName tokensByUniqueName with
+ | Some methodToken -> Map.add methodToken info acc
+ | None -> acc)
+
+let computeRefreshedEncMethodDebugInfos
+ (g: TcGlobals)
+ (baseline: FSharpEmitBaseline)
+ (implementationFiles: CheckedAssemblyAfterOptimization)
+ : Map =
+ computeRefreshedEncMethodDebugInfosWithScope g baseline ImplementationFileScope.Full None implementationFiles
+
+///
+/// Re-keys name-keyed per-method closure-name tables (occurrence-chain -> closure type
+/// name, produced by ClosureNameAllocator.computeBaselineClosureNameRows in the fsc emit
+/// path) by baseline MethodDef token, for storage as FSharpEmitBaseline.EncClosureNames.
+/// Resolution is fail closed exactly like computeRefreshedEncMethodDebugInfos: only
+/// compiled names identifying exactly one baseline MethodDef row resolve, so a table can
+/// never attach to the wrong method.
+///
+let resolveClosureNameRowsByToken
+ (baseline: FSharpEmitBaseline)
+ (rowsByMethodName: Map>)
+ : Map> =
+ if Map.isEmpty rowsByMethodName then
+ Map.empty
+ else
+ let tokensByUniqueName = tokensByUniqueMethodName baseline
+
+ (Map.empty, rowsByMethodName)
+ ||> Map.fold (fun acc methName rows ->
+ match Map.tryFind methName tokensByUniqueName with
+ | Some methodToken -> Map.add methodToken rows acc
+ | None -> acc)
+
+///
+/// Re-derives the closure-name tables of a baseline whose EnC method debug information
+/// was attached AFTER creation (the checker's read-from-disk path decodes the sibling
+/// PDB as a separate input — see service.fs createBaseline). Pure re-application of the
+/// createCore derivation over the final EncMethodDebugInfos.
+///
+let deriveEncClosureNames (ilModule: ILModuleDef) (baseline: FSharpEmitBaseline) : Map> =
+ deriveEncClosureNamesFromEncDebugInfos
+ baseline.EncMethodDebugInfos
+ (methodNamesByToken baseline.MethodTokens)
+ (collectTypeDefSimpleNames ilModule)
+
+/// Per-member compiled-name -> occurrence-list view of an implementation, restricted to
+/// compiled names claimed by exactly one member binding (the shared fail-closed keying).
+let private memberOccurrencesByUniqueNameInFiles
+ (g: TcGlobals)
+ (implFiles: CheckedImplFile list)
+ : Map =
+ let allMembers =
+ implFiles |> List.collect (TypedTreeDiff.collectMemberLambdaOccurrences g)
+
+ let ambiguousNames =
+ allMembers
+ |> List.choose (fun (symbol, _) -> symbol.CompiledName)
+ |> List.countBy id
+ |> List.filter (fun (_, count) -> count > 1)
+ |> List.map fst
+ |> Set.ofList
+
+ (Map.empty, allMembers)
+ ||> List.fold (fun acc (symbol: TypedTreeDiff.SymbolId, occurrences) ->
+ match symbol.CompiledName with
+ | Some methName when not (Set.contains methName ambiguousNames) -> Map.add methName occurrences acc
+ | _ -> acc)
+
+let private memberOccurrencesByUniqueName
+ (g: TcGlobals)
+ (implementationFiles: CheckedAssemblyAfterOptimization)
+ : Map =
+ implementationFiles
+ |> checkedImplFiles
+ |> memberOccurrencesByUniqueNameInFiles g
+
+///
+/// Derives the stamp -> closure-class-name table a flag-on BASELINE compile installs
+/// before lowering: every lambda occurrence's closure class is named
+/// {memberCompiledName}@hotreload#g0_o{occurrenceChain} — a pure function of
+/// occurrence identity, so a session started from the on-disk baseline in another
+/// process can re-derive the same names from the persisted EnC CDI occurrence keys
+/// (see deriveEncClosureNamesFromEncDebugInfos). Gating mirrors the baseline CDI emission
+/// exactly, so a name is derived if and only if the corresponding occurrence key is
+/// persisted: members without a unique compiled name are dropped, and a member is
+/// dropped entirely when ANY of its occurrence chains is not CDI-encodable (depth > 2
+/// or ordinals past the packing limits) — such members keep pure sequence-replay
+/// naming, exactly like flag-off behavior, and stay fail-closed for lambda set changes.
+///
+let computeBaselineOccurrenceKeyedClosureNames (g: TcGlobals) (optimizedImpls: CheckedAssemblyAfterOptimization) : Map =
+ (Map.empty, memberOccurrencesByUniqueName g optimizedImpls)
+ ||> Map.fold (fun acc methName occurrences ->
+ let chains = occurrences |> List.map ClosureNameAllocator.occurrenceOrdinalChain
+
+ let allChainsEncodable =
+ chains |> List.forall (fun chain -> (tryEncodeOccurrenceKey chain).IsSome)
+
+ if not allChainsEncodable then
+ acc
+ else
+ let nameBase = cleanUpGeneratedTypeName methName
+
+ (acc, List.zip occurrences chains)
+ ||> List.fold (fun acc (occurrence, chain) ->
+ // Stamp 0 is the extraction's "no root lambda" sentinel and can never
+ // be a real Expr stamp; never install a name for it.
+ if occurrence.RootExprStamp = 0L then
+ acc
+ else
+ Map.add occurrence.RootExprStamp (ClosureNameAllocator.formatGenerationSuffixedClosureName nameBase 0 chain) acc))
+
+///
+/// Runs the occurrence-keyed closure name allocator for a delta compile:
+/// aligns the fresh implementation's lambda occurrences with the previous generation's
+/// (the implementation files the session chains) and assigns each fresh occurrence its
+/// closure class name — the baseline name verbatim for compatible survivors, a
+/// generation-suffixed fresh name otherwise. Returns:
+/// - the stamp -> assigned-name table to install on the compiling CompilerGlobalState
+/// (the IlxGen closure call site consults it before sequence replay), and
+/// - the refreshed per-method occurrence-chain -> name tables keyed by baseline
+/// MethodDef token, to chain into the next-generation baseline alongside the
+/// refreshed EnC debug infos (see chainClosureNameRows).
+/// Fail closed at every join: members without a unique compiled name, without a
+/// resolvable baseline MethodDef token, or without a baseline chain -> name table get no
+/// assignments (their closures keep pure sequence-replay naming) and no refreshed table.
+/// Both tables are derived deterministically from session state + the fresh typed tree,
+/// so the emit-time install (fsc hook) and the delta-emission refresh (checker) agree.
+///
+let computeOccurrenceKeyedClosureNamesWithScope
+ (g: TcGlobals)
+ (baseline: FSharpEmitBaseline)
+ (scope: ImplementationFileScope)
+ (baselineImplementation: CheckedAssemblyAfterOptimization)
+ (freshImplementation: CheckedAssemblyAfterOptimization)
+ (generation: int)
+ : Map * Map> =
+
+ if Map.isEmpty baseline.EncClosureNames then
+ Map.empty, Map.empty
+ else
+ let baselineImplFiles, freshImplFiles =
+ scopedBaselineAndFreshImplFiles scope baselineImplementation freshImplementation
+
+ let baselineOccurrencesByName =
+ memberOccurrencesByUniqueNameInFiles g baselineImplFiles
+
+ let freshOccurrencesByName = memberOccurrencesByUniqueNameInFiles g freshImplFiles
+ let tokensByUniqueName = tokensByUniqueMethodName baseline
+
+ ((Map.empty, Map.empty), freshOccurrencesByName)
+ ||> Map.fold (fun (assignedNames, refreshedRows) methName freshOccurrences ->
+ let baselineTable =
+ Map.tryFind methName tokensByUniqueName
+ |> Option.bind (fun token ->
+ Map.tryFind token baseline.EncClosureNames
+ |> Option.map (fun table -> token, table))
+
+ match freshOccurrences, baselineTable with
+ | _ :: _, Some(methodToken, namesByChain) ->
+ let baselineOccurrences =
+ Map.tryFind methName baselineOccurrencesByName |> Option.defaultValue []
+
+ let freshNameBase = cleanUpGeneratedTypeName methName
+
+ let allocation =
+ ClosureNameAllocator.allocateMemberClosureNames
+ baselineOccurrences
+ namesByChain
+ freshOccurrences
+ freshNameBase
+ generation
+
+ let baselineTableIsComplete =
+ baselineOccurrences
+ |> List.forall (fun occurrence ->
+ namesByChain
+ |> Map.containsKey (ClosureNameAllocator.occurrenceOrdinalChain occurrence))
+
+ let baselineOccurrenceChains =
+ baselineOccurrences
+ |> List.map ClosureNameAllocator.occurrenceOrdinalChain
+ |> Set.ofList
+
+ let baselineOccurrenceByChain =
+ baselineOccurrences
+ |> List.map (fun occurrence -> ClosureNameAllocator.occurrenceOrdinalChain occurrence, occurrence)
+ |> Map.ofList
+
+ let replayAssignments =
+ allocation.Assignments
+ |> List.choose (fun (occurrence, assignment) ->
+ let occurrenceChain = ClosureNameAllocator.occurrenceOrdinalChain occurrence
+
+ match assignment with
+ | ClosureNameAllocator.ClosureNameAssignment.Reused _ when baselineTableIsComplete -> Some(occurrence, assignment)
+ | ClosureNameAllocator.ClosureNameAssignment.Reused _ ->
+ if not (Set.contains occurrenceChain baselineOccurrenceChains) then
+ Some(occurrence, assignment)
+ else
+ match Map.tryFind occurrenceChain baselineOccurrenceByChain with
+ | Some baselineOccurrence when baselineOccurrence.BodyHash <> occurrence.BodyHash ->
+ Some(
+ occurrence,
+ ClosureNameAllocator.ClosureNameAssignment.Fresh(
+ ClosureNameAllocator.formatGenerationSuffixedClosureName
+ freshNameBase
+ generation
+ occurrenceChain
+ )
+ )
+ | _ -> None
+ | ClosureNameAllocator.ClosureNameAssignment.Fresh _ when baselineTableIsComplete -> Some(occurrence, assignment)
+ | ClosureNameAllocator.ClosureNameAssignment.Fresh _ -> None)
+
+ let assignedNames =
+ (assignedNames, replayAssignments)
+ ||> List.fold (fun acc (occurrence, assignment) ->
+ // Stamp 0 is the extraction's "no root lambda" sentinel and can
+ // never be a real Expr stamp; never install a name for it.
+ if occurrence.RootExprStamp = 0L then
+ acc
+ else
+ Map.add occurrence.RootExprStamp assignment.Name acc)
+
+ let refreshedNames =
+ replayAssignments
+ |> List.map (fun (occurrence, assignment) -> ClosureNameAllocator.occurrenceOrdinalChain occurrence, assignment.Name)
+ |> Map.ofList
+
+ assignedNames, Map.add methodToken refreshedNames refreshedRows
+ | _ -> assignedNames, refreshedRows)
+
+let computeOccurrenceKeyedClosureNames
+ (g: TcGlobals)
+ (baseline: FSharpEmitBaseline)
+ (baselineImplementation: CheckedAssemblyAfterOptimization)
+ (freshImplementation: CheckedAssemblyAfterOptimization)
+ (generation: int)
+ : Map * Map> =
+ computeOccurrenceKeyedClosureNamesWithScope
+ g
+ baseline
+ ImplementationFileScope.Full
+ baselineImplementation
+ freshImplementation
+ generation
+
+///
+/// Carries the per-method closure-name tables forward into the next-generation baseline
+/// after a delta, with exactly the chainEncMethodDebugInfos semantics: every updated or
+/// added method's table is replaced by the one recomputed from the fresh compile, or
+/// dropped when the fresh compile produced none (fail closed — the method's closures
+/// then fall back to sequence replay in later generations). Unchanged methods keep their
+/// baseline tables.
+///
+let chainClosureNameRows
+ (baseline: FSharpEmitBaseline)
+ (refreshedClosureNameRows: Map>)
+ (updatedMethodTokens: int list)
+ : FSharpEmitBaseline =
+ let chainedRows =
+ (baseline.EncClosureNames, updatedMethodTokens)
+ ||> List.fold (fun acc methodToken ->
+ match Map.tryFind methodToken refreshedClosureNameRows with
+ | Some rows -> Map.add methodToken rows acc
+ | None -> Map.remove methodToken acc)
+
+ { baseline with
+ EncClosureNames = chainedRows
+ }
+
+/// Create an without capturing the ILX environment snapshot.
+let create
+ (ilModule: ILModuleDef)
+ (tokenMappings: ILTokenMappings)
+ (metadataSnapshot: MetadataSnapshot)
+ (moduleId: Guid)
+ (portablePdbSnapshot: PortablePdbSnapshot option)
+ =
+ createCore moduleId ilModule tokenMappings metadataSnapshot None portablePdbSnapshot
+
+/// Create an that carries the captured ILX environment snapshot.
+let createWithEnvironment
+ (ilModule: ILModuleDef)
+ (tokenMappings: ILTokenMappings)
+ (metadataSnapshot: MetadataSnapshot)
+ (ilxGenEnvironment: IlxGenEnvSnapshot)
+ (moduleId: Guid)
+ (portablePdbSnapshot: PortablePdbSnapshot option)
+ =
+ createCore moduleId ilModule tokenMappings metadataSnapshot (Some ilxGenEnvironment) portablePdbSnapshot
+
+// ============================================================================
+// Byte-based functions using ILBaselineReader (no SRM dependency)
+// ============================================================================
+
+/// Extract metadata snapshot from PE file bytes without using SRM.
+let metadataSnapshotFromBytes (bytes: byte[]) : MetadataSnapshot option =
+ ILBaselineReader.metadataSnapshotFromBytes bytes
+
+/// Read Module.Mvid GUID from PE file bytes without using SRM.
+let readModuleMvid (bytes: byte[]) : Guid option =
+ ILBaselineReader.readModuleMvidFromBytes bytes
+
+/// Build method handles from baseline using ILBaselineReader.
+let private buildMethodHandlesFromBytes
+ (reader: ILBaselineReader.BaselineMetadataReader)
+ (methodTokens: Map)
+ : Map =
+ methodTokens
+ |> Seq.choose (fun kvp ->
+ let key = kvp.Key
+ let token = kvp.Value
+ let rowId = token &&& 0x00FFFFFF
+
+ match reader.GetMethodDef(rowId) with
+ | None -> None
+ | Some methodDef ->
+ let firstParamRowId =
+ match reader.GetMethodParamRange(rowId) with
+ | Some(first, _) -> Some first
| None -> None
- Some(createCore moduleId metadata portablePdb (buildTokenMaps reader))
- | _ -> None
+ let result: MethodDefinitionMetadataHandles =
+ {
+ NameOffset =
+ if methodDef.NameOffset = 0 then
+ None
+ else
+ Some(StringOffset methodDef.NameOffset)
+ SignatureOffset =
+ if methodDef.SignatureOffset = 0 then
+ None
+ else
+ Some(BlobOffset methodDef.SignatureOffset)
+ FirstParameterRowId = firstParamRowId
+ Rva = Some methodDef.RVA
+ Attributes = Some(LanguagePrimitives.EnumOfValue methodDef.Flags)
+ ImplAttributes = Some(LanguagePrimitives.EnumOfValue methodDef.ImplFlags)
+ }
+
+ Some(key, result))
+ |> Map.ofSeq
+
+/// Build parameter handles from baseline using ILBaselineReader.
+let private buildParameterHandlesFromBytes
+ (reader: ILBaselineReader.BaselineMetadataReader)
+ (methodTokens: Map)
+ : Map =
+ methodTokens
+ |> Seq.collect (fun kvp ->
+ let methodKey = kvp.Key
+ let token = kvp.Value
+ let methodRowId = token &&& 0x00FFFFFF
+
+ match reader.GetMethodParamRange(methodRowId) with
+ | None -> Seq.empty
+ | Some(firstParam, lastParam) ->
+ seq {
+ for paramRowId in firstParam..lastParam do
+ match reader.GetParam(paramRowId) with
+ | None -> ()
+ | Some param ->
+ let key =
+ {
+ ParameterDefinitionKey.Method = methodKey
+ SequenceNumber = param.Sequence
+ }
+
+ let result: ParameterDefinitionMetadataHandles =
+ {
+ NameOffset =
+ if param.NameOffset = 0 then
+ None
+ else
+ Some(StringOffset param.NameOffset)
+ Name =
+ if param.NameOffset = 0 then
+ None
+ else
+ Some(reader.GetString param.NameOffset)
+ RowId = Some paramRowId
+ }
+
+ yield key, result
+ })
+ |> Map.ofSeq
+
+/// Build property handles from baseline using ILBaselineReader.
+let private buildPropertyHandlesFromBytes
+ (reader: ILBaselineReader.BaselineMetadataReader)
+ (propertyTokens: Map)
+ : Map =
+ propertyTokens
+ |> Seq.choose (fun kvp ->
+ let key = kvp.Key
+ let token = kvp.Value
+ let rowId = token &&& 0x00FFFFFF
+
+ match reader.GetProperty(rowId) with
+ | None -> None
+ | Some prop ->
+ let result: PropertyDefinitionMetadataHandles =
+ {
+ NameOffset =
+ if prop.NameOffset = 0 then
+ None
+ else
+ Some(StringOffset prop.NameOffset)
+ SignatureOffset =
+ if prop.SignatureOffset = 0 then
+ None
+ else
+ Some(BlobOffset prop.SignatureOffset)
+ }
+
+ Some(key, result))
+ |> Map.ofSeq
+
+/// Build event handles from baseline using ILBaselineReader.
+let private buildEventHandlesFromBytes
+ (reader: ILBaselineReader.BaselineMetadataReader)
+ (eventTokens: Map)
+ : Map =
+ eventTokens
+ |> Seq.choose (fun kvp ->
+ let key = kvp.Key
+ let token = kvp.Value
+ let rowId = token &&& 0x00FFFFFF
+
+ match reader.GetEvent(rowId) with
+ | None -> None
+ | Some event ->
+ let result: EventDefinitionMetadataHandles =
+ {
+ NameOffset =
+ if event.NameOffset = 0 then
+ None
+ else
+ Some(StringOffset event.NameOffset)
+ }
+
+ Some(key, result))
+ |> Map.ofSeq
+
+/// Build assembly reference tokens from baseline using ILBaselineReader.
+let private assemblyReferenceKeyFromBytes
+ (reader: ILBaselineReader.BaselineMetadataReader)
+ (assemblyRef: ILBaselineReader.AssemblyRefRowData)
+ =
+ {
+ AssemblyReferenceKey.Name = reader.GetString(assemblyRef.NameOffset)
+ MajorVersion = assemblyRef.MajorVersion
+ MinorVersion = assemblyRef.MinorVersion
+ BuildNumber = assemblyRef.BuildNumber
+ RevisionNumber = assemblyRef.RevisionNumber
+ Culture =
+ if assemblyRef.Culture = 0 then
+ ""
+ else
+ reader.GetString assemblyRef.Culture
+ PublicKeyOrToken = reader.GetBlob assemblyRef.PublicKeyOrToken |> Array.toList
+ Flags = assemblyRef.Flags
+ }
+
+/// Build assembly reference tokens from baseline using the complete AssemblyRef row identity.
+let private buildAssemblyReferenceTokensFromBytes (reader: ILBaselineReader.BaselineMetadataReader) : Map =
+ seq {
+ for rowId in 1 .. reader.AssemblyRefCount do
+ match reader.GetAssemblyRef(rowId) with
+ | Some assemblyRef ->
+ let key = assemblyReferenceKeyFromBytes reader assemblyRef
+ // AssemblyRef table index is 0x23, token = (0x23 << 24) | rowId
+ let token = (0x23 <<< 24) ||| rowId
+ yield key, token
+ | None -> ()
+ }
+ |> Map.ofSeq
+
+/// Build type reference tokens from baseline using ILBaselineReader.
+/// Keys carry the full typed scope chain (AssemblyRef identity, or the enclosing TypeRef key for
+/// nested TypeRefs) so rows with duplicate names under different scopes stay distinguishable.
+let private buildTypeReferenceTokensFromBytes (reader: ILBaselineReader.BaselineMetadataReader) : Map =
+ let keyCache = Dictionary()
+
+ // Resolution scope chains are bounded by nesting depth; guard against malformed metadata cycles.
+ let rec tryKeyForRow (rowId: int) (depth: int) : TypeReferenceKey option =
+ if depth > 64 then
+ None
+ else
+ match keyCache.TryGetValue rowId with
+ | true, cached -> cached
+ | _ ->
+ let result =
+ match reader.GetTypeRef(rowId) with
+ | None -> None
+ | Some typeRef ->
+ let (tableIndex, scopeRowId) = reader.DecodeResolutionScope(typeRef.ResolutionScope)
+
+ let scopeOpt =
+ // AssemblyRef scope (table 0x23 = 35)
+ if tableIndex = 35 then
+ reader.GetAssemblyRef(scopeRowId)
+ |> Option.map (assemblyReferenceKeyFromBytes reader >> TypeReferenceScope.Assembly)
+ // Nested TypeRef scope (table 0x01 = 1)
+ elif tableIndex = 1 && scopeRowId <> rowId then
+ tryKeyForRow scopeRowId (depth + 1) |> Option.map TypeReferenceScope.Nested
+ else
+ // Module/ModuleRef scopes have no stable cross-compilation identity here.
+ None
+
+ scopeOpt
+ |> Option.map (fun scope ->
+ {
+ TypeReferenceKey.Scope = scope
+ Namespace = reader.GetString(typeRef.NamespaceOffset)
+ Name = reader.GetString(typeRef.NameOffset)
+ })
+
+ keyCache[rowId] <- result
+ result
+
+ seq {
+ for rowId in 1 .. reader.TypeRefCount do
+ match tryKeyForRow rowId 0 with
+ | Some key ->
+ // TypeRef table index is 0x01, token = (0x01 << 24) | rowId
+ yield key, (0x01 <<< 24) ||| rowId
+ | None -> ()
+ }
+ |> Map.ofSeq
+
+let private attachMetadataHandlesFromBytesCore (bytes: byte[]) (baseline: FSharpEmitBaseline) : FSharpEmitBaseline =
+ match ILBaselineReader.BaselineMetadataReader.Create(bytes) with
+ | None -> baseline // Return unchanged if we can't read the metadata
+ | Some reader ->
+ let methodHandles = buildMethodHandlesFromBytes reader baseline.MethodTokens
+ let parameterHandles = buildParameterHandlesFromBytes reader baseline.MethodTokens
+ let propertyHandles = buildPropertyHandlesFromBytes reader baseline.PropertyTokens
+ let eventHandles = buildEventHandlesFromBytes reader baseline.EventTokens
+ let typeReferenceTokens = buildTypeReferenceTokensFromBytes reader
+ let assemblyReferenceTokens = buildAssemblyReferenceTokensFromBytes reader
+
+ let memberReferenceRows =
+ seq {
+ for rowId in 1 .. reader.MemberRefCount do
+ match reader.GetMemberRef rowId with
+ | Some row ->
+ yield
+ rowId,
+ {
+ BaselineMemberRefRow.Name = reader.GetString row.NameOffset
+ ParentToken = reader.DecodeMemberRefParentToken row.Parent
+ Signature = reader.GetBlob row.SignatureOffset
+ }
+ | None -> ()
+ }
+ |> Map.ofSeq
+
+ let typeSpecSignatures =
+ seq {
+ for rowId in 1 .. reader.TypeSpecCount do
+ match reader.GetTypeSpecSignatureOffset rowId with
+ | Some sigOffset -> yield rowId, reader.GetBlob sigOffset
+ | None -> ()
+ }
+ |> Map.ofSeq
+
+ let customAttributeRows =
+ seq {
+ for rowId in 1 .. reader.CustomAttributeCount do
+ match reader.GetCustomAttributeRow rowId with
+ | Some row ->
+ yield
+ rowId,
+ {
+ BaselineCustomAttributeRow.ParentToken = reader.DecodeHasCustomAttributeToken row.Parent
+ ConstructorToken = reader.DecodeCustomAttributeTypeToken row.Constructor
+ Value = reader.GetBlob row.ValueOffset
+ }
+ | None -> ()
+ }
+ |> Map.ofSeq
+
+ let cache =
+ {
+ MethodHandles = methodHandles
+ ParameterHandles = parameterHandles
+ PropertyHandles = propertyHandles
+ EventHandles = eventHandles
+ }
+
+ let moduleNameOffset =
+ match reader.GetModule() with
+ | Some m when m.NameOffset > 0 -> Some(StringOffset m.NameOffset)
+ | _ -> None
+
+ { baseline with
+ MetadataHandles = cache
+ ModuleNameOffset = moduleNameOffset
+ TypeReferenceTokens = typeReferenceTokens
+ AssemblyReferenceTokens = assemblyReferenceTokens
+ MemberReferenceRows = memberReferenceRows
+ TypeSpecSignatures = typeSpecSignatures
+ CustomAttributeRows = customAttributeRows
+ }
+
+/// Attach metadata handles from PE bytes without using SRM MetadataReader.
+let attachMetadataHandlesFromBytes (bytes: byte[]) (baseline: FSharpEmitBaseline) : FSharpEmitBaseline =
+ try
+ attachMetadataHandlesFromBytesCore bytes baseline
with
| :? BadImageFormatException
| :? IO.IOException
| :? ArgumentException
| :? IndexOutOfRangeException
| :? InvalidOperationException
- | :? OverflowException -> None
-
-let readFromAssemblyAndPdbBytes (assemblyBytes: byte[]) (portablePdbBytes: byte[] option) =
- match tryReadFromAssemblyAndPdbBytes assemblyBytes portablePdbBytes with
- | Some baseline -> baseline
- | None -> invalidArg (nameof assemblyBytes) "assembly bytes do not contain readable CLI metadata"
-
-let metadataSnapshotFromBytes = ILBaselineReader.metadataSnapshotFromBytes
-
-let readModuleMvid = ILBaselineReader.readModuleMvidFromBytes
+ | :? OverflowException -> baseline
+
+///
+/// Create a baseline directly from emitted assembly artifacts.
+/// Shared by CLI and checker entry points to keep token/heap capture behavior aligned.
+///
+let createFromEmittedArtifacts
+ (ilModule: ILModuleDef)
+ (tokenMappings: ILTokenMappings)
+ (assemblyBytes: byte[])
+ (portablePdbSnapshot: PortablePdbSnapshot option)
+ (ilxGenEnvironment: IlxGenEnvSnapshot option)
+ : FSharpEmitBaseline =
+ let moduleId =
+ readModuleMvid assemblyBytes |> Option.defaultWith System.Guid.NewGuid
+
+ let metadataSnapshot =
+ metadataSnapshotFromBytes assemblyBytes
+ |> Option.defaultWith (fun () -> failwith "Failed to read metadata from assembly bytes")
+
+ let baselineCore =
+ match ilxGenEnvironment with
+ | Some snapshot -> createWithEnvironment ilModule tokenMappings metadataSnapshot snapshot moduleId portablePdbSnapshot
+ | None -> create ilModule tokenMappings metadataSnapshot moduleId portablePdbSnapshot
+
+ attachMetadataHandlesFromBytes assemblyBytes baselineCore
diff --git a/src/Compiler/CodeGen/HotReloadPdb.fs b/src/Compiler/CodeGen/HotReloadPdb.fs
new file mode 100644
index 00000000000..d6d6925a3c6
--- /dev/null
+++ b/src/Compiler/CodeGen/HotReloadPdb.fs
@@ -0,0 +1,244 @@
+/// PDB delta emission for hot reload. createSnapshot reads the baseline via the SRM-free
+/// ILBaselineReader; emitDelta serializes the Portable PDB delta through SRM's PortablePdbBuilder.
+module internal FSharp.Compiler.HotReloadPdb
+
+open System
+open System.Collections.Immutable
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.Reflection.Metadata
+open System.Reflection.Metadata.Ecma335
+open System.Security.Cryptography
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.AbstractIL.ILPdbWriter
+open FSharp.Compiler.HotReloadBaseline
+
+module ILBaselineReader = FSharp.Compiler.AbstractIL.ILBaselineReader
+
+let private shouldTracePdb () =
+ let isEnabled (name: string) =
+ match Environment.GetEnvironmentVariable(name) with
+ | null -> false
+ | value when String.Equals(value, "1", StringComparison.OrdinalIgnoreCase) -> true
+ | value when String.Equals(value, "true", StringComparison.OrdinalIgnoreCase) -> true
+ | _ -> false
+
+ isEnabled "FSHARP_HOTRELOAD_TRACE_PDB"
+ || isEnabled "FSHARP_HOTRELOAD_TRACE_METADATA"
+
+/// Create a PDB snapshot from Portable PDB bytes.
+/// Uses pure F# parsing instead of SRM for the reading path.
+let private createPortablePdbContentIdProvider (checksumAlgorithm: HashAlgorithm) : Func, BlobContentId> =
+ let algorithm =
+ match checksumAlgorithm with
+ | HashAlgorithm.Sha1 -> SHA1.Create() :> System.Security.Cryptography.HashAlgorithm
+ | HashAlgorithm.Sha256 -> SHA256.Create() :> System.Security.Cryptography.HashAlgorithm
+
+ Func, BlobContentId>(fun content ->
+ let contentBytes = content |> Seq.collect (fun c -> c.GetBytes()) |> Array.ofSeq
+ let hash = algorithm.ComputeHash contentBytes
+ BlobContentId.FromHash hash)
+
+let createSnapshot (pdbBytes: byte[]) : PortablePdbSnapshot =
+ match ILBaselineReader.readPortablePdbMetadata pdbBytes with
+ | None -> failwith "Failed to parse Portable PDB metadata"
+ | Some pdbMeta ->
+ // Convert PDB table row counts to full 64-element array
+ // PDB tables start at index 0x30
+ let counts = Array.zeroCreate DeltaTokens.TableCount
+ // pdbMeta.TableRowCounts has 8 elements (indices 0-7 map to PDB tables 0x30-0x37)
+ counts.[DeltaTokens.tableDocument] <- pdbMeta.TableRowCounts.[0]
+ counts.[DeltaTokens.tableMethodDebugInformation] <- pdbMeta.TableRowCounts.[1]
+ counts.[DeltaTokens.tableLocalScope] <- pdbMeta.TableRowCounts.[2]
+ counts.[DeltaTokens.tableLocalVariable] <- pdbMeta.TableRowCounts.[3]
+ counts.[DeltaTokens.tableLocalConstant] <- pdbMeta.TableRowCounts.[4]
+ counts.[DeltaTokens.tableImportScope] <- pdbMeta.TableRowCounts.[5]
+ counts.[DeltaTokens.tableStateMachineMethod] <- pdbMeta.TableRowCounts.[6]
+ counts.[DeltaTokens.tableCustomDebugInformation] <- pdbMeta.TableRowCounts.[7]
+
+ {
+ Bytes = Array.copy pdbBytes
+ TableRowCounts = ImmutableArray.CreateRange counts
+ EntryPointToken = pdbMeta.EntryPointToken
+ }
+
+/// Emit a PDB delta for the given hot reload generation.
+/// Takes the metadata EncLog and EncMap (using TableName for type safety)
+/// and produces a Portable PDB delta that matches the metadata delta.
+let emitDelta
+ (baseline: FSharpEmitBaseline)
+ (updatedPdbBytes: byte[])
+ (addedOrChangedMethods: AddedOrChangedMethodInfo list)
+ (deltaToUpdatedMethodToken: IReadOnlyDictionary)
+ (_metadataEncLog: (TableName * int * EditAndContinueOperation) array)
+ (_metadataEncMap: (TableName * int) array)
+ : byte[] option =
+ match baseline.PortablePdb with
+ | None -> None
+ | Some _ ->
+ // info.MethodToken values are BASELINE-coordinate MethodDef tokens (the row the
+ // metadata delta re-emits the method at), NOT the fresh compile's tokens. Sort the
+ // distinct tokens by their BASELINE MethodDef row so the PDB MethodDebugInformation
+ // rows are appended in the same order the metadata writer sorts its method EncMap
+ // entries (FSharpDeltaMetadataWriter.fs emits Method EncMap rows using row.RowId — the
+ // baseline row — then sorts the whole EncMap ascending by token). Keeping both orders
+ // identical is the ORDERING INVARIANT: the delta's Nth MethodDebugInformation row must
+ // correspond to the Nth (baseline-row-sorted) PDB EncMap entry, or ApplyUpdate binds
+ // sequence points to the wrong method. Sorting here (rather than later, only on the
+ // EncMap) is required because for a multi-method delta after an add the fresh rows and
+ // baseline rows no longer share an order.
+ let distinctTokens =
+ addedOrChangedMethods
+ |> List.map (fun info -> info.MethodToken)
+ |> List.distinct
+ |> List.filter (fun token -> token <> 0)
+ |> List.sortBy (fun token -> MetadataTokens.GetRowNumber(MetadataTokens.MethodDefinitionHandle token))
+
+ if List.isEmpty distinctTokens then
+ if shouldTracePdb () then
+ printfn "[hotreload-pdb] distinct token list empty"
+
+ None
+ else
+ use provider =
+ MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.CreateRange updatedPdbBytes)
+
+ let reader = provider.GetMetadataReader()
+ let metadata = MetadataBuilder()
+ let documentMap = Dictionary()
+ let emittedMethodRows = ResizeArray()
+ let mutable emitted = false
+
+ let getOrAddDocument (sourceHandle: DocumentHandle) =
+ match documentMap.TryGetValue sourceHandle with
+ | true, handle -> handle
+ | _ ->
+ try
+ let document = reader.GetDocument sourceHandle
+ let name = reader.GetString document.Name
+
+ let hashBytes =
+ if document.Hash.IsNil then
+ Array.empty
+ else
+ reader.GetBlobBytes document.Hash
+
+ let hashAlgorithmGuid =
+ if document.HashAlgorithm.IsNil then
+ Guid.Empty
+ else
+ reader.GetGuid document.HashAlgorithm
+
+ let languageGuid =
+ if document.Language.IsNil then
+ Guid.Empty
+ else
+ reader.GetGuid document.Language
+
+ let nameHandle = metadata.GetOrAddDocumentName name
+ let hashHandle = metadata.GetOrAddBlob hashBytes
+ let hashAlgorithmHandle = metadata.GetOrAddGuid hashAlgorithmGuid
+ let languageHandle = metadata.GetOrAddGuid languageGuid
+
+ let added =
+ metadata.AddDocument(nameHandle, hashAlgorithmHandle, hashHandle, languageHandle)
+
+ documentMap[sourceHandle] <- added
+ added
+ with :? BadImageFormatException as ex ->
+ // Corrupted PDB metadata - skip this document gracefully
+ if shouldTracePdb () then
+ printfn "[hotreload-pdb] warning: could not read document (handle=%A): %s" sourceHandle ex.Message
+
+ DocumentHandle()
+
+ for token in distinctTokens do
+ let sourceToken =
+ match deltaToUpdatedMethodToken.TryGetValue token with
+ | true, mapped -> mapped
+ | _ -> token
+
+ if sourceToken = 0 then
+ if shouldTracePdb () then
+ printfn "[hotreload-pdb] method token missing for delta token 0x%08x" token
+ else
+ let sourceHandle = MetadataTokens.MethodDefinitionHandle sourceToken
+
+ if sourceHandle.IsNil then
+ if shouldTracePdb () then
+ printfn "[hotreload-pdb] source handle nil for delta token 0x%08x (source token=0x%08x)" token sourceToken
+ else
+ // Read at sourceHandle (fresh) and bounds-check against the fresh table
+ // count, but record the BASELINE MethodDef row in the EncMap: the metadata
+ // delta re-emits the method at its baseline row (FSharpDeltaMetadataWriter.fs),
+ // and the two EncMaps must agree even after an earlier edit shifts fresh rows.
+ let methodRow = MetadataTokens.GetRowNumber sourceHandle
+
+ let baselineMethodRow =
+ MetadataTokens.GetRowNumber(MetadataTokens.MethodDefinitionHandle token)
+
+ if methodRow <= reader.MethodDebugInformation.Count then
+ let methodInfo = reader.GetMethodDebugInformation sourceHandle
+
+ let targetDocument =
+ if methodInfo.Document.IsNil then
+ DocumentHandle()
+ else
+ getOrAddDocument methodInfo.Document
+
+ let sequencePointsHandle =
+ if methodInfo.SequencePointsBlob.IsNil then
+ BlobHandle()
+ else
+ metadata.GetOrAddBlob(reader.GetBlobBytes methodInfo.SequencePointsBlob)
+
+ metadata.AddMethodDebugInformation(targetDocument, sequencePointsHandle)
+ |> ignore
+
+ emittedMethodRows.Add(baselineMethodRow)
+ emitted <- true
+ else if
+ // A newly added method whose row exceeds the baseline
+ // MethodDebugInformation count has no debug info to re-emit here.
+ shouldTracePdb ()
+ then
+ let rowCount = reader.MethodDebugInformation.Count
+
+ printfn
+ $"[hotreload-pdb] skipping newly added method (row %d{methodRow} > count %d{rowCount}) - debugger stepping unavailable (delta=0x%08x{token}, source=0x%08x{sourceToken})"
+
+ // Per Roslyn DeltaMetadataWriter.cs: PDB delta EncMap should contain MethodDebugInformation
+ // entries (which correspond 1:1 to MethodDef), not metadata table entries. The PDB EncLog
+ // is not used - only EncMap with MethodDebugInformation handles.
+ // MethodDebugInformationHandle is a PDB-specific handle that doesn't implicitly convert
+ // to EntityHandle, so we construct the EntityHandle from the table/row token directly.
+ // Token format: (table_index << 24) | row_number, where MethodDebugInformation = 0x31
+ //
+ // ORDERING INVARIANT: the EncMap must be sorted by baseline row to match both the
+ // appended MethodDebugInformation rows and the metadata writer's method EncMap
+ // (FSharpDeltaMetadataWriter.fs), so the Nth row lines up with the Nth EncMap entry
+ // and ApplyUpdate binds each method's sequence points correctly.
+ for methodRow in emittedMethodRows |> Seq.distinct |> Seq.sort do
+ let token = (DeltaTokens.tableMethodDebugInformation <<< 24) ||| methodRow
+ let entityHandle = MetadataTokens.EntityHandle token
+ metadata.AddEncMapEntry entityHandle
+
+ if not emitted then
+ if shouldTracePdb () then
+ printfn $"[hotreload-pdb] no method debug info emitted for tokens {distinctTokens}"
+
+ None
+ else
+ let entryPointHandle = MethodDefinitionHandle()
+
+ // Use shared content ID provider from ILPdbWriter
+ let idProvider = createPortablePdbContentIdProvider HashAlgorithm.Sha256
+
+ let zeroCounts =
+ ImmutableArray.CreateRange(Array.zeroCreate DeltaTokens.TableCount)
+
+ let builder = PortablePdbBuilder(metadata, zeroCounts, entryPointHandle, idProvider)
+ let blobBuilder = BlobBuilder()
+ builder.Serialize blobBuilder |> ignore
+ Some(blobBuilder.ToArray())
diff --git a/src/Compiler/CodeGen/ILBaselineReader.fs b/src/Compiler/CodeGen/ILBaselineReader.fs
deleted file mode 100644
index e41faadd39e..00000000000
--- a/src/Compiler/CodeGen/ILBaselineReader.fs
+++ /dev/null
@@ -1,1015 +0,0 @@
-/// Minimal binary reader for baseline PE and portable PDB metadata.
-module internal FSharp.Compiler.CodeGen.ILBaselineReader
-
-open System
-open System.Collections.Immutable
-open System.IO
-open System.Reflection.PortableExecutable
-open System.Text
-
-type MetadataHeapSizes =
- {
- StringHeapSize: int
- UserStringHeapSize: int
- BlobHeapSize: int
- GuidHeapSize: int
- }
-
-type MetadataSnapshot =
- {
- HeapSizes: MetadataHeapSizes
- TableRowCounts: int[]
- GuidHeapStart: int
- }
-
-type PortablePdbMetadata =
- {
- ContentId: byte[]
- TableRowCounts: int[]
- EntryPointToken: int option
- }
-
-let private readUInt16 (bytes: byte[]) (offset: int) =
- uint16 bytes[offset] ||| (uint16 bytes[offset + 1] <<< 8)
-
-let private readInt32 (bytes: byte[]) (offset: int) =
- int bytes[offset]
- ||| (int bytes[offset + 1] <<< 8)
- ||| (int bytes[offset + 2] <<< 16)
- ||| (int bytes[offset + 3] <<< 24)
-
-/// Reads an unsigned 64-bit little-endian value without sign-extending either half.
-let internal readUInt64 (bytes: byte[]) (offset: int) =
- uint64 (uint32 (readInt32 bytes offset))
- ||| (uint64 (uint32 (readInt32 bytes (offset + 4))) <<< 32)
-
-[]
-let private tableCount = 64
-
-module private TableIndices =
- let Module = 0
- let TypeRef = 1
- let TypeDef = 2
- let FieldPtr = 3
- let Field = 4
- let MethodPtr = 5
- let MethodDef = 6
- let ParamPtr = 7
- let Param = 8
- let InterfaceImpl = 9
- let MemberRef = 10
- let Constant = 11
- let FieldMarshal = 13
- let DeclSecurity = 14
- let ClassLayout = 15
- let FieldLayout = 16
- let StandAloneSig = 17
- let EventMap = 18
- let EventPtr = 19
- let Event = 20
- let PropertyMap = 21
- let PropertyPtr = 22
- let Property = 23
- let MethodSemantics = 24
- let MethodImpl = 25
- let ModuleRef = 26
- let TypeSpec = 27
- let ImplMap = 28
- let FieldRVA = 29
- let Assembly = 32
- let AssemblyRef = 35
- let File = 38
- let ExportedType = 39
- let ManifestResource = 40
- let NestedClass = 41
- let GenericParam = 42
- let MethodSpec = 43
- let GenericParamConstraint = 44
-
-type private StreamHeader =
- { Offset: int; Size: int; Name: string }
-
-let private tryRvaToOffset (bytes: byte[]) (coffHeader: int) (optionalHeader: int) (sizeOfOptionalHeader: int) (rva: int) =
- let numberOfSections = int (readUInt16 bytes (coffHeader + 2))
- let sectionHeadersStart = optionalHeader + sizeOfOptionalHeader
-
- let rec loop sectionIndex =
- if sectionIndex >= numberOfSections then
- None
- else
- let sectionOffset = sectionHeadersStart + sectionIndex * 40
-
- if sectionOffset + 40 > bytes.Length then
- None
- else
- let virtualSize = readInt32 bytes (sectionOffset + 8)
- let virtualAddress = readInt32 bytes (sectionOffset + 12)
- let rawSize = readInt32 bytes (sectionOffset + 16)
- let pointerToRawData = readInt32 bytes (sectionOffset + 20)
- let span = max virtualSize rawSize
-
- if rva >= virtualAddress && rva < virtualAddress + span then
- Some(rva - virtualAddress + pointerToRawData)
- else
- loop (sectionIndex + 1)
-
- loop 0
-
-let private findMetadataRoot (bytes: byte[]) : int option =
- try
- if bytes.Length < 64 || bytes[0] <> 0x4Duy || bytes[1] <> 0x5Auy then
- None
- else
- let peOffset = readInt32 bytes 0x3C
-
- if peOffset < 0 || peOffset + 24 > bytes.Length then
- None
- elif
- bytes[peOffset] <> 0x50uy
- || bytes[peOffset + 1] <> 0x45uy
- || bytes[peOffset + 2] <> 0uy
- || bytes[peOffset + 3] <> 0uy
- then
- None
- else
- let coffHeader = peOffset + 4
- let sizeOfOptionalHeader = int (readUInt16 bytes (coffHeader + 16))
- let optionalHeader = coffHeader + 20
- let magic = readUInt16 bytes optionalHeader
-
- let dataDirectoryStart =
- if magic = 0x20Bus then
- optionalHeader + 112
- else
- optionalHeader + 96
-
- let cliDirectory = dataDirectoryStart + 14 * 8
-
- if cliDirectory + 8 > bytes.Length then
- None
- else
- let cliHeaderRva = readInt32 bytes cliDirectory
-
- if cliHeaderRva = 0 then
- None
- else
- match tryRvaToOffset bytes coffHeader optionalHeader sizeOfOptionalHeader cliHeaderRva with
- | None -> None
- | Some cliHeaderOffset when cliHeaderOffset + 12 > bytes.Length -> None
- | Some cliHeaderOffset ->
- let metadataRva = readInt32 bytes (cliHeaderOffset + 8)
- tryRvaToOffset bytes coffHeader optionalHeader sizeOfOptionalHeader metadataRva
- with
- | :? IndexOutOfRangeException
- | :? ArgumentOutOfRangeException -> None
-
-let private parseStreamHeaders (bytes: byte[]) (metadataRoot: int) : StreamHeader list =
- let signature = readInt32 bytes metadataRoot
-
- if signature <> 0x424A5342 then
- []
- else
- let versionLength = readInt32 bytes (metadataRoot + 12)
- let paddedVersionLength = (versionLength + 3) &&& ~~~3
- let streamsOffset = metadataRoot + 16 + paddedVersionLength
- let numberOfStreams = int (readUInt16 bytes (streamsOffset + 2))
- let mutable currentOffset = streamsOffset + 4
- let headers = ResizeArray()
-
- for _ in 1..numberOfStreams do
- let offset = readInt32 bytes currentOffset
- let size = readInt32 bytes (currentOffset + 4)
- let mutable nameEnd = currentOffset + 8
-
- while nameEnd < bytes.Length && bytes[nameEnd] <> 0uy do
- nameEnd <- nameEnd + 1
-
- if nameEnd >= bytes.Length then
- invalidArg (nameof bytes) "invalid metadata stream header"
-
- let name =
- Encoding.ASCII.GetString(bytes, currentOffset + 8, nameEnd - currentOffset - 8)
-
- let paddedNameLength = ((nameEnd - currentOffset - 8 + 1) + 3) &&& ~~~3
-
- headers.Add(
- {
- Offset = metadataRoot + offset
- Size = size
- Name = name
- }
- )
-
- currentOffset <- currentOffset + 8 + paddedNameLength
-
- headers |> Seq.toList
-
-let private findStream (headers: StreamHeader list) (name: string) =
- headers |> List.tryFind (fun header -> header.Name = name)
-
-let private parseTablesStream (bytes: byte[]) (tablesStream: StreamHeader) =
- let offset = tablesStream.Offset
- let heapSizes = bytes[offset + 6]
- let valid = readUInt64 bytes (offset + 8)
- let rowCounts = Array.zeroCreate tableCount
- let mutable rowCountOffset = offset + 24
-
- for i in 0..63 do
- if (valid &&& (1UL <<< i)) <> 0UL then
- let rowCount = readInt32 bytes rowCountOffset
-
- if rowCount < 0 then
- invalidArg (nameof bytes) "metadata table row counts must be non-negative"
-
- rowCounts[i] <- rowCount
- rowCountOffset <- rowCountOffset + 4
-
- heapSizes, rowCounts, offset, valid
-
-/// Computes the first table-row offset from the table header's valid-table mask.
-let internal tableDataStart tablesOffset (valid: uint64) =
- let mutable remaining = valid
- let mutable presentTableCount = 0
-
- while remaining <> 0UL do
- presentTableCount <- presentTableCount + 1
- remaining <- remaining &&& (remaining - 1UL)
-
- tablesOffset + 24 + (presentTableCount * 4)
-
-let metadataSnapshotFromBytes (bytes: byte[]) : MetadataSnapshot option =
- try
- match findMetadataRoot bytes with
- | None -> None
- | Some metadataRoot ->
- let streamHeaders = parseStreamHeaders bytes metadataRoot
- let stringsStream = findStream streamHeaders "#Strings"
- let userStringsStream = findStream streamHeaders "#US"
- let blobStream = findStream streamHeaders "#Blob"
- let guidStream = findStream streamHeaders "#GUID"
-
- let tablesStream =
- findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
-
- match tablesStream with
- | None -> None
- | Some tables ->
- let _, rowCounts, _, _ = parseTablesStream bytes tables
-
- let trimmedStringHeapSize =
- match stringsStream with
- | None -> 0
- | Some stream ->
- if stream.Size = 0 then
- 0
- else
- let last = stream.Offset + stream.Size - 1
- let mutable i = last
-
- while i >= stream.Offset && bytes[i] = 0uy do
- i <- i - 1
-
- if i = last then stream.Size else i - stream.Offset + 2
-
- let heapSizes =
- {
- StringHeapSize = trimmedStringHeapSize
- UserStringHeapSize =
- userStringsStream
- |> Option.map (fun stream -> stream.Size)
- |> Option.defaultValue 0
- BlobHeapSize = blobStream |> Option.map (fun stream -> stream.Size) |> Option.defaultValue 0
- GuidHeapSize = guidStream |> Option.map (fun stream -> stream.Size) |> Option.defaultValue 0
- }
-
- Some
- {
- HeapSizes = heapSizes
- TableRowCounts = rowCounts
- GuidHeapStart = heapSizes.GuidHeapSize
- }
- with
- | :? IndexOutOfRangeException
- | :? ArgumentOutOfRangeException -> None
-
-let private readGuidFromBytes (bytes: byte[]) (guidIndex: int) =
- if guidIndex <= 0 then
- None
- else
- match findMetadataRoot bytes with
- | None -> None
- | Some metadataRoot ->
- let streamHeaders = parseStreamHeaders bytes metadataRoot
-
- match findStream streamHeaders "#GUID" with
- | None -> None
- | Some guidStream ->
- let offset = guidStream.Offset + (guidIndex - 1) * 16
- let streamEnd = int64 guidStream.Offset + int64 guidStream.Size
- let guidEnd = int64 offset + 16L
-
- if
- guidStream.Offset < 0
- || guidStream.Size < 0
- || streamEnd > int64 bytes.Length
- || offset < guidStream.Offset
- || guidEnd > streamEnd
- then
- None
- else
- Some(Guid(bytes[offset .. offset + 15]))
-
-/// Reads the portable CodeView content ID embedded in a PE debug directory.
-let readCodeViewContentIdFromBytes (bytes: byte[]) : byte[] option =
- try
- use peReader = new PEReader(ImmutableArray.CreateRange bytes)
-
- peReader.ReadDebugDirectory()
- |> Seq.tryFind (fun entry -> entry.IsPortableCodeView)
- |> Option.map (fun entry ->
- let data = peReader.ReadCodeViewDebugDirectoryData entry
- let contentId = Array.zeroCreate 20
- data.Guid.ToByteArray().CopyTo(contentId, 0)
- BitConverter.GetBytes(entry.Stamp).CopyTo(contentId, 16)
- contentId)
- with
- | :? BadImageFormatException
- | :? IOException
- | :? InvalidOperationException -> None
-
-/// Parsed metadata context for reading table rows.
-/// Internal (not private): tiny reader members can get cross-module inlined in Release
-/// builds, and inlined code referencing a module-private type fails CLR visibility
-/// checks at runtime.
-type internal MetadataContext =
- {
- Bytes: byte[]
- HeapSizes: byte
- RowCounts: int[]
- TablesStart: int
- StringIndexSize: int
- GuidIndexSize: int
- BlobIndexSize: int
- StringsStreamOffset: int
- StringsStreamSize: int
- BlobStreamOffset: int
- }
-
-let private tableIndexSize (rowCounts: int[]) tableIndex =
- if rowCounts[tableIndex] <= 65535 then 2 else 4
-
-let private codedIndexSize (rowCounts: int[]) (tableIndices: int[]) tagBits =
- let maxRows =
- tableIndices
- |> Array.map (fun tableIndex -> if tableIndex < tableCount then rowCounts[tableIndex] else 0)
- |> Array.max
-
- let maxValue = (maxRows <<< tagBits) ||| ((1 <<< tagBits) - 1)
- if maxValue <= 65535 then 2 else 4
-
-let private resolutionScopeSize rowCounts =
- codedIndexSize
- rowCounts
- [|
- TableIndices.Module
- TableIndices.ModuleRef
- TableIndices.AssemblyRef
- TableIndices.TypeRef
- |]
- 2
-
-let private typeDefOrRefSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.TypeDef; TableIndices.TypeRef; TableIndices.TypeSpec |] 2
-
-let private hasConstantSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.Field; TableIndices.Param; TableIndices.Property |] 2
-
-let private hasCustomAttributeSize rowCounts =
- codedIndexSize
- rowCounts
- [|
- TableIndices.MethodDef
- TableIndices.Field
- TableIndices.TypeRef
- TableIndices.TypeDef
- TableIndices.Param
- TableIndices.InterfaceImpl
- TableIndices.MemberRef
- TableIndices.Module
- TableIndices.DeclSecurity
- TableIndices.Property
- TableIndices.Event
- TableIndices.StandAloneSig
- TableIndices.ModuleRef
- TableIndices.TypeSpec
- TableIndices.Assembly
- TableIndices.AssemblyRef
- TableIndices.File
- TableIndices.ExportedType
- TableIndices.ManifestResource
- TableIndices.GenericParam
- TableIndices.GenericParamConstraint
- TableIndices.MethodSpec
- |]
- 5
-
-let private hasFieldMarshalSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.Field; TableIndices.Param |] 1
-
-let private hasDeclSecuritySize rowCounts =
- codedIndexSize rowCounts [| TableIndices.TypeDef; TableIndices.MethodDef; TableIndices.Assembly |] 2
-
-let private memberRefParentSize rowCounts =
- codedIndexSize
- rowCounts
- [|
- TableIndices.TypeDef
- TableIndices.TypeRef
- TableIndices.ModuleRef
- TableIndices.MethodDef
- TableIndices.TypeSpec
- |]
- 3
-
-let private hasSemanticsSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.Event; TableIndices.Property |] 1
-
-let private methodDefOrRefSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.MethodDef; TableIndices.MemberRef |] 1
-
-let private memberForwardedSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.Field; TableIndices.MethodDef |] 1
-
-let private implementationSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.File; TableIndices.AssemblyRef; TableIndices.ExportedType |] 2
-
-let private customAttributeTypeSize rowCounts =
- codedIndexSize rowCounts [| 0; 0; TableIndices.MethodDef; TableIndices.MemberRef; 0 |] 3
-
-let private typeOrMethodDefSize rowCounts =
- codedIndexSize rowCounts [| TableIndices.TypeDef; TableIndices.MethodDef |] 1
-
-let private calculateTableRowSizes (ctx: MetadataContext) =
- let rowCounts = ctx.RowCounts
- let strIdx = ctx.StringIndexSize
- let guidIdx = ctx.GuidIndexSize
- let blobIdx = ctx.BlobIndexSize
- let sizes = Array.zeroCreate tableCount
-
- sizes[0] <- 2 + strIdx + guidIdx + guidIdx + guidIdx
- sizes[1] <- resolutionScopeSize rowCounts + strIdx + strIdx
-
- sizes[2] <-
- 4
- + strIdx
- + strIdx
- + typeDefOrRefSize rowCounts
- + tableIndexSize rowCounts TableIndices.Field
- + tableIndexSize rowCounts TableIndices.MethodDef
-
- sizes[4] <- 2 + strIdx + blobIdx
- sizes[6] <- 4 + 2 + 2 + strIdx + blobIdx + tableIndexSize rowCounts TableIndices.Param
- sizes[8] <- 2 + 2 + strIdx
- sizes[9] <- tableIndexSize rowCounts TableIndices.TypeDef + typeDefOrRefSize rowCounts
- sizes[10] <- memberRefParentSize rowCounts + strIdx + blobIdx
- sizes[11] <- 2 + hasConstantSize rowCounts + blobIdx
- sizes[12] <- hasCustomAttributeSize rowCounts + customAttributeTypeSize rowCounts + blobIdx
- sizes[13] <- hasFieldMarshalSize rowCounts + blobIdx
- sizes[14] <- 2 + hasDeclSecuritySize rowCounts + blobIdx
- sizes[15] <- 2 + 4 + tableIndexSize rowCounts TableIndices.TypeDef
- sizes[16] <- 4 + tableIndexSize rowCounts TableIndices.Field
- sizes[17] <- blobIdx
-
- sizes[18] <-
- tableIndexSize rowCounts TableIndices.TypeDef
- + tableIndexSize rowCounts TableIndices.Event
-
- sizes[20] <- 2 + strIdx + typeDefOrRefSize rowCounts
-
- sizes[21] <-
- tableIndexSize rowCounts TableIndices.TypeDef
- + tableIndexSize rowCounts TableIndices.Property
-
- sizes[23] <- 2 + strIdx + blobIdx
- sizes[24] <- 2 + tableIndexSize rowCounts TableIndices.MethodDef + hasSemanticsSize rowCounts
-
- sizes[25] <-
- tableIndexSize rowCounts TableIndices.TypeDef
- + methodDefOrRefSize rowCounts
- + methodDefOrRefSize rowCounts
-
- sizes[26] <- strIdx
- sizes[27] <- blobIdx
-
- sizes[28] <-
- 2
- + memberForwardedSize rowCounts
- + strIdx
- + tableIndexSize rowCounts TableIndices.ModuleRef
-
- sizes[29] <- 4 + tableIndexSize rowCounts TableIndices.Field
- sizes[32] <- 4 + 2 + 2 + 2 + 2 + 4 + blobIdx + strIdx + strIdx
- sizes[35] <- 2 + 2 + 2 + 2 + 4 + blobIdx + strIdx + strIdx + blobIdx
- sizes[38] <- 4 + strIdx + blobIdx
- sizes[39] <- 4 + 4 + strIdx + strIdx + implementationSize rowCounts
- sizes[40] <- 4 + 4 + strIdx + implementationSize rowCounts
-
- sizes[41] <-
- tableIndexSize rowCounts TableIndices.TypeDef
- + tableIndexSize rowCounts TableIndices.TypeDef
-
- sizes[42] <- 2 + 2 + typeOrMethodDefSize rowCounts + strIdx
- sizes[43] <- methodDefOrRefSize rowCounts + blobIdx
- sizes[44] <- tableIndexSize rowCounts TableIndices.GenericParam + typeDefOrRefSize rowCounts
- sizes
-
-let private calculateTableOffsets (ctx: MetadataContext) (rowSizes: int[]) =
- let offsets = Array.zeroCreate tableCount
- let mutable currentOffset = ctx.TablesStart
-
- for i in 0 .. tableCount - 1 do
- offsets[i] <- currentOffset
- currentOffset <- currentOffset + rowSizes[i] * ctx.RowCounts[i]
-
- offsets
-
-let private readHeapIndex (bytes: byte[]) offset indexSize =
- if indexSize = 2 then
- int (readUInt16 bytes offset)
- else
- readInt32 bytes offset
-
-let private createMetadataContext (bytes: byte[]) =
- match findMetadataRoot bytes with
- | None -> None
- | Some metadataRoot ->
- let streamHeaders = parseStreamHeaders bytes metadataRoot
-
- let tablesStream =
- findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
-
- match tablesStream with
- | None -> None
- | Some stream ->
- let heapSizes, rowCounts, tablesOffset, valid = parseTablesStream bytes stream
-
- let pointerTables =
- [|
- TableIndices.FieldPtr
- TableIndices.MethodPtr
- TableIndices.ParamPtr
- TableIndices.EventPtr
- TableIndices.PropertyPtr
- |]
-
- // The #- stream permits pointer-table indirection. This reader consumes the
- // definition tables directly, so accepting a non-empty pointer table would
- // associate members with the wrong declaring type.
- if
- stream.Name = "#-"
- && pointerTables |> Array.exists (fun table -> rowCounts[table] <> 0)
- then
- None
- else
- let stringsBig = (heapSizes &&& 0x01uy) <> 0uy
- let guidsBig = (heapSizes &&& 0x02uy) <> 0uy
- let blobsBig = (heapSizes &&& 0x04uy) <> 0uy
-
- let stringsStream =
- streamHeaders |> List.tryFind (fun header -> header.Name = "#Strings")
-
- Some
- {
- Bytes = bytes
- HeapSizes = heapSizes
- RowCounts = rowCounts
- TablesStart = tableDataStart tablesOffset valid
- StringIndexSize = if stringsBig then 4 else 2
- GuidIndexSize = if guidsBig then 4 else 2
- BlobIndexSize = if blobsBig then 4 else 2
- StringsStreamOffset =
- stringsStream
- |> Option.map (fun header -> header.Offset)
- |> Option.defaultValue 0
- StringsStreamSize = stringsStream |> Option.map (fun header -> header.Size) |> Option.defaultValue 0
- BlobStreamOffset =
- streamHeaders
- |> List.tryFind (fun h -> h.Name = "#Blob")
- |> Option.map (fun h -> h.Offset)
- |> Option.defaultValue 0
- }
-
-let private readStringFromHeap (ctx: MetadataContext) offset =
- if offset = 0 then
- ""
- else
- let streamStart = int64 ctx.StringsStreamOffset
- let streamSize = int64 ctx.StringsStreamSize
- let streamEnd = streamStart + streamSize
- let stringStart = streamStart + int64 offset
-
- // Metadata indices are scoped to #Strings, not to the containing PE image.
- // Failing before decoding prevents malformed offsets from reading an adjacent heap.
- if
- offset < 0
- || streamStart < 0L
- || streamSize < 0L
- || streamEnd > int64 ctx.Bytes.Length
- || stringStart < streamStart
- || stringStart >= streamEnd
- then
- raise (BadImageFormatException("String heap index is outside the #Strings stream."))
-
- let start = int stringStart
- let streamEnd = int streamEnd
- let mutable endPos = start
-
- while endPos < streamEnd && ctx.Bytes[endPos] <> 0uy do
- endPos <- endPos + 1
-
- if endPos = streamEnd then
- raise (BadImageFormatException("String heap value is not terminated inside the #Strings stream."))
-
- Encoding.UTF8.GetString(ctx.Bytes, start, endPos - start)
-
-let private readBlobFromHeap (ctx: MetadataContext) offset =
- if offset <= 0 then
- Array.empty
- else
- let start = ctx.BlobStreamOffset + offset
- let b0 = int ctx.Bytes[start]
-
- let length, headerSize =
- if b0 &&& 0x80 = 0 then
- b0, 1
- elif b0 &&& 0xC0 = 0x80 then
- ((b0 &&& 0x3F) <<< 8) ||| int ctx.Bytes[start + 1], 2
- else
- (((b0 &&& 0x1F) <<< 24)
- ||| (int ctx.Bytes[start + 1] <<< 16)
- ||| (int ctx.Bytes[start + 2] <<< 8)
- ||| int ctx.Bytes[start + 3]),
- 4
-
- if length = 0 then
- Array.empty
- else
- ctx.Bytes[start + headerSize .. start + headerSize + length - 1]
-
-type TypeDefRowData =
- {
- Flags: int
- NameOffset: int
- NamespaceOffset: int
- Extends: int
- FieldList: int
- MethodList: int
- }
-
-type FieldRowData =
- {
- Flags: int
- NameOffset: int
- SignatureOffset: int
- }
-
-type MethodDefRowData =
- {
- RVA: int
- ImplFlags: int
- Flags: int
- NameOffset: int
- SignatureOffset: int
- ParamList: int
- }
-
-type PropertyMapRowData = { Parent: int; PropertyList: int }
-
-type PropertyRowData =
- {
- Flags: int
- NameOffset: int
- SignatureOffset: int
- }
-
-type EventMapRowData = { Parent: int; EventList: int }
-
-type EventRowData =
- {
- Flags: int
- NameOffset: int
- EventType: int
- }
-
-type ModuleRowData =
- {
- Generation: int
- NameOffset: int
- MvidIndex: int
- EncIdIndex: int
- EncBaseIdIndex: int
- }
-
-let private rowOffset (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) tableIndex rowId =
- if rowId < 1 || rowId > ctx.RowCounts[tableIndex] then
- None
- else
- Some(tableOffsets[tableIndex] + (rowId - 1) * rowSizes[tableIndex])
-
-let private readTypeDefRow ctx rowSizes tableOffsets rowId =
- rowOffset ctx rowSizes tableOffsets TableIndices.TypeDef rowId
- |> Option.map (fun offset ->
- let extendsOffset = offset + 4 + ctx.StringIndexSize + ctx.StringIndexSize
-
- {
- Flags = readInt32 ctx.Bytes offset
- NameOffset = readHeapIndex ctx.Bytes (offset + 4) ctx.StringIndexSize
- NamespaceOffset = readHeapIndex ctx.Bytes (offset + 4 + ctx.StringIndexSize) ctx.StringIndexSize
- Extends = readHeapIndex ctx.Bytes extendsOffset (typeDefOrRefSize ctx.RowCounts)
- FieldList =
- readHeapIndex ctx.Bytes (extendsOffset + typeDefOrRefSize ctx.RowCounts) (tableIndexSize ctx.RowCounts TableIndices.Field)
- MethodList =
- readHeapIndex
- ctx.Bytes
- (extendsOffset
- + typeDefOrRefSize ctx.RowCounts
- + tableIndexSize ctx.RowCounts TableIndices.Field)
- (tableIndexSize ctx.RowCounts TableIndices.MethodDef)
- })
-
-let private readFieldRow ctx rowSizes tableOffsets rowId =
- rowOffset ctx rowSizes tableOffsets TableIndices.Field rowId
- |> Option.map (fun offset ->
- {
- Flags = int (readUInt16 ctx.Bytes offset)
- NameOffset = readHeapIndex ctx.Bytes (offset + 2) ctx.StringIndexSize
- SignatureOffset = readHeapIndex ctx.Bytes (offset + 2 + ctx.StringIndexSize) ctx.BlobIndexSize
- })
-
-let private readMethodDefRow ctx rowSizes tableOffsets rowId =
- rowOffset ctx rowSizes tableOffsets TableIndices.MethodDef rowId
- |> Option.map (fun offset ->
- {
- RVA = readInt32 ctx.Bytes offset
- ImplFlags = int (readUInt16 ctx.Bytes (offset + 4))
- Flags = int (readUInt16 ctx.Bytes (offset + 6))
- NameOffset = readHeapIndex ctx.Bytes (offset + 8) ctx.StringIndexSize
- SignatureOffset = readHeapIndex ctx.Bytes (offset + 8 + ctx.StringIndexSize) ctx.BlobIndexSize
- ParamList =
- readHeapIndex
- ctx.Bytes
- (offset + 8 + ctx.StringIndexSize + ctx.BlobIndexSize)
- (tableIndexSize ctx.RowCounts TableIndices.Param)
- })
-
-let private readPropertyMapRow ctx rowSizes tableOffsets rowId =
- rowOffset ctx rowSizes tableOffsets TableIndices.PropertyMap rowId
- |> Option.map (fun offset ->
- {
- Parent = readHeapIndex ctx.Bytes offset (tableIndexSize ctx.RowCounts TableIndices.TypeDef)
- PropertyList =
- readHeapIndex
- ctx.Bytes
- (offset + tableIndexSize ctx.RowCounts TableIndices.TypeDef)
- (tableIndexSize ctx.RowCounts TableIndices.Property)
- })
-
-let private readPropertyRow ctx rowSizes tableOffsets rowId =
- rowOffset ctx rowSizes tableOffsets TableIndices.Property rowId
- |> Option.map (fun offset ->
- {
- Flags = int (readUInt16 ctx.Bytes offset)
- NameOffset = readHeapIndex ctx.Bytes (offset + 2) ctx.StringIndexSize
- SignatureOffset = readHeapIndex ctx.Bytes (offset + 2 + ctx.StringIndexSize) ctx.BlobIndexSize
- })
-
-let private readEventMapRow ctx rowSizes tableOffsets rowId =
- rowOffset ctx rowSizes tableOffsets TableIndices.EventMap rowId
- |> Option.map (fun offset ->
- {
- Parent = readHeapIndex ctx.Bytes offset (tableIndexSize ctx.RowCounts TableIndices.TypeDef)
- EventList =
- readHeapIndex
- ctx.Bytes
- (offset + tableIndexSize ctx.RowCounts TableIndices.TypeDef)
- (tableIndexSize ctx.RowCounts TableIndices.Event)
- })
-
-let private readEventRow ctx rowSizes tableOffsets rowId =
- rowOffset ctx rowSizes tableOffsets TableIndices.Event rowId
- |> Option.map (fun offset ->
- {
- Flags = int (readUInt16 ctx.Bytes offset)
- NameOffset = readHeapIndex ctx.Bytes (offset + 2) ctx.StringIndexSize
- EventType = readHeapIndex ctx.Bytes (offset + 2 + ctx.StringIndexSize) (typeDefOrRefSize ctx.RowCounts)
- })
-
-let private readModuleRow (ctx: MetadataContext) (tableOffsets: int[]) =
- if ctx.RowCounts[TableIndices.Module] < 1 then
- None
- else
- let offset = tableOffsets[TableIndices.Module]
-
- Some
- {
- Generation = int (readUInt16 ctx.Bytes offset)
- NameOffset = readHeapIndex ctx.Bytes (offset + 2) ctx.StringIndexSize
- MvidIndex = readHeapIndex ctx.Bytes (offset + 2 + ctx.StringIndexSize) ctx.GuidIndexSize
- EncIdIndex = readHeapIndex ctx.Bytes (offset + 2 + ctx.StringIndexSize + ctx.GuidIndexSize) ctx.GuidIndexSize
- EncBaseIdIndex =
- readHeapIndex ctx.Bytes (offset + 2 + ctx.StringIndexSize + ctx.GuidIndexSize + ctx.GuidIndexSize) ctx.GuidIndexSize
- }
-
-type BaselineMetadataReader private (ctx: MetadataContext, rowSizes: int[], tableOffsets: int[]) =
-
- static member Create(bytes: byte[]) =
- try
- match createMetadataContext bytes with
- | None -> None
- | Some ctx ->
- let rowSizes = calculateTableRowSizes ctx
- let tableOffsets = calculateTableOffsets ctx rowSizes
- Some(BaselineMetadataReader(ctx, rowSizes, tableOffsets))
- with
- | :? IndexOutOfRangeException
- | :? ArgumentOutOfRangeException -> None
-
- member _.RowCounts = ctx.RowCounts
-
- member _.TypeDefCount = ctx.RowCounts[TableIndices.TypeDef]
-
- member _.FieldCount = ctx.RowCounts[TableIndices.Field]
-
- member _.MethodDefCount = ctx.RowCounts[TableIndices.MethodDef]
-
- member _.PropertyMapCount = ctx.RowCounts[TableIndices.PropertyMap]
-
- member _.PropertyCount = ctx.RowCounts[TableIndices.Property]
-
- member _.EventMapCount = ctx.RowCounts[TableIndices.EventMap]
-
- member _.EventCount = ctx.RowCounts[TableIndices.Event]
-
- member _.GetModule() = readModuleRow ctx tableOffsets
-
- member _.GetTypeDef(rowId: int) =
- readTypeDefRow ctx rowSizes tableOffsets rowId
-
- member _.GetField(rowId: int) =
- readFieldRow ctx rowSizes tableOffsets rowId
-
- member _.GetMethodDef(rowId: int) =
- readMethodDefRow ctx rowSizes tableOffsets rowId
-
- member _.GetPropertyMap(rowId: int) =
- readPropertyMapRow ctx rowSizes tableOffsets rowId
-
- member _.GetProperty(rowId: int) =
- readPropertyRow ctx rowSizes tableOffsets rowId
-
- member _.GetEventMap(rowId: int) =
- readEventMapRow ctx rowSizes tableOffsets rowId
-
- member _.GetEvent(rowId: int) =
- readEventRow ctx rowSizes tableOffsets rowId
-
- member _.GetString(offset: int) = readStringFromHeap ctx offset
-
- member _.GetBlob(offset: int) = readBlobFromHeap ctx offset
-
- member this.GetTypeFieldRange(typeRowId: int) =
- match this.GetTypeDef typeRowId with
- | None -> None
- | Some typeDef ->
- let firstField = typeDef.FieldList
-
- let lastField =
- if typeRowId < ctx.RowCounts[TableIndices.TypeDef] then
- match this.GetTypeDef(typeRowId + 1) with
- | Some next -> next.FieldList - 1
- | None -> ctx.RowCounts[TableIndices.Field]
- else
- ctx.RowCounts[TableIndices.Field]
-
- if firstField <= 0 || firstField > lastField then
- None
- else
- Some(firstField, lastField)
-
- member this.GetTypeMethodRange(typeRowId: int) =
- match this.GetTypeDef typeRowId with
- | None -> None
- | Some typeDef ->
- let firstMethod = typeDef.MethodList
-
- let lastMethod =
- if typeRowId < ctx.RowCounts[TableIndices.TypeDef] then
- match this.GetTypeDef(typeRowId + 1) with
- | Some next -> next.MethodList - 1
- | None -> ctx.RowCounts[TableIndices.MethodDef]
- else
- ctx.RowCounts[TableIndices.MethodDef]
-
- if firstMethod <= 0 || firstMethod > lastMethod then
- None
- else
- Some(firstMethod, lastMethod)
-
- member this.GetPropertyMapRange(propertyMapRowId: int) =
- match this.GetPropertyMap propertyMapRowId with
- | None -> None
- | Some map ->
- let firstProperty = map.PropertyList
-
- let lastProperty =
- if propertyMapRowId < ctx.RowCounts[TableIndices.PropertyMap] then
- match this.GetPropertyMap(propertyMapRowId + 1) with
- | Some next -> next.PropertyList - 1
- | None -> ctx.RowCounts[TableIndices.Property]
- else
- ctx.RowCounts[TableIndices.Property]
-
- if firstProperty <= 0 || firstProperty > lastProperty then
- None
- else
- Some(map.Parent, firstProperty, lastProperty)
-
- member this.GetEventMapRange(eventMapRowId: int) =
- match this.GetEventMap eventMapRowId with
- | None -> None
- | Some map ->
- let firstEvent = map.EventList
-
- let lastEvent =
- if eventMapRowId < ctx.RowCounts[TableIndices.EventMap] then
- match this.GetEventMap(eventMapRowId + 1) with
- | Some next -> next.EventList - 1
- | None -> ctx.RowCounts[TableIndices.Event]
- else
- ctx.RowCounts[TableIndices.Event]
-
- if firstEvent <= 0 || firstEvent > lastEvent then
- None
- else
- Some(map.Parent, firstEvent, lastEvent)
-
-let readModuleMvidFromBytes (bytes: byte[]) : Guid option =
- try
- match BaselineMetadataReader.Create bytes with
- | None -> None
- | Some reader -> reader.GetModule() |> Option.bind (fun m -> readGuidFromBytes bytes m.MvidIndex)
- with
- | :? IndexOutOfRangeException
- | :? ArgumentOutOfRangeException -> None
-
-let private parsePdbStream (bytes: byte[]) (pdbStream: StreamHeader) =
- if pdbStream.Size < 24 then
- None
- else
- let entryPointToken = readInt32 bytes (pdbStream.Offset + 20)
- if entryPointToken = 0 then None else Some entryPointToken
-
-let private parsePdbTablesStream (bytes: byte[]) (tablesStream: StreamHeader) =
- let offset = tablesStream.Offset
- let valid = readUInt64 bytes (offset + 8)
- let pdbRowCounts = Array.zeroCreate 8
- let mutable rowCountOffset = offset + 24
-
- for i in 0..63 do
- if (valid &&& (1UL <<< i)) <> 0UL then
- let count = readInt32 bytes rowCountOffset
-
- if i >= 0x30 && i <= 0x37 then
- pdbRowCounts[i - 0x30] <- count
-
- rowCountOffset <- rowCountOffset + 4
-
- pdbRowCounts
-
-let readPortablePdbMetadata (pdbBytes: byte[]) =
- if pdbBytes.Length < 4 then
- None
- else
- try
- if readInt32 pdbBytes 0 <> 0x424A5342 then
- None
- else
- let streamHeaders = parseStreamHeaders pdbBytes 0
-
- let tablesStream =
- findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
-
- let pdbStream = findStream streamHeaders "#Pdb"
-
- Option.map2
- (fun stream pdb ->
- {
- ContentId = pdbBytes[pdb.Offset .. pdb.Offset + 19]
- TableRowCounts = parsePdbTablesStream pdbBytes stream
- EntryPointToken = parsePdbStream pdbBytes pdb
- })
- tablesStream
- (pdbStream |> Option.filter (fun stream -> stream.Size >= 24))
- with
- | :? IndexOutOfRangeException
- | :? ArgumentOutOfRangeException -> None
diff --git a/src/Compiler/CodeGen/IlxDeltaEmitter.fs b/src/Compiler/CodeGen/IlxDeltaEmitter.fs
new file mode 100644
index 00000000000..40f74af59c4
--- /dev/null
+++ b/src/Compiler/CodeGen/IlxDeltaEmitter.fs
@@ -0,0 +1,6225 @@
+module internal FSharp.Compiler.IlxDeltaEmitter
+
+open System
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.IO
+open System.Linq
+open System.Reflection.Metadata
+open System.Reflection.Metadata.Ecma335
+open System.Reflection
+open System.Reflection.Emit
+open System.Reflection.PortableExecutable
+open FSharp.Compiler.AbstractIL.IL
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.AbstractIL.ILPdbWriter
+open FSharp.Compiler.EditAndContinue
+open FSharp.Compiler.HotReload
+open FSharp.Compiler.HotReload.SymbolChanges
+open FSharp.Compiler.HotReload.SymbolMatcher
+open FSharp.Compiler.HotReloadBaseline
+open FSharp.Compiler.HotReloadPdb
+open FSharp.Compiler.AbstractIL.IlxDeltaStreams
+open FSharp.Compiler.CodeGen.FSharpDefinitionIndex
+open FSharp.Compiler.GeneratedNames
+open FSharp.Compiler.SynthesizedTypeMaps
+open FSharp.Compiler.Syntax.PrettyNaming
+open FSharp.Compiler.TypedTreeDiff
+open Internal.Utilities
+open FSharp.Compiler.EnvironmentHelpers
+
+module MetadataWriter = FSharp.Compiler.AbstractIL.FSharpDeltaMetadataWriter
+
+open MetadataWriter
+open FSharp.Compiler.AbstractIL.DeltaMetadataTables
+open FSharp.Compiler.AbstractIL.DeltaMetadataTypes
+
+exception HotReloadUnsupportedEditException of string
+
+module ILWriter = FSharp.Compiler.AbstractIL.ILBinaryWriter
+
+let private normalizeGeneratedFieldName (name: string) =
+ match name.IndexOf('@') with
+ | -1 -> name
+ | idx when idx > 0 -> name.Substring(0, idx)
+ | _ -> name
+
+/// Returns a synthesized-type match only when structural matching produced one
+/// unambiguous candidate; recorded snapshots never justify choosing an arbitrary row.
+let internal tryGetUniqueSynthesizedTypeMatch (matches: 'T array) =
+ match matches with
+ | [| single |] -> Some single
+ | _ -> None
+
+/// Chooses by deterministic recorded allocation-slot order only when a complete recorded
+/// snapshot accompanies the same whole-module read-back artifact. The caller first removes
+/// aliases already claimed by a different fresh type, so the resulting choice remains
+/// injective. This is recorded-slot recovery, not an arbitrary shape fallback; all other
+/// paths retain the general unique-and-structural fail-closed rule.
+let internal tryGetRecordedOrUniqueSynthesizedTypeMatch canUseRecordedOrder (matches: 'T array) =
+ if canUseRecordedOrder then
+ Array.tryHead matches
+ else
+ tryGetUniqueSynthesizedTypeMatch matches
+
+/// Converts an SRM EntityHandle to our TypeDefOrRef type for EventType fields
+let private entityHandleToTypeDefOrRef (handle: EntityHandle) : TypeDefOrRef =
+ let rowId = MetadataTokens.GetRowNumber handle
+
+ match handle.Kind with
+ | HandleKind.TypeDefinition -> TDR_TypeDef(TypeDefHandle rowId)
+ | HandleKind.TypeReference -> TDR_TypeRef(TypeRefHandle rowId)
+ | HandleKind.TypeSpecification -> TDR_TypeSpec(TypeSpecHandle rowId)
+ | _ -> TDR_TypeDef(TypeDefHandle 0) // Nil handle maps to TypeDef 0
+
+/// Converts SRM ExceptionRegion to our IlExceptionRegion type
+let private convertExceptionRegions (regions: ImmutableArray) : IlExceptionRegion[] =
+ if regions.IsDefaultOrEmpty then
+ [||]
+ else
+ regions.ToArray()
+ |> Array.map (fun region ->
+ let kind =
+ match region.Kind with
+ | ExceptionRegionKind.Catch -> IlExceptionRegionKind.Catch
+ | ExceptionRegionKind.Filter -> IlExceptionRegionKind.Filter
+ | ExceptionRegionKind.Finally -> IlExceptionRegionKind.Finally
+ | ExceptionRegionKind.Fault -> IlExceptionRegionKind.Fault
+ | _ -> IlExceptionRegionKind.Catch
+
+ let catchToken =
+ if region.CatchType.IsNil then
+ 0
+ else
+ MetadataTokens.GetToken(region.CatchType)
+
+ {
+ IlExceptionRegion.Kind = kind
+ TryOffset = region.TryOffset
+ TryLength = region.TryLength
+ HandlerOffset = region.HandlerOffset
+ HandlerLength = region.HandlerLength
+ CatchTypeToken = catchToken
+ FilterOffset = region.FilterOffset
+ })
+
+/// Represents the emitted artifacts for a hot reload delta.
+/// This is the primary output from IlxDeltaEmitter, containing all deltas needed
+/// for MetadataUpdater.ApplyUpdate.
+type IlxDelta =
+ {
+ Metadata: byte[]
+ IL: byte[]
+ Pdb: byte[] option
+ /// EncLog entries using TableName from BinaryConstants for type safety
+ EncLog: (TableName * int * EditAndContinueOperation) array
+ /// EncMap entries using TableName from BinaryConstants for type safety
+ EncMap: (TableName * int) array
+ UpdatedTypeTokens: int list
+ UpdatedMethodTokens: int list
+ /// Runtime capabilities the host must verify before applying this delta.
+ RequiredCapabilities: string list
+ AddedOrChangedMethods: HotReloadBaseline.AddedOrChangedMethodInfo list
+ MethodBodies: MethodBodyUpdate list
+ StandaloneSignatures: StandaloneSignatureUpdate list
+ GenerationId: Guid
+ BaseGenerationId: Guid
+ UserStringUpdates: (int * int * string) list
+ MethodDefinitionRows: MethodDefinitionRowInfo list
+ UpdatedBaseline: FSharpEmitBaseline option
+ /// Per-document line updates for methods whose code MOVED without changing (Roslyn
+ /// SequencePointUpdates). The debugger rebinds these methods' sequence points
+ /// without any metadata/IL being applied for them.
+ SequencePointUpdates: FSharp.Compiler.CodeAnalysis.FSharpSequencePointUpdates list
+ /// The next committed sequence-point view, keyed by baseline/delta MethodDef token —
+ /// the fresh compile's sequence points for every matched method. None when the
+ /// analysis was unavailable (no baseline PDB or no fresh PDB). Hosts/sessions replace
+ /// FSharpEmitBaseline.SequencePointSnapshots with this map when the update is committed;
+ /// for deltas that chain a baseline this is already folded into UpdatedBaseline.
+ ChainedSequencePoints: Map option
+ /// Per-statement remap results for the host-supplied active statements (Roslyn
+ /// ManagedHotReloadUpdate.ActiveStatements). Populated by the language service after a
+ /// successful emit; the raw emitter leaves it empty.
+ ActiveStatementUpdates: FSharp.Compiler.CodeAnalysis.FSharpActiveStatementRemapResult list
+ }
+
+/// Bytes and token mappings produced by the same whole-module write that created a fresh output
+/// assembly. Callers pass None when no such write is available, preserving the legacy emitter
+/// re-serialization path.
+type HotReloadEmittedArtifacts =
+ {
+ AssemblyBytes: byte[]
+ PdbBytes: byte[] option
+ TokenMappings: ILWriter.ILTokenMappings
+ }
+
+/// Request payload used when producing a delta.
+type IlxDeltaRequest =
+ {
+ Baseline: FSharpEmitBaseline
+ UpdatedTypes: string list
+ UpdatedMethods: MethodDefinitionKey list
+ UpdatedAccessors: AccessorUpdate list
+ Module: ILModuleDef
+ SymbolChanges: FSharpSymbolChanges option
+ CurrentGeneration: int
+ PreviousGenerationId: Guid option
+ SynthesizedNames: FSharpSynthesizedTypeMaps option
+ EmittedArtifacts: HotReloadEmittedArtifacts option
+ }
+
+type private MethodMetadataInfo = MethodAttributes * MethodImplAttributes * string * byte[] * StringOffset option * BlobOffset option
+
+[]
+type internal EntityTokenRemapKind =
+ | TypeDef
+ | FieldDef
+ | MethodDef
+ | MemberRef
+ | MethodSpec
+ | TypeRef
+ | TypeSpec
+ | Event
+ | Property
+ | AssemblyRef
+ | Passthrough
+
+let internal classifyEntityTokenRemapKind (token: int) : EntityTokenRemapKind =
+ match token &&& 0xFF000000 with
+ | 0x02000000 -> EntityTokenRemapKind.TypeDef
+ | 0x04000000 -> EntityTokenRemapKind.FieldDef
+ | 0x06000000 -> EntityTokenRemapKind.MethodDef
+ | 0x0A000000 -> EntityTokenRemapKind.MemberRef
+ | 0x2B000000 -> EntityTokenRemapKind.MethodSpec
+ | 0x01000000 -> EntityTokenRemapKind.TypeRef
+ | 0x1B000000 -> EntityTokenRemapKind.TypeSpec
+ | 0x14000000 -> EntityTokenRemapKind.Event
+ | 0x17000000 -> EntityTokenRemapKind.Property
+ | 0x23000000 -> EntityTokenRemapKind.AssemblyRef
+ // Existing baseline tables that can legitimately appear in IL but do not participate
+ // in delta remapping. Keep these explicit so new table tags fail closed by default.
+ | 0x00000000
+ | 0x11000000
+ | 0x1A000000 -> EntityTokenRemapKind.Passthrough
+ | tableTag ->
+ raise (
+ HotReloadUnsupportedEditException(
+ sprintf
+ "Unsupported metadata token table 0x%02X in method-body remap (token=0x%08X). Please rebuild."
+ (tableTag >>> 24)
+ token
+ )
+ )
+
+/// Keeps synthesized baseline aliases that are either unpaired or already paired with the
+/// current fresh type. This makes the fresh-to-baseline TypeDef relation injective while still
+/// allowing a repeated lookup of the same pairing during the emitter's recursive walks.
+let internal filterAvailableBaselineTypeMatches
+ (newTypeNameByBaseline: Dictionary)
+ (newFullName: string)
+ (matches: (string * 'Token)[])
+ =
+ matches
+ |> Array.filter (fun (matchedName, _) ->
+ match newTypeNameByBaseline.TryGetValue matchedName with
+ | true, existingNewName -> String.Equals(existingNewName, newFullName, StringComparison.Ordinal)
+ | false, _ -> true)
+
+/// Enables baseline-alias recovery only when a complete recorded name snapshot accompanies a
+/// whole-module in-process emit. Other emit paths keep their established fail-closed mapping.
+let internal shouldFilterSynthesizedBaselineAliases usesRecordedSnapshot hasWholeModuleArtifacts =
+ usesRecordedSnapshot && hasWholeModuleArtifacts
+
+/// Helper that produces an empty delta payload.
+let private emptyDelta: IlxDelta =
+ {
+ Metadata = Array.empty
+ IL = Array.empty
+ Pdb = None
+ EncLog = Array.empty
+ EncMap = Array.empty
+ UpdatedTypeTokens = []
+ UpdatedMethodTokens = []
+ RequiredCapabilities = []
+ AddedOrChangedMethods = []
+ MethodBodies = []
+ StandaloneSignatures = []
+ GenerationId = Guid.Empty
+ BaseGenerationId = Guid.Empty
+ UserStringUpdates = []
+ MethodDefinitionRows = []
+ UpdatedBaseline = None
+ SequencePointUpdates = []
+ ChainedSequencePoints = None
+ ActiveStatementUpdates = []
+ }
+
+let private defaultWriterOptions (ilg: ILGlobals) (checksumAlgorithm: HashAlgorithm) : ILWriter.options =
+ // ILBinaryWriter insists on having an output path even when we emit to memory. Generate a
+ // unique, throwaway file name per invocation so parallel sessions never collide, and so we
+ // leave a breadcrumb for debugging when traces mention the synthetic assembly.
+ let scratchDll =
+ let fileName =
+ sprintf "fsharp-hotreload-%s.dll" (System.Guid.NewGuid().ToString("N"))
+
+ Path.Combine(Path.GetTempPath(), fileName)
+
+ let scratchPdb = Path.ChangeExtension(scratchDll, ".pdb")
+
+ {
+ ilg = ilg
+ outfile = scratchDll
+ pdbfile = Some scratchPdb
+ portablePDB = true
+ embeddedPDB = false
+ embedAllSource = false
+ embedSourceList = []
+ allGivenSources = []
+ sourceLink = ""
+ checksumAlgorithm = checksumAlgorithm
+ signer = None
+ emitTailcalls = false
+ deterministic = true
+ dumpDebugInfo = false
+ referenceAssemblyOnly = false
+ referenceAssemblyAttribOpt = None
+ referenceAssemblySignatureHash = None
+ pathMap = PathMap.empty
+ moduleCustomDebugInfoRows = []
+ methodCustomDebugInfoRows = Map.empty
+ }
+
+let private opCodeLookup: Lazy> =
+ lazy
+ (let dict = Dictionary()
+
+ for field in typeof.GetFields(BindingFlags.Public ||| BindingFlags.Static) do
+ let op = field.GetValue(null) :?> OpCode
+ let value = int (uint16 op.Value)
+
+ if not (dict.ContainsKey(value)) then
+ dict[value] <- op
+
+ dict)
+
+let private traceFlag name = lazy (isEnvVarTruthy name)
+
+/// Trace flags for hot reload debugging - controlled via environment variables
+let private traceUserStringUpdates = traceFlag "FSHARP_HOTRELOAD_TRACE_STRINGS"
+
+let private traceSynthesizedMappings =
+ traceFlag "FSHARP_HOTRELOAD_TRACE_SYNTHESIZED"
+
+let private traceMethodUpdates = traceFlag "FSHARP_HOTRELOAD_TRACE_METHODS"
+let private traceMetadata = traceFlag "FSHARP_HOTRELOAD_TRACE_METADATA"
+let private traceHeapOffsets = traceFlag "FSHARP_HOTRELOAD_TRACE_HEAP_OFFSETS"
+
+type private PositionalTypeInfo =
+ {
+ FullName: string
+ EnclosingFullName: string
+ NormalizedBasicName: string
+ Ordinal: int list
+ Shape: SynthesizedTypeShape
+ }
+
+let internal tryFindSynthesizedTypeShapeMismatch (baseline: SynthesizedTypeShape) (fresh: SynthesizedTypeShape) =
+ if baseline.GenericArity <> fresh.GenericArity then
+ Some($"generic arity changed baseline={baseline.GenericArity} fresh={fresh.GenericArity}")
+ elif baseline.BaseType <> fresh.BaseType then
+ Some($"base type changed baseline={baseline.BaseType} fresh={fresh.BaseType}")
+ elif baseline.InterfaceTypes <> fresh.InterfaceTypes then
+ Some($"interface set changed baseline={baseline.InterfaceTypes} fresh={fresh.InterfaceTypes}")
+ elif baseline.FieldTypeNames <> fresh.FieldTypeNames then
+ Some($"field type multiset changed baseline={baseline.FieldTypeNames} fresh={fresh.FieldTypeNames}")
+ elif baseline.MethodNameAndArities <> fresh.MethodNameAndArities then
+ Some($"method set changed baseline={baseline.MethodNameAndArities} fresh={fresh.MethodNameAndArities}")
+ else
+ None
+
+/// Deduplicates method keys while preserving order
+let private dedupeMethodKeys (keys: MethodDefinitionKey list) =
+ let seen = HashSet(HashIdentity.Structural)
+
+ keys
+ |> List.fold (fun acc key -> if seen.Add key then key :: acc else acc) []
+ |> List.rev
+
+let private rewriteMethodBody (remapUserString: int -> int) (remapEntityToken: int -> int) (body: MethodBodyBlock) =
+ let ilBytes = body.GetILBytes().ToArray()
+ let rewritten = Array.copy ilBytes
+ let referencedMethodSpecs = HashSet()
+ let mutable offset = 0
+ let length = ilBytes.Length
+
+ let advance count = offset <- offset + count
+
+ while offset < length do
+ let opcodeValue, size =
+ let first = int ilBytes.[offset]
+
+ if first = 0xFE then
+ let second = int ilBytes.[offset + 1]
+ ((0xFE00 ||| second), 2)
+ else
+ (first, 1)
+
+ advance size
+
+ let operandType =
+ match opCodeLookup.Value.TryGetValue opcodeValue with
+ | true, op -> op.OperandType
+ | _ -> OperandType.InlineNone
+
+ let operandStart = offset
+
+ let inline readInt32 () =
+ let value = BitConverter.ToInt32(ilBytes, operandStart)
+ advance 4
+ value
+
+ let inline readInt16 () =
+ let value = BitConverter.ToInt16(ilBytes, operandStart)
+ advance 2
+ value
+
+ let inline readSByte () =
+ let value = sbyte ilBytes.[operandStart]
+ advance 1
+ value
+
+ let inline readByte () =
+ let value = ilBytes.[operandStart]
+ advance 1
+ value
+
+ match operandType with
+ | OperandType.InlineNone -> ()
+ | OperandType.ShortInlineI -> readSByte () |> ignore
+ | OperandType.InlineI -> readInt32 () |> ignore
+ | OperandType.InlineI8 -> advance 8
+ | OperandType.ShortInlineR -> advance 4
+ | OperandType.InlineR -> advance 8
+ | OperandType.InlineBrTarget -> readInt32 () |> ignore
+ | OperandType.ShortInlineBrTarget -> readSByte () |> ignore
+ | OperandType.ShortInlineVar -> readByte () |> ignore
+ | OperandType.InlineVar -> readInt16 () |> ignore
+ | OperandType.InlineString ->
+ let original = readInt32 ()
+ let updated = remapUserString original
+ let tokenBytes = BitConverter.GetBytes(updated: int)
+ Buffer.BlockCopy(tokenBytes, 0, rewritten, operandStart, 4)
+ | OperandType.InlineField
+ | OperandType.InlineMethod
+ | OperandType.InlineTok
+ | OperandType.InlineType ->
+ let original = readInt32 ()
+ let updated = remapEntityToken original
+
+ if original <> updated then
+ let tokenBytes = BitConverter.GetBytes(updated: int)
+ Buffer.BlockCopy(tokenBytes, 0, rewritten, operandStart, 4)
+
+ if (updated &&& 0xFF000000) = 0x2B000000 then
+ referencedMethodSpecs.Add(updated) |> ignore
+ | OperandType.InlineSig ->
+ // A calli operand is a StandAloneSig token. Passing the fresh-compile row id
+ // through would bind the instruction to an unrelated baseline signature. Until
+ // call-site signatures participate in delta row remapping, reject the edit.
+ raise (
+ HotReloadUnsupportedEditException(
+ "Updated method contains a calli signature that cannot yet be remapped safely; please rebuild."
+ )
+ )
+ | OperandType.InlineSwitch ->
+ let count = readInt32 ()
+ advance (count * 4)
+ | OperandType.InlinePhi ->
+ let count = int (readByte ())
+ advance (count * 2)
+ | _ -> ()
+
+ rewritten, (referencedMethodSpecs |> Seq.toList)
+
+/// Remaps a TypeDefOrRefOrSpec coded index (ECMA-335 II.23.2.8) using the entity-token remapper.
+/// TypeSpec coded indexes route through the content-validated TypeSpec remap, which reuses a
+/// matching baseline row or appends a new TypeSpec row to the delta.
+let private remapTypeDefOrRefCodedIndexWith (remapEntityToken: int -> int) (coded: int) : int =
+ let rowId = coded >>> 2
+
+ if rowId = 0 then
+ coded
+ else
+ match coded &&& 0x3 with
+ | 0 -> (((remapEntityToken (0x02000000 ||| rowId)) &&& 0x00FFFFFF) <<< 2)
+ | 1 -> (((remapEntityToken (0x01000000 ||| rowId)) &&& 0x00FFFFFF) <<< 2) ||| 1
+ | 2 -> (((remapEntityToken (0x1B000000 ||| rowId)) &&& 0x00FFFFFF) <<< 2) ||| 2
+ | _ -> coded
+
+/// Rewrites the TypeDefOrRefOrSpec coded indexes embedded in an ECMA-335 signature blob so the blob
+/// can be stored in a delta against the baseline metadata tables.
+///
+/// Signature blobs captured from the in-memory recompile embed TypeDef/TypeRef row ids of that
+/// compile. When the baseline tables have a different shape (for example SDK-built baselines carry
+/// import-scope TypeRefs that the in-memory rewrite does not regenerate), copying the blob raw makes
+/// its coded indexes resolve to arbitrary baseline rows - observed at runtime as
+/// "The generic type 'IntrinsicOperators' was used with the wrong number of generic arguments".
+/// Walks the signature grammar (II.23.2) and remaps every embedded coded index; fails closed on
+/// unknown element types.
+let private remapSignatureBlobCore (isTypeSpecBlob: bool) (remapTypeDefOrRefCodedIndex: int -> int) (signature: byte[]) : byte[] =
+ if isNull (box signature) || signature.Length = 0 then
+ signature
+ else
+ let builder = BlobBuilder()
+ let mutable pos = 0
+
+ let fail (message: string) : 'T =
+ raise (
+ HotReloadUnsupportedEditException(
+ sprintf "Unsupported signature blob during delta remap at offset %d: %s. Please rebuild." pos message
+ )
+ )
+
+ let peek () =
+ if pos >= signature.Length then
+ fail "unexpected end of signature"
+ else
+ int signature.[pos]
+
+ let readByteValue () =
+ let value = peek ()
+ pos <- pos + 1
+ value
+
+ let copyByte () =
+ builder.WriteByte(byte (readByteValue ()))
+
+ let compressedLength (first: int) =
+ if first &&& 0x80 = 0 then
+ 1
+ elif first &&& 0xC0 = 0x80 then
+ 2
+ elif first &&& 0xE0 = 0xC0 then
+ 4
+ else
+ fail (sprintf "invalid compressed integer lead byte 0x%02X" first)
+
+ let readCompressedUInt () =
+ let first = readByteValue ()
+
+ match compressedLength first with
+ | 1 -> first
+ | 2 -> ((first &&& 0x3F) <<< 8) ||| readByteValue ()
+ | _ ->
+ let b2 = readByteValue ()
+ let b3 = readByteValue ()
+ let b4 = readByteValue ()
+ ((first &&& 0x1F) <<< 24) ||| (b2 <<< 16) ||| (b3 <<< 8) ||| b4
+
+ // Copies a compressed (possibly signed) integer without re-encoding.
+ let copyCompressed () =
+ let length = compressedLength (peek ())
+
+ for _ in 1..length do
+ copyByte ()
+
+ let remapCodedIndex () =
+ let coded = readCompressedUInt ()
+ builder.WriteCompressedInteger(remapTypeDefOrRefCodedIndex coded)
+
+ // CMOD_REQD/CMOD_OPT carry a TypeDefOrRefOrSpec coded index; PINNED is a bare prefix.
+ let rec copyCustomModsAndConstraints () =
+ if pos < signature.Length then
+ match peek () with
+ | 0x1F
+ | 0x20 ->
+ copyByte ()
+ remapCodedIndex ()
+ copyCustomModsAndConstraints ()
+ | 0x45 ->
+ copyByte ()
+ copyCustomModsAndConstraints ()
+ | _ -> ()
+
+ let rec copyType () =
+ copyCustomModsAndConstraints ()
+
+ match readByteValue () with
+ // VOID and primitive element types, TYPEDBYREF, I, U, OBJECT
+ | (0x01 | 0x02 | 0x03 | 0x04 | 0x05 | 0x06 | 0x07 | 0x08 | 0x09 | 0x0A | 0x0B | 0x0C | 0x0D | 0x0E | 0x16 | 0x18 | 0x19 | 0x1C) as code ->
+ builder.WriteByte(byte code)
+ // PTR, BYREF, SZARRAY wrap a single type
+ | (0x0F | 0x10 | 0x1D) as code ->
+ builder.WriteByte(byte code)
+ copyType ()
+ // VALUETYPE, CLASS carry a TypeDefOrRefOrSpec coded index
+ | (0x11 | 0x12) as code ->
+ builder.WriteByte(byte code)
+ remapCodedIndex ()
+ // VAR, MVAR carry a generic parameter ordinal
+ | (0x13 | 0x1E) as code ->
+ builder.WriteByte(byte code)
+ copyCompressed ()
+ // ARRAY: type, rank, sizes, lower bounds
+ | 0x14 ->
+ builder.WriteByte 0x14uy
+ copyType ()
+ copyCompressed () // rank
+ let numSizes = readCompressedUInt ()
+ builder.WriteCompressedInteger numSizes
+
+ for _ in 1..numSizes do
+ copyCompressed ()
+
+ let numLoBounds = readCompressedUInt ()
+ builder.WriteCompressedInteger numLoBounds
+
+ for _ in 1..numLoBounds do
+ copyCompressed () // signed; copied verbatim
+
+ // GENERICINST: (CLASS | VALUETYPE), coded index, argument count, arguments
+ | 0x15 ->
+ builder.WriteByte 0x15uy
+
+ match readByteValue () with
+ | (0x11 | 0x12) as kind ->
+ builder.WriteByte(byte kind)
+ remapCodedIndex ()
+ let argCount = readCompressedUInt ()
+ builder.WriteCompressedInteger argCount
+
+ for _ in 1..argCount do
+ copyType ()
+ | kind -> fail (sprintf "unexpected GENERICINST kind 0x%02X" kind)
+ // FNPTR wraps a full method signature
+ | 0x1B ->
+ builder.WriteByte 0x1Buy
+ copyMethodSignature ()
+ | code -> fail (sprintf "unexpected signature element type 0x%02X" code)
+
+ and copyMethodSignature () =
+ let callingConvention = readByteValue ()
+ builder.WriteByte(byte callingConvention)
+
+ if callingConvention &&& 0x10 <> 0 then
+ copyCompressed () // generic parameter count
+
+ let paramCount = readCompressedUInt ()
+ builder.WriteCompressedInteger paramCount
+ copyType () // return type
+
+ let mutable remaining = paramCount
+
+ while remaining > 0 do
+ if peek () = 0x41 then
+ copyByte () // SENTINEL (vararg) does not consume a parameter slot
+ else
+ copyType ()
+ remaining <- remaining - 1
+
+ (if isTypeSpecBlob then
+ // TypeSpec blob (ECMA-335 II.23.2.14): a bare Type with no calling-convention
+ // header.
+ copyType ()
+ else
+
+ match peek () with
+ // FieldSig
+ | 0x06 ->
+ copyByte ()
+ copyType ()
+ // LocalVarSig
+ | 0x07 ->
+ copyByte ()
+ let count = readCompressedUInt ()
+ builder.WriteCompressedInteger count
+
+ for _ in 1..count do
+ copyType ()
+ // MethodSpec instantiation (GENERICINST; not a valid method calling convention)
+ | 0x0A ->
+ copyByte ()
+ let argCount = readCompressedUInt ()
+ builder.WriteCompressedInteger argCount
+
+ for _ in 1..argCount do
+ copyType ()
+ // MethodDefSig/MethodRefSig/PropertySig
+ | _ -> copyMethodSignature ())
+
+ if pos <> signature.Length then
+ fail "trailing bytes in signature"
+
+ builder.ToArray()
+
+/// Remaps the TypeDefOrRef coded indexes embedded in a method/field/local/property
+/// signature blob (ECMA-335 II.23.2) from fresh-compile rows to baseline rows.
+let internal remapSignatureBlobWith (remapTypeDefOrRefCodedIndex: int -> int) (signature: byte[]) : byte[] =
+ remapSignatureBlobCore false remapTypeDefOrRefCodedIndex signature
+
+/// Remaps the TypeDefOrRef coded indexes embedded in a TypeSpec signature blob
+/// (ECMA-335 II.23.2.14: a bare Type, no calling-convention header).
+/// HasCustomAttribute tag -> owning table number (ECMA-335 II.24.2.6 ordering), used to
+/// project emitted CA row parents back to metadata tokens for baseline pairing/chaining.
+let private hcaTagToTable =
+ [|
+ 0x06
+ 0x04
+ 0x01
+ 0x02
+ 0x08
+ 0x09
+ 0x0A
+ 0x00
+ 0x0E
+ 0x17
+ 0x14
+ 0x11
+ 0x1A
+ 0x1B
+ 0x20
+ 0x23
+ 0x26
+ 0x27
+ 0x28
+ 0x2A
+ 0x2C
+ 0x2B
+ |]
+
+let internal hasCustomAttributeParentToken (parent: HasCustomAttribute) =
+ (hcaTagToTable.[parent.CodedTag] <<< 24) ||| parent.RowId
+
+let internal customAttributeConstructorToken (ctor: CustomAttributeType) =
+ match ctor with
+ | CAT_MethodDef handle -> 0x06000000 ||| handle.RowId
+ | CAT_MemberRef handle -> 0x0A000000 ||| handle.RowId
+
+let internal remapTypeSpecBlobWith (remapTypeDefOrRefCodedIndex: int -> int) (blob: byte[]) : byte[] =
+ remapSignatureBlobCore true remapTypeDefOrRefCodedIndex blob
+
+let private buildUpdatedTypeTokens
+ (tryGetBaselineTypeName: string -> string)
+ (baselineTypeTokens: Map)
+ (updatedTypes: string list)
+ (symbolChangeTypeNames: string list)
+ (resolvedMethods: (ILTypeDef list * ILTypeDef * ILMethodDef * MethodDefinitionKey) list)
+ =
+ let methodTypeNames =
+ resolvedMethods
+ |> List.map (fun (enclosing, typeDef, _, _) ->
+ let typeRef = mkRefForNestedILTypeDef ILScopeRef.Local (enclosing, typeDef)
+ tryGetBaselineTypeName typeRef.FullName)
+
+ (updatedTypes @ symbolChangeTypeNames @ methodTypeNames)
+ |> List.map tryGetBaselineTypeName
+ |> List.distinct
+ |> List.choose (fun typeName -> baselineTypeTokens |> Map.tryFind typeName)
+
+/// Converts a delta MemberRef row's parent to the metadata token stored in the chained
+/// baseline's MemberReferenceRows (used for content-validated passthrough next generation).
+let private memberRefParentToken (parent: MemberRefParent) =
+ match parent with
+ | MRP_TypeDef handle -> 0x02000000 ||| handle.RowId
+ | MRP_TypeRef handle -> 0x01000000 ||| handle.RowId
+ | MRP_ModuleRef handle -> 0x1A000000 ||| handle.RowId
+ | MRP_MethodDef handle -> 0x06000000 ||| handle.RowId
+ | MRP_TypeSpec handle -> 0x1B000000 ||| handle.RowId
+
+let private buildUpdatedBaseline
+ (updatedBaselineCore: FSharpEmitBaseline)
+ (parameterDefinitionRowsSnapshot: ParameterDefinitionRowInfo list)
+ (memberReferenceRowList: MemberReferenceRowInfo list)
+ (typeSpecificationRowList: TypeSpecificationRowInfo list)
+ (customAttributeRowList: CustomAttributeRowInfo list)
+ (propertyMapRowsSnapshot: PropertyMapRowInfo list)
+ (eventMapRowsSnapshot: EventMapRowInfo list)
+ (methodSemanticsRowsSnapshot: MethodSemanticsMetadataUpdate list)
+ (methodTokenToKey: Dictionary)
+ (addedMethodDeltaTokens: Dictionary)
+ (addedFieldDeltaTokens: Dictionary)
+ (addedPropertyDeltaTokens: Dictionary)
+ (addedEventDeltaTokens: Dictionary)
+ (addedTypeDeltaTokens: Dictionary)
+ (addedTypeShapes: Dictionary)
+ (addedTypeReferenceTokens: Dictionary)
+ (addedAssemblyReferenceTokens: Dictionary)
+ =
+ let addPropertyMapEntry (entries: Map) (row: PropertyMapRowInfo) =
+ if row.IsAdded then
+ entries |> Map.add row.DeclaringType row.RowId
+ else
+ entries
+
+ let addEventMapEntry (entries: Map) (row: EventMapRowInfo) =
+ if row.IsAdded then
+ entries |> Map.add row.DeclaringType row.RowId
+ else
+ entries
+
+ let extendMethodSemanticsMap (entries: Map) (row: MethodSemanticsMetadataUpdate) =
+ if row.IsAdded then
+ match methodTokenToKey.TryGetValue row.MethodToken with
+ | true, methodKey ->
+ let newEntry =
+ {
+ MethodSemanticsEntry.RowId = row.RowId
+ Attributes = row.Attributes
+ Association = row.AssociationInfo
+ }
+
+ let updatedList =
+ match entries |> Map.tryFind methodKey with
+ | Some existing -> newEntry :: existing |> List.distinctBy (fun entry -> entry.RowId)
+ | None -> [ newEntry ]
+
+ entries |> Map.add methodKey updatedList
+ | _ -> entries
+ else
+ entries
+
+ let updatedPropertyMapEntries =
+ propertyMapRowsSnapshot
+ |> List.fold addPropertyMapEntry updatedBaselineCore.PropertyMapEntries
+
+ let updatedEventMapEntries =
+ eventMapRowsSnapshot
+ |> List.fold addEventMapEntry updatedBaselineCore.EventMapEntries
+
+ let updatedMethodSemanticsEntries =
+ methodSemanticsRowsSnapshot
+ |> List.fold extendMethodSemanticsMap updatedBaselineCore.MethodSemanticsEntries
+
+ let updatedMethodTokenMap =
+ addedMethodDeltaTokens
+ |> Seq.fold (fun acc (KeyValue(key, token)) -> acc |> Map.add key token) updatedBaselineCore.MethodTokens
+
+ // Chain added field tokens into the next-generation baseline so a later generation can
+ // resolve (and not re-add) the field, mirroring AddedOrChangedMethods chaining.
+ let updatedFieldTokenMap =
+ addedFieldDeltaTokens
+ |> Seq.fold (fun acc (KeyValue(key, token)) -> acc |> Map.add key token) updatedBaselineCore.FieldTokens
+
+ let updatedPropertyTokenMap =
+ addedPropertyDeltaTokens
+ |> Seq.fold (fun acc (KeyValue(key, token)) -> acc |> Map.add key token) updatedBaselineCore.PropertyTokens
+
+ let updatedEventTokenMap =
+ addedEventDeltaTokens
+ |> Seq.fold (fun acc (KeyValue(key, token)) -> acc |> Map.add key token) updatedBaselineCore.EventTokens
+
+ // Chain added TypeDef tokens (new closure classes) so later generations resolve the
+ // type in place (e.g. a gen-3 body edit of a lambda added in gen 2) instead of
+ // re-adding it.
+ let updatedTypeTokenMap =
+ addedTypeDeltaTokens
+ |> Seq.fold (fun acc (KeyValue(fullName, token)) -> acc |> Map.add fullName token) updatedBaselineCore.TypeTokens
+
+ let updatedSynthesizedTypeShapes =
+ addedTypeShapes
+ |> Seq.fold (fun acc (KeyValue(fullName, shape)) -> acc |> Map.add fullName shape) updatedBaselineCore.SynthesizedTypeShapes
+
+ // Param rows added by a delta are part of the committed metadata baseline. Retain their
+ // stable keys and row ids so a later edit of the same method reuses them instead of
+ // appending duplicate Param rows with broken MethodDef.ParamList ranges.
+ let updatedParameterHandles =
+ parameterDefinitionRowsSnapshot
+ |> List.fold
+ (fun acc row ->
+ acc
+ |> Map.add
+ row.Key
+ {
+ ParameterDefinitionMetadataHandles.NameOffset = row.NameOffset
+ Name = row.Name
+ RowId = Some row.RowId
+ })
+ updatedBaselineCore.MetadataHandles.ParameterHandles
+
+ let updatedTypeReferenceTokens =
+ addedTypeReferenceTokens
+ |> Seq.fold (fun acc (KeyValue(key, token)) -> acc |> Map.add key token) updatedBaselineCore.TypeReferenceTokens
+
+ let updatedAssemblyReferenceTokens =
+ addedAssemblyReferenceTokens
+ |> Seq.fold (fun acc (KeyValue(key, token)) -> acc |> Map.add key token) updatedBaselineCore.AssemblyReferenceTokens
+
+ // Chain delta-appended MemberRef rows (already in baseline coordinates) so the next
+ // generation's content-validated passthrough can recognize and reuse them.
+ let updatedMemberReferenceRows =
+ memberReferenceRowList
+ |> List.fold
+ (fun acc (row: MemberReferenceRowInfo) ->
+ acc
+ |> Map.add
+ row.RowId
+ {
+ HotReloadBaseline.BaselineMemberRefRow.Name = row.Name
+ ParentToken = memberRefParentToken row.Parent
+ Signature = row.Signature
+ })
+ updatedBaselineCore.MemberReferenceRows
+
+ // Chain delta-appended TypeSpec rows (signature blobs already in baseline
+ // coordinates) so the next generation's content search recognizes and reuses
+ // them instead of appending duplicates.
+ let updatedTypeSpecSignatures =
+ typeSpecificationRowList
+ |> List.fold
+ (fun acc (row: TypeSpecificationRowInfo) -> acc |> Map.add row.RowId row.Signature)
+ updatedBaselineCore.TypeSpecSignatures
+
+ // Chain emitted CustomAttribute rows (updates REPLACE the baseline entry, adds extend
+ // the map) so the next generation's attribute pairing sees the current row contents.
+ let updatedCustomAttributeRows =
+ customAttributeRowList
+ |> List.fold
+ (fun acc (row: CustomAttributeRowInfo) ->
+ acc
+ |> Map.add
+ row.RowId
+ {
+ HotReloadBaseline.BaselineCustomAttributeRow.ParentToken = hasCustomAttributeParentToken row.Parent
+ ConstructorToken = customAttributeConstructorToken row.Constructor
+ Value = row.Value
+ })
+ updatedBaselineCore.CustomAttributeRows
+
+ { updatedBaselineCore with
+ MetadataHandles =
+ { updatedBaselineCore.MetadataHandles with
+ ParameterHandles = updatedParameterHandles
+ }
+ TypeTokens = updatedTypeTokenMap
+ SynthesizedTypeShapes = updatedSynthesizedTypeShapes
+ MemberReferenceRows = updatedMemberReferenceRows
+ TypeSpecSignatures = updatedTypeSpecSignatures
+ CustomAttributeRows = updatedCustomAttributeRows
+ MethodTokens = updatedMethodTokenMap
+ FieldTokens = updatedFieldTokenMap
+ PropertyTokens = updatedPropertyTokenMap
+ EventTokens = updatedEventTokenMap
+ PropertyMapEntries = updatedPropertyMapEntries
+ EventMapEntries = updatedEventMapEntries
+ MethodSemanticsEntries = updatedMethodSemanticsEntries
+ TypeReferenceTokens = updatedTypeReferenceTokens
+ AssemblyReferenceTokens = updatedAssemblyReferenceTokens
+ }
+
+let private tryBuildMethodUpdateInput
+ (traceMethodUpdates: bool)
+ (metadataReader: MetadataReader)
+ (peReader: PEReader)
+ (baselineMethodTokens: Map)
+ (freshMethodTokenByBaseline: Dictionary)
+ (addedMethodTokens: Dictionary)
+ (addedMethodDeltaTokens: Dictionary)
+ (key: MethodDefinitionKey)
+ : struct (MethodDefinitionKey * int * MethodDefinitionHandle * MethodDefinition * MethodBodyBlock) option =
+
+ // readerToken addresses the FRESH compile's metadata reader; deltaToken is the
+ // baseline-coordinate row the update is emitted at.
+ let tryCreateInput (readerToken: int) (deltaToken: int) (isAddedMethod: bool) =
+ let methodHandle = MetadataTokens.MethodDefinitionHandle readerToken
+
+ if methodHandle.IsNil then
+ None
+ else
+ let methodDef = metadataReader.GetMethodDefinition methodHandle
+
+ if isAddedMethod && methodDef.RelativeVirtualAddress = 0 then
+ // Bodiless added method. Abstract slots (added interfaces, abstract
+ // members of added classes) and runtime-implemented members (a delegate's
+ // .ctor/Invoke, ImplFlags CodeTypeMask = Runtime) legitimately have no IL
+ // body: Roslyn emits their MethodDef rows with RVA 0 (C# 'new_interface'
+ // and 'new_delegate' reference templates) and so do we — the input carries
+ // a null MethodBodyBlock and the row's RVA column stays 0. Anything else
+ // bodiless (extern/pinvoke would also need ImplMap rows) keeps failing
+ // closed precisely.
+ let isAbstract = methodDef.Attributes.HasFlag MethodAttributes.Abstract
+
+ let isRuntimeImplemented =
+ methodDef.ImplAttributes &&& MethodImplAttributes.CodeTypeMask = MethodImplAttributes.Runtime
+
+ if isAbstract || isRuntimeImplemented then
+ if traceMethodUpdates then
+ printfn "[fsharp-hotreload][method-add] %s::%s token=0x%08X (bodiless, RVA 0)" key.DeclaringType key.Name deltaToken
+
+ Some(struct (key, deltaToken, methodHandle, methodDef, Unchecked.defaultof))
+ else
+ raise (
+ HotReloadUnsupportedEditException(
+ $"Added method '{key.DeclaringType}::{key.Name}' has no IL body (extern); hot reload deltas cannot express extern added methods yet. Please rebuild."
+ )
+ )
+ else
+
+ let body = peReader.GetMethodBody(methodDef.RelativeVirtualAddress)
+
+ if traceMethodUpdates then
+ if isAddedMethod then
+ printfn "[fsharp-hotreload][method-add] %s::%s token=0x%08X" key.DeclaringType key.Name deltaToken
+ else
+ printfn
+ "[fsharp-hotreload][method-update] %s::%s readerToken=0x%08X token=0x%08X"
+ key.DeclaringType
+ key.Name
+ readerToken
+ deltaToken
+
+ Some(struct (key, deltaToken, methodHandle, methodDef, body))
+
+ match baselineMethodTokens |> Map.tryFind key with
+ | Some methodToken ->
+ // The baseline token addresses the LOADED module's row space, which is not generally
+ // valid in the fresh compile's reader: methods added by an earlier delta chain sit past
+ // the original baseline tables, while the fresh compile lays them out at their natural
+ // source positions, displacing later fresh rows. Read through the fresh token (recorded
+ // by collectTypeMappings when it differs from the baseline token) and emit at the
+ // baseline token.
+ let readerToken =
+ match freshMethodTokenByBaseline.TryGetValue methodToken with
+ | true, freshToken -> freshToken
+ | _ -> methodToken
+
+ tryCreateInput readerToken methodToken false
+ | None ->
+ match addedMethodTokens.TryGetValue key, addedMethodDeltaTokens.TryGetValue key with
+ | (true, methodToken), (true, deltaToken) -> tryCreateInput methodToken deltaToken true
+ | _ -> None
+
+let private buildReferenceRows
+ (traceMetadata: bool)
+ (typeReferenceRows: ResizeArray)
+ (memberReferenceRows: ResizeArray)
+ (assemblyReferenceRows: ResizeArray)
+ (typeSpecificationRows: ResizeArray)
+ (methodSpecificationRowsSnapshot: MethodSpecificationRowInfo list)
+ (customAttributeRowList: CustomAttributeRowInfo list)
+ =
+ let typeReferenceRowList =
+ typeReferenceRows |> Seq.sortBy (fun row -> row.RowId) |> Seq.toList
+
+ let memberReferenceRowList =
+ memberReferenceRows |> Seq.sortBy (fun row -> row.RowId) |> Seq.toList
+
+ let assemblyReferenceRowList =
+ assemblyReferenceRows |> Seq.sortBy (fun row -> row.RowId) |> Seq.toList
+
+ let typeSpecificationRowList =
+ typeSpecificationRows |> Seq.sortBy (fun row -> row.RowId) |> Seq.toList
+
+ if traceMetadata then
+ printfn
+ "[fsharp-hotreload][metadata] row-counts typeRef=%d memberRef=%d methodSpec=%d typeSpec=%d assemblyRef=%d customAttr=%d"
+ typeReferenceRowList.Length
+ memberReferenceRowList.Length
+ methodSpecificationRowsSnapshot.Length
+ typeSpecificationRowList.Length
+ assemblyReferenceRowList.Length
+ customAttributeRowList.Length
+
+ for row in typeReferenceRowList do
+ printfn
+ "[fsharp-hotreload][metadata] typeref rowId=%d name=%s scope=%A row=%d"
+ row.RowId
+ row.Name
+ row.ResolutionScope
+ row.ResolutionScope.RowId
+
+ for row in memberReferenceRowList do
+ printfn
+ "[fsharp-hotreload][metadata] memberref rowId=%d name=%s parent=%A row=%d"
+ row.RowId
+ row.Name
+ row.Parent
+ row.Parent.RowId
+
+ for row in methodSpecificationRowsSnapshot do
+ printfn
+ "[fsharp-hotreload][metadata] methodspec rowId=%d methodTag=%d methodRow=%d"
+ row.RowId
+ row.Method.CodedTag
+ row.Method.RowId
+
+ for row in typeSpecificationRowList do
+ printfn "[fsharp-hotreload][metadata] typespec rowId=%d blobLength=%d" row.RowId row.Signature.Length
+
+ for row in assemblyReferenceRowList do
+ printfn "[fsharp-hotreload][metadata] assemblyref rowId=%d name=%s" row.RowId row.Name
+
+ typeReferenceRowList, memberReferenceRowList, assemblyReferenceRowList, typeSpecificationRowList
+
+let private emitMetadataDelta
+ (traceMetadata: bool)
+ (moduleName: string)
+ (baselineModuleNameOffset: StringOffset option)
+ (currentGeneration: int)
+ (encId: Guid)
+ (encBaseId: Guid)
+ (moduleMvid: Guid)
+ (typeDefinitionRowsSnapshot: TypeDefinitionRowInfo list)
+ (nestedClassRowsSnapshot: NestedClassRowInfo list)
+ (interfaceImplRowsSnapshot: InterfaceImplRowInfo list)
+ (methodImplRowsSnapshot: MethodImplRowInfo list)
+ (constantRowsSnapshot: ConstantRowInfo list)
+ (methodDefinitionRowsSnapshot: MethodDefinitionRowInfo list)
+ (parameterDefinitionRowsSnapshot: ParameterDefinitionRowInfo list)
+ (fieldDefinitionRowsSnapshot: FieldDefinitionRowInfo list)
+ (typeReferenceRowList: TypeReferenceRowInfo list)
+ (memberReferenceRowList: MemberReferenceRowInfo list)
+ (methodSpecificationRowsSnapshot: MethodSpecificationRowInfo list)
+ (typeSpecificationRowList: TypeSpecificationRowInfo list)
+ (genericParamRowsSnapshot: GenericParamRowInfo list)
+ (genericParamConstraintRowsSnapshot: GenericParamConstraintRowInfo list)
+ (assemblyReferenceRowList: AssemblyReferenceRowInfo list)
+ (propertyDefinitionRowsSnapshot: PropertyDefinitionRowInfo list)
+ (eventDefinitionRowsSnapshot: EventDefinitionRowInfo list)
+ (propertyMapRowsSnapshot: PropertyMapRowInfo list)
+ (eventMapRowsSnapshot: EventMapRowInfo list)
+ (methodSemanticsRowsSnapshot: MethodSemanticsMetadataUpdate list)
+ (standaloneSignatures: StandaloneSignatureUpdate list)
+ (customAttributeRowList: CustomAttributeRowInfo list)
+ (userStringEntries: (int * int * string) list)
+ (methodUpdates: MethodMetadataUpdate list)
+ (baselineHeapOffsets: MetadataHeapOffsets)
+ (baselineTableRowCounts: int[])
+ =
+ let writerStandaloneSignatures: FSharp.Compiler.AbstractIL.IlxDeltaStreams.StandaloneSignatureUpdate list =
+ standaloneSignatures
+ |> List.map (fun signature ->
+ {
+ RowId = signature.RowId
+ Blob = signature.Blob
+ })
+
+ let metadataDelta =
+ MetadataWriter.emitWithTypeDefinitions
+ moduleName
+ baselineModuleNameOffset
+ currentGeneration
+ encId
+ encBaseId
+ moduleMvid
+ typeDefinitionRowsSnapshot
+ nestedClassRowsSnapshot
+ interfaceImplRowsSnapshot
+ methodImplRowsSnapshot
+ constantRowsSnapshot
+ methodDefinitionRowsSnapshot
+ parameterDefinitionRowsSnapshot
+ fieldDefinitionRowsSnapshot
+ typeReferenceRowList
+ memberReferenceRowList
+ methodSpecificationRowsSnapshot
+ typeSpecificationRowList
+ genericParamRowsSnapshot
+ genericParamConstraintRowsSnapshot
+ assemblyReferenceRowList
+ propertyDefinitionRowsSnapshot
+ eventDefinitionRowsSnapshot
+ propertyMapRowsSnapshot
+ eventMapRowsSnapshot
+ methodSemanticsRowsSnapshot
+ writerStandaloneSignatures
+ customAttributeRowList
+ userStringEntries
+ methodUpdates
+ baselineHeapOffsets
+ baselineTableRowCounts
+
+ if traceMetadata then
+ let count idx = metadataDelta.TableRowCounts.[idx]
+
+ printfn
+ "[fsharp-hotreload][metadata] table-counts module=%d method=%d param=%d typeRef=%d memberRef=%d methodSpec=%d typeSpec=%d assemblyRef=%d customAttr=%d standAloneSig=%d"
+ (count TableNames.Module.Index)
+ (count TableNames.Method.Index)
+ (count TableNames.Param.Index)
+ (count TableNames.TypeRef.Index)
+ (count TableNames.MemberRef.Index)
+ (count TableNames.MethodSpec.Index)
+ (count TableNames.TypeSpec.Index)
+ (count TableNames.AssemblyRef.Index)
+ (count TableNames.CustomAttribute.Index)
+ (count TableNames.StandAloneSig.Index)
+
+ metadataDelta
+
+let private buildMethodUpdatesWithMetadata
+ (orderedMethodInputs: struct (MethodDefinitionKey * int * MethodDefinitionHandle * MethodDefinition * MethodBodyBlock) list)
+ (metadataReader: MetadataReader)
+ (builder: IlDeltaStreamBuilder)
+ (remapUserString: int -> int)
+ (remapEntityToken: int -> int)
+ =
+ let methodUpdatesWithDefs =
+ orderedMethodInputs
+ |> List.map (fun struct (key, methodToken, methodHandle, methodDef, body) ->
+ let bodyUpdate, referencedMethodSpecs =
+ if isNull (box body) then
+ // Bodiless added method (abstract slot of an added interface/class or
+ // runtime-implemented delegate member): no IL chunk enters the delta;
+ // the MethodDef row's RVA column stays 0 (Roslyn template parity —
+ // AddMethodRow writes CodeOffset only when CodeLength > 0 and added
+ // rows have no baseline RVA to fall back to).
+ {
+ MethodToken = methodToken
+ LocalSignatureToken = 0
+ CodeOffset = 0
+ CodeLength = 0
+ },
+ []
+ else
+ let ilBytes, referencedMethodSpecs =
+ rewriteMethodBody remapUserString remapEntityToken body
+
+ let localSigToken =
+ if body.LocalSignature.IsNil then
+ 0
+ else
+ let standalone = metadataReader.GetStandaloneSignature body.LocalSignature
+ // Local signatures are emitted into the delta blob heap; remap the embedded
+ // TypeDefOrRef coded indexes from the fresh compile to baseline rows.
+ let signatureBytes =
+ metadataReader.GetBlobBytes standalone.Signature
+ |> remapSignatureBlobWith (remapTypeDefOrRefCodedIndexWith remapEntityToken)
+
+ builder.AddStandaloneSignature(signatureBytes)
+
+ builder.AddMethodBody(
+ methodToken,
+ localSigToken,
+ ilBytes,
+ body.MaxStack,
+ body.LocalVariablesInitialized,
+ convertExceptionRegions body.ExceptionRegions,
+ remapEntityToken
+ ),
+ referencedMethodSpecs
+
+ // Convert SRM MethodDefinitionHandle to F# MethodDefHandle
+ let methodHandleEntity: EntityHandle =
+ MethodDefinitionHandle.op_Implicit methodHandle
+
+ let methodRowId = MetadataTokens.GetRowNumber(methodHandleEntity)
+
+ ({
+ MethodKey = key
+ MethodToken = methodToken
+ MethodHandle = MethodDefHandle methodRowId
+ Body = bodyUpdate
+ },
+ methodDef,
+ referencedMethodSpecs))
+
+ let methodMetadataLookup =
+ let dict: Dictionary =
+ Dictionary(HashIdentity.Structural)
+
+ for update, methodDef, _ in methodUpdatesWithDefs do
+ let name = metadataReader.GetString methodDef.Name
+ let signature = metadataReader.GetBlobBytes methodDef.Signature
+
+ let nameOffset =
+ if methodDef.Name.IsNil then
+ None
+ else
+ Some(StringOffset(MetadataTokens.GetHeapOffset methodDef.Name))
+
+ let signatureOffset =
+ if methodDef.Signature.IsNil then
+ None
+ else
+ Some(BlobOffset(MetadataTokens.GetHeapOffset methodDef.Signature))
+
+ dict[update.MethodKey] <- (methodDef.Attributes, methodDef.ImplAttributes, name, signature, nameOffset, signatureOffset)
+
+ dict
+
+ methodUpdatesWithDefs, methodMetadataLookup
+
+let private buildParameterDefinitionRowsSnapshot
+ (parameterDefinitionRowsRaw: struct (int * ParameterDefinitionKey * bool) list)
+ (parameterHandleLookup: Dictionary)
+ (baselineParameterHandles: Map)
+ (syntheticParameterInfo: Dictionary)
+ (firstParamRowByMethod: Dictionary)
+ (returnParameterKeys: HashSet)
+ (metadataReader: MetadataReader)
+ : ParameterDefinitionRowInfo list =
+ let rows =
+ parameterDefinitionRowsRaw
+ |> List.choose (fun struct (rowId, key, isAdded) ->
+ if rowId = 0 then
+ None
+ else
+ let attrs, sequence, nameOpt, resolvedOffsetOpt =
+ match parameterHandleLookup.TryGetValue key with
+ | true, handle when not handle.IsNil ->
+ let parameter = metadataReader.GetParameter handle
+
+ let name =
+ if parameter.Name.IsNil then
+ None
+ else
+ metadataReader.GetString parameter.Name |> Some
+
+ let baselineInfo = baselineParameterHandles |> Map.tryFind key
+
+ let resolvedOffset =
+ match baselineInfo |> Option.bind (fun info -> info.NameOffset) with
+ | Some offset ->
+ // Reuse the baseline name offset only when the fresh name
+ // matches the baseline name. A differing name is a
+ // parameter RENAME (classification gates it on the
+ // UpdateParameters capability): the re-emitted Param row
+ // writes the NEW name into the delta string heap — the C#
+ // 'param_rename' template shape.
+ match baselineInfo |> Option.bind (fun info -> info.Name) with
+ | Some baselineName when name <> Some baselineName -> None
+ | _ -> Some offset
+ | None ->
+ // Added parameter rows must write their name into the delta
+ // string heap; fresh-compile heap offsets are not valid
+ // against the baseline+delta heap layout.
+ if isAdded || parameter.Name.IsNil then
+ None
+ else
+ Some(StringOffset(MetadataTokens.GetHeapOffset parameter.Name))
+
+ parameter.Attributes, int parameter.SequenceNumber, name, resolvedOffset
+ | _ ->
+ let attrs =
+ match syntheticParameterInfo.TryGetValue key with
+ | true, value -> value
+ | _ -> ParameterAttributes.None
+
+ attrs, key.SequenceNumber, None, None
+
+ match firstParamRowByMethod.TryGetValue key.Method with
+ | true, existing when existing <= rowId -> ()
+ | _ -> firstParamRowByMethod[key.Method] <- rowId
+
+ // Treat synthesized return parameter rows as added so EncLog/EncMap
+ // reflect the new Param table entry, mirroring Roslyn ENC behavior.
+ let effectiveIsAdded = if returnParameterKeys.Contains key then true else isAdded
+
+ let nameChanged =
+ baselineParameterHandles
+ |> Map.tryFind key
+ |> Option.exists (fun baseline -> baseline.Name <> nameOpt)
+
+ // Existing unchanged parameters only seed MethodDef.ParamList resolution;
+ // they are not rows in the physical delta. Parameter renames are the sole
+ // supported existing-Param update and must re-emit their baseline row.
+ if not effectiveIsAdded && not nameChanged then
+ None
+ else
+ Some
+ {
+ ParameterDefinitionRowInfo.Key = key
+ RowId = rowId
+ IsAdded = effectiveIsAdded
+ Attributes = attrs
+ SequenceNumber = sequence
+ Name = nameOpt
+ NameOffset = resolvedOffsetOpt
+ })
+
+ if traceMethodUpdates.Value then
+ printfn "[fsharp-hotreload][param-rows] count=%d" rows.Length
+
+ rows
+
+let private buildMethodDefinitionRowsSnapshot
+ (methodDefinitionRowsRaw: struct (int * MethodDefinitionKey * bool) list)
+ (methodUpdatesWithDefs: (MethodMetadataUpdate * MethodDefinition * int list) list)
+ (methodMetadataLookup: Dictionary)
+ (baselineMethodHandles: Map)
+ (firstParamRowByMethod: Dictionary)
+ (baselineMethodTokens: Map)
+ (methodDefinitionIndex: DefinitionIndex)
+ (remapAddedSignature: byte[] -> byte[])
+ (tryGetTypeDefRowId: string -> int option)
+ : MethodDefinitionRowInfo list =
+
+ let tryBuildMethodRow rowId key isAdded =
+ match methodMetadataLookup.TryGetValue key with
+ | true, (attrs, implAttrs, name, signature, _, _) ->
+ let baselineHandles = baselineMethodHandles |> Map.tryFind key
+ // Methods without baseline heap entries - added this generation OR added by an
+ // EARLIER delta and re-emitted now (the handle cache only covers the on-disk
+ // baseline) - must write their name/signature into THIS delta's heaps (offset
+ // None). Offsets captured from the fresh compile's heaps are meaningless
+ // against the baseline+delta heap layout and produce garbage references.
+ let resolvedNameOffset =
+ baselineHandles |> Option.bind (fun info -> info.NameOffset)
+
+ let resolvedSignatureOffset =
+ baselineHandles |> Option.bind (fun info -> info.SignatureOffset)
+ // Signature blobs entering the delta blob heap embed TypeDefOrRef coded
+ // indexes of the fresh compile; remap them to baseline rows.
+ let resolvedSignature =
+ if resolvedSignatureOffset.IsNone then
+ remapAddedSignature signature
+ else
+ signature
+
+ let resolvedAttributes =
+ match baselineHandles |> Option.bind (fun info -> info.Attributes) with
+ | Some value -> value
+ | None -> attrs
+
+ let resolvedImplAttributes =
+ match baselineHandles |> Option.bind (fun info -> info.ImplAttributes) with
+ | Some value -> value
+ | None -> implAttrs
+
+ let resolvedCodeRva = baselineHandles |> Option.bind (fun info -> info.Rva)
+
+ let baselineFirstParam =
+ baselineHandles |> Option.bind (fun info -> info.FirstParameterRowId)
+
+ let firstParam =
+ match firstParamRowByMethod.TryGetValue key with
+ | true, value when value > 0 -> Some value
+ | _ ->
+ match baselineFirstParam with
+ | Some _ as baselineRow -> baselineRow
+ | None -> None
+
+ // Parent TypeDef row id is required for ADDED methods: the CLR EnC applier links
+ // the new method into the parent's member list via the AddMethod EncLog entry.
+ let parentTypeDefRowId =
+ if isAdded then
+ tryGetTypeDefRowId key.DeclaringType
+ else
+ None
+
+ Some
+ {
+ MethodDefinitionRowInfo.Key = key
+ RowId = rowId
+ IsAdded = isAdded
+ ParentTypeDefRowId = parentTypeDefRowId
+ Attributes = resolvedAttributes
+ ImplAttributes = resolvedImplAttributes
+ Name = name
+ NameOffset = resolvedNameOffset
+ Signature = resolvedSignature
+ SignatureOffset = resolvedSignatureOffset
+ FirstParameterRowId = firstParam
+ CodeRva = resolvedCodeRva
+ }
+ | _ -> None
+
+ let initialRows =
+ methodDefinitionRowsRaw
+ |> List.choose (fun struct (rowId, key, isAdded) -> tryBuildMethodRow rowId key isAdded)
+
+ let existingKeys =
+ HashSet(initialRows |> Seq.map (fun row -> row.Key), HashIdentity.Structural)
+
+ let missingRows =
+ methodUpdatesWithDefs
+ |> List.choose (fun (update, _, _) ->
+ if existingKeys.Contains update.MethodKey then
+ None
+ else
+ let rowId =
+ match baselineMethodTokens |> Map.tryFind update.MethodKey with
+ | Some token -> token &&& 0x00FFFFFF
+ | None -> methodDefinitionIndex.GetRowId update.MethodKey
+
+ tryBuildMethodRow rowId update.MethodKey false)
+
+ let rows = initialRows @ missingRows
+
+ if traceMethodUpdates.Value then
+ printfn "[fsharp-hotreload][method-rows] count=%d (missing=%d)" rows.Length missingRows.Length
+ printfn "[fsharp-hotreload][params] firstParamRowByMethod entries:"
+
+ for KeyValue(k, v) in firstParamRowByMethod do
+ printfn " %s::%s firstParamRowId=%d" k.DeclaringType k.Name v
+
+ printfn "[fsharp-hotreload][methods] FirstParameterRowId after merge:"
+
+ for row in rows do
+ let fp = defaultArg row.FirstParameterRowId 0
+ printfn " method=%s::%s rowId=%d firstParam=%d isAdded=%b" row.Key.DeclaringType row.Key.Name row.RowId fp row.IsAdded
+
+ rows
+
+let private buildMethodSpecificationRowsSnapshot
+ (traceMetadata: bool)
+ (methodUpdatesWithDefs: (MethodMetadataUpdate * MethodDefinition * int list) list)
+ (baselineMethodSpecRowCount: int)
+ (methodSpecRowsByToken: Dictionary)
+ : MethodSpecificationRowInfo list =
+
+ let referencedMethodSpecTokens =
+ methodUpdatesWithDefs
+ |> List.collect (fun (_, _, methodSpecs) -> methodSpecs)
+ |> List.distinct
+
+ if traceMetadata then
+ printfn
+ "[fsharp-hotreload][metadata] methodspec candidates=%d baselineRows=%d tokens=%s"
+ referencedMethodSpecTokens.Length
+ baselineMethodSpecRowCount
+ (referencedMethodSpecTokens
+ |> List.map (fun token -> sprintf "0x%08X" token)
+ |> String.concat ",")
+
+ referencedMethodSpecTokens
+ |> List.choose (fun methodSpecToken ->
+ match methodSpecRowsByToken.TryGetValue methodSpecToken with
+ | true, row -> Some row
+ | _ ->
+ if traceMetadata then
+ printfn "[fsharp-hotreload][metadata] missing mapped methodspec token=0x%08X" methodSpecToken
+
+ None)
+ |> Seq.sortBy _.RowId
+ |> Seq.toList
+
+let private buildPropertyEventAndSemanticsRows
+ (traceMethodUpdates: bool)
+ (request: IlxDeltaRequest)
+ (tryGetTypeDefToken: string -> int option)
+ (metadataReader: MetadataReader)
+ (propertyDefinitionIndex: DefinitionIndex)
+ (eventDefinitionIndex: DefinitionIndex)
+ (propertyHandleLookup: Dictionary)
+ (eventHandleLookup: Dictionary)
+ (baselinePropertyHandles: Map)
+ (baselineEventHandles: Map)
+ (baselineTableRowCounts: int[])
+ (remapMethodToken: int -> int)
+ (remapEventTypeToken: int -> int)
+ (remapAddedSignature: byte[] -> byte[])
+ =
+ let propertyDefinitionRowsSnapshot =
+ propertyDefinitionIndex.Rows
+ |> List.choose (fun struct (rowId, key, isAdded) ->
+ match propertyHandleLookup.TryGetValue key with
+ | true, handle when not handle.IsNil ->
+ let propertyDef = metadataReader.GetPropertyDefinition handle
+ let name = metadataReader.GetString propertyDef.Name
+ let baselineHandles = baselinePropertyHandles |> Map.tryFind key
+ // Properties without baseline heap entries - added this generation OR added
+ // by an EARLIER delta and re-registered now (the handle cache only covers
+ // the on-disk baseline) - must carry their name/signature as delta heap
+ // content (offset None). Fresh-compile heap offsets are never valid against
+ // the baseline+delta heap layout (same rule as method rows).
+ let resolvedNameOffset =
+ baselineHandles |> Option.bind (fun info -> info.NameOffset)
+
+ let resolvedSignatureOffset =
+ baselineHandles |> Option.bind (fun info -> info.SignatureOffset)
+
+ let signature =
+ let rawSignature = metadataReader.GetBlobBytes propertyDef.Signature
+ // PropertySig blobs entering the delta blob heap need their embedded
+ // TypeDefOrRef coded indexes remapped to baseline rows. Gated on isAdded
+ // because the writers only EMIT added Property rows (an accessor body
+ // edit re-registers the existing row but its snapshot row is dropped);
+ // remapping a never-emitted row could side-effect TypeRef appends.
+ if isAdded && resolvedSignatureOffset.IsNone then
+ remapAddedSignature rawSignature
+ else
+ rawSignature
+
+ Some
+ {
+ PropertyDefinitionRowInfo.Key = key
+ RowId = rowId
+ IsAdded = isAdded
+ // Filled below once the PropertyMap row ids are allocated.
+ ParentPropertyMapRowId = None
+ Name = name
+ NameOffset = resolvedNameOffset
+ Signature = signature
+ SignatureOffset = resolvedSignatureOffset
+ Attributes = propertyDef.Attributes
+ }
+ | _ -> None)
+
+ if traceMethodUpdates then
+ printfn "[fsharp-hotreload][property-rows] count=%d" propertyDefinitionRowsSnapshot.Length
+
+ let eventDefinitionRowsSnapshot =
+ eventDefinitionIndex.Rows
+ |> List.choose (fun struct (rowId, key, isAdded) ->
+ match eventHandleLookup.TryGetValue key with
+ | true, handle when not handle.IsNil ->
+ let eventDef = metadataReader.GetEventDefinition handle
+ let name = metadataReader.GetString eventDef.Name
+ // Events without a baseline heap entry write their name into the delta
+ // string heap (see the property snapshot above for rationale).
+ let resolvedNameOffset =
+ baselineEventHandles
+ |> Map.tryFind key
+ |> Option.bind (fun info -> info.NameOffset)
+
+ let eventType =
+ // Added events carry a fresh-compile TypeDefOrRef in their EventType
+ // column; remap it to baseline/delta rows (the content-validated
+ // reference remapper appends TypeRef rows as needed).
+ if isAdded && not eventDef.Type.IsNil then
+ let remappedToken = remapEventTypeToken (MetadataTokens.GetToken eventDef.Type)
+ let rowNumber = remappedToken &&& 0x00FFFFFF
+
+ match remappedToken >>> 24 with
+ | 0x02 -> TDR_TypeDef(TypeDefHandle rowNumber)
+ | 0x01 -> TDR_TypeRef(TypeRefHandle rowNumber)
+ | 0x1b -> TDR_TypeSpec(TypeSpecHandle rowNumber)
+ | _ -> TDR_TypeDef(TypeDefHandle 0)
+ else
+ entityHandleToTypeDefOrRef eventDef.Type
+
+ Some
+ {
+ EventDefinitionRowInfo.Key = key
+ RowId = rowId
+ IsAdded = isAdded
+ // Filled below once the EventMap row ids are allocated.
+ ParentEventMapRowId = None
+ Name = name
+ NameOffset = resolvedNameOffset
+ Attributes = eventDef.Attributes
+ EventType = eventType
+ }
+ | _ -> None)
+
+ let propertyRowsByType =
+ propertyDefinitionRowsSnapshot
+ |> Seq.groupBy (fun row -> row.Key.DeclaringType)
+ |> dict
+
+ let eventRowsByType =
+ eventDefinitionRowsSnapshot
+ |> Seq.groupBy (fun row -> row.Key.DeclaringType)
+ |> dict
+
+ let baselinePropertyMapRowCount =
+ baselineTableRowCounts.[TableNames.PropertyMap.Index]
+
+ let baselineEventMapRowCount = baselineTableRowCounts.[TableNames.EventMap.Index]
+
+ let propertyMapDefinitionIndex =
+ let tryExisting typeName =
+ request.Baseline.PropertyMapEntries |> Map.tryFind typeName
+
+ DefinitionIndex(tryExisting, baselinePropertyMapRowCount)
+
+ let eventMapDefinitionIndex =
+ let tryExisting typeName =
+ request.Baseline.EventMapEntries |> Map.tryFind typeName
+
+ DefinitionIndex(tryExisting, baselineEventMapRowCount)
+
+ let propertyMapRowsSnapshot =
+ let missingTypes =
+ propertyDefinitionRowsSnapshot
+ |> Seq.filter _.IsAdded
+ |> Seq.map (fun row -> row.Key.DeclaringType)
+ |> Seq.filter (fun typeName -> not (request.Baseline.PropertyMapEntries |> Map.containsKey typeName))
+ |> Seq.distinct
+ |> Seq.toList
+
+ for typeName in missingTypes do
+ propertyMapDefinitionIndex.Add typeName |> ignore
+
+ propertyMapDefinitionIndex.Rows
+ |> List.choose (fun struct (rowId, typeName, isAdded) ->
+ // ADDED types (closure classes and user-defined) have no baseline TypeDef token; their map rows
+ // parent the new delta TypeDef row.
+ let typeTokenOpt = tryGetTypeDefToken typeName
+
+ let firstPropertyRowIdOpt =
+ match propertyRowsByType.TryGetValue typeName with
+ | true, rows -> rows |> Seq.sortBy _.RowId |> Seq.tryHead |> Option.map _.RowId
+ | _ -> None
+
+ let shouldAdd = isAdded || List.contains typeName missingTypes
+
+ match typeTokenOpt, firstPropertyRowIdOpt, shouldAdd with
+ | Some typeToken, Some firstRowId, true ->
+ Some
+ {
+ PropertyMapRowInfo.DeclaringType = typeName
+ RowId = rowId
+ TypeDefRowId = typeToken &&& 0x00FFFFFF
+ FirstPropertyRowId = Some firstRowId
+ IsAdded = true
+ }
+ | _ -> None)
+
+ let eventMapRowsSnapshot =
+ let missingTypes =
+ eventDefinitionRowsSnapshot
+ |> Seq.filter _.IsAdded
+ |> Seq.map (fun row -> row.Key.DeclaringType)
+ |> Seq.filter (fun typeName -> not (request.Baseline.EventMapEntries |> Map.containsKey typeName))
+ |> Seq.distinct
+ |> Seq.toList
+
+ for typeName in missingTypes do
+ eventMapDefinitionIndex.Add typeName |> ignore
+
+ eventMapDefinitionIndex.Rows
+ |> List.choose (fun struct (rowId, typeName, isAdded) ->
+ let typeTokenOpt = tryGetTypeDefToken typeName
+
+ let firstEventRowIdOpt =
+ match eventRowsByType.TryGetValue typeName with
+ | true, rows -> rows |> Seq.sortBy _.RowId |> Seq.tryHead |> Option.map _.RowId
+ | _ -> None
+
+ let shouldAdd = isAdded || List.contains typeName missingTypes
+
+ match typeTokenOpt, firstEventRowIdOpt, shouldAdd with
+ | Some typeToken, Some firstRowId, true ->
+ Some
+ {
+ EventMapRowInfo.DeclaringType = typeName
+ RowId = rowId
+ TypeDefRowId = typeToken &&& 0x00FFFFFF
+ FirstEventRowId = Some firstRowId
+ IsAdded = true
+ }
+ | _ -> None)
+
+ let mutable nextMethodSemanticsRowId =
+ baselineTableRowCounts.[TableNames.MethodSemantics.Index]
+
+ // MethodSemantics rows for ADDED properties/events are derived from the fresh
+ // compile's accessor relationships (Roslyn parity: DeltaMetadataWriter emits the
+ // semantics rows from the symbol model, not from the edit list), so every added
+ // Property/Event row carries its Getter/Setter/Adder/Remover/Raiser bindings even
+ // when the accessors are compiler-synthesized (module values, [] members).
+ // Accessor method tokens come from the fresh metadata and are remapped to
+ // baseline/delta MethodDef rows. A Property/Event row without its semantics rows is
+ // corrupt metadata, so a missing/unmappable accessor fails closed below.
+ let methodSemanticsRowsSnapshot =
+ let accessorRow (memberDisplay: string) (attrs: MethodSemanticsAttributes) (handle: MethodDefinitionHandle) association =
+ if handle.IsNil then
+ None
+ else
+ let freshToken = MetadataTokens.GetToken(EntityHandle.op_Implicit handle)
+ let mappedToken = remapMethodToken freshToken
+
+ if mappedToken >>> 24 <> 0x06 || mappedToken &&& 0x00FFFFFF = 0 then
+ raise (
+ HotReloadUnsupportedEditException(
+ $"Added member '{memberDisplay}' has an accessor that does not map to a baseline or delta MethodDef row; please rebuild."
+ )
+ )
+
+ nextMethodSemanticsRowId <- nextMethodSemanticsRowId + 1
+
+ Some
+ {
+ MethodSemanticsMetadataUpdate.RowId = nextMethodSemanticsRowId
+ MethodToken = mappedToken
+ Attributes = attrs
+ IsAdded = true
+ AssociationInfo = association
+ }
+
+ let propertySemantics =
+ propertyDefinitionRowsSnapshot
+ |> List.filter (fun row -> row.IsAdded)
+ |> List.collect (fun row ->
+ let display = $"{row.Key.DeclaringType}::{row.Key.Name}"
+ let association = MethodSemanticsAssociation.PropertyAssociation(row.Key, row.RowId)
+
+ let rows =
+ match propertyHandleLookup.TryGetValue row.Key with
+ | true, handle when not handle.IsNil ->
+ let accessors = (metadataReader.GetPropertyDefinition handle).GetAccessors()
+
+ [
+ yield!
+ accessorRow display MethodSemanticsAttributes.Getter accessors.Getter association
+ |> Option.toList
+ yield!
+ accessorRow display MethodSemanticsAttributes.Setter accessors.Setter association
+ |> Option.toList
+ for other in accessors.Others do
+ yield!
+ accessorRow display MethodSemanticsAttributes.Other other association
+ |> Option.toList
+ ]
+ | _ -> []
+
+ if List.isEmpty rows then
+ // Fail closed: a Property row whose accessors cannot be bound would be
+ // unreachable, corrupt metadata after apply.
+ raise (
+ HotReloadUnsupportedEditException(
+ $"Added property '{display}' has no resolvable accessor methods; please rebuild."
+ )
+ )
+
+ rows)
+
+ let eventSemantics =
+ eventDefinitionRowsSnapshot
+ |> List.filter (fun row -> row.IsAdded)
+ |> List.collect (fun row ->
+ let display = $"{row.Key.DeclaringType}::{row.Key.Name}"
+ let association = MethodSemanticsAssociation.EventAssociation(row.Key, row.RowId)
+
+ let rows =
+ match eventHandleLookup.TryGetValue row.Key with
+ | true, handle when not handle.IsNil ->
+ let accessors = (metadataReader.GetEventDefinition handle).GetAccessors()
+
+ [
+ yield!
+ accessorRow display MethodSemanticsAttributes.Adder accessors.Adder association
+ |> Option.toList
+ yield!
+ accessorRow display MethodSemanticsAttributes.Remover accessors.Remover association
+ |> Option.toList
+ yield!
+ accessorRow display MethodSemanticsAttributes.Raiser accessors.Raiser association
+ |> Option.toList
+ for other in accessors.Others do
+ yield!
+ accessorRow display MethodSemanticsAttributes.Other other association
+ |> Option.toList
+ ]
+ | _ -> []
+
+ if List.isEmpty rows then
+ raise (
+ HotReloadUnsupportedEditException($"Added event '{display}' has no resolvable accessor methods; please rebuild.")
+ )
+
+ rows)
+
+ propertySemantics @ eventSemantics
+
+ // Fill parent map row ids for ADDED properties/events now that map rows are allocated;
+ // the AddProperty/AddEvent EncLog entries carry the parent map token (CLR requirement).
+ // Covers both newly added map rows and maps that already exist in the baseline.
+ let propertyDefinitionRowsSnapshot =
+ propertyDefinitionRowsSnapshot
+ |> List.map (fun row ->
+ if
+ row.IsAdded
+ && row.ParentPropertyMapRowId.IsNone
+ && propertyMapDefinitionIndex.Contains row.Key.DeclaringType
+ then
+ { row with
+ ParentPropertyMapRowId = Some(propertyMapDefinitionIndex.GetRowId row.Key.DeclaringType)
+ }
+ else
+ row)
+
+ let eventDefinitionRowsSnapshot =
+ eventDefinitionRowsSnapshot
+ |> List.map (fun row ->
+ if
+ row.IsAdded
+ && row.ParentEventMapRowId.IsNone
+ && eventMapDefinitionIndex.Contains row.Key.DeclaringType
+ then
+ { row with
+ ParentEventMapRowId = Some(eventMapDefinitionIndex.GetRowId row.Key.DeclaringType)
+ }
+ else
+ row)
+
+ propertyDefinitionRowsSnapshot, eventDefinitionRowsSnapshot, propertyMapRowsSnapshot, eventMapRowsSnapshot, methodSemanticsRowsSnapshot
+
+/// Fresh-compile tokens and delta row tokens of the members ADDED by this delta, used to
+/// emit their CustomAttribute rows (the fresh compile's attributes ARE the attributes the
+/// added member must carry; C# reference templates show 2-4 CA rows per added member,
+/// e.g. [CompilerGenerated]/[DebuggerBrowsable] on auto-property backing fields and
+/// accessors).
+type private AddedMemberAttributeSources =
+ {
+ AddedFieldTokens: Dictionary
+ AddedFieldDeltaTokens: Dictionary
+ AddedPropertyTokens: Dictionary
+ AddedPropertyDeltaTokens: Dictionary
+ AddedEventTokens: Dictionary
+ AddedEventDeltaTokens: Dictionary