From e8a353f9c138cfb7c87bbcc5596778cd708d5d87 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Wed, 2 Sep 2026 09:28:31 -0500 Subject: [PATCH 1/6] [wasm] Preserve R2R variable debug info Preserve variable debug information produced by RyuJIT for ReadyToRun WebAssembly code, including scope ranges across relooper block ordering and packed wasm local register locations. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/codegen.h | 6 ++ src/coreclr/jit/compiler.cpp | 7 -- src/coreclr/jit/scopeinfo.cpp | 86 +++++++++++++++++-- .../TestCases/R2RTestSuites.cs | 33 ++++++- .../TestCases/Webcil/WasmWebcilModule.cs | 7 ++ .../DebugInfo.cs | 25 +++++- 6 files changed, 147 insertions(+), 17 deletions(-) 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/scopeinfo.cpp b/src/coreclr/jit/scopeinfo.cpp index edae8a122f9693..ae7ee24d155f4d 100644 --- a/src/coreclr/jit/scopeinfo.cpp +++ b/src/coreclr/jit/scopeinfo.cpp @@ -208,6 +208,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 +495,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 +1628,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 +1720,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..85f46fb4fa0acd 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -126,12 +126,28 @@ static void Validate(ReadyToRunReader reader) List methods = R2RAssert.GetAllMethods(reader); Assert.True(methods.Exists(method => method.SignatureString.Contains("AddIntegers", StringComparison.Ordinal))); + 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.DoesNotContain(doubleVariables, + variable => variable.VariableLocation.VarLocType is VarLocType.VLT_REG_FP or VarLocType.VLT_FPSTK); + Assert.Contains(doubleVariables, variable => + variable.VariableLocation.VarLocType == VarLocType.VLT_REG && + (uint)variable.VariableLocation.Data1 >> WasmRegTypeShift == F64WasmValueType); // 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))); // 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)); + Assert.Contains(sumWithFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.DebugInfo is not null && + runtimeFunction.DebugInfo.VariablesList.Exists(variable => variable.Variable.Type == VariableType.Local)); // 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 => @@ -411,6 +427,19 @@ static void Validate(ReadyToRunReader reader) } } + [Theory] + [InlineData(2, "ambient SP")] + [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)")] + 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..204b026268da07 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 @@ -17,6 +17,13 @@ public static int AddIntegers(int left, int right) return left + right; } + [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) diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs index 58cb620fe98558..768731c4303b63 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs @@ -87,12 +87,35 @@ 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) + { + 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) From 3979515fee3b9d3ef5495932dd423a351bb1fd99 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 01:10:07 -0500 Subject: [PATCH 2/6] [wasm] Exclude portable entry pointer from debug variables The end-to-end variable-debug-info validation exposed the hidden wasm portable-entry-pointer argument as a source local. The argument is appended after user arguments, but unlike the wasm stack-pointer argument it was not recorded or excluded by compMap2ILvarNum. AddDoubles therefore reported the hidden i32 argument as source local 0, alongside the real f64 parameters. Record the argument's local number when it is created, map it to UNKNOWN_ILNUM, and account for it when mapping later internal locals back to IL variable numbers. Replace the count-only wasm R2R checks with complete exact records for the AddDoubles parameters and SumWithFinally local: variable identity, native range, location kind, packed wasm local, and frame-pointer-relative offset. Mutate the local-index bits of one packed register and prove the exact oracle rejects the corrupted record. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/jit/compiler.h | 3 +- src/coreclr/jit/lclvars.cpp | 16 +- .../TestCases/R2RTestSuites.cs | 142 +++++++++++++++++- 3 files changed, 149 insertions(+), 12 deletions(-) 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/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 85f46fb4fa0acd..275de8e98f2d37 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -134,20 +134,129 @@ static void Validate(ReadyToRunReader reader) .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); - Assert.Contains(doubleVariables, variable => - variable.VariableLocation.VarLocType == VarLocType.VLT_REG && - (uint)variable.VariableLocation.Data1 >> WasmRegTypeShift == F64WasmValueType); + 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: 2, + 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: 2, + 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))); // Has a try/finally, so the JIT materializes the table base via a well-known-global global.get. ReadyToRunMethod sumWithFinally = Assert.Single(methods, method => method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal)); - Assert.Contains(sumWithFinally.RuntimeFunctions, runtimeFunction => - runtimeFunction.DebugInfo is not null && - runtimeFunction.DebugInfo.VariablesList.Exists(variable => variable.Variable.Type == VariableType.Local)); + 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: 2, + 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: 2, + data2: 0x24, + data3: 0)); + Assert.Single(sumWithFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); // 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 => @@ -155,6 +264,27 @@ runtimeFunction.DebugInfo is not null && 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; + // 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 From 9829600506374018ffe024c46a0f244054f59ec1 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 12:45:40 -0500 Subject: [PATCH 3/6] [wasm] Centralize variable debug register encoding constants Define the packed WASM debug-register bit layout in ICorDebugInfo and have the JIT derive its register masks from that shared encoding contract. Assert that the JIT register representation and WasmValueType count remain compatible with the debug-info format. Document that the static ReadyToRun reader's compiled-in shift must move with a versioned R2R debug-info format change, since it has no live target descriptor from which to discover a different layout. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/cordebuginfo.h | 9 +++++++++ src/coreclr/jit/registeropswasm.cpp | 6 ++++-- .../aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) 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/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/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs index 768731c4303b63..bab9e18b8aab47 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/DebugInfo.cs @@ -95,6 +95,9 @@ public static string GetPlatformSpecificRegister(Machine machine, int regnum) 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; From bf831077c6d51ee05f570f86ec534f52a058747c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 13:45:16 -0500 Subject: [PATCH 4/6] [wasm] Advertise variable debug register encoding Publish the shared WASM register type shift and value-type count through the target data descriptor so version-skewed readers can reject incompatible variable debug information. Document the producer-owned encoding and extend the static ReadyToRun reader coverage for reserved and unsupported value-type codes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/DebugInfo.md | 36 +++++++++++++++++++ .../TestCases/R2RTestSuites.cs | 2 ++ .../vm/datadescriptor/datadescriptor.inc | 2 ++ 3 files changed, 40 insertions(+) diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index cf83e203cac90d..c3c40da8c5e833 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -223,6 +223,42 @@ 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. + ### 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/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs index 275de8e98f2d37..a4f991ccf85422 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -559,12 +559,14 @@ 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)); 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 From bec5e78930dd69e0c1698dc42d70af09645b562d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 15:13:49 -0500 Subject: [PATCH 5/6] [wasm] Validate frame-resident GC variable debug info Add a no-opt object local that remains live across a GC call in a finally funclet. Assert its IL class type, complete ReadyToRun variable tuples, frame-relative GC slot, and safepoint coverage. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../TestCases/R2RTestSuites.cs | 90 ++++++++++++++++++- .../TestCases/Webcil/WasmWebcilModule.cs | 33 +++++++ 2 files changed, 119 insertions(+), 4 deletions(-) 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 a4f991ccf85422..9b4b9dbed37920 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun.Tests/TestCases/R2RTestSuites.cs @@ -130,6 +130,7 @@ static void Validate(ReadyToRunReader reader) method.SignatureString.Contains("AddDoubles", StringComparison.Ordinal)); const int WasmRegTypeShift = 29; const uint F64WasmValueType = 4; + const int WasmEncodedFrameBase = 2; List doubleVariables = addDoubles.RuntimeFunctions .Where(runtimeFunction => runtimeFunction.DebugInfo is not null) .SelectMany(runtimeFunction => runtimeFunction.DebugInfo!.VariablesList) @@ -158,7 +159,7 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x22, endOffset: 0x34, locationType: VarLocType.VLT_STK2, - data1: 2, + data1: WasmEncodedFrameBase, data2: 0x18, data3: 0)); Assert.Single(doubleVariables, variable => @@ -182,7 +183,7 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x22, endOffset: 0x34, locationType: VarLocType.VLT_STK2, - data1: 2, + data1: WasmEncodedFrameBase, data2: 0x10, data3: 0)); Assert.All( @@ -240,7 +241,7 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x3B, endOffset: 0x1F2, locationType: VarLocType.VLT_STK, - data1: 2, + data1: WasmEncodedFrameBase, data2: 0x2C, data3: 0)); Assert.Single(sumWithFinallyRoot.DebugInfo.VariablesList, variable => @@ -252,11 +253,92 @@ static void Validate(ReadyToRunReader reader) startOffset: 0x3B, endOffset: 0x1F2, locationType: VarLocType.VLT_STK, - data1: 2, + 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); + Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => + runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); + + // Wasm frame homes are addressed through the producer's logical frame pointer. The + // current wire encoding collapses REG_FPBASE, REG_SPBASE, and REGNUM_AMBIENT_SP to 2, + // so the consumer must distinguish FP from SP using the live frame context. + 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); // 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 => 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 204b026268da07..aa25f80544c4a9 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,6 +11,16 @@ 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) { @@ -55,6 +65,29 @@ 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 int CatchException(int value) { From 2ea1ed1481d412fa5f461f1b355ce8e51a985dde Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 14 Sep 2026 16:43:57 -0500 Subject: [PATCH 6/6] [wasm] Validate optimized and frame variable records Add exact optimized tracked-variable coverage, frame-base ABI variations including localloc and funclets, and same-type GC slot identity with a legitimate null reference. Pin the current stack VarLoc base encoding and document that absolute frame reconstruction is independent of unstable wasm local indices. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/DebugInfo.md | 25 + src/coreclr/jit/scopeinfo.cpp | 8 + .../TestCases/R2RTestSuites.cs | 470 +++++++++++++++++- .../TestCases/Webcil/WasmWebcilModule.cs | 77 +++ 4 files changed, 573 insertions(+), 7 deletions(-) diff --git a/docs/design/datacontracts/DebugInfo.md b/docs/design/datacontracts/DebugInfo.md index c3c40da8c5e833..f079e1d7e084a3 100644 --- a/docs/design/datacontracts/DebugInfo.md +++ b/docs/design/datacontracts/DebugInfo.md @@ -259,6 +259,31 @@ specification type set. Managed references currently use the JIT's machine `I32` 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/jit/scopeinfo.cpp b/src/coreclr/jit/scopeinfo.cpp index ae7ee24d155f4d..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 //============================================================================ 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 9b4b9dbed37920..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,11 +137,184 @@ 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; - const int WasmEncodedFrameBase = 2; List doubleVariables = addDoubles.RuntimeFunctions .Where(runtimeFunction => runtimeFunction.DebugInfo is not null) .SelectMany(runtimeFunction => runtimeFunction.DebugInfo!.VariablesList) @@ -211,8 +395,30 @@ static void Validate(ReadyToRunReader reader) 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. ReadyToRunMethod sumWithFinally = Assert.Single(methods, method => method.SignatureString.Contains("SumWithFinally", StringComparison.Ordinal)); @@ -326,12 +532,12 @@ static void Validate(ReadyToRunReader reader) 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); - Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => + RuntimeFunction gcLocalFunclet = Assert.Single(gcLocalAcrossFinally.RuntimeFunctions, runtimeFunction => runtimeFunction.WasmIsFunclet && runtimeFunction.DebugInfo is null); - // Wasm frame homes are addressed through the producer's logical frame pointer. The - // current wire encoding collapses REG_FPBASE, REG_SPBASE, and REGNUM_AMBIENT_SP to 2, - // so the consumer must distinguish FP from SP using the live frame context. + // 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); @@ -339,6 +545,210 @@ static void Validate(ReadyToRunReader reader) 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 => @@ -367,6 +777,52 @@ static bool IsExactVariable( && 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 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 aa25f80544c4a9..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 @@ -27,6 +27,36 @@ 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) { @@ -88,6 +118,53 @@ 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) {