Skip to content

[wasm] Preserve R2R variable debug information - #133086

Merged
lewing merged 18 commits into
dotnet:mainfrom
lewing:lewing-r2r-wasm-debug-info
Nov 2, 2026
Merged

lewing merged 18 commits into
dotnet:mainfrom
lewing:lewing-r2r-wasm-debug-info

Conversation

@lewing

@lewing lewing commented Sep 2, 2026 •

Copy link
Copy Markdown
Member

Re-enables RyuJIT variable debug info for ReadyToRun WebAssembly code, which was disabled before wasm locals could be encoded, and defines the encoding that readers consume.

  • Register fields hold localIndex + WASM_LOCAL_REGNUM_BASE (3); values 0–2 keep their meanings (REGNUM_PC, REGNUM_COUNT, REGNUM_AMBIENT_SP).
  • Stack records use base 2 (REG_FPBASE = REG_SPBASE = REGNUM_AMBIENT_SP on wasm), so they are slots relative to the logical frame, which readers reconstruct from the stack walk.
  • Scopes open in the first emitted block: wasm is AOT-only, so getVars always reports method-wide scopes, and wasm block order doesn't follow IL order.
  • The hidden portable-entrypoint argument is excluded from source variables, and R2RDump decodes wasm registers.

Both constants are fixed for DebugInfo contract c1 and documented in DebugInfo.md. Changing either needs a new contract version and, because R2R images persist the encoding, a ReadyToRun format version change. Browser and WASI publishes strip debug info by default (#134690), so these records reach an image only with PublishReadyToRunStripDebugInfo=false; generated code is unchanged. The cDAC consumer is #133890.

Test coverage and follow-ups

WasmWebcilModule checks invariants rather than exact codegen: register records name declared locals with the expected wasm types, stack records use base 2 without overlapping ranges, recorded frame slots are actually accessed (including from a finally funclet), and GC locals' ranges cover the safepoint. Removing the +3 bias makes it fail.

Follow-ups: NativeAOT DWARF (#135740) can describe locals with DW_OP_WASM_location (local index = register − 3); frame-slot variables additionally need the engine local holding the frame pointer, which this format doesn't record. #134808 plans R2R DWARF on top of these records.

Note

This pull request description was updated with the assistance of GitHub Copilot.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
11 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@lewing lewing changed the title [wasm] Preserve R2R variable debug information [wip][wasm] Preserve R2R variable debug information Sep 2, 2026
@lewing lewing added the arch-wasm WebAssembly architecture label Sep 2, 2026
@lewing
lewing requested a review from davidwrighton September 2, 2026 16:26
@lewing
lewing requested a review from AndyAyersMS September 3, 2026 17:26

@AndyAyersMS AndyAyersMS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this handle describing locals that are on the linear stack? Last I looked those SP/FP based encodings were wonky for Wasm.

Note the exact Wasm locals that hold SP/FP can vary and may even vary between main method and its funclets, which I doubt we can encode.

An easy test for this is to look at any GC-typed local, these are always on the linear stack.

@davidwrighton

Copy link
Copy Markdown
Member

@lewing, what value does this debugging data provide? Given my understanding that debugging is likely to only be supported on mostly interpreted scenarios, why should we make this correct for R2R scenarios. (I expect that nativeaot will make good use of this data, so I think overall its goodness, but I don't see why its particularly useful for R2RDump.)

@lewing

lewing commented Sep 5, 2026

Copy link
Copy Markdown
Member Author

It came out of the DWARF discussion and I'm working on something in the nesm debugger and I'm trying to plumb out more info, it is isn't meant to be user facing currently.

lewing and others added 2 commits September 14, 2026 09:18
Preserve variable debug information produced by RyuJIT for ReadyToRun WebAssembly code, including scope ranges across relooper block ordering and packed wasm local register locations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The end-to-end variable-debug-info validation exposed the hidden wasm
portable-entry-pointer argument as a source local. The argument is appended
after user arguments, but unlike the wasm stack-pointer argument it was not
recorded or excluded by compMap2ILvarNum. AddDoubles therefore reported the
hidden i32 argument as source local 0, alongside the real f64 parameters.

Record the argument's local number when it is created, map it to
UNKNOWN_ILNUM, and account for it when mapping later internal locals back to
IL variable numbers.

Replace the count-only wasm R2R checks with complete exact records for the
AddDoubles parameters and SumWithFinally local: variable identity, native
range, location kind, packed wasm local, and frame-pointer-relative offset.
Mutate the local-index bits of one packed register and prove the exact oracle
rejects the corrupted record.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing

lewing commented Sep 14, 2026

Copy link
Copy Markdown
Member Author

I opened #133890 (which includes these commits) with cDAC consumer side changes

Define the packed WASM debug-register bit layout in ICorDebugInfo and have
the JIT derive its register masks from that shared encoding contract. Assert
that the JIT register representation and WasmValueType count remain
compatible with the debug-info format.

Document that the static ReadyToRun reader's compiled-in shift must move
with a versioned R2R debug-info format change, since it has no live target
descriptor from which to discover a different layout.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing and others added 3 commits September 14, 2026 13:45
Publish the shared WASM register type shift and value-type count through the target data descriptor so version-skewed readers can reject incompatible variable debug information.

Document the producer-owned encoding and extend the static ReadyToRun reader coverage for reserved and unsupported value-type codes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a no-opt object local that remains live across a GC call in a finally funclet. Assert its IL class type, complete ReadyToRun variable tuples, frame-relative GC slot, and safepoint coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add exact optimized tracked-variable coverage, frame-base ABI variations including localloc and funclets, and same-type GC slot identity with a legitimate null reference.

Pin the current stack VarLoc base encoding and document that absolute frame reconstruction is independent of unstable wasm local indices.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing lewing changed the title [wip][wasm] Preserve R2R variable debug information [wasm] Preserve R2R variable debug information Sep 14, 2026
@lewing
lewing marked this pull request as ready for review September 14, 2026 23:41
Copilot AI lite review requested due to automatic review settings September 14, 2026 23:41
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread docs/design/datacontracts/DebugInfo.md Resolved
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
@lewing
lewing requested a review from adamperlin October 23, 2026 00:32
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human verification is needed for the cross-component ReadyToRun DebugInfo contract change, including persisted Wasm encodings and end-to-end cDAC/R2RDump compatibility, because the required browser/WASI, NativeAOT, and cDAC validation was not run.

No open issues 🎉

Review effort: Lite

@lewing
lewing requested review from jakobbotsch and jkotas November 1, 2026 03:34

@jakobbotsch jakobbotsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems reasonable.

@lewing

lewing commented Nov 2, 2026

Copy link
Copy Markdown
Member Author

/azp run runtime

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@lewing
lewing enabled auto-merge (squash) November 2, 2026 20:09
@lewing
lewing merged commit f4d81ef into dotnet:main Nov 2, 2026
130 of 132 checks passed
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 12.0-preview1 milestone Nov 4, 2026
lewing added a commit that referenced this pull request Nov 5, 2026
cDAC support for resolving wasm R2R variable locations from the debug
info in #133086 (merged). The change is confined to `ClrDataFrame`'s
variable resolution and `DebugInfo.md`; the `DebugInfo` contract reports
wasm locations with the existing kinds and register numbers.

- Resolve `VLT_STK` / `VLT_STK2` against the frame pointer in the
stack-walk context. For ReadyToRun code (base register `2`) that is the
logical frame pointer, which for a funclet is the establishing method's
frame (#135140). For interpreted code (base `REGNUM_FP`, which is `0` on
wasm) it is `InterpMethodContextFrame.pStack`.
- Report register-based kinds as having no location. A wasm "register"
is a local index biased by `WASM_LOCAL_REGNUM_BASE`, and wasm locals are
engine-private, not readable through the data target. A consumer
attached to the engine subtracts the bias and reads the local itself.
- Fail an unreadable or null `VLT_STK_BYREF` indirection with
`VirtualReadException` (`CORDBG_E_READVIRTUAL_FAILURE`) instead of
reading linear address 0, which is valid memory on wasm.
- Debug-assert that a wasm stack record uses one of those two base
registers.
- List `WASM_LOCAL_REGNUM_BASE` (`3`) and the wasm stack base register
(`2`) in DebugInfo.md's constants table.

Local index spaces are per wasm function, and funclets are separate
functions, so a consumer must know which function a frame is in before
using a local index. The existing `IExecutionManager` APIs
(`GetCodeBlockHandle`, `IsFunclet`, `GetUnwindInfo`) identify it.

## Null and unavailable values

1. Engine-private wasm local: zero locations; `GetBytes` / `GetAddress`
fail with `E_NOINTERFACE`.
2. Unreadable or null `VLT_STK_BYREF` indirection: resolution fails with
`CORDBG_E_READVIRTUAL_FAILURE`.
3. Direct stack-homed null reference: one readable location that reads
as zero.

Native `DereferenceByRefVar` turns a failed byref read into address 0,
which is safe natively because address 0 is unmapped. On wasm, linear
address 0 is readable, so the same representation would read unrelated
bytes as the variable's value. Non-wasm behavior is unchanged.

## Tests

On head `efdab45a717` (on `main` `f4d81ef0a30`):

- `./build.sh -s tools.cdactests -test`: **passed** (UnitTests
3929/3929, DataGeneratorTests 46/46, UsageTests 4/4).
- `pwsh src/native/managed/cdac/tools/CdacUsageGraph/generate-docs.ps1
-Check`: **passed**.
- Mutations, each confirmed applied by diff, failing the listed tests,
then restored:

| Mutation | Fails |
| --- | --- |
| Resolve stack slots against SP instead of FP | 5 tests, including the
funclet establishing-frame test |
| Skip the byref dereference | 3 `VLT_STK_BYREF` tests |
| Drop the null-byref check | the null-byref case |
| Let `Register` resolve like a stack slot | 4 register-kind cases |
| Drop the interpreter base `0` | the interpreted-frame test, on the
debug assertion |

Live browser validation on the earlier head `b126a409cee` (browser
`clr+libs` Debug build, sample published with
`-p:PublishReadyToRunStripDebugInfo=false`, headless Chrome over CDP):
**passed**. Paused in R2R `GcSlotIdentity` after a `GC.Collect()`
reached through a call, the cDAC resolved each IL local to a single
`VLT_STK` home at `FP + offset` inside the frame, reported by the GC
info as a frame-relative root, and read the two marker objects back (17
and 29) and the null local as zero. A live mutation adding 4 to the
stack address failed as expected. The stack-slot resolution code is
unchanged since then; the live run has not been repeated on this head.

## Limitations

- No native DAC is built for wasm, so these results are covered by
managed unit tests, not live `_legacyImpl` comparison.
- On wasm, `GetMethodVarInfo`'s `codeOffset` is in the synthetic
virtual-IP space, while variable ranges are JIT emitter offsets, so
`ClrDataFrame`'s range selection is only reliable for method-wide
ranges. Unoptimized wasm scopes are method-wide, but a variable whose
location changes mid-method can select the wrong record. A fix needs
virtual-IP-space ranges from the producer or a virtual-IP →
native-offset map.
- Default Browser/WASI publishing strips R2R debug info (#134690). Use
`-p:PublishReadyToRunStripDebugInfo=false` to keep it.

> [!NOTE]
> This pull request description was generated with GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Max Charlamb <44248479+max-charlamb@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI area-ReadyToRun

Projects

None yet

Development

Successfully merging this pull request may close these issues.

9 participants