From 7eec7f7fa04f6c2808580c83c011538b08e49faf Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Tue, 14 Apr 2026 19:38:45 -0700 Subject: [PATCH 01/16] Checkpoint gcinfo wire-up Checkpoint Checkpoint Comment out some overly sensitive asserts that can be hit when things are working as designed Funclet start blocks need labels on Wasm for some reason. Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Cleanup jit-format Compile the gc info decoder too, since we'll need it Add comment --- src/coreclr/gcinfo/CMakeLists.txt | 1 + src/coreclr/gcinfo/gcinfoencoder.cpp | 2 - src/coreclr/inc/gcinfotypes.h | 51 ++++++++++++++++++- src/coreclr/jit/CMakeLists.txt | 4 +- src/coreclr/jit/codegencommon.cpp | 18 ++++--- src/coreclr/jit/codegenlinear.cpp | 20 +++++--- src/coreclr/jit/codegenwasm.cpp | 37 +++++++++++++- src/coreclr/jit/emit.cpp | 16 +++--- src/coreclr/jit/gcencode.cpp | 2 + src/coreclr/jit/gcinfo.cpp | 4 +- src/coreclr/jit/targetwasm.h | 2 +- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 11 +++- src/coreclr/vm/eetwain.cpp | 5 +- src/coreclr/vm/gcinfodecoder.cpp | 2 - 14 files changed, 140 insertions(+), 35 deletions(-) diff --git a/src/coreclr/gcinfo/CMakeLists.txt b/src/coreclr/gcinfo/CMakeLists.txt index a652648f32a689..05521608a09a73 100644 --- a/src/coreclr/gcinfo/CMakeLists.txt +++ b/src/coreclr/gcinfo/CMakeLists.txt @@ -66,6 +66,7 @@ endif() if (CLR_CMAKE_TARGET_ARCH_ARM64 OR CLR_CMAKE_TARGET_ARCH_AMD64) create_gcinfo_lib(TARGET gcinfo_universal_arm64 OS universal ARCH arm64) create_gcinfo_lib(TARGET gcinfo_unix_x64 OS unix ARCH x64) + create_gcinfo_lib(TARGET gcinfo_universal_wasm OS universal ARCH wasm) if (CLR_CMAKE_BUILD_COMMUNITY_ALTJITS EQUAL 1) create_gcinfo_lib(TARGET gcinfo_unix_loongarch64 OS unix ARCH loongarch64) create_gcinfo_lib(TARGET gcinfo_unix_riscv64 OS unix ARCH riscv64) diff --git a/src/coreclr/gcinfo/gcinfoencoder.cpp b/src/coreclr/gcinfo/gcinfoencoder.cpp index e877fb534cb163..2a0636e11dbb7e 100644 --- a/src/coreclr/gcinfo/gcinfoencoder.cpp +++ b/src/coreclr/gcinfo/gcinfoencoder.cpp @@ -2634,10 +2634,8 @@ int BitStreamWriter::EncodeVarLengthSigned( SSIZE_T n, UINT32 base ) } } -#ifndef TARGET_WASM // Instantiate the encoder so other files can use it template class TGcInfoEncoder; -#endif // !TARGET_WASM #ifdef FEATURE_INTERPRETER template class TGcInfoEncoder; diff --git a/src/coreclr/inc/gcinfotypes.h b/src/coreclr/inc/gcinfotypes.h index dc56c94477db0e..68aa15eae549df 100644 --- a/src/coreclr/inc/gcinfotypes.h +++ b/src/coreclr/inc/gcinfotypes.h @@ -909,13 +909,60 @@ struct X86GcInfoEncoding { static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; }; -#elif defined(TARGET_WASM) +#elif defined(TARGET_WASM) && !defined(TARGET_64BIT) #ifndef TARGET_POINTER_SIZE #define TARGET_POINTER_SIZE 4 // equal to sizeof(void*) and the managed pointer size in bytes for this target #endif -#define TargetGcInfoEncoding InterpreterGcInfoEncoding +#define TargetGcInfoEncoding Wasm32GcInfoEncoding + +struct Wasm32GcInfoEncoding { + static const uint32_t NUM_NORM_CODE_OFFSETS_PER_CHUNK = (64); + static const uint32_t NUM_NORM_CODE_OFFSETS_PER_CHUNK_LOG2 = (6); + static inline constexpr int32_t NORMALIZE_STACK_SLOT (int32_t x) { return (x); } + static inline constexpr int32_t DENORMALIZE_STACK_SLOT (int32_t x) { return (x); } + static inline constexpr uint32_t NORMALIZE_CODE_LENGTH (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_CODE_LENGTH (uint32_t x) { return (x); } + static inline constexpr uint32_t NORMALIZE_STACK_BASE_REGISTER (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_STACK_BASE_REGISTER (uint32_t x) { return (x); } + static inline constexpr uint32_t NORMALIZE_SIZE_OF_STACK_AREA (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_SIZE_OF_STACK_AREA (uint32_t x) { return (x); } + static const bool CODE_OFFSETS_NEED_NORMALIZATION = false; + static inline constexpr uint32_t NORMALIZE_CODE_OFFSET (uint32_t x) { return (x); } + static inline constexpr uint32_t DENORMALIZE_CODE_OFFSET (uint32_t x) { return (x); } + + static const int PSP_SYM_STACK_SLOT_ENCBASE = 6; + static const int GENERICS_INST_CONTEXT_STACK_SLOT_ENCBASE = 6; + static const int SECURITY_OBJECT_STACK_SLOT_ENCBASE = 6; + static const int GS_COOKIE_STACK_SLOT_ENCBASE = 6; + static const int CODE_LENGTH_ENCBASE = 6; + static const int SIZE_OF_RETURN_KIND_IN_SLIM_HEADER = 2; + static const int SIZE_OF_RETURN_KIND_IN_FAT_HEADER = 2; + static const int STACK_BASE_REGISTER_ENCBASE = 3; + static const int SIZE_OF_STACK_AREA_ENCBASE = 6; + static const int SIZE_OF_EDIT_AND_CONTINUE_PRESERVED_AREA_ENCBASE = 3; + static const int REVERSE_PINVOKE_FRAME_ENCBASE = 6; + static const int NUM_REGISTERS_ENCBASE = 3; + static const int NUM_STACK_SLOTS_ENCBASE = 5; + static const int NUM_UNTRACKED_SLOTS_ENCBASE = 5; + static const int NORM_PROLOG_SIZE_ENCBASE = 4; + static const int NORM_EPILOG_SIZE_ENCBASE = 3; + static const int NORM_CODE_OFFSET_DELTA_ENCBASE = 3; + static const int INTERRUPTIBLE_RANGE_DELTA1_ENCBASE = 5; + static const int INTERRUPTIBLE_RANGE_DELTA2_ENCBASE = 5; + static const int REGISTER_ENCBASE = 3; + static const int REGISTER_DELTA_ENCBASE = REGISTER_ENCBASE; + static const int STACK_SLOT_ENCBASE = 6; + static const int STACK_SLOT_DELTA_ENCBASE = 4; + static const int NUM_SAFE_POINTS_ENCBASE = 4; + static const int NUM_INTERRUPTIBLE_RANGES_ENCBASE = 1; + static const int NUM_EH_CLAUSES_ENCBASE = 2; + static const int POINTER_SIZE_ENCBASE = 3; + static const int LIVESTATE_RLE_RUN_ENCBASE = 2; + static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; + static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = false; +}; #else // No target defined diff --git a/src/coreclr/jit/CMakeLists.txt b/src/coreclr/jit/CMakeLists.txt index 2efb2b841bb04d..8779b1e4605f31 100644 --- a/src/coreclr/jit/CMakeLists.txt +++ b/src/coreclr/jit/CMakeLists.txt @@ -23,7 +23,7 @@ function(create_standalone_jit) if(TARGETDETAILS_OS STREQUAL "unix_osx" OR TARGETDETAILS_OS STREQUAL "unix_anyos") set(JIT_ARCH_LINK_LIBRARIES gcinfo_unix_${TARGETDETAILS_ARCH}) - elseif(NOT ${TARGETDETAILS_ARCH} MATCHES "wasm") + else() set(JIT_ARCH_LINK_LIBRARIES gcinfo_${TARGETDETAILS_OS}_${TARGETDETAILS_ARCH}) endif() @@ -292,6 +292,8 @@ set( JIT_RISCV64_SOURCES ) set( JIT_WASM_SOURCES + gcdecode.cpp + gcencode.cpp codegenwasm.cpp emitwasm.cpp fgwasm.cpp diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 5ebd0dd3a83a59..15673e3f594210 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -854,7 +854,9 @@ bool CodeGen::genIsSameLocalVar(GenTree* op1, GenTree* op2) // inline void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bool isDying DEBUGARG(GenTree* tree)) { -#if EMIT_GENERATE_GCINFO // The regset being updated here is only needed for codegen-level GCness tracking +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) + // The regset being updated here is only needed for codegen-level GCness tracking, + // and Wasm does not have registers regMaskTP regMask = genGetRegMask(varDsc); #ifdef DEBUG @@ -884,7 +886,7 @@ void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bo assert(varDsc->IsAlwaysAliveInMemory() || ((regSet.GetMaskVars() & regMask) == 0)); regSet.AddMaskVars(regMask); } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM } #ifndef TARGET_WASM @@ -1032,6 +1034,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) bool isByRef = varDsc->TypeIs(TYP_BYREF); bool isInReg = varDsc->lvIsInReg(); bool isInMemory = !isInReg || varDsc->IsAlwaysAliveInMemory(); +#ifndef TARGET_WASM if (isInReg) { // TODO-Cleanup: Move the code from compUpdateLifeVar to genUpdateRegLife that updates the @@ -1047,7 +1050,8 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) } codeGen->genUpdateRegLife(varDsc, false /*isBorn*/, true /*isDying*/ DEBUGARG(nullptr)); } - // Update the gcVarPtrSetCur if it is in memory. +#endif // !TARGET_WASM + // Update the gcVarPtrSetCur if it is in memory. if (isInMemory && (isGCRef || isByRef)) { VarSetOps::RemoveElemD(this, codeGen->gcInfo.gcVarPtrSetCur, deadVarIndex); @@ -1070,6 +1074,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) bool isByRef = varDsc->TypeIs(TYP_BYREF); if (varDsc->lvIsInReg()) { +#ifndef TARGET_WASM // If this variable is going live in a register, it is no longer live on the stack, // unless it is an EH/"spill at single-def" var, which always remains live on the stack. if (!varDsc->IsAlwaysAliveInMemory()) @@ -1092,6 +1097,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) { codeGen->gcInfo.gcRegByrefSetCur |= regMask; } +#endif // !TARGET_WASM } else if (lvaIsGCTracked(varDsc)) { @@ -1769,7 +1775,7 @@ void CodeGen::genExitCode(BasicBlock* block) genIPmappingAdd(IPmappingDscKind::Epilog, DebugInfo(), true); -#if EMIT_GENERATE_GCINFO && defined(DEBUG) +#if EMIT_GENERATE_GCINFO && defined(DEBUG) && !defined(TARGET_WASM) // For returnining epilogs do some validation that the GC info looks right. if (!block->HasFlag(BBF_HAS_JMP)) { @@ -1791,7 +1797,7 @@ void CodeGen::genExitCode(BasicBlock* block) } } } -#endif // EMIT_GENERATE_GCINFO && defined(DEBUG) +#endif // EMIT_GENERATE_GCINFO && defined(DEBUG) && !defined(TARGET_WASM) if (m_compiler->getNeedsGSSecurityCookie()) { @@ -7202,7 +7208,7 @@ void CodeGen::genReturn(GenTree* treeNode) } } -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) if (treeNode->OperIs(GT_RETURN, GT_SWIFT_ERROR_RET)) { genMarkReturnGCInfo(); diff --git a/src/coreclr/jit/codegenlinear.cpp b/src/coreclr/jit/codegenlinear.cpp index 5386aa65a5d74b..a5a0bdf982b46b 100644 --- a/src/coreclr/jit/codegenlinear.cpp +++ b/src/coreclr/jit/codegenlinear.cpp @@ -259,7 +259,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) // and before first of the current block is emitted genUpdateLife(block->bbLiveIn); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) // Even if liveness didn't change, we need to update the registers containing GC references. // genUpdateLife will update the registers live due to liveness changes. But what about registers that didn't // change? We cleared them out above. Maybe we should just not clear them out, but update the ones that change @@ -353,7 +353,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } } } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM /* Start a new code output block */ @@ -405,6 +405,14 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } #endif +#ifdef TARGET_WASM + // FIXME-WASM: Why is this only necessary on Wasm? + if (m_compiler->bbIsFuncletBeg(block)) + { + needLabel = true; + } +#endif + if (needLabel) { // Mark a label and update the current set of live GC refs @@ -569,7 +577,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) regSet.rsSpillChk(); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) // Make sure we didn't bungle pointer register tracking regMaskTP ptrRegs = gcInfo.gcRegGCrefSetCur | gcInfo.gcRegByrefSetCur; regMaskTP nonVarPtrRegs = ptrRegs & ~regSet.GetMaskVars(); @@ -618,7 +626,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } noway_assert(nonVarPtrRegs == RBM_NONE); -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM #endif // DEBUG #if defined(DEBUG) @@ -1601,7 +1609,7 @@ regNumber CodeGen::genConsumeReg(GenTree* tree) // genUpdateLife() will also spill local var if marked as GTF_SPILL by calling CodeGen::genSpillVar genUpdateLife(tree); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) // there are three cases where consuming a reg means clearing the bit in the live mask // 1. it was not produced by a local // 2. it was produced by a local that is going dead @@ -1659,7 +1667,7 @@ regNumber CodeGen::genConsumeReg(GenTree* tree) { gcInfo.gcMarkRegSetNpt(tree->gtGetRegMask()); } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM genCheckConsumeNode(tree); return tree->GetRegNum(); diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index caaec17c160446..4df06a6e5d22b0 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -9,6 +9,8 @@ #include "codegen.h" #include "regallocwasm.h" #include "fgwasm.h" +#include "gcinfo.h" +#include "gcinfoencoder.h" static const int LINEAR_MEMORY_INDEX = 0; @@ -3351,7 +3353,40 @@ void CodeGen::inst_JMP(emitJumpKind jmp, BasicBlock* tgtBlock) void CodeGen::genCreateAndStoreGCInfo(unsigned codeSize, unsigned prologSize, unsigned epilogSize DEBUGARG(void* code)) { - // GCInfo not captured/created by codegen. + IAllocator* allowZeroAlloc = new (m_compiler, CMK_GC) CompIAllocator(m_compiler->getAllocatorGC()); + GcInfoEncoder* gcInfoEncoder = new (m_compiler, CMK_GC) + GcInfoEncoder(m_compiler->info.compCompHnd, m_compiler->info.compMethodInfo, allowZeroAlloc, NOMEM); + assert(gcInfoEncoder != nullptr); + + // Follow the code pattern of the x86 gc info encoder (genCreateAndStoreGCInfoJIT32). + gcInfo.gcInfoBlockHdrSave(gcInfoEncoder, codeSize, prologSize); + + // We keep the call count for the second call to gcMakeRegPtrTable() below. + unsigned callCnt = 0; + + // First we figure out the encoder ID's for the stack slots and registers. + gcInfo.gcMakeRegPtrTable(gcInfoEncoder, codeSize, prologSize, GCInfo::MAKE_REG_PTR_MODE_ASSIGN_SLOTS, &callCnt); + + // Now we've requested all the slots we'll need; "finalize" these (make more compact data structures for them). + gcInfoEncoder->FinalizeSlotIds(); + + // Now we can actually use those slot ID's to declare live ranges. + gcInfo.gcMakeRegPtrTable(gcInfoEncoder, codeSize, prologSize, GCInfo::MAKE_REG_PTR_MODE_DO_WORK, &callCnt); + + if (m_compiler->opts.IsReversePInvoke()) + { + unsigned reversePInvokeFrameVarNumber = m_compiler->lvaReversePInvokeFrameVar; + assert(reversePInvokeFrameVarNumber != BAD_VAR_NUM); + const LclVarDsc* reversePInvokeFrameVar = m_compiler->lvaGetDesc(reversePInvokeFrameVarNumber); + gcInfoEncoder->SetReversePInvokeFrameSlot(reversePInvokeFrameVar->GetStackOffset()); + } + + gcInfoEncoder->Build(); + + // GC Encoder automatically puts the GC info in the right spot using ICorJitInfo::allocGCInfo(size_t) + // let's save the values anyway for debugging purposes + m_compiler->compInfoBlkAddr = gcInfoEncoder->Emit(); + m_compiler->compInfoBlkSize = gcInfoEncoder->GetEncodedGCInfoSize(); } //--------------------------------------------------------------------- diff --git a/src/coreclr/jit/emit.cpp b/src/coreclr/jit/emit.cpp index 8f36252fb769bc..7b14603d502baa 100644 --- a/src/coreclr/jit/emit.cpp +++ b/src/coreclr/jit/emit.cpp @@ -9076,7 +9076,7 @@ void emitter::emitUpdateLiveGCregs(GCtype gcType, regMaskTP regs, BYTE* addr) return; } -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) regMaskTP life; regMaskTP dead; regMaskTP chg; @@ -9131,7 +9131,7 @@ void emitter::emitUpdateLiveGCregs(GCtype gcType, regMaskTP regs, BYTE* addr) // The 2 GC reg masks can't be overlapping assert((emitThisGCrefRegs & emitThisByrefRegs) == 0); -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM } /***************************************************************************** @@ -9434,7 +9434,7 @@ void emitter::emitGCregLiveUpd(GCtype gcType, regNumber reg, BYTE* addr) { assert(emitIssuing); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) // Don't track GC changes in epilogs if (emitIGisInEpilog(emitCurIG)) { @@ -9476,7 +9476,7 @@ void emitter::emitGCregLiveUpd(GCtype gcType, regNumber reg, BYTE* addr) // The 2 GC reg masks can't be overlapping assert((emitThisGCrefRegs & emitThisByrefRegs) == 0); -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM } /***************************************************************************** @@ -9494,7 +9494,7 @@ void emitter::emitGCregDeadUpdMask(regMaskTP regs, BYTE* addr) return; } -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) // First, handle the gcref regs going dead regMaskTP gcrefRegs = emitThisGCrefRegs & regs; @@ -9530,7 +9530,7 @@ void emitter::emitGCregDeadUpdMask(regMaskTP regs, BYTE* addr) emitThisByrefRegs &= ~byrefRegs; } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM } /***************************************************************************** @@ -9542,7 +9542,7 @@ void emitter::emitGCregDeadUpd(regNumber reg, BYTE* addr) { assert(emitIssuing); -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) // Don't track GC changes in epilogs if (emitIGisInEpilog(emitCurIG)) { @@ -9571,7 +9571,7 @@ void emitter::emitGCregDeadUpd(regNumber reg, BYTE* addr) emitThisByrefRegs &= ~regMask; } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM } /***************************************************************************** diff --git a/src/coreclr/jit/gcencode.cpp b/src/coreclr/jit/gcencode.cpp index 40908eccefa695..1f270c59a976a2 100644 --- a/src/coreclr/jit/gcencode.cpp +++ b/src/coreclr/jit/gcencode.cpp @@ -4646,6 +4646,7 @@ void GCInfo::gcInfoRecordGCRegStateChange(GcInfoEncoder* gcInfoEncoder, regMaskSmall byRefMask, regMaskSmall* pPtrRegs) { +#ifndef TARGET_WASM // Precondition: byRefMask is a subset of regMask. assert((byRefMask & ~regMask) == 0); @@ -4703,6 +4704,7 @@ void GCInfo::gcInfoRecordGCRegStateChange(GcInfoEncoder* gcInfoEncoder, // Turn the bit we've just generated off and continue. regMask ^= tmpMask; // EAX,ECX,EDX,EBX,---,EBP,ESI,EDI } +#endif // !TARGET_WASM } /************************************************************************** diff --git a/src/coreclr/jit/gcinfo.cpp b/src/coreclr/jit/gcinfo.cpp index e687e169e2a049..f26c87cbcbeeee 100644 --- a/src/coreclr/jit/gcinfo.cpp +++ b/src/coreclr/jit/gcinfo.cpp @@ -196,7 +196,7 @@ void GCInfo::gcMarkRegSetNpt(regMaskTP regMask DEBUGARG(bool forceOutput)) void GCInfo::gcMarkRegPtrVal(regNumber reg, var_types type) { -#if EMIT_GENERATE_GCINFO +#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) regMaskTP regMask = genRegMask(reg); switch (type) @@ -211,7 +211,7 @@ void GCInfo::gcMarkRegPtrVal(regNumber reg, var_types type) gcMarkRegSetNpt(regMask); break; } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM } //------------------------------------------------------------------------ diff --git a/src/coreclr/jit/targetwasm.h b/src/coreclr/jit/targetwasm.h index ba00eb9624900d..1262df809f9d3b 100644 --- a/src/coreclr/jit/targetwasm.h +++ b/src/coreclr/jit/targetwasm.h @@ -50,7 +50,7 @@ #define CSE_CONSTS 1 // Enable if we want to CSE constants #define LOWER_DECOMPOSE_LONGS 0 // Decompose TYP_LONG operations into (typically two) TYP_INT ones #define EMIT_TRACK_STACK_DEPTH 0 // No need to track arg pushes/pops -#define EMIT_GENERATE_GCINFO 0 // Codegen and emit not responsible for GC liveness tracking and GCInfo generation +#define EMIT_GENERATE_GCINFO 1 // Codegen and emit generate GC info; on WASM this enables stack slot GC info encoding without fixed-register GC tracking // Since we don't have a fixed register set on WASM, we set most of the following register defines to 'none'-like values. #define REG_FP_FIRST REG_NA diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index 0be908211636f7..ce51f68858eaee 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -1581,10 +1581,15 @@ private ObjectNode.ObjectData EncodeEHInfo() /// private void setVars(CORINFO_METHOD_STRUCT_* ftn, uint cVars, NativeVarInfo* vars) { - Debug.Assert(_debugVarInfos == null); + // FIXME: This can erroneously trigger when a method fails compilation late and we + // successfully retry compilation after the failure. + // Debug.Assert(_debugVarInfos == null); if (cVars == 0) + { + _debugVarInfos = null; return; + } _debugVarInfos = new NativeVarInfo[cVars]; for (int i = 0; i < cVars; i++) @@ -1601,7 +1606,9 @@ private void setVars(CORINFO_METHOD_STRUCT_* ftn, uint cVars, NativeVarInfo* var /// private void setBoundaries(CORINFO_METHOD_STRUCT_* ftn, uint cMap, OffsetMapping* pMap) { - Debug.Assert(_debugLocInfos == null); + // FIXME: This can erroneously trigger when a method fails compilation late and we + // successfully retry compilation after the failure. + // Debug.Assert(_debugLocInfos == null); _debugLocInfos = new OffsetMapping[cMap]; for (int i = 0; i < cMap; i++) diff --git a/src/coreclr/vm/eetwain.cpp b/src/coreclr/vm/eetwain.cpp index 2fef103782ad88..12261ea5d4d492 100644 --- a/src/coreclr/vm/eetwain.cpp +++ b/src/coreclr/vm/eetwain.cpp @@ -712,6 +712,7 @@ bool EECodeManager::IsGcSafe( EECodeInfo *pCodeInfo, return false; } +// FIXME-WASM: Add TARGET_WASM once we implement tail calls on Wasm. #if defined(TARGET_ARM) || defined(TARGET_ARM64) || defined(TARGET_LOONGARCH64) || defined(TARGET_RISCV64) bool EECodeManager::HasTailCalls( EECodeInfo *pCodeInfo) { @@ -1941,7 +1942,7 @@ void InterpreterCodeManager::ResumeAfterCatch(CONTEXT *pContext, size_t targetSS #if defined(HOST_AMD64) && defined(HOST_WINDOWS) targetSSP = pInterpreterFrame->GetInterpExecMethodSSP(); -#endif +#endif ExecuteFunctionBelowContext((PCODE)ThrowResumeAfterCatchException, pContext, targetSSP, resumeSP, resumeIP); #endif // TARGET_WASM } @@ -2112,7 +2113,7 @@ static void VirtualUnwindInterpreterCallFrame(TADDR sp, T_CONTEXT *pContext) else { // This indicates that there are no more interpreter frames to unwind in the current InterpExecMethod - // The stack walker will not find any code manager for the address InterpreterFrame::DummyCallerIP (0) + // The stack walker will not find any code manager for the address InterpreterFrame::DummyCallerIP (0) // and move on to the next explicit frame which is the InterpreterFrame. // The SP is set to the address of the InterpreterFrame. For the case of interpreted exception handling // funclets, this matches the pExInfo->m_csfEHClause.SP that the CallFunclet sets. diff --git a/src/coreclr/vm/gcinfodecoder.cpp b/src/coreclr/vm/gcinfodecoder.cpp index 82842705d217f7..681411ae521851 100644 --- a/src/coreclr/vm/gcinfodecoder.cpp +++ b/src/coreclr/vm/gcinfodecoder.cpp @@ -2274,10 +2274,8 @@ template void TGcInfoDecoder::ReportSt pCallBack(hCallBack, pObjRef, gcFlags DAC_ARG(DacSlotLocation(GetStackReg(spBase), spOffset, true))); } -#ifndef TARGET_WASM // Instantiate the decoder so other files can use it template class TGcInfoDecoder; -#endif // !TARGET_WASM #ifdef FEATURE_INTERPRETER template class TGcInfoDecoder; From bcdbb2ffc63ee117a3dd760eaaaba896b8cbe053 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Mon, 27 Apr 2026 23:18:45 -0700 Subject: [PATCH 02/16] Always disable tracked GC slots for Wasm --- src/coreclr/jit/gcencode.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/coreclr/jit/gcencode.cpp b/src/coreclr/jit/gcencode.cpp index 1f270c59a976a2..c5d5f6b458b8cb 100644 --- a/src/coreclr/jit/gcencode.cpp +++ b/src/coreclr/jit/gcencode.cpp @@ -4091,7 +4091,12 @@ void GCInfo::gcMakeRegPtrTable( { GCENCODER_WITH_LOGGING(gcInfoEncoderWithLog, gcInfoEncoder); + // TODO-WASM: Enable tracked GC slots for precise GC +#ifdef TARGET_WASM + const bool noTrackedGCSlots = true; +#else const bool noTrackedGCSlots = m_compiler->opts.MinOpts(); +#endif if (mode == MAKE_REG_PTR_MODE_ASSIGN_SLOTS) { From e923a5d7fbf6398111a72d4796f42e62a6f7525d Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Tue, 28 Apr 2026 22:30:34 -0700 Subject: [PATCH 03/16] Re-enable asserts since 127185 avoids hitting them --- .../JitInterface/CorInfoImpl.ReadyToRun.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs index ce51f68858eaee..0be908211636f7 100644 --- a/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs +++ b/src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/CorInfoImpl.ReadyToRun.cs @@ -1581,15 +1581,10 @@ private ObjectNode.ObjectData EncodeEHInfo() /// private void setVars(CORINFO_METHOD_STRUCT_* ftn, uint cVars, NativeVarInfo* vars) { - // FIXME: This can erroneously trigger when a method fails compilation late and we - // successfully retry compilation after the failure. - // Debug.Assert(_debugVarInfos == null); + Debug.Assert(_debugVarInfos == null); if (cVars == 0) - { - _debugVarInfos = null; return; - } _debugVarInfos = new NativeVarInfo[cVars]; for (int i = 0; i < cVars; i++) @@ -1606,9 +1601,7 @@ private void setVars(CORINFO_METHOD_STRUCT_* ftn, uint cVars, NativeVarInfo* var /// private void setBoundaries(CORINFO_METHOD_STRUCT_* ftn, uint cMap, OffsetMapping* pMap) { - // FIXME: This can erroneously trigger when a method fails compilation late and we - // successfully retry compilation after the failure. - // Debug.Assert(_debugLocInfos == null); + Debug.Assert(_debugLocInfos == null); _debugLocInfos = new OffsetMapping[cMap]; for (int i = 0; i < cMap; i++) From 9053792d36d93f6ae4ec98a58b62c9ae5ed4568d Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Tue, 28 Apr 2026 22:46:01 -0700 Subject: [PATCH 04/16] Address feedback from SingleAccretion --- src/coreclr/jit/CMakeLists.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/CMakeLists.txt b/src/coreclr/jit/CMakeLists.txt index 8779b1e4605f31..dd83c6891518fe 100644 --- a/src/coreclr/jit/CMakeLists.txt +++ b/src/coreclr/jit/CMakeLists.txt @@ -120,6 +120,8 @@ set( JIT_SOURCES fgstmt.cpp flowgraph.cpp forwardsub.cpp + gcdecode.cpp + gcencode.cpp gcinfo.cpp gentree.cpp gschecks.cpp @@ -186,8 +188,6 @@ set ( JIT_NATIVE_TARGET_SOURCES lsra.cpp lsrabuild.cpp regMaskTPOps.cpp - gcdecode.cpp - gcencode.cpp unwind.cpp ) @@ -292,8 +292,6 @@ set( JIT_RISCV64_SOURCES ) set( JIT_WASM_SOURCES - gcdecode.cpp - gcencode.cpp codegenwasm.cpp emitwasm.cpp fgwasm.cpp From 9f7ec383ef8eca2e1752480f47db61b2cc19f56f Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Wed, 29 Apr 2026 04:25:59 -0700 Subject: [PATCH 05/16] Cleanups --- src/coreclr/jit/codegencommon.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 15673e3f594210..a98d3d5c44aaa7 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -1051,7 +1051,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) codeGen->genUpdateRegLife(varDsc, false /*isBorn*/, true /*isDying*/ DEBUGARG(nullptr)); } #endif // !TARGET_WASM - // Update the gcVarPtrSetCur if it is in memory. + // Update the gcVarPtrSetCur if it is in memory. if (isInMemory && (isGCRef || isByRef)) { VarSetOps::RemoveElemD(this, codeGen->gcInfo.gcVarPtrSetCur, deadVarIndex); @@ -7213,7 +7213,7 @@ void CodeGen::genReturn(GenTree* treeNode) { genMarkReturnGCInfo(); } -#endif // EMIT_GENERATE_GCINFO +#endif // EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) #ifdef PROFILING_SUPPORTED From c82e23063da568df1dac9e5c0bdbfe9291982634 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Wed, 29 Apr 2026 04:26:39 -0700 Subject: [PATCH 06/16] Remove unnecessary workaround --- src/coreclr/jit/codegenlinear.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/coreclr/jit/codegenlinear.cpp b/src/coreclr/jit/codegenlinear.cpp index a5a0bdf982b46b..2c4ba50735b79d 100644 --- a/src/coreclr/jit/codegenlinear.cpp +++ b/src/coreclr/jit/codegenlinear.cpp @@ -405,14 +405,6 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } #endif -#ifdef TARGET_WASM - // FIXME-WASM: Why is this only necessary on Wasm? - if (m_compiler->bbIsFuncletBeg(block)) - { - needLabel = true; - } -#endif - if (needLabel) { // Mark a label and update the current set of live GC refs From 46df7c9cc5a6dec5142e1192f0e53c9b73fe2648 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Wed, 29 Apr 2026 08:44:10 -0700 Subject: [PATCH 07/16] jit-format --- src/coreclr/jit/codegencommon.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index a98d3d5c44aaa7..7985a631138583 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -1051,7 +1051,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife) codeGen->genUpdateRegLife(varDsc, false /*isBorn*/, true /*isDying*/ DEBUGARG(nullptr)); } #endif // !TARGET_WASM - // Update the gcVarPtrSetCur if it is in memory. + // Update the gcVarPtrSetCur if it is in memory. if (isInMemory && (isGCRef || isByRef)) { VarSetOps::RemoveElemD(this, codeGen->gcInfo.gcVarPtrSetCur, deadVarIndex); From 64287267ef900f26dabb7df582d7cb8893828340 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Wed, 29 Apr 2026 23:51:15 -0700 Subject: [PATCH 08/16] Address PR feedback - prevent enregistering GC vars unless they are already enregistered implicitly due to being params; add todo comment --- src/coreclr/inc/gcinfotypes.h | 2 ++ src/coreclr/jit/regalloc.cpp | 10 ---------- src/coreclr/jit/regallocwasm.cpp | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/coreclr/inc/gcinfotypes.h b/src/coreclr/inc/gcinfotypes.h index 68aa15eae549df..8419ecd011f9e5 100644 --- a/src/coreclr/inc/gcinfotypes.h +++ b/src/coreclr/inc/gcinfotypes.h @@ -917,6 +917,8 @@ struct X86GcInfoEncoding { #define TargetGcInfoEncoding Wasm32GcInfoEncoding +// TODO-WASM: Investigate normalizing stack slots to save space based on wasm stack alignment + struct Wasm32GcInfoEncoding { static const uint32_t NUM_NORM_CODE_OFFSETS_PER_CHUNK = (64); static const uint32_t NUM_NORM_CODE_OFFSETS_PER_CHUNK_LOG2 = (6); diff --git a/src/coreclr/jit/regalloc.cpp b/src/coreclr/jit/regalloc.cpp index 315bedffc55a4a..a4195f336ea64b 100644 --- a/src/coreclr/jit/regalloc.cpp +++ b/src/coreclr/jit/regalloc.cpp @@ -415,16 +415,6 @@ bool RegAllocImpl::isRegCandidate(LclVarDsc* varDsc) return false; } -#if defined(TARGET_WASM) - // Wasm RA currently does not support EH write-thru, so any local live in or out - // of a handler must be located only on the stack. - // - if (varDsc->lvLiveInOutOfHndlr) - { - compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); - return false; - } -#endif // defined(TARGET_WASM) if (varDsc->lvDoNotEnregister) { diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index 2d8cfcf359626f..c5aebc5c1be895 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -170,7 +170,23 @@ void WasmRegAlloc::IdentifyCandidates() LclVarDsc* varDsc = m_compiler->lvaGetDesc(lclNum); varDsc->SetRegNum(REG_STK); - if (isRegCandidate(varDsc)) + bool varIsRegCandidate = isRegCandidate(varDsc); + + // Wasm RA currently does not support EH write-thru, so any local live in or out + // of a handler must be located only on the stack. + // We also need to ensure that any GC refs are not stored in wasm locals until we have support for + // spilling them to the stack before calls. + // TODO-WASM: Add support for spilling GC refs in order to relax this second restriction. + if (varDsc->lvLiveInOutOfHndlr || + // We can't apply this to parameters without causing errors in regalloc around temporaries not being consumed + (varTypeIsGC(varDsc->lvType) && !varDsc->lvIsParam) + ) + { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); + varIsRegCandidate = false; + } + + if (varIsRegCandidate) { JITDUMP("RA candidate: V%02u\n", lclNum); InitializeCandidate(varDsc); From 8416d92343895cce8697012e10085a0e984d0cdc Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Wed, 29 Apr 2026 23:55:40 -0700 Subject: [PATCH 09/16] jit-format --- src/coreclr/jit/regalloc.cpp | 1 - src/coreclr/jit/regallocwasm.cpp | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/coreclr/jit/regalloc.cpp b/src/coreclr/jit/regalloc.cpp index a4195f336ea64b..259f9cd9cdfb52 100644 --- a/src/coreclr/jit/regalloc.cpp +++ b/src/coreclr/jit/regalloc.cpp @@ -415,7 +415,6 @@ bool RegAllocImpl::isRegCandidate(LclVarDsc* varDsc) return false; } - if (varDsc->lvDoNotEnregister) { return false; diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index c5aebc5c1be895..282036bb108131 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -178,9 +178,9 @@ void WasmRegAlloc::IdentifyCandidates() // spilling them to the stack before calls. // TODO-WASM: Add support for spilling GC refs in order to relax this second restriction. if (varDsc->lvLiveInOutOfHndlr || - // We can't apply this to parameters without causing errors in regalloc around temporaries not being consumed - (varTypeIsGC(varDsc->lvType) && !varDsc->lvIsParam) - ) + // We can't apply this to parameters without causing errors in regalloc around temporaries not being + // consumed + (varTypeIsGC(varDsc->lvType) && !varDsc->lvIsParam)) { m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); varIsRegCandidate = false; From f0efa42a3e8540e4f48b1e870c9604a86724f91f Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Wed, 29 Apr 2026 23:59:20 -0700 Subject: [PATCH 10/16] Use has_fixed_register_set instead of !target_wasm --- src/coreclr/jit/codegencommon.cpp | 8 ++++---- src/coreclr/jit/codegenlinear.cpp | 12 ++++++------ src/coreclr/jit/emit.cpp | 16 ++++++++-------- src/coreclr/jit/gcinfo.cpp | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index 7985a631138583..f6f6f7086ab15d 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -854,7 +854,7 @@ bool CodeGen::genIsSameLocalVar(GenTree* op1, GenTree* op2) // inline void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bool isDying DEBUGARG(GenTree* tree)) { -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // The regset being updated here is only needed for codegen-level GCness tracking, // and Wasm does not have registers regMaskTP regMask = genGetRegMask(varDsc); @@ -886,7 +886,7 @@ void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bo assert(varDsc->IsAlwaysAliveInMemory() || ((regSet.GetMaskVars() & regMask) == 0)); regSet.AddMaskVars(regMask); } -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } #ifndef TARGET_WASM @@ -7208,12 +7208,12 @@ void CodeGen::genReturn(GenTree* treeNode) } } -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET if (treeNode->OperIs(GT_RETURN, GT_SWIFT_ERROR_RET)) { genMarkReturnGCInfo(); } -#endif // EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET #ifdef PROFILING_SUPPORTED diff --git a/src/coreclr/jit/codegenlinear.cpp b/src/coreclr/jit/codegenlinear.cpp index 2c4ba50735b79d..b93ca108bb29b4 100644 --- a/src/coreclr/jit/codegenlinear.cpp +++ b/src/coreclr/jit/codegenlinear.cpp @@ -259,7 +259,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) // and before first of the current block is emitted genUpdateLife(block->bbLiveIn); -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Even if liveness didn't change, we need to update the registers containing GC references. // genUpdateLife will update the registers live due to liveness changes. But what about registers that didn't // change? We cleared them out above. Maybe we should just not clear them out, but update the ones that change @@ -353,7 +353,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } } } -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET /* Start a new code output block */ @@ -569,7 +569,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) regSet.rsSpillChk(); -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Make sure we didn't bungle pointer register tracking regMaskTP ptrRegs = gcInfo.gcRegGCrefSetCur | gcInfo.gcRegByrefSetCur; regMaskTP nonVarPtrRegs = ptrRegs & ~regSet.GetMaskVars(); @@ -618,7 +618,7 @@ void CodeGen::genCodeForBlock(BasicBlock* block) } noway_assert(nonVarPtrRegs == RBM_NONE); -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET #endif // DEBUG #if defined(DEBUG) @@ -1601,7 +1601,7 @@ regNumber CodeGen::genConsumeReg(GenTree* tree) // genUpdateLife() will also spill local var if marked as GTF_SPILL by calling CodeGen::genSpillVar genUpdateLife(tree); -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // there are three cases where consuming a reg means clearing the bit in the live mask // 1. it was not produced by a local // 2. it was produced by a local that is going dead @@ -1659,7 +1659,7 @@ regNumber CodeGen::genConsumeReg(GenTree* tree) { gcInfo.gcMarkRegSetNpt(tree->gtGetRegMask()); } -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET genCheckConsumeNode(tree); return tree->GetRegNum(); diff --git a/src/coreclr/jit/emit.cpp b/src/coreclr/jit/emit.cpp index 7b14603d502baa..bb1794404c67b7 100644 --- a/src/coreclr/jit/emit.cpp +++ b/src/coreclr/jit/emit.cpp @@ -9076,7 +9076,7 @@ void emitter::emitUpdateLiveGCregs(GCtype gcType, regMaskTP regs, BYTE* addr) return; } -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET regMaskTP life; regMaskTP dead; regMaskTP chg; @@ -9131,7 +9131,7 @@ void emitter::emitUpdateLiveGCregs(GCtype gcType, regMaskTP regs, BYTE* addr) // The 2 GC reg masks can't be overlapping assert((emitThisGCrefRegs & emitThisByrefRegs) == 0); -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** @@ -9434,7 +9434,7 @@ void emitter::emitGCregLiveUpd(GCtype gcType, regNumber reg, BYTE* addr) { assert(emitIssuing); -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Don't track GC changes in epilogs if (emitIGisInEpilog(emitCurIG)) { @@ -9476,7 +9476,7 @@ void emitter::emitGCregLiveUpd(GCtype gcType, regNumber reg, BYTE* addr) // The 2 GC reg masks can't be overlapping assert((emitThisGCrefRegs & emitThisByrefRegs) == 0); -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** @@ -9494,7 +9494,7 @@ void emitter::emitGCregDeadUpdMask(regMaskTP regs, BYTE* addr) return; } -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // First, handle the gcref regs going dead regMaskTP gcrefRegs = emitThisGCrefRegs & regs; @@ -9530,7 +9530,7 @@ void emitter::emitGCregDeadUpdMask(regMaskTP regs, BYTE* addr) emitThisByrefRegs &= ~byrefRegs; } -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** @@ -9542,7 +9542,7 @@ void emitter::emitGCregDeadUpd(regNumber reg, BYTE* addr) { assert(emitIssuing); -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET // Don't track GC changes in epilogs if (emitIGisInEpilog(emitCurIG)) { @@ -9571,7 +9571,7 @@ void emitter::emitGCregDeadUpd(regNumber reg, BYTE* addr) emitThisByrefRegs &= ~regMask; } -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } /***************************************************************************** diff --git a/src/coreclr/jit/gcinfo.cpp b/src/coreclr/jit/gcinfo.cpp index f26c87cbcbeeee..d4259158b14291 100644 --- a/src/coreclr/jit/gcinfo.cpp +++ b/src/coreclr/jit/gcinfo.cpp @@ -196,7 +196,7 @@ void GCInfo::gcMarkRegSetNpt(regMaskTP regMask DEBUGARG(bool forceOutput)) void GCInfo::gcMarkRegPtrVal(regNumber reg, var_types type) { -#if EMIT_GENERATE_GCINFO && !defined(TARGET_WASM) +#if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET regMaskTP regMask = genRegMask(reg); switch (type) @@ -211,7 +211,7 @@ void GCInfo::gcMarkRegPtrVal(regNumber reg, var_types type) gcMarkRegSetNpt(regMask); break; } -#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM +#endif // EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET } //------------------------------------------------------------------------ From 47b17dc6f658e3f48fbda6d383e07f0cebccfa85 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Thu, 30 Apr 2026 19:30:28 -0700 Subject: [PATCH 11/16] Address PR feedback --- src/coreclr/jit/compiler.h | 1 + src/coreclr/jit/lclvars.cpp | 11 +++++++---- src/coreclr/jit/regallocwasm.cpp | 10 +++++----- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 7562b34fefb930..eed47fd90a6d84 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -488,6 +488,7 @@ enum class DoNotEnregisterReason CallSpCheck, // the local is used to do SP check on every call SimdUserForcesDep, // a promoted struct was used by a SIMD/HWI node; it must be dependently promoted HiddenBufferStructArg, // the argument is a hidden return buffer passed to a method. + WasmGCVisibility, }; enum class AddressExposedReason diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index ba92c8035fd012..34ad714fc1d2c0 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -2345,6 +2345,9 @@ void Compiler::lvaSetVarDoNotEnregister(unsigned varNum DEBUGARG(DoNotEnregister case DoNotEnregisterReason::LocalField: JITDUMP("was accessed as a local field\n"); break; + case DoNotEnregisterReason::WasmGCVisibility: + JITDUMP("Wasm GC needs to see it\n"); + break; case DoNotEnregisterReason::VMNeedsStackAddr: JITDUMP("VM needs stack addr\n"); break; @@ -4176,7 +4179,7 @@ unsigned Compiler::lvaGetMaxSpillTempSize() * * Wasm leaf frame, no localloc * - * + * * | caller frame | * +=======================+ <---- Virtual '0' * | | @@ -4190,7 +4193,7 @@ unsigned Compiler::lvaGetMaxSpillTempSize() * V * * Wasm, leaf frame, localloc - * + * * | caller frame | * +=======================+ <---- Virtual '0' * | | @@ -4225,9 +4228,9 @@ unsigned Compiler::lvaGetMaxSpillTempSize() * | | Stack grows | * | downward * V - * + * * Wasm, non-leaf frame, localloc - * + * * | caller frame | * +=======================+ <---- Virtual '0' * | | diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index 282036bb108131..c7ef1de181d0c6 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -177,14 +177,14 @@ void WasmRegAlloc::IdentifyCandidates() // We also need to ensure that any GC refs are not stored in wasm locals until we have support for // spilling them to the stack before calls. // TODO-WASM: Add support for spilling GC refs in order to relax this second restriction. - if (varDsc->lvLiveInOutOfHndlr || - // We can't apply this to parameters without causing errors in regalloc around temporaries not being - // consumed - (varTypeIsGC(varDsc->lvType) && !varDsc->lvIsParam)) - { + if (varDsc->lvLiveInOutOfHndlr) { m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); varIsRegCandidate = false; } + if (varTypeIsGC(varDsc->lvType)) { + m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::WasmGCVisibility)); + varIsRegCandidate = false; + } if (varIsRegCandidate) { From 52618cf806c6b2e79fa1eeaebbe52cd8c6bc492e Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Thu, 30 Apr 2026 21:22:45 -0700 Subject: [PATCH 12/16] Orphaned GT_INDs from block stores can turn into isolated GT_NULLCHECK nodes with multiply-used operands we need to collect --- src/coreclr/jit/regallocwasm.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index c7ef1de181d0c6..86a3102dc3381f 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -429,6 +429,13 @@ void WasmRegAlloc::CollectReferencesForNode(GenTree* node) { switch (node->OperGet()) { + case GT_NULLCHECK: + if (node->gtGetOp1()->gtLIRFlags & LIR::Flags::MultiplyUsed) + { + ConsumeTemporaryRegForOperand(node->gtGetOp1() DEBUGARG("Orphaned GT_NULLCHECK with multiply-used flag")); + } + break; + case GT_LCL_VAR: CollectReferencesForLclVar(node->AsLclVar()); break; From 9282f3167a502439adbd536faec3e38004a5fb62 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Thu, 30 Apr 2026 21:26:00 -0700 Subject: [PATCH 13/16] Fix unreached assertion --- src/coreclr/jit/compiler.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index daf91aaf30e983..53a076073909f2 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -10738,6 +10738,9 @@ void Compiler::EnregisterStats::RecordLocal(const LclVarDsc* varDsc) m_simdUserForcesDep++; break; + case DoNotEnregisterReason::WasmGCVisibility: + break; + default: unreached(); break; From 2af3ce74e40f41172f1e96226ae8f32457993798 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Thu, 30 Apr 2026 21:36:29 -0700 Subject: [PATCH 14/16] Cleanup --- src/coreclr/jit/gcencode.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/gcencode.cpp b/src/coreclr/jit/gcencode.cpp index c5d5f6b458b8cb..f7742b072d689e 100644 --- a/src/coreclr/jit/gcencode.cpp +++ b/src/coreclr/jit/gcencode.cpp @@ -4651,7 +4651,7 @@ void GCInfo::gcInfoRecordGCRegStateChange(GcInfoEncoder* gcInfoEncoder, regMaskSmall byRefMask, regMaskSmall* pPtrRegs) { -#ifndef TARGET_WASM +#if HAS_FIXED_REGISTER_SET // Precondition: byRefMask is a subset of regMask. assert((byRefMask & ~regMask) == 0); @@ -4709,7 +4709,7 @@ void GCInfo::gcInfoRecordGCRegStateChange(GcInfoEncoder* gcInfoEncoder, // Turn the bit we've just generated off and continue. regMask ^= tmpMask; // EAX,ECX,EDX,EBX,---,EBP,ESI,EDI } -#endif // !TARGET_WASM +#endif // HAS_FIXED_REGISTER_SET } /************************************************************************** From b829c0be639f7d75d12d6df5d1aa9b219cd6ffee Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Thu, 30 Apr 2026 21:42:01 -0700 Subject: [PATCH 15/16] jit-format --- src/coreclr/jit/regallocwasm.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index 86a3102dc3381f..a0b07a8788d475 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -177,11 +177,13 @@ void WasmRegAlloc::IdentifyCandidates() // We also need to ensure that any GC refs are not stored in wasm locals until we have support for // spilling them to the stack before calls. // TODO-WASM: Add support for spilling GC refs in order to relax this second restriction. - if (varDsc->lvLiveInOutOfHndlr) { + if (varDsc->lvLiveInOutOfHndlr) + { m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); varIsRegCandidate = false; } - if (varTypeIsGC(varDsc->lvType)) { + if (varTypeIsGC(varDsc->lvType)) + { m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::WasmGCVisibility)); varIsRegCandidate = false; } @@ -432,7 +434,8 @@ void WasmRegAlloc::CollectReferencesForNode(GenTree* node) case GT_NULLCHECK: if (node->gtGetOp1()->gtLIRFlags & LIR::Flags::MultiplyUsed) { - ConsumeTemporaryRegForOperand(node->gtGetOp1() DEBUGARG("Orphaned GT_NULLCHECK with multiply-used flag")); + ConsumeTemporaryRegForOperand(node->gtGetOp1() + DEBUGARG("Orphaned GT_NULLCHECK with multiply-used flag")); } break; From d8f5f9f8534b458a38dcc7a833f345ffe84812a4 Mon Sep 17 00:00:00 2001 From: Katelyn Gadd Date: Sat, 2 May 2026 05:26:17 -0700 Subject: [PATCH 16/16] Address PR feedback --- src/coreclr/jit/codegencommon.cpp | 4 ++-- src/coreclr/jit/compiler.cpp | 2 ++ src/coreclr/jit/compiler.h | 1 + src/coreclr/jit/regallocwasm.cpp | 6 +++--- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index f6f6f7086ab15d..2fe77b1a3294b0 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -854,9 +854,9 @@ bool CodeGen::genIsSameLocalVar(GenTree* op1, GenTree* op2) // inline void CodeGenInterface::genUpdateRegLife(const LclVarDsc* varDsc, bool isBorn, bool isDying DEBUGARG(GenTree* tree)) { + // Targets like Wasm do not have a fixed set of registers so the regset logic in this method is unnecessary. #if EMIT_GENERATE_GCINFO && HAS_FIXED_REGISTER_SET - // The regset being updated here is only needed for codegen-level GCness tracking, - // and Wasm does not have registers + // The regset being updated here is only needed for codegen-level GCness tracking. regMaskTP regMask = genGetRegMask(varDsc); #ifdef DEBUG diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index 53a076073909f2..24634a2fc4d823 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -10739,6 +10739,7 @@ void Compiler::EnregisterStats::RecordLocal(const LclVarDsc* varDsc) break; case DoNotEnregisterReason::WasmGCVisibility: + m_wasmGcVisibility++; break; default: @@ -10869,6 +10870,7 @@ void Compiler::EnregisterStats::Dump(FILE* fout) const PRINT_STATS(m_swizzleArg, notEnreg); PRINT_STATS(m_blockOpRet, notEnreg); PRINT_STATS(m_returnSpCheck, notEnreg); + PRINT_STATS(m_wasmGcVisibility, notEnreg); PRINT_STATS(m_callSpCheck, notEnreg); PRINT_STATS(m_simdUserForcesDep, notEnreg); diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index eed47fd90a6d84..af6eec47811d11 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -11760,6 +11760,7 @@ class Compiler unsigned m_liveInOutHndlr; unsigned m_depField; unsigned m_noRegVars; + unsigned m_wasmGcVisibility; #ifdef JIT32_GCENCODER unsigned m_PinningRef; #endif // JIT32_GCENCODER diff --git a/src/coreclr/jit/regallocwasm.cpp b/src/coreclr/jit/regallocwasm.cpp index a0b07a8788d475..2e296c3110a1e0 100644 --- a/src/coreclr/jit/regallocwasm.cpp +++ b/src/coreclr/jit/regallocwasm.cpp @@ -174,14 +174,14 @@ void WasmRegAlloc::IdentifyCandidates() // Wasm RA currently does not support EH write-thru, so any local live in or out // of a handler must be located only on the stack. - // We also need to ensure that any GC refs are not stored in wasm locals until we have support for - // spilling them to the stack before calls. - // TODO-WASM: Add support for spilling GC refs in order to relax this second restriction. if (varDsc->lvLiveInOutOfHndlr) { m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::LiveInOutOfHandler)); varIsRegCandidate = false; } + // We also need to ensure that any GC refs are not stored in wasm locals until we have support for + // spilling them to the stack before calls. + // TODO-WASM: Add support for spilling GC refs in order to relax this second restriction. if (varTypeIsGC(varDsc->lvType)) { m_compiler->lvaSetVarDoNotEnregister(lclNum DEBUGARG(DoNotEnregisterReason::WasmGCVisibility));