Skip to content
Open
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
2 changes: 2 additions & 0 deletions src/coreclr/jit/codegen.h
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,8 @@ class CodeGen final : public CodeGenInterface
void instGen(instruction ins);
#if defined(TARGET_XARCH)
void inst_JMP(emitJumpKind jmp, BasicBlock* tgtBlock, bool isRemovableJmpCandidate = false);
#elif defined(TARGET_WASM)
void inst_JMP(emitJumpKind jmp, BasicBlock* tgtBlock, bool isTempLabel = false);
#else
void inst_JMP(emitJumpKind jmp, BasicBlock* tgtBlock);
#endif
Expand Down
87 changes: 85 additions & 2 deletions src/coreclr/jit/codegenwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@
#ifdef TARGET_64BIT
static const instruction INS_I_const = INS_i64_const;
static const instruction INS_I_add = INS_i64_add;
static const instruction INS_I_le_u = INS_i64_le_u;
static const instruction INS_I_gt_u = INS_i64_gt_u;
#else // !TARGET_64BIT
static const instruction INS_I_const = INS_i32_const;
static const instruction INS_I_add = INS_i32_add;
static const instruction INS_I_le_u = INS_i32_le_u;
static const instruction INS_I_gt_u = INS_i32_gt_u;
#endif // !TARGET_64BIT

void CodeGen::genMarkLabelsForCodegen()
Expand Down Expand Up @@ -353,6 +357,10 @@ void CodeGen::genCodeForTreeNode(GenTree* treeNode)
genCodeForNegNot(treeNode->AsOp());
break;

case GT_NULLCHECK:
genCodeForNullCheck(treeNode->AsIndir());
break;

default:
#ifdef DEBUG
NYIRAW(GenTree::OpName(treeNode->OperGet()));
Expand Down Expand Up @@ -570,6 +578,37 @@ void CodeGen::genCodeForDivMod(GenTreeOp* treeNode)
{
genConsumeOperands(treeNode);

// wasm stack is
// divisor (top)
// dividend (next)
// ...
// TODO-WASM: To check for exception, we will have to spill these to
// internal registers along the way, like so:
//
// ... push dividend
// tee.local $temp1
// ... push divisor
// tee.local $temp2
// ... exception checks (using $temp1 and $temp2; will introduce flow)
// div/mod op

if (!varTypeIsFloating(treeNode->TypeGet()))
{
ExceptionSetFlags exSetFlags = treeNode->OperExceptions(compiler);

// TODO-WASM:(AnyVal / 0) => DivideByZeroException
//
if ((exSetFlags & ExceptionSetFlags::DivideByZeroException) != ExceptionSetFlags::None)
{
}

// TODO-WASM: (MinInt / -1) => ArithmeticException
//
if ((exSetFlags & ExceptionSetFlags::ArithmeticException) != ExceptionSetFlags::None)
{
}
}

instruction ins;
switch (PackOperAndType(treeNode))
{
Expand Down Expand Up @@ -784,6 +823,49 @@ void CodeGen::genCodeForNegNot(GenTreeOp* tree)
genProduceReg(tree);
}

//---------------------------------------------------------------------
// genCodeForNullCheck - generate code for a GT_NULLCHECK node
//
// Arguments:
// tree - the GT_NULLCHECK node
//
// Notes:
// If throw helper calls are being emitted inline, we need
// to wrap the resulting codegen in a block/end pair.
//
void CodeGen::genCodeForNullCheck(GenTreeIndir* tree)
{
genConsumeAddress(tree->Addr());

// TODO-WASM: refactor once we have implemented other cases invoking throw helpers
if (compiler->fgUseThrowHelperBlocks())
{
Compiler::AddCodeDsc* const add = compiler->fgFindExcptnTarget(SCK_NULL_CHECK, compiler->compCurBB);
assert(add != nullptr);
assert(add->acdUsed);
GetEmitter()->emitIns_I(INS_I_const, EA_PTRSIZE, compiler->compMaxUncheckedOffsetForNullObject);
GetEmitter()->emitIns(INS_I_le_u);
inst_JMP(EJ_jmpif, add->acdDstBlk);
}
else
{
BasicBlock* const tgtBlk = genCreateTempLabel();
GetEmitter()->emitIns(INS_block);
GetEmitter()->emitIns_I(INS_I_const, EA_PTRSIZE, compiler->compMaxUncheckedOffsetForNullObject);
GetEmitter()->emitIns(INS_I_gt_u);
// tgtBlock is not on the model wasm control stack we set up earlier.
// Since we have just emitted a `begin` that will end at this block, it will be at depth 0.
// Indicate this via isTempLabel.
inst_JMP(EJ_jmpif, tgtBlk, /* isTempLabel */ true);
// TODO-WASM: codegen for the call
// genEmitHelperCall(compiler->acdHelper(SCK_NULL_CHECK), 0, EA_UNKNOWN);
// The helper won't return, and we may have things pended on the stack, so emit unreachable
GetEmitter()->emitIns(INS_unreachable);
GetEmitter()->emitIns(INS_end);
genDefineTempLabel(tgtBlk);
}
}

//------------------------------------------------------------------------
// genCodeForLclAddr: Generates the code for GT_LCL_ADDR.
//
Expand Down Expand Up @@ -1086,11 +1168,12 @@ void CodeGen::genSpillVar(GenTree* tree)
// Arguments:
// jmp - kind of jump to emit
// tgtBlock - target of the jump
// isTempLabel - true if target is a temp label (implicitly at top of control flow stack)
//
void CodeGen::inst_JMP(emitJumpKind jmp, BasicBlock* tgtBlock)
void CodeGen::inst_JMP(emitJumpKind jmp, BasicBlock* tgtBlock, bool isTempLabel)
{
instruction instr = emitter::emitJumpKindToIns(jmp);
unsigned const depth = findTargetDepth(tgtBlock);
unsigned const depth = isTempLabel ? 0 : findTargetDepth(tgtBlock);
GetEmitter()->emitIns_J(instr, EA_4BYTE, depth, tgtBlock);
}

Expand Down
8 changes: 8 additions & 0 deletions src/coreclr/jit/compiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,14 @@ Compiler::Compiler(ArenaAllocator* arena,

compMaxUncheckedOffsetForNullObject = eeInfo->maxUncheckedOffsetForNullObject;

#if defined(DEBUG) && defined(TARGET_WASM)
// if we are cross-replaying wasm, override compMaxUncheckedOffsetForNullObject
if (!info.compMatchedVM)
{
compMaxUncheckedOffsetForNullObject = 1024 - 1;
}
#endif

info.compProfilerCallback = false; // Assume false until we are told to hook this method.

info.compCode = methodInfo->ILCode;
Expand Down
36 changes: 36 additions & 0 deletions src/coreclr/jit/fgwasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -318,9 +318,45 @@ class FgWasm
// consider exceptional successors or successors that require runtime intervention
// (eg funclet returns).
//
// For method and funclet entries we add any "ACD" blocks as successors before the
// true successors. This ensures the ACD blocks end up at the end of the funclet
// region, and that we create proper Wasm blocks so we can branch to them from
// anywhere within the method region or funclet region.
//
template <typename TFunc>
BasicBlockVisit FgWasm::VisitWasmSuccs(Compiler* comp, BasicBlock* block, TFunc func, bool useProfile)
{
// Special case throw helper blocks that are not yet connected in the flow graph.
//
Compiler::AddCodeDscMap* const acdMap = comp->fgGetAddCodeDscMap();
if (acdMap != nullptr)
{
// Behave as if these blocks have edges from their respective region entry blocks.
//
if ((block == comp->fgFirstBB) || comp->bbIsFuncletBeg(block))
{
Compiler::AcdKeyDesignator dsg;
const unsigned blockData = comp->bbThrowIndex(block, &dsg);

// We do not expect any ACDs to be mapped to try regions (only method/handler/filter)
//
assert(dsg != Compiler::AcdKeyDesignator::KD_TRY);

for (const Compiler::AddCodeDscKey& key : Compiler::AddCodeDscMap::KeyIteration(acdMap))
{
if (key.Data() == blockData)
{
// This ACD refers to a throw helper block in the right region.
// Make the block a successor.
//
Compiler::AddCodeDsc* acd = nullptr;
acdMap->Lookup(key, &acd);
RETURN_ON_ABORT(func(acd->acdDstBlk));
}
}
}
}

switch (block->GetKind())
{
// Funclet returns have no successors
Expand Down
27 changes: 25 additions & 2 deletions src/coreclr/jit/flowgraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3326,6 +3326,8 @@ unsigned Compiler::acdHelper(SpecialCodeKind codeKind)
return CORINFO_HELP_OVERFLOW;
case SCK_FAIL_FAST:
return CORINFO_HELP_FAIL_FAST;
case SCK_NULL_CHECK:
return CORINFO_HELP_THROWNULLREF;
default:
assert(!"Bad codeKind");
return 0;
Expand Down Expand Up @@ -3358,6 +3360,8 @@ const char* sckName(SpecialCodeKind codeKind)
return "SCK_ARITH_EXCPN";
case SCK_FAIL_FAST:
return "SCK_FAIL_FAST";
case SCK_NULL_CHECK:
return "SCK_NULL_CHECK";
default:
return "SCK_UNKNOWN";
}
Expand Down Expand Up @@ -3490,6 +3494,7 @@ PhaseStatus Compiler::fgCreateThrowHelperBlocks()
BBJ_THROW, // SCK_ARG_EXCPN
BBJ_THROW, // SCK_ARG_RNG_EXCPN
BBJ_THROW, // SCK_FAIL_FAST
BBJ_THROW, // SCK_NULL_CHECK
};

noway_assert(sizeof(jumpKinds) == SCK_COUNT); // sanity check
Expand Down Expand Up @@ -3555,6 +3560,9 @@ PhaseStatus Compiler::fgCreateThrowHelperBlocks()
case SCK_FAIL_FAST:
msg = " for FAIL_FAST";
break;
case SCK_NULL_CHECK:
msg = " for NULL_CHECK";
break;
default:
msg = " for ??";
break;
Expand Down Expand Up @@ -3708,13 +3716,28 @@ unsigned Compiler::bbThrowIndex(BasicBlock* blk, AcdKeyDesignator* dsg)

assert(inTry || inHnd);

#if defined(TARGET_WASM)
// The current plan for Wasm: method regions or funclets with
// trys will have a single Wasm try handle all
// resumption from catches via virtual IPs.
//
// So we do not need to consider the nesting of the throw
// in try regions, just in handlers.
//
if (!inHnd)
{
*dsg = AcdKeyDesignator::KD_NONE;
return 0;
}
#else
if (inTry && (!inHnd || (tryIndex < hndIndex)))
{
// The most enclosing region is a try body, use it
assert(tryIndex <= 0x3FFFFFFF);
*dsg = AcdKeyDesignator::KD_TRY;
return tryIndex;
}
#endif // !defined(TARGET_WASM)

// The most enclosing region is a handler which will be a funclet
// Now we have to figure out if blk is in the filter or handler
Expand All @@ -3731,7 +3754,7 @@ unsigned Compiler::bbThrowIndex(BasicBlock* blk, AcdKeyDesignator* dsg)
}

//------------------------------------------------------------------------
// AddCodedDscKey: construct from kind and block
// AddCodeDscKey: construct from kind and block
//
// Arguments:
// kind - exception kind
Expand All @@ -3755,7 +3778,7 @@ Compiler::AddCodeDscKey::AddCodeDscKey(SpecialCodeKind kind, BasicBlock* block,
}

//------------------------------------------------------------------------
// AddCodedDscKey: construct from AddCodeDsc
// AddCodeDscKey: construct from AddCodeDsc
//
// Arguments:
// add - add code dsc in querstion
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/gentree.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ enum SpecialCodeKind
SCK_ARG_EXCPN, // target on ArgumentException (currently used only for SIMD intrinsics)
SCK_ARG_RNG_EXCPN, // target on ArgumentOutOfRangeException (currently used only for SIMD intrinsics)
SCK_FAIL_FAST, // target for fail fast exception
SCK_NULL_CHECK, // target for NullReferenceException (Wasm)
SCK_COUNT
};

Expand Down
8 changes: 7 additions & 1 deletion src/coreclr/jit/stacklevelsetter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ PhaseStatus StackLevelSetter::DoPhase()
}
}
}

return madeChanges ? PhaseStatus::MODIFIED_EVERYTHING : PhaseStatus::MODIFIED_NOTHING;
}

Expand Down Expand Up @@ -273,6 +272,13 @@ void StackLevelSetter::SetThrowHelperBlocks(GenTree* node, BasicBlock* block)
break;
#endif

#if defined(TARGET_WASM)
// TODO-WASM: add other opers that imply null checks
case GT_NULLCHECK:
SetThrowHelperBlock(SCK_NULL_CHECK, block);
break;
#endif // defined(TARGET_WASM)

default: // Other opers can target throw only due to overflow.
break;
}
Expand Down
14 changes: 12 additions & 2 deletions src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3381,8 +3381,18 @@ private void getEEInfo(ref CORINFO_EE_INFO pEEInfoOut)

pEEInfoOut.osPageSize = 0x1000;

pEEInfoOut.maxUncheckedOffsetForNullObject = (_compilation.NodeFactory.Target.IsWindows) ?
(32 * 1024 - 1) : (pEEInfoOut.osPageSize / 2 - 1);
if (_compilation.NodeFactory.Target.IsWasm)
{
pEEInfoOut.maxUncheckedOffsetForNullObject = 1024 - 1;
Copy link
Member

Choose a reason for hiding this comment

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

This represents maximum offset that gets handled by hardware throwing access violation exceptions.

How is this value used on wasm? I would expect this to be 0 on wasm since we are not going to depend on "hardware" to throw exceptions for all cases.

Copy link
Member

Choose a reason for hiding this comment

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

(In any case, it would be useful to make the comment in corinfo.h more descriptive.)

Copy link
Member Author

Choose a reason for hiding this comment

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

There is some discussion in a related pr #123021 (comment) which refers to an open issue in NAOT-LLVM dotnet/runtimelab#3127.

I agree that it seems counterintuitive on Wasm to have a value other than 0. The plan is to get there eventually. I can make it clearer this is expected to be an interim state as we bring up the support in the JIT.

Copy link
Member

Choose a reason for hiding this comment

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

Is the JIT not able to deal with this value being 0 at the moment?

Copy link
Member

@EgorBo EgorBo Jan 16, 2026

Choose a reason for hiding this comment

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

I've just tried to run spmi diffs for a single collection (libraries.pmi) for maxUncheckedOffsetForNullObject being 0 on win-x64:

[02:21:08] 139,498 contexts with diffs (648 size improvements, 138,402 size regressions, 448 same size)
[02:21:08]                             (265 PerfScore improvements, 138,500 PerfScore regressions, 733 same PerfScore)
[02:21:08]   -9,075/+945,747 bytes
[02:21:08]   -2.84%/+29.11% PerfScore

quite a massive regression 😐

it's 32767 by default for me, but 1024 handles 99% of the regressions

Copy link
Member Author

Choose a reason for hiding this comment

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

Is the JIT not able to deal with this value being 0 at the moment?

As far as I know 0 works" just fine (modulo possible latent bugs and the code size bloat).

Copy link
Member

@jkotas jkotas Jan 16, 2026

Choose a reason for hiding this comment

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

quite a massive regression

Right, that's expected. The explicit null checks for wasm need work to be reasonably efficient.

We typically go for correctness first during bring ups and optimize later. 0 is the only correct value for wasm. I understand that it produces inefficient code, but that is expected at this point.

Copy link
Contributor

Choose a reason for hiding this comment

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

0 is the only correct value for wasm

Since with explicit null checks, there are no "OS details" involved, we can choose any value we like that is < __global_base (1024 by default for optimized link - we will need to set it explicitly for non-optimized link). I agree that in the long term 0 is "the best" value, since the code sequences are smallest with it.

However, at this present point it will be both less efficient and (perhaps more importantly) less correct. Less correct because (among corner case bugs I am either not aware of or forgot about), we have a number of places in the Jit where we produce "naked" IND(x + 8)-like trees (for things like x.Length and such) and expect the implicit null-checking to kick in.

}
else if (_compilation.NodeFactory.Target.IsWindows)
{
pEEInfoOut.maxUncheckedOffsetForNullObject = 32 * 1024 - 1;
}
else
{
pEEInfoOut.maxUncheckedOffsetForNullObject = pEEInfoOut.osPageSize / 2 - 1;
}

pEEInfoOut.targetAbi = TargetABI;
pEEInfoOut.osType = TargetToOs(_compilation.NodeFactory.Target);
Expand Down
11 changes: 7 additions & 4 deletions src/coreclr/vm/jitinterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@

#include "corjit.h"

#ifndef TARGET_UNIX
#define MAX_UNCHECKED_OFFSET_FOR_NULL_OBJECT ((32*1024)-1) // when generating JIT code
#else // !TARGET_UNIX
#if defined (TARGET_WASM)
#define MAX_UNCHECKED_OFFSET_FOR_NULL_OBJECT (1024 - 1)
#elif defined (TARGET_UNIX)
#define MAX_UNCHECKED_OFFSET_FOR_NULL_OBJECT ((GetOsPageSize() / 2) - 1)
#endif // !TARGET_UNIX
#else
#define MAX_UNCHECKED_OFFSET_FOR_NULL_OBJECT ((32*1024)-1) // when generating JIT code
#endif

#include "pgo.h"

class ILCodeStream;
Expand Down
Loading