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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/coreclr/gcinfo/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 0 additions & 2 deletions src/coreclr/gcinfo/gcinfoencoder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<TargetGcInfoEncoding>;
#endif // !TARGET_WASM

#ifdef FEATURE_INTERPRETER
template class TGcInfoEncoder<InterpreterGcInfoEncoding>;
Expand Down
51 changes: 49 additions & 2 deletions src/coreclr/inc/gcinfotypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
kg marked this conversation as resolved.

#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

Expand Down
4 changes: 3 additions & 1 deletion src/coreclr/jit/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Comment thread
kg marked this conversation as resolved.

Expand Down Expand Up @@ -292,6 +292,8 @@ set( JIT_RISCV64_SOURCES
)

set( JIT_WASM_SOURCES
gcdecode.cpp
gcencode.cpp
codegenwasm.cpp
emitwasm.cpp
fgwasm.cpp
Expand Down
23 changes: 12 additions & 11 deletions src/coreclr/jit/codegencommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
Comment on lines +1053 to 1057
Copy link

Copilot AI Apr 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On TARGET_WASM the register-handling block above is compiled out, but the logic below only updates gcVarPtrSetCur when isInMemory is true. Since the Wasm reg allocator can enregister TYP_REF/TYP_BYREF locals (see src/coreclr/jit/regalloc.cpp:434-446), isInMemory can be false for GC-tracked locals and they won't be removed from (or added to) gcVarPtrSetCur correctly, producing incorrect stack-root reporting in the encoded GC info. Consider either (1) preventing GC-tracked locals from being enregistered on Wasm, or (2) keeping a real stack home for GC refs and ensuring gcVarPtrSetCur reflects it, or (3) implementing register-slot tracking/encoding for Wasm instead of compiling it out.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sounds like it could be a real problem, thoughts @AndyAyersMS @SingleAccretion ? I think out of its proposed options, 3 doesn't make sense and 1 would make our performance terrible. I think the plan was that we'd spill any enregistered gc refs to memory before safe points, yeah? Which is roughly 2. Wondering if this indicates a change I need to make.

Expand All @@ -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())
Expand All @@ -1092,6 +1097,7 @@ void Compiler::compChangeLife(VARSET_VALARG_TP newLife)
{
codeGen->gcInfo.gcRegByrefSetCur |= regMask;
}
#endif // !TARGET_WASM
}
else if (lvaIsGCTracked(varDsc))
{
Expand Down Expand Up @@ -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))
{
Expand All @@ -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())
{
Expand Down Expand Up @@ -2418,11 +2424,6 @@ void CodeGen::genEmitMachineCode()
//
void CodeGen::genEmitUnwindDebugGCandEH()
{
#ifdef TARGET_WASM
// TODO-WASM: Fix this phase causing an assertion failure even for methods with no GC locals or EH clauses
return;
#endif

/* Now that the code is issued, we can finalize and emit the unwind data */

m_compiler->unwindEmit(*codePtr, coldCodePtr);
Expand Down Expand Up @@ -7245,7 +7246,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();
Expand Down
20 changes: 14 additions & 6 deletions src/coreclr/jit/codegenlinear.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 */

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
37 changes: 36 additions & 1 deletion src/coreclr/jit/codegenwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -3259,7 +3261,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();
}

void CodeGen::genReportEH()
Expand Down
16 changes: 8 additions & 8 deletions src/coreclr/jit/emit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
}

/*****************************************************************************
Expand Down Expand Up @@ -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))
{
Expand Down Expand Up @@ -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
}

/*****************************************************************************
Expand All @@ -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;
Expand Down Expand Up @@ -9530,7 +9530,7 @@ void emitter::emitGCregDeadUpdMask(regMaskTP regs, BYTE* addr)

emitThisByrefRegs &= ~byrefRegs;
}
#endif // EMIT_GENERATE_GCINFO
#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM
}

/*****************************************************************************
Expand All @@ -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))
{
Expand Down Expand Up @@ -9571,7 +9571,7 @@ void emitter::emitGCregDeadUpd(regNumber reg, BYTE* addr)

emitThisByrefRegs &= ~regMask;
}
#endif // EMIT_GENERATE_GCINFO
#endif // EMIT_GENERATE_GCINFO && !TARGET_WASM
}

/*****************************************************************************
Expand Down
Loading
Loading