diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index b99fc3d2b99a1c..dd5b4b0ef0edda 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -225,6 +225,50 @@ 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. Every register field in the Vars stream holds a WebAssembly local +index biased past the reserved register numbers: + +```text +register = localIndex + WASM_LOCAL_REGNUM_BASE +``` + +`WASM_LOCAL_REGNUM_BASE` is `3`, one past `REGNUM_AMBIENT_SP`. Values `0` through `2` remain +`REGNUM_PC`, `REGNUM_COUNT`, and `REGNUM_AMBIENT_SP`. As on other targets, the value type is not +encoded: it is implied by the variable's type and by the local's declaration in the WebAssembly +function. + +`WASM_LOCAL_REGNUM_BASE` (`3`) and the stack base register (`2`, described below) are fixed +constants of DebugInfo contract version `c1`; readers hardcode them. Changing either requires a new +DebugInfo contract version. Because ReadyToRun images persist this encoding, it also requires a +ReadyToRun format version change so static readers such as R2RDump do not misdecode older images. + +### 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..e4cfcd88be0550 100644 --- a/src/coreclr/inc/cordebuginfo.h +++ b/src/coreclr/inc/cordebuginfo.h @@ -299,6 +299,12 @@ class ICorDebugInfo }; +#ifdef TARGET_WASM + // WASM has no physical registers. A register field in a variable location holds a wasm + // local index biased past the reserved RegNum values above. + static constexpr uint32_t WASM_LOCAL_REGNUM_BASE = REGNUM_AMBIENT_SP + 1; +#endif // TARGET_WASM + // VarLoc describes the location of a native variable. Note that currently, VLT_REG_BYREF and VLT_STK_BYREF // are only used for value types on X64. // [cDAC]: Mirrored in managed code (IDacDbiInterface.cs). diff --git a/src/coreclr/jit/codegen.h b/src/coreclr/jit/codegen.h index 27ef29f71f05a9..1256b6fd86e7bb 100644 --- a/src/coreclr/jit/codegen.h +++ b/src/coreclr/jit/codegen.h @@ -741,6 +741,12 @@ class CodeGen final : public CodeGenInterface IL_OFFSET siLastEndOffs; // IL offset of the (exclusive) end of the last block processed +#if defined(TARGET_WASM) + // Wasm opens its method-wide scopes in the first emitted block rather than relying on the + // monotonic enter/exit cursors used by other targets. + bool siWasmScopesOpened; +#endif // defined(TARGET_WASM) + /* XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index 87cf3549ff7b34..34367278325017 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -2901,13 +2901,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/ee_il_dll.cpp b/src/coreclr/jit/ee_il_dll.cpp index 11dc072b49ae23..9e337116bf07d5 100644 --- a/src/coreclr/jit/ee_il_dll.cpp +++ b/src/coreclr/jit/ee_il_dll.cpp @@ -866,7 +866,11 @@ void Compiler::eeDispVar(ICorDebugInfo::NativeVarInfo* var) { case CodeGenInterface::VLT_REG: case CodeGenInterface::VLT_REG_BYREF: +#if defined(TARGET_WASM) + printf("$%u", (unsigned)var->loc.vlReg.vlrReg - ICorDebugInfo::WASM_LOCAL_REGNUM_BASE); +#else printf("%s", getRegName(var->loc.vlReg.vlrReg)); +#endif if (var->loc.vlType == (ICorDebugInfo::VarLocType)CodeGenInterface::VLT_REG_BYREF) { printf(" byref"); diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index 6cb32a2f78cec9..f67f211166c66d 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -1281,7 +1281,7 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const } #if defined(TARGET_WASM) - if (varNum == lvaWasmSpArg) + if ((varNum == lvaWasmSpArg) || (varNum == lvaWasmPortableEntryPointArg)) { return (unsigned)ICorDebugInfo::UNKNOWN_ILNUM; } @@ -1319,6 +1319,11 @@ unsigned Compiler::compMap2ILvarNum(unsigned varNum) const { varNum--; } + + if ((lvaWasmPortableEntryPointArg != BAD_VAR_NUM) && (originalVarNum > lvaWasmPortableEntryPointArg)) + { + varNum--; + } #endif if (varNum >= info.compLocalsCount) diff --git a/src/coreclr/jit/scopeinfo.cpp b/src/coreclr/jit/scopeinfo.cpp index f167f159f40b42..c12596db7acc86 100644 --- a/src/coreclr/jit/scopeinfo.cpp +++ b/src/coreclr/jit/scopeinfo.cpp @@ -57,6 +57,28 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX #include "emit.h" #include "codegen.h" +#if defined(TARGET_WASM) +// Stack VarLoc records use the reserved ambient SP register number, not a wasm local. +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); + +//------------------------------------------------------------------------ +// siWasmDebugRegNum: Encode a wasm local as a debug-info register number. +// +// Arguments: +// reg - the JIT register representing a wasm local. +// +// Return Value: +// The wasm local index biased past the reserved debug-info register numbers. +// +static regNumber siWasmDebugRegNum(regNumber reg) +{ + return static_cast(ICorDebugInfo::WASM_LOCAL_REGNUM_BASE + WasmRegToIndex(reg)); +} +#endif // defined(TARGET_WASM) + //============================================================================ // siVarLoc functions //============================================================================ @@ -208,6 +230,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 = siWasmDebugRegNum(reg); + } + else + { + assert(genIsValidReg(otherReg)); + vlType = VLT_REG_REG; + vlRegReg.vlrrReg1 = siWasmDebugRegNum(reg); + vlRegReg.vlrrReg2 = siWasmDebugRegNum(otherReg); + } + return; +#endif // defined(TARGET_WASM) + if (otherReg == REG_NA) { if (genIsValidFloatReg(reg)) @@ -462,6 +508,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 = siWasmDebugRegNum(varDsc->GetRegNum()); + return; +#endif // defined(TARGET_WASM) + switch (type) { case TYP_INT: @@ -623,7 +675,11 @@ void CodeGenInterface::dumpSiVarLoc(const siVarLoc* varLoc) const { case VLT_REG: case VLT_REG_BYREF: +#if defined(TARGET_WASM) + printf("$%u", (unsigned)varLoc->vlReg.vlrReg - ICorDebugInfo::WASM_LOCAL_REGNUM_BASE); +#else printf("%s", getRegName(varLoc->vlReg.vlrReg)); +#endif if (varLoc->vlType == VLT_REG_BYREF) { printf(" byref"); @@ -1589,6 +1645,10 @@ void CodeGen::siInit() siLastEndOffs = 0; m_compiler->compResetScopeLists(); + +#if defined(TARGET_WASM) + siWasmScopesOpened = false; +#endif // defined(TARGET_WASM) } /***************************************************************************** @@ -1674,11 +1734,35 @@ void CodeGen::siBeginBlock(BasicBlock* block) void CodeGen::siOpenScopesForNonTrackedVars(const BasicBlock* block, unsigned int lastBlockILEndOffset) { #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. - // + // Wasm is compiled only ahead of time, where getVars reports no explicit ranges, so every + // scope covers the whole method. Our elimination of irreducible flow does not emit blocks in + // increasing IL offset order, so open all scopes in the first emitted block. + if (m_compiler->opts.OptimizationDisabled() && !siWasmScopesOpened) + { + siWasmScopesOpened = true; + + for (unsigned i = 0; i < m_compiler->info.compVarScopesCount; i++) + { + VarScopeDsc* varScope = &m_compiler->info.compVarScopes[i]; + assert(varScope->vsdLifeBeg == 0); + + 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) 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 52e9e298f3684b..9b5832196811e0 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection.Metadata; using System.Reflection.PortableExecutable; using ILCompiler.ReadyToRun.Tests.TestCasesRunner; using ILCompiler.Reflection.ReadyToRun; @@ -458,6 +459,12 @@ public void WasmWebcilModule() static void ValidateDefault(ReadyToRunReader reader) { + const byte WasmI32 = 0x7F; + const byte WasmF64 = 0x7C; + const byte WasmV128 = 0x7B; + const int WasmEncodedFrameBase = 2; + const int WasmLocalRegisterBase = 3; + var webcilReader = Assert.IsType(reader.CompositeReader); WasmR2RAssert.AssertFrameIdentityPrologs(webcilReader); Assert.True(webcilReader.IsWasmWrapped); @@ -467,12 +474,157 @@ static void ValidateDefault(ReadyToRunReader reader) List methods = R2RAssert.GetAllMethods(reader); Assert.True(methods.Exists(method => method.SignatureString.Contains("AddIntegers", StringComparison.Ordinal))); + + // The variable debug-info checks validate the encoding contract rather than exact + // codegen: register records must name a declared wasm local of the expected type, and + // stack records must be frame-relative slots. Native offsets, frame layout, and + // instruction selection are deliberately not pinned. + foreach (ReadyToRunMethod method in methods) + { + foreach (RuntimeFunction runtimeFunction in method.RuntimeFunctions) + { + if (runtimeFunction.DebugInfo is DebugInfo debugInfo) + { + AssertValidRecords(debugInfo.VariablesList, ResolveWasmBody(reader, webcilReader, runtimeFunction)); + } + } + } + + // Optimized code keeps tracked variables, including vectors, in wasm locals. + ReadyToRunMethod optimizedTrackedVariables = GetMethod(methods, "OptimizedTrackedVariables"); + RuntimeFunction optimizedRoot = GetRoot(optimizedTrackedVariables); + WebcilImageReader.WasmFunctionInfo optimizedBody = ResolveWasmBody(reader, webcilReader, optimizedRoot); + AssertRegisterRecord(optimizedRoot, optimizedBody, VariableType.Parameter, 0, WasmI32); + AssertRegisterRecord(optimizedRoot, optimizedBody, VariableType.Parameter, 1, WasmI32); + AssertRegisterRecord(optimizedRoot, optimizedBody, VariableType.Local, 0, WasmI32); + ReadyToRunMethod optimizedVectorVariables = GetMethod(methods, "OptimizedVectorVariables"); + RuntimeFunction optimizedVectorRoot = GetRoot(optimizedVectorVariables); + WebcilImageReader.WasmFunctionInfo optimizedVectorBody = + ResolveWasmBody(reader, webcilReader, optimizedVectorRoot); + AssertRegisterRecord(optimizedVectorRoot, optimizedVectorBody, VariableType.Parameter, 0, WasmV128); + AssertRegisterRecord(optimizedVectorRoot, optimizedVectorBody, VariableType.Parameter, 1, WasmV128); + AssertRegisterRecord(optimizedVectorRoot, optimizedVectorBody, VariableType.Local, 0, WasmV128); + + // Unoptimized code receives parameters in wasm locals and then homes them to the frame. + ReadyToRunMethod leafFrameLocal = GetMethod(methods, "LeafFrameLocal"); + Assert.Equal(new[] { "int" }, leafFrameLocal.LocalSignature); + RuntimeFunction leafRoot = GetRoot(leafFrameLocal); + WebcilImageReader.WasmFunctionInfo leafBody = ResolveWasmBody(reader, webcilReader, leafRoot); + AssertHomedParameter(leafRoot, leafBody, 0, WasmI32, VarLocType.VLT_STK); + NativeVarInfo leafLocal = AssertStackRecord(leafRoot, VariableType.Local, 0, VarLocType.VLT_STK); + AssertReferencesFrameOffset(leafBody, leafLocal.VariableLocation.Data2); + + // localloc gives the root a frame pointer distinct from SP; stack records remain + // relative to the logical frame. + ReadyToRunMethod locallocFrameLocal = GetMethod(methods, "LocallocFrameLocal"); + Assert.Equal(new[] { "int" }, locallocFrameLocal.LocalSignature); + RuntimeFunction locallocRoot = GetRoot(locallocFrameLocal); + WebcilImageReader.WasmFunctionInfo locallocBody = ResolveWasmBody(reader, webcilReader, locallocRoot); + NativeVarInfo locallocParameter = AssertHomedParameter(locallocRoot, locallocBody, 0, WasmI32, VarLocType.VLT_STK); + AssertReferencesFrameOffset(locallocBody, locallocParameter.VariableLocation.Data2); + AssertStackRecord(locallocRoot, VariableType.Local, 0, VarLocType.VLT_STK); + + // Floating-point values use the same local encoding, and a double homed to the frame + // occupies two 32-bit slots. + ReadyToRunMethod addDoubles = GetMethod(methods, "AddDoubles"); + RuntimeFunction addDoublesRoot = GetRoot(addDoubles); + WebcilImageReader.WasmFunctionInfo addDoublesBody = ResolveWasmBody(reader, webcilReader, addDoublesRoot); + AssertHomedParameter(addDoublesRoot, addDoublesBody, 0, WasmF64, VarLocType.VLT_STK2); + AssertHomedParameter(addDoublesRoot, addDoublesBody, 1, WasmF64, VarLocType.VLT_STK2); + // 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 = GetMethod(methods, "SumStaticData"); + RuntimeFunction sumStaticRoot = GetRoot(sumStaticData); + AssertRegisterRecord( + sumStaticRoot, + ResolveWasmBody(reader, webcilReader, sumStaticRoot), + VariableType.Parameter, + 0, + WasmI32); + // 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))); + // Variable records describe the root method's frame; funclets do not report scopes. + ReadyToRunMethod sumWithFinally = GetMethod(methods, "SumWithFinally"); + RuntimeFunction sumWithFinallyRoot = GetRoot(sumWithFinally); + AssertHomedParameter( + sumWithFinallyRoot, + ResolveWasmBody(reader, webcilReader, sumWithFinallyRoot), + 0, + WasmI32, + VarLocType.VLT_STK); + AssertStackRecord(sumWithFinallyRoot, VariableType.Local, 0, VarLocType.VLT_STK); + Assert.Single(sumWithFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + + // A GC reference kept live in the parent frame across a GC reached from a finally + // funclet. The funclet addresses the same frame slot through the parent frame. + ReadyToRunMethod gcLocalAcrossFinally = GetMethod(methods, "GcLocalAcrossFinally"); + Assert.Equal( + new[] { "Webcil.WasmWebcilModule+GcMarker", "int" }, + gcLocalAcrossFinally.LocalSignature); + RuntimeFunction gcLocalRoot = GetRoot(gcLocalAcrossFinally); + AssertHomedParameter( + gcLocalRoot, + ResolveWasmBody(reader, webcilReader, gcLocalRoot), + 0, + WasmI32, + VarLocType.VLT_STK); + NativeVarInfo gcMarker = AssertStackRecord(gcLocalRoot, VariableType.Local, 0, VarLocType.VLT_STK); + AssertStackRecord(gcLocalRoot, VariableType.Local, 1, VarLocType.VLT_STK); + AssertFrameGcSlot(gcLocalAcrossFinally, gcMarker); + AssertSafepointCovered(gcLocalAcrossFinally, gcLocalRoot, gcMarker); + RuntimeFunction gcLocalFunclet = Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + AssertReferencesFrameOffset( + ResolveWasmBody(reader, webcilReader, gcLocalFunclet), + gcMarker.VariableLocation.Data2); + + ReadyToRunMethod locallocAcrossFinally = GetMethod(methods, "LocallocAcrossFinally"); + Assert.Equal(new[] { "int", "int*", "int" }, locallocAcrossFinally.LocalSignature); + RuntimeFunction locallocFinallyRoot = GetRoot(locallocAcrossFinally); + WebcilImageReader.WasmFunctionInfo locallocFinallyBody = + ResolveWasmBody(reader, webcilReader, locallocFinallyRoot); + NativeVarInfo locallocFinallyParameter = + AssertHomedParameter(locallocFinallyRoot, locallocFinallyBody, 0, WasmI32, VarLocType.VLT_STK); + AssertReferencesFrameOffset(locallocFinallyBody, locallocFinallyParameter.VariableLocation.Data2); + for (int localIndex = 0; localIndex < 3; localIndex++) + { + AssertStackRecord(locallocFinallyRoot, VariableType.Local, localIndex, VarLocType.VLT_STK); + } + Assert.Single(locallocAcrossFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + + // Same-typed GC locals, including a null reference, keep distinct frame slots. Wasm opens + // every method-wide scope in the first emitted block, so the locals share one range. + ReadyToRunMethod gcSlotIdentity = GetMethod(methods, "GcSlotIdentity"); + Assert.Equal( + new[] + { + "Webcil.WasmWebcilModule+GcMarker", + "Webcil.WasmWebcilModule+GcMarker", + "Webcil.WasmWebcilModule+GcMarker", + "int", + }, + gcSlotIdentity.LocalSignature); + RuntimeFunction gcSlotRoot = GetRoot(gcSlotIdentity); + NativeVarInfo[] slotLocals = new NativeVarInfo[4]; + for (int localIndex = 0; localIndex < slotLocals.Length; localIndex++) + { + slotLocals[localIndex] = AssertStackRecord(gcSlotRoot, VariableType.Local, localIndex, VarLocType.VLT_STK); + } + Assert.All(slotLocals, local => + { + Assert.Equal(slotLocals[0].StartOffset, local.StartOffset); + Assert.Equal(slotLocals[0].EndOffset, local.EndOffset); + }); + Assert.Equal(slotLocals.Length, slotLocals.Select(local => local.VariableLocation.Data2).Distinct().Count()); + NativeVarInfo[] markers = slotLocals[..3]; + foreach (NativeVarInfo marker in markers) + { + AssertFrameGcSlot(gcSlotIdentity, marker); + } + AssertSafepointCovered(gcSlotIdentity, gcSlotRoot, markers); + // 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 => @@ -494,6 +646,230 @@ static void ValidateDefault(ReadyToRunReader reader) out string funcletDiagnostic), funcletDiagnostic); + static ReadyToRunMethod GetMethod(List methods, string name) + => Assert.Single(methods, method => method.SignatureString.Contains(name, StringComparison.Ordinal)); + + static RuntimeFunction GetRoot(ReadyToRunMethod method) + => Assert.Single(method.RuntimeFunctions, runtimeFunction => runtimeFunction.DebugInfo is not null); + + static void AssertValidRecords(List records, WebcilImageReader.WasmFunctionInfo body) + { + foreach (NativeVarInfo record in records) + { + Assert.True( + record.StartOffset < record.EndOffset, + $"Variable {record.VariableNumber} has an empty range [{record.StartOffset:X}, {record.EndOffset:X})."); + switch (record.VariableLocation.VarLocType) + { + case VarLocType.VLT_REG: + Assert.True(record.VariableLocation.Data1 >= WasmLocalRegisterBase); + GetWasmLocalType(body, record.VariableLocation.Data1 - WasmLocalRegisterBase); + break; + case VarLocType.VLT_STK: + case VarLocType.VLT_STK2: + Assert.Equal(WasmEncodedFrameBase, record.VariableLocation.Data1); + break; + default: + Assert.Fail($"Unexpected wasm variable location {record.VariableLocation.VarLocType}."); + break; + } + } + + foreach (IGrouping variable in records.GroupBy(record => record.VariableNumber)) + { + NativeVarInfo[] ranges = variable.OrderBy(record => record.StartOffset).ToArray(); + for (int i = 1; i < ranges.Length; i++) + { + Assert.True( + ranges[i - 1].EndOffset <= ranges[i].StartOffset, + $"Variable {variable.Key} has overlapping ranges."); + } + } + } + + static NativeVarInfo AssertRegisterRecord( + RuntimeFunction root, + WebcilImageReader.WasmFunctionInfo body, + VariableType variableType, + int variableIndex, + byte wasmType) + { + NativeVarInfo[] records = root.DebugInfo!.VariablesList + .Where(record => record.Variable.Type == variableType + && record.Variable.Index == variableIndex + && record.VariableLocation.VarLocType == VarLocType.VLT_REG) + .OrderBy(record => record.StartOffset) + .ToArray(); + Assert.NotEmpty(records); + Assert.All(records, record => Assert.Equal( + wasmType, + GetWasmLocalType(body, record.VariableLocation.Data1 - WasmLocalRegisterBase))); + return records[0]; + } + + static NativeVarInfo AssertStackRecord( + RuntimeFunction root, + VariableType variableType, + int variableIndex, + VarLocType locationType) + => Assert.Single(root.DebugInfo!.VariablesList, record => + record.Variable.Type == variableType + && record.Variable.Index == variableIndex + && record.VariableLocation.VarLocType == locationType); + + static NativeVarInfo AssertHomedParameter( + RuntimeFunction root, + WebcilImageReader.WasmFunctionInfo body, + int parameterIndex, + byte wasmType, + VarLocType stackLocationType) + { + NativeVarInfo inLocal = AssertRegisterRecord(root, body, VariableType.Parameter, parameterIndex, wasmType); + Assert.Equal(0u, inLocal.StartOffset); + NativeVarInfo onFrame = AssertStackRecord(root, VariableType.Parameter, parameterIndex, stackLocationType); + Assert.True(inLocal.EndOffset <= onFrame.StartOffset); + return onFrame; + } + + static byte GetWasmLocalType(WebcilImageReader.WasmFunctionInfo body, int localIndex) + { + if (localIndex < body.ParamTypes.Count) + { + return body.ParamTypes[localIndex]; + } + + int remaining = localIndex - body.ParamTypes.Count; + foreach ((uint count, byte valType) in body.Locals) + { + if (remaining < count) + { + return valType; + } + + remaining -= (int)count; + } + + Assert.Fail($"Wasm local {localIndex} is not declared by the function."); + return 0; + } + + static void AssertReferencesFrameOffset(WebcilImageReader.WasmFunctionInfo body, int frameOffset) + { + // Look for a load or store whose memarg offset addresses the frame slot. + const byte FirstMemoryAccess = 0x28; + const byte LastMemoryAccess = 0x3E; + ReadOnlySpan code = body.Image.AsSpan(body.InstructionOffset, body.InstructionLength); + for (int i = 0; i < code.Length; i++) + { + if (code[i] is >= FirstMemoryAccess and <= LastMemoryAccess + && TryReadUnsignedLeb128(code, i + 1, out _, out int offsetPosition) + && TryReadUnsignedLeb128(code, offsetPosition, out ulong memoryOffset, out _) + && memoryOffset == (ulong)frameOffset) + { + return; + } + } + + Assert.Fail($"Expected a wasm load or store at frame offset 0x{frameOffset:X}."); + } + + static bool TryReadUnsignedLeb128(ReadOnlySpan code, int position, out ulong value, out int next) + { + value = 0; + for (int shift = 0; position < code.Length && shift < 64; shift += 7) + { + byte b = code[position++]; + value |= (ulong)(b & 0x7F) << shift; + if ((b & 0x80) == 0) + { + next = position; + return true; + } + } + + next = position; + return false; + } + + static void AssertFrameGcSlot(ReadyToRunMethod method, NativeVarInfo record) + { + ILCompiler.Reflection.ReadyToRun.Amd64.GcInfo gcInfo = + Assert.IsType(method.GcInfo); + Assert.Equal(0u, gcInfo.SlotTable.NumRegisters); + ILCompiler.Reflection.ReadyToRun.Amd64.GcSlotTable.GcSlot slot = Assert.Single( + gcInfo.SlotTable.GcSlots, + candidate => candidate.StackSlot?.SpOffset == record.VariableLocation.Data2); + Assert.Equal(GcStackSlotBase.GC_FRAMEREG_REL, slot.StackSlot.Base); + } + + static void AssertSafepointCovered(ReadyToRunMethod method, RuntimeFunction root, params NativeVarInfo[] records) + { + uint callILOffset = FindCallILOffset(method, "CollectAtGcSafepoint"); + DebugInfoBoundsEntry call = Assert.Single( + root.DebugInfo!.BoundsList, + bound => bound.ILOffset == callILOffset); + Assert.All(records, record => Assert.True( + record.StartOffset <= call.NativeOffset && call.NativeOffset < record.EndOffset, + $"Variable {record.VariableNumber} range [{record.StartOffset:X}, {record.EndOffset:X}) does not cover the safepoint at {call.NativeOffset:X}.")); + } + + static uint FindCallILOffset(ReadyToRunMethod method, string calleeName) + { + const byte CallOpcode = 0x28; + const int MethodDefTable = 0x06; + byte[] il = GetMethodILBytes(method); + System.Reflection.Metadata.MetadataReader metadata = method.ComponentReader.MetadataReader; + int callOffset = -1; + for (int i = 0; i + sizeof(int) < il.Length; i++) + { + if (il[i] != CallOpcode) + { + continue; + } + + int token = BitConverter.ToInt32(il, i + 1); + int row = token & 0xFFFFFF; + if ((token >> 24) != MethodDefTable || row == 0 || row > metadata.MethodDefinitions.Count) + { + continue; + } + + MethodDefinitionHandle callee = System.Reflection.Metadata.Ecma335.MetadataTokens.MethodDefinitionHandle(row); + if (metadata.StringComparer.Equals(metadata.GetMethodDefinition(callee).Name, calleeName)) + { + Assert.Equal(-1, callOffset); + callOffset = i; + } + } + + Assert.True(callOffset >= 0, $"Expected a call to {calleeName}."); + return (uint)callOffset; + } + + 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); + } + // 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 @@ -940,6 +1316,18 @@ static void Validate(ReadyToRunReader reader) } } + [Theory] + [InlineData(0, "PC")] + [InlineData(2, "ambient SP")] + [InlineData(3, "$0")] + [InlineData(4, "$1")] + [InlineData(0x2000_0002, "$536870911")] + [InlineData(-1, "Unknown '-1'")] + 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 222bc392d603df..1ef6027d73662c 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 @@ -4,6 +4,7 @@ using System; using System.Reflection; using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; namespace Webcil; @@ -100,6 +101,16 @@ public struct GenericTokenValue 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) { @@ -116,6 +127,62 @@ public static void FoldableBodyTwo() { } + [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)] + public static Vector128 OptimizedVectorVariables(Vector128 left, Vector128 right) + { + Vector128 sum = left + right; + if (sum.GetElement(0) < 0) + { + return AddVectors(sum, right); + } + + Vector128 difference = left - right; + return AddVectors(sum, difference); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + public static Vector128 AddVectors(Vector128 left, Vector128 right) + { + return left + right; + } + + [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; + } + // Explicit construction avoids distinct compiler-generated delegate cache fields. [MethodImpl(MethodImplOptions.NoInlining)] public static MethodInfo SelfMethodOne() => new Func(SelfMethodOne).Method; @@ -154,6 +221,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 CatchExceptionOne(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..280bf9f757f2fb 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs @@ -87,12 +87,27 @@ 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_LOCAL_REGNUM_BASE in cordebuginfo.h. + const int WasmLocalRegisterBase = 3; + + return regnum switch + { + 0 => "PC", + 1 => "REGNUM_COUNT", + 2 => "ambient SP", + >= WasmLocalRegisterBase => $"${regnum - WasmLocalRegisterBase}", + _ => $"Unknown '{regnum}'", + }; + } + private void EnsureInitialized() { if (_boundsList != null)