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/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
index b49aa2d0835..176ee6934c5 100644
--- a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
+++ b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
@@ -23,7 +23,7 @@
* Fix internal error (FS0193) when calling an indexed property setter with a named argument that matches an indexer parameter. ([Issue #16034](https://github.com/dotnet/fsharp/issues/16034), [PR #19851](https://github.com/dotnet/fsharp/pull/19851))
* Fix missing FS1182 ("unused binding") warning for unused `let` function bindings inside class types. ([Issue #13849](https://github.com/dotnet/fsharp/issues/13849), [PR #19805](https://github.com/dotnet/fsharp/pull/19805))
* Fix internal compiler error FS1110 in `task { let! }` (and other computation expressions) when a generic IL extension method whose `this`-parameter is a method-level type variable is in scope (e.g. `open ReactiveUI`). Regression from PR #19536. ([Issue #19936](https://github.com/dotnet/fsharp/issues/19936))
-* Fix inner mutually-recursive `let rec ... and ...` functions under `--realsig+` not being lifted to top-level static methods (TLR), causing `FSharpFunc` closure allocations and loss of `tail.` opcodes — the large struct-mutual-recursion perf regression reported in [Issue #17607](https://github.com/dotnet/fsharp/issues/17607). ([PR #19882](https://github.com/dotnet/fsharp/pull/19882))
+* Fix inner mutually-recursive `let rec ... and ...` functions under `--realsig+` not being lifted to top-level static methods (TLR), causing `FSharpFunc` closure allocations and loss of `tail.` opcodes - the large struct-mutual-recursion perf regression reported in [Issue #17607](https://github.com/dotnet/fsharp/issues/17607). ([PR #19882](https://github.com/dotnet/fsharp/pull/19882))
* Fix `TypeLoadException` ("Specialize tried to implicitly override a method with weaker type parameter constraints") and the related CLR crash with constrained inline calls by stripping constraints from closure-class typars in `EraseClosures.convIlxClosureDef`. ([Issue #14492](https://github.com/dotnet/fsharp/issues/14492), [Issue #19075](https://github.com/dotnet/fsharp/issues/19075), [PR #19882](https://github.com/dotnet/fsharp/pull/19882))
* Fix `FieldAccessException` at runtime when the optimizer relocates a read of a `protected` (family) base-class field into a method outside the field's family (e.g. a trivial member inlined into module/startup code under `--optimize+`). Protected (family) IL field access is no longer hoisted out of its declaring family by inlining or method-splitting. ([Issue #19963](https://github.com/dotnet/fsharp/issues/19963), [PR #19964](https://github.com/dotnet/fsharp/pull/19964))
@@ -128,6 +128,10 @@
### Added
+* Added internal synthesized-name replay infrastructure for compiler-generated names, preserving normal compilation output while enabling future hot reload name stability work.
+* Add an internal typed-tree diff utility for future F# hot reload edit classification. It is not called by normal compilation. ([PR #19941](https://github.com/dotnet/fsharp/pull/19941))
+* Added internal F# hot reload delta emitter and symbol matcher infrastructure with direct emitter test coverage.
+* 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.
* 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))
@@ -140,6 +144,10 @@
* Checker: recover on checking language version ([PR ##19970](https://github.com/dotnet/fsharp/pull/19970))
* Implied argument names for function-to-delegate coercions now fall back to the delegate's `Invoke` parameter names when the function has no recoverable names (e.g. a partial application like `System.Func((+) 1)`), instead of synthetic `delegateArg0`, `delegateArg1`, … names. ([PR #20001](https://github.com/dotnet/fsharp/pull/20001))
* Add internal `ResetCompilerGeneratedNameState` to `CompilerGlobalState` name generators so warm-checker re-compilation can produce fresh-process-identical generated names. ([PR #20017](https://github.com/dotnet/fsharp/pull/20017))
+* Add Roslyn-format EnC CustomDebugInformation codec and portable PDB method CDI emission support to AbstractIL. ([PR #20018](https://github.com/dotnet/fsharp/pull/20018))
+* Add internal ECMA-335 Edit-and-Continue metadata delta writer to AbstractIL. ([PR #20019](https://github.com/dotnet/fsharp/pull/20019))
+* 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 experimental hot reload support: `--test:HotReloadDeltas` baseline capture in fsc, EnC metadata/IL/PDB delta emission, rude-edit detection, and an `FSharpChecker` session API (`CreateHotReloadSession`, with `AddProject`/`EmitDelta`/`Commit`/`Discard`). Off by default; flag-off compilation is unchanged. ([Issue #11636](https://github.com/dotnet/fsharp/issues/11636), [PR #19941](https://github.com/dotnet/fsharp/pull/19941))
### Improved
@@ -151,4 +159,4 @@
* Exception field serialization (`GetObjectData` and field-restoring constructor) is now gated behind `langversion:11` (`LanguageFeature.ExceptionFieldSerializationSupport`). With langversion ≤10, exception codegen is unchanged from pre-#19342 behavior. ([PR #19746](https://github.com/dotnet/fsharp/pull/19746))
### Breaking Changes
-* Optimizer: don't inline named functions in debug builds ([PR #19548](https://github.com/dotnet/fsharp/pull/19548)
\ No newline at end of file
+* Optimizer: don't inline named functions in debug builds ([PR #19548](https://github.com/dotnet/fsharp/pull/19548)
diff --git a/release-notes.md b/release-notes.md
index 54445f86e2f..9529ea08182 100644
--- a/release-notes.md
+++ b/release-notes.md
@@ -18,6 +18,7 @@ These release notes track our current efforts to document changes to the F# proj
### FSharp Compiler Service (main)
+* Added internal hot reload baseline reading for recorded EnC state and synthesized-name snapshot PDB data.
* In FSharpParsingOptions, rename ConditionalCompilationDefines --> ConditionalDefines
* Some syntax tree nodes have changed, e.g. introduction of SyntaxTree trivia
* Resolved expressions (FSharpExpr) now reveal debug points, you must match them explicitly using `DebugPoint(dp, expr)`
diff --git a/src/Compiler/AbstractIL/ILBaselineReader.fs b/src/Compiler/AbstractIL/ILBaselineReader.fs
new file mode 100644
index 00000000000..85827d2b24c
--- /dev/null
+++ b/src/Compiler/AbstractIL/ILBaselineReader.fs
@@ -0,0 +1,1337 @@
+/// 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 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 64-bit integer from bytes at offset.
+let private readInt64 (bytes: byte[]) (offset: int) =
+ int64 (readInt32 bytes offset) ||| (int64 (readInt32 bytes (offset + 4)) <<< 32)
+
+/// Number of metadata tables per ECMA-335.
+let private tableCount = 64
+
+/// 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 (heapSizes byte, table row counts array, tables stream offset).
+let private parseTablesStream (bytes: byte[]) (tablesStream: StreamHeader) : byte * int[] * int =
+ 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 = readInt64 bytes (offset + 8)
+
+ let rowCounts = Array.zeroCreate tableCount
+ let mutable rowCountOffset = offset + 24
+
+ for i in 0..63 do
+ if (valid &&& (1L <<< i)) <> 0L then
+ rowCounts.[i] <- readInt32 bytes rowCountOffset
+ rowCountOffset <- rowCountOffset + 4
+
+ heapSizes, rowCounts, offset
+
+/// 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
+
+ if offset + 16 > bytes.Length then
+ None
+ else
+ let guidBytes = bytes.[offset .. offset + 15]
+ Some(System.Guid(guidBytes))
+
+// ============================================================================
+// Table row reading infrastructure
+// ============================================================================
+
+/// Table indices per ECMA-335 II.22
+module private TableIndices =
+ let Module = 0
+ let TypeRef = 1
+ let TypeDef = 2
+ let Field = 4
+ let MethodDef = 6
+ 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 Event = 20
+ let PropertyMap = 21
+ 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
+ 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 = parseTablesStream bytes tablesStream
+
+ 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 mutable rowCountSize = 0
+
+ for i in 0..63 do
+ if rowCounts.[i] > 0 then
+ rowCountSize <- rowCountSize + 4
+
+ let tablesStart = tablesOffset + 24 + rowCountSize
+
+ let stringsOffset =
+ streamHeaders
+ |> List.tryFind (fun h -> h.Name = "#Strings")
+ |> Option.map (fun h -> h.Offset)
+ |> Option.defaultValue 0
+
+ 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 = stringsOffset
+ 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 start = ctx.StringsStreamOffset + offset
+ let mutable endPos = start
+
+ while ctx.Bytes.[endPos] <> 0uy do
+ endPos <- endPos + 1
+
+ System.Text.Encoding.UTF8.GetString(ctx.Bytes, start, endPos - start)
+
+// ============================================================================
+// Table row reading functions
+// ============================================================================
+
+/// 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
+
+ /// Read a MethodDef row by 1-based row ID.
+ member _.GetMethodDef(rowId: int) =
+ readMethodDefRow ctx rowSizes tableOffsets rowId
+
+ /// 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 = 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 mutable rowCountSize = 0
+
+ for i in 0..63 do
+ if rowCounts.[i] > 0 then
+ rowCountSize <- rowCountSize + 4
+
+ let tablesStart = tablesOffset + 24 + rowCountSize
+
+ // 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 =
+ {
+ /// 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 = readInt64 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 &&& (1L <<< i)) <> 0L 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 with
+ | None -> None
+ | Some tablesStream ->
+ let rowCounts = parsePdbTablesStream pdbBytes tablesStream
+ let entryPoint = pdbStreamOpt |> Option.bind (fun s -> parsePdbStream pdbBytes s)
+
+ Some
+ {
+ TableRowCounts = rowCounts
+ EntryPointToken = entryPoint
+ }
+ with
+ | :? System.IndexOutOfRangeException -> None
+ | :? System.ArgumentOutOfRangeException -> None
diff --git a/src/Compiler/AbstractIL/ILDeltaHandles.fs b/src/Compiler/AbstractIL/ILDeltaHandles.fs
new file mode 100644
index 00000000000..ab0e8606b3b
--- /dev/null
+++ b/src/Compiler/AbstractIL/ILDeltaHandles.fs
@@ -0,0 +1,720 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+/// F# types and utilities for hot reload delta metadata emission.
+///
+/// These handles/coded-index unions are intentionally delta-owned to keep the
+/// hot-reload pipeline isolated from broad mainline signature churn.
+/// The core IL writer keeps its own row models; adapters below convert between
+/// delta-owned and core-owned representations when boundary crossings are needed.
+module internal FSharp.Compiler.AbstractIL.ILDeltaHandles
+
+open System
+open FSharp.Compiler.AbstractIL.BinaryConstants
+
+// ============================================================================
+// Entity Token
+// ============================================================================
+// Generic token representation for EncLog/EncMap entries
+
+/// Represents a metadata token as table index and row ID
+/// Used for EncLog and EncMap entries
+[]
+type EntityToken =
+ {
+ TableIndex: int
+ RowId: int
+ }
+
+ /// Creates a token from table index and row ID
+ static member Create(tableIndex: int, rowId: int) =
+ {
+ TableIndex = tableIndex
+ RowId = rowId
+ }
+
+ /// Gets the full 32-bit token value (table << 24 | rowId)
+ member this.Token = (this.TableIndex <<< 24) ||| (this.RowId &&& 0x00FFFFFF)
+
+// ============================================================================
+// Typed handles and coded indices used by delta metadata code
+// ============================================================================
+
+[]
+type ModuleHandle =
+ | ModuleHandle of rowId: int
+
+ member this.RowId = let (ModuleHandle v) = this in v
+
+[]
+type TypeRefHandle =
+ | TypeRefHandle of rowId: int
+
+ member this.RowId = let (TypeRefHandle v) = this in v
+
+[]
+type TypeDefHandle =
+ | TypeDefHandle of rowId: int
+
+ member this.RowId = let (TypeDefHandle v) = this in v
+
+[]
+type FieldHandle =
+ | FieldHandle of rowId: int
+
+ member this.RowId = let (FieldHandle v) = this in v
+
+[]
+type MethodDefHandle =
+ | MethodDefHandle of rowId: int
+
+ member this.RowId = let (MethodDefHandle v) = this in v
+
+[]
+type ParamHandle =
+ | ParamHandle of rowId: int
+
+ member this.RowId = let (ParamHandle v) = this in v
+
+[]
+type InterfaceImplHandle =
+ | InterfaceImplHandle of rowId: int
+
+ member this.RowId = let (InterfaceImplHandle v) = this in v
+
+[]
+type MemberRefHandle =
+ | MemberRefHandle of rowId: int
+
+ member this.RowId = let (MemberRefHandle v) = this in v
+
+[]
+type DeclSecurityHandle =
+ | DeclSecurityHandle of rowId: int
+
+ member this.RowId = let (DeclSecurityHandle v) = this in v
+
+[]
+type StandAloneSigHandle =
+ | StandAloneSigHandle of rowId: int
+
+ member this.RowId = let (StandAloneSigHandle v) = this in v
+
+[]
+type EventHandle =
+ | EventHandle of rowId: int
+
+ member this.RowId = let (EventHandle v) = this in v
+
+[]
+type PropertyHandle =
+ | PropertyHandle of rowId: int
+
+ member this.RowId = let (PropertyHandle v) = this in v
+
+[]
+type ModuleRefHandle =
+ | ModuleRefHandle of rowId: int
+
+ member this.RowId = let (ModuleRefHandle v) = this in v
+
+[]
+type TypeSpecHandle =
+ | TypeSpecHandle of rowId: int
+
+ member this.RowId = let (TypeSpecHandle v) = this in v
+
+[]
+type AssemblyHandle =
+ | AssemblyHandle of rowId: int
+
+ member this.RowId = let (AssemblyHandle v) = this in v
+
+[]
+type AssemblyRefHandle =
+ | AssemblyRefHandle of rowId: int
+
+ member this.RowId = let (AssemblyRefHandle v) = this in v
+
+[]
+type FileHandle =
+ | FileHandle of rowId: int
+
+ member this.RowId = let (FileHandle v) = this in v
+
+[]
+type ExportedTypeHandle =
+ | ExportedTypeHandle of rowId: int
+
+ member this.RowId = let (ExportedTypeHandle v) = this in v
+
+[]
+type ManifestResourceHandle =
+ | ManifestResourceHandle of rowId: int
+
+ member this.RowId = let (ManifestResourceHandle v) = this in v
+
+[]
+type GenericParamHandle =
+ | GenericParamHandle of rowId: int
+
+ member this.RowId = let (GenericParamHandle v) = this in v
+
+[]
+type MethodSpecHandle =
+ | MethodSpecHandle of rowId: int
+
+ member this.RowId = let (MethodSpecHandle v) = this in v
+
+[]
+type GenericParamConstraintHandle =
+ | GenericParamConstraintHandle of rowId: int
+
+ member this.RowId = let (GenericParamConstraintHandle v) = this in v
+
+[]
+type StringOffset =
+ | StringOffset of offset: int
+
+ member this.Value = let (StringOffset v) = this in v
+ static member Zero = StringOffset 0
+
+[]
+type BlobOffset =
+ | BlobOffset of offset: int
+
+ member this.Value = let (BlobOffset v) = this in v
+ static member Zero = BlobOffset 0
+
+[]
+type GuidIndex =
+ | GuidIndex of index: int
+
+ member this.Value = let (GuidIndex v) = this in v
+ static member Zero = GuidIndex 0
+
+[]
+type UserStringOffset =
+ | UserStringOffset of offset: int
+
+ member this.Value = let (UserStringOffset v) = this in v
+ static member Zero = UserStringOffset 0
+
+/// TypeDefOrRef coded index (ECMA-335 II.24.2.6)
+type TypeDefOrRef =
+ | TDR_TypeDef of TypeDefHandle
+ | TDR_TypeRef of TypeRefHandle
+ | TDR_TypeSpec of TypeSpecHandle
+
+ member this.CodedTag =
+ match this with
+ | TDR_TypeDef _ -> tdor_TypeDef.Tag
+ | TDR_TypeRef _ -> tdor_TypeRef.Tag
+ | TDR_TypeSpec _ -> tdor_TypeSpec.Tag
+
+ member this.RowId =
+ match this with
+ | TDR_TypeDef h -> h.RowId
+ | TDR_TypeRef h -> h.RowId
+ | TDR_TypeSpec h -> h.RowId
+
+/// HasCustomAttribute coded index (ECMA-335 II.24.2.6)
+type HasCustomAttribute =
+ | HCA_MethodDef of MethodDefHandle
+ | HCA_Field of FieldHandle
+ | HCA_TypeRef of TypeRefHandle
+ | HCA_TypeDef of TypeDefHandle
+ | HCA_Param of ParamHandle
+ | HCA_InterfaceImpl of InterfaceImplHandle
+ | HCA_MemberRef of MemberRefHandle
+ | HCA_Module of ModuleHandle
+ | HCA_DeclSecurity of DeclSecurityHandle
+ | HCA_Property of PropertyHandle
+ | HCA_Event of EventHandle
+ | HCA_StandAloneSig of StandAloneSigHandle
+ | HCA_ModuleRef of ModuleRefHandle
+ | HCA_TypeSpec of TypeSpecHandle
+ | HCA_Assembly of AssemblyHandle
+ | HCA_AssemblyRef of AssemblyRefHandle
+ | HCA_File of FileHandle
+ | HCA_ExportedType of ExportedTypeHandle
+ | HCA_ManifestResource of ManifestResourceHandle
+ | HCA_GenericParam of GenericParamHandle
+ | HCA_GenericParamConstraint of GenericParamConstraintHandle
+ | HCA_MethodSpec of MethodSpecHandle
+
+ member this.CodedTag =
+ match this with
+ | HCA_MethodDef _ -> hca_MethodDef.Tag
+ | HCA_Field _ -> hca_FieldDef.Tag
+ | HCA_TypeRef _ -> hca_TypeRef.Tag
+ | HCA_TypeDef _ -> hca_TypeDef.Tag
+ | HCA_Param _ -> hca_ParamDef.Tag
+ | HCA_InterfaceImpl _ -> hca_InterfaceImpl.Tag
+ | HCA_MemberRef _ -> hca_MemberRef.Tag
+ | HCA_Module _ -> hca_Module.Tag
+ | HCA_DeclSecurity _ -> hca_Permission.Tag
+ | HCA_Property _ -> hca_Property.Tag
+ | HCA_Event _ -> hca_Event.Tag
+ | HCA_StandAloneSig _ -> hca_StandAloneSig.Tag
+ | HCA_ModuleRef _ -> hca_ModuleRef.Tag
+ | HCA_TypeSpec _ -> hca_TypeSpec.Tag
+ | HCA_Assembly _ -> hca_Assembly.Tag
+ | HCA_AssemblyRef _ -> hca_AssemblyRef.Tag
+ | HCA_File _ -> hca_File.Tag
+ | HCA_ExportedType _ -> hca_ExportedType.Tag
+ | HCA_ManifestResource _ -> hca_ManifestResource.Tag
+ | HCA_GenericParam _ -> hca_GenericParam.Tag
+ // HasCustomAttribute coded-index tags for GenericParamConstraint (0x14) and
+ // MethodSpec (0x15), per ECMA-335 II.24.2.6.
+ | HCA_GenericParamConstraint _ -> 20
+ | HCA_MethodSpec _ -> 21
+
+ member this.RowId =
+ match this with
+ | HCA_MethodDef h -> h.RowId
+ | HCA_Field h -> h.RowId
+ | HCA_TypeRef h -> h.RowId
+ | HCA_TypeDef h -> h.RowId
+ | HCA_Param h -> h.RowId
+ | HCA_InterfaceImpl h -> h.RowId
+ | HCA_MemberRef h -> h.RowId
+ | HCA_Module h -> h.RowId
+ | HCA_DeclSecurity h -> h.RowId
+ | HCA_Property h -> h.RowId
+ | HCA_Event h -> h.RowId
+ | HCA_StandAloneSig h -> h.RowId
+ | HCA_ModuleRef h -> h.RowId
+ | HCA_TypeSpec h -> h.RowId
+ | HCA_Assembly h -> h.RowId
+ | HCA_AssemblyRef h -> h.RowId
+ | HCA_File h -> h.RowId
+ | HCA_ExportedType h -> h.RowId
+ | HCA_ManifestResource h -> h.RowId
+ | HCA_GenericParam h -> h.RowId
+ | HCA_GenericParamConstraint h -> h.RowId
+ | HCA_MethodSpec h -> h.RowId
+
+/// MemberRefParent coded index (ECMA-335 II.24.2.6)
+type MemberRefParent =
+ | MRP_TypeDef of TypeDefHandle
+ | MRP_TypeRef of TypeRefHandle
+ | MRP_ModuleRef of ModuleRefHandle
+ | MRP_MethodDef of MethodDefHandle
+ | MRP_TypeSpec of TypeSpecHandle
+
+ member this.CodedTag =
+ match this with
+ // BinaryConstants does not expose this tag on main; keep the ECMA tag id explicit here.
+ | MRP_TypeDef _ -> 0
+ | MRP_TypeRef _ -> mrp_TypeRef.Tag
+ | MRP_ModuleRef _ -> mrp_ModuleRef.Tag
+ | MRP_MethodDef _ -> mrp_MethodDef.Tag
+ | MRP_TypeSpec _ -> mrp_TypeSpec.Tag
+
+ member this.RowId =
+ match this with
+ | MRP_TypeDef h -> h.RowId
+ | MRP_TypeRef h -> h.RowId
+ | MRP_ModuleRef h -> h.RowId
+ | MRP_MethodDef h -> h.RowId
+ | MRP_TypeSpec h -> h.RowId
+
+/// HasSemantics coded index (ECMA-335 II.24.2.6)
+type HasSemantics =
+ | HS_Event of EventHandle
+ | HS_Property of PropertyHandle
+
+ member this.CodedTag =
+ match this with
+ | HS_Event _ -> hs_Event.Tag
+ | HS_Property _ -> hs_Property.Tag
+
+ member this.RowId =
+ match this with
+ | HS_Event h -> h.RowId
+ | HS_Property h -> h.RowId
+
+/// CustomAttributeType coded index (ECMA-335 II.24.2.6)
+type CustomAttributeType =
+ | CAT_MethodDef of MethodDefHandle
+ | CAT_MemberRef of MemberRefHandle
+
+ member this.CodedTag =
+ match this with
+ | CAT_MethodDef _ -> cat_MethodDef.Tag
+ | CAT_MemberRef _ -> cat_MemberRef.Tag
+
+ member this.RowId =
+ match this with
+ | CAT_MethodDef h -> h.RowId
+ | CAT_MemberRef h -> h.RowId
+
+/// ResolutionScope coded index (ECMA-335 II.24.2.6)
+type ResolutionScope =
+ | RS_Module of ModuleHandle
+ | RS_ModuleRef of ModuleRefHandle
+ | RS_AssemblyRef of AssemblyRefHandle
+ | RS_TypeRef of TypeRefHandle
+
+ member this.CodedTag =
+ match this with
+ | RS_Module _ -> rs_Module.Tag
+ | RS_ModuleRef _ -> rs_ModuleRef.Tag
+ | RS_AssemblyRef _ -> rs_AssemblyRef.Tag
+ | RS_TypeRef _ -> rs_TypeRef.Tag
+
+ member this.RowId =
+ match this with
+ | RS_Module h -> h.RowId
+ | RS_ModuleRef h -> h.RowId
+ | RS_AssemblyRef h -> h.RowId
+ | RS_TypeRef h -> h.RowId
+
+/// MethodDefOrRef coded index (ECMA-335 II.24.2.6)
+type MethodDefOrRef =
+ | MDOR_MethodDef of MethodDefHandle
+ | MDOR_MemberRef of MemberRefHandle
+
+ member this.CodedTag =
+ match this with
+ | MDOR_MethodDef _ -> mdor_MethodDef.Tag
+ | MDOR_MemberRef _ -> mdor_MemberRef.Tag
+
+ member this.RowId =
+ match this with
+ | MDOR_MethodDef h -> h.RowId
+ | MDOR_MemberRef h -> h.RowId
+
+// ----------------------------------------------------------------------------
+// Adapters from delta-owned coded indices to boundary-safe primitives.
+// ilbinary.fsi intentionally hides core handle/coded-index unions; by using
+// primitives at boundaries we keep hot-reload isolated without widening core APIs.
+// ----------------------------------------------------------------------------
+module CoreTypeAdapters =
+ let moduleRowId (ModuleHandle rowId) = rowId
+ let typeRefRowId (TypeRefHandle rowId) = rowId
+ let typeDefRowId (TypeDefHandle rowId) = rowId
+ let memberRefRowId (MemberRefHandle rowId) = rowId
+ let methodDefRowId (MethodDefHandle rowId) = rowId
+ let typeSpecRowId (TypeSpecHandle rowId) = rowId
+ let moduleRefRowId (ModuleRefHandle rowId) = rowId
+ let assemblyRefRowId (AssemblyRefHandle rowId) = rowId
+
+ /// Returns (coded tag, row id) for TypeDefOrRef.
+ let typeDefOrRefParts (value: TypeDefOrRef) = value.CodedTag, value.RowId
+
+ /// Returns (coded tag, row id) for MemberRefParent.
+ let memberRefParentParts (value: MemberRefParent) = value.CodedTag, value.RowId
+
+ /// Returns (coded tag, row id) for MethodDefOrRef.
+ let methodDefOrRefParts (value: MethodDefOrRef) = value.CodedTag, value.RowId
+
+ /// Returns (coded tag, row id) for ResolutionScope.
+ let resolutionScopeParts (value: ResolutionScope) = value.CodedTag, value.RowId
+
+// ============================================================================
+// Additional Coded Index Types (less frequently used)
+// ============================================================================
+// These are defined here rather than in BinaryConstants because they are
+// primarily used by delta code and not needed for baseline IL writing.
+
+/// HasConstant coded index (2-bit tag)
+/// Tag: Field=0, Param=1, Property=2
+type HasConstant =
+ | HC_Field of FieldHandle
+ | HC_Param of ParamHandle
+ | HC_Property of PropertyHandle
+
+ member this.TableIndex =
+ match this with
+ | HC_Field _ -> 0x04
+ | HC_Param _ -> 0x08
+ | HC_Property _ -> 0x17
+
+ member this.RowId =
+ match this with
+ | HC_Field(FieldHandle rid) -> rid
+ | HC_Param(ParamHandle rid) -> rid
+ | HC_Property(PropertyHandle rid) -> rid
+
+/// HasFieldMarshal coded index (1-bit tag)
+/// Tag: Field=0, Param=1
+type HasFieldMarshal =
+ | HFM_Field of FieldHandle
+ | HFM_Param of ParamHandle
+
+ member this.TableIndex =
+ match this with
+ | HFM_Field _ -> 0x04
+ | HFM_Param _ -> 0x08
+
+ member this.RowId =
+ match this with
+ | HFM_Field(FieldHandle rid) -> rid
+ | HFM_Param(ParamHandle rid) -> rid
+
+/// HasDeclSecurity coded index (2-bit tag)
+/// Tag: TypeDef=0, MethodDef=1, Assembly=2
+type HasDeclSecurity =
+ | HDS_TypeDef of TypeDefHandle
+ | HDS_MethodDef of MethodDefHandle
+ | HDS_Assembly of AssemblyHandle
+
+ member this.TableIndex =
+ match this with
+ | HDS_TypeDef _ -> 0x02
+ | HDS_MethodDef _ -> 0x06
+ | HDS_Assembly _ -> 0x20
+
+ member this.RowId =
+ match this with
+ | HDS_TypeDef(TypeDefHandle rid) -> rid
+ | HDS_MethodDef(MethodDefHandle rid) -> rid
+ | HDS_Assembly(AssemblyHandle rid) -> rid
+
+/// MemberForwarded coded index (1-bit tag)
+/// Tag: Field=0, MethodDef=1
+type MemberForwarded =
+ | MF_Field of FieldHandle
+ | MF_MethodDef of MethodDefHandle
+
+ member this.TableIndex =
+ match this with
+ | MF_Field _ -> 0x04
+ | MF_MethodDef _ -> 0x06
+
+ member this.RowId =
+ match this with
+ | MF_Field(FieldHandle rid) -> rid
+ | MF_MethodDef(MethodDefHandle rid) -> rid
+
+/// Implementation coded index (2-bit tag)
+/// Tag: File=0, AssemblyRef=1, ExportedType=2
+type Implementation =
+ | IMP_File of FileHandle
+ | IMP_AssemblyRef of AssemblyRefHandle
+ | IMP_ExportedType of ExportedTypeHandle
+
+ member this.TableIndex =
+ match this with
+ | IMP_File _ -> 0x26
+ | IMP_AssemblyRef _ -> 0x23
+ | IMP_ExportedType _ -> 0x27
+
+ member this.RowId =
+ match this with
+ | IMP_File(FileHandle rid) -> rid
+ | IMP_AssemblyRef(AssemblyRefHandle rid) -> rid
+ | IMP_ExportedType(ExportedTypeHandle rid) -> rid
+
+/// TypeOrMethodDef coded index (1-bit tag)
+/// Tag: TypeDef=0, MethodDef=1
+type TypeOrMethodDef =
+ | TOMD_TypeDef of TypeDefHandle
+ | TOMD_MethodDef of MethodDefHandle
+
+ member this.TableIndex =
+ match this with
+ | TOMD_TypeDef _ -> 0x02
+ | TOMD_MethodDef _ -> 0x06
+
+ member this.CodedTag =
+ match this with
+ | TOMD_TypeDef _ -> tomd_TypeDef.Tag
+ | TOMD_MethodDef _ -> tomd_MethodDef.Tag
+
+ member this.RowId =
+ match this with
+ | TOMD_TypeDef(TypeDefHandle rid) -> rid
+ | TOMD_MethodDef(MethodDefHandle rid) -> rid
+
+// ============================================================================
+// DeltaTokens Module
+// ============================================================================
+// Utilities for metadata token manipulation, replacing MetadataTokens static methods.
+
+/// Token arithmetic utilities (replaces System.Reflection.Metadata.Ecma335.MetadataTokens)
+module DeltaTokens =
+
+ /// Number of metadata tables defined in ECMA-335 (includes reserved slots)
+ let TableCount = 64
+
+ /// Extract the row number (lower 24 bits) from a metadata token
+ let getRowNumber (token: int) = token &&& 0x00FFFFFF
+
+ /// Extract the table index (upper 8 bits) from a metadata token
+ let getTableIndex (token: int) = (token >>> 24) &&& 0xFF
+
+ /// Create a metadata token from a TableName and row number.
+ /// Token format: [table index : 8 bits][row number : 24 bits]
+ /// Internal: TableName is from BinaryConstants which is internal.
+ let internal makeToken (table: TableName) (rowNumber: int) =
+ (table.Index <<< 24) ||| (rowNumber &&& 0x00FFFFFF)
+
+ /// Create a metadata token from a raw table index (int) and row number.
+ /// Use this for PDB tables which don't have TableName definitions,
+ /// or when calling from outside the compiler assembly.
+ let makeTokenFromIndex (tableIndex: int) (rowNumber: int) =
+ (tableIndex <<< 24) ||| (rowNumber &&& 0x00FFFFFF)
+
+ /// Create an EntityToken from a raw token value
+ let toEntityToken (token: int) : EntityToken =
+ {
+ TableIndex = getTableIndex token
+ RowId = getRowNumber token
+ }
+
+ /// Convert an EntityToken to a raw token value
+ let fromEntityToken (entity: EntityToken) : int = entity.Token
+
+ // -------------------------------------------------------------------------
+ // Portable PDB Table Indices (not part of ECMA-335, defined in Portable PDB spec)
+ // -------------------------------------------------------------------------
+ // These tables are used for debug information in Portable PDB format.
+ // They start at index 0x30 to avoid collision with ECMA-335 tables.
+ // Reference: https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md
+
+ let tableDocument = 0x30
+ let tableMethodDebugInformation = 0x31
+ let tableLocalScope = 0x32
+ let tableLocalVariable = 0x33
+ let tableLocalConstant = 0x34
+ let tableImportScope = 0x35
+ let tableStateMachineMethod = 0x36
+ let tableCustomDebugInformation = 0x37
+
+// ============================================================================
+// Conversion Helpers
+// ============================================================================
+// Functions to convert between F# handles and raw values
+
+module HandleConversions =
+ /// Create a HasCustomAttribute from table index and row ID
+ /// Returns None for invalid table indices
+ let tryMakeHasCustomAttribute (tableIndex: int) (rowId: int) : HasCustomAttribute option =
+ match tableIndex with
+ | 0x06 -> Some(HCA_MethodDef(MethodDefHandle rowId))
+ | 0x04 -> Some(HCA_Field(FieldHandle rowId))
+ | 0x01 -> Some(HCA_TypeRef(TypeRefHandle rowId))
+ | 0x02 -> Some(HCA_TypeDef(TypeDefHandle rowId))
+ | 0x08 -> Some(HCA_Param(ParamHandle rowId))
+ | 0x09 -> Some(HCA_InterfaceImpl(InterfaceImplHandle rowId))
+ | 0x0A -> Some(HCA_MemberRef(MemberRefHandle rowId))
+ | 0x00 -> Some(HCA_Module(ModuleHandle rowId))
+ | 0x0E -> Some(HCA_DeclSecurity(DeclSecurityHandle rowId))
+ | 0x17 -> Some(HCA_Property(PropertyHandle rowId))
+ | 0x14 -> Some(HCA_Event(EventHandle rowId))
+ | 0x11 -> Some(HCA_StandAloneSig(StandAloneSigHandle rowId))
+ | 0x1A -> Some(HCA_ModuleRef(ModuleRefHandle rowId))
+ | 0x1B -> Some(HCA_TypeSpec(TypeSpecHandle rowId))
+ | 0x20 -> Some(HCA_Assembly(AssemblyHandle rowId))
+ | 0x23 -> Some(HCA_AssemblyRef(AssemblyRefHandle rowId))
+ | 0x26 -> Some(HCA_File(FileHandle rowId))
+ | 0x27 -> Some(HCA_ExportedType(ExportedTypeHandle rowId))
+ | 0x28 -> Some(HCA_ManifestResource(ManifestResourceHandle rowId))
+ | 0x2A -> Some(HCA_GenericParam(GenericParamHandle rowId))
+ | 0x2C -> Some(HCA_GenericParamConstraint(GenericParamConstraintHandle rowId))
+ | 0x2B -> Some(HCA_MethodSpec(MethodSpecHandle rowId))
+ | _ -> None
+
+ /// Create a ResolutionScope from table index and row ID
+ let tryMakeResolutionScope (tableIndex: int) (rowId: int) : ResolutionScope option =
+ match tableIndex with
+ | 0x00 -> Some(RS_Module(ModuleHandle rowId))
+ | 0x1A -> Some(RS_ModuleRef(ModuleRefHandle rowId))
+ | 0x23 -> Some(RS_AssemblyRef(AssemblyRefHandle rowId))
+ | 0x01 -> Some(RS_TypeRef(TypeRefHandle rowId))
+ | _ -> None
+
+ /// Create a MemberRefParent from table index and row ID
+ let tryMakeMemberRefParent (tableIndex: int) (rowId: int) : MemberRefParent option =
+ match tableIndex with
+ | 0x02 -> Some(MRP_TypeDef(TypeDefHandle rowId))
+ | 0x01 -> Some(MRP_TypeRef(TypeRefHandle rowId))
+ | 0x1A -> Some(MRP_ModuleRef(ModuleRefHandle rowId))
+ | 0x06 -> Some(MRP_MethodDef(MethodDefHandle rowId))
+ | 0x1B -> Some(MRP_TypeSpec(TypeSpecHandle rowId))
+ | _ -> None
+
+ /// Create a CustomAttributeType from table index and row ID
+ let tryMakeCustomAttributeType (tableIndex: int) (rowId: int) : CustomAttributeType option =
+ match tableIndex with
+ | 0x06 -> Some(CAT_MethodDef(MethodDefHandle rowId))
+ | 0x0A -> Some(CAT_MemberRef(MemberRefHandle rowId))
+ | _ -> None
+
+ /// Create a TypeDefOrRef from table index and row ID
+ let tryMakeTypeDefOrRef (tableIndex: int) (rowId: int) : TypeDefOrRef option =
+ match tableIndex with
+ | 0x02 -> Some(TDR_TypeDef(TypeDefHandle rowId))
+ | 0x01 -> Some(TDR_TypeRef(TypeRefHandle rowId))
+ | 0x1B -> Some(TDR_TypeSpec(TypeSpecHandle rowId))
+ | _ -> None
+
+// ============================================================================
+// Edit-and-Continue Operation Codes
+// ============================================================================
+// F# native enum for EncLog operation codes.
+// Replaces System.Reflection.Metadata.Ecma335.EditAndContinueOperation.
+
+/// Operation code for EncLog entries per ECMA-335.
+/// Indicates whether a row is new (AddXxx) or an update (Default).
+[]
+type EditAndContinueOperation =
+ | Default
+ | AddMethod
+ | AddField
+ | AddParameter
+ | AddProperty
+ | AddEvent
+
+ /// Get the numeric value for serialization.
+ /// Values match the CLR EnC operation codes (and SRM's
+ /// System.Reflection.Metadata.Ecma335.EditAndContinueOperation):
+ /// Default=0, AddMethod=1, AddField=2, AddParameter=3, AddProperty=4, AddEvent=5.
+ member this.Value =
+ match this with
+ | Default -> 0
+ | AddMethod -> 1
+ | AddField -> 2
+ | AddParameter -> 3
+ | AddProperty -> 4
+ | AddEvent -> 5
+
+ override this.GetHashCode() = this.Value
+
+ override this.Equals obj =
+ match obj with
+ | :? EditAndContinueOperation as other -> this.Value = other.Value
+ | _ -> false
+
+ interface IEquatable with
+ member this.Equals other = this.Value = other.Value
+
+// ============================================================================
+// IL Exception Region Types
+// ============================================================================
+// These replace System.Reflection.Metadata.ExceptionRegion and ExceptionRegionKind
+
+/// Kind of exception handling region in IL method body
+type IlExceptionRegionKind =
+ | Catch = 0
+ | Filter = 1
+ | Finally = 2
+ | Fault = 4
+
+/// Exception handling region in IL method body.
+/// Replaces System.Reflection.Metadata.ExceptionRegion for delta emission.
+[]
+type IlExceptionRegion =
+ {
+ Kind: IlExceptionRegionKind
+ TryOffset: int
+ TryLength: int
+ HandlerOffset: int
+ HandlerLength: int
+ /// For Catch: the catch type token; for others: 0
+ CatchTypeToken: int
+ /// For Filter: the filter offset; for others: 0
+ FilterOffset: int
+ }
diff --git a/src/Compiler/AbstractIL/ILMetadataHeaps.fs b/src/Compiler/AbstractIL/ILMetadataHeaps.fs
new file mode 100644
index 00000000000..5c85fc57489
--- /dev/null
+++ b/src/Compiler/AbstractIL/ILMetadataHeaps.fs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+/// Abstractions for metadata heap indexing.
+/// 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.
+/// This interface allows both full assembly and delta emission to share
+/// the same heap access patterns while using different underlying storage.
+type IMetadataHeaps =
+ /// Get or add a string to the #Strings heap, returning the heap index.
+ /// Empty/null strings return 0.
+ abstract GetStringHeapIdx: string -> int
+
+ /// Get or add a byte array to the #Blob heap, returning the heap index.
+ /// Empty arrays return 0.
+ abstract GetBlobHeapIdx: byte[] -> int
+
+ /// Get or add a GUID to the #GUID heap, returning the 1-based index.
+ abstract GetGuidIdx: byte[] -> int
+
+ /// Get or add a string to the #US (User Strings) heap, returning the heap index.
+ abstract GetUserStringHeapIdx: string -> int
+
+/// Extension functions for IMetadataHeaps
+[]
+module MetadataHeapsExtensions =
+ type IMetadataHeaps with
+ /// Get string heap index for an optional string, returning 0 for None.
+ member this.GetStringHeapIdxOption(sopt: string option) =
+ match sopt with
+ | Some s -> this.GetStringHeapIdx s
+ | None -> 0
diff --git a/src/Compiler/AbstractIL/ilwrite.fs b/src/Compiler/AbstractIL/ilwrite.fs
index 13feeab294a..f848e12ebc3 100644
--- a/src/Compiler/AbstractIL/ilwrite.fs
+++ b/src/Compiler/AbstractIL/ilwrite.fs
@@ -11,6 +11,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
@@ -162,59 +163,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
[]
@@ -242,33 +243,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
@@ -361,57 +362,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
@@ -504,11 +503,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
@@ -652,6 +651,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
@@ -1115,7 +1129,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
@@ -1129,7 +1143,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:")
@@ -2477,6 +2491,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
// --------------------------------------------------------------------
@@ -2672,31 +2704,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 _ ->
@@ -3272,7 +3304,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.
@@ -3676,7 +3711,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
@@ -3859,9 +3909,22 @@ type options =
referenceAssemblyOnly: bool
referenceAssemblyAttribOpt: ILAttribute option
referenceAssemblySignatureHash : int option
- pathMap: PathMap }
-
-let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRefs) =
+ pathMap: PathMap
+ // 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
+ // 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 }
+
+///
+/// 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
@@ -3974,22 +4037,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
@@ -4022,7 +4091,15 @@ let writeBinaryAux (stream: Stream, options: options, modul, normalizeAssemblyRe
match options.pdbfile, options.portablePDB with
| Some _, true ->
let pdbInfo =
- generatePortablePdb options.embedAllSource options.embedSourceList options.sourceLink options.checksumAlgorithm pdbData options.pathMap
+ generatePortablePdb
+ options.embedAllSource
+ options.embedSourceList
+ options.sourceLink
+ options.checksumAlgorithm
+ pdbData
+ options.pathMap
+ options.moduleCustomDebugInfoRows
+ options.methodCustomDebugInfoRows
if options.embeddedPDB then
let uncompressedLength, contentId, stream, algorithmName, checkSum = pdbInfo
@@ -4564,6 +4641,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 =
@@ -4609,12 +4689,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
@@ -4640,12 +4728,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 d074f0bc584..7af65c95c41 100644
--- a/src/Compiler/AbstractIL/ilwrite.fsi
+++ b/src/Compiler/AbstractIL/ilwrite.fsi
@@ -9,24 +9,69 @@ open FSharp.Compiler.AbstractIL.ILPdbWriter
open FSharp.Compiler.AbstractIL.StrongNameSign
type options =
- { ilg: ILGlobals
- outfile: string
- pdbfile: string option
- portablePDB: bool
- embeddedPDB: bool
- embedAllSource: bool
- embedSourceList: string list
- allGivenSources: ILSourceDocument list
- sourceLink: string
- checksumAlgorithm: HashAlgorithm
- signer: ILStrongNameSigner option
- emitTailcalls: bool
- deterministic: bool
- dumpDebugInfo: bool
- referenceAssemblyOnly: bool
- referenceAssemblyAttribOpt: ILAttribute option
- referenceAssemblySignatureHash: int option
- pathMap: PathMap }
+ {
+ ilg: ILGlobals
+ outfile: string
+ pdbfile: string option
+ portablePDB: bool
+ embeddedPDB: bool
+ embedAllSource: bool
+ embedSourceList: string list
+ allGivenSources: ILSourceDocument list
+ sourceLink: string
+ checksumAlgorithm: HashAlgorithm
+ signer: ILStrongNameSigner option
+ emitTailcalls: bool
+ deterministic: bool
+ dumpDebugInfo: bool
+ referenceAssemblyOnly: bool
+ 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 unless a gated hot reload capture
+ /// compile needs to persist extra deterministic state.
+ moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list
+ /// 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
/// Write a binary to the file system.
val WriteILBinaryFile: options: options * inputModule: ILModuleDef * (ILAssemblyRef -> ILAssemblyRef) -> unit
@@ -34,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 86a19d50c6c..b3234e0c25c 100644
--- a/src/Compiler/AbstractIL/ilwritepdb.fs
+++ b/src/Compiler/AbstractIL/ilwritepdb.fs
@@ -118,6 +118,14 @@ type PdbMethodData =
DebugPoints: PdbDebugPoint array
}
+/// A pre-serialized CustomDebugInformation row (kind GUID + blob) to attach to a method
+/// definition row in the portable PDB.
+type PdbMethodCustomDebugInfo = { KindGuid: Guid; Blob: byte[] }
+
+/// A pre-serialized CustomDebugInformation row (kind GUID + blob) to attach to the
+/// module definition row in the portable PDB.
+type PdbModuleCustomDebugInfo = { KindGuid: Guid; Blob: byte[] }
+
module SequencePoint =
let orderBySource sp1 sp2 =
let c1 = compare sp1.Document sp2.Document
@@ -163,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)
@@ -337,7 +363,16 @@ let scopeSorter (scope1: PdbMethodScope) (scope2: PdbMethodScope) =
0
type PortablePdbGenerator
- (embedAllSource: bool, embedSourceList: string list, sourceLink: string, checksumAlgorithm, info: PdbData, pathMap: PathMap) =
+ (
+ embedAllSource: bool,
+ embedSourceList: string list,
+ sourceLink: string,
+ checksumAlgorithm,
+ info: PdbData,
+ pathMap: PathMap,
+ moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list,
+ methodCustomDebugInfoRows: Map
+ ) =
// Deterministic: build the Document table in a stable order by mapped file path,
// but preserve the original-document-index -> handle mapping by filename.
@@ -369,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.
@@ -472,6 +502,14 @@ type PortablePdbGenerator
)
|> ignore
+ for cdiRow in moduleCustomDebugInfoRows |> List.sortBy (fun row -> row.KindGuid) do
+ metadata.AddCustomDebugInformation(
+ ModuleDefinitionHandle.op_Implicit EntityHandle.ModuleDefinition,
+ metadata.GetOrAddGuid cdiRow.KindGuid,
+ metadata.GetOrAddBlob cdiRow.Blob
+ )
+ |> ignore
+
index
let mutable lastLocalVariableHandle = Unchecked.defaultof
@@ -488,6 +526,27 @@ type PortablePdbGenerator
let moduleImportScopeHandle = MetadataTokens.ImportScopeHandle(1)
let importScopesTable = Dictionary()
+ // Per-method CustomDebugInformation rows keyed by IL method name. Names that match
+ // more than one method row (overloads, same name on different types) fail closed and
+ // attach nothing, so a row can never land on the wrong method.
+ let methodCustomDebugInfoByName =
+ if Map.isEmpty methodCustomDebugInfoRows then
+ methodCustomDebugInfoRows
+ else
+ let nameCounts = Dictionary()
+
+ for minfo in info.Methods do
+ nameCounts[minfo.MethName] <-
+ match nameCounts.TryGetValue minfo.MethName with
+ | true, count -> count + 1
+ | _ -> 1
+
+ methodCustomDebugInfoRows
+ |> Map.filter (fun methName _ ->
+ match nameCounts.TryGetValue methName with
+ | true, 1 -> true
+ | _ -> false)
+
let serializeImport (writer: BlobBuilder) (import: PdbImport) =
match import with
// We don't yet emit these kinds of imports
@@ -777,6 +836,23 @@ type PortablePdbGenerator
metadata.AddMethodDebugInformation(docHandle, sequencePointBlob) |> ignore
+ // MetadataBuilder sorts the CustomDebugInformation table by parent at serialize
+ // time, so adding rows in method order here is safe.
+ match Map.tryFind minfo.MethName methodCustomDebugInfoByName with
+ | Some cdiRows ->
+ // MethToken is the uncoded token (0x06 <<< 24 ||| rid); the handle needs the rid.
+ let methodHandle =
+ MetadataTokens.MethodDefinitionHandle(minfo.MethToken &&& 0x00FFFFFF)
+
+ for cdiRow in cdiRows do
+ metadata.AddCustomDebugInformation(
+ MethodDefinitionHandle.op_Implicit methodHandle,
+ metadata.GetOrAddGuid cdiRow.KindGuid,
+ metadata.GetOrAddBlob cdiRow.Blob
+ )
+ |> ignore
+ | None -> ()
+
match minfo.RootScope with
| None -> ()
| Some scope -> writeMethodScopes minfo.MethToken scope
@@ -831,9 +907,20 @@ let generatePortablePdb
checksumAlgorithm
(info: PdbData)
(pathMap: PathMap)
+ (moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list)
+ (methodCustomDebugInfoRows: Map)
=
let generator =
- PortablePdbGenerator(embedAllSource, embedSourceList, sourceLink, checksumAlgorithm, info, pathMap)
+ PortablePdbGenerator(
+ embedAllSource,
+ embedSourceList,
+ sourceLink,
+ checksumAlgorithm,
+ info,
+ pathMap,
+ moduleCustomDebugInfoRows,
+ methodCustomDebugInfoRows
+ )
generator.Emit()
diff --git a/src/Compiler/AbstractIL/ilwritepdb.fsi b/src/Compiler/AbstractIL/ilwritepdb.fsi
index 5987cc165e3..508b892b813 100644
--- a/src/Compiler/AbstractIL/ilwritepdb.fsi
+++ b/src/Compiler/AbstractIL/ilwritepdb.fsi
@@ -67,6 +67,18 @@ type PdbMethodData =
DebugRange: (PdbSourceLoc * PdbSourceLoc) option
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 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
+/// in the portable PDB (kind GUID + blob). Supplied by hot reload for F#-owned
+/// deterministic baseline records.
+type PdbModuleCustomDebugInfo = { KindGuid: System.Guid; Blob: byte[] }
+
[]
type PdbData =
{
@@ -109,6 +121,8 @@ val generatePortablePdb:
checksumAlgorithm: HashAlgorithm ->
info: PdbData ->
pathMap: PathMap ->
+ moduleCustomDebugInfoRows: PdbModuleCustomDebugInfo list ->
+ methodCustomDebugInfoRows: Map ->
int64 * BlobContentId * MemoryStream * string * byte[]
val compressPortablePdbStream: stream: MemoryStream -> MemoryStream
diff --git a/src/Compiler/CodeGen/DeltaIndexSizing.fs b/src/Compiler/CodeGen/DeltaIndexSizing.fs
new file mode 100644
index 00000000000..ca56f5d0af2
--- /dev/null
+++ b/src/Compiler/CodeGen/DeltaIndexSizing.fs
@@ -0,0 +1,186 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+/// Computes coded index sizing for delta metadata emission.
+///
+/// This module determines whether various metadata indices require 2 or 4 bytes
+/// based on row counts in the metadata tables. This is per ECMA-335 II.24.2.6.
+///
+/// Uses TableNames from BinaryConstants.fs for ECMA-335 metadata table indices,
+/// following the same pattern as the baseline IL writer (ilwrite.fs).
+module internal FSharp.Compiler.CodeGen.DeltaIndexSizing
+
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.CodeGen.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 =
+ {
+ StringsBig: bool
+ GuidsBig: bool
+ BlobsBig: bool
+ SimpleIndexBig: bool[]
+ TypeDefOrRefBig: bool
+ TypeOrMethodDefBig: bool
+ HasConstantBig: bool
+ HasCustomAttributeBig: bool
+ HasFieldMarshalBig: bool
+ HasDeclSecurityBig: bool
+ MemberRefParentBig: bool
+ HasSemanticsBig: bool
+ MethodDefOrRefBig: bool
+ MemberForwardedBig: bool
+ ImplementationBig: bool
+ CustomAttributeTypeBig: bool
+ ResolutionScopeBig: bool
+ }
+
+let private tableSize (tableRowCounts: int[]) (table: int) = tableRowCounts.[table]
+
+let private totalRowCount (tableRowCounts: int[]) (externalRowCounts: int[]) (table: int) =
+ let index = table
+
+ let external =
+ if externalRowCounts.Length = tableRowCounts.Length then
+ externalRowCounts.[index]
+ else
+ 0
+
+ tableRowCounts.[index] + external
+
+let private referenceExceedsLimit (tableRowCounts: int[]) (externalRowCounts: int[]) (maxValueExclusive: int) (tables: int[]) =
+ tables
+ |> Array.exists (fun table -> totalRowCount tableRowCounts externalRowCounts table >= maxValueExclusive)
+
+/// Determines if a coded index requires 4 bytes (big) or 2 bytes (small).
+/// For EnC deltas (uncompressed), all indices are 4 bytes.
+/// For compressed metadata, size depends on whether any referenced table
+/// has enough rows to overflow the available bits after the tag.
+let private codedBigness (tagBits: int) (tableRowCounts: int[]) (externalRowCounts: int[]) (isCompressed: bool) (tables: int[]) =
+ if not isCompressed then
+ // EnC deltas always use 4-byte indices
+ true
+ else
+ let limit = pown 2 (16 - tagBits)
+ referenceExceedsLimit tableRowCounts externalRowCounts limit tables
+
+let private isSimpleIndexBig (tableRowCounts: int[]) (externalRowCounts: int[]) (isCompressed: bool) (tableIndex: int) =
+ if not isCompressed then
+ true
+ else
+ let local =
+ if tableIndex < tableRowCounts.Length then
+ tableRowCounts.[tableIndex]
+ else
+ 0
+
+ let external =
+ if tableIndex < externalRowCounts.Length then
+ externalRowCounts.[tableIndex]
+ else
+ 0
+
+ local + external >= 0x10000
+
+/// Compute coded index sizes for all index types.
+/// This determines the byte width of each reference type in the metadata tables.
+let compute (tableRowCounts: int[]) (externalRowCounts: int[]) (heapSizes: MetadataHeapSizes) (isEncDelta: bool) : CodedIndexSizes =
+
+ let isCompressed = not isEncDelta
+
+ // Heap indices: 4 bytes if uncompressed or heap >= 64KB
+ let stringsBig = (not isCompressed) || heapSizes.StringHeapSize >= 0x10000
+ let blobsBig = (not isCompressed) || heapSizes.BlobHeapSize >= 0x10000
+ let guidsBig = (not isCompressed) || heapSizes.GuidHeapSize >= 0x10000
+
+ // Simple table indices
+ let simpleIndexBig =
+ Array.init DeltaTokens.TableCount (fun i -> isSimpleIndexBig tableRowCounts externalRowCounts isCompressed i)
+
+ // Helper to compute coded index bigness for a set of tables
+ let coded tag tables =
+ codedBigness tag tableRowCounts externalRowCounts isCompressed tables
+
+ // -------------------------------------------------------------------------
+ // Coded Index Definitions (per ECMA-335 II.24.2.6)
+ // -------------------------------------------------------------------------
+ // Each coded index combines a tag (to identify which table) with a row index.
+ // The tag uses the low N bits; the row index uses the remaining bits.
+ // If any table in the coded index exceeds (2^(16-N) - 1) rows, we need 4 bytes.
+
+ // TypeDefOrRef: TypeDef(0), TypeRef(1), TypeSpec(2) - 2-bit tag
+ let typeDefOrRefBig =
+ coded CodedIndices.TypeDefOrRef.TagBits CodedIndices.TypeDefOrRef.Tables
+
+ // TypeOrMethodDef: TypeDef(0), MethodDef(1) - 1-bit tag
+ let typeOrMethodDefBig =
+ coded CodedIndices.TypeOrMethodDef.TagBits CodedIndices.TypeOrMethodDef.Tables
+
+ // HasConstant: Field(0), Param(1), Property(2) - 2-bit tag
+ let hasConstantBig =
+ coded CodedIndices.HasConstant.TagBits CodedIndices.HasConstant.Tables
+
+ // HasCustomAttribute: 22 possible parent types - 5-bit tag
+ // This is the largest coded index, covering most metadata entities
+ let hasCustomAttributeBig =
+ coded CodedIndices.HasCustomAttribute.TagBits CodedIndices.HasCustomAttribute.Tables
+
+ // HasFieldMarshal: Field(0), Param(1) - 1-bit tag
+ let hasFieldMarshalBig =
+ coded CodedIndices.HasFieldMarshal.TagBits CodedIndices.HasFieldMarshal.Tables
+
+ // HasDeclSecurity: TypeDef(0), MethodDef(1), Assembly(2) - 2-bit tag
+ let hasDeclSecurityBig =
+ coded CodedIndices.HasDeclSecurity.TagBits CodedIndices.HasDeclSecurity.Tables
+
+ // MemberRefParent: TypeDef(0), TypeRef(1), ModuleRef(2), MethodDef(3), TypeSpec(4) - 3-bit tag
+ let memberRefParentBig =
+ coded CodedIndices.MemberRefParent.TagBits CodedIndices.MemberRefParent.Tables
+
+ // HasSemantics: Event(0), Property(1) - 1-bit tag
+ let hasSemanticsBig =
+ coded CodedIndices.HasSemantics.TagBits CodedIndices.HasSemantics.Tables
+
+ // MethodDefOrRef: MethodDef(0), MemberRef(1) - 1-bit tag
+ let methodDefOrRefBig =
+ coded CodedIndices.MethodDefOrRef.TagBits CodedIndices.MethodDefOrRef.Tables
+
+ // MemberForwarded: Field(0), MethodDef(1) - 1-bit tag
+ let memberForwardedBig =
+ coded CodedIndices.MemberForwarded.TagBits CodedIndices.MemberForwarded.Tables
+
+ // Implementation: File(0), AssemblyRef(1), ExportedType(2) - 2-bit tag
+ let implementationBig =
+ coded CodedIndices.Implementation.TagBits CodedIndices.Implementation.Tables
+
+ // CustomAttributeType: MethodDef(2), MemberRef(3) - 3-bit tag
+ // Note: tags 0, 1, 4 are reserved/unused
+ let customAttributeTypeBig =
+ coded CodedIndices.CustomAttributeType.TagBits CodedIndices.CustomAttributeType.Tables
+
+ // ResolutionScope: Module(0), ModuleRef(1), AssemblyRef(2), TypeRef(3) - 2-bit tag
+ let resolutionScopeBig =
+ coded CodedIndices.ResolutionScope.TagBits CodedIndices.ResolutionScope.Tables
+
+ {
+ StringsBig = stringsBig
+ GuidsBig = guidsBig
+ BlobsBig = blobsBig
+ SimpleIndexBig = simpleIndexBig
+ TypeDefOrRefBig = typeDefOrRefBig
+ TypeOrMethodDefBig = typeOrMethodDefBig
+ HasConstantBig = hasConstantBig
+ HasCustomAttributeBig = hasCustomAttributeBig
+ HasFieldMarshalBig = hasFieldMarshalBig
+ HasDeclSecurityBig = hasDeclSecurityBig
+ MemberRefParentBig = memberRefParentBig
+ HasSemanticsBig = hasSemanticsBig
+ MethodDefOrRefBig = methodDefOrRefBig
+ MemberForwardedBig = memberForwardedBig
+ ImplementationBig = implementationBig
+ CustomAttributeTypeBig = customAttributeTypeBig
+ ResolutionScopeBig = resolutionScopeBig
+ }
diff --git a/src/Compiler/CodeGen/DeltaMetadataEncoding.fs b/src/Compiler/CodeGen/DeltaMetadataEncoding.fs
new file mode 100644
index 00000000000..86f76df0a1d
--- /dev/null
+++ b/src/Compiler/CodeGen/DeltaMetadataEncoding.fs
@@ -0,0 +1,289 @@
+module internal FSharp.Compiler.CodeGen.DeltaMetadataEncoding
+
+open FSharp.Compiler.AbstractIL.BinaryConstants
+
+/// Encodes row-element tags for delta table rows.
+/// This stays hot-reload-owned so delta serialization can evolve without expanding ilwrite.fsi.
+module RowElementTags =
+ []
+ let UShort = 0
+
+ []
+ let ULong = 1
+
+ []
+ let Data = 2
+
+ []
+ let DataResources = 3
+
+ []
+ let Guid = 4
+
+ []
+ let Blob = 5
+
+ []
+ let String = 6
+
+ []
+ let SimpleIndexMin = 7
+
+ []
+ let SimpleIndexMax = 119
+
+ let SimpleIndex (table: TableName) = SimpleIndexMin + table.Index
+
+ []
+ let TypeDefOrRefOrSpecMin = 120
+
+ []
+ let TypeDefOrRefOrSpecMax = 122
+
+ let TypeDefOrRefOrSpec (tag: TypeDefOrRefTag) = TypeDefOrRefOrSpecMin + int tag.Tag
+
+ []
+ let TypeOrMethodDefMin = 123
+
+ []
+ let TypeOrMethodDefMax = 124
+
+ let TypeOrMethodDef (tag: TypeOrMethodDefTag) = TypeOrMethodDefMin + int tag.Tag
+
+ []
+ let HasConstantMin = 125
+
+ []
+ let HasConstantMax = 127
+
+ let HasConstant (tag: HasConstantTag) = HasConstantMin + int tag.Tag
+
+ []
+ let HasCustomAttributeMin = 128
+
+ []
+ let HasCustomAttributeMax = 149
+
+ let HasCustomAttribute (tag: HasCustomAttributeTag) = HasCustomAttributeMin + int tag.Tag
+
+ []
+ let HasFieldMarshalMin = 150
+
+ []
+ let HasFieldMarshalMax = 151
+
+ let HasFieldMarshal (tag: HasFieldMarshalTag) = HasFieldMarshalMin + int tag.Tag
+
+ []
+ let HasDeclSecurityMin = 152
+
+ []
+ let HasDeclSecurityMax = 154
+
+ let HasDeclSecurity (tag: HasDeclSecurityTag) = HasDeclSecurityMin + int tag.Tag
+
+ []
+ let MemberRefParentMin = 155
+
+ []
+ let MemberRefParentMax = 159
+
+ let MemberRefParent (tag: MemberRefParentTag) = MemberRefParentMin + int tag.Tag
+
+ []
+ let HasSemanticsMin = 160
+
+ []
+ let HasSemanticsMax = 161
+
+ let HasSemantics (tag: HasSemanticsTag) = HasSemanticsMin + int tag.Tag
+
+ []
+ let MethodDefOrRefMin = 162
+
+ []
+ let MethodDefOrRefMax = 164
+
+ let MethodDefOrRef (tag: MethodDefOrRefTag) = MethodDefOrRefMin + int tag.Tag
+
+ []
+ let MemberForwardedMin = 165
+
+ []
+ let MemberForwardedMax = 166
+
+ let MemberForwarded (tag: MemberForwardedTag) = MemberForwardedMin + int tag.Tag
+
+ []
+ let ImplementationMin = 167
+
+ []
+ let ImplementationMax = 169
+
+ let Implementation (tag: ImplementationTag) = ImplementationMin + int tag.Tag
+
+ []
+ let CustomAttributeTypeMin = 170
+
+ []
+ let CustomAttributeTypeMax = 173
+
+ let CustomAttributeType (tag: CustomAttributeTypeTag) = CustomAttributeTypeMin + int tag.Tag
+
+ []
+ let ResolutionScopeMin = 174
+
+ []
+ let ResolutionScopeMax = 178
+
+ let ResolutionScope (tag: ResolutionScopeTag) = ResolutionScopeMin + int tag.Tag
+
+type CodedIndexDefinition = { TagBits: int; Tables: int[] }
+
+/// Canonical coded-index table orders for hot reload metadata sizing and serialization.
+module CodedIndices =
+ /// TypeDef(0), TypeRef(1), TypeSpec(2)
+ let TypeDefOrRef =
+ {
+ TagBits = 2
+ Tables =
+ [|
+ TableNames.TypeDef.Index
+ TableNames.TypeRef.Index
+ TableNames.TypeSpec.Index
+ |]
+ }
+
+ /// TypeDef(0), MethodDef(1)
+ let TypeOrMethodDef =
+ {
+ TagBits = 1
+ Tables = [| TableNames.TypeDef.Index; TableNames.Method.Index |]
+ }
+
+ /// Field(0), Param(1), Property(2)
+ let HasConstant =
+ {
+ TagBits = 2
+ Tables = [| TableNames.Field.Index; TableNames.Param.Index; TableNames.Property.Index |]
+ }
+
+ /// MethodDef(0), Field(1), TypeRef(2), TypeDef(3), Param(4), InterfaceImpl(5),
+ /// MemberRef(6), Module(7), DeclSecurity(8), Property(9), Event(10), StandAloneSig(11),
+ /// ModuleRef(12), TypeSpec(13), Assembly(14), AssemblyRef(15), File(16),
+ /// ExportedType(17), ManifestResource(18), GenericParam(19), GenericParamConstraint(20), MethodSpec(21)
+ let HasCustomAttribute =
+ {
+ TagBits = 5
+ Tables =
+ [|
+ TableNames.Method.Index
+ TableNames.Field.Index
+ TableNames.TypeRef.Index
+ TableNames.TypeDef.Index
+ TableNames.Param.Index
+ TableNames.InterfaceImpl.Index
+ TableNames.MemberRef.Index
+ TableNames.Module.Index
+ TableNames.Permission.Index
+ TableNames.Property.Index
+ TableNames.Event.Index
+ TableNames.StandAloneSig.Index
+ TableNames.ModuleRef.Index
+ TableNames.TypeSpec.Index
+ TableNames.Assembly.Index
+ TableNames.AssemblyRef.Index
+ TableNames.File.Index
+ TableNames.ExportedType.Index
+ TableNames.ManifestResource.Index
+ TableNames.GenericParam.Index
+ TableNames.GenericParamConstraint.Index
+ TableNames.MethodSpec.Index
+ |]
+ }
+
+ /// Field(0), Param(1)
+ let HasFieldMarshal =
+ {
+ TagBits = 1
+ Tables = [| TableNames.Field.Index; TableNames.Param.Index |]
+ }
+
+ /// TypeDef(0), MethodDef(1), Assembly(2)
+ let HasDeclSecurity =
+ {
+ TagBits = 2
+ Tables =
+ [|
+ TableNames.TypeDef.Index
+ TableNames.Method.Index
+ TableNames.Assembly.Index
+ |]
+ }
+
+ /// TypeDef(0), TypeRef(1), ModuleRef(2), MethodDef(3), TypeSpec(4)
+ let MemberRefParent =
+ {
+ TagBits = 3
+ Tables =
+ [|
+ TableNames.TypeDef.Index
+ TableNames.TypeRef.Index
+ TableNames.ModuleRef.Index
+ TableNames.Method.Index
+ TableNames.TypeSpec.Index
+ |]
+ }
+
+ /// Event(0), Property(1)
+ let HasSemantics =
+ {
+ TagBits = 1
+ Tables = [| TableNames.Event.Index; TableNames.Property.Index |]
+ }
+
+ /// MethodDef(0), MemberRef(1)
+ let MethodDefOrRef =
+ {
+ TagBits = 1
+ Tables = [| TableNames.Method.Index; TableNames.MemberRef.Index |]
+ }
+
+ /// Field(0), MethodDef(1)
+ let MemberForwarded =
+ {
+ TagBits = 1
+ Tables = [| TableNames.Field.Index; TableNames.Method.Index |]
+ }
+
+ /// File(0), AssemblyRef(1), ExportedType(2)
+ let Implementation =
+ {
+ TagBits = 2
+ Tables =
+ [|
+ TableNames.File.Index
+ TableNames.AssemblyRef.Index
+ TableNames.ExportedType.Index
+ |]
+ }
+
+ /// MethodDef(2), MemberRef(3)
+ let CustomAttributeType =
+ {
+ TagBits = 3
+ Tables = [| TableNames.Method.Index; TableNames.MemberRef.Index |]
+ }
+
+ /// Module(0), ModuleRef(1), AssemblyRef(2), TypeRef(3)
+ let ResolutionScope =
+ {
+ TagBits = 2
+ Tables =
+ [|
+ TableNames.Module.Index
+ TableNames.ModuleRef.Index
+ TableNames.AssemblyRef.Index
+ TableNames.TypeRef.Index
+ |]
+ }
diff --git a/src/Compiler/CodeGen/DeltaMetadataSerializer.fs b/src/Compiler/CodeGen/DeltaMetadataSerializer.fs
new file mode 100644
index 00000000000..eea159d329d
--- /dev/null
+++ b/src/Compiler/CodeGen/DeltaMetadataSerializer.fs
@@ -0,0 +1,487 @@
+module internal FSharp.Compiler.CodeGen.DeltaMetadataSerializer
+
+open System
+open System.Collections.Generic
+open System.IO
+open System.Text
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.CodeGen.DeltaMetadataTables
+open FSharp.Compiler.CodeGen.DeltaMetadataTypes
+open FSharp.Compiler.CodeGen.DeltaTableLayout
+
+module Encoding = FSharp.Compiler.CodeGen.DeltaMetadataEncoding
+
+let private padTo4 (bytes: byte[]) =
+ if bytes.Length % 4 = 0 then
+ bytes
+ else
+ let padded = Array.zeroCreate (bytes.Length + (4 - (bytes.Length % 4)))
+ Array.Copy(bytes, padded, bytes.Length)
+ padded
+
+/// Represents the aligned heap streams that will be written into the delta metadata.
+type DeltaHeapStreams =
+ {
+ Strings: byte[]
+ StringsLength: int
+ Blobs: byte[]
+ BlobsLength: int
+ Guids: byte[]
+ GuidsLength: int
+ UserStrings: byte[]
+ UserStringsLength: int
+ }
+
+let buildHeapStreams (mirror: DeltaMetadataTables) : DeltaHeapStreams =
+ let stringBytes = mirror.StringHeapBytes
+ let blobBytes = mirror.BlobHeapBytes
+ let guidBytes = mirror.GuidHeapBytes
+ let userStringBytes = mirror.UserStringHeapBytes
+
+ // Per Roslyn DeltaMetadataWriter.cs:234-241 and SRM MetadataBuilder.cs:86-89:
+ // - Stream header Size fields use GetAlignedHeapSize (aligned to 4 bytes)
+ // - String heap cumulative tracking uses unaligned HeapSizes
+ // - Blob/UserString heap cumulative tracking uses aligned sizes
+ // The Length fields become stream header Size values, which must match
+ // the actual padded byte array lengths for correct runtime parsing.
+ let paddedStrings = padTo4 stringBytes
+ let paddedBlobs = padTo4 blobBytes
+ let paddedGuids = padTo4 guidBytes
+ let paddedUserStrings = padTo4 userStringBytes
+
+ {
+ Strings = paddedStrings
+ StringsLength = paddedStrings.Length // Stream header uses padded size
+ Blobs = paddedBlobs
+ BlobsLength = paddedBlobs.Length // Stream header uses padded size
+ Guids = paddedGuids
+ GuidsLength = paddedGuids.Length // Stream header uses padded size
+ UserStrings = paddedUserStrings
+ UserStringsLength = paddedUserStrings.Length
+ } // Stream header uses padded size
+
+/// Represents the serialized `#~` stream (metadata tables) including its padded bytes.
+type DeltaTableStream =
+ {
+ Bytes: byte[]
+ UnpaddedSize: int
+ PaddedSize: int
+ }
+
+/// Captures the sizing data needed to build delta metadata, mirroring Roslyn's MetadataSizes.
+type DeltaMetadataSizes =
+ {
+ RowCounts: int[]
+ HeapSizes: MetadataHeapSizes
+ BitMasks: TableBitMasks
+ IndexSizes: DeltaIndexSizing.CodedIndexSizes
+ IsEncDelta: bool
+ }
+
+/// Compute sizing information needed for delta serialization.
+/// This determines index widths, heap sizes, and bit masks for the #~ stream header.
+let computeMetadataSizes (tableMirror: DeltaMetadataTables) (externalRowCounts: int[]) : DeltaMetadataSizes =
+ let normalizedExternal =
+ if externalRowCounts.Length = DeltaTokens.TableCount then
+ externalRowCounts
+ else
+ Array.zeroCreate DeltaTokens.TableCount
+
+ let rowCounts = tableMirror.TableRowCounts
+ let heapSizes = tableMirror.HeapSizes
+ // A delta is an EnC delta if it contains EncLog or EncMap entries
+ let isEncDelta =
+ rowCounts[TableNames.ENCLog.Index] > 0 || rowCounts[TableNames.ENCMap.Index] > 0
+
+ let bitMasks = DeltaTableLayout.computeBitMasks rowCounts isEncDelta
+
+ let indexSizes =
+ DeltaIndexSizing.compute rowCounts normalizedExternal heapSizes isEncDelta
+
+ {
+ RowCounts = rowCounts
+ HeapSizes = heapSizes
+ BitMasks = bitMasks
+ IndexSizes = indexSizes
+ IsEncDelta = isEncDelta
+ }
+
+type DeltaTableSerializerInput =
+ {
+ Tables: TableRows
+ MetadataSizes: DeltaMetadataSizes
+ StringHeap: byte[]
+ StringHeapOffsets: int[]
+ BlobHeap: byte[]
+ BlobHeapOffsets: int[]
+ GuidHeap: byte[]
+ HeapOffsets: MetadataHeapOffsets
+ }
+
+let private writeUInt16 (writer: BinaryWriter) (value: int) = writer.Write(uint16 value)
+
+let private writeUInt32 (writer: BinaryWriter) (value: int) = writer.Write(value)
+
+let private writeHeapIndex (writer: BinaryWriter) (isBig: bool) (value: int) =
+ if isBig then
+ writeUInt32 writer value
+ else
+ writeUInt16 writer value
+
+let private writeTaggedIndex (writer: BinaryWriter) (nbits: int) (isBig: bool) (tag: int) (value: int) =
+ let encoded = (value <<< nbits) ||| tag
+
+ if isBig then
+ writeUInt32 writer encoded
+ else
+ writeUInt16 writer encoded
+
+/// Maps TableRows to an array indexed by ECMA-335 table number.
+/// Uses TableNames from BinaryConstants for proper table indices.
+let private tableRowsByIndex (tables: TableRows) =
+ let rows = Array.create DeltaTokens.TableCount Array.empty
+ rows[TableNames.Module.Index] <- tables.Module
+ rows[TableNames.TypeDef.Index] <- tables.TypeDef
+ rows[TableNames.Nested.Index] <- tables.NestedClass
+ rows[TableNames.InterfaceImpl.Index] <- tables.InterfaceImpl
+ rows[TableNames.Constant.Index] <- tables.Constant
+ rows[TableNames.MethodImpl.Index] <- tables.MethodImpl
+ rows[TableNames.Field.Index] <- tables.Field
+ rows[TableNames.Method.Index] <- tables.MethodDef
+ rows[TableNames.Param.Index] <- tables.Param
+ rows[TableNames.TypeRef.Index] <- tables.TypeRef
+ rows[TableNames.MemberRef.Index] <- tables.MemberRef
+ rows[TableNames.MethodSpec.Index] <- tables.MethodSpec
+ rows[TableNames.TypeSpec.Index] <- tables.TypeSpec
+ rows[TableNames.GenericParam.Index] <- tables.GenericParam
+ rows[TableNames.GenericParamConstraint.Index] <- tables.GenericParamConstraint
+ rows[TableNames.CustomAttribute.Index] <- tables.CustomAttribute
+ rows[TableNames.AssemblyRef.Index] <- tables.AssemblyRef
+ rows[TableNames.StandAloneSig.Index] <- tables.StandAloneSig
+ rows[TableNames.Property.Index] <- tables.Property
+ rows[TableNames.Event.Index] <- tables.Event
+ rows[TableNames.PropertyMap.Index] <- tables.PropertyMap
+ rows[TableNames.EventMap.Index] <- tables.EventMap
+ rows[TableNames.MethodSemantics.Index] <- tables.MethodSemantics
+ rows[TableNames.ENCLog.Index] <- tables.EncLog
+ rows[TableNames.ENCMap.Index] <- tables.EncMap
+ rows
+
+let private isTablePresent (bitmaskLow: int) (bitmaskHigh: int) (index: int) =
+ if index < 32 then
+ ((bitmaskLow >>> index) &&& 1) <> 0
+ else
+ ((bitmaskHigh >>> (index - 32)) &&& 1) <> 0
+
+let private writeRowElement
+ (writer: BinaryWriter)
+ (indexSizes: DeltaIndexSizing.CodedIndexSizes)
+ (input: DeltaTableSerializerInput)
+ (element: RowElementData)
+ =
+ let tag = element.Tag
+ let value = element.Value
+
+ if tag = Encoding.RowElementTags.UShort then
+ writeUInt16 writer value
+ elif tag = Encoding.RowElementTags.ULong then
+ writeUInt32 writer value
+ elif tag = Encoding.RowElementTags.String then
+ let offset =
+ if element.IsAbsolute then
+ value
+ elif value = 0 then
+ 0
+ elif value < 0 || value >= input.StringHeapOffsets.Length then
+ invalidArg "element" $"String heap offset index out of range: {value} (offsetCount={input.StringHeapOffsets.Length})"
+ else
+ input.HeapOffsets.StringHeapStart + input.StringHeapOffsets.[value]
+
+ writeHeapIndex writer indexSizes.StringsBig offset
+ elif tag = Encoding.RowElementTags.Blob then
+ let offset =
+ if element.IsAbsolute then
+ value
+ elif value = 0 then
+ 0
+ elif value < 0 || value >= input.BlobHeapOffsets.Length then
+ invalidArg "element" $"Blob heap offset index out of range: {value} (offsetCount={input.BlobHeapOffsets.Length})"
+ else
+ input.HeapOffsets.BlobHeapStart + input.BlobHeapOffsets.[value]
+
+ writeHeapIndex writer indexSizes.BlobsBig offset
+ elif tag = Encoding.RowElementTags.Guid then
+ // Encode GUID columns as byte offsets into the *combined* Guid heap
+ // (baseline length + delta entries). Each Guid entry is 16 bytes.
+ // Absolute handles are already full offsets and are written verbatim.
+ let adjusted =
+ if element.IsAbsolute then
+ value
+ elif value = 0 then
+ 0
+ else
+ // Guid heap indexes are entry counts (1-based), not byte offsets.
+ let baselineEntries = input.HeapOffsets.GuidHeapStart / 16
+ baselineEntries + value
+
+ if Environment.GetEnvironmentVariable("FSHARP_HOTRELOAD_TRACE_HEAP_OFFSETS") = "1" then
+ printfn
+ "[fsharp-hotreload][guid-serialize] isAbsolute=%b value=%d adjusted=%d guidsBig=%b"
+ element.IsAbsolute
+ value
+ adjusted
+ indexSizes.GuidsBig
+
+ writeHeapIndex writer indexSizes.GuidsBig adjusted
+ elif
+ tag >= Encoding.RowElementTags.SimpleIndexMin
+ && tag <= Encoding.RowElementTags.SimpleIndexMax
+ then
+ let tableIndex = tag - Encoding.RowElementTags.SimpleIndexMin
+ writeHeapIndex writer indexSizes.SimpleIndexBig.[tableIndex] value
+ elif
+ tag >= Encoding.RowElementTags.TypeDefOrRefOrSpecMin
+ && tag <= Encoding.RowElementTags.TypeDefOrRefOrSpecMax
+ then
+ let subTag = tag - Encoding.RowElementTags.TypeDefOrRefOrSpecMin
+ writeTaggedIndex writer Encoding.CodedIndices.TypeDefOrRef.TagBits indexSizes.TypeDefOrRefBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.TypeOrMethodDefMin
+ && tag <= Encoding.RowElementTags.TypeOrMethodDefMax
+ then
+ let subTag = tag - Encoding.RowElementTags.TypeOrMethodDefMin
+ writeTaggedIndex writer Encoding.CodedIndices.TypeOrMethodDef.TagBits indexSizes.TypeOrMethodDefBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.HasConstantMin
+ && tag <= Encoding.RowElementTags.HasConstantMax
+ then
+ let subTag = tag - Encoding.RowElementTags.HasConstantMin
+ writeTaggedIndex writer Encoding.CodedIndices.HasConstant.TagBits indexSizes.HasConstantBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.HasCustomAttributeMin
+ && tag <= Encoding.RowElementTags.HasCustomAttributeMax
+ then
+ let subTag = tag - Encoding.RowElementTags.HasCustomAttributeMin
+ writeTaggedIndex writer Encoding.CodedIndices.HasCustomAttribute.TagBits indexSizes.HasCustomAttributeBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.HasFieldMarshalMin
+ && tag <= Encoding.RowElementTags.HasFieldMarshalMax
+ then
+ let subTag = tag - Encoding.RowElementTags.HasFieldMarshalMin
+ writeTaggedIndex writer Encoding.CodedIndices.HasFieldMarshal.TagBits indexSizes.HasFieldMarshalBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.HasDeclSecurityMin
+ && tag <= Encoding.RowElementTags.HasDeclSecurityMax
+ then
+ let subTag = tag - Encoding.RowElementTags.HasDeclSecurityMin
+ writeTaggedIndex writer Encoding.CodedIndices.HasDeclSecurity.TagBits indexSizes.HasDeclSecurityBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.MemberRefParentMin
+ && tag <= Encoding.RowElementTags.MemberRefParentMax
+ then
+ let subTag = tag - Encoding.RowElementTags.MemberRefParentMin
+ writeTaggedIndex writer Encoding.CodedIndices.MemberRefParent.TagBits indexSizes.MemberRefParentBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.HasSemanticsMin
+ && tag <= Encoding.RowElementTags.HasSemanticsMax
+ then
+ let subTag = tag - Encoding.RowElementTags.HasSemanticsMin
+ writeTaggedIndex writer Encoding.CodedIndices.HasSemantics.TagBits indexSizes.HasSemanticsBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.MethodDefOrRefMin
+ && tag <= Encoding.RowElementTags.MethodDefOrRefMax
+ then
+ let subTag = tag - Encoding.RowElementTags.MethodDefOrRefMin
+ writeTaggedIndex writer Encoding.CodedIndices.MethodDefOrRef.TagBits indexSizes.MethodDefOrRefBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.MemberForwardedMin
+ && tag <= Encoding.RowElementTags.MemberForwardedMax
+ then
+ let subTag = tag - Encoding.RowElementTags.MemberForwardedMin
+ writeTaggedIndex writer Encoding.CodedIndices.MemberForwarded.TagBits indexSizes.MemberForwardedBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.ImplementationMin
+ && tag <= Encoding.RowElementTags.ImplementationMax
+ then
+ let subTag = tag - Encoding.RowElementTags.ImplementationMin
+ writeTaggedIndex writer Encoding.CodedIndices.Implementation.TagBits indexSizes.ImplementationBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.CustomAttributeTypeMin
+ && tag <= Encoding.RowElementTags.CustomAttributeTypeMax
+ then
+ let subTag = tag - Encoding.RowElementTags.CustomAttributeTypeMin
+ writeTaggedIndex writer Encoding.CodedIndices.CustomAttributeType.TagBits indexSizes.CustomAttributeTypeBig subTag value
+ elif
+ tag >= Encoding.RowElementTags.ResolutionScopeMin
+ && tag <= Encoding.RowElementTags.ResolutionScopeMax
+ then
+ let subTag = tag - Encoding.RowElementTags.ResolutionScopeMin
+ writeTaggedIndex writer Encoding.CodedIndices.ResolutionScope.TagBits indexSizes.ResolutionScopeBig subTag value
+ else
+ invalidArg "element" $"Unsupported row element tag: {tag} (value={value})"
+
+let private align4 value = (value + 3) &&& ~~~3
+
+let buildTableStream (input: DeltaTableSerializerInput) : DeltaTableStream =
+ let sizes = input.MetadataSizes
+ let bitMasks = sizes.BitMasks
+ let indexSizes = sizes.IndexSizes
+ use ms = new MemoryStream()
+ use writer = new BinaryWriter(ms)
+
+ writer.Write(0u)
+ writer.Write(byte 2)
+ writer.Write(byte 0)
+
+ let heapFlags =
+ // #~ stream header HeapSizes byte (ECMA-335 II.24.2.6): low bits mark wide heaps;
+ // EnC deltas additionally set 0x20|0x80, mirroring Roslyn MetadataSizes for EmitDifference.
+ let baseFlags =
+ (if indexSizes.StringsBig then 0x01 else 0)
+ ||| (if indexSizes.GuidsBig then 0x02 else 0)
+ ||| (if indexSizes.BlobsBig then 0x04 else 0)
+
+ let encFlags = if sizes.IsEncDelta then (0x20 ||| 0x80) else 0
+ baseFlags ||| encFlags
+
+ writer.Write(byte heapFlags)
+ writer.Write(byte 1)
+ writer.Write(bitMasks.ValidLow)
+ writer.Write(bitMasks.ValidHigh)
+ writer.Write(bitMasks.SortedLow)
+ writer.Write(bitMasks.SortedHigh)
+
+ for tableIndex = 0 to DeltaTokens.TableCount - 1 do
+ if isTablePresent bitMasks.ValidLow bitMasks.ValidHigh tableIndex then
+ writer.Write(sizes.RowCounts.[tableIndex])
+
+ let rowsByIndex = tableRowsByIndex input.Tables
+
+ for tableIndex = 0 to DeltaTokens.TableCount - 1 do
+ let rows = rowsByIndex.[tableIndex]
+
+ if rows.Length > 0 then
+ for row in rows do
+ for element in row do
+ writeRowElement writer indexSizes input element
+
+ writer.Flush()
+ let unpaddedSize = int ms.Length
+ let paddedSize = align4 unpaddedSize
+ let bytes = ms.ToArray()
+
+ if paddedSize = unpaddedSize then
+ {
+ Bytes = bytes
+ UnpaddedSize = unpaddedSize
+ PaddedSize = paddedSize
+ }
+ else
+ let padded = Array.zeroCreate paddedSize
+ Array.Copy(bytes, padded, bytes.Length)
+
+ {
+ Bytes = padded
+ UnpaddedSize = unpaddedSize
+ PaddedSize = paddedSize
+ }
+
+type private StreamDescriptor =
+ {
+ Name: string
+ Offset: int
+ Size: int
+ Bytes: byte[]
+ }
+
+let private versionString = "v4.0.30319"
+
+let private encodeName (writer: BinaryWriter) (name: string) =
+ let bytes = Text.Encoding.UTF8.GetBytes(name)
+ writer.Write(bytes)
+ writer.Write(byte 0)
+
+ while writer.BaseStream.Position % 4L <> 0L do
+ writer.Write(byte 0)
+
+let private streamHeaderSize (name: string) =
+ let nameLength = Text.Encoding.UTF8.GetByteCount(name) + 1
+ 8 + align4 nameLength
+
+let serializeMetadataRoot (input: DeltaTableSerializerInput) (heaps: DeltaHeapStreams) (tableStream: DeltaTableStream) : byte[] =
+ let includeJtd = input.MetadataSizes.IsEncDelta
+
+ let baseStreams =
+ [
+ "#-", tableStream.UnpaddedSize, tableStream.Bytes
+ "#Strings", heaps.StringsLength, heaps.Strings
+ "#US", heaps.UserStringsLength, heaps.UserStrings
+ "#GUID", heaps.GuidsLength, heaps.Guids
+ "#Blob", heaps.BlobsLength, heaps.Blobs
+ ]
+
+ let streams =
+ if includeJtd then
+ baseStreams @ [ "#JTD", 0, Array.empty ]
+ else
+ baseStreams
+
+ let versionBytes = Text.Encoding.UTF8.GetBytes(versionString)
+ let versionStringLength = versionBytes.Length + 1
+ let versionLength = align4 versionStringLength
+
+ let headerBaseSize = 4 + 2 + 2 + 4 + 4 + versionLength + 2 + 2
+
+ let streamsHeaderSize =
+ streams |> List.sumBy (fun (name, _, _) -> streamHeaderSize name)
+
+ let headerSize = headerBaseSize + streamsHeaderSize
+
+ let mutable offset = headerSize
+
+ let descriptors =
+ streams
+ |> List.map (fun (name, size, bytes) ->
+ let descriptor =
+ {
+ Name = name
+ Offset = offset
+ Size = size
+ Bytes = bytes
+ }
+
+ offset <- offset + bytes.Length
+ descriptor)
+
+ use ms = new MemoryStream()
+ use writer = new BinaryWriter(ms)
+
+ writer.Write(0x424A5342u)
+ writer.Write(uint16 1)
+ writer.Write(uint16 1)
+ writer.Write(0u)
+ writer.Write(uint32 versionLength)
+ writer.Write(versionBytes)
+ writer.Write(byte 0)
+ let paddingBytes = versionLength - versionStringLength
+
+ if paddingBytes > 0 then
+ writer.Write(Array.zeroCreate paddingBytes)
+
+ while ms.Position % 4L <> 0L do
+ writer.Write(byte 0)
+
+ writer.Write(uint16 0)
+ writer.Write(uint16 descriptors.Length)
+
+ for descriptor in descriptors do
+ writer.Write(uint32 descriptor.Offset)
+ writer.Write(uint32 descriptor.Size)
+ encodeName writer descriptor.Name
+
+ for descriptor in descriptors do
+ writer.Write(descriptor.Bytes)
+
+ ms.ToArray()
diff --git a/src/Compiler/CodeGen/DeltaMetadataTables.fs b/src/Compiler/CodeGen/DeltaMetadataTables.fs
new file mode 100644
index 00000000000..c76ceadeda2
--- /dev/null
+++ b/src/Compiler/CodeGen/DeltaMetadataTables.fs
@@ -0,0 +1,1046 @@
+module internal FSharp.Compiler.CodeGen.DeltaMetadataTables
+
+open System
+open System.Collections.Generic
+open System.IO
+open System.Text
+open Microsoft.FSharp.Collections
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.AbstractIL.ILMetadataHeaps
+open FSharp.Compiler.HotReloadBaseline
+open FSharp.Compiler.IlxDeltaStreams
+open FSharp.Compiler.CodeGen.DeltaMetadataTypes
+
+module Encoding = FSharp.Compiler.CodeGen.DeltaMetadataEncoding
+
+let private traceHeapOffsets =
+ lazy
+ (match Environment.GetEnvironmentVariable("FSHARP_HOTRELOAD_TRACE_HEAP_OFFSETS") with
+ | null
+ | "" -> false
+ | value -> value = "1" || String.Equals(value, "true", StringComparison.OrdinalIgnoreCase))
+
+/// Mirrors the AbstractIL metadata tables for the subset of rows emitted by
+/// hot reload deltas. The tables are populated alongside the SRM metadata
+/// builder so we can eventually serialize deltas directly via AbstractIL.
+type MetadataHeapOffsets =
+ {
+ StringHeapStart: int
+ BlobHeapStart: int
+ GuidHeapStart: int
+ UserStringHeapStart: int
+ }
+
+ static member Zero =
+ {
+ StringHeapStart = 0
+ BlobHeapStart = 0
+ GuidHeapStart = 0
+ UserStringHeapStart = 0
+ }
+
+ static member OfHeapSizes(heapSizes: MetadataHeapSizes) =
+ {
+ StringHeapStart = heapSizes.StringHeapSize
+ BlobHeapStart = heapSizes.BlobHeapSize
+ GuidHeapStart = heapSizes.GuidHeapSize
+ UserStringHeapStart = heapSizes.UserStringHeapSize
+ }
+
+let private byteArrayComparer: IEqualityComparer =
+ { new IEqualityComparer with
+ member _.Equals(x, y) =
+ match x, y with
+ | null, null -> true
+ | null, _
+ | _, null -> false
+ | x, y ->
+ if obj.ReferenceEquals(x, y) then
+ true
+ elif x.Length <> y.Length then
+ false
+ else
+ let mutable idx = 0
+ let mutable equal = true
+
+ while equal && idx < x.Length do
+ if x[idx] <> y[idx] then
+ equal <- false
+
+ idx <- idx + 1
+
+ equal
+
+ member _.GetHashCode(array: byte[]) =
+ if isNull (box array) then
+ 0
+ else
+ let mutable hash = 17
+
+ for value in array do
+ hash <- (hash * 23) + int value
+
+ hash
+ }
+
+let private writeCompressedUnsigned (writer: BinaryWriter) (value: int) =
+ if value <= 0x7F then
+ writer.Write(byte value)
+ elif value <= 0x3FFF then
+ let b1 = byte ((value >>> 8) ||| 0x80)
+ let b0 = byte (value &&& 0xFF)
+ writer.Write(b1)
+ writer.Write(b0)
+ elif value <= 0x1FFFFFFF then
+ let b2 = byte ((value >>> 24) ||| 0xC0)
+ let b1 = byte ((value >>> 16) &&& 0xFF)
+ let b0 = byte ((value >>> 8) &&& 0xFF)
+ let bLowest = byte (value &&& 0xFF)
+ writer.Write(b2)
+ writer.Write(b1)
+ writer.Write(b0)
+ writer.Write(bLowest)
+ else
+ invalidArg (nameof value) "Compressed integer is too large for CLI metadata."
+
+type private RowTableBuilder() =
+ let rows = ResizeArray()
+
+ member _.Add(elements: RowElementData[]) = rows.Add elements
+ member _.Entries = rows.ToArray()
+ member _.Count = rows.Count
+
+type private StringHeapBuilder() =
+ let entries = ResizeArray()
+ let lookup = Dictionary(StringComparer.Ordinal)
+ let utf8 = Encoding.UTF8
+ let mutable bytesCache: byte[] option = None
+ let mutable offsetsCache: int[] option = None
+
+ member _.AddSharedEntry(value: string) : int =
+ if String.IsNullOrEmpty value then
+ 0
+ else
+ match lookup.TryGetValue value with
+ | true, index -> index
+ | _ ->
+ let index = entries.Count + 1
+ entries.Add value
+ lookup[value] <- index
+ bytesCache <- None
+ offsetsCache <- None
+ index
+
+ member private this.BuildIfNeeded() =
+ match bytesCache, offsetsCache with
+ | Some _, Some _ -> ()
+ | _ ->
+ use ms = new MemoryStream()
+ use writer = new BinaryWriter(ms, utf8, leaveOpen = true)
+ let entryOffsets = Array.zeroCreate (entries.Count + 1)
+ writer.Write(byte 0)
+ let mutable currentOffset = int ms.Length
+
+ for i = 0 to entries.Count - 1 do
+ let entryIndex = i + 1
+ entryOffsets.[entryIndex] <- currentOffset
+ let bytes = utf8.GetBytes entries.[i]
+ writer.Write(bytes)
+ writer.Write(byte 0)
+ currentOffset <- currentOffset + bytes.Length + 1
+
+ writer.Flush()
+ bytesCache <- Some(ms.ToArray())
+ offsetsCache <- Some entryOffsets
+
+ member this.Bytes =
+ this.BuildIfNeeded()
+ bytesCache.Value
+
+ member this.EntryOffsets =
+ this.BuildIfNeeded()
+ offsetsCache.Value
+
+type private ByteArrayHeapBuilder() =
+ let entries = ResizeArray()
+ let lookup = Dictionary(byteArrayComparer)
+ let mutable bytesCache: byte[] option = None
+ let mutable offsetsCache: int[] option = None
+
+ member _.AddSharedEntry(value: byte[]) : int =
+ if isNull (box value) || value.Length = 0 then
+ 0
+ else
+ match lookup.TryGetValue value with
+ | true, index -> index
+ | _ ->
+ let index = entries.Count + 1
+ entries.Add value
+ lookup[value] <- index
+ bytesCache <- None
+ offsetsCache <- None
+ index
+
+ member private this.BuildIfNeeded() =
+ match bytesCache, offsetsCache with
+ | Some _, Some _ -> ()
+ | _ ->
+ use ms = new MemoryStream()
+ use writer = new BinaryWriter(ms, Encoding.UTF8, leaveOpen = true)
+ let entryOffsets = Array.zeroCreate (entries.Count + 1)
+ writer.Write(byte 0)
+ let mutable currentOffset = int ms.Length
+
+ for i = 0 to entries.Count - 1 do
+ let entryIndex = i + 1
+ entryOffsets.[entryIndex] <- currentOffset
+ let value = entries.[i]
+ writeCompressedUnsigned writer value.Length
+
+ if value.Length > 0 then
+ writer.Write(value)
+
+ currentOffset <- int ms.Length
+
+ writer.Flush()
+ bytesCache <- Some(ms.ToArray())
+ offsetsCache <- Some entryOffsets
+
+ member this.Bytes =
+ this.BuildIfNeeded()
+ bytesCache.Value
+
+ member this.EntryOffsets =
+ this.BuildIfNeeded()
+ offsetsCache.Value
+
+ member _.Entries = entries |> Seq.toArray
+
+type private UserStringHeapBuilder() =
+ let entries = HashSet()
+ let mutable buffer: byte[] option = None
+ let mutable maxLength = 1
+ let mutable bytesCache: byte[] option = None
+
+ /// Encodes a user string per ECMA-335 II.24.2.4:
+ /// - Compressed unsigned length prefix (byte count including trailing flag)
+ /// - UTF-16LE encoded string bytes
+ /// - Trailing flag byte (computed via markerForUnicodeBytes from ILBinaryWriter)
+ let encodeUserString (value: string) =
+ let utf16Bytes = Text.Encoding.Unicode.GetBytes(value)
+ let byteCount = utf16Bytes.Length + 1 // +1 for trailing flag
+
+ // Use existing markerForUnicodeBytes from ilwrite.fs for trailing flag
+ let trailingFlag = byte (markerForUnicodeBytes utf16Bytes)
+
+ // Encode compressed length prefix
+ use ms = new MemoryStream()
+ use writer = new BinaryWriter(ms, Text.Encoding.UTF8, leaveOpen = true)
+ writeCompressedUnsigned writer byteCount
+ writer.Write(utf16Bytes)
+ writer.Write(trailingFlag)
+ writer.Flush()
+ ms.ToArray()
+
+ let ensureBuffer lengthNeeded =
+ let requiredLength = max lengthNeeded 1
+
+ match buffer with
+ | Some existing when existing.Length >= requiredLength -> existing
+ | Some existing ->
+ let resized = Array.zeroCreate requiredLength
+ Buffer.BlockCopy(existing, 0, resized, 0, existing.Length)
+ buffer <- Some resized
+ resized
+ | None ->
+ let initial = Array.zeroCreate requiredLength
+ initial[0] <- 0uy
+ buffer <- Some initial
+ initial
+
+ member _.AddEntry(offset: int, value: string) =
+ // Use < 0 instead of <= 0 because offset 0 is valid for delta heaps
+ // (the null byte at offset 0 is only in the baseline heap, not the delta)
+ if offset < 0 then
+ ()
+ elif entries.Add offset then
+ let bytes = encodeUserString value
+ let neededLength = offset + bytes.Length
+ let storage = ensureBuffer neededLength
+ Buffer.BlockCopy(bytes, 0, storage, offset, bytes.Length)
+ maxLength <- max maxLength neededLength
+ bytesCache <- None
+
+ member _.NextOffset = maxLength
+
+ member this.Bytes =
+ match buffer with
+ | Some data ->
+ match bytesCache with
+ | Some cached -> cached
+ | None ->
+ let length = max maxLength 1
+
+ let trimmed =
+ if data.Length = length then
+ data
+ else
+ let slice = Array.zeroCreate length
+ Buffer.BlockCopy(data, 0, slice, 0, min data.Length length)
+ slice
+
+ bytesCache <- Some trimmed
+ trimmed
+ | None ->
+ let minimal = Array.zeroCreate 1
+ minimal[0] <- 0uy
+ minimal
+
+type DeltaMetadataTables(?heapOffsets: MetadataHeapOffsets) =
+ let heapOffsets = defaultArg heapOffsets MetadataHeapOffsets.Zero
+ let strings = StringHeapBuilder()
+ let blobs = ByteArrayHeapBuilder()
+ let guids = ByteArrayHeapBuilder()
+ let userStrings = UserStringHeapBuilder()
+ let userStringLookup = Dictionary(StringComparer.Ordinal)
+ let mutable stringHeapBytesCache: byte[] option = None
+ let mutable blobHeapBytesCache: byte[] option = None
+ let mutable guidHeapBytesCache: byte[] option = None
+ let mutable userStringHeapBytesCache: byte[] option = None
+
+ let moduleRows = RowTableBuilder()
+ let typeDefRows = RowTableBuilder()
+ let nestedClassRows = RowTableBuilder()
+ let interfaceImplRows = RowTableBuilder()
+ let methodImplRows = RowTableBuilder()
+ let constantRows = RowTableBuilder()
+ let fieldRows = RowTableBuilder()
+ let methodRows = RowTableBuilder()
+ let paramRows = RowTableBuilder()
+ let typeRefRows = RowTableBuilder()
+ let memberRefRows = RowTableBuilder()
+ let methodSpecRows = RowTableBuilder()
+ let typeSpecRows = RowTableBuilder()
+ let genericParamRows = RowTableBuilder()
+ let genericParamConstraintRows = RowTableBuilder()
+ let assemblyRefRows = RowTableBuilder()
+ let standAloneSigRows = RowTableBuilder()
+ let customAttributeRows = RowTableBuilder()
+ let propertyRows = RowTableBuilder()
+ let eventRows = RowTableBuilder()
+ let propertyMapRows = RowTableBuilder()
+ let eventMapRows = RowTableBuilder()
+ let methodSemanticsRows = RowTableBuilder()
+ let encLogRows = RowTableBuilder()
+ let encMapRows = RowTableBuilder()
+
+ let rowElement tag value =
+ {
+ Tag = tag
+ Value = value
+ IsAbsolute = false
+ }
+
+ let rowElementAbsolute tag value =
+ {
+ Tag = tag
+ Value = value
+ IsAbsolute = true
+ }
+
+ let rowElementUShort (value: uint16) =
+ rowElement Encoding.RowElementTags.UShort (int value)
+
+ let rowElementULong (value: int) =
+ rowElement Encoding.RowElementTags.ULong value
+
+ let rowElementString value =
+ rowElement Encoding.RowElementTags.String value
+
+ let rowElementBlob value =
+ rowElement Encoding.RowElementTags.Blob value
+
+ let rowElementStringAbsolute value =
+ rowElementAbsolute Encoding.RowElementTags.String value
+
+ let rowElementBlobAbsolute value =
+ rowElementAbsolute Encoding.RowElementTags.Blob value
+
+ let rowElementGuidAbsolute value =
+ rowElementAbsolute Encoding.RowElementTags.Guid value
+
+ let rowElementSimpleIndex table value =
+ rowElement (Encoding.RowElementTags.SimpleIndex table) value
+
+ let rowElementTypeDefOrRef tag value =
+ rowElement (Encoding.RowElementTags.TypeDefOrRefOrSpec tag) value
+
+ let rowElementHasSemantics tag value =
+ rowElement (Encoding.RowElementTags.HasSemantics tag) value
+
+ let rowElementMethodDefOrRef (methodRef: MethodDefOrRef) =
+ rowElement (Encoding.RowElementTags.MethodDefOrRef(mkMethodDefOrRefTag methodRef.CodedTag)) methodRef.RowId
+
+ let rowElementTypeOrMethodDef (owner: TypeOrMethodDef) =
+ rowElement (Encoding.RowElementTags.TypeOrMethodDef(mkTypeOrMethodDefTag owner.CodedTag)) owner.RowId
+
+ let rowElementResolutionScope (scope: ResolutionScope) =
+ rowElement (Encoding.RowElementTags.ResolutionScopeMin + scope.CodedTag) scope.RowId
+
+ let rowElementMemberRefParent (parent: MemberRefParent) =
+ rowElement (Encoding.RowElementTags.MemberRefParentMin + parent.CodedTag) parent.RowId
+
+ /// HasCustomAttribute coded index per ECMA-335 II.24.2.6.
+ /// Uses the HasCustomAttribute DU from ILDeltaHandles.
+ let rowElementHasCustomAttribute (parent: HasCustomAttribute) =
+ rowElement (Encoding.RowElementTags.HasCustomAttributeMin + parent.CodedTag) parent.RowId
+
+ /// HasConstant coded index per ECMA-335 II.24.2.6 (Field=0, Param=1, Property=2).
+ /// Uses the HasConstant DU from ILDeltaHandles.
+ let rowElementHasConstant (parent: HasConstant) =
+ let tag =
+ match parent with
+ | HC_Field _ -> 0
+ | HC_Param _ -> 1
+ | HC_Property _ -> 2
+
+ rowElement (Encoding.RowElementTags.HasConstantMin + tag) parent.RowId
+
+ /// CustomAttributeType coded index per ECMA-335 II.24.2.6.
+ /// Uses the CustomAttributeType DU from ILDeltaHandles.
+ let rowElementCustomAttributeType (ctor: CustomAttributeType) =
+ let tag = mkILCustomAttributeTypeTag ctor.CodedTag
+ rowElement (Encoding.RowElementTags.CustomAttributeType tag) ctor.RowId
+
+ let addStringValue (value: string) =
+ if String.IsNullOrEmpty value then
+ 0
+ else
+ strings.AddSharedEntry value
+
+ let addUserStringValue (value: string) =
+ if String.IsNullOrEmpty value then
+ 0
+ else
+ match userStringLookup.TryGetValue value with
+ | true, offset -> offset
+ | _ ->
+ // #US tokens store offsets, so allocate a new literal at the next free delta-local offset
+ // and translate it back to the absolute heap offset expected by IL operands.
+ let relativeOffset = userStrings.NextOffset
+ let absoluteOffset = heapOffsets.UserStringHeapStart + relativeOffset
+ userStrings.AddEntry(relativeOffset, value)
+ userStringLookup[value] <- absoluteOffset
+ userStringHeapBytesCache <- None
+ absoluteOffset
+
+ let addExistingStringOffset (offsetOpt: StringOffset option) (value: string) : int * bool =
+ match offsetOpt with
+ | Some(StringOffset offset) -> offset, true
+ | None ->
+ let idx = addStringValue value
+ idx, false
+
+ let addExistingStringOffsetOption (offsetOpt: StringOffset option) (valueOpt: string option) : int * bool =
+ match offsetOpt with
+ | Some(StringOffset offset) -> offset, true
+ | None ->
+ match valueOpt with
+ | Some v when not (String.IsNullOrEmpty v) -> strings.AddSharedEntry v, false
+ | _ -> 0, false
+
+ let addBlobBytes (bytes: byte[]) =
+ if obj.ReferenceEquals(bytes, null) || bytes.Length = 0 then
+ 0
+ else
+ blobs.AddSharedEntry bytes
+
+ let addExistingBlobOffset (offsetOpt: BlobOffset option) (value: byte[]) : int * bool =
+ match offsetOpt with
+ | Some(BlobOffset offset) -> offset, true
+ | None ->
+ let idx = addBlobBytes value
+ idx, false
+
+ /// Force-add a GUID to the heap, even if it's the nil GUID.
+ /// Returns the 1-based index in the delta's GUID heap.
+ let forceAddGuidValue (value: Guid) =
+ guids.AddSharedEntry(value.ToByteArray())
+
+ let stringElement (token, isAbsolute) =
+ if isAbsolute then
+ rowElementStringAbsolute token
+ else
+ rowElementString token
+
+ let blobElement (token, isAbsolute) =
+ if isAbsolute then
+ rowElementBlobAbsolute token
+ else
+ rowElementBlob token
+
+ let encodeTypeDefOrRef (typeRef: TypeDefOrRef) =
+ match typeRef with
+ | TDR_TypeDef(TypeDefHandle rowId) -> tdor_TypeDef, rowId
+ | TDR_TypeRef(TypeRefHandle rowId) -> tdor_TypeRef, rowId
+ | TDR_TypeSpec(TypeSpecHandle rowId) -> tdor_TypeSpec, rowId
+
+ let buildStringHeapBytes () = strings.Bytes
+
+ let buildBlobHeapBytes () = blobs.Bytes
+
+ let buildGuidHeapBytes () =
+ use ms = new MemoryStream()
+ use writer = new BinaryWriter(ms, Encoding.UTF8, leaveOpen = true)
+ // Guid heap is a packed list of 16-byte entries; no sentinel is emitted.
+ for entry in guids.Entries do
+ if entry.Length = 16 then
+ writer.Write(entry)
+ else
+ invalidArg "entry" "GUID entries must be 16 bytes."
+
+ if Environment.GetEnvironmentVariable("FSHARP_HOTRELOAD_TRACE_METADATA") = "1" then
+ let dumpGuid (bytes: byte[]) =
+ if bytes.Length >= 16 then
+ BitConverter.ToString(bytes, 0, 16)
+ else
+ ""
+
+ printfn "[delta-guid-heap] entries=%d" guids.Entries.Length
+
+ guids.Entries
+ |> Seq.mapi (fun idx b -> idx + 1, dumpGuid b)
+ |> Seq.iter (fun (idx, g) -> printfn "[delta-guid-heap] idx=%d guidBytes=%s" idx g)
+
+ writer.Flush()
+ ms.ToArray()
+
+ let buildUserStringHeapBytes () = userStrings.Bytes
+
+ member _.AddModuleRow(name: string, nameOffsetOpt: StringOffset option, generation: int, moduleId: Guid, encId: Guid, encBaseId: Guid) =
+ if moduleRows.Count = 0 then
+ let nameToken =
+ match nameOffsetOpt with
+ | Some(StringOffset offset) -> offset, true
+ | None -> addStringValue name, false
+ // For EnC deltas:
+ // - Delta GUID heap contains: nil at 1, MVID at 2, EncId at 3
+ // - Module row stores raw delta-local indices using rowElementGuidAbsolute
+ // - The runtime interprets these as-is (no baseline offset adjustment needed)
+ // Force-add GUIDs in order to get predictable indices:
+ let _nilGuidIndex = forceAddGuidValue System.Guid.Empty // Index 1 (nil placeholder)
+ let mvidIndex = forceAddGuidValue moduleId // Index 2
+ let encIdIndex = forceAddGuidValue encId // Index 3
+ // EncBaseId is 0 (nil) for generation 1, otherwise reference previous EncId
+ let encBaseIdIndex =
+ if encBaseId = System.Guid.Empty then
+ 0
+ else
+ forceAddGuidValue encBaseId // Index 4 if not nil
+
+ if traceHeapOffsets.Value then
+ printfn
+ "[fsharp-hotreload][module-row-write] generation=%d mvidIndex=%d encIdIndex=%d encBaseIdIndex=%d"
+ generation
+ mvidIndex
+ encIdIndex
+ encBaseIdIndex
+
+ moduleRows.Add
+ [|
+ rowElementUShort (uint16 generation)
+ stringElement nameToken
+ rowElementGuidAbsolute mvidIndex // MVID - delta-local absolute index
+ rowElementGuidAbsolute encIdIndex // EncId - delta-local absolute index
+ rowElementGuidAbsolute encBaseIdIndex // EncBaseId - 0 or delta-local index
+ |]
+
+ /// Add a TypeDef table row per ECMA-335 II.22.37: Flags (4 bytes), TypeName,
+ /// TypeNamespace (string heap), Extends (TypeDefOrRef coded index), FieldList,
+ /// MethodList (simple indices). The member-list columns are written as 0 (Roslyn
+ /// EnC parity): members are linked via the AddField/AddMethod EncLog entries.
+ member _.AddTypeDefinitionRow(row: TypeDefinitionRowInfo) =
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+ let namespaceToken = addExistingStringOffset row.NamespaceOffset row.Namespace
+
+ let extendsTag, extendsRow =
+ match row.Extends with
+ | Some extends -> encodeTypeDefOrRef extends
+ | None -> tdor_TypeDef, 0
+
+ let rowElements =
+ [|
+ rowElementULong (int row.Attributes)
+ stringElement nameToken
+ stringElement namespaceToken
+ rowElementTypeDefOrRef extendsTag extendsRow
+ rowElementSimpleIndex TableNames.Field 0
+ rowElementSimpleIndex TableNames.Method 0
+ |]
+
+ typeDefRows.Add rowElements
+
+ /// Add a NestedClass table row per ECMA-335 II.22.32: NestedClass and
+ /// EnclosingClass are both TypeDef row indices.
+ member _.AddNestedClassRow(row: NestedClassRowInfo) =
+ let rowElements =
+ [|
+ rowElementSimpleIndex TableNames.TypeDef row.NestedTypeDefRowId
+ rowElementSimpleIndex TableNames.TypeDef row.EnclosingTypeDefRowId
+ |]
+
+ nestedClassRows.Add rowElements
+
+ /// Add an InterfaceImpl table row per ECMA-335 II.22.23: Class (TypeDef row index)
+ /// and Interface (TypeDefOrRef coded index).
+ member _.AddInterfaceImplRow(row: InterfaceImplRowInfo) =
+ let interfaceTag, interfaceRow = encodeTypeDefOrRef row.Interface
+
+ let rowElements =
+ [|
+ rowElementSimpleIndex TableNames.TypeDef row.ClassTypeDefRowId
+ rowElementTypeDefOrRef interfaceTag interfaceRow
+ |]
+
+ interfaceImplRows.Add rowElements
+
+ /// Add a Constant table row per ECMA-335 II.22.9: Type (1-byte ELEMENT_TYPE code,
+ /// physically encoded as a little-endian u2 whose high byte is the zero padding),
+ /// Parent (HasConstant coded index) and Value (#Blob offset). The value blob always
+ /// enters the DELTA blob heap (fresh-compile heap offsets are meaningless against
+ /// the baseline+delta layout).
+ member _.AddConstantRow(row: ConstantRowInfo) =
+ let valueToken = addExistingBlobOffset None row.Value
+
+ let rowElements =
+ [|
+ rowElementUShort (uint16 row.TypeCode)
+ rowElementHasConstant row.Parent
+ blobElement valueToken
+ |]
+
+ constantRows.Add rowElements
+
+ /// Add a MethodImpl table row per ECMA-335 II.22.27: Class (TypeDef row index),
+ /// MethodBody and MethodDeclaration (MethodDefOrRef coded indexes).
+ member _.AddMethodImplRow(row: MethodImplRowInfo) =
+ let rowElements =
+ [|
+ rowElementSimpleIndex TableNames.TypeDef row.ClassTypeDefRowId
+ rowElementMethodDefOrRef row.MethodBody
+ rowElementMethodDefOrRef row.MethodDeclaration
+ |]
+
+ methodImplRows.Add rowElements
+
+ member _.AddMethodRow(row: MethodDefinitionRowInfo, body: MethodBodyUpdate) =
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+
+ let signatureToken = addExistingBlobOffset row.SignatureOffset row.Signature
+
+ let codeRva =
+ if body.CodeLength > 0 then
+ body.CodeOffset
+ else
+ match row.CodeRva with
+ | Some rva -> rva
+ | None -> 0
+
+ let rowElements =
+ [|
+ rowElementULong codeRva
+ rowElementUShort (uint16 row.ImplAttributes)
+ rowElementUShort (uint16 row.Attributes)
+ stringElement nameToken
+ blobElement signatureToken
+ rowElementSimpleIndex TableNames.Param (row.FirstParameterRowId |> Option.defaultValue 0)
+ |]
+
+ methodRows.Add rowElements
+
+ /// Add a Field table row per ECMA-335 II.22.15: Flags (2 bytes), Name (string
+ /// heap), Signature (blob heap, FieldSig per II.23.2.4).
+ member _.AddFieldRow(row: FieldDefinitionRowInfo) =
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+ let signatureToken = addExistingBlobOffset row.SignatureOffset row.Signature
+
+ let rowElements =
+ [|
+ rowElementUShort (uint16 row.Attributes)
+ stringElement nameToken
+ blobElement signatureToken
+ |]
+
+ fieldRows.Add rowElements
+
+ member _.AddParameterRow(row: ParameterDefinitionRowInfo) =
+ // Validate parameter row per ECMA-335 II.22.33
+ if row.RowId <= 0 then
+ invalidArg "row" $"Parameter RowId must be > 0, got {row.RowId}"
+
+ if row.SequenceNumber < 0 then
+ invalidArg "row" $"Parameter SequenceNumber must be >= 0, got {row.SequenceNumber}"
+
+ let nameToken = addExistingStringOffsetOption row.NameOffset row.Name
+
+ let rowElements =
+ [|
+ rowElementUShort (uint16 row.Attributes)
+ rowElementUShort (uint16 row.SequenceNumber)
+ stringElement nameToken
+ |]
+
+ paramRows.Add rowElements
+
+ member _.AddTypeReferenceRow(row: TypeReferenceRowInfo) =
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+ let namespaceToken = addExistingStringOffset row.NamespaceOffset row.Namespace
+
+ let rowElements =
+ [|
+ rowElementResolutionScope row.ResolutionScope
+ stringElement nameToken
+ stringElement namespaceToken
+ |]
+
+ typeRefRows.Add rowElements
+
+ member _.AddMemberReferenceRow(row: MemberReferenceRowInfo) =
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+ let signatureToken = addExistingBlobOffset row.SignatureOffset row.Signature
+
+ let rowElements =
+ [|
+ rowElementMemberRefParent row.Parent
+ stringElement nameToken
+ blobElement signatureToken
+ |]
+
+ memberRefRows.Add rowElements
+
+ member _.AddMethodSpecificationRow(row: MethodSpecificationRowInfo) =
+ let signatureToken = addExistingBlobOffset row.SignatureOffset row.Signature
+
+ let rowElements =
+ [| rowElementMethodDefOrRef row.Method; blobElement signatureToken |]
+
+ methodSpecRows.Add rowElements
+
+ member _.AddTypeSpecificationRow(row: TypeSpecificationRowInfo) =
+ // TypeSpec row per ECMA-335 II.22.39: a single #Blob signature column.
+ let signatureToken = addExistingBlobOffset row.SignatureOffset row.Signature
+ let rowElements = [| blobElement signatureToken |]
+ typeSpecRows.Add rowElements
+
+ member _.AddGenericParamRow(row: GenericParamRowInfo) =
+ // GenericParam row per ECMA-335 II.22.20: Number, Flags, Owner
+ // (TypeOrMethodDef coded index), Name.
+ if row.RowId <= 0 then
+ invalidArg "row" $"GenericParam RowId must be > 0, got {row.RowId}"
+
+ if row.Number < 0 then
+ invalidArg "row" $"GenericParam Number must be >= 0, got {row.Number}"
+
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+
+ let rowElements =
+ [|
+ rowElementUShort (uint16 row.Number)
+ rowElementUShort (uint16 row.Attributes)
+ rowElementTypeOrMethodDef row.Owner
+ stringElement nameToken
+ |]
+
+ genericParamRows.Add rowElements
+
+ /// Add a GenericParamConstraint table row per ECMA-335 II.22.21: Owner (GenericParam
+ /// row index) and Constraint (TypeDefOrRef coded index).
+ member _.AddGenericParamConstraintRow(row: GenericParamConstraintRowInfo) =
+ let constraintTag, constraintRow = encodeTypeDefOrRef row.Constraint
+
+ let rowElements =
+ [|
+ rowElementSimpleIndex TableNames.GenericParam row.OwnerGenericParamRowId
+ rowElementTypeDefOrRef constraintTag constraintRow
+ |]
+
+ genericParamConstraintRows.Add rowElements
+
+ member _.AddAssemblyReferenceRow(row: AssemblyReferenceRowInfo) =
+ let publicKeyToken =
+ addExistingBlobOffset row.PublicKeyOrTokenOffset row.PublicKeyOrToken
+
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+ let cultureToken = addExistingStringOffsetOption row.CultureOffset row.Culture
+ let hashToken = addExistingBlobOffset row.HashValueOffset row.HashValue
+
+ let versionComponent value =
+ if value >= 0 && value <= 0xFFFF then uint16 value else 0us
+
+ let rowElements =
+ [|
+ rowElementUShort (versionComponent row.Version.Major)
+ rowElementUShort (versionComponent row.Version.Minor)
+ rowElementUShort (versionComponent row.Version.Build)
+ rowElementUShort (versionComponent row.Version.Revision)
+ rowElementULong (int row.Flags)
+ blobElement publicKeyToken
+ stringElement nameToken
+ stringElement cultureToken
+ blobElement hashToken
+ |]
+
+ assemblyRefRows.Add rowElements
+
+ member _.AddStandaloneSignatureRow(signatureBytes: byte[]) =
+ if not (isNull (box signatureBytes)) && signatureBytes.Length > 0 then
+ let blobIndex = addBlobBytes signatureBytes
+ let rowElements = [| blobElement (blobIndex, false) |]
+ standAloneSigRows.Add rowElements
+
+ member _.AddCustomAttributeRow(row: CustomAttributeRowInfo) =
+ let valueToken = addExistingBlobOffset row.ValueOffset row.Value
+
+ let rowElements =
+ [|
+ rowElementHasCustomAttribute row.Parent
+ rowElementCustomAttributeType row.Constructor
+ blobElement valueToken
+ |]
+
+ customAttributeRows.Add rowElements
+
+ member _.AddPropertyRow(row: PropertyDefinitionRowInfo) =
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+
+ let signatureToken = addExistingBlobOffset row.SignatureOffset row.Signature
+
+ let rowElements =
+ [|
+ rowElementUShort (uint16 row.Attributes)
+ stringElement nameToken
+ blobElement signatureToken
+ |]
+
+ propertyRows.Add rowElements
+
+ member _.AddEventRow(row: EventDefinitionRowInfo) =
+ let tdorTag, tdorRow = encodeTypeDefOrRef row.EventType
+ let nameToken = addExistingStringOffset row.NameOffset row.Name
+
+ let rowElements =
+ [|
+ rowElementUShort (uint16 row.Attributes)
+ stringElement nameToken
+ rowElementTypeDefOrRef tdorTag tdorRow
+ |]
+
+ eventRows.Add rowElements
+
+ member _.AddPropertyMapRow(row: PropertyMapRowInfo) =
+ let rowElements =
+ [|
+ rowElementSimpleIndex TableNames.TypeDef row.TypeDefRowId
+ rowElementSimpleIndex TableNames.Property (row.FirstPropertyRowId |> Option.defaultValue 0)
+ |]
+
+ propertyMapRows.Add rowElements
+
+ member _.AddEventMapRow(row: EventMapRowInfo) =
+ let rowElements =
+ [|
+ rowElementSimpleIndex TableNames.TypeDef row.TypeDefRowId
+ rowElementSimpleIndex TableNames.Event (row.FirstEventRowId |> Option.defaultValue 0)
+ |]
+
+ eventMapRows.Add rowElements
+
+ member _.AddMethodSemanticsRow(row: MethodSemanticsMetadataUpdate) =
+ let methodRowId = DeltaTokens.getRowNumber row.MethodToken
+
+ let assocTag, assocRowId =
+ match row.AssociationInfo with
+ | MethodSemanticsAssociation.PropertyAssociation(_, propertyRowId) -> hs_Property, propertyRowId
+ | MethodSemanticsAssociation.EventAssociation(_, eventRowId) -> hs_Event, eventRowId
+
+ let rowElements =
+ [|
+ rowElementUShort (uint16 row.Attributes)
+ rowElementSimpleIndex TableNames.Method methodRowId
+ rowElementHasSemantics assocTag assocRowId
+ |]
+
+ methodSemanticsRows.Add rowElements
+
+ /// Add an entry to the EncLog table.
+ /// The EncLog records each modification made in this delta generation.
+ /// Per ECMA-335 II.22.7, each entry contains a token and operation.
+ member _.AddEncLogRow(table: TableName, rowId: int, operation: EditAndContinueOperation) =
+ let token = DeltaTokens.makeToken table rowId
+ let rowElements = [| rowElementULong token; rowElementULong operation.Value |]
+ encLogRows.Add rowElements
+
+ /// Add an entry to the EncMap table.
+ /// The EncMap provides a sorted list of all tokens present in this delta.
+ /// Per ECMA-335 II.22.6, entries are sorted by table then row.
+ member _.AddEncMapRow(table: TableName, rowId: int) =
+ let token = DeltaTokens.makeToken table rowId
+ let rowElements = [| rowElementULong token |]
+ encMapRows.Add rowElements
+
+ member _.StringHeapBytes =
+ match stringHeapBytesCache with
+ | Some bytes -> bytes
+ | None ->
+ let bytes = buildStringHeapBytes ()
+ stringHeapBytesCache <- Some bytes
+ bytes
+
+ member _.StringHeapOffsets = strings.EntryOffsets
+
+ member _.BlobHeapBytes =
+ match blobHeapBytesCache with
+ | Some bytes -> bytes
+ | None ->
+ let bytes = buildBlobHeapBytes ()
+ blobHeapBytesCache <- Some bytes
+ bytes
+
+ member _.BlobHeapOffsets = blobs.EntryOffsets
+
+ member _.GuidHeapBytes =
+ match guidHeapBytesCache with
+ | Some bytes -> bytes
+ | None ->
+ let bytes = buildGuidHeapBytes ()
+ guidHeapBytesCache <- Some bytes
+ bytes
+
+ member _.UserStringHeapBytes =
+ match userStringHeapBytesCache with
+ | Some bytes -> bytes
+ | None ->
+ let bytes = buildUserStringHeapBytes ()
+ userStringHeapBytesCache <- Some bytes
+ bytes
+
+ member this.StringHeapSize = this.StringHeapBytes.Length
+
+ member this.BlobHeapSize = this.BlobHeapBytes.Length
+
+ member this.GuidHeapSize = this.GuidHeapBytes.Length
+
+ member this.HeapSizes: MetadataHeapSizes =
+ {
+ StringHeapSize = this.StringHeapSize
+ UserStringHeapSize = this.UserStringHeapBytes.Length
+ BlobHeapSize = this.BlobHeapSize
+ GuidHeapSize = this.GuidHeapSize
+ }
+
+ member _.TableRows: TableRows =
+ {
+ Module = moduleRows.Entries
+ TypeDef = typeDefRows.Entries
+ NestedClass = nestedClassRows.Entries
+ InterfaceImpl = interfaceImplRows.Entries
+ Constant = constantRows.Entries
+ MethodImpl = methodImplRows.Entries
+ Field = fieldRows.Entries
+ MethodDef = methodRows.Entries
+ Param = paramRows.Entries
+ TypeRef = typeRefRows.Entries
+ MemberRef = memberRefRows.Entries
+ MethodSpec = methodSpecRows.Entries
+ TypeSpec = typeSpecRows.Entries
+ GenericParam = genericParamRows.Entries
+ GenericParamConstraint = genericParamConstraintRows.Entries
+ AssemblyRef = assemblyRefRows.Entries
+ StandAloneSig = standAloneSigRows.Entries
+ CustomAttribute = customAttributeRows.Entries
+ Property = propertyRows.Entries
+ Event = eventRows.Entries
+ PropertyMap = propertyMapRows.Entries
+ EventMap = eventMapRows.Entries
+ MethodSemantics = methodSemanticsRows.Entries
+ EncLog = encLogRows.Entries
+ EncMap = encMapRows.Entries
+ }
+
+ member _.HeapOffsets = heapOffsets
+
+ /// Returns an array of row counts indexed by table number.
+ /// Uses TableNames from BinaryConstants for ECMA-335 table indices.
+ member _.TableRowCounts: int[] =
+ let counts = Array.zeroCreate DeltaTokens.TableCount
+ counts[TableNames.Module.Index] <- moduleRows.Count
+ counts[TableNames.TypeDef.Index] <- typeDefRows.Count
+ counts[TableNames.Nested.Index] <- nestedClassRows.Count
+ counts[TableNames.InterfaceImpl.Index] <- interfaceImplRows.Count
+ counts[TableNames.Constant.Index] <- constantRows.Count
+ counts[TableNames.MethodImpl.Index] <- methodImplRows.Count
+ counts[TableNames.Field.Index] <- fieldRows.Count
+ counts[TableNames.Method.Index] <- methodRows.Count
+ counts[TableNames.Param.Index] <- paramRows.Count
+ counts[TableNames.TypeRef.Index] <- typeRefRows.Count
+ counts[TableNames.MemberRef.Index] <- memberRefRows.Count
+ counts[TableNames.MethodSpec.Index] <- methodSpecRows.Count
+ counts[TableNames.TypeSpec.Index] <- typeSpecRows.Count
+ counts[TableNames.GenericParam.Index] <- genericParamRows.Count
+ counts[TableNames.GenericParamConstraint.Index] <- genericParamConstraintRows.Count
+ counts[TableNames.AssemblyRef.Index] <- assemblyRefRows.Count
+ counts[TableNames.StandAloneSig.Index] <- standAloneSigRows.Count
+ counts[TableNames.CustomAttribute.Index] <- customAttributeRows.Count
+ counts[TableNames.Property.Index] <- propertyRows.Count
+ counts[TableNames.Event.Index] <- eventRows.Count
+ counts[TableNames.PropertyMap.Index] <- propertyMapRows.Count
+ counts[TableNames.EventMap.Index] <- eventMapRows.Count
+ counts[TableNames.MethodSemantics.Index] <- methodSemanticsRows.Count
+ counts[TableNames.ENCLog.Index] <- encLogRows.Count
+ counts[TableNames.ENCMap.Index] <- encMapRows.Count
+ counts
+
+ /// Add a user string literal to the delta's #US heap.
+ /// The offset parameter is the ABSOLUTE offset from IL tokens (baseline size + delta-local offset).
+ /// We convert to RELATIVE offset within the delta heap bytes, since the delta heap starts at 0
+ /// but the stream header will indicate it represents data starting at heapOffsets.UserStringHeapStart.
+ /// This matches how the runtime resolves tokens: absolute_token - stream_header_offset = position_in_delta_bytes.
+ member _.AddUserStringLiteral(offset: int, value: string) =
+ let start = heapOffsets.UserStringHeapStart
+ // Use >= to properly compute relative offset when offset equals the heap start
+ let relativeOffset = if offset >= start then offset - start else offset
+
+ if traceHeapOffsets.Value then
+ printfn
+ "[fsharp-hotreload][heap-offsets] AddUserStringLiteral: absolute offset=%d, heapStart=%d, relative=%d, value=%A%s"
+ offset
+ start
+ relativeOffset
+ (value.Substring(0, min 20 value.Length))
+ (if value.Length > 20 then "..." else "")
+
+ if offset <= start then
+ printfn
+ "[fsharp-hotreload][heap-offsets] WARNING: offset %d <= heapStart %d - this may indicate stale baseline!"
+ offset
+ start
+
+ userStrings.AddEntry(relativeOffset, value)
+ userStringHeapBytesCache <- None
+
+ // =========================================================================
+ // IMetadataHeaps interface implementation
+ // Provides unified heap access for code that works with both full assembly
+ // and delta emission.
+ // =========================================================================
+
+ /// Get the IMetadataHeaps interface for unified heap access.
+ member this.AsMetadataHeaps() : IMetadataHeaps =
+ { new IMetadataHeaps with
+ member _.GetStringHeapIdx s = addStringValue s
+ member _.GetBlobHeapIdx bytes = addBlobBytes bytes
+ member _.GetGuidIdx info = guids.AddSharedEntry info
+ member _.GetUserStringHeapIdx s = addUserStringValue s
+ }
diff --git a/src/Compiler/CodeGen/DeltaMetadataTypes.fs b/src/Compiler/CodeGen/DeltaMetadataTypes.fs
new file mode 100644
index 00000000000..1310fea66cb
--- /dev/null
+++ b/src/Compiler/CodeGen/DeltaMetadataTypes.fs
@@ -0,0 +1,323 @@
+module internal FSharp.Compiler.CodeGen.DeltaMetadataTypes
+
+open System
+open System.Reflection
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.HotReloadBaseline
+
+/// Minimal shared types for hot-reload metadata tables.
+type RowElementData =
+ {
+ Tag: int
+ Value: int
+ IsAbsolute: bool
+ }
+
+type MethodDefinitionRowInfo =
+ {
+ Key: MethodDefinitionKey
+ RowId: int
+ IsAdded: bool
+ /// Row id of the baseline TypeDef that receives an ADDED method. Required for added
+ /// rows: the CLR EnC applier (CMiniMdRW::ApplyDelta) reads the parent TypeDef from
+ /// the AddMethod EncLog entry and links the new method into that type's member list.
+ ParentTypeDefRowId: int option
+ Attributes: MethodAttributes
+ ImplAttributes: MethodImplAttributes
+ Name: string
+ NameOffset: StringOffset option
+ Signature: byte[]
+ SignatureOffset: BlobOffset option
+ FirstParameterRowId: int option
+ CodeRva: int option
+ }
+
+type ParameterDefinitionRowInfo =
+ {
+ Key: ParameterDefinitionKey
+ RowId: int
+ IsAdded: bool
+ Attributes: ParameterAttributes
+ SequenceNumber: int
+ Name: string option
+ NameOffset: StringOffset option
+ }
+
+/// Row model for a Field table entry emitted into a delta (ECMA-335 II.22.15:
+/// Flags, Name, Signature). Added fields additionally record the parent TypeDef
+/// row so the EncLog can emit the Roslyn-style AddField parent entry.
+type FieldDefinitionRowInfo =
+ {
+ Key: FieldDefinitionKey
+ RowId: int
+ IsAdded: bool
+ /// Row id of the baseline TypeDef that receives the field; used for the
+ /// EncLog (TypeDef, AddField) parent entry preceding the Field row.
+ ParentTypeDefRowId: int
+ Attributes: FieldAttributes
+ Name: string
+ NameOffset: StringOffset option
+ Signature: byte[]
+ SignatureOffset: BlobOffset option
+ }
+
+/// Row model for an ADDED TypeDef table entry emitted into a delta (ECMA-335
+/// II.22.37: Flags, TypeName, TypeNamespace, Extends, FieldList, MethodList).
+/// Roslyn parity (DeltaMetadataWriter.GetFirstFieldDefinitionHandle /
+/// GetFirstMethodDefinitionHandle return default in EnC deltas): the
+/// FieldList/MethodList columns are always written as 0 — members are linked
+/// to the new type through the AddField/AddMethod EncLog parent entries.
+type TypeDefinitionRowInfo =
+ {
+ /// Full name of the added type (namespace-qualified, '+'-nested), used as the
+ /// baseline TypeTokens key when chaining the next-generation baseline.
+ FullName: string
+ RowId: int
+ Attributes: TypeAttributes
+ Name: string
+ NameOffset: StringOffset option
+ Namespace: string
+ NamespaceOffset: StringOffset option
+ /// Base type, remapped to baseline/delta rows. None encodes the nil
+ /// TypeDefOrRef (interfaces / ).
+ Extends: TypeDefOrRef option
+ /// Row id of the enclosing TypeDef when the added type is nested; drives the
+ /// NestedClass row the writer emits alongside the TypeDef row.
+ EnclosingTypeDefRowId: int option
+ }
+
+/// Row model for a NestedClass table entry (ECMA-335 II.22.32: NestedClass,
+/// EnclosingClass — both TypeDef row indices). Emitted for added nested types;
+/// logged as a plain Default EncLog entry (Roslyn parity).
+type NestedClassRowInfo =
+ {
+ RowId: int
+ NestedTypeDefRowId: int
+ EnclosingTypeDefRowId: int
+ }
+
+/// Row model for an InterfaceImpl table entry (ECMA-335 II.22.23: Class — a TypeDef row
+/// index — and Interface — a TypeDefOrRef coded index). Emitted for the interfaces
+/// implemented by ADDED types (records/unions implement IComparable/IEquatable and
+/// friends); logged as a plain Default EncLog entry trailing the log and listed in
+/// EncMap as an add (C# 'new_class' reference template: InterfaceImpl 0x09000001 trails
+/// the generation-1 log of a new class implementing IDisposable).
+type InterfaceImplRowInfo =
+ {
+ RowId: int
+ ClassTypeDefRowId: int
+ Interface: TypeDefOrRef
+ }
+
+/// Row model for a MethodImpl table entry (ECMA-335 II.22.27: Class — a TypeDef row
+/// index — MethodBody and MethodDeclaration — MethodDefOrRef coded indexes). Emitted
+/// for the explicit interface implementations of ADDED types (F# classes implement
+/// interfaces explicitly, so unlike C#'s implicit public mapping every implemented
+/// interface slot carries a MethodImpl row).
+type MethodImplRowInfo =
+ {
+ RowId: int
+ ClassTypeDefRowId: int
+ MethodBody: MethodDefOrRef
+ MethodDeclaration: MethodDefOrRef
+ }
+
+/// Row model for a Constant table entry (ECMA-335 II.22.9: Type — a 1-byte
+/// ELEMENT_TYPE code followed by a zero padding byte — Parent — a HasConstant coded
+/// index — and Value — a #Blob offset). Emitted for the literal (HasDefault) fields
+/// of ADDED types and members: enum members, union Tags holder constants, []
+/// module values. Logged as plain Default EncLog entries trailing the log and listed
+/// in EncMap as adds (C# 'new_enum' reference template: the three Constant rows of an
+/// added enum trail the generation-1 log, parents are the new Field rows, value blobs
+/// live in the delta #Blob heap).
+type ConstantRowInfo =
+ {
+ RowId: int
+ /// ELEMENT_TYPE constant type code (ECMA-335 II.23.1.16, e.g. 0x08 = I4).
+ TypeCode: byte
+ Parent: HasConstant
+ Value: byte[]
+ }
+
+type TypeReferenceRowInfo =
+ {
+ RowId: int
+ ResolutionScope: ResolutionScope
+ Name: string
+ NameOffset: StringOffset option
+ Namespace: string
+ NamespaceOffset: StringOffset option
+ }
+
+type MemberReferenceRowInfo =
+ {
+ RowId: int
+ Parent: MemberRefParent
+ Name: string
+ NameOffset: StringOffset option
+ Signature: byte[]
+ SignatureOffset: BlobOffset option
+ }
+
+type MethodSpecificationRowInfo =
+ {
+ RowId: int
+ Method: MethodDefOrRef
+ Signature: byte[]
+ SignatureOffset: BlobOffset option
+ }
+
+/// Row model for a TypeSpec table entry (ECMA-335 II.22.39: a single #Blob signature
+/// column carrying a bare Type, II.23.2.14). Appended with a plain Default EncLog entry
+/// (C# reference template parity) when an edit references a generic instantiation that
+/// has no matching baseline row — e.g. an added lambda whose closure class extends a
+/// brand-new FSharpFunc instantiation.
+type TypeSpecificationRowInfo =
+ {
+ RowId: int
+ Signature: byte[]
+ SignatureOffset: BlobOffset option
+ }
+
+/// Row model for a GenericParam table entry (ECMA-335 II.22.20: Number (u2),
+/// Flags (u2), Owner (TypeOrMethodDef coded index), Name (#Strings)). Emitted for
+/// the generic parameters of ADDED generic methods (and added generic types).
+/// Logged as a plain Default EncLog entry and listed in EncMap as an add — the
+/// recorded C# reference template (csharp_enc_reference 'generic_method_add')
+/// shows 'GenericParam 0x2a000001 Default' trailing the AddMethod/AddParameter
+/// pairs, with the row present in EncMap. GenericParam rows of UPDATED methods
+/// are baseline rows and are never re-emitted.
+type GenericParamRowInfo =
+ {
+ RowId: int
+ /// Zero-based ordinal of the generic parameter within its owner.
+ Number: int
+ Attributes: GenericParameterAttributes
+ Owner: TypeOrMethodDef
+ Name: string
+ NameOffset: StringOffset option
+ }
+
+/// Row model for a GenericParamConstraint table entry (ECMA-335 II.22.21: Owner — a
+/// GenericParam row index — and Constraint — a TypeDefOrRef coded index). Emitted for
+/// the IL constraints of ADDED generic definitions' type parameters; logged as a plain
+/// Default EncLog entry after the GenericParam entries and listed in EncMap as an add
+/// (C# reference template 'generic_constraint_add': GenericParamConstraint 0x2c000001
+/// Default trailing the GenericParam entry).
+type GenericParamConstraintRowInfo =
+ {
+ RowId: int
+ OwnerGenericParamRowId: int
+ Constraint: TypeDefOrRef
+ }
+
+type AssemblyReferenceRowInfo =
+ {
+ RowId: int
+ Version: Version
+ Flags: AssemblyFlags
+ PublicKeyOrToken: byte[]
+ PublicKeyOrTokenOffset: BlobOffset option
+ Name: string
+ NameOffset: StringOffset option
+ Culture: string option
+ CultureOffset: StringOffset option
+ HashValue: byte[]
+ HashValueOffset: BlobOffset option
+ }
+
+type CustomAttributeRowInfo =
+ {
+ RowId: int
+ Parent: HasCustomAttribute
+ Constructor: CustomAttributeType
+ Value: byte[]
+ ValueOffset: BlobOffset option
+ }
+
+type PropertyDefinitionRowInfo =
+ {
+ Key: PropertyDefinitionKey
+ RowId: int
+ IsAdded: bool
+ /// PropertyMap row id owning an ADDED property; the AddProperty EncLog entry must
+ /// carry the parent PropertyMap token (CLR links via AddPropertyToPropertyMap).
+ ParentPropertyMapRowId: int option
+ Name: string
+ NameOffset: StringOffset option
+ Signature: byte[]
+ SignatureOffset: BlobOffset option
+ Attributes: PropertyAttributes
+ }
+
+type EventDefinitionRowInfo =
+ {
+ Key: EventDefinitionKey
+ RowId: int
+ IsAdded: bool
+ /// EventMap row id owning an ADDED event; the AddEvent EncLog entry must carry the
+ /// parent EventMap token (CLR links via AddEventToEventMap).
+ ParentEventMapRowId: int option
+ Name: string
+ NameOffset: StringOffset option
+ Attributes: EventAttributes
+ EventType: TypeDefOrRef
+ }
+
+type PropertyMapRowInfo =
+ {
+ DeclaringType: string
+ RowId: int
+ TypeDefRowId: int
+ FirstPropertyRowId: int option
+ IsAdded: bool
+ }
+
+type EventMapRowInfo =
+ {
+ DeclaringType: string
+ RowId: int
+ TypeDefRowId: int
+ FirstEventRowId: int option
+ IsAdded: bool
+ }
+
+type MethodSemanticsMetadataUpdate =
+ {
+ RowId: int
+ MethodToken: int
+ Attributes: MethodSemanticsAttributes
+ IsAdded: bool
+ /// Association info is required - provides property/event key and rowId
+ AssociationInfo: MethodSemanticsAssociation
+ }
+
+type TableRows =
+ {
+ Module: RowElementData[][]
+ TypeDef: RowElementData[][]
+ NestedClass: RowElementData[][]
+ InterfaceImpl: RowElementData[][]
+ Constant: RowElementData[][]
+ MethodImpl: RowElementData[][]
+ Field: RowElementData[][]
+ MethodDef: RowElementData[][]
+ Param: RowElementData[][]
+ TypeRef: RowElementData[][]
+ MemberRef: RowElementData[][]
+ MethodSpec: RowElementData[][]
+ TypeSpec: RowElementData[][]
+ GenericParam: RowElementData[][]
+ GenericParamConstraint: RowElementData[][]
+ AssemblyRef: RowElementData[][]
+ StandAloneSig: RowElementData[][]
+ CustomAttribute: RowElementData[][]
+ Property: RowElementData[][]
+ Event: RowElementData[][]
+ PropertyMap: RowElementData[][]
+ EventMap: RowElementData[][]
+ MethodSemantics: RowElementData[][]
+ EncLog: RowElementData[][]
+ EncMap: RowElementData[][]
+ }
diff --git a/src/Compiler/CodeGen/DeltaTableLayout.fs b/src/Compiler/CodeGen/DeltaTableLayout.fs
new file mode 100644
index 00000000000..f55fa8fee2d
--- /dev/null
+++ b/src/Compiler/CodeGen/DeltaTableLayout.fs
@@ -0,0 +1,94 @@
+// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
+
+/// Computes metadata table bit masks for delta emission.
+///
+/// The #~ stream header contains two 64-bit masks:
+/// - Valid: which tables have rows (bit set = table present)
+/// - Sorted: which tables are sorted (per ECMA-335)
+///
+/// Uses TableNames from BinaryConstants.fs for ECMA-335 metadata tables,
+/// and DeltaTokens for Portable PDB tables (which aren't in TableNames).
+module internal FSharp.Compiler.CodeGen.DeltaTableLayout
+
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+
+type TableBitMasks =
+ {
+ ValidLow: int
+ ValidHigh: int
+ SortedLow: int
+ SortedHigh: int
+ }
+
+// -------------------------------------------------------------------------
+// Sorted Tables (per ECMA-335 II.22)
+// -------------------------------------------------------------------------
+// These tables must be sorted by their primary key column for binary search.
+// The sorted bit mask indicates which tables the runtime can expect to be sorted.
+
+/// ECMA-335 metadata tables that are sorted by primary key
+let private sortedTypeSystemTables =
+ [
+ TableNames.InterfaceImpl.Index // Sorted by Class column
+ TableNames.Constant.Index // Sorted by Parent column
+ TableNames.CustomAttribute.Index // Sorted by Parent column
+ TableNames.FieldMarshal.Index // Sorted by Parent column
+ TableNames.Permission.Index // Sorted by Parent column (DeclSecurity)
+ TableNames.ClassLayout.Index // Sorted by Parent column
+ TableNames.FieldLayout.Index // Sorted by Field column
+ TableNames.MethodSemantics.Index // Sorted by Association column
+ TableNames.MethodImpl.Index // Sorted by Class column
+ TableNames.ImplMap.Index // Sorted by MemberForwarded column
+ TableNames.FieldRVA.Index // Sorted by Field column
+ TableNames.Nested.Index // Sorted by NestedClass column
+ TableNames.GenericParam.Index // Sorted by Owner column
+ TableNames.GenericParamConstraint.Index
+ ] // Sorted by Owner column
+
+/// Portable PDB tables that are sorted (not in TableNames, use DeltaTokens)
+let private sortedDebugTables =
+ [
+ DeltaTokens.tableLocalScope // 0x32: Sorted by Method column
+ DeltaTokens.tableStateMachineMethod // 0x36: Sorted by MoveNextMethod column
+ DeltaTokens.tableCustomDebugInformation
+ ] // 0x37: Sorted by Parent column
+
+let private maskForTables (tables: int list) =
+ tables |> List.fold (fun acc tableIndex -> acc ||| (1UL <<< tableIndex)) 0UL
+
+let private sortedTypeSystemMask = maskForTables sortedTypeSystemTables
+let private sortedDebugMask = maskForTables sortedDebugTables
+
+let private toLow (mask: uint64) = int (mask &&& 0xFFFFFFFFUL)
+let private toHigh (mask: uint64) = int ((mask >>> 32) &&& 0xFFFFFFFFUL)
+
+/// Compute Valid and Sorted bit masks for the #~ stream header.
+///
+/// For EnC deltas, CustomAttribute is excluded from the sorted mask
+/// to match Roslyn's behavior (it's not pre-sorted in deltas).
+let computeBitMasks (tableRowCounts: int[]) (isEncDelta: bool) : TableBitMasks =
+ // Valid mask: bit set for each table with rows
+ let presentMask =
+ tableRowCounts
+ |> Array.mapi (fun index count -> if count <> 0 then 1UL <<< index else 0UL)
+ |> Array.fold (|||) 0UL
+
+ // Sorted mask: which present tables are sorted
+ let typeSystemMask =
+ if isEncDelta then
+ // Roslyn clears CustomAttribute for EnC deltas to mirror MetadataSizes.
+ // CustomAttribute table in deltas is appended, not globally sorted.
+ sortedTypeSystemMask &&& ~~~(1UL <<< TableNames.CustomAttribute.Index)
+ else
+ sortedTypeSystemMask
+
+ // Combine type system sorted tables with present debug tables that are sorted
+ let sortedMask = typeSystemMask ||| (presentMask &&& sortedDebugMask)
+
+ {
+ ValidLow = toLow presentMask
+ ValidHigh = toHigh presentMask
+ SortedLow = toLow sortedMask
+ SortedHigh = toHigh sortedMask
+ }
diff --git a/src/Compiler/CodeGen/EncMethodDebugInformation.fs b/src/Compiler/CodeGen/EncMethodDebugInformation.fs
new file mode 100644
index 00000000000..b0aa5433829
--- /dev/null
+++ b/src/Compiler/CodeGen/EncMethodDebugInformation.fs
@@ -0,0 +1,962 @@
+/// 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
+ ->
+ let key = ((parent + 1) <<< 16) ||| ordinal
+ if key <= MaxOccurrenceKey then Some 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 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()
+
+ if nameCount < 0 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/FSharpDeltaMetadataWriter.fs b/src/Compiler/CodeGen/FSharpDeltaMetadataWriter.fs
new file mode 100644
index 00000000000..8c8c1bebd73
--- /dev/null
+++ b/src/Compiler/CodeGen/FSharpDeltaMetadataWriter.fs
@@ -0,0 +1,846 @@
+module internal FSharp.Compiler.CodeGen.FSharpDeltaMetadataWriter
+
+open System
+open System.Collections.Generic
+open FSharp.Compiler.EnvironmentHelpers
+open Microsoft.FSharp.Collections
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.IlxDeltaStreams
+open FSharp.Compiler.HotReloadBaseline
+open FSharp.Compiler.CodeGen.DeltaMetadataTables
+open FSharp.Compiler.CodeGen.DeltaMetadataTypes
+open FSharp.Compiler.CodeGen.DeltaTableLayout
+open FSharp.Compiler.CodeGen.DeltaMetadataSerializer
+
+[]
+let private TraceMetadataFlagName = "FSHARP_HOTRELOAD_TRACE_METADATA"
+
+[]
+let private TraceHeapsFlagName = "FSHARP_HOTRELOAD_TRACE_HEAPS"
+
+[]
+let private TraceMethodsFlagName = "FSHARP_HOTRELOAD_TRACE_METHODS"
+
+let private shouldTraceMetadata () = isEnvVarTruthy TraceMetadataFlagName
+
+let private shouldTraceHeaps () = isEnvVarTruthy TraceHeapsFlagName
+
+let private shouldTraceMethodRows () = isEnvVarTruthy TraceMethodsFlagName
+
+type MethodDefinitionRowInfo = DeltaMetadataTypes.MethodDefinitionRowInfo
+
+type ParameterDefinitionRowInfo = DeltaMetadataTypes.ParameterDefinitionRowInfo
+
+type FieldDefinitionRowInfo = DeltaMetadataTypes.FieldDefinitionRowInfo
+
+type MethodMetadataUpdate =
+ {
+ MethodKey: MethodDefinitionKey
+ MethodToken: int
+ MethodHandle: MethodDefHandle
+ Body: MethodBodyUpdate
+ }
+
+type PropertyDefinitionRowInfo = DeltaMetadataTypes.PropertyDefinitionRowInfo
+
+type EventDefinitionRowInfo = DeltaMetadataTypes.EventDefinitionRowInfo
+
+type MethodSpecificationRowInfo = DeltaMetadataTypes.MethodSpecificationRowInfo
+
+type TypeSpecificationRowInfo = DeltaMetadataTypes.TypeSpecificationRowInfo
+
+type GenericParamRowInfo = DeltaMetadataTypes.GenericParamRowInfo
+
+type GenericParamConstraintRowInfo = DeltaMetadataTypes.GenericParamConstraintRowInfo
+
+type PropertyMapRowInfo = DeltaMetadataTypes.PropertyMapRowInfo
+
+type EventMapRowInfo = DeltaMetadataTypes.EventMapRowInfo
+
+type MethodSemanticsMetadataUpdate = DeltaMetadataTypes.MethodSemanticsMetadataUpdate
+type StandaloneSignatureUpdate = FSharp.Compiler.IlxDeltaStreams.StandaloneSignatureUpdate
+
+/// Result of delta metadata emission.
+/// Contains serialized metadata bytes and all supporting data structures.
+type MetadataDelta =
+ {
+ Metadata: byte[]
+ StringHeap: byte[]
+ BlobHeap: byte[]
+ GuidHeap: byte[]
+ /// EncLog entries: (table, rowId, operation) using TableName from BinaryConstants
+ EncLog: (TableName * int * EditAndContinueOperation) array
+ /// EncMap entries: (table, rowId) using TableName from BinaryConstants
+ EncMap: (TableName * int) array
+ TableRowCounts: int[]
+ HeapSizes: MetadataHeapSizes
+ HeapOffsets: MetadataHeapOffsets
+ Tables: TableRows
+ TableBitMasks: TableBitMasks
+ IndexSizes: DeltaIndexSizing.CodedIndexSizes
+ TableStream: DeltaTableStream
+ /// The EncId GUID for this generation (used as EncBaseId for subsequent generations)
+ GenerationId: Guid
+ /// The EncBaseId GUID (EncId of the previous generation, or Empty for generation 1)
+ BaseGenerationId: Guid
+ }
+
+let emitWithTypeDefinitions
+ (moduleName: string)
+ (moduleNameOffset: StringOffset option)
+ (generation: int)
+ (encId: Guid)
+ (encBaseId: Guid)
+ (moduleId: Guid)
+ (typeDefinitionRows: TypeDefinitionRowInfo list)
+ (nestedClassRows: NestedClassRowInfo list)
+ (interfaceImplRows: InterfaceImplRowInfo list)
+ (methodImplRows: MethodImplRowInfo list)
+ (constantRows: ConstantRowInfo list)
+ (methodDefinitionRows: MethodDefinitionRowInfo list)
+ (parameterDefinitionRows: ParameterDefinitionRowInfo list)
+ (fieldDefinitionRows: FieldDefinitionRowInfo list)
+ (typeReferenceRows: TypeReferenceRowInfo list)
+ (memberReferenceRows: MemberReferenceRowInfo list)
+ (methodSpecificationRows: MethodSpecificationRowInfo list)
+ (typeSpecificationRows: TypeSpecificationRowInfo list)
+ (genericParamRows: GenericParamRowInfo list)
+ (genericParamConstraintRows: GenericParamConstraintRowInfo list)
+ (assemblyReferenceRows: AssemblyReferenceRowInfo list)
+ (propertyDefinitionRows: PropertyDefinitionRowInfo list)
+ (eventDefinitionRows: EventDefinitionRowInfo list)
+ (propertyMapRows: PropertyMapRowInfo list)
+ (eventMapRows: EventMapRowInfo list)
+ (methodSemanticsRows: MethodSemanticsMetadataUpdate list)
+ (standaloneSignatureRows: StandaloneSignatureUpdate list)
+ (customAttributeRows: CustomAttributeRowInfo list)
+ (userStringUpdates: (int * int * string) list)
+ (updates: MethodMetadataUpdate list)
+ (heapOffsets: MetadataHeapOffsets)
+ (externalRowCounts: int[])
+ : MetadataDelta =
+ if shouldTraceMetadata () then
+ printfn "[fsharp-hotreload][metadata-writer] emit invoked updates=%d" (List.length updates)
+
+ for row in methodDefinitionRows do
+ let offset =
+ match row.NameOffset with
+ | Some(StringOffset o) -> Some o
+ | None -> None
+
+ printfn "[fsharp-hotreload][metadata-writer] method-row name=%s isAdded=%b offset=%A" row.Name row.IsAdded offset
+
+ let normalizedExternalRowCounts =
+ if externalRowCounts.Length = DeltaTokens.TableCount then
+ externalRowCounts
+ else
+ Array.zeroCreate DeltaTokens.TableCount
+
+ // A delta can carry row additions without any method-body update: a []
+ // instance field appends a Field row but changes no constructor. Only
+ // short-circuit when there is genuinely nothing to write.
+ let hasRowPayload =
+ not (List.isEmpty updates)
+ || not (List.isEmpty typeDefinitionRows)
+ || not (List.isEmpty nestedClassRows)
+ || not (List.isEmpty methodDefinitionRows)
+ || not (List.isEmpty parameterDefinitionRows)
+ || not (List.isEmpty fieldDefinitionRows)
+ || not (List.isEmpty typeReferenceRows)
+ || not (List.isEmpty memberReferenceRows)
+ || not (List.isEmpty methodSpecificationRows)
+ || not (List.isEmpty typeSpecificationRows)
+ || not (List.isEmpty genericParamRows)
+ || not (List.isEmpty genericParamConstraintRows)
+ || not (List.isEmpty assemblyReferenceRows)
+ || not (List.isEmpty interfaceImplRows)
+ || not (List.isEmpty methodImplRows)
+ || not (List.isEmpty constantRows)
+ || not (List.isEmpty propertyDefinitionRows)
+ || not (List.isEmpty eventDefinitionRows)
+ || not (List.isEmpty propertyMapRows)
+ || not (List.isEmpty eventMapRows)
+ || not (List.isEmpty methodSemanticsRows)
+ || not (List.isEmpty standaloneSignatureRows)
+ || not (List.isEmpty customAttributeRows)
+
+ if not hasRowPayload then
+ let emptyMirror = DeltaMetadataTables(heapOffsets)
+
+ let emptySizes =
+ DeltaMetadataSerializer.computeMetadataSizes emptyMirror normalizedExternalRowCounts
+
+ {
+ Metadata = Array.empty
+ StringHeap = Array.empty
+ BlobHeap = Array.empty
+ GuidHeap = Array.empty
+ EncLog = Array.empty
+ EncMap = Array.empty
+ TableRowCounts = emptySizes.RowCounts
+ HeapSizes = emptySizes.HeapSizes
+ HeapOffsets = heapOffsets
+ Tables = emptyMirror.TableRows
+ TableBitMasks = emptySizes.BitMasks
+ IndexSizes = emptySizes.IndexSizes
+ TableStream =
+ {
+ Bytes = Array.empty
+ UnpaddedSize = 0
+ PaddedSize = 0
+ }
+ GenerationId = encId
+ BaseGenerationId = encBaseId
+ }
+ else
+
+ if shouldTraceMetadata () then
+ printfn
+ "[fsharp-hotreload][metadata-writer] generation=%d moduleId=%A encId=%A encBaseId=%A"
+ generation
+ moduleId
+ encId
+ encBaseId
+
+ let tableMirror = DeltaMetadataTables(heapOffsets)
+ tableMirror.AddModuleRow(moduleName, moduleNameOffset, generation, moduleId, encId, encBaseId)
+
+ let updatesByKey =
+ Dictionary(HashIdentity.Structural)
+
+ for update in updates do
+ updatesByKey[update.MethodKey] <- update
+
+ // Build EncLog and EncMap entries using TableName for type safety.
+ // EncLog records each modification; EncMap provides sorted token listing.
+ let mutable encLog =
+ ResizeArray()
+
+ let mutable encMap = ResizeArray()
+
+ // Module row is always present in deltas
+ encLog.Add(struct (TableNames.Module, 1, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.Module, 1))
+
+ // ---------------------------------------------------------------------------------
+ // EncLog shape for ADDED members (Roslyn DeltaMetadataWriter.PopulateEncLogTableRows
+ // parity, verified against a hotreload-delta-gen C# reference delta and the CLR's
+ // EnC applier CMiniMdRW::ApplyDelta): an added member is logged as its PARENT row
+ // tagged with the Add* operation, immediately followed by the new member row with
+ // the Default operation. The runtime reads the parent token from the Add* entry and
+ // links the member created by the FOLLOWING entry into the parent's member list, so
+ // each pair must stay adjacent and the parent must already exist when processed:
+ // AddMethod / AddField -> parent TypeDef row
+ // AddParameter -> parent MethodDef row
+ // AddProperty/AddEvent -> parent PropertyMap/EventMap row
+ // Only the added member row (never the parent entry) appears in EncMap.
+ // ---------------------------------------------------------------------------------
+ let methodEncLogEntries =
+ ResizeArray()
+
+ let methodRowsByKey =
+ Dictionary(HashIdentity.Structural)
+
+ // Added TypeDef rows are logged as plain Default entries (the row content is
+ // applied via ApplyTableDelta, like PropertyMap/EventMap rows) and MUST precede
+ // every AddField/AddMethod entry that names them as the parent. C# reference
+ // (csharp_enc_reference, added capturing lambda -> new display class): the new
+ // TypeDef row's Default entry comes immediately before its AddField/AddMethod
+ // member pairs; the NestedClass row trails at the end of the log.
+ let typeDefEncLogEntries =
+ ResizeArray()
+
+ for row in typeDefinitionRows |> List.sortBy (fun row -> row.RowId) do
+ tableMirror.AddTypeDefinitionRow row
+ typeDefEncLogEntries.Add(struct (TableNames.TypeDef, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.TypeDef, row.RowId))
+
+ let nestedClassEncLogEntries =
+ ResizeArray()
+
+ for row in nestedClassRows |> List.sortBy (fun row -> row.RowId) do
+ tableMirror.AddNestedClassRow row
+ nestedClassEncLogEntries.Add(struct (TableNames.Nested, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.Nested, row.RowId))
+
+ // InterfaceImpl/MethodImpl rows of ADDED types are plain Default adds applied via
+ // ApplyTableDelta. The C# 'new_class' reference template logs the InterfaceImpl
+ // row trailing the generation-1 log; MethodImpl rows (F#'s explicit interface
+ // implementations) follow the same shape.
+ let interfaceImplEncLogEntries =
+ ResizeArray()
+
+ for row in interfaceImplRows |> List.sortBy (fun row -> row.RowId) do
+ tableMirror.AddInterfaceImplRow row
+ interfaceImplEncLogEntries.Add(struct (TableNames.InterfaceImpl, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.InterfaceImpl, row.RowId))
+
+ let methodImplEncLogEntries =
+ ResizeArray()
+
+ for row in methodImplRows |> List.sortBy (fun row -> row.RowId) do
+ tableMirror.AddMethodImplRow row
+ methodImplEncLogEntries.Add(struct (TableNames.MethodImpl, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.MethodImpl, row.RowId))
+
+ // Constant rows (literal values of ADDED fields) are plain Default adds trailing
+ // the log: the C# 'new_enum' reference template logs the three Constant rows of
+ // an added enum LAST, after the member pairs and the updated-method rows.
+ let constantEncLogEntries =
+ ResizeArray()
+
+ for row in constantRows |> List.sortBy (fun row -> row.RowId) do
+ tableMirror.AddConstantRow row
+ constantEncLogEntries.Add(struct (TableNames.Constant, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.Constant, row.RowId))
+
+ for row in methodDefinitionRows do
+ match updatesByKey.TryGetValue row.Key with
+ | true, update ->
+ tableMirror.AddMethodRow(row, update.Body)
+ methodRowsByKey[row.Key] <- row
+
+ if shouldTraceMethodRows () then
+ printfn
+ "[fsharp-hotreload][writer] method-row key=%s::%s rowId=%d isAdded=%b"
+ row.Key.DeclaringType
+ row.Key.Name
+ row.RowId
+ row.IsAdded
+
+ if row.IsAdded then
+ match row.ParentTypeDefRowId with
+ | Some parentRowId ->
+ methodEncLogEntries.Add(struct (TableNames.TypeDef, parentRowId, EditAndContinueOperation.AddMethod))
+ | None ->
+ invalidOp
+ $"Added method '{row.Key.DeclaringType}::{row.Key.Name}' has no parent TypeDef row id; the AddMethod EncLog entry cannot be emitted."
+
+ methodEncLogEntries.Add(struct (TableNames.Method, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.Method, row.RowId))
+ | _ ->
+ if shouldTraceMetadata () then
+ printfn "[fsharp-hotreload][metadata-writer] missing update payload for %A" row.Key
+
+ let parameterEncLogEntries =
+ ResizeArray()
+
+ for row in parameterDefinitionRows do
+ tableMirror.AddParameterRow row
+
+ if row.IsAdded then
+ match methodRowsByKey.TryGetValue row.Key.Method with
+ | true, methodRow ->
+ parameterEncLogEntries.Add(struct (TableNames.Method, methodRow.RowId, EditAndContinueOperation.AddParameter))
+ | _ ->
+ invalidOp
+ $"Added parameter (sequence {row.SequenceNumber}) of '{row.Key.Method.DeclaringType}::{row.Key.Method.Name}' has no method row; the AddParameter EncLog entry cannot be emitted."
+
+ parameterEncLogEntries.Add(struct (TableNames.Param, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.Param, row.RowId))
+
+ for row in fieldDefinitionRows do
+ if row.IsAdded then
+ tableMirror.AddFieldRow row
+ encMap.Add(struct (TableNames.Field, row.RowId))
+
+ let fieldEncLogPairs =
+ fieldDefinitionRows
+ |> List.filter (fun row -> row.IsAdded)
+ |> List.sortBy (fun row -> row.RowId)
+ |> List.collect (fun row ->
+ [
+ struct (TableNames.TypeDef, row.ParentTypeDefRowId, EditAndContinueOperation.AddField)
+ struct (TableNames.Field, row.RowId, EditAndContinueOperation.Default)
+ ])
+
+ for row in typeReferenceRows do
+ tableMirror.AddTypeReferenceRow row
+
+ encLog.Add(struct (TableNames.TypeRef, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.TypeRef, row.RowId))
+
+ for row in memberReferenceRows do
+ tableMirror.AddMemberReferenceRow row
+
+ encLog.Add(struct (TableNames.MemberRef, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.MemberRef, row.RowId))
+
+ for row in methodSpecificationRows do
+ tableMirror.AddMethodSpecificationRow row
+
+ encLog.Add(struct (TableNames.MethodSpec, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.MethodSpec, row.RowId))
+
+ // Appended TypeSpec rows (new generic instantiations) are plain Default adds
+ // applied via ApplyTableDelta, exactly like the C# reference template's
+ // "TypeSpec 0x1b00xxxx Default" entry for an added-lambda delta.
+ for row in typeSpecificationRows do
+ tableMirror.AddTypeSpecificationRow row
+
+ encLog.Add(struct (TableNames.TypeSpec, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.TypeSpec, row.RowId))
+
+ // GenericParam rows of ADDED generic methods/types are plain Default adds applied
+ // via ApplyTableDelta — the C# reference template ('generic_method_add') logs
+ // 'GenericParam 0x2a000001 Default' trailing the AddMethod/AddParameter pairs and
+ // lists the row in EncMap. Kept as a dedicated group appended after the parameter
+ // pairs so the owning method rows are already logged.
+ let genericParamEncLogEntries =
+ ResizeArray()
+
+ for row in genericParamRows |> List.sortBy (fun row -> row.RowId) do
+ tableMirror.AddGenericParamRow row
+ genericParamEncLogEntries.Add(struct (TableNames.GenericParam, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.GenericParam, row.RowId))
+
+ // GenericParamConstraint rows of ADDED generic definitions are plain Default
+ // adds trailing the GenericParam entries (C# reference template
+ // 'generic_constraint_add': GenericParamConstraint 0x2c000001 Default follows
+ // GenericParam 0x2a000001 Default; both EncMap adds).
+ for row in genericParamConstraintRows |> List.sortBy (fun row -> row.RowId) do
+ tableMirror.AddGenericParamConstraintRow row
+ genericParamEncLogEntries.Add(struct (TableNames.GenericParamConstraint, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.GenericParamConstraint, row.RowId))
+
+ for row in assemblyReferenceRows do
+ tableMirror.AddAssemblyReferenceRow row
+
+ encLog.Add(struct (TableNames.AssemblyRef, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.AssemblyRef, row.RowId))
+
+ for signature in standaloneSignatureRows do
+ let rowId = signature.RowId
+ tableMirror.AddStandaloneSignatureRow(signature.Blob)
+
+ let operation = EditAndContinueOperation.Default
+ encLog.Add(struct (TableNames.StandAloneSig, rowId, operation))
+ encMap.Add(struct (TableNames.StandAloneSig, rowId))
+
+ for row in customAttributeRows do
+ tableMirror.AddCustomAttributeRow row
+
+ encLog.Add(struct (TableNames.CustomAttribute, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.CustomAttribute, row.RowId))
+
+ // Newly created PropertyMap/EventMap rows are logged as plain Default entries (the
+ // row content is applied via ApplyTableDelta) and MUST precede the AddProperty /
+ // AddEvent entries that reference them as parents.
+ let propertyMapEncLogEntries =
+ ResizeArray()
+
+ let propertyMapRowIdByType = Dictionary(StringComparer.Ordinal)
+
+ for row in propertyMapRows do
+ if row.IsAdded then
+ tableMirror.AddPropertyMapRow row
+ propertyMapEncLogEntries.Add(struct (TableNames.PropertyMap, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.PropertyMap, row.RowId))
+
+ propertyMapRowIdByType[row.DeclaringType] <- row.RowId
+
+ let eventMapEncLogEntries =
+ ResizeArray()
+
+ let eventMapRowIdByType = Dictionary(StringComparer.Ordinal)
+
+ for row in eventMapRows do
+ if row.IsAdded then
+ tableMirror.AddEventMapRow row
+ eventMapEncLogEntries.Add(struct (TableNames.EventMap, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.EventMap, row.RowId))
+
+ eventMapRowIdByType[row.DeclaringType] <- row.RowId
+
+ let propertyEncLogEntries =
+ ResizeArray()
+
+ for row in propertyDefinitionRows do
+ if row.IsAdded then
+ tableMirror.AddPropertyRow row
+
+ let parentMapRowId =
+ match row.ParentPropertyMapRowId with
+ | Some rowId -> rowId
+ | None ->
+ match propertyMapRowIdByType.TryGetValue row.Key.DeclaringType with
+ | true, rowId -> rowId
+ | _ ->
+ invalidOp
+ $"Added property '{row.Key.DeclaringType}::{row.Key.Name}' has no parent PropertyMap row id; the AddProperty EncLog entry cannot be emitted."
+
+ propertyEncLogEntries.Add(struct (TableNames.PropertyMap, parentMapRowId, EditAndContinueOperation.AddProperty))
+ propertyEncLogEntries.Add(struct (TableNames.Property, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.Property, row.RowId))
+
+ let eventEncLogEntries =
+ ResizeArray()
+
+ for row in eventDefinitionRows do
+ if row.IsAdded then
+ tableMirror.AddEventRow row
+
+ let parentMapRowId =
+ match row.ParentEventMapRowId with
+ | Some rowId -> rowId
+ | None ->
+ match eventMapRowIdByType.TryGetValue row.Key.DeclaringType with
+ | true, rowId -> rowId
+ | _ ->
+ invalidOp
+ $"Added event '{row.Key.DeclaringType}::{row.Key.Name}' has no parent EventMap row id; the AddEvent EncLog entry cannot be emitted."
+
+ eventEncLogEntries.Add(struct (TableNames.EventMap, parentMapRowId, EditAndContinueOperation.AddEvent))
+ eventEncLogEntries.Add(struct (TableNames.Event, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.Event, row.RowId))
+
+ // MethodSemantics rows are logged as plain Default entries (Roslyn parity); the CLR
+ // applies them via ApplyTableDelta like any other appended row.
+ let methodSemanticsEncLogEntries =
+ ResizeArray()
+
+ for row in methodSemanticsRows do
+ if row.IsAdded then
+ tableMirror.AddMethodSemanticsRow row
+
+ methodSemanticsEncLogEntries.Add(struct (TableNames.MethodSemantics, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.MethodSemantics, row.RowId))
+
+ for _, newToken, literal in userStringUpdates do
+ let offset = newToken &&& 0x00FFFFFF
+ tableMirror.AddUserStringLiteral(offset, literal)
+
+ // Assemble the EncLog. Groups follow the established F# ordering (Module first, then
+ // member tables, then reference tables); parent/member Add* pairs are appended as
+ // pre-built adjacent sequences so no per-table sorting can separate a parent entry
+ // from the member row it creates. Map rows precede the Add* entries that use them as
+ // parents, and method entries precede the parameter pairs that reference them.
+ let encLogEntries =
+ let snapshot = encLog |> Seq.toArray
+
+ let referenceTables =
+ [|
+ TableNames.TypeRef
+ TableNames.MemberRef
+ TableNames.MethodSpec
+ TableNames.TypeSpec
+ TableNames.AssemblyRef
+ TableNames.StandAloneSig
+ TableNames.CustomAttribute
+ |]
+
+ let handledTables =
+ Set.ofList
+ [
+ TableNames.Module.Index
+ yield! referenceTables |> Seq.map (fun t -> t.Index)
+ ]
+
+ let builder = ResizeArray()
+
+ let appendEntries (table: TableName) =
+ snapshot
+ |> Seq.filter (fun struct (t, _, _) -> t.Index = table.Index)
+ |> Seq.sortBy (fun struct (_, rowId, _) -> rowId)
+ |> Seq.iter builder.Add
+
+ appendEntries TableNames.Module
+ // ECMA table order: TypeDef (0x02) / Field (0x04) precede Method (0x06); Roslyn
+ // likewise logs added-field pairs ahead of the method rows that consume them.
+ // New TypeDef rows come first of all: their Default entries must be applied
+ // before any AddField/AddMethod pair that names them as the parent.
+ builder.AddRange typeDefEncLogEntries
+ fieldEncLogPairs |> List.iter builder.Add
+ builder.AddRange methodEncLogEntries
+ builder.AddRange parameterEncLogEntries
+ // GenericParam rows trail the method/parameter pairs that introduced their
+ // owners (C# reference order: the GenericParam Default entry is logged after
+ // the AddParameter pair of the added generic method).
+ builder.AddRange genericParamEncLogEntries
+ referenceTables |> Array.iter appendEntries
+ builder.AddRange propertyMapEncLogEntries
+ builder.AddRange propertyEncLogEntries
+ builder.AddRange eventMapEncLogEntries
+ builder.AddRange eventEncLogEntries
+ builder.AddRange methodSemanticsEncLogEntries
+ // InterfaceImpl/MethodImpl rows trail the log (C# reference order: the
+ // 'new_class' template's InterfaceImpl entry is the last log entry), followed
+ // by NestedClass rows; the CLR applies all three via ApplyTableDelta after
+ // the new TypeDef row already exists.
+ builder.AddRange interfaceImplEncLogEntries
+ builder.AddRange methodImplEncLogEntries
+ builder.AddRange nestedClassEncLogEntries
+ // Constant rows trail the whole log (C# 'new_enum' reference order); the CLR
+ // only needs their parent Field rows applied first.
+ builder.AddRange constantEncLogEntries
+
+ // Any tables not handled above are appended sorted by token.
+ snapshot
+ |> Seq.filter (fun struct (table, _, _) -> not (handledTables |> Set.contains table.Index))
+ |> Seq.sortBy (fun struct (table, rowId, _) -> (table.Index <<< 24) ||| (rowId &&& 0x00FFFFFF))
+ |> Seq.iter builder.Add
+
+ builder.ToArray()
+
+ // Sort EncMap entries by token (table index << 24 | row ID)
+ let encMapEntries =
+ encMap
+ |> Seq.sortBy (fun struct (table, rowId) -> (table.Index <<< 24) ||| (rowId &&& 0x00FFFFFF))
+ |> Seq.toArray
+
+ // Write EncLog and EncMap rows to the mirror
+ for struct (table, rowId, operation) in encLogEntries do
+ tableMirror.AddEncLogRow(table, rowId, operation)
+
+ for struct (table, rowId) in encMapEntries do
+ tableMirror.AddEncMapRow(table, rowId)
+
+ let metadataSizes =
+ DeltaMetadataSerializer.computeMetadataSizes tableMirror normalizedExternalRowCounts
+
+ let tableRowCounts = metadataSizes.RowCounts
+ let tableBitMasks = metadataSizes.BitMasks
+ let indexSizes = metadataSizes.IndexSizes
+
+ let tableStreamInput =
+ {
+ DeltaMetadataSerializer.DeltaTableSerializerInput.Tables = tableMirror.TableRows
+ MetadataSizes = metadataSizes
+ StringHeap = tableMirror.StringHeapBytes
+ StringHeapOffsets = tableMirror.StringHeapOffsets
+ BlobHeap = tableMirror.BlobHeapBytes
+ BlobHeapOffsets = tableMirror.BlobHeapOffsets
+ GuidHeap = tableMirror.GuidHeapBytes
+ HeapOffsets = heapOffsets
+ }
+
+ let tableStream = DeltaMetadataSerializer.buildTableStream tableStreamInput
+ let heapStreams = DeltaMetadataSerializer.buildHeapStreams tableMirror
+
+ let metadataBytes =
+ DeltaMetadataSerializer.serializeMetadataRoot tableStreamInput heapStreams tableStream
+
+ if shouldTraceMetadata () then
+ printfn
+ "[fsharp-hotreload][index-sizes] stringsBig=%b guidsBig=%b blobsBig=%b"
+ indexSizes.StringsBig
+ indexSizes.GuidsBig
+ indexSizes.BlobsBig
+
+ let methodRows = tableRowCounts[TableNames.Method.Index]
+ let paramRows = tableRowCounts[TableNames.Param.Index]
+ let propertyRows = tableRowCounts[TableNames.Property.Index]
+ let eventRows = tableRowCounts[TableNames.Event.Index]
+
+ printfn
+ "[fsharp-hotreload][metadata-writer] rows method=%d param=%d property=%d event=%d stringHeap=%d blobHeap=%d guidHeap=%d"
+ methodRows
+ paramRows
+ propertyRows
+ eventRows
+ heapStreams.StringsLength
+ heapStreams.BlobsLength
+ heapStreams.GuidsLength
+
+ if shouldTraceHeaps () then
+ printfn
+ "[fsharp-hotreload][heap-summary] baseline:string=%d blob=%d guid=%d | delta:string=%d blob=%d guid=%d"
+ heapOffsets.StringHeapStart
+ heapOffsets.BlobHeapStart
+ heapOffsets.GuidHeapStart
+ heapStreams.StringsLength
+ heapStreams.BlobsLength
+ heapStreams.GuidsLength
+
+ printfn "[fsharp-hotreload][heap-bytes] blob-bytes=%A" heapStreams.Blobs
+
+ // HeapSizes should match what SRM's GetHeapSize returns:
+ // - StringHeap: SRM trims trailing zeros, so use unpadded size
+ // - UserStringHeap, BlobHeap, GuidHeap: SRM does NOT trim, so use padded size (stream header size)
+ // This is important for EnC offset calculations via MetadataAggregator
+ let heapSizes: MetadataHeapSizes =
+ {
+ StringHeapSize = tableMirror.StringHeapBytes.Length // unpadded - SRM trims trailing zeros
+ UserStringHeapSize = heapStreams.UserStringsLength // padded - SRM does not trim
+ BlobHeapSize = heapStreams.BlobsLength // padded - SRM does not trim
+ GuidHeapSize = heapStreams.GuidsLength
+ } // padded - SRM does not trim
+
+ {
+ Metadata = metadataBytes
+ StringHeap = heapStreams.Strings
+ BlobHeap = heapStreams.Blobs
+ GuidHeap = heapStreams.Guids
+ EncLog = encLogEntries |> Array.map (fun struct (a, b, c) -> (a, b, c))
+ EncMap = encMapEntries |> Array.map (fun struct (a, b) -> (a, b))
+ TableRowCounts = tableRowCounts
+ HeapSizes = heapSizes
+ HeapOffsets = heapOffsets
+ Tables = tableMirror.TableRows
+ TableBitMasks = tableBitMasks
+ IndexSizes = indexSizes
+ TableStream = tableStream
+ GenerationId = encId
+ BaseGenerationId = encBaseId
+ }
+
+/// Back-compat entry point without added TypeDef/NestedClass rows.
+let emitWithUserStrings
+ (moduleName: string)
+ (moduleNameOffset: StringOffset option)
+ (generation: int)
+ (encId: Guid)
+ (encBaseId: Guid)
+ (moduleId: Guid)
+ (methodDefinitionRows: MethodDefinitionRowInfo list)
+ (parameterDefinitionRows: ParameterDefinitionRowInfo list)
+ (fieldDefinitionRows: FieldDefinitionRowInfo list)
+ (typeReferenceRows: TypeReferenceRowInfo list)
+ (memberReferenceRows: MemberReferenceRowInfo list)
+ (methodSpecificationRows: MethodSpecificationRowInfo list)
+ (assemblyReferenceRows: AssemblyReferenceRowInfo list)
+ (propertyDefinitionRows: PropertyDefinitionRowInfo list)
+ (eventDefinitionRows: EventDefinitionRowInfo list)
+ (propertyMapRows: PropertyMapRowInfo list)
+ (eventMapRows: EventMapRowInfo list)
+ (methodSemanticsRows: MethodSemanticsMetadataUpdate list)
+ (standaloneSignatureRows: StandaloneSignatureUpdate list)
+ (customAttributeRows: CustomAttributeRowInfo list)
+ (userStringUpdates: (int * int * string) list)
+ (updates: MethodMetadataUpdate list)
+ (heapOffsets: MetadataHeapOffsets)
+ (externalRowCounts: int[])
+ : MetadataDelta =
+ emitWithTypeDefinitions
+ moduleName
+ moduleNameOffset
+ generation
+ encId
+ encBaseId
+ moduleId
+ ([]: TypeDefinitionRowInfo list)
+ ([]: NestedClassRowInfo list)
+ ([]: InterfaceImplRowInfo list)
+ ([]: MethodImplRowInfo list)
+ ([]: ConstantRowInfo list)
+ methodDefinitionRows
+ parameterDefinitionRows
+ fieldDefinitionRows
+ typeReferenceRows
+ memberReferenceRows
+ methodSpecificationRows
+ ([]: TypeSpecificationRowInfo list)
+ ([]: GenericParamRowInfo list)
+ ([]: GenericParamConstraintRowInfo list)
+ assemblyReferenceRows
+ propertyDefinitionRows
+ eventDefinitionRows
+ propertyMapRows
+ eventMapRows
+ methodSemanticsRows
+ standaloneSignatureRows
+ customAttributeRows
+ userStringUpdates
+ updates
+ heapOffsets
+ externalRowCounts
+
+let emitWithReferences
+ (moduleName: string)
+ (moduleNameOffset: StringOffset option)
+ (generation: int)
+ (encId: Guid)
+ (encBaseId: Guid)
+ (moduleId: Guid)
+ (methodDefinitionRows: MethodDefinitionRowInfo list)
+ (parameterDefinitionRows: ParameterDefinitionRowInfo list)
+ (fieldDefinitionRows: FieldDefinitionRowInfo list)
+ (typeReferenceRows: TypeReferenceRowInfo list)
+ (memberReferenceRows: MemberReferenceRowInfo list)
+ (methodSpecificationRows: MethodSpecificationRowInfo list)
+ (assemblyReferenceRows: AssemblyReferenceRowInfo list)
+ (propertyDefinitionRows: PropertyDefinitionRowInfo list)
+ (eventDefinitionRows: EventDefinitionRowInfo list)
+ (propertyMapRows: PropertyMapRowInfo list)
+ (eventMapRows: EventMapRowInfo list)
+ (methodSemanticsRows: MethodSemanticsMetadataUpdate list)
+ (standaloneSignatureRows: StandaloneSignatureUpdate list)
+ (customAttributeRows: CustomAttributeRowInfo list)
+ (userStringUpdates: (int * int * string) list)
+ (updates: MethodMetadataUpdate list)
+ (heapOffsets: MetadataHeapOffsets)
+ (externalRowCounts: int[])
+ : MetadataDelta =
+ emitWithUserStrings
+ moduleName
+ moduleNameOffset
+ generation
+ encId
+ encBaseId
+ moduleId
+ methodDefinitionRows
+ parameterDefinitionRows
+ fieldDefinitionRows
+ typeReferenceRows
+ memberReferenceRows
+ methodSpecificationRows
+ assemblyReferenceRows
+ propertyDefinitionRows
+ eventDefinitionRows
+ propertyMapRows
+ eventMapRows
+ methodSemanticsRows
+ standaloneSignatureRows
+ customAttributeRows
+ userStringUpdates
+ updates
+ heapOffsets
+ externalRowCounts
+
+let emit
+ (moduleName: string)
+ (moduleNameOffset: StringOffset option)
+ (generation: int)
+ (encId: Guid)
+ (encBaseId: Guid)
+ (moduleId: Guid)
+ (methodDefinitionRows: MethodDefinitionRowInfo list)
+ (parameterDefinitionRows: ParameterDefinitionRowInfo list)
+ (propertyDefinitionRows: PropertyDefinitionRowInfo list)
+ (eventDefinitionRows: EventDefinitionRowInfo list)
+ (propertyMapRows: PropertyMapRowInfo list)
+ (eventMapRows: EventMapRowInfo list)
+ (methodSemanticsRows: MethodSemanticsMetadataUpdate list)
+ (standaloneSignatureRows: StandaloneSignatureUpdate list)
+ (customAttributeRows: CustomAttributeRowInfo list)
+ (updates: MethodMetadataUpdate list)
+ (heapOffsets: MetadataHeapOffsets)
+ (externalRowCounts: int[])
+ : MetadataDelta =
+ emitWithReferences
+ moduleName
+ moduleNameOffset
+ generation
+ encId
+ encBaseId
+ moduleId
+ methodDefinitionRows
+ parameterDefinitionRows
+ ([]: FieldDefinitionRowInfo list)
+ []
+ []
+ []
+ []
+ propertyDefinitionRows
+ eventDefinitionRows
+ propertyMapRows
+ eventMapRows
+ methodSemanticsRows
+ standaloneSignatureRows
+ customAttributeRows
+ ([]: (int * int * string) list)
+ updates
+ heapOffsets
+ externalRowCounts
diff --git a/src/Compiler/CodeGen/HotReloadBaseline.fs b/src/Compiler/CodeGen/HotReloadBaseline.fs
new file mode 100644
index 00000000000..b7e8ff2aef4
--- /dev/null
+++ b/src/Compiler/CodeGen/HotReloadBaseline.fs
@@ -0,0 +1,1710 @@
+module internal FSharp.Compiler.HotReloadBaseline
+
+open System
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.Reflection
+open FSharp.Compiler.AbstractIL.IL
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+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
+
+let private tableCount = DeltaTokens.TableCount
+
+[]
+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 =
+ {
+ MethodToken: int
+ LocalSignatureToken: int
+ CodeOffset: int
+ CodeLength: int
+ }
+
+/// Stable identifier for a method definition used when correlating baseline tokens.
+type MethodDefinitionKey =
+ {
+ DeclaringType: string
+ Name: string
+ GenericArity: int
+ ParameterTypes: ILType list
+ ReturnType: ILType
+ }
+
+/// Baseline metadata handles reused to keep heap offsets stable across deltas.
+/// Stable identifier for a method parameter (sequence number within a method).
+type ParameterDefinitionKey =
+ {
+ Method: MethodDefinitionKey
+ SequenceNumber: int
+ }
+
+/// Stable identifier for a field definition in the baseline assembly.
+type FieldDefinitionKey =
+ {
+ DeclaringType: string
+ Name: string
+ FieldType: ILType
+ }
+
+/// Stable identifier for a property definition (including indexer parameter shapes).
+type PropertyDefinitionKey =
+ {
+ DeclaringType: string
+ Name: string
+ PropertyType: ILType
+ IndexParameterTypes: ILType list
+ }
+
+/// Stable identifier for an event definition in the baseline assembly.
+type EventDefinitionKey =
+ {
+ DeclaringType: string
+ Name: string
+ EventType: ILType option
+ }
+
+type MethodDefinitionMetadataHandles =
+ {
+ NameOffset: StringOffset option
+ SignatureOffset: BlobOffset option
+ FirstParameterRowId: int option
+ Rva: int option
+ Attributes: MethodAttributes option
+ ImplAttributes: MethodImplAttributes option
+ }
+
+///
+/// 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 TypeReferenceScope =
+ /// TypeRef resolved against an AssemblyRef row, identified by the referenced assembly's simple name.
+ | Assembly of assemblyName: string
+ /// Nested TypeRef whose resolution scope is its enclosing TypeRef.
+ | Nested of enclosing: TypeReferenceKey
+
+and TypeReferenceKey =
+ {
+ Scope: TypeReferenceScope
+ Namespace: string
+ Name: string
+ }
+
+type ParameterDefinitionMetadataHandles =
+ {
+ 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 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 =
+ {
+ Name: string
+ /// Decoded MemberRefParent as a metadata token (0x02/0x01/0x1A/0x06/0x1B tables).
+ ParentToken: int
+ /// Signature blob bytes (baseline coordinates).
+ Signature: byte[]
+ }
+
+/// 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 =
+ {
+ /// 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 =
+ | PropertyAssociation of PropertyDefinitionKey * rowId: int
+ | EventAssociation of EventDefinitionKey * rowId: int
+
+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
+ 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
+ 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
+ }
+
+type private BaselineMaps =
+ {
+ TypeTokens: Map
+ MethodTokens: Map
+ FieldTokens: Map
+ PropertyTokens: Map
+ EventTokens: Map
+ PropertyMapEntries: Map
+ EventMapEntries: Map
+ SynthesizedTypeShapes: Map
+ }
+
+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 internal collectSynthesizedNameSnapshot (ilModule: ILModuleDef) =
+ let buckets = Dictionary>(StringComparer.Ordinal)
+
+ 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)
+
+ typeDef.Properties.AsList()
+ |> List.iter (fun propertyDef -> recordName propertyDef.Name)
+
+ typeDef.Events.AsList() |> List.iter (fun eventDef -> recordName eventDef.Name)
+
+ typeDef.NestedTypes.AsList() |> List.iter collectTypeDef
+
+ ilModule.TypeDefs.AsList() |> List.iter collectTypeDef
+
+ buckets
+ |> Seq.map (fun (KeyValue(key, bucket)) -> key, bucket.ToArray())
+ |> Map.ofSeq
+
+/// 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 maps =
+ if IsCompilerGeneratedName tdef.Name then
+ { maps with
+ SynthesizedTypeShapes = maps.SynthesizedTypeShapes |> Map.add typeName (shapeOfSynthesizedTypeDef tdef)
+ }
+ else
+ maps
+
+ 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
+ }
+
+ let token = tokenMappings.MethodDefTokenMap (enclosing, tdef) mdef
+
+ { 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
+ }
+
+ let token = tokenMappings.FieldDefTokenMap (enclosing, tdef) fdef
+
+ { acc with
+ FieldTokens = acc.FieldTokens |> Map.add key token
+ })
+ maps
+
+ let propertyDefs = tdef.Properties.AsList()
+
+ 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 token = tokenMappings.PropertyTokenMap (enclosing, tdef) pdef
+
+ { acc with
+ PropertyTokens = acc.PropertyTokens |> Map.add key token
+ })
+ maps
+
+ let maps =
+ match propertyDefs with
+ | first :: _ ->
+ let token = tokenMappings.PropertyTokenMap (enclosing, tdef) first
+ let rowId = token &&& 0x00FFFFFF
+
+ { maps with
+ PropertyMapEntries = maps.PropertyMapEntries |> Map.add typeName rowId
+ }
+ | [] -> maps
+
+ let eventDefs = tdef.Events.AsList()
+
+ let maps =
+ eventDefs
+ |> List.fold
+ (fun (acc: BaselineMaps) edef ->
+ let key =
+ {
+ DeclaringType = typeName
+ Name = edef.Name
+ EventType = edef.EventType
+ }
+
+ let token = tokenMappings.EventTokenMap (enclosing, tdef) edef
+
+ { acc with
+ EventTokens = acc.EventTokens |> Map.add key token
+ })
+ maps
+
+ let maps =
+ match eventDefs with
+ | first :: _ ->
+ let token = tokenMappings.EventTokenMap (enclosing, tdef) first
+ let rowId = token &&& 0x00FFFFFF
+
+ { maps with
+ EventMapEntries = maps.EventMapEntries |> Map.add typeName rowId
+ }
+ | [] -> maps
+
+ tdef.NestedTypes.AsList()
+ |> List.fold (collectType tokenMappings scope (enclosing @ [ tdef ])) maps
+
+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)
+ (typeDefSimpleNames: Set)
+ : Map> =
+
+ if Map.isEmpty encMethodDebugInfos then
+ Map.empty
+ else
+ let hasMidSessionClosureNames =
+ typeDefSimpleNames
+ |> Set.exists (fun name ->
+ match GeneratedNames.TryGetHotReloadNameGeneration name with
+ | Some generation -> generation >= 1
+ | None -> false)
+
+ if hasMidSessionClosureNames then
+ Map.empty
+ else
+ let hasReplayNamedTypeDef nameBase =
+ let prefix = nameBase + "@hotreload"
+
+ typeDefSimpleNames
+ |> Set.exists (fun name ->
+ name.StartsWith(prefix, StringComparison.Ordinal)
+ && not (GeneratedNames.IsHotReloadGenerationSuffixedName name))
+
+ let derivedRows =
+ encMethodDebugInfos
+ |> Map.toList
+ |> List.choose (fun (methodToken, info) ->
+ match info.Closures, Map.tryFind methodToken methodNamesByToken with
+ | [], _
+ | _, None -> None
+ | closures, Some methName ->
+ let nameBase = cleanUpGeneratedTypeName methName
+
+ let table =
+ closures
+ |> List.choose (fun closure ->
+ let chain = decodeOccurrenceKey closure.SyntaxOffset
+ let name = ClosureNameAllocator.formatGenerationSuffixedClosureName nameBase 0 chain
+
+ if Set.contains name typeDefSimpleNames then
+ Some(chain, name)
+ else
+ None)
+
+ Some(methodToken, nameBase, table))
+
+ let hasReplayOnlyCdiMethod =
+ derivedRows
+ |> 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 || hasReplayOnlyTypeDef then
+ Map.empty
+ else
+ (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 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
+ 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 =
+ 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
+ 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 maps.MethodTokens)
+ (collectTypeDefSimpleNames ilModule)
+ SequencePointSnapshots = sequencePointSnapshots
+ ModuleNameOffset = None
+ }
+
+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.
+ 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 = baseline.Metadata.HeapSizes.GuidHeapSize + 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 = baseline.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