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
6 changes: 4 additions & 2 deletions src/coreclr/jit/codegencommon.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1555,7 +1555,6 @@ bool CodeGen::genCreateAddrMode(GenTree* addr,
return true;
}

#ifndef TARGET_WASM
//------------------------------------------------------------------------
// genEmitCallWithCurrentGC:
// Emit a call with GC information captured from current GC information.
Expand All @@ -1570,7 +1569,6 @@ void CodeGen::genEmitCallWithCurrentGC(EmitCallParams& params)
params.byrefRegs = gcInfo.gcRegByrefSetCur;
GetEmitter()->emitIns_Call(params);
}
#endif // !TARGET_WASM

/*****************************************************************************
*
Expand Down Expand Up @@ -5737,6 +5735,8 @@ CORINFO_FIELD_HANDLE CodeGen::genEmitAsyncResumeInfo(unsigned stateNum)
return compiler->eeFindJitDataOffs(baseOffs + stateNum * sizeof(CORINFO_AsyncResumeInfo));
}

#endif // TARGET_WASM
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

The endif comment is incorrect. The opening directive at line 5641 is "#if !defined(TARGET_WASM)" so the comment should be "#endif // !TARGET_WASM" not "#endif // TARGET_WASM".

Suggested change
#endif // TARGET_WASM
#endif // !TARGET_WASM

Copilot uses AI. Check for mistakes.

//------------------------------------------------------------------------
// getCallTarget - Get the node that evaluates to the call target
//
Expand Down Expand Up @@ -5814,6 +5814,8 @@ regNumber CodeGen::getCallIndirectionCellReg(GenTreeCall* call)
return result;
}

#if !defined(TARGET_WASM)

//------------------------------------------------------------------------
// genDefinePendingLabel - If necessary, define the pending call label after a
// call instruction was emitted.
Expand Down
259 changes: 230 additions & 29 deletions src/coreclr/jit/codegenwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,10 @@ void CodeGen::genCodeForTreeNode(GenTree* treeNode)
genCodeForStoreInd(treeNode->AsStoreInd());
break;

case GT_CALL:
genCall(treeNode->AsCall());
break;

default:
#ifdef DEBUG
NYIRAW(GenTree::OpName(treeNode->OperGet()));
Expand Down Expand Up @@ -1016,45 +1020,61 @@ void CodeGen::genCodeForDivMod(GenTreeOp* treeNode)
//
void CodeGen::genCodeForConstant(GenTree* treeNode)
{
instruction ins;
cnsval_ssize_t bits;
instruction ins = INS_none;
cnsval_ssize_t bits = 0;
var_types type = treeNode->TypeIs(TYP_REF, TYP_BYREF) ? TYP_I_IMPL : treeNode->TypeGet();
static_assert(sizeof(cnsval_ssize_t) >= sizeof(double));

switch (type)
GenTreeIntConCommon* icon = nullptr;
if ((type == TYP_INT) || (type == TYP_LONG))
{
case TYP_INT:
icon = treeNode->AsIntConCommon();
if (icon->ImmedValNeedsReloc(compiler))
{
ins = INS_i32_const;
GenTreeIntConCommon* con = treeNode->AsIntConCommon();
bits = con->IntegralValue();
break;
// WASM-TODO: Generate reloc for this handle
ins = INS_I_const;
bits = 0;
}
case TYP_LONG:
{
ins = INS_i64_const;
GenTreeIntConCommon* con = treeNode->AsIntConCommon();
bits = con->IntegralValue();
break;
}
case TYP_FLOAT:
else
{
ins = INS_f32_const;
GenTreeDblCon* con = treeNode->AsDblCon();
double value = con->DconValue();
memcpy(&bits, &value, sizeof(double));
break;
bits = icon->IntegralValue();
}
case TYP_DOUBLE:
}

if (ins == INS_none)
{
switch (type)
{
ins = INS_f64_const;
GenTreeDblCon* con = treeNode->AsDblCon();
double value = con->DconValue();
memcpy(&bits, &value, sizeof(double));
break;
case TYP_INT:
{
ins = INS_i32_const;
assert(((INT64)(INT32)bits) == bits);
break;
}
case TYP_LONG:
{
ins = INS_i64_const;
break;
}
case TYP_FLOAT:
{
ins = INS_f32_const;
GenTreeDblCon* con = treeNode->AsDblCon();
double value = con->DconValue();
memcpy(&bits, &value, sizeof(double));
break;
}
case TYP_DOUBLE:
{
ins = INS_f64_const;
GenTreeDblCon* con = treeNode->AsDblCon();
double value = con->DconValue();
memcpy(&bits, &value, sizeof(double));
break;
}
default:
unreached();
}
default:
unreached();
}

// The IF_ for the selected instruction, i.e. IF_F64, determines how these bits are emitted
Expand Down Expand Up @@ -1373,6 +1393,187 @@ void CodeGen::genCodeForStoreInd(GenTreeStoreInd* tree)
genUpdateLife(tree);
}

//------------------------------------------------------------------------
// genCall: Produce code for a GT_CALL node
//
void CodeGen::genCall(GenTreeCall* call)
{
if (call->NeedsNullCheck())
{
NYI_WASM("Insert nullchecks for calls that need it in lowering");
}

assert(!call->IsTailCall());

genCallInstruction(call);
Copy link
Contributor

Choose a reason for hiding this comment

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

Arguments need to be consumed. Something like:

for (CallArg& arg : call->gtArgs.EarlyArgs())
{
    genConsumeReg(arg.GetEarlyNode());
}

for (CallArg& arg : call->gtArgs.LateArgs())
{
    genConsumeReg(arg.GetLateNode());
}

genProduceReg(call);
}

//------------------------------------------------------------------------
// genCallInstruction - Generate instructions necessary to transfer control to the call.
//
// Arguments:
// call - the GT_CALL node
//
void CodeGen::genCallInstruction(GenTreeCall* call)
{
// Determine return value size(s).
const ReturnTypeDesc* pRetTypeDesc = call->GetReturnTypeDesc();
Comment on lines +1420 to +1421
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// Determine return value size(s).
const ReturnTypeDesc* pRetTypeDesc = call->GetReturnTypeDesc();

Unused.

EmitCallParams params;

// unused values are of no interest to GC.
if (!call->IsUnusedValue())
{
if (call->HasMultiRegRetVal())
{
NYI_WASM("multi-reg return values");
}
else
{
assert(!call->TypeIs(TYP_STRUCT));

if (call->TypeIs(TYP_REF))
{
params.retSize = EA_GCREF;
}
else if (call->TypeIs(TYP_BYREF))
{
params.retSize = EA_BYREF;
}
}
}

Comment on lines +1423 to +1445
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// unused values are of no interest to GC.
if (!call->IsUnusedValue())
{
if (call->HasMultiRegRetVal())
{
NYI_WASM("multi-reg return values");
}
else
{
assert(!call->TypeIs(TYP_STRUCT));
if (call->TypeIs(TYP_REF))
{
params.retSize = EA_GCREF;
}
else if (call->TypeIs(TYP_BYREF))
{
params.retSize = EA_BYREF;
}
}
}

Only relevant for GC info tracking in emitter which we don't need.

params.isJump = call->IsFastTailCall();
params.hasAsyncRet = call->IsAsync();

// We need to propagate the debug information to the call instruction, so we can emit
// an IL to native mapping record for the call, to support managed return value debugging.
// We don't want tail call helper calls that were converted from normal calls to get a record,
// so we skip this hash table lookup logic in that case.
if (compiler->opts.compDbgInfo && compiler->genCallSite2DebugInfoMap != nullptr && !call->IsTailCall())
{
DebugInfo di;
(void)compiler->genCallSite2DebugInfoMap->Lookup(call, &di);
params.debugInfo = di;
}

#ifdef DEBUG
// Pass the call signature information down into the emitter so the emitter can associate
// native call sites with the signatures they were generated from.
if (!call->IsHelperCall())
{
params.sigInfo = call->callSig;
}
#endif // DEBUG
GenTree* target = getCallTarget(call, &params.methHnd);

if (target != nullptr)
{
// A call target can not be a contained indirection
assert(!target->isContainedIndir());

Comment on lines +1472 to +1474
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// A call target can not be a contained indirection
assert(!target->isContainedIndir());

We can't contain anything for WASM calls.

// Codegen should have already evaluated our target node (last) and pushed it onto the stack,
// ready for call_indirect. Consume it.
genConsumeReg(target);

params.callType = EC_INDIR_R;
genEmitCallWithCurrentGC(params);
}
else
{
// If we have no target and this is a call with indirection cell then
// we do an optimization where we load the call address directly from
// the indirection cell instead of duplicating the tree. In BuildCall
// we ensure that get an extra register for the purpose. Note that for
// CFG the call might have changed to
// CORINFO_HELP_DISPATCH_INDIRECT_CALL in which case we still have the
// indirection cell but we should not try to optimize.
WellKnownArg indirectionCellArgKind = WellKnownArg::None;
if (!call->IsHelperCall(compiler, CORINFO_HELP_DISPATCH_INDIRECT_CALL))
{
indirectionCellArgKind = call->GetIndirectionCellArgKind();
}

if (indirectionCellArgKind != WellKnownArg::None)
Copy link
Contributor

Choose a reason for hiding this comment

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

This part of the code doesn't look right. On native platforms:

  1. We have a call with a known target address which also happens to have an indirection cell arg (typical example is interface dispatch).
  2. Without the optimization, we'd generate:
mov r13, <cell> ; it is a constant
mov r14, <cell> ; duplicated here
ld  r14, [r14]
call r14
  1. With the optimization, we get rid of mov r14, <cell> and use r13 directly in ld r14, [r13].

Now, this is perfectly translatable to WASM as well, where we could have, say:

i32.const <cell>
local.tee cell
local.get cell
i32.load

Instead of:

i32.const <cell>
i32.const <cell>
i32.load

But the current code doesn't do this.

I would propose to do this optimization in lowering with regular IR instead seeing as we don't have something like targetAddrReg. It would also get rid of the assumption that the indirection cell arg is last. It implies removing this special case and adding #ifndef TARGET_WASM in LowerCall around if (!hasIndirectionCell).

{
assert(call->IsR2ROrVirtualStubRelativeIndir());

params.callType = EC_INDIR_R;
// params.ireg = targetAddrReg;
genEmitCallWithCurrentGC(params);
}
else
{
// Generate a direct call to a non-virtual user defined or helper method
assert(call->IsHelperCall() || (call->gtCallType == CT_USER_FUNC));

if (call->gtEntryPoint.addr != NULL)
Copy link
Member

Choose a reason for hiding this comment

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

Per the calling convention, for user and helper calls we'll also be invoking them indirectly.

Copy link
Member Author

Choose a reason for hiding this comment

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

Right. I'm not clear on whether that means there won't be an addr filled in

{
NYI_WASM("Call with statically known address");
}
else
{
if (call->IsHelperCall())
{
NYI_WASM("Call helper statically without indirection cell");
CorInfoHelpFunc helperNum = compiler->eeGetHelperNum(params.methHnd);
noway_assert(helperNum != CORINFO_HELP_UNDEF);

CORINFO_CONST_LOOKUP helperLookup = compiler->compGetHelperFtn(helperNum);
params.addr = helperLookup.addr;
assert(helperLookup.accessType == IAT_VALUE);
Comment on lines +1519 to +1524
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

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

There is unreachable code following NYI_WASM. Lines 1519-1524 will never execute because NYI_WASM throws an exception. Either the NYI_WASM call should be removed if this functionality is intended to be implemented, or the unreachable code should be deleted.

Suggested change
CorInfoHelpFunc helperNum = compiler->eeGetHelperNum(params.methHnd);
noway_assert(helperNum != CORINFO_HELP_UNDEF);
CORINFO_CONST_LOOKUP helperLookup = compiler->compGetHelperFtn(helperNum);
params.addr = helperLookup.addr;
assert(helperLookup.accessType == IAT_VALUE);

Copilot uses AI. Check for mistakes.
}
else
{
// Direct call to a non-virtual user function.
params.addr = call->gtDirectCallAddress;
}
}

params.callType = EC_FUNC_TOKEN;
// params.ireg = params.isJump ? rsGetRsvdReg() : REG_RA;

Comment on lines +1534 to +1535
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// params.ireg = params.isJump ? rsGetRsvdReg() : REG_RA;

genEmitCallWithCurrentGC(params);
}
}
}

/*****************************************************************************
* Emit a call to a helper function.
*/
void CodeGen::genEmitHelperCall(unsigned helper, int argSize, emitAttr retSize, regNumber callTargetReg /*= REG_NA */)
{
EmitCallParams params;

CORINFO_CONST_LOOKUP helperFunction = compiler->compGetHelperFtn((CorInfoHelpFunc)helper);
params.ireg = callTargetReg;

if (helperFunction.accessType == IAT_VALUE)
{
params.callType = EC_FUNC_TOKEN;
params.addr = helperFunction.addr;
}
else
{
// TODO-WASM: Just put helperFunction.addr in params.addr and do the indirect load
// further down in the emitter for all IAT_PVALUE calls instead of doing it here
Comment on lines +1558 to +1559
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// TODO-WASM: Just put helperFunction.addr in params.addr and do the indirect load
// further down in the emitter for all IAT_PVALUE calls instead of doing it here

params.addr = nullptr;
assert(helperFunction.accessType == IAT_PVALUE);
void* pAddr = helperFunction.addr;

// Push indirection cell address onto stack for genEmitCall to dereference
GetEmitter()->emitIns_I(INS_i32_const, emitActualTypeSize(TYP_I_IMPL), (cnsval_ssize_t)pAddr);

params.callType = EC_INDIR_R;
}

params.methHnd = compiler->eeFindHelper(helper);
params.argSize = argSize;
params.retSize = retSize;

genEmitCallWithCurrentGC(params);
}

//------------------------------------------------------------------------
// genCodeForCompare: Produce code for a GT_EQ/GT_NE/GT_LT/GT_LE/GT_GE/GT_GT node.
//
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/jit/emit.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3566,6 +3566,8 @@ void emitter::emitSetSecondRetRegGCType(instrDescCGCA* id, emitAttr secondRetSiz
}
#endif // MULTIREG_HAS_SECOND_GC_RET

#ifndef TARGET_WASM

/*****************************************************************************
*
* Allocate an instruction descriptor for an indirect call.
Expand Down Expand Up @@ -3731,6 +3733,8 @@ emitter::instrDesc* emitter::emitNewInstrCallDir(int argCnt,
}
}

#endif // TARGET_WASM

/*****************************************************************************
*
* Be very careful, some instruction descriptors are allocated as "tiny" and
Expand Down
21 changes: 11 additions & 10 deletions src/coreclr/jit/emitfmtswasm.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,17 @@ enum ID_OPS
// (unused)
//////////////////////////////////////////////////////////////////////////////

IF_DEF(NONE, IS_NONE, NONE)
IF_DEF(OPCODE, IS_NONE, NONE) // <opcode>
IF_DEF(BLOCK, IS_NONE, NONE) // <opcode> <0x40>
IF_DEF(RAW_ULEB128, IS_NONE, NONE) // <ULEB128 immediate>
IF_DEF(ULEB128, IS_NONE, NONE) // <opcode> <ULEB128 immediate>
IF_DEF(SLEB128, IS_NONE, NONE) // <opcode> <LEB128 immediate (signed)>
IF_DEF(F32, IS_NONE, NONE) // <opcode> <f32 immediate (stored as 64-bit integer constant)>
IF_DEF(F64, IS_NONE, NONE) // <opcode> <f64 immediate (stored as 64-bit integer constant)>
IF_DEF(MEMARG, IS_NONE, NONE) // <opcode> <memarg> (<align> <offset>)
IF_DEF(LOCAL_DECL, IS_NONE, NONE) // <ULEB128 immediate> <byte>
IF_DEF(NONE, IS_NONE, NONE)
IF_DEF(OPCODE, IS_NONE, NONE) // <opcode>
IF_DEF(BLOCK, IS_NONE, NONE) // <opcode> <0x40>
IF_DEF(RAW_ULEB128, IS_NONE, NONE) // <ULEB128 immediate>
IF_DEF(ULEB128, IS_NONE, NONE) // <opcode> <ULEB128 immediate>
IF_DEF(SLEB128, IS_NONE, NONE) // <opcode> <LEB128 immediate (signed)>
IF_DEF(F32, IS_NONE, NONE) // <opcode> <f32 immediate (stored as 64-bit integer constant)>
IF_DEF(F64, IS_NONE, NONE) // <opcode> <f64 immediate (stored as 64-bit integer constant)>
IF_DEF(MEMARG, IS_NONE, NONE) // <opcode> <memarg> (<align> <offset>)
IF_DEF(LOCAL_DECL, IS_NONE, NONE) // <ULEB128 immediate> <byte>
IF_DEF(CALL_INDIRECT, IS_NONE, NONE) // <opcode> <ULEB128 immediate> <ULEB128 immediate>

#undef IF_DEF
#endif // !DEFINE_ID_OPS
Expand Down
Loading
Loading