diff --git a/FSharp.slnx b/FSharp.slnx
index 50819fbfb6b..c97fdbce36c 100644
--- a/FSharp.slnx
+++ b/FSharp.slnx
@@ -43,6 +43,11 @@
+
+
+
+
+
diff --git a/docs/hot-reload-rude-edits.md b/docs/hot-reload-rude-edits.md
new file mode 100644
index 00000000000..a4a974d537a
--- /dev/null
+++ b/docs/hot-reload-rude-edits.md
@@ -0,0 +1,27 @@
+# F# hot reload rude-edit diagnostics
+
+F# hot reload reports an `FSHRDL` diagnostic when an edit cannot be applied safely to the running process. The application is rebuilt and restarted instead of applying a delta that could leave it in an invalid state.
+
+The diagnostic message identifies the affected declaration and the reason for the restart. These codes are owned by the F# compiler and are separate from Roslyn's `ENC` diagnostic namespace.
+
+| Code | Meaning | What to do |
+| --- | --- | --- |
+| `FSHRDL001` | A member signature changed. | Undo the signature change to apply in place, or allow the rebuild and restart. |
+| `FSHRDL002` | An `inline` annotation changed. | Allow the rebuild and restart. |
+| `FSHRDL003` | A type representation or layout changed. | Allow the rebuild and restart. |
+| `FSHRDL004` | A declaration was added in a shape the runtime cannot add. | Allow the rebuild and restart. |
+| `FSHRDL005` | A declaration was removed. | Allow the rebuild and restart. |
+| `FSHRDL006` | A virtual, abstract, or override member was added. | Allow the rebuild and restart. |
+| `FSHRDL007` | A constructor was added. | Allow the rebuild and restart. |
+| `FSHRDL008` | A user-defined operator was added. | Allow the rebuild and restart. |
+| `FSHRDL009` | An explicit interface implementation was added. | Allow the rebuild and restart. |
+| `FSHRDL010` | A member was added to an interface. | Allow the rebuild and restart. |
+| `FSHRDL011` | A field was added in a shape the runtime cannot add. | Allow the rebuild and restart. |
+| `FSHRDL012` | A lambda's lowered shape changed incompatibly. | Allow the rebuild and restart. |
+| `FSHRDL013` | A state machine's resumable or hoisted layout changed incompatibly. | Keep the existing resume-point and captured-value layout, or allow the rebuild and restart. |
+| `FSHRDL014` | A query expression's lowered shape changed incompatibly. | Allow the rebuild and restart. |
+| `FSHRDL015` | A synthesized compiler declaration changed incompatibly. | Allow the rebuild and restart. |
+| `FSHRDL016` | The runtime did not advertise a capability required by the edit. | Update the runtime if a newer version supports the capability, or allow the rebuild and restart. |
+| `FSHRDL099` | The edit is unsupported for another fail-closed reason. | Follow the detailed message and allow the rebuild and restart. |
+
+These diagnostics are intentionally fail closed. If the compiler cannot prove that an edit is safe, it requests a restart and leaves the running application unchanged.
diff --git a/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md b/docs/release-notes/.FSharp.Compiler.Service/11.0.100.md
index 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/DeltaIndexSizing.fs b/src/Compiler/AbstractIL/DeltaIndexSizing.fs
new file mode 100644
index 00000000000..2e2c979461c
--- /dev/null
+++ b/src/Compiler/AbstractIL/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.AbstractIL.DeltaIndexSizing
+
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.AbstractIL.DeltaMetadataEncoding
+
+type MetadataHeapSizes = FSharp.Compiler.AbstractIL.ILBinaryWriter.MetadataHeapSizes
+
+/// Holds computed "bigness" flags for all coded index types.
+/// When true, the index requires 4 bytes; when false, 2 bytes suffice.
+type CodedIndexSizes =
+ {
+ 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/AbstractIL/DeltaMetadataEncoding.fs b/src/Compiler/AbstractIL/DeltaMetadataEncoding.fs
new file mode 100644
index 00000000000..98e141d8d34
--- /dev/null
+++ b/src/Compiler/AbstractIL/DeltaMetadataEncoding.fs
@@ -0,0 +1,289 @@
+module internal FSharp.Compiler.AbstractIL.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/AbstractIL/DeltaMetadataSerializer.fs b/src/Compiler/AbstractIL/DeltaMetadataSerializer.fs
new file mode 100644
index 00000000000..66fcc68c1f8
--- /dev/null
+++ b/src/Compiler/AbstractIL/DeltaMetadataSerializer.fs
@@ -0,0 +1,486 @@
+module internal FSharp.Compiler.AbstractIL.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.AbstractIL.DeltaMetadataTables
+open FSharp.Compiler.AbstractIL.DeltaMetadataTypes
+open FSharp.Compiler.AbstractIL.DeltaTableLayout
+
+module Encoding = FSharp.Compiler.AbstractIL.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 1-based indexes into the cumulative GUID heap.
+ // Absolute handles are already cumulative indexes 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.PaddedSize, 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/AbstractIL/DeltaMetadataTables.fs b/src/Compiler/AbstractIL/DeltaMetadataTables.fs
new file mode 100644
index 00000000000..14843b39398
--- /dev/null
+++ b/src/Compiler/AbstractIL/DeltaMetadataTables.fs
@@ -0,0 +1,1056 @@
+module internal FSharp.Compiler.AbstractIL.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.AbstractIL.IlxDeltaStreams
+open FSharp.Compiler.AbstractIL.DeltaMetadataTypes
+
+module Encoding = FSharp.Compiler.AbstractIL.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
+
+ do
+ if heapOffsets.GuidHeapStart < 0 || heapOffsets.GuidHeapStart % 16 <> 0 then
+ invalidArg
+ (nameof heapOffsets)
+ $"GUID heap start must be a non-negative multiple of 16 bytes, but was {heapOffsets.GuidHeapStart}."
+
+ let priorGuidEntryCount = heapOffsets.GuidHeapStart / 16
+ 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-adds a GUID to this generation and returns its 1-based index in the
+ /// cumulative GUID heap address space used by metadata handles.
+ let forceAddGuidValue (value: Guid) =
+ priorGuidEntryCount + 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)
+
+ // Roslyn zero-fills each delta #GUID stream through the prior cumulative heap
+ // size, then appends this generation's entries. Module handles are cumulative,
+ // so the zero prefix keeps handle N at byte offset (N - 1) * 16 in the stream.
+ if heapOffsets.GuidHeapStart > 0 then
+ writer.Write(Array.zeroCreate heapOffsets.GuidHeapStart)
+
+ 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] priorEntries=%d addedEntries=%d" priorGuidEntryCount 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
+ // EnC Module rows use cumulative GUID handles. The delta stream is zero-padded
+ // through prior generations, and these entries follow that prefix in stable order.
+ let mvidIndex = forceAddGuidValue moduleId
+ let encIdIndex = forceAddGuidValue encId
+
+ // EncBaseId is handle 0 for generation 1; later generations append the previous EncId.
+ let encBaseIdIndex =
+ if encBaseId = System.Guid.Empty then
+ 0
+ else
+ forceAddGuidValue encBaseId
+
+ 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
+ rowElementGuidAbsolute encIdIndex
+ rowElementGuidAbsolute encBaseIdIndex
+ |]
+
+ /// 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/AbstractIL/DeltaMetadataTypes.fs b/src/Compiler/AbstractIL/DeltaMetadataTypes.fs
new file mode 100644
index 00000000000..057ca154798
--- /dev/null
+++ b/src/Compiler/AbstractIL/DeltaMetadataTypes.fs
@@ -0,0 +1,382 @@
+module internal FSharp.Compiler.AbstractIL.DeltaMetadataTypes
+
+open System
+open System.Reflection
+open FSharp.Compiler.AbstractIL.IL
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+
+// ============================================================================
+// Definition keys
+// ============================================================================
+// Stable, content-based identifiers for metadata definitions. These are used to
+// correlate a definition across compiles/generations (e.g. baseline vs. fresh
+// compile) independently of row-id churn. Lifted from the hot-reload baseline
+// module: unlike the rest of that module (FSharpEmitBaseline, handle caches,
+// token maps, TypeReferenceKey, ...), these records carry no session state and
+// are pure structural identities over ILType/string data, so they belong beside
+// the *RowInfo contract types below rather than with baseline bookkeeping.
+
+/// Stable identifier for a method definition used when correlating baseline tokens.
+type MethodDefinitionKey =
+ {
+ DeclaringType: string
+ Name: string
+ GenericArity: int
+ ParameterTypes: ILType list
+ ReturnType: ILType
+ }
+
+/// 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
+ }
+
+/// Identifies the property or event a MethodSemantics row (getter/setter/add/remove) is
+/// associated with, plus the row id of that PropertyMap/EventMap-owned parent.
+type MethodSemanticsAssociation =
+ | PropertyAssociation of PropertyDefinitionKey * rowId: int
+ | EventAssociation of EventDefinitionKey * rowId: int
+
+/// 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/AbstractIL/DeltaTableLayout.fs b/src/Compiler/AbstractIL/DeltaTableLayout.fs
new file mode 100644
index 00000000000..f297d6ebaae
--- /dev/null
+++ b/src/Compiler/AbstractIL/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.AbstractIL.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/AbstractIL/EncMethodDebugInformation.fs b/src/Compiler/AbstractIL/EncMethodDebugInformation.fs
new file mode 100644
index 00000000000..36160e5c981
--- /dev/null
+++ b/src/Compiler/AbstractIL/EncMethodDebugInformation.fs
@@ -0,0 +1,724 @@
+/// Edit-and-Continue method debug information blobs.
+///
+/// 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.
+///
+/// Every "syntax offset" slot in these blobs is an opaque, caller-defined integer key
+/// (Roslyn: the syntax offset of the lambda/closure/state-machine-suspension syntax
+/// node). This module does not require the key to be a source offset; it only requires
+/// determinism across generations. tryEncodeOccurrenceKey/decodeOccurrenceKey provide one
+/// reusable way to pack a short (depth <= 2) ordinal chain into such a key.
+module internal FSharp.Compiler.AbstractIL.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
+
+/// 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-5 of the leading byte (bit 6 is unused, 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: caller-defined 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.
+type EncClosureInfo =
+ {
+ /// Caller-defined key (Roslyn: syntax offset of the scope owning the closure).
+ SyntaxOffset: int
+ }
+
+/// One lambda in the EnC Lambda and Closure Map.
+type EncLambdaInfo =
+ {
+ /// Caller-defined 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
+ /// Caller-defined 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 ordinal chain (root-first enclosing ordinals, ending with the innermost
+/// ordinal) into a deterministic int suitable for a "syntax offset" blob slot. Packing:
+/// 16-bit segments, least-significant segment = the innermost ordinal; an enclosing
+/// ordinal p is stored as (p + 1) shifted left 16 so that depth-1 keys (< 0x10000) and
+/// depth-2 keys (>= 0x10000) never collide. Fails closed (None) past the limits: chains
+/// deeper than 2, ordinals > 0xFFFF, or keys exceeding the compressed-integer budget,
+/// callers must then treat the chain as unmappable, never truncate.
+let tryEncodeOccurrenceKey (ordinalChain: int list) : int option =
+ match ordinalChain with
+ | [ ordinal ] when ordinal >= 0 && ordinal <= MaxOccurrenceSegment -> Some ordinal
+ | [ parent; ordinal ] when
+ parent >= 0
+ && ordinal >= 0
+ && ordinal <= MaxOccurrenceSegment
+ && parent < MaxOccurrenceSegment
+ ->
+ // Pack in int64: a large parent (e.g. 0xFFFE) would wrap ((parent + 1) <<< 16) negative in
+ // int32 and a negative key slips past the <= MaxOccurrenceKey bound, failing OPEN.
+ let key = ((int64 parent + 1L) <<< 16) ||| int64 ordinal
+
+ if key <= int64 MaxOccurrenceKey then
+ Some(int key)
+ else
+ None
+ | _ -> None
+
+/// Unpacks an occurrence key produced by tryEncodeOccurrenceKey back into its
+/// root-first ordinal chain.
+let decodeOccurrenceKey (key: int) : int list =
+ if key < 0 then
+ invalidArg (nameof key) $"occurrence key must be non-negative, got %d{key}"
+ elif key <= MaxOccurrenceSegment then
+ [ key ]
+ else
+ [ (key >>> 16) - 1; key &&& MaxOccurrenceSegment ]
+
+// ---------------------------------------------------------------------------
+// Blob helpers
+// ---------------------------------------------------------------------------
+
+let private invalidData (blobName: string) (offset: int) =
+ raise (InvalidDataException $"invalid EnC %s{blobName} blob: unexpected data at offset %d{offset}")
+
+// Absent CDI rows arrive as null at runtime even though the parameter is non-null in the
+// nullness model, so guard with box (FS3261-safe) rather than dropping the check.
+let private isEmpty (blob: byte[]) = isNull (box blob) || blob.Length = 0
+
+// ---------------------------------------------------------------------------
+// F# hot reload module CDI: synthesized-name allocation snapshot
+// Format:
+// compressed(version = 1), compressed(bucket count),
+// then buckets sorted by key for deterministic PDB bytes:
+// string key, compressed(name count), string name in allocation-slot order.
+// Strings are compressed(byte length) followed by UTF-8 bytes.
+// ---------------------------------------------------------------------------
+
+[]
+let private SynthesizedNameSnapshotBlobVersion = 1
+
+let private writeUtf8String (builder: BlobBuilder) (value: string) =
+ if isNull (box value) then
+ invalidArg (nameof value) "snapshot strings must be non-null"
+
+ let bytes = Encoding.UTF8.GetBytes value
+ builder.WriteCompressedInteger bytes.Length
+ builder.WriteBytes bytes
+
+let private readUtf8String (blobName: string) (reader: byref) =
+ let length = reader.ReadCompressedInteger()
+
+ if length < 0 || length > reader.RemainingBytes then
+ invalidData blobName reader.Offset
+
+ let bytes = reader.ReadBytes length
+ Encoding.UTF8.GetString(bytes, 0, bytes.Length)
+
+let private materializeSynthesizedNameSnapshot (snapshot: seq) =
+ snapshot
+ |> Seq.map (fun struct (key, names) ->
+ if isNull (box key) then
+ invalidArg (nameof snapshot) "snapshot keys must be non-null"
+
+ if isNull (box names) then
+ invalidArg (nameof snapshot) $"snapshot bucket '{key}' must be non-null"
+
+ key, Array.copy names)
+ |> Seq.sortBy fst
+ |> Seq.toArray
+
+/// Serializes an allocation-ordered synthesized-name snapshot into the F#-owned module
+/// CDI blob. An empty snapshot returns an empty blob so no CDI row needs to be emitted.
+let serializeSynthesizedNameSnapshot (snapshot: seq) : byte[] =
+ let buckets = materializeSynthesizedNameSnapshot snapshot
+
+ if buckets.Length = 0 then
+ Array.empty
+ else
+ let builder = BlobBuilder()
+ builder.WriteCompressedInteger SynthesizedNameSnapshotBlobVersion
+ builder.WriteCompressedInteger buckets.Length
+
+ for key, names in buckets do
+ writeUtf8String builder key
+ builder.WriteCompressedInteger names.Length
+
+ for name in names do
+ writeUtf8String builder name
+
+ builder.ToArray()
+
+/// Deserializes the F#-owned synthesized-name snapshot CDI blob. Bucket order in the
+/// blob is deterministic only; each bucket array is returned exactly in recorded slot order.
+let deserializeSynthesizedNameSnapshot (blob: byte[]) : Map =
+ if isEmpty blob then
+ Map.empty
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ try
+ let version = reader.ReadCompressedInteger()
+
+ if version <> SynthesizedNameSnapshotBlobVersion then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ let bucketCount = reader.ReadCompressedInteger()
+
+ if bucketCount <= 0 || bucketCount > reader.RemainingBytes / 2 then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ let buckets = ResizeArray()
+
+ for _ in 1..bucketCount do
+ let key = readUtf8String "synthesized name snapshot" &reader
+ let nameCount = reader.ReadCompressedInteger()
+
+ // Every serialized name consumes at least one byte for its UTF-8
+ // length, so this check bounds allocation before Array.zeroCreate.
+ if nameCount < 0 || nameCount > reader.RemainingBytes then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ let names = Array.zeroCreate nameCount
+
+ for i in 0 .. nameCount - 1 do
+ names[i] <- readUtf8String "synthesized name snapshot" &reader
+
+ buckets.Add(key, names)
+
+ if reader.RemainingBytes <> 0 then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ buckets |> Seq.map id |> Map.ofSeq
+ with :? BadImageFormatException ->
+ invalidData "synthesized name snapshot" reader.Offset
+ finally
+ handle.Free()
+
+/// Creates the module-level CustomDebugInformation row for the allocation-ordered
+/// synthesized-name snapshot. Empty snapshots emit no row.
+let computeSynthesizedNameSnapshotCustomDebugInfoRows (snapshot: seq) : 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-5 =
+// 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), 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
+ }
+
+/// 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.
+/// Fail safe: a null/empty or non-PDB image yields the empty map, and a method whose
+/// blobs do not decode is omitted rather than guessed.
+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): callers still get an empty
+ // map instead of a crash.
+ Map.empty
+
+/// Reads the F#-owned allocation-ordered synthesized-name snapshot from a portable PDB.
+/// None means either the record is absent or invalid; callers must fall back to IL
+/// reconstruction rather than trusting a partial layout.
+let readSynthesizedNameSnapshotFromPortablePdb (pdbBytes: byte[]) : Map 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/AbstractIL/EncMethodDebugInformation.fsi b/src/Compiler/AbstractIL/EncMethodDebugInformation.fsi
new file mode 100644
index 00000000000..6d83c3a42b6
--- /dev/null
+++ b/src/Compiler/AbstractIL/EncMethodDebugInformation.fsi
@@ -0,0 +1,199 @@
+/// Edit-and-Continue method debug information blobs.
+///
+/// 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.
+///
+/// Every "syntax offset" slot in these blobs is an opaque, caller-defined integer key
+/// (Roslyn: the syntax offset of the lambda/closure/state-machine-suspension syntax
+/// node). This module does not require the key to be a source offset; it only requires
+/// determinism across generations. tryEncodeOccurrenceKey/decodeOccurrenceKey provide one
+/// reusable way to pack a short (depth <= 2) ordinal chain into such a key.
+module internal FSharp.Compiler.AbstractIL.EncMethodDebugInformation
+
+/// 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.
+ val encLocalSlotMap: System.Guid
+
+ /// EnC Lambda and Closure Map CDI kind.
+ val encLambdaAndClosureMap: System.Guid
+
+ /// EnC State Machine State Map CDI kind.
+ val encStateMachineStateMap: System.Guid
+
+ /// F#-owned hot reload synthesized-name snapshot CDI kind.
+ val fsharpSynthesizedNameSnapshot: System.Guid
+
+/// Closure ordinal of a lambda that is lowered to a static (non-capturing) method.
+/// Mirrors Roslyn's LambdaDebugInfo.StaticClosureOrdinal.
+[]
+val StaticClosureOrdinal: int = -1
+
+/// Closure ordinal of a lambda closed over the 'this' pointer only.
+/// Mirrors Roslyn's LambdaDebugInfo.ThisOnlyClosureOrdinal.
+[]
+val ThisOnlyClosureOrdinal: int = -2
+
+/// Smallest valid closure ordinal. Mirrors Roslyn's LambdaDebugInfo.MinClosureOrdinal.
+[]
+val MinClosureOrdinal: int = -2
+
+/// Method ordinal of a method that has no lambda map (an empty blob decodes to this).
+/// Mirrors Roslyn's DebugId.UndefinedOrdinal.
+[]
+val UndefinedMethodOrdinal: int = -1
+
+/// Largest synthesized-local kind serializable in the slot map: the kind is stored as
+/// (kind + 1) in bits 0-5 of the leading byte (bit 6 is unused, bit 7 flags a trailing ordinal), and
+/// Roslyn's reader recovers it with mask 0x3F, so only kinds 0..0x3E round-trip.
+[]
+val MaxSerializableLocalKind: int = 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: caller-defined 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.
+type EncClosureInfo =
+ {
+ /// Caller-defined key (Roslyn: syntax offset of the scope owning the closure).
+ SyntaxOffset: int
+ }
+
+/// One lambda in the EnC Lambda and Closure Map.
+type EncLambdaInfo =
+ {
+ /// Caller-defined 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
+ /// Caller-defined 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: EncMethodDebugInformation
+
+/// Packs an ordinal chain (root-first enclosing ordinals, ending with the innermost
+/// ordinal) into a deterministic int suitable for a "syntax offset" blob slot. Fails
+/// closed (None) past the limits: chains deeper than 2, ordinals > 0xFFFF, or keys
+/// exceeding the compressed-integer budget.
+val tryEncodeOccurrenceKey: ordinalChain: int list -> int option
+
+/// Unpacks an occurrence key produced by tryEncodeOccurrenceKey back into its
+/// root-first ordinal chain.
+val decodeOccurrenceKey: key: int -> int list
+
+/// Serializes an allocation-ordered synthesized-name snapshot into the F#-owned module
+/// CDI blob. An empty snapshot returns an empty blob so no CDI row needs to be emitted.
+val serializeSynthesizedNameSnapshot: snapshot: seq -> byte[]
+
+/// Deserializes the F#-owned synthesized-name snapshot CDI blob. Bucket order in the
+/// blob is deterministic only; each bucket array is returned exactly in recorded slot order.
+val deserializeSynthesizedNameSnapshot: blob: byte[] -> Map
+
+/// Creates the module-level CustomDebugInformation row for the allocation-ordered
+/// synthesized-name snapshot. Empty snapshots emit no row.
+val computeSynthesizedNameSnapshotCustomDebugInfoRows:
+ snapshot: seq -> FSharp.Compiler.AbstractIL.ILPdbWriter.PdbModuleCustomDebugInfo list
+
+/// Serializes the EnC Local Slot Map blob for 'info', byte-for-byte as Roslyn's
+/// SerializeLocalSlots. Returns the empty array when there are no slots (no CDI row
+/// should be emitted then).
+val serializeLocalSlots: info: EncMethodDebugInformation -> byte[]
+
+/// Deserializes an EnC Local Slot Map blob, byte-for-byte as Roslyn's UncompressSlotMap.
+/// An empty (or null) blob yields no slots.
+val deserializeLocalSlots: blob: byte[] -> EncLocalSlotInfo list
+
+/// 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).
+val serializeLambdaMap: info: EncMethodDebugInformation -> byte[]
+
+/// Deserializes an EnC Lambda and Closure Map blob, byte-for-byte as Roslyn's
+/// UncompressLambdaMap. An empty (or null) blob yields (UndefinedMethodOrdinal, [], []).
+val deserializeLambdaMap: blob: byte[] -> int * EncClosureInfo list * EncLambdaInfo list
+
+/// 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).
+val serializeStateMachineStates: info: EncMethodDebugInformation -> byte[]
+
+/// 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.
+val deserializeStateMachineStates: blob: byte[] -> EncStateMachineStateInfo list
+
+/// Deserializes EnC method debug information from the three blobs (any of which may be
+/// null or empty). Mirrors Roslyn's EditAndContinueMethodDebugInformation.Create.
+val deserialize:
+ slotMapBlob: byte[] -> lambdaMapBlob: byte[] -> stateMachineStateMapBlob: byte[] -> EncMethodDebugInformation
+
+/// 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.
+/// Fail safe: a null/empty or non-PDB image yields the empty map, and a method whose
+/// blobs do not decode is omitted rather than guessed.
+val readEncMethodDebugInfoFromPortablePdb: pdbBytes: byte[] -> Map
+
+/// Reads the F#-owned allocation-ordered synthesized-name snapshot from a portable PDB.
+/// None means either the record is absent or invalid; callers must fall back to IL
+/// reconstruction rather than trusting a partial layout.
+val readSynthesizedNameSnapshotFromPortablePdb: pdbBytes: byte[] -> Map option
diff --git a/src/Compiler/AbstractIL/FSharpDeltaMetadataWriter.fs b/src/Compiler/AbstractIL/FSharpDeltaMetadataWriter.fs
new file mode 100644
index 00000000000..f4decb82666
--- /dev/null
+++ b/src/Compiler/AbstractIL/FSharpDeltaMetadataWriter.fs
@@ -0,0 +1,992 @@
+module internal FSharp.Compiler.AbstractIL.FSharpDeltaMetadataWriter
+
+open System
+open System.Collections.Generic
+open Microsoft.FSharp.Collections
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.AbstractIL.IlxDeltaStreams
+open FSharp.Compiler.AbstractIL.DeltaMetadataTables
+open FSharp.Compiler.AbstractIL.DeltaMetadataTypes
+open FSharp.Compiler.AbstractIL.DeltaTableLayout
+open FSharp.Compiler.AbstractIL.DeltaMetadataSerializer
+
+[]
+let private TraceMetadataFlagName = "FSHARP_HOTRELOAD_TRACE_METADATA"
+
+[]
+let private TraceHeapsFlagName = "FSHARP_HOTRELOAD_TRACE_HEAPS"
+
+[]
+let private TraceMethodsFlagName = "FSHARP_HOTRELOAD_TRACE_METHODS"
+
+/// Local copy of FSharp.Compiler.EnvironmentHelpers.isEnvVarTruthy. That module is a new
+/// utility file added by the hot-reload feature branch and isn't part of this extraction's
+/// scope, so the writer's trace-flag checks carry their own tiny copy instead of pulling in
+/// an extra out-of-scope file.
+let private isEnvVarTruthy (name: string) =
+ match Environment.GetEnvironmentVariable(name) with
+ | null
+ | "" -> false
+ | value when String.Equals(value, "1", StringComparison.OrdinalIgnoreCase) -> true
+ | value when String.Equals(value, "true", StringComparison.OrdinalIgnoreCase) -> true
+ | _ -> false
+
+let private shouldTraceMetadata () = isEnvVarTruthy TraceMetadataFlagName
+
+let private shouldTraceHeaps () = isEnvVarTruthy TraceHeapsFlagName
+
+let private shouldTraceMethodRows () = isEnvVarTruthy TraceMethodsFlagName
+
+let private sortRowsByRowId tableName getRowId rows =
+ let sorted = rows |> List.sortBy getRowId
+
+ sorted
+ |> List.pairwise
+ |> List.iter (fun (previous, current) ->
+ let rowId = getRowId current
+
+ if getRowId previous = rowId then
+ invalidArg "rows" $"Duplicate {tableName} row id {rowId}.")
+
+ sorted
+
+let private validatePrimaryKeyOrder tableName getPrimaryKey rows =
+ rows
+ |> List.pairwise
+ |> List.iter (fun (previous, current) ->
+ if getPrimaryKey previous > getPrimaryKey current then
+ invalidArg "rows" $"{tableName} row ids are not allocated in the table's required primary-key order.")
+
+ rows
+
+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.AbstractIL.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 =
+ let methodDefinitionRows =
+ methodDefinitionRows |> sortRowsByRowId "MethodDef" (fun row -> row.RowId)
+
+ 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
+ if updatesByKey.ContainsKey update.MethodKey then
+ invalidArg (nameof updates) $"Duplicate method update for '{update.MethodKey.DeclaringType}::{update.MethodKey.Name}'."
+
+ updatesByKey.Add(update.MethodKey, update)
+
+ let methodRowKeys = HashSet(HashIdentity.Structural)
+
+ for row in methodDefinitionRows do
+ if not (methodRowKeys.Add row.Key) then
+ invalidArg (nameof methodDefinitionRows) $"Duplicate method row for '{row.Key.DeclaringType}::{row.Key.Name}'."
+
+ if not (updatesByKey.ContainsKey row.Key) then
+ invalidOp $"Method row '{row.Key.DeclaringType}::{row.Key.Name}' has no matching update payload."
+
+ for update in updates do
+ if not (methodRowKeys.Contains update.MethodKey) then
+ invalidArg
+ (nameof updates)
+ $"Method update for '{update.MethodKey.DeclaringType}::{update.MethodKey.Name}' has no matching method row."
+
+ // 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()
+
+ let typeDefinitionRows =
+ typeDefinitionRows |> sortRowsByRowId "TypeDef" (fun row -> row.RowId)
+
+ for row in typeDefinitionRows do
+ tableMirror.AddTypeDefinitionRow row
+ typeDefEncLogEntries.Add(struct (TableNames.TypeDef, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.TypeDef, row.RowId))
+
+ let nestedClassEncLogEntries =
+ ResizeArray()
+
+ let nestedClassRows =
+ nestedClassRows
+ |> sortRowsByRowId "NestedClass" (fun row -> row.RowId)
+ |> validatePrimaryKeyOrder "NestedClass" (fun row -> row.NestedTypeDefRowId)
+
+ for row in nestedClassRows 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()
+
+ let interfaceImplRows =
+ interfaceImplRows
+ |> sortRowsByRowId "InterfaceImpl" (fun row -> row.RowId)
+ |> validatePrimaryKeyOrder "InterfaceImpl" (fun row -> row.ClassTypeDefRowId)
+
+ for row in interfaceImplRows do
+ tableMirror.AddInterfaceImplRow row
+ interfaceImplEncLogEntries.Add(struct (TableNames.InterfaceImpl, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.InterfaceImpl, row.RowId))
+
+ let methodImplEncLogEntries =
+ ResizeArray()
+
+ let methodImplRows =
+ methodImplRows
+ |> sortRowsByRowId "MethodImpl" (fun row -> row.RowId)
+ |> validatePrimaryKeyOrder "MethodImpl" (fun row -> row.ClassTypeDefRowId)
+
+ for row in methodImplRows 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()
+
+ let hasConstantKey (parent: HasConstant) =
+ let tag =
+ match parent with
+ | HC_Field _ -> 0
+ | HC_Param _ -> 1
+ | HC_Property _ -> 2
+
+ (parent.RowId <<< 2) ||| tag
+
+ let constantRows =
+ constantRows
+ |> sortRowsByRowId "Constant" (fun row -> row.RowId)
+ |> validatePrimaryKeyOrder "Constant" (fun row -> hasConstantKey row.Parent)
+
+ for row in constantRows 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))
+ | _ ->
+ // The one-to-one validation above makes this branch unreachable.
+ invalidOp $"Method row '{row.Key.DeclaringType}::{row.Key.Name}' has no matching update payload."
+
+ let parameterEncLogEntries =
+ ResizeArray()
+
+ let parameterDefinitionRows =
+ parameterDefinitionRows |> sortRowsByRowId "Param" (fun row -> row.RowId)
+
+ 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))
+
+ let fieldDefinitionRows =
+ fieldDefinitionRows |> sortRowsByRowId "Field" (fun row -> 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)
+ ])
+
+ let typeReferenceRows =
+ typeReferenceRows |> sortRowsByRowId "TypeRef" (fun row -> row.RowId)
+
+ for row in typeReferenceRows do
+ tableMirror.AddTypeReferenceRow row
+
+ encLog.Add(struct (TableNames.TypeRef, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.TypeRef, row.RowId))
+
+ let memberReferenceRows =
+ memberReferenceRows |> sortRowsByRowId "MemberRef" (fun row -> 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))
+
+ let methodSpecificationRows =
+ methodSpecificationRows |> sortRowsByRowId "MethodSpec" (fun row -> 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.
+ let typeSpecificationRows =
+ typeSpecificationRows |> sortRowsByRowId "TypeSpec" (fun row -> row.RowId)
+
+ 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()
+
+ let typeOrMethodDefKey (owner: TypeOrMethodDef) = (owner.RowId <<< 1) ||| owner.CodedTag
+
+ let genericParamRows =
+ genericParamRows
+ |> sortRowsByRowId "GenericParam" (fun row -> row.RowId)
+ |> validatePrimaryKeyOrder "GenericParam" (fun row -> typeOrMethodDefKey row.Owner, row.Number)
+
+ for row in genericParamRows 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).
+ let genericParamConstraintRows =
+ genericParamConstraintRows
+ |> sortRowsByRowId "GenericParamConstraint" (fun row -> row.RowId)
+ |> validatePrimaryKeyOrder "GenericParamConstraint" (fun row -> row.OwnerGenericParamRowId)
+
+ for row in genericParamConstraintRows do
+ tableMirror.AddGenericParamConstraintRow row
+ genericParamEncLogEntries.Add(struct (TableNames.GenericParamConstraint, row.RowId, EditAndContinueOperation.Default))
+ encMap.Add(struct (TableNames.GenericParamConstraint, row.RowId))
+
+ let assemblyReferenceRows =
+ assemblyReferenceRows |> sortRowsByRowId "AssemblyRef" (fun row -> 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))
+
+ let standaloneSignatureRows =
+ standaloneSignatureRows
+ |> sortRowsByRowId "StandAloneSig" (fun row -> 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))
+
+ let customAttributeRows =
+ customAttributeRows |> sortRowsByRowId "CustomAttribute" (fun row -> row.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)
+
+ let propertyMapRows =
+ propertyMapRows |> sortRowsByRowId "PropertyMap" (fun row -> row.RowId)
+
+ 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)
+
+ let eventMapRows = eventMapRows |> sortRowsByRowId "EventMap" (fun row -> row.RowId)
+
+ 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()
+
+ let propertyDefinitionRows =
+ propertyDefinitionRows |> sortRowsByRowId "Property" (fun row -> row.RowId)
+
+ 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()
+
+ let eventDefinitionRows =
+ eventDefinitionRows |> sortRowsByRowId "Event" (fun row -> row.RowId)
+
+ 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()
+
+ let hasSemanticsKey row =
+ match row.AssociationInfo with
+ | MethodSemanticsAssociation.EventAssociation(_, rowId) -> rowId <<< 1
+ | MethodSemanticsAssociation.PropertyAssociation(_, rowId) -> (rowId <<< 1) ||| 1
+
+ let methodSemanticsRows =
+ methodSemanticsRows
+ |> sortRowsByRowId "MethodSemantics" (fun row -> row.RowId)
+ |> validatePrimaryKeyOrder "MethodSemantics" hasSemanticsKey
+
+ 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 |> List.sortBy (fun (_, newToken, _) -> newToken) 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/AbstractIL/ILBaselineReader.fs b/src/Compiler/AbstractIL/ILBaselineReader.fs
new file mode 100644
index 00000000000..e4fcb51fc39
--- /dev/null
+++ b/src/Compiler/AbstractIL/ILBaselineReader.fs
@@ -0,0 +1,1459 @@
+/// Minimal binary reader for baseline metadata extraction.
+/// Replaces SRM MetadataReader dependency for hot reload baseline creation.
+/// Parses PE/CLI metadata headers to extract heap sizes and table row counts.
+///
+/// This module provides a pure F# implementation for reading the minimum metadata
+/// needed to create an FSharpEmitBaseline, without requiring System.Reflection.Metadata.
+///
+/// References:
+/// - ECMA-335 II.24 (Metadata physical layout)
+/// - Roslyn DeltaMetadataWriter.cs for heap offset handling
+module internal FSharp.Compiler.AbstractIL.ILBaselineReader
+
+open System
+open System.Collections.Immutable
+open System.IO
+open System.Reflection.PortableExecutable
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+
+/// Read a little-endian 16-bit integer from bytes at offset.
+let private readUInt16 (bytes: byte[]) (offset: int) =
+ uint16 bytes.[offset] ||| (uint16 bytes.[offset + 1] <<< 8)
+
+/// Read a little-endian 32-bit integer from bytes at offset.
+let private readInt32 (bytes: byte[]) (offset: int) =
+ int bytes.[offset]
+ ||| (int bytes.[offset + 1] <<< 8)
+ ||| (int bytes.[offset + 2] <<< 16)
+ ||| (int bytes.[offset + 3] <<< 24)
+
+/// Read a little-endian 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
+ let rowCount = readInt32 bytes rowCountOffset
+
+ if rowCount < 0 then
+ invalidArg (nameof bytes) "metadata table row counts must be non-negative"
+
+ rowCounts.[i] <- rowCount
+ rowCountOffset <- rowCountOffset + 4
+
+ heapSizes, rowCounts, offset
+
+/// Extract metadata snapshot from PE file bytes.
+/// This replaces metadataSnapshotFromReader for hot reload baseline creation.
+let metadataSnapshotFromBytes (bytes: byte[]) : MetadataSnapshot option =
+ match findMetadataRoot bytes with
+ | None -> None
+ | Some metadataRoot ->
+ let streamHeaders = parseStreamHeaders bytes metadataRoot
+
+ // Find required streams
+ let stringsStream = findStream streamHeaders "#Strings"
+ let userStringsStream = findStream streamHeaders "#US"
+ let blobStream = findStream streamHeaders "#Blob"
+ let guidStream = findStream streamHeaders "#GUID"
+
+ let tablesStream =
+ findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
+
+ match tablesStream with
+ | None -> None
+ | Some tables ->
+ let _, rowCounts, _ = parseTablesStream bytes tables
+
+ // SRM's StringHeap trims the #Strings alignment padding down to a single
+ // terminating zero (StringHeap.TrimEnd), and EnC heap aggregation (runtime,
+ // MetadataAggregator, Roslyn EmitBaseline) places generation-1 strings right
+ // after that TRIMMED size. The baseline snapshot must use the same virtual size
+ // or every delta-heap string reference is shifted by the padding bytes.
+ // #US/#Blob/#GUID are not trimmed by SRM and keep the stream header size.
+ let trimmedStringHeapSize =
+ match stringsStream with
+ | None -> 0
+ | Some stream ->
+ if stream.Size = 0 then
+ 0
+ else
+ let last = stream.Offset + stream.Size - 1
+ let mutable i = last
+
+ while i >= stream.Offset && bytes.[i] = 0uy do
+ i <- i - 1
+
+ if i = last then
+ // No trailing zero: malformed but mirror SRM and keep the raw size.
+ stream.Size
+ else
+ // Keep one terminating zero after the last non-zero byte.
+ i - stream.Offset + 2
+
+ let heapSizeInfo =
+ {
+ StringHeapSize = trimmedStringHeapSize
+ UserStringHeapSize = userStringsStream |> Option.map (fun s -> s.Size) |> Option.defaultValue 0
+ BlobHeapSize = blobStream |> Option.map (fun s -> s.Size) |> Option.defaultValue 0
+ GuidHeapSize = guidStream |> Option.map (fun s -> s.Size) |> Option.defaultValue 0
+ }
+
+ Some
+ {
+ HeapSizes = heapSizeInfo
+ TableRowCounts = rowCounts
+ GuidHeapStart = heapSizeInfo.GuidHeapSize
+ }
+
+/// Read GUID from #GUID stream at 1-based index.
+let readGuidFromBytes (bytes: byte[]) (guidIndex: int) : Guid option =
+ if guidIndex <= 0 then
+ None
+ else
+ match findMetadataRoot bytes with
+ | None -> None
+ | Some metadataRoot ->
+ let streamHeaders = parseStreamHeaders bytes metadataRoot
+
+ match findStream streamHeaders "#GUID" with
+ | None -> None
+ | Some guidStream ->
+ // GUID indices are 1-based; each GUID is 16 bytes
+ let offset = guidStream.Offset + (guidIndex - 1) * 16
+ let streamEnd = int64 guidStream.Offset + int64 guidStream.Size
+ let guidEnd = int64 offset + 16L
+
+ if
+ guidStream.Offset < 0
+ || guidStream.Size < 0
+ || streamEnd > int64 bytes.Length
+ || offset < guidStream.Offset
+ || guidEnd > streamEnd
+ then
+ None
+ else
+ let guidBytes = bytes.[offset .. offset + 15]
+ Some(System.Guid(guidBytes))
+
+/// Reads the portable CodeView content ID embedded in a PE debug directory.
+let readCodeViewContentIdFromBytes (bytes: byte[]) : byte[] option =
+ try
+ use peReader = new PEReader(ImmutableArray.CreateRange bytes)
+
+ peReader.ReadDebugDirectory()
+ |> Seq.tryFind (fun entry -> entry.IsPortableCodeView)
+ |> Option.map (fun entry ->
+ let data = peReader.ReadCodeViewDebugDirectoryData entry
+ let contentId = Array.zeroCreate 20
+ data.Guid.ToByteArray().CopyTo(contentId, 0)
+ BitConverter.GetBytes(entry.Stamp).CopyTo(contentId, 16)
+ contentId)
+ with
+ | :? BadImageFormatException
+ | :? IOException
+ | :? InvalidOperationException -> None
+
+// ============================================================================
+// Table row reading infrastructure
+// ============================================================================
+
+/// Table indices per ECMA-335 II.22
+module private TableIndices =
+ let Module = 0
+ let TypeRef = 1
+ let TypeDef = 2
+ let FieldPtr = 3
+ let Field = 4
+ let MethodPtr = 5
+ let MethodDef = 6
+ let ParamPtr = 7
+ let Param = 8
+ let InterfaceImpl = 9
+ let MemberRef = 10
+ let Constant = 11
+ let CustomAttribute = 12
+ let FieldMarshal = 13
+ let DeclSecurity = 14
+ let ClassLayout = 15
+ let FieldLayout = 16
+ let StandAloneSig = 17
+ let EventMap = 18
+ let EventPtr = 19
+ let Event = 20
+ let PropertyMap = 21
+ let PropertyPtr = 22
+ let Property = 23
+ let MethodSemantics = 24
+ let MethodImpl = 25
+ let ModuleRef = 26
+ let TypeSpec = 27
+ let ImplMap = 28
+ let FieldRVA = 29
+ let Assembly = 32
+ let AssemblyRef = 35
+ let File = 38
+ let ExportedType = 39
+ let ManifestResource = 40
+ let NestedClass = 41
+ let GenericParam = 42
+ let MethodSpec = 43
+ let GenericParamConstraint = 44
+
+/// Parsed metadata context for reading table rows.
+/// Internal (not private): tiny reader members like TypeRefCount get cross-module
+/// inlined in Release builds, and inlined code referencing a module-private type
+/// fails CLR visibility checks at runtime (observed as an access violation on
+/// RowCounts from HotReloadBaseline's state machines).
+type internal MetadataContext =
+ {
+ Bytes: byte[]
+ HeapSizes: byte
+ RowCounts: int[]
+ TablesStart: int
+ StringIndexSize: int
+ GuidIndexSize: int
+ BlobIndexSize: int
+ StringsStreamOffset: int
+ 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 pointerTables =
+ [|
+ TableIndices.FieldPtr
+ TableIndices.MethodPtr
+ TableIndices.ParamPtr
+ TableIndices.EventPtr
+ TableIndices.PropertyPtr
+ |]
+
+ // The #- stream permits pointer-table indirection. This reader consumes the
+ // definition tables directly, so accepting a non-empty pointer table would
+ // associate members with the wrong declaring type.
+ if
+ tablesStream.Name = "#-"
+ && pointerTables |> Array.exists (fun table -> rowCounts.[table] <> 0)
+ then
+ None
+ else
+ let stringsBig = (heapSizes &&& 0x01uy) <> 0uy
+ let guidsBig = (heapSizes &&& 0x02uy) <> 0uy
+ let blobsBig = (heapSizes &&& 0x04uy) <> 0uy
+
+ // Calculate where row data starts (after row count array)
+ let 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
+// ============================================================================
+
+/// TypeDef row data needed for byte-only baseline token maps.
+type TypeDefRowData =
+ {
+ Flags: int
+ NameOffset: int
+ NamespaceOffset: int
+ Extends: int
+ FieldList: int
+ MethodList: int
+ }
+
+/// Read a TypeDef row by 1-based row ID.
+let private readTypeDefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : TypeDefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.TypeDef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.TypeDef]
+ let offset = tableOffsets.[TableIndices.TypeDef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let extendsOffset = offset + 4 + ctx.StringIndexSize + ctx.StringIndexSize
+ let fieldListOffset = extendsOffset + typeDefOrRefSize ctx.RowCounts
+
+ let methodListOffset =
+ fieldListOffset + tableIndexSize ctx.RowCounts TableIndices.Field
+
+ Some
+ {
+ Flags = readInt32 bytes offset
+ NameOffset = readHeapIndex bytes (offset + 4) ctx.StringIndexSize
+ NamespaceOffset = readHeapIndex bytes (offset + 4 + ctx.StringIndexSize) ctx.StringIndexSize
+ Extends = readHeapIndex bytes extendsOffset (typeDefOrRefSize ctx.RowCounts)
+ FieldList = readHeapIndex bytes fieldListOffset (tableIndexSize ctx.RowCounts TableIndices.Field)
+ MethodList = readHeapIndex bytes methodListOffset (tableIndexSize ctx.RowCounts TableIndices.MethodDef)
+ }
+
+/// MethodDef row data needed for baseline cache.
+type MethodDefRowData =
+ {
+ RVA: int
+ ImplFlags: int
+ Flags: int
+ NameOffset: int
+ SignatureOffset: int
+ ParamList: int // First Param row ID (1-based)
+ }
+
+/// Read a MethodDef row by 1-based row ID.
+let private readMethodDefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : MethodDefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.MethodDef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.MethodDef]
+ let offset = tableOffsets.[TableIndices.MethodDef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // MethodDef: RVA(4) + ImplFlags(2) + Flags(2) + Name(str) + Signature(blob) + ParamList(Param)
+ let rva = readInt32 bytes offset
+ let implFlags = int (readUInt16 bytes (offset + 4))
+ let flags = int (readUInt16 bytes (offset + 6))
+ let nameOffset = readHeapIndex bytes (offset + 8) ctx.StringIndexSize
+
+ let sigOffset =
+ readHeapIndex bytes (offset + 8 + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ let paramList =
+ readHeapIndex bytes (offset + 8 + ctx.StringIndexSize + ctx.BlobIndexSize) (tableIndexSize ctx.RowCounts TableIndices.Param)
+
+ Some
+ {
+ RVA = rva
+ ImplFlags = implFlags
+ Flags = flags
+ NameOffset = nameOffset
+ SignatureOffset = sigOffset
+ ParamList = paramList
+ }
+
+/// Param row data.
+type ParamRowData =
+ {
+ Flags: int
+ Sequence: int
+ NameOffset: int
+ }
+
+/// Read a Param row by 1-based row ID.
+let private readParamRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : ParamRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.Param] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.Param]
+ let offset = tableOffsets.[TableIndices.Param] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // Param: Flags(2) + Sequence(2) + Name(str)
+ let flags = int (readUInt16 bytes offset)
+ let sequence = int (readUInt16 bytes (offset + 2))
+ let nameOffset = readHeapIndex bytes (offset + 4) ctx.StringIndexSize
+
+ Some
+ {
+ Flags = flags
+ Sequence = sequence
+ NameOffset = nameOffset
+ }
+
+/// Property row data.
+type PropertyRowData =
+ {
+ Flags: int
+ NameOffset: int
+ SignatureOffset: int
+ }
+
+/// Read a Property row by 1-based row ID.
+let private readPropertyRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : PropertyRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.Property] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.Property]
+ let offset = tableOffsets.[TableIndices.Property] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // Property: Flags(2) + Name(str) + Type(blob)
+ let flags = int (readUInt16 bytes offset)
+ let nameOffset = readHeapIndex bytes (offset + 2) ctx.StringIndexSize
+
+ let sigOffset =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ Some
+ {
+ Flags = flags
+ NameOffset = nameOffset
+ SignatureOffset = sigOffset
+ }
+
+/// Event row data.
+type EventRowData =
+ {
+ Flags: int
+ NameOffset: int
+ EventType: int // Coded index (TypeDefOrRef)
+ }
+
+/// Read an Event row by 1-based row ID.
+let private readEventRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : EventRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.Event] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.Event]
+ let offset = tableOffsets.[TableIndices.Event] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // Event: EventFlags(2) + Name(str) + EventType(TypeDefOrRef)
+ let flags = int (readUInt16 bytes offset)
+ let nameOffset = readHeapIndex bytes (offset + 2) ctx.StringIndexSize
+
+ Some
+ {
+ Flags = flags
+ NameOffset = nameOffset
+ EventType = 0
+ }
+
+/// TypeRef row data.
+type TypeRefRowData =
+ {
+ ResolutionScope: int // Coded index
+ NameOffset: int
+ NamespaceOffset: int
+ }
+
+/// Read a TypeRef row by 1-based row ID.
+let private readTypeRefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : TypeRefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.TypeRef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.TypeRef]
+ let offset = tableOffsets.[TableIndices.TypeRef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let resScopeSize = resolutionScopeSize ctx.RowCounts
+
+ // TypeRef: ResolutionScope(coded) + TypeName(str) + TypeNamespace(str)
+ let resScope = readHeapIndex bytes offset resScopeSize
+ let nameOffset = readHeapIndex bytes (offset + resScopeSize) ctx.StringIndexSize
+
+ let nsOffset =
+ readHeapIndex bytes (offset + resScopeSize + ctx.StringIndexSize) ctx.StringIndexSize
+
+ Some
+ {
+ ResolutionScope = resScope
+ NameOffset = nameOffset
+ NamespaceOffset = nsOffset
+ }
+
+/// MemberRef row data.
+type MemberRefRowData =
+ {
+ /// Raw MemberRefParent coded index value (tag bits 0-2, row id above).
+ Parent: int
+ NameOffset: int
+ SignatureOffset: int
+ }
+
+/// Read a MemberRef row by 1-based row ID.
+let private readMemberRefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : MemberRefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.MemberRef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.MemberRef]
+ let offset = tableOffsets.[TableIndices.MemberRef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let parentSize = memberRefParentSize ctx.RowCounts
+
+ // MemberRef: Class(MemberRefParent) + Name(str) + Signature(blob)
+ let parent = readHeapIndex bytes offset parentSize
+ let nameOffset = readHeapIndex bytes (offset + parentSize) ctx.StringIndexSize
+
+ let sigOffset =
+ readHeapIndex bytes (offset + parentSize + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ Some
+ {
+ Parent = parent
+ NameOffset = nameOffset
+ SignatureOffset = sigOffset
+ }
+
+/// CustomAttribute row data.
+type CustomAttributeRowData =
+ {
+ /// Raw HasCustomAttribute coded index value (tag bits 0-4, row id above).
+ Parent: int
+ /// Raw CustomAttributeType coded index value (tag bits 0-2, row id above).
+ Constructor: int
+ ValueOffset: int
+ }
+
+/// Read a CustomAttribute row by 1-based row ID.
+let private readCustomAttributeRow
+ (ctx: MetadataContext)
+ (rowSizes: int[])
+ (tableOffsets: int[])
+ (rowId: int)
+ : CustomAttributeRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.CustomAttribute] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.CustomAttribute]
+ let offset = tableOffsets.[TableIndices.CustomAttribute] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+ let parentSize = hasCustomAttributeSize ctx.RowCounts
+ let ctorSize = customAttributeTypeSize ctx.RowCounts
+
+ // CustomAttribute: Parent(HasCustomAttribute) + Type(CustomAttributeType) + Value(blob)
+ let parent = readHeapIndex bytes offset parentSize
+ let ctor = readHeapIndex bytes (offset + parentSize) ctorSize
+
+ let valueOffset =
+ readHeapIndex bytes (offset + parentSize + ctorSize) ctx.BlobIndexSize
+
+ Some
+ {
+ Parent = parent
+ Constructor = ctor
+ ValueOffset = valueOffset
+ }
+
+/// Read a TypeSpec row by 1-based row ID; the row is a single #Blob signature column.
+let private readTypeSpecRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : int option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.TypeSpec] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.TypeSpec]
+ let offset = tableOffsets.[TableIndices.TypeSpec] + (rowId - 1) * rowSize
+ Some(readHeapIndex ctx.Bytes offset ctx.BlobIndexSize)
+
+/// Read a length-prefixed blob (ECMA-335 II.24.2.4 compressed length) from the #Blob heap.
+let private readBlobFromHeap (ctx: MetadataContext) (offset: int) : byte[] =
+ if offset <= 0 then
+ Array.empty
+ else
+ let start = ctx.BlobStreamOffset + offset
+ let b0 = int ctx.Bytes.[start]
+
+ let length, headerSize =
+ if b0 &&& 0x80 = 0 then
+ b0, 1
+ elif b0 &&& 0xC0 = 0x80 then
+ (((b0 &&& 0x3F) <<< 8) ||| int ctx.Bytes.[start + 1]), 2
+ else
+ (((b0 &&& 0x1F) <<< 24)
+ ||| (int ctx.Bytes.[start + 1] <<< 16)
+ ||| (int ctx.Bytes.[start + 2] <<< 8)
+ ||| int ctx.Bytes.[start + 3]),
+ 4
+
+ if length = 0 then
+ Array.empty
+ else
+ ctx.Bytes.[start + headerSize .. start + headerSize + length - 1]
+
+/// AssemblyRef row data.
+type AssemblyRefRowData =
+ {
+ MajorVersion: int
+ MinorVersion: int
+ BuildNumber: int
+ RevisionNumber: int
+ Flags: int
+ PublicKeyOrToken: int // Blob offset
+ NameOffset: int
+ Culture: int // String offset
+ HashValue: int // Blob offset
+ }
+
+/// Read an AssemblyRef row by 1-based row ID.
+let private readAssemblyRefRow (ctx: MetadataContext) (rowSizes: int[]) (tableOffsets: int[]) (rowId: int) : AssemblyRefRowData option =
+ if rowId < 1 || rowId > ctx.RowCounts.[TableIndices.AssemblyRef] then
+ None
+ else
+ let rowSize = rowSizes.[TableIndices.AssemblyRef]
+ let offset = tableOffsets.[TableIndices.AssemblyRef] + (rowId - 1) * rowSize
+ let bytes = ctx.Bytes
+
+ // AssemblyRef: MajorVersion(2) + MinorVersion(2) + BuildNumber(2) + RevisionNumber(2) +
+ // Flags(4) + PublicKeyOrToken(blob) + Name(str) + Culture(str) + HashValue(blob)
+ let major = int (readUInt16 bytes offset)
+ let minor = int (readUInt16 bytes (offset + 2))
+ let build = int (readUInt16 bytes (offset + 4))
+ let rev = int (readUInt16 bytes (offset + 6))
+ let flags = readInt32 bytes (offset + 8)
+ let pkOffset = readHeapIndex bytes (offset + 12) ctx.BlobIndexSize
+
+ let nameOffset =
+ readHeapIndex bytes (offset + 12 + ctx.BlobIndexSize) ctx.StringIndexSize
+
+ let cultureOffset =
+ readHeapIndex bytes (offset + 12 + ctx.BlobIndexSize + ctx.StringIndexSize) ctx.StringIndexSize
+
+ let hashOffset =
+ readHeapIndex bytes (offset + 12 + ctx.BlobIndexSize + ctx.StringIndexSize + ctx.StringIndexSize) ctx.BlobIndexSize
+
+ Some
+ {
+ MajorVersion = major
+ MinorVersion = minor
+ BuildNumber = build
+ RevisionNumber = rev
+ Flags = flags
+ PublicKeyOrToken = pkOffset
+ NameOffset = nameOffset
+ Culture = cultureOffset
+ HashValue = hashOffset
+ }
+
+/// Module row data (including name offset).
+type ModuleRowData =
+ {
+ Generation: int
+ NameOffset: int
+ MvidIndex: int
+ EncIdIndex: int
+ EncBaseIdIndex: int
+ }
+
+/// Read the Module row (there's only one, row 1).
+let private readModuleRow (ctx: MetadataContext) (_rowSizes: int[]) (tableOffsets: int[]) : ModuleRowData option =
+ if ctx.RowCounts.[TableIndices.Module] < 1 then
+ None
+ else
+ let offset = tableOffsets.[TableIndices.Module]
+ let bytes = ctx.Bytes
+
+ // Module: Generation(2) + Name(str) + Mvid(guid) + EncId(guid) + EncBaseId(guid)
+ let generation = int (readUInt16 bytes offset)
+ let nameOffset = readHeapIndex bytes (offset + 2) ctx.StringIndexSize
+
+ let mvidIndex =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize) ctx.GuidIndexSize
+
+ let encIdIndex =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize + ctx.GuidIndexSize) ctx.GuidIndexSize
+
+ let encBaseIdIndex =
+ readHeapIndex bytes (offset + 2 + ctx.StringIndexSize + ctx.GuidIndexSize + ctx.GuidIndexSize) ctx.GuidIndexSize
+
+ Some
+ {
+ Generation = generation
+ NameOffset = nameOffset
+ MvidIndex = mvidIndex
+ EncIdIndex = encIdIndex
+ EncBaseIdIndex = encBaseIdIndex
+ }
+
+// ============================================================================
+// Public API for baseline metadata extraction
+// ============================================================================
+
+/// Baseline metadata reader that provides access to table rows without SRM.
+type BaselineMetadataReader private (ctx: MetadataContext, rowSizes: int[], tableOffsets: int[]) =
+
+ /// Create a reader from PE file bytes.
+ static member Create(bytes: byte[]) : BaselineMetadataReader option =
+ match createMetadataContext bytes with
+ | None -> None
+ | Some ctx ->
+ let rowSizes = calculateTableRowSizes ctx
+ let tableOffsets = calculateTableOffsets ctx rowSizes
+ Some(BaselineMetadataReader(ctx, rowSizes, tableOffsets))
+
+ /// Get the table row counts.
+ member _.RowCounts = ctx.RowCounts
+
+ /// Get the TypeDef row count.
+ member _.TypeDefCount = ctx.RowCounts.[TableIndices.TypeDef]
+
+ /// Read a TypeDef row by 1-based row ID.
+ member _.GetTypeDef(rowId: int) =
+ readTypeDefRow ctx rowSizes tableOffsets rowId
+
+ /// Read a MethodDef row by 1-based row ID.
+ member _.GetMethodDef(rowId: int) =
+ readMethodDefRow ctx rowSizes tableOffsets rowId
+
+ /// Get the MethodDef row range owned by a TypeDef row.
+ member this.GetTypeMethodRange(typeRowId: int) : (int * int) option =
+ match this.GetTypeDef(typeRowId) with
+ | None -> None
+ | Some typeDef ->
+ let firstMethod = typeDef.MethodList
+
+ let lastMethod =
+ if typeRowId < ctx.RowCounts.[TableIndices.TypeDef] then
+ match this.GetTypeDef(typeRowId + 1) with
+ | Some next -> next.MethodList - 1
+ | None -> ctx.RowCounts.[TableIndices.MethodDef]
+ else
+ ctx.RowCounts.[TableIndices.MethodDef]
+
+ if firstMethod > lastMethod then
+ None
+ else
+ Some(firstMethod, lastMethod)
+
+ /// Read a Param row by 1-based row ID.
+ member _.GetParam(rowId: int) =
+ readParamRow ctx rowSizes tableOffsets rowId
+
+ /// Get the last param row for a method (based on next method's ParamList or table end).
+ member this.GetMethodParamRange(methodRowId: int) : (int * int) option =
+ match this.GetMethodDef(methodRowId) with
+ | None -> None
+ | Some methodDef ->
+ let firstParam = methodDef.ParamList
+
+ let lastParam =
+ if methodRowId < ctx.RowCounts.[TableIndices.MethodDef] then
+ match this.GetMethodDef(methodRowId + 1) with
+ | Some next -> next.ParamList - 1
+ | None -> ctx.RowCounts.[TableIndices.Param]
+ else
+ ctx.RowCounts.[TableIndices.Param]
+
+ if firstParam > lastParam then
+ None
+ else
+ Some(firstParam, lastParam)
+
+ /// Read a Property row by 1-based row ID.
+ member _.GetProperty(rowId: int) =
+ readPropertyRow ctx rowSizes tableOffsets rowId
+
+ /// Read an Event row by 1-based row ID.
+ member _.GetEvent(rowId: int) =
+ readEventRow ctx rowSizes tableOffsets rowId
+
+ /// Read a TypeRef row by 1-based row ID.
+ member _.GetTypeRef(rowId: int) =
+ readTypeRefRow ctx rowSizes tableOffsets rowId
+
+ /// Read an AssemblyRef row by 1-based row ID.
+ member _.GetAssemblyRef(rowId: int) =
+ readAssemblyRefRow ctx rowSizes tableOffsets rowId
+
+ /// Get the AssemblyRef row count.
+ member _.AssemblyRefCount = ctx.RowCounts.[TableIndices.AssemblyRef]
+
+ /// Get the TypeRef row count.
+ member _.TypeRefCount = ctx.RowCounts.[TableIndices.TypeRef]
+
+ /// Read the Module row.
+ member _.GetModule() = readModuleRow ctx rowSizes tableOffsets
+
+ /// Read a string from the #Strings heap.
+ member _.GetString(offset: int) = readStringFromHeap ctx offset
+
+ /// Read a MemberRef row by 1-based row ID.
+ member _.GetMemberRef(rowId: int) =
+ readMemberRefRow ctx rowSizes tableOffsets rowId
+
+ /// Get the MemberRef row count.
+ member _.MemberRefCount = ctx.RowCounts.[TableIndices.MemberRef]
+
+ /// Read a TypeSpec row's signature blob offset by 1-based row ID.
+ member _.GetTypeSpecSignatureOffset(rowId: int) =
+ readTypeSpecRow ctx rowSizes tableOffsets rowId
+
+ /// Get the TypeSpec row count.
+ member _.TypeSpecCount = ctx.RowCounts.[TableIndices.TypeSpec]
+
+ /// Read a length-prefixed blob from the #Blob heap.
+ member _.GetBlob(offset: int) = readBlobFromHeap ctx offset
+
+ /// Read a CustomAttribute row by 1-based row ID.
+ member _.GetCustomAttributeRow(rowId: int) =
+ readCustomAttributeRow ctx rowSizes tableOffsets rowId
+
+ /// Get the CustomAttribute row count.
+ member _.CustomAttributeCount = ctx.RowCounts.[TableIndices.CustomAttribute]
+
+ /// Decode a HasCustomAttribute coded index to a metadata token.
+ /// Tag bits (5), ECMA-335 II.24.2.6 ordering.
+ member _.DecodeHasCustomAttributeToken(codedIndex: int) : int =
+ let tag = codedIndex &&& 0x1F
+ let rowId = codedIndex >>> 5
+
+ let table =
+ match tag with
+ | 0 -> 0x06 // MethodDef
+ | 1 -> 0x04 // Field
+ | 2 -> 0x01 // TypeRef
+ | 3 -> 0x02 // TypeDef
+ | 4 -> 0x08 // Param
+ | 5 -> 0x09 // InterfaceImpl
+ | 6 -> 0x0A // MemberRef
+ | 7 -> 0x00 // Module
+ | 8 -> 0x0E // DeclSecurity
+ | 9 -> 0x17 // Property
+ | 10 -> 0x14 // Event
+ | 11 -> 0x11 // StandAloneSig
+ | 12 -> 0x1A // ModuleRef
+ | 13 -> 0x1B // TypeSpec
+ | 14 -> 0x20 // Assembly
+ | 15 -> 0x23 // AssemblyRef
+ | 16 -> 0x26 // File
+ | 17 -> 0x27 // ExportedType
+ | 18 -> 0x28 // ManifestResource
+ | 19 -> 0x2A // GenericParam
+ | 20 -> 0x2C // GenericParamConstraint
+ | _ -> 0x2B // MethodSpec
+
+ (table <<< 24) ||| rowId
+
+ /// Decode a CustomAttributeType coded index to a metadata token.
+ /// Tag bits (3): 2=MethodDef, 3=MemberRef.
+ member _.DecodeCustomAttributeTypeToken(codedIndex: int) : int =
+ let tag = codedIndex &&& 0x7
+ let rowId = codedIndex >>> 3
+
+ let table =
+ match tag with
+ | 2 -> 0x06 // MethodDef
+ | _ -> 0x0A // MemberRef
+
+ (table <<< 24) ||| rowId
+
+ /// Decode a MemberRefParent coded index to a metadata token.
+ /// Tag bits (3): 0=TypeDef, 1=TypeRef, 2=ModuleRef, 3=MethodDef, 4=TypeSpec.
+ member _.DecodeMemberRefParentToken(codedIndex: int) : int =
+ let tag = codedIndex &&& 0x7
+ let rowId = codedIndex >>> 3
+
+ let tableIndex =
+ match tag with
+ | 0 -> TableIndices.TypeDef
+ | 1 -> TableIndices.TypeRef
+ | 2 -> TableIndices.ModuleRef
+ | 3 -> TableIndices.MethodDef
+ | 4 -> TableIndices.TypeSpec
+ | _ -> -1
+
+ if tableIndex < 0 then 0 else (tableIndex <<< 24) ||| rowId
+
+ /// Decode ResolutionScope coded index to (table index, row id).
+ /// Tag bits: 0=Module, 1=ModuleRef, 2=AssemblyRef, 3=TypeRef
+ member _.DecodeResolutionScope(codedIndex: int) : (int * int) =
+ let tag = codedIndex &&& 0x3
+ let rowId = codedIndex >>> 2
+
+ let tableIndex =
+ match tag with
+ | 0 -> TableIndices.Module
+ | 1 -> TableIndices.ModuleRef
+ | 2 -> TableIndices.AssemblyRef
+ | 3 -> TableIndices.TypeRef
+ | _ -> -1
+
+ (tableIndex, rowId)
+
+/// Read Module.Mvid GUID from assembly bytes.
+/// Module table row 1 contains the Mvid index.
+let readModuleMvidFromBytes (bytes: byte[]) : System.Guid option =
+ match findMetadataRoot bytes with
+ | None -> None
+ | Some metadataRoot ->
+ let streamHeaders = parseStreamHeaders bytes metadataRoot
+
+ let tablesStreamOpt =
+ findStream streamHeaders "#~" |> Option.orElse (findStream streamHeaders "#-")
+
+ match tablesStreamOpt with
+ | None -> None
+ | Some tablesStream ->
+ let heapSizes, rowCounts, tablesOffset = 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 =
+ {
+ /// Content ID stored in the #Pdb stream.
+ ContentId: byte[]
+ /// Row counts for PDB tables (indexed by PDB table index - 0x30)
+ /// Index 0 = Document, 1 = MethodDebugInformation, etc.
+ TableRowCounts: int[]
+ /// Entry point method token (if present)
+ EntryPointToken: int option
+ }
+
+/// Parse the #Pdb stream to extract PDB-specific info.
+/// The #Pdb stream contains: PdbId (20 bytes), EntryPoint token (4 bytes), ReferencedTypeSystemTables (8 bytes), TypeSystemTableRows (var)
+let private parsePdbStream (bytes: byte[]) (pdbStream: StreamHeader) : int option =
+ if pdbStream.Size < 24 then
+ None
+ else
+ let offset = pdbStream.Offset
+ // PdbId: 20 bytes (GUID + 4 bytes stamp)
+ // EntryPoint: 4 bytes (method def token, or 0 if no entry point)
+ let entryPointToken = readInt32 bytes (offset + 20)
+ if entryPointToken = 0 then None else Some entryPointToken
+
+/// Parse Portable PDB table row counts from the #~ stream.
+/// Portable PDB uses tables 0x30-0x37, but the valid bits are still in position 0x30+.
+let private parsePdbTablesStream (bytes: byte[]) (tablesStream: StreamHeader) : int[] =
+ let offset = tablesStream.Offset
+
+ // Header: Reserved(4) + MajorVersion(1) + MinorVersion(1) + HeapSizes(1) + Reserved(1) + Valid(8) + Sorted(8) + RowCounts(var)
+ let valid = 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, pdbStreamOpt with
+ | Some tablesStream, Some pdbStream when pdbStream.Size >= 24 ->
+ let rowCounts = parsePdbTablesStream pdbBytes tablesStream
+ let entryPoint = parsePdbStream pdbBytes pdbStream
+
+ Some
+ {
+ ContentId = pdbBytes.[pdbStream.Offset .. pdbStream.Offset + 19]
+ TableRowCounts = rowCounts
+ EntryPointToken = entryPoint
+ }
+ | _ -> None
+ with
+ | :? System.IndexOutOfRangeException -> None
+ | :? System.ArgumentOutOfRangeException -> None
diff --git a/src/Compiler/AbstractIL/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/IlxDeltaStreams.fs b/src/Compiler/AbstractIL/IlxDeltaStreams.fs
new file mode 100644
index 00000000000..0cefc8804b5
--- /dev/null
+++ b/src/Compiler/AbstractIL/IlxDeltaStreams.fs
@@ -0,0 +1,299 @@
+module internal FSharp.Compiler.AbstractIL.IlxDeltaStreams
+
+open System
+open System.Collections.Generic
+open System.Text
+open FSharp.Compiler.AbstractIL.BinaryConstants
+open FSharp.Compiler.AbstractIL.ILBinaryWriter
+open FSharp.Compiler.AbstractIL.ILDeltaHandles
+open FSharp.Compiler.IO
+
+// ============================================================================
+// Pure F# Token Calculators (replaces SRM MetadataBuilder for token arithmetic)
+// ============================================================================
+
+/// User string heap token calculator.
+/// Tracks user strings added during delta emission and computes tokens.
+/// Token format: 0x70000000 | heap_offset
+type UserStringTokenCalculator(heapStartOffset: int) =
+ let cache = Dictionary(StringComparer.Ordinal)
+ // #US heaps reserve offset 0 for the null/empty entry.
+ // First emitted delta literal must start at relative offset 1.
+ let mutable currentOffset = 1
+
+ /// Encode a user string per ECMA-335 II.24.2.4:
+ /// - Compressed length prefix (1-4 bytes)
+ /// - UTF-16LE encoded characters
+ /// - Terminal byte computed via markerForUnicodeBytes (shared with ilwrite.fs)
+ let encodeUserString (value: string) : byte[] =
+ let utf16Bytes = Encoding.Unicode.GetBytes(value)
+ let blobLength = utf16Bytes.Length + 1 // +1 for terminal byte
+
+ // Compute compressed length encoding size
+ let lengthBytes =
+ if blobLength <= 0x7F then 1
+ elif blobLength <= 0x3FFF then 2
+ else 4
+
+ let result = Array.zeroCreate (lengthBytes + utf16Bytes.Length + 1)
+ let mutable pos = 0
+
+ // Write compressed length
+ if blobLength <= 0x7F then
+ result.[pos] <- byte blobLength
+ pos <- pos + 1
+ elif blobLength <= 0x3FFF then
+ result.[pos] <- byte (0x80 ||| (blobLength >>> 8))
+ result.[pos + 1] <- byte blobLength
+ pos <- pos + 2
+ else
+ result.[pos] <- byte (0xC0 ||| (blobLength >>> 24))
+ result.[pos + 1] <- byte (blobLength >>> 16)
+ result.[pos + 2] <- byte (blobLength >>> 8)
+ result.[pos + 3] <- byte blobLength
+ pos <- pos + 4
+
+ // Write UTF-16LE bytes
+ Buffer.BlockCopy(utf16Bytes, 0, result, pos, utf16Bytes.Length)
+ pos <- pos + utf16Bytes.Length
+
+ // Write terminal byte - use shared markerForUnicodeBytes from ILBinaryWriter
+ result.[pos] <- byte (markerForUnicodeBytes utf16Bytes)
+
+ result
+
+ /// Get or add a user string, returning the absolute token.
+ member _.GetOrAddUserString(value: string) : int =
+ match cache.TryGetValue(value) with
+ | true, token -> token
+ | _ ->
+ let absoluteOffset = heapStartOffset + currentOffset
+ let token = 0x70000000 ||| absoluteOffset
+ cache.[value] <- token
+ let encoded = encodeUserString value
+ currentOffset <- currentOffset + encoded.Length
+ token
+
+/// Standalone signature token calculator.
+/// Tracks signatures added during delta emission and computes tokens.
+/// Token format: 0x11000000 | row_id (StandaloneSig table = 0x11)
+type StandaloneSignatureTokenCalculator(baselineRowCount: int) =
+ let cache = Dictionary(HashIdentity.Structural)
+ let signatures = ResizeArray()
+ let mutable nextRowId = baselineRowCount + 1
+
+ /// Add a standalone signature and return its token.
+ member _.AddStandaloneSignature(signature: byte[]) : int =
+ if signature.Length = 0 then
+ 0
+ else
+ match cache.TryGetValue(signature) with
+ | true, token -> token
+ | _ ->
+ let rowId = nextRowId
+ nextRowId <- nextRowId + 1
+ let token = 0x11000000 ||| rowId
+ cache.[Array.copy signature] <- token
+ signatures.Add((rowId, Array.copy signature))
+ token
+
+ /// Get the list of (rowId, blob) tuples for serialization.
+ member _.GetSignatures() : (int * byte[]) list = signatures |> Seq.toList
+
+/// Represents a method body update captured for an Edit-and-Continue delta.
+type MethodBodyUpdate =
+ {
+ MethodToken: int
+ LocalSignatureToken: int
+ CodeOffset: int
+ CodeLength: int
+ }
+
+/// Represents a standalone signature (e.g., local signature) emitted in the delta metadata.
+type StandaloneSignatureUpdate = { RowId: int; Blob: byte[] }
+
+/// The emitted metadata and IL payloads produced by .
+type IlDeltaStreams =
+ {
+ IL: byte[]
+ MethodBodies: MethodBodyUpdate list
+ StandaloneSignatures: StandaloneSignatureUpdate list
+ }
+
+///
+/// Accumulates metadata tables, Edit-and-Continue bookkeeping, and encoded method bodies prior to serialising
+/// a hot reload delta. Uses pure F# token calculators instead of SRM MetadataBuilder.
+/// Callers retrieve the resulting byte arrays via .
+///
+///
+/// Baseline #US heap size (bytes) to seed the user-string token calculator, or 0 for a baseline-less builder.
+///
+///
+/// Baseline StandAloneSig table row count to seed standalone signature row numbering, or 0 for a baseline-less
+/// builder.
+///
+///
+/// The feature branch this was extracted from seeds these values from an ilwrite-produced baseline snapshot
+/// type. That snapshot type is part of a larger, not-yet-upstreamed baseline-capture change to ilwrite.fs/.fsi,
+/// so it is intentionally out of scope here; callers that have such a snapshot should pass its two relevant
+/// fields (heap size / row count) directly.
+///
+type IlDeltaStreamBuilder(initialUserStringHeapSize: int, initialStandAloneSigRowCount: int) =
+ let userStringCalculator = UserStringTokenCalculator(initialUserStringHeapSize)
+
+ let standaloneSigCalculator =
+ StandaloneSignatureTokenCalculator(initialStandAloneSigRowCount)
+
+ let methodBodyStream = ByteBuffer.Create(256)
+ let methodBodies = ResizeArray()
+ let mutable isBuilt = false
+
+ let alignStream alignment =
+ // Align to N-byte boundary by padding with zeros
+ let pos = methodBodyStream.Position
+ let padding = (alignment - (pos % alignment)) % alignment
+
+ for _ = 1 to padding do
+ methodBodyStream.EmitByte 0uy
+
+ /// Construct a builder with no baseline (generation-1 / test scenarios).
+ new() = IlDeltaStreamBuilder(0, 0)
+
+ /// Expose the user string token calculator for advanced scenarios.
+ member _.UserStringCalculator = userStringCalculator
+
+ /// Inspection hook primarily used in unit tests.
+ member _.MethodBodies = methodBodies |> Seq.toList
+
+ /// Get the standalone signatures that were added.
+ member _.StandaloneSignatures =
+ standaloneSigCalculator.GetSignatures()
+ |> List.map (fun (rowId, blob) -> { RowId = rowId; Blob = blob })
+
+ /// Add a method body update for the supplied metadata token.
+ member _.AddMethodBody
+ (
+ methodToken: int,
+ localSignatureToken: int,
+ ilBytes: byte[],
+ maxStack: int,
+ initLocals: bool,
+ exceptionRegions: IlExceptionRegion[],
+ remapEntityToken: int -> int
+ ) =
+ let ilLength = ilBytes.Length
+ let hasExceptionRegions = exceptionRegions.Length > 0
+
+ let flags =
+ int e_CorILMethod_FatFormat
+ ||| (if hasExceptionRegions then
+ int e_CorILMethod_MoreSects
+ else
+ 0)
+ ||| (if initLocals then int e_CorILMethod_InitLocals else 0)
+
+ alignStream 4
+ let offset = methodBodyStream.Position
+
+ methodBodyStream.EmitByte(byte flags)
+ methodBodyStream.EmitByte(0x30uy)
+ methodBodyStream.EmitUInt16(uint16 maxStack)
+ methodBodyStream.EmitInt32(ilLength)
+ methodBodyStream.EmitInt32(localSignatureToken)
+ methodBodyStream.EmitBytes(ilBytes)
+
+ let padding = (4 - (ilLength % 4)) &&& 0x3
+
+ if padding > 0 then
+ for _ = 1 to padding do
+ methodBodyStream.EmitByte 0uy
+
+ if hasExceptionRegions then
+ alignStream 4
+ let regions = exceptionRegions
+ let smallSize = regions.Length * 12 + 4
+
+ let canUseSmall =
+ smallSize <= 0xFF
+ && regions
+ |> Array.forall (fun region ->
+ region.TryOffset <= 0xFFFF
+ && region.HandlerOffset <= 0xFFFF
+ && region.TryLength <= 0xFF
+ && region.HandlerLength <= 0xFF)
+
+ let encodeKind (region: IlExceptionRegion) : int * int =
+ match region.Kind with
+ | IlExceptionRegionKind.Catch ->
+ let token =
+ if region.CatchTypeToken = 0 then
+ 0
+ else
+ remapEntityToken region.CatchTypeToken
+
+ e_COR_ILEXCEPTION_CLAUSE_EXCEPTION, token
+ | IlExceptionRegionKind.Filter -> e_COR_ILEXCEPTION_CLAUSE_FILTER, region.FilterOffset
+ | IlExceptionRegionKind.Finally -> e_COR_ILEXCEPTION_CLAUSE_FINALLY, 0
+ | IlExceptionRegionKind.Fault -> e_COR_ILEXCEPTION_CLAUSE_FAULT, 0
+ | _ -> e_COR_ILEXCEPTION_CLAUSE_EXCEPTION, 0
+
+ if canUseSmall then
+ methodBodyStream.EmitByte(e_CorILMethod_Sect_EHTable)
+ methodBodyStream.EmitByte(byte smallSize)
+ methodBodyStream.EmitByte(0uy)
+ methodBodyStream.EmitByte(0uy)
+
+ for region in regions do
+ let kind, extra = encodeKind region
+ methodBodyStream.EmitUInt16(uint16 kind)
+ methodBodyStream.EmitUInt16(uint16 region.TryOffset)
+ methodBodyStream.EmitByte(byte region.TryLength)
+ methodBodyStream.EmitUInt16(uint16 region.HandlerOffset)
+ methodBodyStream.EmitByte(byte region.HandlerLength)
+ methodBodyStream.EmitInt32(extra)
+ else
+ let bigSize = regions.Length * 24 + 4
+ methodBodyStream.EmitByte(e_CorILMethod_Sect_EHTable ||| e_CorILMethod_Sect_FatFormat)
+ methodBodyStream.EmitByte(byte bigSize)
+ methodBodyStream.EmitByte(byte (bigSize >>> 8))
+ methodBodyStream.EmitByte(byte (bigSize >>> 16))
+
+ for region in regions do
+ let kind, extra = encodeKind region
+ methodBodyStream.EmitInt32(kind)
+ methodBodyStream.EmitInt32(region.TryOffset)
+ methodBodyStream.EmitInt32(region.TryLength)
+ methodBodyStream.EmitInt32(region.HandlerOffset)
+ methodBodyStream.EmitInt32(region.HandlerLength)
+ methodBodyStream.EmitInt32(extra)
+
+ let update =
+ {
+ MethodToken = methodToken
+ LocalSignatureToken = localSignatureToken
+ CodeOffset = offset
+ CodeLength = ilLength
+ }
+
+ methodBodies.Add(update)
+ update
+
+ /// Adds a standalone signature blob to the metadata stream and returns its token.
+ member _.AddStandaloneSignature(signature: byte[]) =
+ standaloneSigCalculator.AddStandaloneSignature(signature)
+
+ ///
+ /// Finalise the builder and emit the metadata and IL blobs. The builder can only be consumed once; subsequent
+ /// invocations throw to prevent mismatched Edit-and-Continue state.
+ ///
+ member this.Build() =
+ if isBuilt then
+ invalidOp "IlDeltaStreamBuilder.Build may only be called once per builder instance."
+
+ isBuilt <- true
+
+ {
+ IL = methodBodyStream.AsMemory().ToArray()
+ MethodBodies = methodBodies |> Seq.toList
+ StandaloneSignatures = this.StandaloneSignatures
+ }
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/EncMethodDebugInformation.fs b/src/Compiler/CodeGen/EncMethodDebugInformation.fs
new file mode 100644
index 00000000000..c67be11a632
--- /dev/null
+++ b/src/Compiler/CodeGen/EncMethodDebugInformation.fs
@@ -0,0 +1,970 @@
+/// Edit-and-Continue method debug information blobs for hot reload.
+///
+/// This module replicates, byte for byte, the three Portable-PDB CustomDebugInformation
+/// blob formats Roslyn persists per method to support Edit and Continue
+/// (roslyn/src/Compilers/Core/Portable/Emit/EditAndContinueMethodDebugInformation.cs):
+///
+/// - EnC Local Slot Map (kind 755F52A8-91C5-45BE-B4B8-209571E552BD)
+/// - EnC Lambda and Closure Map (kind A643004C-0240-496F-A783-30D64F4979DE)
+/// - EnC State Machine State Map (kind 8B78CD68-2EDE-420B-980B-E15884B8AAA3)
+///
+/// (GUIDs: roslyn/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs.)
+///
+/// All multi-byte integers use the ECMA-335 compressed unsigned/signed encodings via
+/// System.Reflection.Metadata's BlobBuilder.WriteCompressedInteger /
+/// WriteCompressedSignedInteger and BlobReader.ReadCompressedInteger /
+/// ReadCompressedSignedInteger, exactly as Roslyn writes/reads them.
+///
+/// F# semantics of the "syntax offset" slots: Roslyn stores the syntax offset of the
+/// lambda/closure/state-machine-suspension syntax node. The F# typed-tree diff has no
+/// syntax map; instead these integer slots carry OCCURRENCE KEYS — a deterministic
+/// int packed from the occurrence ordinal chain of the lambda occurrence model
+/// (TypedTreeDiff.LambdaOccurrenceId). See tryEncodeOccurrenceKey/decodeOccurrenceKey.
+/// The blob format is identical either way, so mdv/Roslyn tooling can still decode our
+/// maps; only the *meaning* of the integers is F#-specific (debugger-interop
+/// caveat documented in docs/hot-reload-closure-mapping.md).
+module internal FSharp.Compiler.EncMethodDebugInformation
+
+#nowarn "9" // NativePtr: BlobReader only exposes a byte*-based constructor
+
+open System
+open System.Collections.Generic
+open System.Collections.Immutable
+open System.IO
+open System.Reflection.Metadata
+open System.Reflection.Metadata.Ecma335
+open System.Runtime.InteropServices
+open System.Text
+open Microsoft.FSharp.NativeInterop
+
+open FSharp.Compiler.AbstractIL.ILPdbWriter
+open FSharp.Compiler.TcGlobals
+open FSharp.Compiler.TypedTree
+open FSharp.Compiler.TypedTreeDiff
+
+/// Portable-PDB CustomDebugInformation kind GUIDs for the EnC blobs, copied verbatim
+/// from roslyn/src/Dependencies/CodeAnalysis.Debugging/PortableCustomDebugInfoKinds.cs.
+[]
+module PortableCustomDebugInfoKinds =
+
+ /// EnC Local Slot Map CDI kind.
+ let encLocalSlotMap = Guid("755F52A8-91C5-45BE-B4B8-209571E552BD")
+
+ /// EnC Lambda and Closure Map CDI kind.
+ let encLambdaAndClosureMap = Guid("A643004C-0240-496F-A783-30D64F4979DE")
+
+ /// EnC State Machine State Map CDI kind.
+ let encStateMachineStateMap = Guid("8B78CD68-2EDE-420B-980B-E15884B8AAA3")
+
+ /// F#-owned hot reload synthesized-name snapshot CDI kind. The blob records
+ /// FSharpSynthesizedTypeMaps.Snapshot bucket arrays in allocation-slot order.
+ let fsharpSynthesizedNameSnapshot = Guid("49DDB47E-9C74-46EC-8626-0350676571EB")
+
+/// Closure ordinal of a lambda that is lowered to a static (non-capturing) method.
+/// Mirrors Roslyn's LambdaDebugInfo.StaticClosureOrdinal.
+[]
+let StaticClosureOrdinal = -1
+
+/// Closure ordinal of a lambda closed over the 'this' pointer only.
+/// Mirrors Roslyn's LambdaDebugInfo.ThisOnlyClosureOrdinal.
+[]
+let ThisOnlyClosureOrdinal = -2
+
+/// Smallest valid closure ordinal. Mirrors Roslyn's LambdaDebugInfo.MinClosureOrdinal.
+[]
+let MinClosureOrdinal = ThisOnlyClosureOrdinal
+
+/// Method ordinal of a method that has no lambda map (an empty blob decodes to this).
+/// Mirrors Roslyn's DebugId.UndefinedOrdinal.
+[]
+let UndefinedMethodOrdinal = -1
+
+/// Marker byte introducing the (optional) negative syntax-offset baseline in the
+/// local-slot-map blob. Mirrors Roslyn's SyntaxOffsetBaseline = 0xFF.
+[]
+let private SyntaxOffsetBaselineMarker = 0xFFuy
+
+/// Largest synthesized-local kind serializable in the slot map: the kind is stored as
+/// (kind + 1) in bits 0-6 of the leading byte (bit 7 flags a trailing ordinal), and
+/// Roslyn's reader recovers it with mask 0x3F, so only kinds 0..0x3E round-trip.
+[]
+let MaxSerializableLocalKind = 0x3E
+
+/// One slot in the EnC Local Slot Map: the local variable layout of a method body,
+/// recorded so a later generation can map its locals onto the same slot indices.
+[]
+type EncLocalSlotInfo =
+ /// A short-lived lowering temp: serialized as the single byte 0x00, carrying no
+ /// identity (a later generation never reuses it).
+ | Temp
+
+ /// A long-lived synthesized local.
+ /// kind: synthesized-local kind (Roslyn SynthesizedLocalKind value, 0..MaxSerializableLocalKind;
+ /// 0 = user-defined local).
+ /// syntaxOffset: in F#, the occurrence key of the declaring occurrence
+ /// (Roslyn: syntax offset of the local's declarator).
+ /// ordinal: zero-based disambiguator among slots sharing the same kind and offset (>= 0).
+ | Slot of kind: int * syntaxOffset: int * ordinal: int
+
+/// One closure scope in the EnC Lambda and Closure Map. The closure's ordinal is its
+/// index in EncMethodDebugInformation.Closures; lambdas reference closures by that index.
+/// SyntaxOffset: in F#, the occurrence key of the closure's occurrence.
+type EncClosureInfo =
+ {
+ /// Occurrence key (Roslyn: syntax offset of the scope owning the closure).
+ SyntaxOffset: int
+ }
+
+/// One lambda in the EnC Lambda and Closure Map.
+type EncLambdaInfo =
+ {
+ /// Occurrence key (Roslyn: syntax offset of the lambda body).
+ SyntaxOffset: int
+ /// Index into EncMethodDebugInformation.Closures of the closure holding the
+ /// lambda's captures, or StaticClosureOrdinal / ThisOnlyClosureOrdinal.
+ ClosureOrdinal: int
+ }
+
+/// One suspension point in the EnC State Machine State Map.
+type EncStateMachineStateInfo =
+ {
+ /// State machine state number assigned to the suspension point (may be negative:
+ /// Roslyn uses negative numbers for increasing-iteration finalize states).
+ StateNumber: int
+ /// Occurrence key (Roslyn: syntax offset of the await/yield syntax node).
+ SyntaxOffset: int
+ }
+
+/// Debugging information associated with a method, persisted by the compiler in the
+/// Portable PDB to support Edit and Continue. Mirrors Roslyn's
+/// EditAndContinueMethodDebugInformation.
+type EncMethodDebugInformation =
+ {
+ /// Ordinal of the method within its generation (>= -1; UndefinedMethodOrdinal when absent).
+ MethodOrdinal: int
+ /// Local slot layout, in slot-index order (EnC Local Slot Map).
+ LocalSlots: EncLocalSlotInfo list
+ /// Closure scopes, in ordinal order (EnC Lambda and Closure Map).
+ Closures: EncClosureInfo list
+ /// Lambdas, in ordinal order (EnC Lambda and Closure Map).
+ Lambdas: EncLambdaInfo list
+ /// State machine suspension points (EnC State Machine State Map).
+ StateMachineStates: EncStateMachineStateInfo list
+ }
+
+ /// An empty map (no slots, lambdas, closures or states; undefined method ordinal).
+ static member Empty =
+ {
+ MethodOrdinal = UndefinedMethodOrdinal
+ LocalSlots = []
+ Closures = []
+ Lambdas = []
+ StateMachineStates = []
+ }
+
+// ---------------------------------------------------------------------------
+// Occurrence-key packing
+// ---------------------------------------------------------------------------
+
+/// Maximum encodable occurrence ordinal: each chain segment is 16 bits.
+[]
+let private MaxOccurrenceSegment = 0xFFFF
+
+/// Compressed unsigned integers must lie in [0, 0x1FFFFFFF); after baseline adjustment
+/// the serialized value is (key - baseline) with baseline <= -1, so keys must stay
+/// strictly below 0x1FFFFFFF - 1 to be writable. Cap at 29 bits minus the adjustment.
+[]
+let private MaxOccurrenceKey = 0x1FFFFFFD
+
+/// Packs an occurrence ordinal chain (root-first enclosing-occurrence ordinals,
+/// ending with the occurrence's own ordinal) into the deterministic int carried in the
+/// "syntax offset" blob slots. Packing: 16-bit segments, least-significant segment =
+/// the occurrence's own ordinal; an enclosing ordinal p is stored as (p + 1) shifted
+/// left 16 so that depth-1 keys (< 0x10000) and depth-2 keys (>= 0x10000) never collide.
+/// Fails closed (None) past the limits: chains deeper than 2, ordinals > 0xFFFF,
+/// or keys exceeding the compressed-integer budget — callers must then treat the
+/// occurrence as unmappable (rude edit), never truncate.
+let tryEncodeOccurrenceKey (ordinalChain: int list) : int option =
+ match ordinalChain with
+ | [ ordinal ] when ordinal >= 0 && ordinal <= MaxOccurrenceSegment -> Some ordinal
+ | [ parent; ordinal ] when
+ parent >= 0
+ && ordinal >= 0
+ && ordinal <= MaxOccurrenceSegment
+ && parent < MaxOccurrenceSegment
+ ->
+ // Pack in int64: a large parent would wrap negative in int32 and otherwise pass
+ // the upper-bound check, turning an unrepresentable occurrence into a corrupt key.
+ let key = ((int64 parent + 1L) <<< 16) ||| int64 ordinal
+
+ if key <= int64 MaxOccurrenceKey then
+ Some(int key)
+ else
+ None
+ | _ -> None
+
+/// Unpacks an occurrence key produced by tryEncodeOccurrenceKey back into its
+/// root-first ordinal chain.
+let decodeOccurrenceKey (key: int) : int list =
+ if key < 0 then
+ invalidArg (nameof key) $"occurrence key must be non-negative, got %d{key}"
+ elif key <= MaxOccurrenceSegment then
+ [ key ]
+ else
+ [ (key >>> 16) - 1; key &&& MaxOccurrenceSegment ]
+
+// ---------------------------------------------------------------------------
+// Blob helpers
+// ---------------------------------------------------------------------------
+
+let private invalidData (blobName: string) (offset: int) =
+ raise (InvalidDataException $"invalid EnC %s{blobName} blob: unexpected data at offset %d{offset}")
+
+// Absent CDI rows arrive as null at runtime even though the parameter is non-null in the
+// nullness model, so guard with box (FS3261-safe) rather than dropping the check.
+let private isEmpty (blob: byte[]) = isNull (box blob) || blob.Length = 0
+
+// ---------------------------------------------------------------------------
+// F# hot reload module CDI: synthesized-name allocation snapshot
+// Format:
+// compressed(version = 1), compressed(bucket count),
+// then buckets sorted by key for deterministic PDB bytes:
+// string key, compressed(name count), string name in allocation-slot order.
+// Strings are compressed(byte length) followed by UTF-8 bytes.
+// ---------------------------------------------------------------------------
+
+[]
+let private SynthesizedNameSnapshotBlobVersion = 1
+
+let private writeUtf8String (builder: BlobBuilder) (value: string) =
+ if isNull (box value) then
+ invalidArg (nameof value) "snapshot strings must be non-null"
+
+ let bytes = Encoding.UTF8.GetBytes value
+ builder.WriteCompressedInteger bytes.Length
+ builder.WriteBytes bytes
+
+let private readUtf8String (blobName: string) (reader: byref) =
+ let length = reader.ReadCompressedInteger()
+
+ if length < 0 || length > reader.RemainingBytes then
+ invalidData blobName reader.Offset
+
+ let bytes = reader.ReadBytes length
+ Encoding.UTF8.GetString(bytes, 0, bytes.Length)
+
+let private materializeSynthesizedNameSnapshot (snapshot: seq) =
+ snapshot
+ |> Seq.map (fun struct (key, names) ->
+ if isNull (box key) then
+ invalidArg (nameof snapshot) "snapshot keys must be non-null"
+
+ if isNull (box names) then
+ invalidArg (nameof snapshot) $"snapshot bucket '{key}' must be non-null"
+
+ key, Array.copy names)
+ |> Seq.sortBy fst
+ |> Seq.toArray
+
+/// Serializes an allocation-ordered synthesized-name snapshot into the F#-owned module
+/// CDI blob. An empty snapshot returns an empty blob so no CDI row needs to be emitted.
+let serializeSynthesizedNameSnapshot (snapshot: seq) : byte[] =
+ let buckets = materializeSynthesizedNameSnapshot snapshot
+
+ if buckets.Length = 0 then
+ Array.empty
+ else
+ let builder = BlobBuilder()
+ builder.WriteCompressedInteger SynthesizedNameSnapshotBlobVersion
+ builder.WriteCompressedInteger buckets.Length
+
+ for key, names in buckets do
+ writeUtf8String builder key
+ builder.WriteCompressedInteger names.Length
+
+ for name in names do
+ writeUtf8String builder name
+
+ builder.ToArray()
+
+/// Deserializes the F#-owned synthesized-name snapshot CDI blob. Bucket order in the
+/// blob is deterministic only; each bucket array is returned exactly in recorded slot order.
+let deserializeSynthesizedNameSnapshot (blob: byte[]) : Map =
+ if isEmpty blob then
+ Map.empty
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ try
+ let version = reader.ReadCompressedInteger()
+
+ if version <> SynthesizedNameSnapshotBlobVersion then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ let bucketCount = reader.ReadCompressedInteger()
+
+ if bucketCount <= 0 || bucketCount > reader.RemainingBytes / 2 then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ let buckets = ResizeArray()
+
+ for _ in 1..bucketCount do
+ let key = readUtf8String "synthesized name snapshot" &reader
+ let nameCount = reader.ReadCompressedInteger()
+
+ // Every serialized name consumes at least one byte for its UTF-8 length,
+ // so bound allocation by the remaining payload before creating the array.
+ if nameCount < 0 || nameCount > reader.RemainingBytes then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ let names = Array.zeroCreate nameCount
+
+ for i in 0 .. nameCount - 1 do
+ names[i] <- readUtf8String "synthesized name snapshot" &reader
+
+ buckets.Add(key, names)
+
+ if reader.RemainingBytes <> 0 then
+ invalidData "synthesized name snapshot" reader.Offset
+
+ buckets |> Seq.map id |> Map.ofSeq
+ with :? BadImageFormatException ->
+ invalidData "synthesized name snapshot" reader.Offset
+ finally
+ handle.Free()
+
+/// Creates the module-level CustomDebugInformation row for the allocation-ordered
+/// synthesized-name snapshot. Empty snapshots emit no row.
+let computeSynthesizedNameSnapshotCustomDebugInfoRows (snapshot: seq) : PdbModuleCustomDebugInfo list =
+
+ let blob = serializeSynthesizedNameSnapshot snapshot
+
+ if blob.Length = 0 then
+ []
+ else
+ [
+ {
+ KindGuid = PortableCustomDebugInfoKinds.fsharpSynthesizedNameSnapshot
+ Blob = blob
+ }
+ ]
+
+// ---------------------------------------------------------------------------
+// EnC Local Slot Map
+// Format (EditAndContinueMethodDebugInformation.cs, SerializeLocalSlots lines 145-191,
+// UncompressSlotMap lines 92-143): optional baseline record [0xFF, compressed(-baseline)],
+// then one record per slot: 0x00 for a temp, otherwise a leading byte with bits 0-6 =
+// kind + 1 and bit 7 = has-ordinal flag, followed by compressed(syntaxOffset - baseline)
+// and, when flagged, compressed(ordinal).
+// ---------------------------------------------------------------------------
+
+/// Serializes the EnC Local Slot Map blob for 'info', byte-for-byte as Roslyn's
+/// SerializeLocalSlots. Returns the empty array when there are no slots (no CDI row
+/// should be emitted then).
+let serializeLocalSlots (info: EncMethodDebugInformation) : byte[] =
+ match info.LocalSlots with
+ | [] -> Array.empty
+ | slots ->
+ let builder = BlobBuilder()
+
+ // The baseline is the most negative syntax offset, or -1 when none is negative
+ // (Roslyn lines 147-160). Offsets are stored relative to it so the common
+ // all-non-negative case costs no baseline record.
+ let syntaxOffsetBaseline =
+ (-1, slots)
+ ||> List.fold (fun acc slot ->
+ match slot with
+ | EncLocalSlotInfo.Temp -> acc
+ | EncLocalSlotInfo.Slot(_, syntaxOffset, _) -> min acc syntaxOffset)
+
+ if syntaxOffsetBaseline <> -1 then
+ builder.WriteByte SyntaxOffsetBaselineMarker
+ builder.WriteCompressedInteger(-syntaxOffsetBaseline)
+
+ for slot in slots do
+ match slot with
+ | EncLocalSlotInfo.Temp -> builder.WriteByte 0uy
+ | EncLocalSlotInfo.Slot(kind, syntaxOffset, ordinal) ->
+ if kind < 0 || kind > MaxSerializableLocalKind then
+ invalidArg (nameof info) $"local slot kind %d{kind} is outside the serializable range 0..%d{MaxSerializableLocalKind}"
+
+ if ordinal < 0 then
+ invalidArg (nameof info) $"local slot ordinal must be non-negative, got %d{ordinal}"
+
+ let hasOrdinal = ordinal > 0
+ let b = byte (kind + 1) ||| (if hasOrdinal then 0x80uy else 0uy)
+ builder.WriteByte b
+ builder.WriteCompressedInteger(syntaxOffset - syntaxOffsetBaseline)
+
+ if hasOrdinal then
+ builder.WriteCompressedInteger ordinal
+
+ builder.ToArray()
+
+/// Deserializes an EnC Local Slot Map blob, byte-for-byte as Roslyn's UncompressSlotMap.
+/// An empty (or null) blob yields no slots.
+let deserializeLocalSlots (blob: byte[]) : EncLocalSlotInfo list =
+ if isEmpty blob then
+ []
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ let slots = ResizeArray()
+ let mutable syntaxOffsetBaseline = -1
+
+ try
+ while reader.RemainingBytes > 0 do
+ let b = reader.ReadByte()
+
+ if b = SyntaxOffsetBaselineMarker then
+ syntaxOffsetBaseline <- -reader.ReadCompressedInteger()
+ elif b = 0uy then
+ slots.Add EncLocalSlotInfo.Temp
+ else
+ // Roslyn recovers the kind with mask 0x3F (line 126); bit 7 flags
+ // a trailing ordinal, bit 6 is unused by the writer.
+ let kind = int (b &&& 0x3Fuy) - 1
+ let hasOrdinal = b &&& 0x80uy <> 0uy
+ let syntaxOffset = reader.ReadCompressedInteger() + syntaxOffsetBaseline
+ let ordinal = if hasOrdinal then reader.ReadCompressedInteger() else 0
+ slots.Add(EncLocalSlotInfo.Slot(kind, syntaxOffset, ordinal))
+ with :? BadImageFormatException ->
+ invalidData "local slot map" reader.Offset
+
+ List.ofSeq slots
+ finally
+ handle.Free()
+
+// ---------------------------------------------------------------------------
+// EnC Lambda and Closure Map
+// Format (SerializeLambdaMap lines 261-302, UncompressLambdaMap lines 197-259):
+// compressed(methodOrdinal + 1), compressed(-baseline), compressed(closureCount),
+// closureCount * compressed(syntaxOffset - baseline), then until the blob ends:
+// [compressed(syntaxOffset - baseline), compressed(closureOrdinal - MinClosureOrdinal)]
+// per lambda.
+// ---------------------------------------------------------------------------
+
+/// Serializes the EnC Lambda and Closure Map blob for 'info', byte-for-byte as Roslyn's
+/// SerializeLambdaMap. Returns the empty array when there are no lambdas and no closures
+/// (Roslyn's MetadataWriter skips the CDI row in that case; note the method ordinal is
+/// then not persisted and decodes back as UndefinedMethodOrdinal).
+let serializeLambdaMap (info: EncMethodDebugInformation) : byte[] =
+ match info.Closures, info.Lambdas with
+ | [], [] -> Array.empty
+ | closures, lambdas ->
+ if info.MethodOrdinal < -1 then
+ invalidArg (nameof info) $"method ordinal must be >= -1, got %d{info.MethodOrdinal}"
+
+ let builder = BlobBuilder()
+ builder.WriteCompressedInteger(info.MethodOrdinal + 1)
+
+ // Negative offsets are rare (Roslyn: field/property initializers; F#: reserved),
+ // so the baseline is -1 unless a smaller offset exists (Roslyn lines 266-286).
+ let syntaxOffsetBaseline =
+ let closureMin = (-1, closures) ||> List.fold (fun acc c -> min acc c.SyntaxOffset)
+ (closureMin, lambdas) ||> List.fold (fun acc l -> min acc l.SyntaxOffset)
+
+ builder.WriteCompressedInteger(-syntaxOffsetBaseline)
+ builder.WriteCompressedInteger closures.Length
+
+ for closure in closures do
+ builder.WriteCompressedInteger(closure.SyntaxOffset - syntaxOffsetBaseline)
+
+ for lambda in lambdas do
+ if
+ lambda.ClosureOrdinal < MinClosureOrdinal
+ || lambda.ClosureOrdinal >= closures.Length
+ then
+ invalidArg
+ (nameof info)
+ $"lambda closure ordinal %d{lambda.ClosureOrdinal} is outside [%d{MinClosureOrdinal}, %d{closures.Length})"
+
+ builder.WriteCompressedInteger(lambda.SyntaxOffset - syntaxOffsetBaseline)
+ builder.WriteCompressedInteger(lambda.ClosureOrdinal - MinClosureOrdinal)
+
+ builder.ToArray()
+
+/// Deserializes an EnC Lambda and Closure Map blob, byte-for-byte as Roslyn's
+/// UncompressLambdaMap. An empty (or null) blob yields (UndefinedMethodOrdinal, [], []).
+let deserializeLambdaMap (blob: byte[]) : int * EncClosureInfo list * EncLambdaInfo list =
+ if isEmpty blob then
+ UndefinedMethodOrdinal, [], []
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ let closures = ResizeArray()
+ let lambdas = ResizeArray()
+ let mutable methodOrdinal = UndefinedMethodOrdinal
+
+ try
+ methodOrdinal <- reader.ReadCompressedInteger() - 1
+ let syntaxOffsetBaseline = -reader.ReadCompressedInteger()
+ let closureCount = reader.ReadCompressedInteger()
+
+ for _ in 1..closureCount do
+ let syntaxOffset = reader.ReadCompressedInteger() + syntaxOffsetBaseline
+ closures.Add { SyntaxOffset = syntaxOffset }
+
+ while reader.RemainingBytes > 0 do
+ let syntaxOffset = reader.ReadCompressedInteger() + syntaxOffsetBaseline
+ let closureOrdinal = reader.ReadCompressedInteger() + MinClosureOrdinal
+
+ if closureOrdinal >= closureCount then
+ invalidData "lambda map" reader.Offset
+
+ lambdas.Add
+ {
+ SyntaxOffset = syntaxOffset
+ ClosureOrdinal = closureOrdinal
+ }
+ with :? BadImageFormatException ->
+ invalidData "lambda map" reader.Offset
+
+ methodOrdinal, List.ofSeq closures, List.ofSeq lambdas
+ finally
+ handle.Free()
+
+// ---------------------------------------------------------------------------
+// EnC State Machine State Map
+// Format (SerializeStateMachineStates lines 364-381, UncompressStateMachineStates
+// lines 309-362): compressed(count); when count > 0: compressed(-baseline) followed by
+// count * [compressedSigned(stateNumber), compressed(syntaxOffset - baseline)], entries
+// ordered by syntax offset.
+// ---------------------------------------------------------------------------
+
+/// Serializes the EnC State Machine State Map blob for 'info', byte-for-byte as
+/// Roslyn's SerializeStateMachineStates: entries are sorted by syntax offset (stably,
+/// preserving relative order of equal offsets, which encodes the per-offset relative
+/// ordinal). Returns the empty array when there are no states (no CDI row then).
+let serializeStateMachineStates (info: EncMethodDebugInformation) : byte[] =
+ match info.StateMachineStates with
+ | [] -> Array.empty
+ | states ->
+ let builder = BlobBuilder()
+ builder.WriteCompressedInteger states.Length
+
+ // Unlike the other two blobs the baseline here is min(minOffset, 0)
+ // (Roslyn line 372).
+ let syntaxOffsetBaseline =
+ min (states |> List.map (fun s -> s.SyntaxOffset) |> List.min) 0
+
+ builder.WriteCompressedInteger(-syntaxOffsetBaseline)
+
+ // Roslyn's reader rejects more than 256 entries sharing one syntax offset
+ // (relative ordinal must fit a byte, line 344); fail closed at write time.
+ for _, group in states |> List.groupBy (fun s -> s.SyntaxOffset) do
+ if group.Length > 256 then
+ invalidArg (nameof info) $"more than 256 state machine states share syntax offset %d{group.Head.SyntaxOffset}"
+
+ for state in states |> List.sortBy (fun s -> s.SyntaxOffset) do
+ builder.WriteCompressedSignedInteger state.StateNumber
+ builder.WriteCompressedInteger(state.SyntaxOffset - syntaxOffsetBaseline)
+
+ builder.ToArray()
+
+/// Deserializes an EnC State Machine State Map blob, byte-for-byte as Roslyn's
+/// UncompressStateMachineStates (including the ordered-by-offset and <= 256-per-offset
+/// validations). An empty (or null) blob yields no states.
+let deserializeStateMachineStates (blob: byte[]) : EncStateMachineStateInfo list =
+ if isEmpty blob then
+ []
+ else
+ let handle = GCHandle.Alloc(blob, GCHandleType.Pinned)
+
+ try
+ let mutable reader =
+ BlobReader(NativePtr.ofNativeInt (handle.AddrOfPinnedObject()), blob.Length)
+
+ let states = ResizeArray()
+
+ try
+ let count = reader.ReadCompressedInteger()
+
+ if count > 0 then
+ let syntaxOffsetBaseline = -reader.ReadCompressedInteger()
+ let mutable lastSyntaxOffset = Int32.MinValue
+ let mutable relativeOrdinal = 0
+
+ for _ in 1..count do
+ let stateNumber = reader.ReadCompressedSignedInteger()
+ let syntaxOffset = syntaxOffsetBaseline + reader.ReadCompressedInteger()
+
+ // Entries must be ordered by syntax offset and at most 256 may
+ // share one offset (Roslyn lines 336-347).
+ if syntaxOffset < lastSyntaxOffset then
+ invalidData "state machine state map" reader.Offset
+
+ relativeOrdinal <-
+ if syntaxOffset = lastSyntaxOffset then
+ relativeOrdinal + 1
+ else
+ 0
+
+ if relativeOrdinal > 255 then
+ invalidData "state machine state map" reader.Offset
+
+ states.Add
+ {
+ StateNumber = stateNumber
+ SyntaxOffset = syntaxOffset
+ }
+
+ lastSyntaxOffset <- syntaxOffset
+ with :? BadImageFormatException ->
+ invalidData "state machine state map" reader.Offset
+
+ List.ofSeq states
+ finally
+ handle.Free()
+
+/// Deserializes EnC method debug information from the three blobs (any of which may be
+/// null or empty). Mirrors Roslyn's EditAndContinueMethodDebugInformation.Create.
+let deserialize (slotMapBlob: byte[]) (lambdaMapBlob: byte[]) (stateMachineStateMapBlob: byte[]) : EncMethodDebugInformation =
+ let methodOrdinal, closures, lambdas = deserializeLambdaMap lambdaMapBlob
+
+ {
+ MethodOrdinal = methodOrdinal
+ LocalSlots = deserializeLocalSlots slotMapBlob
+ Closures = closures
+ Lambdas = lambdas
+ StateMachineStates = deserializeStateMachineStates stateMachineStateMapBlob
+ }
+
+// ---------------------------------------------------------------------------
+// Baseline emission bridge: lambda occurrences -> CDI rows for the
+// portable PDB writer. Computed in the fsc emit path when --test:HotReloadDeltas
+// is on; the rows ride the IL writer options into ilwritepdb keyed by IL method name.
+// ---------------------------------------------------------------------------
+
+/// Root-first ordinal chain of an occurrence: the occurrence id stores enclosing
+/// ordinals nearest-enclosing-first, while the key packing wants root-first with the
+/// occurrence's own ordinal last.
+let private occurrenceOrdinalChain (occurrence: LambdaOccurrence) =
+ List.rev occurrence.Id.ParentChain @ [ occurrence.Id.Ordinal ]
+
+/// Builds the EnC method debug information for one member from its lambda
+/// occurrence sequence. Modeling decisions (documented in
+/// docs/hot-reload-closure-mapping.md, "Baseline CDI emission as implemented"):
+/// - MethodOrdinal stays UndefinedMethodOrdinal: F# needs no Roslyn-style
+/// partial-method/ordinal disambiguation at baseline.
+/// - One closure scope per occurrence, and lambda i references closure i: IlxGen
+/// lowers every lambda occurrence (curried group) to its own closure class, so
+/// unlike C# there is no shared display-class scope to model and no static/this-only
+/// lambdas at the typed-tree level (refinement to Static/ThisOnly ordinals is a
+/// lowering-side concern).
+/// - LocalSlots stays empty: the EnC Local Slot Map describes the lowered local slot
+/// layout, an IlxGen emission artifact that is not trivially derivable from the
+/// typed tree; it is omitted rather than guessed.
+/// Fails closed (None) when any occurrence key is not encodable (chains deeper than 2
+/// or ordinals past the packing limits): a partial map could silently mismatch
+/// occurrences, so the method then gets no lambda map at all.
+let tryCreateFromLambdaOccurrences (occurrences: LambdaOccurrence list) : EncMethodDebugInformation option =
+ let keys =
+ occurrences |> List.map (occurrenceOrdinalChain >> tryEncodeOccurrenceKey)
+
+ if keys |> List.exists Option.isNone then
+ None
+ else
+ let keys = keys |> List.map Option.get
+
+ Some
+ {
+ MethodOrdinal = UndefinedMethodOrdinal
+ LocalSlots = []
+ Closures = keys |> List.map (fun key -> { SyntaxOffset = key })
+ Lambdas =
+ keys
+ |> List.mapi (fun closureOrdinal key ->
+ {
+ SyntaxOffset = key
+ ClosureOrdinal = closureOrdinal
+ })
+ StateMachineStates = []
+ }
+
+/// Computes the per-member EnC method debug information of a flag-on compilation from its
+/// implementation files, keyed by IL method (compiled) name. Keying is fail closed: members
+/// without a compiled name, compiled names claimed by more than one member binding anywhere
+/// in the assembly (overloads, same-named members on different types), and members with
+/// unencodable occurrence chains are omitted, so an entry can never describe the wrong
+/// method. Members without lambda occurrences carry no entry.
+let computeMethodEncDebugInfo (g: TcGlobals) (implFiles: CheckedImplFile list) : Map =
+ let allMembers = implFiles |> List.collect (collectMemberLambdaOccurrences g)
+
+ let ambiguousNames =
+ allMembers
+ |> List.choose (fun (symbol, _) -> symbol.CompiledName)
+ |> List.countBy id
+ |> List.filter (fun (_, count) -> count > 1)
+ |> List.map fst
+ |> Set.ofList
+
+ (Map.empty, allMembers)
+ ||> List.fold (fun acc (symbol: SymbolId, occurrences) ->
+ match symbol.CompiledName, occurrences with
+ | Some methName, _ :: _ when not (Set.contains methName ambiguousNames) ->
+ match tryCreateFromLambdaOccurrences occurrences with
+ | Some info -> Map.add methName info acc
+ | None -> acc
+ | _ -> acc)
+
+/// Computes the per-method EnC CustomDebugInformation side channel for the baseline PDB
+/// writer from the optimized implementation files of a flag-on compilation, keyed by IL
+/// method (compiled) name (fail-closed keying per computeMethodEncDebugInfo) — the writer
+/// additionally drops any name that does not identify exactly one IL method row, so a map
+/// can never attach to the wrong method.
+let computeMethodCustomDebugInfoRows
+ (g: TcGlobals)
+ (implFiles: CheckedImplFile list)
+ (stateMachineResumePointsByStructName: Map)
+ : Map =
+
+ // State machine resume points are recorded by the IlxGen lowering against the
+ // emitted state machine STRUCT's full name ('{member}@hotreload...' nested in the
+ // member's enclosing type); the basic name of the struct's simple name is the
+ // owning member's compiled name, which is this conduit's key. Fail closed on
+ // collisions (two recordings reducing to one basic name: same-named members, or
+ // nested CEs lowering several machines inside one member) — a state map must never
+ // describe the wrong method. The PDB writer additionally drops any name that does
+ // not identify exactly one IL method row.
+ let recordedStateMachineStatesByMethodName =
+ let simpleName (fullName: string) =
+ let separatorIndex = fullName.LastIndexOfAny [| '+'; '.' |]
+
+ if separatorIndex >= 0 then
+ fullName.Substring(separatorIndex + 1)
+ else
+ fullName
+
+ let basicName (name: string) =
+ match name.IndexOf('@') with
+ | atIndex when atIndex > 0 -> name.Substring(0, atIndex)
+ | _ -> name
+
+ stateMachineResumePointsByStructName
+ |> Map.toList
+ |> List.map (fun (structFullName, resumePoints) -> basicName (simpleName structFullName), resumePoints)
+ |> List.groupBy fst
+ |> List.choose (fun (methName, group) ->
+ match group with
+ | [ (_, resumePoints) ] when not resumePoints.IsEmpty ->
+ // SyntaxOffset carries the resume point's ORDINAL (state numbers are
+ // positional in the F# lowering), keeping the occurrence-key
+ // philosophy: deterministic ints, not source offsets.
+ let states =
+ resumePoints
+ |> List.sortBy id
+ |> List.mapi (fun ordinal stateNumber ->
+ {
+ StateNumber = stateNumber
+ SyntaxOffset = ordinal
+ })
+
+ Some(methName, states)
+ | _ -> None)
+ |> Map.ofList
+
+ let derivedStateMachineStatesByMethodName =
+ let memberInputs = implFiles |> List.collect (collectMemberDebugInfoInputs g)
+
+ let ambiguousNames =
+ memberInputs
+ |> List.choose (fun input -> input.Symbol.CompiledName)
+ |> List.countBy id
+ |> List.filter (fun (_, count) -> count > 1)
+ |> List.map fst
+ |> Set.ofList
+
+ let tryDeriveStatesFromContinuations (occurrences: LambdaOccurrence list) =
+ let roots =
+ occurrences
+ |> List.filter (fun occurrence -> List.isEmpty occurrence.Id.ParentChain)
+
+ match roots with
+ | [ root ] ->
+ let sameEnd (occurrence: LambdaOccurrence) =
+ occurrence.Range.EndLine = root.Range.EndLine
+ && occurrence.Range.EndColumn = root.Range.EndColumn
+
+ let continuations =
+ occurrences
+ |> List.filter (fun occurrence -> not (List.isEmpty occurrence.Id.ParentChain) && sameEnd occurrence)
+
+ match continuations with
+ | [] -> None
+ | _ ->
+ continuations
+ |> List.mapi (fun ordinal _ ->
+ {
+ StateNumber = ordinal + 1
+ SyntaxOffset = ordinal
+ })
+ |> Some
+ | _ -> None
+
+ (Map.empty, memberInputs)
+ ||> List.fold (fun acc input ->
+ match input.Symbol.CompiledName, input.HasResumableStateMachine with
+ | Some methName, true when not (Set.contains methName ambiguousNames) ->
+ match tryDeriveStatesFromContinuations input.LambdaOccurrences with
+ | Some states -> Map.add methName states acc
+ | None -> acc
+ | _ -> acc)
+
+ let stateMachineStatesByMethodName =
+ (recordedStateMachineStatesByMethodName, derivedStateMachineStatesByMethodName)
+ ||> Map.fold (fun acc methName states ->
+ if Map.containsKey methName acc then
+ acc
+ else
+ Map.add methName states acc)
+
+ let lambdaRows =
+ (Map.empty, computeMethodEncDebugInfo g implFiles)
+ ||> Map.fold (fun acc methName info ->
+ let lambdaMapBlob = serializeLambdaMap info
+
+ if lambdaMapBlob.Length = 0 then
+ acc
+ else
+ // The EnC Local Slot Map stays omitted (see tryCreateFromLambdaOccurrences).
+ Map.add
+ methName
+ [
+ {
+ KindGuid = PortableCustomDebugInfoKinds.encLambdaAndClosureMap
+ Blob = lambdaMapBlob
+ }
+ ]
+ acc)
+
+ (lambdaRows, stateMachineStatesByMethodName)
+ ||> Map.fold (fun acc methName states ->
+ let stateMapBlob =
+ serializeStateMachineStates
+ { EncMethodDebugInformation.Empty with
+ StateMachineStates = states
+ }
+
+ if stateMapBlob.Length = 0 then
+ acc
+ else
+ let stateRow: PdbMethodCustomDebugInfo =
+ {
+ KindGuid = PortableCustomDebugInfoKinds.encStateMachineStateMap
+ Blob = stateMapBlob
+ }
+
+ match Map.tryFind methName acc with
+ | Some rows -> Map.add methName (rows @ [ stateRow ]) acc
+ | None -> Map.add methName [ stateRow ] acc)
+
+// ---------------------------------------------------------------------------
+// Baseline read bridge: portable-PDB EnC CDI rows -> the per-method map the
+// hot reload session baseline (FSharpEmitBaseline.EncMethodDebugInfos) exposes to the
+// generation-aware closure lowering.
+// ---------------------------------------------------------------------------
+
+/// Decodes every method-level EnC CustomDebugInformation row of a portable PDB image into
+/// per-method EnC debug information, keyed by MethodDef token (0x06xxxxxx). The CDI parent
+/// of the EnC rows is always a MethodDef handle, so token keying is unambiguous here — the
+/// name keying on the write side exists only because the PDB writer lacks tokens.
+/// Fail safe: a null/empty or non-PDB image yields the empty map (back-compat with
+/// baselines compiled without --test:HotReloadDeltas or whose PDBs carry no EnC rows), and a method
+/// whose blobs do not decode is omitted rather than guessed.
+let readEncMethodDebugInfoFromPortablePdb (pdbBytes: byte[]) : Map =
+ if isEmpty pdbBytes then
+ Map.empty
+ else
+ try
+ use provider =
+ MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.CreateRange pdbBytes)
+
+ let reader = provider.GetMetadataReader()
+
+ let slotMapBlobs = Dictionary()
+ let lambdaMapBlobs = Dictionary()
+ let stateMapBlobs = Dictionary()
+
+ for cdiHandle in reader.CustomDebugInformation do
+ let cdi = reader.GetCustomDebugInformation cdiHandle
+
+ if cdi.Parent.Kind = HandleKind.MethodDefinition then
+ let methodToken = MetadataTokens.GetToken cdi.Parent
+ let kind = reader.GetGuid cdi.Kind
+
+ if kind = PortableCustomDebugInfoKinds.encLocalSlotMap then
+ slotMapBlobs[methodToken] <- reader.GetBlobBytes cdi.Value
+ elif kind = PortableCustomDebugInfoKinds.encLambdaAndClosureMap then
+ lambdaMapBlobs[methodToken] <- reader.GetBlobBytes cdi.Value
+ elif kind = PortableCustomDebugInfoKinds.encStateMachineStateMap then
+ stateMapBlobs[methodToken] <- reader.GetBlobBytes cdi.Value
+
+ let methodTokens =
+ Seq.concat [ slotMapBlobs.Keys :> seq; lambdaMapBlobs.Keys; stateMapBlobs.Keys ]
+ |> Seq.distinct
+
+ let tryBlob (blobs: Dictionary) token =
+ match blobs.TryGetValue token with
+ | true, blob -> blob
+ | _ -> Array.empty
+
+ (Map.empty, methodTokens)
+ ||> Seq.fold (fun acc token ->
+ try
+ let info =
+ deserialize (tryBlob slotMapBlobs token) (tryBlob lambdaMapBlobs token) (tryBlob stateMapBlobs token)
+
+ Map.add token info acc
+ with :? InvalidDataException ->
+ // Fail closed per method: an undecodable blob never yields a partial
+ // (and so potentially mismatched) map for its method.
+ acc)
+ with :? BadImageFormatException ->
+ // Not a portable PDB image (or a corrupted one): the session still starts,
+ // with no per-method EnC information.
+ Map.empty
+
+/// Reads the F#-owned allocation-ordered synthesized-name snapshot from a portable PDB.
+/// None means either the record is absent (old baseline / flag-off baseline) or invalid;
+/// callers must then fall back to IL reconstruction rather than trusting a partial layout.
+let readSynthesizedNameSnapshotFromPortablePdb (pdbBytes: byte[]) : Map option =
+ if isEmpty pdbBytes then
+ None
+ else
+ try
+ use provider =
+ MetadataReaderProvider.FromPortablePdbImage(ImmutableArray.CreateRange pdbBytes)
+
+ let reader = provider.GetMetadataReader()
+
+ let blobs =
+ [
+ for cdiHandle in reader.CustomDebugInformation do
+ let cdi = reader.GetCustomDebugInformation cdiHandle
+
+ if cdi.Parent.Kind = HandleKind.ModuleDefinition then
+ let kind = reader.GetGuid cdi.Kind
+
+ if kind = PortableCustomDebugInfoKinds.fsharpSynthesizedNameSnapshot then
+ reader.GetBlobBytes cdi.Value
+ ]
+
+ match blobs with
+ | [ blob ] -> Some(deserializeSynthesizedNameSnapshot blob)
+ | _ -> None
+ with
+ | :? BadImageFormatException
+ | :? InvalidDataException -> None
diff --git a/src/Compiler/CodeGen/FSharpDefinitionIndex.fs b/src/Compiler/CodeGen/FSharpDefinitionIndex.fs
new file mode 100644
index 00000000000..5cc78697473
--- /dev/null
+++ b/src/Compiler/CodeGen/FSharpDefinitionIndex.fs
@@ -0,0 +1,105 @@
+module internal FSharp.Compiler.CodeGen.FSharpDefinitionIndex
+
+open System.Collections.Generic
+
+/// Represents the status of a definition row tracked in the index.
+type private EntryStatus<'T> =
+ | Added of rowId: int * item: 'T
+ | Existing of rowId: int * item: 'T
+
+/// F# analogue of Roslyn's DefinitionIndex
+/// Track row ids for definitions reused from the baseline or added in this generation.
+type DefinitionIndex<'T when 'T: not null and 'T: equality>(getExistingRowId: 'T -> int option, lastRowId: int) =
+ let added = Dictionary<'T, int>()
+ let rows = ResizeArray>()
+ let map = Dictionary