diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index cf83e203cac90d..f079e1d7e084a3 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -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. diff --git a/src/coreclr/inc/cordebuginfo.h b/src/coreclr/inc/cordebuginfo.h index bc0d5cc5c9a69f..eb867c9711ddc8 100644 --- a/src/coreclr/inc/cordebuginfo.h +++ b/src/coreclr/inc/cordebuginfo.h @@ -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 diff --git a/src/coreclr/jit/codegen.h b/src/coreclr/jit/codegen.h index 2020048e2acc3f..af165a682b65fa 100644 --- a/src/coreclr/jit/codegen.h +++ b/src/coreclr/jit/codegen.h @@ -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 diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index e20d08a7be1b2e..013c8e0bf0d605 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -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); diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 5e0dbba6988618..cee15dcf93e6b1 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -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 diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index 87a2f95eba7cc5..d3a91e7ee73895 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -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)++; } } @@ -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; } @@ -1314,6 +1315,11 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const { varNum--; } + + if ((lvaWasmPortableEntryPtrArg != BAD_VAR_NUM) && (originalVarNum > lvaWasmPortableEntryPtrArg)) + { + varNum--; + } #endif if (varNum >= info.compLocalsCount) diff --git a/src/coreclr/jit/registeropswasm.cpp b/src/coreclr/jit/registeropswasm.cpp index 112520dd9928b1..e352a084db8003 100644 --- a/src/coreclr/jit/registeropswasm.cpp +++ b/src/coreclr/jit/registeropswasm.cpp @@ -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(WasmValueType::Count) == ICorDebugInfo::WASM_VALUE_TYPE_COUNT); static_assert(((static_cast(WasmValueType::Count) - 1) >> WASM_REG_TYPE_BITS) == 0); //------------------------------------------------------------------------ diff --git a/src/coreclr/jit/scopeinfo.cpp b/src/coreclr/jit/scopeinfo.cpp index edae8a122f9693..dad46b01f0ca55 100644 --- a/src/coreclr/jit/scopeinfo.cpp +++ b/src/coreclr/jit/scopeinfo.cpp @@ -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(REG_NA) == static_cast(ICorDebugInfo::REGNUM_AMBIENT_SP)); +static_assert(static_cast(ICorDebugInfo::REGNUM_AMBIENT_SP) == 2); +#endif // defined(TARGET_WASM) + //============================================================================ // siVarLoc functions //============================================================================ @@ -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)) @@ -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: @@ -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) } /***************************************************************************** @@ -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. diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 262e9de11c1605..9cd397b4c0c075 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using ILCompiler.ObjectWriter; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; @@ -119,6 +120,16 @@ public void WasmWebcilModule() static void Validate(ReadyToRunReader reader) { + const byte WasmI32 = 0x7F; + const byte WasmLocalGet = 0x20; + const byte WasmLocalSet = 0x21; + const byte WasmLocalTee = 0x22; + const byte WasmI32Load = 0x28; + const byte WasmI32Const = 0x41; + const byte WasmI32Sub = 0x6B; + const byte WasmI64 = 0x7E; + const int WasmEncodedFrameBase = 2; + var webcilReader = Assert.IsType(reader.CompositeReader); Assert.True(webcilReader.IsWasmWrapped); Assert.Equal(WasmMachine.Wasm32, reader.Machine); @@ -126,12 +137,618 @@ static void Validate(ReadyToRunReader reader) List methods = R2RAssert.GetAllMethods(reader); Assert.True(methods.Exists(method => method.SignatureString.Contains("AddIntegers", StringComparison.Ordinal))); + ReadyToRunMethod optimizedTrackedVariables = Assert.Single(methods, method => + method.SignatureString.Contains("OptimizedTrackedVariables", StringComparison.Ordinal)); + Assert.Equal(new[] { "int", "int" }, optimizedTrackedVariables.LocalSignature); + RuntimeFunction optimizedRoot = Assert.Single(optimizedTrackedVariables.RuntimeFunctions); + Assert.Equal(5, optimizedRoot.DebugInfo!.VariablesList.Count); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x3C, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x0, + endOffset: 0x3C, + locationType: VarLocType.VLT_REG, + data1: 0x20000002, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x59, + endOffset: 0x74, + locationType: VarLocType.VLT_REG, + data1: 0x20000002, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x2F, + endOffset: 0x51, + locationType: VarLocType.VLT_REG, + data1: 0x20000004, + data2: 0, + data3: 0)); + Assert.Single(optimizedRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x59, + endOffset: 0x74, + locationType: VarLocType.VLT_REG, + data1: 0x20000004, + data2: 0, + data3: 0)); + Assert.DoesNotContain( + optimizedRoot.DebugInfo.VariablesList, + variable => variable.Variable.Type == VariableType.Local && variable.Variable.Index == 1); + WebcilImageReader.WasmFunctionInfo optimizedBody = ResolveWasmBody(reader, webcilReader, optimizedRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32, WasmI32 }, optimizedBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (3, WasmI32) }, optimizedBody.Locals); + AssertWasmInstructionPrefix( + optimizedBody, + [WasmLocalGet, 0, WasmI32Const, 0x10, WasmI32Sub, WasmLocalTee, 0]); + ReadyToRunMethod leafFrameLocal = Assert.Single(methods, method => + method.SignatureString.Contains("LeafFrameLocal", StringComparison.Ordinal)); + Assert.Equal(new[] { "int" }, leafFrameLocal.LocalSignature); + RuntimeFunction leafRoot = Assert.Single(leafFrameLocal.RuntimeFunctions); + Assert.Equal(3, leafRoot.DebugInfo!.VariablesList.Count); + Assert.Single(leafRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x27, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(leafRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x27, + endOffset: 0x43, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x1C, + data3: 0)); + Assert.Single(leafRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x27, + endOffset: 0x43, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x14, + data3: 0)); + WebcilImageReader.WasmFunctionInfo leafBody = ResolveWasmBody(reader, webcilReader, leafRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, leafBody.ParamTypes); + Assert.Empty(leafBody.Locals); + AssertWasmContainsInstructions( + leafBody, + [WasmLocalGet, 0, WasmI32Load, 0, 0x14]); + ReadyToRunMethod locallocFrameLocal = Assert.Single(methods, method => + method.SignatureString.Contains("LocallocFrameLocal", StringComparison.Ordinal)); + Assert.Equal(new[] { "int" }, locallocFrameLocal.LocalSignature); + RuntimeFunction locallocRoot = Assert.Single(locallocFrameLocal.RuntimeFunctions); + Assert.Equal(3, locallocRoot.DebugInfo!.VariablesList.Count); + Assert.Single(locallocRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x41, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(locallocRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x13F, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x1C, + data3: 0)); + Assert.Single(locallocRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x13F, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x14, + data3: 0)); + WebcilImageReader.WasmFunctionInfo locallocRootBody = + ResolveWasmBody(reader, webcilReader, locallocRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, locallocRootBody.ParamTypes); + Assert.Equal( + new (uint Count, byte ValType)[] { (3, WasmI32), (1, WasmI64) }, + locallocRootBody.Locals); + AssertWasmContainsInstructions( + locallocRootBody, + [WasmLocalGet, 0, WasmLocalSet, 3]); + AssertWasmContainsInstructions( + locallocRootBody, + [WasmLocalGet, 3, WasmI32Load, 0, 0x1C]); + ReadyToRunMethod addDoubles = Assert.Single(methods, method => + method.SignatureString.Contains("AddDoubles", StringComparison.Ordinal)); + const int WasmRegTypeShift = 29; + const uint F64WasmValueType = 4; + List doubleVariables = addDoubles.RuntimeFunctions + .Where(runtimeFunction => runtimeFunction.DebugInfo is not null) + .SelectMany(runtimeFunction => runtimeFunction.DebugInfo!.VariablesList) + .ToList(); + Assert.Equal(4, doubleVariables.Count); + Assert.DoesNotContain(doubleVariables, + variable => variable.VariableLocation.VarLocType is VarLocType.VLT_REG_FP or VarLocType.VLT_FPSTK); + NativeVarInfo leftInLocal = Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x22, + locationType: VarLocType.VLT_REG, + data1: unchecked((int)0x80000001), + data2: 0, + data3: 0)); + Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x22, + endOffset: 0x34, + locationType: VarLocType.VLT_STK2, + data1: WasmEncodedFrameBase, + data2: 0x18, + data3: 0)); + Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x0, + endOffset: 0x22, + locationType: VarLocType.VLT_REG, + data1: unchecked((int)0x80000002), + data2: 0, + data3: 0)); + Assert.Single(doubleVariables, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Parameter, + variableIndex: 1, + startOffset: 0x22, + endOffset: 0x34, + locationType: VarLocType.VLT_STK2, + data1: WasmEncodedFrameBase, + data2: 0x10, + data3: 0)); + Assert.All( + doubleVariables.Where(variable => variable.VariableLocation.VarLocType == VarLocType.VLT_REG), + variable => Assert.Equal(F64WasmValueType, (uint)variable.VariableLocation.Data1 >> WasmRegTypeShift)); + Assert.DoesNotContain(doubleVariables, variable => variable.Variable.Type == VariableType.Local); + + // Prove the exact assertion is sensitive to a garbled decoder rather than merely + // counting entries. Mutate the local-index bits in the packed register and verify the + // known-good record no longer matches. + NativeVarInfo mutatedLeft = leftInLocal; + VarLoc mutatedLocation = mutatedLeft.VariableLocation; + mutatedLocation.Data1++; + mutatedLeft.VariableLocation = mutatedLocation; + Assert.NotEqual(leftInLocal.VariableLocation.Data1, mutatedLeft.VariableLocation.Data1); + Assert.False(IsExactVariable( + mutatedLeft, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x22, + locationType: VarLocType.VLT_REG, + data1: unchecked((int)0x80000001), + data2: 0, + data3: 0)); // Reads static data, so the JIT materializes the image base via a well-known-global global.get. - Assert.True(methods.Exists(method => - method.SignatureString.Contains("SumStaticData", StringComparison.Ordinal))); + ReadyToRunMethod sumStaticData = Assert.Single(methods, method => + method.SignatureString.Contains("SumStaticData", StringComparison.Ordinal)); + RuntimeFunction sumStaticRoot = Assert.Single(sumStaticData.RuntimeFunctions); + Assert.Single(sumStaticRoot.DebugInfo!.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0xC1, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + WebcilImageReader.WasmFunctionInfo sumStaticBody = ResolveWasmBody(reader, webcilReader, sumStaticRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, sumStaticBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (7, WasmI32) }, sumStaticBody.Locals); + AssertWasmInstructionPrefix( + sumStaticBody, + [WasmLocalGet, 0, WasmI32Const, 0x20, WasmI32Sub, WasmLocalTee, 0]); + AssertWasmContainsInstructions( + sumStaticBody, + [WasmLocalGet, 0, WasmI32Load, 0, 0x14]); // Has a try/finally, so the JIT materializes the table base via a well-known-global global.get. - Assert.True(methods.Exists(method => - method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal))); + ReadyToRunMethod sumWithFinally = Assert.Single(methods, method => + method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal)); + RuntimeFunction sumWithFinallyRoot = Assert.Single( + sumWithFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.DebugInfo is not null); + Assert.Equal(3, sumWithFinallyRoot.DebugInfo!.VariablesList.Count); + Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x3B, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1F2, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x2C, + data3: 0)); + Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1F2, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x24, + data3: 0)); + Assert.Single(sumWithFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + ReadyToRunMethod gcLocalAcrossFinally = Assert.Single(methods, method => + method.SignatureString.Contains("GcLocalAcrossFinally", StringComparison.Ordinal)); + Assert.Equal( + new[] { "Webcil.WasmWebcilModule+GcMarker", "int" }, + gcLocalAcrossFinally.LocalSignature); + RuntimeFunction gcLocalRoot = Assert.Single( + gcLocalAcrossFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.DebugInfo is not null); + Assert.Equal(4, gcLocalRoot.DebugInfo!.VariablesList.Count); + Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x3B, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1B6, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x2C, + data3: 0)); + NativeVarInfo gcMarker = Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x3B, + endOffset: 0x1B6, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x24, + data3: 0)); + Assert.Single(gcLocalRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 1, + startOffset: 0x3B, + endOffset: 0x1B6, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x20, + data3: 0)); + ILCompiler.Reflection.ReadyToRun.Amd64.GcInfo gcInfo = + Assert.IsType(gcLocalAcrossFinally.GcInfo); + Assert.Equal(2u, gcInfo.SlotTable.NumSlots); + Assert.Equal(0u, gcInfo.SlotTable.NumRegisters); + Assert.Equal(0u, gcInfo.SlotTable.NumStackSlots); + Assert.Equal(2u, gcInfo.SlotTable.NumUntracked); + ILCompiler.Reflection.ReadyToRun.Amd64.GcSlotTable.GcSlot markerGcSlot = Assert.Single( + gcInfo.SlotTable.GcSlots, + slot => slot.StackSlot?.SpOffset == gcMarker.VariableLocation.Data2); + Assert.Equal(GcStackSlotBase.GC_FRAMEREG_REL, markerGcSlot.StackSlot.Base); + Assert.Equal(GcSlotFlags.GC_SLOT_PINNED | GcSlotFlags.GC_SLOT_UNTRACKED, markerGcSlot.Flags); + RuntimeFunction gcLocalFunclet = Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + + // The target reconstructs the absolute logical frame from shadow-stack memory and + // unwind data. The debug-info wire encoding separately collapses REG_FPBASE, + // REG_SPBASE, and REGNUM_AMBIENT_SP to 2; it does not name a V8 local. + DebugInfoBoundsEntry collectCall = Assert.Single( + gcLocalRoot.DebugInfo.BoundsList, + bound => bound.ILOffset == 0x10); + Assert.Equal(0x161u, collectCall.NativeOffset); + Assert.Equal(SourceTypes.StackEmpty, collectCall.SourceTypes); + Assert.True(gcMarker.StartOffset <= collectCall.NativeOffset); + Assert.True(collectCall.NativeOffset < gcMarker.EndOffset); + WebcilImageReader.WasmFunctionInfo gcLocalRootBody = + ResolveWasmBody(reader, webcilReader, gcLocalRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, gcLocalRootBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (2, WasmI32) }, gcLocalRootBody.Locals); + AssertWasmContainsInstructions( + gcLocalRootBody, + [WasmLocalGet, 0, WasmI32Load, 0, 0x24]); + WebcilImageReader.WasmFunctionInfo gcLocalFuncletBody = + ResolveWasmBody(reader, webcilReader, gcLocalFunclet); + Assert.Equal(new byte[] { WasmI32, WasmI32 }, gcLocalFuncletBody.ParamTypes); + Assert.Equal(new (uint Count, byte ValType)[] { (1, WasmI32) }, gcLocalFuncletBody.Locals); + AssertWasmInstructionPrefix( + gcLocalFuncletBody, + [WasmLocalGet, 0, WasmI32Const, 0x10, WasmI32Sub, WasmLocalSet, 0]); + AssertWasmContainsInstructions( + gcLocalFuncletBody, + [WasmLocalGet, 1, WasmI32Load, 0, 0x24]); + ReadyToRunMethod locallocAcrossFinally = Assert.Single(methods, method => + method.SignatureString.Contains("LocallocAcrossFinally", StringComparison.Ordinal)); + Assert.Equal(new[] { "int", "int*", "int" }, locallocAcrossFinally.LocalSignature); + RuntimeFunction locallocFinallyRoot = Assert.Single( + locallocAcrossFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.DebugInfo is not null); + Assert.Equal(5, locallocFinallyRoot.DebugInfo!.VariablesList.Count); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x0, + endOffset: 0x41, + locationType: VarLocType.VLT_REG, + data1: 0x20000001, + data2: 0, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Parameter, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x2C, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x24, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 1, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x20, + data3: 0)); + Assert.Single(locallocFinallyRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 3, + variableType: VariableType.Local, + variableIndex: 2, + startOffset: 0x41, + endOffset: 0x200, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x1C, + data3: 0)); + RuntimeFunction locallocFinallyFunclet = Assert.Single( + locallocAcrossFinally.RuntimeFunctions, + runtimeFunction => runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + WebcilImageReader.WasmFunctionInfo locallocFinallyRootBody = + ResolveWasmBody(reader, webcilReader, locallocFinallyRoot); + Assert.Equal(new byte[] { WasmI32, WasmI32, WasmI32 }, locallocFinallyRootBody.ParamTypes); + Assert.Equal( + new (uint Count, byte ValType)[] { (3, WasmI32), (1, WasmI64) }, + locallocFinallyRootBody.Locals); + AssertWasmContainsInstructions( + locallocFinallyRootBody, + [WasmLocalGet, 0, WasmLocalSet, 3]); + AssertWasmContainsInstructions( + locallocFinallyRootBody, + [WasmLocalGet, 3, WasmI32Load, 0, 0x2C]); + WebcilImageReader.WasmFunctionInfo locallocFinallyFuncletBody = + ResolveWasmBody(reader, webcilReader, locallocFinallyFunclet); + Assert.Equal(new byte[] { WasmI32, WasmI32 }, locallocFinallyFuncletBody.ParamTypes); + Assert.Equal( + new (uint Count, byte ValType)[] { (2, WasmI32), (1, WasmI64) }, + locallocFinallyFuncletBody.Locals); + AssertWasmContainsInstructions( + locallocFinallyFuncletBody, + [WasmLocalGet, 1, WasmI32Load, 0, 0x18]); + ReadyToRunMethod gcSlotIdentity = Assert.Single(methods, method => + method.SignatureString.Contains("GcSlotIdentity", StringComparison.Ordinal)); + Assert.Equal( + new[] + { + "Webcil.WasmWebcilModule+GcMarker", + "Webcil.WasmWebcilModule+GcMarker", + "Webcil.WasmWebcilModule+GcMarker", + "int", + }, + gcSlotIdentity.LocalSignature); + byte[] gcSlotIdentityIL = GetMethodILBytes(gcSlotIdentity); + Assert.Equal(0x57, gcSlotIdentityIL.Length); + Assert.Equal(0x1F, gcSlotIdentityIL[0x00]); + Assert.Equal(17, gcSlotIdentityIL[0x01]); + Assert.Equal(0x0A, gcSlotIdentityIL[0x07]); + Assert.Equal(0x1F, gcSlotIdentityIL[0x12]); + Assert.Equal(29, gcSlotIdentityIL[0x13]); + Assert.Equal(0x0B, gcSlotIdentityIL[0x19]); + Assert.Equal(0x14, gcSlotIdentityIL[0x24]); + Assert.Equal(0x0C, gcSlotIdentityIL[0x25]); + RuntimeFunction gcSlotRoot = Assert.Single(gcSlotIdentity.RuntimeFunctions); + Assert.Equal(4, gcSlotRoot.DebugInfo!.VariablesList.Count); + // The minopts wasm scope latch opens each synthesized source scope once and closes it + // at method end, so these distinct IL locals currently share one exact native range. + NativeVarInfo firstMarker = Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 0, + variableType: VariableType.Local, + variableIndex: 0, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x48, + data3: 0)); + NativeVarInfo secondMarker = Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 1, + variableType: VariableType.Local, + variableIndex: 1, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x44, + data3: 0)); + NativeVarInfo nullMarker = Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 2, + variableType: VariableType.Local, + variableIndex: 2, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x40, + data3: 0)); + Assert.Single(gcSlotRoot.DebugInfo.VariablesList, variable => + IsExactVariable( + variable, + variableNumber: 3, + variableType: VariableType.Local, + variableIndex: 3, + startOffset: 0x36, + endOffset: 0x2B1, + locationType: VarLocType.VLT_STK, + data1: WasmEncodedFrameBase, + data2: 0x3C, + data3: 0)); + Assert.NotEqual(firstMarker.VariableLocation.Data2, secondMarker.VariableLocation.Data2); + ILCompiler.Reflection.ReadyToRun.Amd64.GcInfo slotIdentityGcInfo = + Assert.IsType(gcSlotIdentity.GcInfo); + Assert.Equal(6u, slotIdentityGcInfo.SlotTable.NumSlots); + foreach (NativeVarInfo marker in new[] { firstMarker, secondMarker, nullMarker }) + { + ILCompiler.Reflection.ReadyToRun.Amd64.GcSlotTable.GcSlot slot = Assert.Single( + slotIdentityGcInfo.SlotTable.GcSlots, + candidate => candidate.StackSlot?.SpOffset == marker.VariableLocation.Data2); + Assert.Equal(GcStackSlotBase.GC_FRAMEREG_REL, slot.StackSlot.Base); + Assert.Equal(GcSlotFlags.GC_SLOT_PINNED | GcSlotFlags.GC_SLOT_UNTRACKED, slot.Flags); + } + DebugInfoBoundsEntry slotIdentityGcCall = Assert.Single( + gcSlotRoot.DebugInfo.BoundsList, + bound => bound.ILOffset == 0x2D); + Assert.Equal(0x18Cu, slotIdentityGcCall.NativeOffset); + Assert.All( + new[] { firstMarker, secondMarker, nullMarker }, + marker => + { + Assert.True(marker.StartOffset <= slotIdentityGcCall.NativeOffset); + Assert.True(slotIdentityGcCall.NativeOffset < marker.EndOffset); + }); // Has a catch clause, so the JIT emits a try_table catch_ref that references the // imported restore-context exception tag. Assert.True(methods.Exists(method => @@ -139,6 +756,73 @@ static void Validate(ReadyToRunReader reader) Assert.True(WasmR2RAssert.WasmIndexSpacesHaveExpectedEntries(webcilReader, out string indexDiagnostic), indexDiagnostic); + static bool IsExactVariable( + NativeVarInfo variable, + uint variableNumber, + VariableType variableType, + int variableIndex, + uint startOffset, + uint endOffset, + VarLocType locationType, + int data1, + int data2, + int data3) + => variable.VariableNumber == variableNumber + && variable.Variable.Type == variableType + && variable.Variable.Index == variableIndex + && variable.StartOffset == startOffset + && variable.EndOffset == endOffset + && variable.VariableLocation.VarLocType == locationType + && variable.VariableLocation.Data1 == data1 + && variable.VariableLocation.Data2 == data2 + && variable.VariableLocation.Data3 == data3; + + static WebcilImageReader.WasmFunctionInfo ResolveWasmBody( + ReadyToRunReader reader, + WebcilImageReader webcilReader, + RuntimeFunction runtimeFunction) + { + uint tableIndex = checked(reader.WasmMinFunctionTableIndex + (uint)runtimeFunction.Id); + int functionIndex = webcilReader.GetFunctionIndexFromTableIndex(tableIndex); + Assert.True(functionIndex >= 0, $"Could not resolve wasm table index {tableIndex} to a function body."); + WebcilImageReader.WasmFunctionInfo? body = webcilReader.GetWasmFunctionBody(functionIndex); + Assert.True(body is not null, $"Wasm function body {functionIndex} was not found."); + return body.Value; + } + + static byte[] GetMethodILBytes(ReadyToRunMethod method) + { + MethodDefinition methodDefinition = + method.ComponentReader.MetadataReader.GetMethodDefinition((MethodDefinitionHandle)method.MethodHandle); + byte[]? ilBytes = null; + method.ComponentReader.GetSectionData( + methodDefinition.RelativeVirtualAddress, + sectionData => ilBytes = MethodBodyBlock.Create(sectionData).GetILBytes()); + return Assert.IsType(ilBytes); + } + + static void AssertWasmInstructionPrefix( + WebcilImageReader.WasmFunctionInfo body, + ReadOnlySpan expected) + { + ReadOnlySpan instructions = + body.Image.AsSpan(body.InstructionOffset, body.InstructionLength); + Assert.True( + instructions.StartsWith(expected), + $"Expected wasm instruction prefix {Convert.ToHexString(expected)}, actual {Convert.ToHexString(instructions[..Math.Min(instructions.Length, expected.Length)])}."); + } + + static void AssertWasmContainsInstructions( + WebcilImageReader.WasmFunctionInfo body, + ReadOnlySpan expected) + { + ReadOnlySpan instructions = + body.Image.AsSpan(body.InstructionOffset, body.InstructionLength); + Assert.True( + instructions.IndexOf(expected) >= 0, + $"Expected wasm instruction sequence {Convert.ToHexString(expected)}."); + } + // The wasm JIT references the ABI well-known globals via maximally padded WASM_GLOBAL_INDEX_LEB // relocations that the R2R object writer must self-resolve to the fixed global // indices and shrink down to their minimal size. Verify the emitted code contains a correctly self-resolved 'global.get' for the @@ -411,6 +1095,21 @@ static void Validate(ReadyToRunReader reader) } } + [Theory] + [InlineData(2, "ambient SP")] + [InlineData(3, "Unknown '3'")] + [InlineData(0x20000001, "$1 (i32)")] + [InlineData(0x40000002, "$2 (i64)")] + [InlineData(0x60000003, "$3 (f32)")] + [InlineData(unchecked((int)0x80000004), "$4 (f64)")] + [InlineData(unchecked((int)0xA0000005), "$5 (v128)")] + [InlineData(unchecked((int)0xC0000006), "$6 (exnref)")] + [InlineData(unchecked((int)0xE0000001), "Unknown '-536870911'")] + public void WasmDebugRegisterIsDecoded(int register, string expected) + { + Assert.Equal(expected, DebugInfo.GetPlatformSpecificRegister(WasmMachine.Wasm32, register)); + } + [ConditionalFact(typeof(TestPaths), nameof(TestPaths.IsNotWasmTarget))] public void RuntimeFunctionsSectionSizeExcludesSentinel() { diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs index e84e3fdd8f5d3d..b0103d62338172 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/Webcil/WasmWebcilModule.cs @@ -11,12 +11,59 @@ public static class WasmWebcilModule private static readonly int[] s_primes = new int[] { 3, 5, 7, 11, 13 }; private static int s_counter; + private sealed class GcMarker + { + public readonly int Value; + + public GcMarker(int value) + { + Value = value; + } + } + [MethodImpl(MethodImplOptions.NoInlining)] public static int AddIntegers(int left, int right) { return left + right; } + [MethodImpl(MethodImplOptions.NoInlining)] + public static int OptimizedTrackedVariables(int left, int right) + { + int sum = left + right; + if (sum < 0) + { + return AddIntegers(sum, right); + } + + int difference = left - right; + return AddIntegers(sum, difference); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static unsafe int LeafFrameLocal(int value) + { + int local = value + 1; + int* address = &local; + return *address * 2; + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static unsafe int LocallocFrameLocal(int value) + { + int length = (value & 3) + 1; + int* storage = stackalloc int[length]; + *storage = value; + return *storage; + } + + [MethodImpl(MethodImplOptions.NoOptimization)] + public static double AddDoubles(double left, double right) + { + double result = left + right; + return result; + } + // Reads static data, which forces the JIT to materialize the imageBase address via a // 'global.get' of the wasm imageBase well-known global. public static int SumStaticData(int index) @@ -48,6 +95,76 @@ public static int SumWithFinally(int index) return total + s_counter; } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static int GcLocalAcrossFinally(int value) + { + GcMarker marker = new(value); + try + { + return marker.Value; + } + finally + { + // Keep the newly allocated object live in the parent frame across an actual GC + // reached through the finally funclet. + CollectAtGcSafepoint(); + GC.KeepAlive(marker); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static void CollectAtGcSafepoint() + { + GC.Collect(); + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static unsafe int LocallocAcrossFinally(int value) + { + int length = (value & 3) + 1; + int* storage = stackalloc int[length]; + *storage = value; + try + { + return *storage; + } + finally + { + CollectAtGcSafepoint(); + } + } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + public static int GcSlotIdentity() + { + GcMarker first = new(17); + if (first.Value == 0) + { + return 0; + } + + GcMarker second = new(29); + if (second.Value == 0) + { + return 0; + } + + GcMarker? empty = null; + TouchMarkerSlot(ref empty); + + CollectAtGcSafepoint(); + int result = (first.Value * 100) + second.Value; + GC.KeepAlive(first); + GC.KeepAlive(second); + GC.KeepAlive(empty); + return result; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TouchMarkerSlot(ref GcMarker? marker) + { + } + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] public static int CatchException(int value) { diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs index 58cb620fe98558..bab9e18b8aab47 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs @@ -87,12 +87,38 @@ public static string GetPlatformSpecificRegister(Machine machine, int regnum) case Machine.RiscV64: return ((RiscV64.Registers)regnum).ToString(); case WasmMachine.Wasm32: - return $"NYI '{regnum}'"; // WASM-TODO Implement this correctly. + return GetWasmRegister(regnum); default: throw new NotImplementedException($"No implementation for machine type {machine}."); } } + private static string GetWasmRegister(int regnum) + { + // Keep in sync with ICorDebugInfo::WASM_REG_TYPE_SHIFT in cordebuginfo.h. A future + // width change requires a versioned ReadyToRun debug-info format change because this + // static image reader has no live target descriptor from which to discover it. + const int WasmRegTypeShift = 29; + const uint WasmRegIndexMask = (1u << WasmRegTypeShift) - 1; + + return regnum switch + { + 0 => "PC", + 1 => "REGNUM_COUNT", + 2 => "ambient SP", + _ => ((uint)regnum >> WasmRegTypeShift) switch + { + 1 => $"${(uint)regnum & WasmRegIndexMask} (i32)", + 2 => $"${(uint)regnum & WasmRegIndexMask} (i64)", + 3 => $"${(uint)regnum & WasmRegIndexMask} (f32)", + 4 => $"${(uint)regnum & WasmRegIndexMask} (f64)", + 5 => $"${(uint)regnum & WasmRegIndexMask} (v128)", + 6 => $"${(uint)regnum & WasmRegIndexMask} (exnref)", + _ => $"Unknown '{regnum}'", + }, + }; + } + private void EnsureInitialized() { if (_boundsList != null) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 25c22dc3a928fc..8a6ac46172fd69 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1693,6 +1693,8 @@ CDAC_GLOBAL_STRING(Architecture, loongarch64) CDAC_GLOBAL_STRING(Architecture, riscv64) #elif defined(TARGET_WASM) CDAC_GLOBAL_STRING(Architecture, wasm) +CDAC_GLOBAL(WasmDebugRegisterTypeShift, T_UINT8, ICorDebugInfo::WASM_REG_TYPE_SHIFT) +CDAC_GLOBAL(WasmDebugValueTypeCount, T_UINT8, ICorDebugInfo::WASM_VALUE_TYPE_COUNT) #else #error TARGET_{ARCH} define is not recognized by the cDAC. Update this switch and the enum values in IRuntimeInfo.cs #endif