Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions docs/design/datacontracts/DebugInfo.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,67 @@ Each variable entry in the Vars section is nibble-encoded as follows:

Signed integers are encoded using the same unsigned scheme, with the sign bit stored in bit 0 (`value = unsigned >> 1`, negate if `unsigned & 1`). On x86, stack offsets are DWORD-aligned and stored divided by `sizeof(DWORD)`.

### WebAssembly Variable Register Encoding

WASM has no physical registers. RyuJIT packs a `(local index, debug value type)` tuple into the
32-bit `regNumber` payload, and that packed value appears in every register field of the Vars
stream:

```text
packedRegister = localIndex | ((uint)debugValueType << WasmDebugRegisterTypeShift)
```

The encoding uses the following values:

| Value type | Encoded value |
| --- | --- |
| `Invalid` | `0` |
| `I32` | `1` |
| `I64` | `2` |
| `F32` | `3` |
| `F64` | `4` |
| `V128` | `5` |
| `ExnRef` | `6` |

The target advertises `WasmDebugRegisterTypeShift` and `WasmDebugValueTypeCount` as `uint8`
numeric data descriptor globals. These values define how to separate the local index from the
debug value type. A reader must reject an unsupported or missing encoding rather than fall back
to a compiled-in shift and plausibly decode the wrong local or type.

Debug value type `0` is reserved so that small raw values remain available for pseudo-registers
such as `REGNUM_AMBIENT_SP`. A packed value whose value type is `0` or greater than or equal to
`WasmDebugValueTypeCount` does not name a local.

`WasmDebugValueTypeCount` is JIT debug-encoding vocabulary, not the complete WebAssembly
specification type set. Managed references currently use the JIT's machine `I32`/`I64`
representation; the encoding does not independently identify a managed GC reference. A future
bit-width or value-count change requires a format-aware, versioned reader update.

### WebAssembly Stack Base Encoding

WASM `VLT_STK` and `VLT_STK2` records currently encode base register `2`.
`REG_FPBASE`, `REG_SPBASE`, and `REGNUM_AMBIENT_SP` all have that value on this target, so the
debug record identifies a logical frame-relative stack home; it does not identify a particular
WebAssembly engine local.

The absolute logical frame address is reconstructed by the runtime stack-walk and unwind
protocol from shadow-stack linear memory. The engine's current per-function SP/FP local allocation
is a separate code-generation detail:

* Frame access allocates an FP value when a method has frame locals, uses `localloc`, or has
funclets.
* Without `localloc`, the root function's FP aliases its SP, including methods that make calls.
* `localloc` gives the root a distinct FP so later SP movement does not change frame-relative
addresses.
* Funclets receive a distinct parent establishing FP; with `localloc`, that remains the root's
pre-adjustment frame base.

The numeric WebAssembly local indices holding those values can vary with function parameters and
compiler-created locals and are not part of this debug-info format. Readers must not infer the
logical frame address from a hardcoded `$varN`, and the producer does not advertise SP/FP engine
local indices. A future producer that changes stack records away from base `2` requires a
coordinated, fail-loud reader update.

### Async Suspension Point APIs

We also support decoding async suspension points (and their captured continuation-object locals) from the `AsyncInfo` chunk of the debug info blob. The chunk is present only for methods that the JIT compiled with runtime-async suspension points; for all other methods, `AsyncInfoSize` is `0` in the FAT header and the API returns an empty list.
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/inc/cordebuginfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
class ICorDebugInfo
{
public:
#ifdef TARGET_WASM
// WASM variable locations encode a JIT-local index and a JIT WasmValueType in the 32-bit
// RegNum payload. These constants are part of the debug-info encoding consumed by cDAC and
// ILCompiler.Reflection.ReadyToRun.
static constexpr uint32_t WASM_REG_TYPE_BITS = 3;
static constexpr uint32_t WASM_REG_TYPE_SHIFT = 32 - WASM_REG_TYPE_BITS;
static constexpr uint32_t WASM_VALUE_TYPE_COUNT = 7;
#endif // TARGET_WASM

/*----------------------------- Boundary-info ---------------------------*/

enum MappingTypes
Expand Down
6 changes: 6 additions & 0 deletions src/coreclr/jit/codegen.h
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,12 @@ class CodeGen final : public CodeGenInterface

IL_OFFSET siLastEndOffs; // IL offset of the (exclusive) end of the last block processed

#if defined(TARGET_WASM)
// The relooper can reorder and duplicate blocks, so wasm cannot discover
// scopes with the monotonic enter/exit cursors used by other targets.
bool* siWasmOpenedScopes;
#endif // defined(TARGET_WASM)

/*
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Expand Down
7 changes: 0 additions & 7 deletions src/coreclr/jit/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2898,13 +2898,6 @@ void Compiler::compInitOptions(JitFlags* jitFlags)

opts.compScopeInfo = opts.compDbgInfo;

#ifdef TARGET_WASM
// Wasm uses virtual registers that cannot be encoded in the
// ICorDebugInfo register scheme, and there is no native debugger
// to consume scope info, so disable it entirely.
opts.compScopeInfo = false;
#endif

#ifdef LATE_DISASM
codeGen->getDisAssembler().disOpenForLateDisAsm(info.compMethodName, info.compClassName,
info.compMethodInfo->args.pSig);
Expand Down
3 changes: 2 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -4386,7 +4386,8 @@ class Compiler
#endif // TARGET_X86

#if defined(TARGET_WASM)
unsigned lvaWasmSpArg = BAD_VAR_NUM; // lcl var index of Wasm stack pointer arg
unsigned lvaWasmSpArg = BAD_VAR_NUM; // lcl var index of Wasm stack pointer arg
unsigned lvaWasmPortableEntryPtrArg = BAD_VAR_NUM; // lcl var index of Wasm portable entry point arg
unsigned lvaWasmVirtualIP = BAD_VAR_NUM; // Wasm virtual IP slot
unsigned lvaWasmFunctionIndex = BAD_VAR_NUM; // Wasm function index slot
unsigned lvaWasmResumeIP = BAD_VAR_NUM; // Wasm catch resumption IP slot
Expand Down
16 changes: 11 additions & 5 deletions src/coreclr/jit/lclvars.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -556,10 +556,11 @@ void Compiler::lvaInitWasmPortableEntryPtr(unsigned* curVarNum)
{
if (opts.jitFlags->IsSet(JitFlags::JIT_FLAG_PORTABLE_ENTRY_POINTS))
{
LclVarDsc* varDsc = lvaGetDesc(*curVarNum);
varDsc->lvType = TYP_I_IMPL;
varDsc->lvIsParam = 1;
varDsc->lvOnFrame = true;
LclVarDsc* varDsc = lvaGetDesc(*curVarNum);
varDsc->lvType = TYP_I_IMPL;
varDsc->lvIsParam = 1;
varDsc->lvOnFrame = true;
lvaWasmPortableEntryPtrArg = *curVarNum;
(*curVarNum)++;
}
}
Expand Down Expand Up @@ -1276,7 +1277,7 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const
}

#if defined(TARGET_WASM)
if (varNum == lvaWasmSpArg)
if ((varNum == lvaWasmSpArg) || (varNum == lvaWasmPortableEntryPtrArg))
{
return (unsigned)ICorDebugInfo::UNKNOWN_ILNUM;
}
Expand Down Expand Up @@ -1314,6 +1315,11 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const
{
varNum--;
}

if ((lvaWasmPortableEntryPtrArg != BAD_VAR_NUM) && (originalVarNum > lvaWasmPortableEntryPtrArg))
{
varNum--;
}
#endif

if (varNum >= info.compLocalsCount)
Expand Down
6 changes: 4 additions & 2 deletions src/coreclr/jit/registeropswasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@
#endif

using RegNumUnderlyingType = regNumberSmall;
static const RegNumUnderlyingType WASM_REG_TYPE_BITS = 3;
static const RegNumUnderlyingType WASM_REG_TYPE_SHIFT = 8 * sizeof(RegNumUnderlyingType) - WASM_REG_TYPE_BITS;
static const RegNumUnderlyingType WASM_REG_TYPE_BITS = ICorDebugInfo::WASM_REG_TYPE_BITS;
static const RegNumUnderlyingType WASM_REG_TYPE_SHIFT = ICorDebugInfo::WASM_REG_TYPE_SHIFT;
static const RegNumUnderlyingType WASM_REG_TYPE_MASK = ~0u << WASM_REG_TYPE_SHIFT;
static const unsigned WASM_LOCAL_INDEX_LIMIT = WASM_REG_TYPE_MASK;

static_assert(sizeof(RegNumUnderlyingType) >= sizeof(unsigned));
static_assert(WASM_REG_TYPE_SHIFT == (8 * sizeof(RegNumUnderlyingType) - WASM_REG_TYPE_BITS));
static_assert(static_cast<unsigned>(WasmValueType::Count) == ICorDebugInfo::WASM_VALUE_TYPE_COUNT);
static_assert(((static_cast<RegNumUnderlyingType>(WasmValueType::Count) - 1) >> WASM_REG_TYPE_BITS) == 0);

//------------------------------------------------------------------------
Expand Down
94 changes: 87 additions & 7 deletions src/coreclr/jit/scopeinfo.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
#include "emit.h"
#include "codegen.h"

#if defined(TARGET_WASM)
// Stack VarLoc records use the target register-number convention, not packed wasm local indices.
static_assert(REG_FPBASE == REG_SPBASE);
static_assert(REG_FPBASE == REG_NA);
static_assert(static_cast<int>(REG_NA) == static_cast<int>(ICorDebugInfo::REGNUM_AMBIENT_SP));
static_assert(static_cast<int>(ICorDebugInfo::REGNUM_AMBIENT_SP) == 2);
#endif // defined(TARGET_WASM)

//============================================================================
// siVarLoc functions
//============================================================================
Expand Down Expand Up @@ -208,6 +216,30 @@ void CodeGenInterface::siVarLoc::storeVariableInRegisters(regNumber reg, regNumb
// Note: mask registers (K0-K7) and XMM16+ are accepted but will produce
// VLT_INVALID since they can't be encoded in debug info.

#if defined(TARGET_WASM)
if (reg == REG_NA)
{
vlType = VLT_INVALID;
return;
}

assert(genIsValidReg(reg));

if (otherReg == REG_NA)
{
vlType = VLT_REG;
vlReg.vlrReg = reg;
}
else
{
assert(genIsValidReg(otherReg));
vlType = VLT_REG_REG;
vlRegReg.vlrrReg1 = reg;
vlRegReg.vlrrReg2 = otherReg;
}
return;
#endif // defined(TARGET_WASM)

if (otherReg == REG_NA)
{
if (genIsValidFloatReg(reg))
Expand Down Expand Up @@ -471,6 +503,12 @@ void CodeGenInterface::siVarLoc::siFillStackVarLoc(
void CodeGenInterface::siVarLoc::siFillRegisterVarLoc(
const LclVarDsc* varDsc, var_types type, regNumber baseReg, int offset, bool isFramePointerUsed)
{
#if defined(TARGET_WASM)
this->vlType = VLT_REG;
this->vlReg.vlrReg = varDsc->GetRegNum();
return;
#endif // defined(TARGET_WASM)

switch (type)
{
case TYP_INT:
Expand Down Expand Up @@ -1598,6 +1636,14 @@ void CodeGen::siInit()
siLastEndOffs = 0;

m_compiler->compResetScopeLists();

#if defined(TARGET_WASM)
siWasmOpenedScopes = nullptr;
if (m_compiler->info.compVarScopesCount > 0)
{
siWasmOpenedScopes = new (m_compiler, CMK_DebugInfo) bool[m_compiler->info.compVarScopesCount]();
}
#endif // defined(TARGET_WASM)
}

/*****************************************************************************
Expand Down Expand Up @@ -1682,17 +1728,51 @@ void CodeGen::siBeginBlock(BasicBlock* block)
//
void CodeGen::siOpenScopesForNonTrackedVars(const BasicBlock* block, unsigned int lastBlockILEndOffset)
{
unsigned int beginOffs = block->bbCodeOffs;

#if defined(TARGET_WASM)
// TODO-WASM: Wasm structured control flow
// requirements are incompatible with debug codegen's
// desire to keep blocks in increasing IL offset
// order. Figure out the proper scope manipulations.
//
// Scan all scopes directly because the relooper does not emit blocks in
// increasing IL offset order.
if (m_compiler->opts.OptimizationDisabled())
{
unsigned int endOffs = block->bbCodeOffsEnd;

for (unsigned i = 0; i < m_compiler->info.compVarScopesCount; i++)
{
VarScopeDsc* varScope = &m_compiler->info.compVarScopes[i];

if (siWasmOpenedScopes[i])
{
continue;
}

if ((varScope->vsdLifeBeg >= endOffs) || (varScope->vsdLifeEnd <= beginOffs))
{
continue;
}

siWasmOpenedScopes[i] = true;

LclVarDsc* lclVarDsc = m_compiler->lvaGetDesc(varScope->vsdVarNum);

// Only report locals that were referenced, if we're not doing debug codegen
if (m_compiler->opts.compDbgCode || (lclVarDsc->lvRefCnt() > 0))
{
JITDUMP("Scope info: opening scope, LVnum=%u [%03X..%03X)\n", varScope->vsdLVnum, varScope->vsdLifeBeg,
varScope->vsdLifeEnd);

varLiveKeeper->siStartVariableLiveRange(lclVarDsc, varScope->vsdVarNum);
}
else
{
JITDUMP("Skipping open scope for V%02u, unreferenced\n", varScope->vsdVarNum);
}
}
}

return;
#endif // defined(TARGET_WASM)

unsigned int beginOffs = block->bbCodeOffs;

// There aren't any tracked locals.
//
// For debuggable or minopts code, scopes can begin only on block boundaries.
Expand Down
Loading
Loading