Skip to content

Commit 9a7d2bd

Browse files
committed
Emit LocalScope and LocalVariable rows for named locals
The managed ilasm wrote no LocalScope or LocalVariable rows, so debuggers saw no local names. Each lexical scope of a method body (the body itself and each { } block, including .try, catch, filter, finally and fault bodies) is now recorded when it closes, with its IL offsets and the named locals it declares, and the PDB gets the rows native ilasm writes: one LocalScope per scope that declares a named local, spanning the offsets at its { and } (the whole body for the method's own scope), followed by one LocalVariable per named local with the local's slot as Index and attributes 0. Unnamed locals get no row. The scopes are recorded whenever a PDB is requested, by any of the switches that request one. The rows are sorted by start offset, then longest first, as the table requires; scopes with the same range keep source order. Two cases where native ilasm writes rows the specification does not allow are left out: a scope without instructions (zero length), and a second variable with the same name or index in one scope. Within a scope, each name gets a row at the slot of its first declaration, the declaration the name refers to, unless an earlier row of the scope already describes that slot; later declarations of a name, and unnamed locals, take no part. So a row never describes a local that its name does not refer to. Import scopes and local constants are not recorded, as in native ilasm. Tests: LocalScopeTests and LocalTests, and seeded generated cases that compare the local signature, named references and LocalScope/LocalVariable rows with a model of native ilasm's rules.
1 parent cf96d43 commit 9a7d2bd

8 files changed

Lines changed: 1068 additions & 35 deletions

File tree

‎src/tools/ilasm/MANAGED-ILASM-FIXES.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@
3939
| Security pseudoattributes on unsupported targets | Leaves `DynamicSecurityMethodAttribute` and `SuppressUnmanagedCodeSecurityAttribute` unchanged on targets where no transform is applied. | Reports an invalid target consistently with other recognized pseudoattributes. |
4040
| FieldOffset pseudoattribute ordering with local references | Defers attributes with local member-reference owners or constructors, allowing them to override explicit field offsets or attributes appearing later in the source. | Applies attributes in source order regardless of reference form; explicit field offsets always take precedence. |
4141
| `BackwardBranchOptimization_UsesShortInstructionLimit` | With `/OPTIMIZE`, leaves backward branches long when their short-form displacement is -126, -127, or -128. | Uses the actual short-form displacement and emits a short branch whenever it fits in a signed byte. |
42-
| `ExplicitSlot_OutsideTheSixteenBitRange_IsReportedAndTakesTheNextSlot`, `NextSlot_AfterSlot65535_IsReportedAndTheLocalIsNotDeclared` | Accepts any explicit local slot: `[65536]` and above become slots that no 16-bit local instruction operand can name, and a negative slot other than `[-1]` pads the slot table toward 2^32 entries and does not finish. A local without `[n]` after one at slot 65535 takes slot 65536, which no local instruction can name either, and the PDB records its index as 0. | Reports an explicit slot outside 0 to 65535 (other than `[-1]`, which means no explicit slot) and gives the local the next free slot. Reports a local whose next free slot is past 65535 and does not declare it. |
42+
| `ExplicitSlot_OutsideTheSixteenBitRange_IsReportedAndTakesTheNextSlot`, `NextSlot_AfterSlot65535_IsReportedAndTheLocalIsNotDeclared` | Accepts any explicit local slot: `[65536]` and above become slots that no 16-bit local instruction operand can name, and a negative slot other than `[-1]` pads the slot table toward 2^32 entries and does not finish. A local without `[n]` after one at slot 65535 takes slot 65536, which no local instruction can name either, and the PDB records its index as 0. Reads `[4294967295]` as `[-1]`, and a hexadecimal literal of more than 16 digits by its low bits, so `[0x10000000000000000]` is slot 0; neither is reported. | Reports an explicit slot outside 0 to 65535 (other than `[-1]`, which means no explicit slot) and gives the local the next free slot. The range is checked on the literal as written, so a literal too large for 32 bits is reported as well. Reports a local whose next free slot is past 65535 and does not declare it. |
43+
| `BlockWithoutInstructions_HasNoScopeRow`, `MethodWithoutInstructions_HasNoScopeRow` | Writes a LocalScope row of length 0 for a `{ }` block, or a method body, that has no instructions and declares a named local; the Portable PDB specification requires a positive length. | Writes no LocalScope row for a scope without instructions. |
44+
| `NameDeclaredTwiceInOneScope_HasOneVariableRowForTheFirstDeclaration`, `SlotDeclaredTwiceInOneScope_HasOneVariableRowForTheFirstDeclaration`, `NameAndSlotEachDeclaredTwice_ANameWhoseSlotAlreadyHasARowHasNone`, `SlotOfALaterDeclarationOfAName_DoesNotKeepAnotherNameOut` | Writes a LocalVariable row for every named local of a scope, so a scope that declares a name or a slot twice has two rows with that name or index, which the Portable PDB specification does not allow. | Writes a row for each name of a scope at the slot its first declaration has, unless an earlier row of the scope already describes that slot. A name refers to its first declaration in the scope, so a row never describes a local that the name does not refer to. Later declarations of a name, and unnamed locals, take no part: they claim no slot and keep no other name out. |

‎src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.BuildImage.cs‎

Lines changed: 114 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,13 @@ private ImmutableArray<ValidatedExport> ValidateExports(
557557
}
558558
}
559559

560+
/// <summary>
561+
/// Gets whether a PDB is requested: <see cref="Options.Debug"/>, <see cref="Options.DebugMode"/> or
562+
/// <see cref="Options.Pdb"/> is set. Information that only the PDB uses, such as the lexical scopes of
563+
/// method bodies, is kept only when this is <see langword="true"/>.
564+
/// </summary>
565+
private bool GeneratesPdb => _options.Debug || _options.DebugMode is not null || _options.Pdb;
566+
560567
/// <summary>
561568
/// Builds the Portable PDB and the debug directory that references it, when a PDB is requested.
562569
/// </summary>
@@ -592,8 +599,7 @@ private ImmutableArray<ValidatedExport> ValidateExports(
592599

593600
// As in native ilasm, only /DEBUG (any mode) or /PDB produces a PDB. Without them, sequence
594601
// points from .line directives are parsed and validated but not emitted.
595-
bool generatePdb = _options.Debug || _options.DebugMode is not null || _options.Pdb;
596-
if (!generatePdb)
602+
if (!GeneratesPdb)
597603
{
598604
return null;
599605
}
@@ -667,7 +673,8 @@ private string GetPdbFilePath()
667673
}
668674

669675
/// <summary>
670-
/// Adds the Document rows and one MethodDebugInformation row per MethodDef row to the PDB metadata.
676+
/// Adds the Document rows, one MethodDebugInformation row per MethodDef row, and each method's LocalScope and
677+
/// LocalVariable rows (<see cref="AddLocalScopes"/>) to the PDB metadata.
671678
/// </summary>
672679
/// <remarks>
673680
/// The documents are added in <see cref="PdbDocumentTable"/> order. A method without sequence points, or
@@ -702,29 +709,115 @@ private void BuildPdbMetadata()
702709
if (sequencePoints.Count == 0 || !method.HasBody)
703710
{
704711
_pdbBuilder.AddMethodDebugInformation(default, default);
705-
continue;
712+
}
713+
else
714+
{
715+
int firstDocument = sequencePoints[0].DocumentIndex;
716+
bool singleDocument = true;
717+
for (int i = 1; i < sequencePoints.Count && singleDocument; i++)
718+
{
719+
singleDocument = sequencePoints[i].DocumentIndex == firstDocument;
720+
}
721+
722+
BlobBuilder sequencePointsBlob = EncodeSequencePoints(
723+
sequencePoints,
724+
method.DebugInfo.LocalSignature,
725+
documentHandles,
726+
singleDocument);
727+
// docs/design/specs/PortablePdb-Metadata.md, MethodDebugInformation table: Document is "the row id of the
728+
// single document containing all sequence points of the method, or 0 if the method doesn't have sequence
729+
// points or spans multiple documents", and "_InitialDocument_ is only present if the _Document_ field of
730+
// the _MethodDebugInformation_ table is nil"; the blob then names the documents (EncodeSequencePoints).
731+
_pdbBuilder.AddMethodDebugInformation(
732+
singleDocument ? documentHandles[firstDocument] : default,
733+
_pdbBuilder.GetOrAddBlob(sequencePointsBlob));
706734
}
707735

708-
int firstDocument = sequencePoints[0].DocumentIndex;
709-
bool singleDocument = true;
710-
for (int i = 1; i < sequencePoints.Count && singleDocument; i++)
736+
AddLocalScopes((MethodDefinitionHandle)method.Handle, method.DebugInfo.LocalScopes);
737+
}
738+
}
739+
740+
/// <summary>
741+
/// Adds a method's LocalScope rows, each followed by its LocalVariable rows, to the PDB metadata
742+
/// (docs/design/specs/PortablePdb-Metadata.md, "LocalScope Table" and "LocalVariable Table").
743+
/// </summary>
744+
/// <param name="method">The method's MethodDef row. Methods must be added in MethodDef order.</param>
745+
/// <param name="scopes">The method's lexical scopes (<see cref="EntityRegistry.MethodDebugInfo.LocalScopes"/>).</param>
746+
/// <remarks>
747+
/// <para>
748+
/// As in native ilasm, a scope gets a row only when it declares a named local, and an unnamed local gets no
749+
/// row; so the root scope, which spans the whole body, has a row only when the method-level <c>.locals</c>
750+
/// name a local. Each variable's Index is its slot in the local signature, and its attributes are 0.
751+
/// Import scopes and local constants are not recorded.
752+
/// </para>
753+
/// <para>
754+
/// Two rules keep the rows valid where native ilasm writes rows the specification does not allow: a scope
755+
/// without IL (a block with no instructions, or any scope of a method without a body) gets no row, because
756+
/// a scope's length must be positive; and because a scope may not have two variables with the same name
757+
/// or index, within a scope each name gets a LocalVariable row at the slot its first declaration has,
758+
/// unless an earlier row of the scope already describes that slot. A name refers to its first declaration
759+
/// in the scope, so a row never describes a local that its name does not refer to. Later declarations of
760+
/// a name take no part, and neither do unnamed locals, which have no name to record: neither keeps a name
761+
/// out of the rows.
762+
/// </para>
763+
/// <para>
764+
/// The rows are sorted by start offset, then by length with the longest first, as the table requires;
765+
/// scopes with the same range keep their source order, enclosing scope first. Scopes of a method nest or
766+
/// are disjoint because blocks do, and the variables of a scope are added together right after it, so its
767+
/// VariableList run holds exactly its variables.
768+
/// </para>
769+
/// </remarks>
770+
private void AddLocalScopes(MethodDefinitionHandle method, List<EntityRegistry.LocalScopeRecord> scopes)
771+
{
772+
// Most methods name no local; they get no rows and need no sorting.
773+
if (!scopes.Exists(static scope => !scope.Variables.IsEmpty))
774+
{
775+
return;
776+
}
777+
778+
IEnumerable<EntityRegistry.LocalScopeRecord> ordered = scopes
779+
.Where(scope => scope.Length > 0)
780+
.OrderBy(scope => scope.StartOffset)
781+
.ThenByDescending(scope => scope.Length)
782+
.ThenBy(scope => scope.Order);
783+
var names = new HashSet<string>(StringComparer.Ordinal);
784+
var slots = new HashSet<int>();
785+
var variables = new List<EntityRegistry.LocalVariableRecord>();
786+
foreach (EntityRegistry.LocalScopeRecord scope in ordered)
787+
{
788+
names.Clear();
789+
slots.Clear();
790+
variables.Clear();
791+
foreach (EntityRegistry.LocalVariableRecord variable in scope.Variables)
711792
{
712-
singleDocument = sequencePoints[i].DocumentIndex == firstDocument;
793+
// Only the first declaration of a name takes part: it is the local the name refers to. A later
794+
// declaration of the name claims nothing, so it cannot keep another name's row out. The slot
795+
// is claimed only by a row that is written.
796+
if (names.Add(variable.Name) && slots.Add(variable.Slot))
797+
{
798+
variables.Add(variable);
799+
}
713800
}
714801

715-
BlobBuilder sequencePointsBlob = EncodeSequencePoints(
716-
sequencePoints,
717-
method.DebugInfo.LocalSignature,
718-
documentHandles,
719-
singleDocument);
720-
721-
// docs/design/specs/PortablePdb-Metadata.md, MethodDebugInformation table: Document is "the row id of the
722-
// single document containing all sequence points of the method, or 0 if the method doesn't have sequence
723-
// points or spans multiple documents", and "_InitialDocument_ is only present if the _Document_ field of
724-
// the _MethodDebugInformation_ table is nil"; the blob then names the documents (EncodeSequencePoints).
725-
_pdbBuilder.AddMethodDebugInformation(
726-
singleDocument ? documentHandles[firstDocument] : default,
727-
_pdbBuilder.GetOrAddBlob(sequencePointsBlob));
802+
if (variables.Count == 0)
803+
{
804+
continue;
805+
}
806+
807+
_pdbBuilder.AddLocalScope(
808+
method,
809+
importScope: default,
810+
variableList: MetadataTokens.LocalVariableHandle(_pdbBuilder.GetRowCount(TableIndex.LocalVariable) + 1),
811+
constantList: default,
812+
scope.StartOffset,
813+
scope.Length);
814+
foreach (EntityRegistry.LocalVariableRecord variable in variables)
815+
{
816+
_pdbBuilder.AddLocalVariable(
817+
LocalVariableAttributes.None,
818+
variable.Slot,
819+
_pdbBuilder.GetOrAddString(variable.Name));
820+
}
728821
}
729822
}
730823

‎src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.Conversions.cs‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ public CurrentMethodContext(EntityRegistry.MethodDefinitionEntity definition)
200200
}
201201
}
202202

203-
OpenScopes.Add(new LexicalScope(startOffset: 0));
203+
OpenScopes.Add(new LexicalScope(startOffset: 0, order: 0));
204204
}
205205

206206
public EntityRegistry.MethodDefinitionEntity Definition { get; }
@@ -222,6 +222,11 @@ public CurrentMethodContext(EntityRegistry.MethodDefinitionEntity definition)
222222
/// Gets the method's local slots, indexed by slot. The body's local signature has one entry per slot.
223223
/// </summary>
224224
public List<LocalSlot> LocalSlots { get; } = new();
225+
226+
/// <summary>
227+
/// Gets or sets the source-order position of the next block to open; the root scope is 0.
228+
/// </summary>
229+
public int NextScopeOrder { get; set; } = 1;
225230
}
226231

227232
private CurrentMethodContext? _currentMethod;

‎src/tools/ilasm/src/ILAssembler/Actions/GrammarActions.MethodBodies.Locals.cs‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,10 @@ private void DeclareLocals(
210210
if (local.Name is not null)
211211
{
212212
scope.Names.TryAdd(local.Name, index);
213+
if (GeneratesPdb)
214+
{
215+
scope.Variables.Add(new EntityRegistry.LocalVariableRecord(local.Name, index));
216+
}
213217
}
214218
}
215219
}
@@ -218,15 +222,18 @@ private void DeclareLocals(
218222
/// Opens a lexical scope for a <c>{ }</c> block of the current method body, starting at the current offset.
219223
/// </summary>
220224
private void OpenLexicalScope(CurrentMethodContext method)
221-
=> method.OpenScopes.Add(new LexicalScope(CurrentMethodBodyOffset));
225+
=> method.OpenScopes.Add(new LexicalScope(CurrentMethodBodyOffset, method.NextScopeOrder++));
222226

223227
/// <summary>
224228
/// Closes the innermost open lexical scopes of the method, at the current offset, until <paramref name="count"/>
225229
/// remain open. The slots that a closed scope declared are no longer in use, even when an enclosing scope that
226-
/// is still open declared the same slot (<see cref="LocalSlot.InScope"/>).
230+
/// is still open declared the same slot (<see cref="LocalSlot.InScope"/>). When a PDB is requested
231+
/// (<see cref="GeneratesPdb"/>), the scope is recorded in the method's
232+
/// <see cref="EntityRegistry.MethodDebugInfo.LocalScopes"/> for the PDB.
227233
/// </summary>
228234
private void CloseLexicalScopes(CurrentMethodContext method, int count)
229235
{
236+
bool recordScopes = GeneratesPdb;
230237
while (method.OpenScopes.Count > count)
231238
{
232239
LexicalScope scope = method.OpenScopes[^1];
@@ -235,6 +242,15 @@ private void CloseLexicalScopes(CurrentMethodContext method, int count)
235242
{
236243
method.LocalSlots[slot].InScope = false;
237244
}
245+
246+
if (recordScopes)
247+
{
248+
method.Definition.DebugInfo.LocalScopes.Add(new EntityRegistry.LocalScopeRecord(
249+
scope.StartOffset,
250+
CurrentMethodBodyOffset,
251+
scope.Order,
252+
scope.Variables.ToImmutableArray()));
253+
}
238254
}
239255
}
240256

@@ -308,12 +324,28 @@ private sealed class LocalSlot
308324
/// </summary>
309325
private sealed class LexicalScope
310326
{
327+
/// <summary>Creates an open scope that has declared no locals yet.</summary>
311328
/// <param name="startOffset">The IL offset at which the scope starts.</param>
312-
public LexicalScope(int startOffset) => StartOffset = startOffset;
329+
/// <param name="order">The position of the scope in source order (<see cref="EntityRegistry.LocalScopeRecord.Order"/>).</param>
330+
public LexicalScope(int startOffset, int order)
331+
{
332+
StartOffset = startOffset;
333+
Order = order;
334+
}
313335

314336
/// <summary>Gets the IL offset at which the scope starts: 0 for the root scope, the offset at <c>{</c> for a block.</summary>
315337
public int StartOffset { get; }
316338

339+
/// <summary>Gets the position of the scope in source order: 0 for the root scope, then each block as it opens.</summary>
340+
public int Order { get; }
341+
342+
/// <summary>
343+
/// Gets the named locals declared in this scope, in declaration order, including a second declaration of a
344+
/// name or a slot, from which the scope's LocalVariable rows are chosen. Filled only when a PDB is requested
345+
/// (<see cref="GeneratesPdb"/>).
346+
/// </summary>
347+
public List<EntityRegistry.LocalVariableRecord> Variables { get; } = new();
348+
317349
/// <summary>
318350
/// Gets the slot of each name declared in this scope. Names resolve from the innermost open scope outward.
319351
/// </summary>

0 commit comments

Comments
 (0)