Skip to content

[wasm] Preserve R2R variable debug information - #133086

Open
lewing wants to merge 16 commits into
dotnet:mainfrom
lewing:lewing-r2r-wasm-debug-info
Open

lewing wants to merge 16 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
Comment thread src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/NativeHashtable.cs Outdated
@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
Comment thread docs/design/datacontracts/DebugInfo.md Outdated
lewing added 2 commits October 1, 2026 15:10
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

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

lewing commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

@AndyAyersMS yes, locals that live on the linear stack are covered now. They're encoded as VLT_STK records against base register 2. On wasm REG_FPBASE, REG_SPBASE, and REGNUM_AMBIENT_SP all have that value, so the record says "frame-relative slot in the logical frame". It doesn't name any particular wasm local. You're right that the locals holding SP/FP shift with parameters, compiler temps, localloc, and between a root method and its funclets, so we don't encode them at all. A consumer works out the frame address from the stack walk/unwind over shadow-stack memory and adds the offset. DebugInfo.md in this PR documents that contract.

The current tests cover:

  • a no-opt object local kept live across a GC reached from a finally funclet (GcLocalAcrossFinally). The test checks its IL type, the full variable records, the frame-relative GC slot, and that the range covers the safepoint
  • frame-base variations: a leaf method with frame locals, localloc (root gets a distinct FP), localloc across a finally, and same-typed GC slots that keep distinct identities, including a null
  • byte-level checks against the emitted wasm that the recorded offsets match the actual frame loads (e.g. local.get; i32.load offset=0x14)

One limitation, the same as on other targets: scope info is still not reported inside funclets (siInFuncletRegion). Variable records describe the root method's frame, and funclet code reaches those slots through the parent establishing frame.

Note

This reply was drafted with GitHub Copilot.

lewing added 3 commits October 1, 2026 15:29
Encode wasm locals in debug info as the local index biased past the reserved RegNum values instead of packing the JIT value type, matching other targets where the type is implied by the variable. Remove the now-unneeded cDAC descriptor globals.

Wasm is only compiled ahead of time, where every variable scope covers the whole method, so open all scopes in the first emitted block instead of scanning per block.

Update codegen-shape test pins for drift from main.

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

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Replace exact native offsets, frame sizes, local counts, and instruction byte pins with checks of the debug-info contract: register records name a declared wasm local of the expected type, stack records are frame-relative and their slots are accessed, parameters are homed after their local range, ranges do not overlap, and GC locals cover the safepoint. Unrelated JIT codegen changes no longer break the test.

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

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Add an optimized Vector128 method and verify its parameters and local are recorded as wasm v128 locals.

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

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Comment thread src/coreclr/jit/ee_il_dll.cpp Outdated
Comment thread src/coreclr/jit/scopeinfo.cpp Outdated
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
rcj1
rcj1 previously approved these changes Oct 9, 2026
@rcj1
rcj1 self-requested a review October 9, 2026 13:50
@rcj1
rcj1 dismissed their stale review October 9, 2026 13:52

Approved wrong pr by accident

lewing added a commit that referenced this pull request Oct 13, 2026
…optional in IsVersionable (#135430)

Two `IDebugInfo.GetMethodVarInfo` fixes for targets that use portable
entry points, such as CoreCLR WebAssembly.

### `GetMethodVarInfo` code offset

`codeOffset` was computed relative to `ICodeVersions.GetNativeCode`,
which is the method's **entry point**. On portable-entrypoint targets
that is not the code start, so the offset was wrong. The entry point is
now mapped with `IExecutionManager.GetDiagnosticCodeStartFromEntryPoint`
(#134827), the cDAC mirror of the runtime's
`GetDiagnosticCodeStartFromEntryPoint`. It returns:

- the interpreter bytecode for an interpreted method;
- the R2R virtual IP for a portable entry point;
- otherwise the address unchanged.

Native `ClrDataAccess::GetMethodVarInfo` maps only interpreter entry
points (`GetInterpreterCodeFromEntryPointIfPresent`). The native DAC
does not support wasm, so the R2R portable-entrypoint case has no native
DAC behavior to compare against.

### `IsVersionable` without the ReJIT contract

`GetMethodVarInfo` → `GetNativeCodeVersionForIP` → `IsVersionable`
required the `ReJIT` contract. The runtime advertises that contract only
under `PROFILING_SUPPORTED`, which is off on wasm, so `GetMethodVarInfo`
threw `ContractMissingException` there. Wasm also lacks `FEATURE_REJIT`,
so native `IsEligibleForReJIT` is false. A missing contract is now
treated as not ReJIT-eligible, using `TryGetContract`.

An alternative is to advertise `ReJIT` on wasm, with `IsEnabled()`
returning false; I'm happy to switch if reviewers prefer that.
Separately, `CoreCLRContracts.ValidateForDataAccess` still requires
`IReJIT`, so on wasm the official DAC entry point would fail validation.
That is not changed here and needs its own follow-up.

### Validation

- `./build.sh -s tools.cdactests -test`: passed (Unit tests 3293/3293,
DataGenerator 46/46, Usage 4/4).
- `generate-docs.ps1 -Check`: up to date.
- New tests:
- `GetMethodVarInfo_CodeOffsetIsRelativeToDiagnosticCodeStart` (theory:
identity mapping, and an entry point mapped to a different code start).
- `IsVersionable_ReJITContractOptional` (theory: no ReJIT contract,
ReJIT disabled, ReJIT enabled).
- `GetDebugInfo_R2R_NoDebugInfoSection_ReturnsNull` now registers
`FeatureFlags` and `PrecodeStubs`, which the mapping requires.
- Mutation checks, each confirmed applied by diff and restored
afterwards:
- Revert to `cv.GetNativeCode(ncvh)` →
`GetMethodVarInfo_CodeOffset…(0x400000→0x80010100)` fails with
`Assert.Equal`.
- Revert to `_target.Contracts.ReJIT.IsEnabled()` →
`IsVersionable_ReJITContractOptional(advertiseReJIT: False)` fails with
`ContractMissingException`.
- I also checked against a live CoreCLR browser-wasm target with R2R.
Entry point `0x41ADC1C` maps to diagnostic start `0x800104B3`, and
`GetMethodVarInfo` returns offset `0x2`. That is the same result as a
wasm-only offset computation used in an earlier prototype. That target
used a producer-side branch with variable debug info (#133086); there is
no automated test for this case.

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

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

This branch has not been deployed

No deployments
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.

8 participants