Skip to content

Don't assert decompiled local types match the signature - #3836

Merged
siegfriedpammer merged 1 commit into
masterfrom
fix-localsig-assert
Jun 27, 2026
Merged

Don't assert decompiled local types match the signature#3836
siegfriedpammer merged 1 commit into
masterfrom
fix-localsig-assert

Conversation

@siegfriedpammer

Copy link
Copy Markdown
Member

Problem

DebugInfoGenerator.HandleMethodBody asserted, for every local variable, that its decompiled type is equivalent to the metadata local-signature type at the same slot:

Debug.Assert(v.Index < types.Length && NormalizeTypeVisitor.TypeErasure.EquivalentTypes(v.Type, types[v.Index.Value]));

This aborts portable-PDB generation (Debug builds) for ordinary inputs — e.g. any method containing a fixed statement, and async state machines — surfaced while generating PDBs for real assemblies such as System.Net.Requests.dll.

Why the type check is wrong

The type equivalence holds when the variables are first read from IL, but not afterwards:

  • Variable splitting gives a single IL slot several variables with different (refined) types; the signature type matches at most one.
  • Pinned-region locals are modeled as pointers (int*), while the signature types the slot as int& pinned.
  • Generic-context type-parameter identity differs (e.g. ReadOnlySpan<``0> vs ReadOnlySpan<0>` in local functions / iterators).

Empirically, ~32% of CoreLib's qualifying locals mismatch across these classes; neither a size nor an IsCompatibleTypeForMemoryAccess comparison covers them.

Why it cannot affect debugging

AddLocalVariable writes only the slot index and name to the PDB — never the type, which the debugger reads from the metadata signature directly. And the slot index is correct by construction: it is the IL ldloc/stloc operand, assigned from the signature slot when the variable is created (ILReader.InitLocalVariables) and copied verbatim by SplitVariables; ILVariable.Index is never reassigned. A wrong index is ruled out structurally.

Fix

Drop the type-equivalence check; keep only the (trivially-true, documented) index bound as a guard against a future regression in index assignment.

Test

Adds TestCases/PdbGen/PinnedLocalSlot.il — a minimal fixed statement whose pinned local is int& pinned in metadata but int* after decompilation — and a PdbGenerationTestRunner.PinnedLocalSlot test that runs DebugInfoGenerator over it and asserts it does not throw. Fails (asserts) before the fix, passes after; full PdbGen fixture stays green.

🤖 Generated with Claude Code

DebugInfoGenerator asserted that every local variable's decompiled type
is equivalent to the metadata local-signature type at its slot. That
holds at IL-read time but not afterwards: variable splitting gives one
slot several typed variables, pinned-region locals are modeled as
pointers (int* vs int& pinned), and generic-context type-parameter
identity differs. The assertion therefore aborted PDB generation (Debug
builds) for ordinary inputs such as any method with a fixed statement.

The type is never written to the PDB - only the slot index and name are -
so the mismatch cannot affect the debugging experience. The slot index,
the only emitted value, is correct by construction: it is the IL
ldloc/stloc operand, sourced from the signature slot when the variable is
created and copied verbatim by SplitVariables; it is never reassigned.
Keep only the index-bound check and document why the type is not verified.

Assisted-by: Claude:claude-opus-4-8:Claude Code
@siegfriedpammer
siegfriedpammer merged commit ca5eec3 into master Jun 27, 2026
13 checks passed
@siegfriedpammer
siegfriedpammer deleted the fix-localsig-assert branch June 27, 2026 16:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant