Summary
StateMachineHoistedLocalScopes is indexed by slot number: a debugger parses N out of a state machine field name (<name>5__N, <>8__N) and reads row N-1 of the blob. PortablePdbWriter.BuildStateMachineHoistedLocalScopes (line 330) instead emits one row per surviving ILVariable that has a StateMachineField, ordered by field token, and gives every row the extent (0, CodeSize). That sequence has no relation to the slot numbering, so rows are misaligned, and where it is shorter than the highest slot the hoisted locals above the row count are dropped by the expression evaluator.
Reproduction
public static async Task<int> MixedAsync(IEnumerable<int> source)
{
List<int> chunks = new List<int>();
foreach (int value in source)
chunks.Add(value);
await Task.Yield();
int total = 0;
try
{
foreach (int chunk in chunks)
{
total += chunk;
await Task.Yield();
}
}
catch (InvalidOperationException e)
{
Console.WriteLine(e.Message);
}
return total;
}
State machine fields of <MixedAsync>d__0 (slots 1-7):
<>1__state, <>t__builder, source, <chunks>5__1, <total>5__2, <>s__3,
<value>5__4, <>s__5, <chunk>5__6, <e>5__7, <>u__1
Blob rows for <MixedAsync>d__0.MoveNext, start+length:
compiler: 0+24e, 0+24e, 0+0, 3c+25, 0+0, 124+8c, 1e8+1f
ILSpy: 0+24e, 0+24e, 0+24e, 0+24e, 0+24e, 0+24e, 0+24e
The compiler emits exactly one row per slot, in slot order, with an empty 0+0 placeholder for the synthesized <>s__N slots and the real live range for each user local. ILSpy's rows happen to number seven here, but they enumerate a different set (the hoisted parameter source is included, the <>s__N slots are not), so row N-1 does not describe slot N.
The counts diverge on real code: checking the PDB ILSpy generates against the highest user-visible slot in the binary reports a shortfall in 227 state machines of Newtonsoft.Json 13.0.4 and 2 of Microsoft.Extensions.Http 10.0.11. A shortfall means the hoisted locals whose slot exceeds the row count disappear from the Locals window entirely.
Note
#1230 lists StateMachineHoistedLocalScopes as unimplemented. It is emitted (PortablePdbWriter.cs lines 186-192), just not in the shape a consumer reads.
Posted by an AI agent (Claude) on Siegfried's behalf.
Summary
StateMachineHoistedLocalScopesis indexed by slot number: a debugger parsesNout of a state machine field name (<name>5__N,<>8__N) and reads rowN-1of the blob.PortablePdbWriter.BuildStateMachineHoistedLocalScopes(line 330) instead emits one row per survivingILVariablethat has aStateMachineField, ordered by field token, and gives every row the extent(0, CodeSize). That sequence has no relation to the slot numbering, so rows are misaligned, and where it is shorter than the highest slot the hoisted locals above the row count are dropped by the expression evaluator.Reproduction
State machine fields of
<MixedAsync>d__0(slots 1-7):Blob rows for
<MixedAsync>d__0.MoveNext,start+length:The compiler emits exactly one row per slot, in slot order, with an empty
0+0placeholder for the synthesized<>s__Nslots and the real live range for each user local. ILSpy's rows happen to number seven here, but they enumerate a different set (the hoisted parametersourceis included, the<>s__Nslots are not), so rowN-1does not describe slotN.The counts diverge on real code: checking the PDB ILSpy generates against the highest user-visible slot in the binary reports a shortfall in 227 state machines of Newtonsoft.Json 13.0.4 and 2 of Microsoft.Extensions.Http 10.0.11. A shortfall means the hoisted locals whose slot exceeds the row count disappear from the Locals window entirely.
Note
#1230 lists
StateMachineHoistedLocalScopesas unimplemented. It is emitted (PortablePdbWriter.cslines 186-192), just not in the shape a consumer reads.Posted by an AI agent (Claude) on Siegfried's behalf.