Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
0b80545
Add codegen changes to make calls through PortableEntryPoints stored …
davidwrighton Apr 7, 2026
9c88d47
Add additional comment about the need for second level of indirection…
adamperlin Apr 9, 2026
7141714
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Apr 10, 2026
1878b53
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Apr 15, 2026
7ca6386
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Apr 15, 2026
5e4a83f
WIP Lowering::LowerPEPCall for Wasm
adamperlin Apr 16, 2026
26af840
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Apr 16, 2026
b42c336
Add more specific JITDUMP messages
adamperlin Apr 17, 2026
f2b96b6
Remove boilerplate for getting gtControlExpr use, better JITDUMP mess…
adamperlin Apr 21, 2026
83015d2
Remove addition of R2R indirection cell arg in morph for Wasm; remove…
adamperlin Apr 21, 2026
df103cf
Fix assert in lower.cpp, remove wasm case in GetIndirectionCellKind()…
adamperlin Apr 24, 2026
d6e2cbe
Merge branch 'main' of github.com:dotnet/runtime into adamperlin/wasm…
adamperlin Apr 24, 2026
b2f6e14
jit-format
adamperlin Apr 24, 2026
0cdc541
Update src/coreclr/jit/lower.cpp
adamperlin Apr 24, 2026
4a4ae2c
Use INS_I_LOAD in genEmitHelperCall to ensure compatibility with wasm64
adamperlin Apr 27, 2026
3981152
Merge branch 'adamperlin/wasm-fix-call-codegen' of github.com:adamper…
adamperlin Apr 27, 2026
4a60460
Move LowerPEPCall def into lowerwasm.cpp
adamperlin Apr 27, 2026
d74680e
More review feedback
adamperlin Apr 27, 2026
29cf060
Update src/coreclr/jit/lowerwasm.cpp
adamperlin Apr 27, 2026
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
68 changes: 26 additions & 42 deletions src/coreclr/jit/codegenwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2515,6 +2515,9 @@ void CodeGen::genCallInstruction(GenTreeCall* call)

params.wasmSignature = m_compiler->info.compCompHnd->getWasmTypeSymbol(typeStack.Data(), typeStack.Height());

// A non-null target expression always indicates an indirect call on Wasm,
// as currently the only possible result of the target expression would be a
// table index which must be used via call_indirect
if (target != nullptr)
{
// Codegen should have already evaluated our target node (last) and pushed it onto the stack,
Expand All @@ -2526,52 +2529,28 @@ void CodeGen::genCallInstruction(GenTreeCall* call)
}
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(CORINFO_HELP_DISPATCH_INDIRECT_CALL))
{
indirectionCellArgKind = call->GetIndirectionCellArgKind();
}
// Generate a direct call to a non-virtual user defined or helper method
assert(call->IsHelperCall() || (call->gtCallType == CT_USER_FUNC));

if (indirectionCellArgKind != WellKnownArg::None)
{
assert(call->IsR2ROrVirtualStubRelativeIndir());
assert(call->gtEntryPoint.addr == NULL);

params.callType = EC_INDIR_R;
// params.ireg = targetAddrReg;
genEmitCallWithCurrentGC(params);
if (call->IsHelperCall())
{
assert(!call->IsFastTailCall());
CorInfoHelpFunc helperNum = m_compiler->eeGetHelperNum(params.methHnd);
noway_assert(helperNum != CORINFO_HELP_UNDEF);
CORINFO_CONST_LOOKUP helperLookup = m_compiler->compGetHelperFtn(helperNum);
assert(helperLookup.accessType == IAT_VALUE);
params.addr = helperLookup.addr;
}
else
{
// Generate a direct call to a non-virtual user defined or helper method
assert(call->IsHelperCall() || (call->gtCallType == CT_USER_FUNC));

assert(call->gtEntryPoint.addr == NULL);

if (call->IsHelperCall())
{
assert(!call->IsFastTailCall());
CorInfoHelpFunc helperNum = m_compiler->eeGetHelperNum(params.methHnd);
noway_assert(helperNum != CORINFO_HELP_UNDEF);
CORINFO_CONST_LOOKUP helperLookup = m_compiler->compGetHelperFtn(helperNum);
assert(helperLookup.accessType == IAT_VALUE);
params.addr = helperLookup.addr;
}
else
{
// Direct call to a non-virtual user function.
params.addr = call->gtDirectCallAddress;
}

params.callType = EC_FUNC_TOKEN;
genEmitCallWithCurrentGC(params);
// Direct call to a non-virtual user function.
params.addr = call->gtDirectCallAddress;
}

params.callType = EC_FUNC_TOKEN;
genEmitCallWithCurrentGC(params);
}
}

Expand Down Expand Up @@ -2676,16 +2655,21 @@ void CodeGen::genEmitHelperCall(unsigned helper, int argSize, emitAttr retSize,
if (helperIsManaged)
{
// Push PEP onto the stack because we are calling a managed helper that expects it as the last parameter.
// The helper function address is the address of an indirection cell, so we load from the cell to get the PEP
// address to push.
assert(helperFunction.accessType == IAT_PVALUE);
GetEmitter()->emitAddressConstant(helperFunction.addr);
GetEmitter()->emitIns_I(INS_I_load, EA_PTRSIZE, 0);
}

if (params.callType == EC_INDIR_R)
{
// Push the call target onto the wasm evaluation stack by dereferencing the PEP.
// Push the call target onto the wasm evaluation stack by dereferencing the indirection cell
// and then the PEP pointed to by the indirection cell.
assert(helperFunction.accessType == IAT_PVALUE);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See if it's possible to use the new ADDR_REL reloc type for this to avoid the i32.const <address>; i32.load pattern.

GetEmitter()->emitAddressConstant(helperFunction.addr);
GetEmitter()->emitIns_I(INS_i32_load, EA_PTRSIZE, 0);
GetEmitter()->emitIns_I(INS_I_load, EA_PTRSIZE, 0);
GetEmitter()->emitIns_I(INS_I_load, EA_PTRSIZE, 0);
}
Comment thread
adamperlin marked this conversation as resolved.

genEmitCallWithCurrentGC(params);
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/gentree.h
Original file line number Diff line number Diff line change
Expand Up @@ -5761,7 +5761,7 @@ struct GenTreeCall final : public GenTree
return WellKnownArg::VirtualStubCell;
}

#if defined(TARGET_ARMARCH) || defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64) || defined(TARGET_WASM)
#if defined(TARGET_ARMARCH) || defined(TARGET_RISCV64) || defined(TARGET_LOONGARCH64)
// For ARM architectures, we always use an indirection cell for R2R calls.
if (IsR2RRelativeIndir() && !IsDelegateInvoke())
{
Expand Down
9 changes: 9 additions & 0 deletions src/coreclr/jit/lower.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3022,6 +3022,15 @@ GenTree* Lowering::LowerCall(GenTree* node)
}
}

#ifdef TARGET_WASM
// For any type of managed call, if we have portable entry points enabled, we need to lower
// the call according to the portable entrypoint abi
if (!call->IsUnmanaged() && m_compiler->opts.jitFlags->IsSet(JitFlags::JIT_FLAG_PORTABLE_ENTRY_POINTS))
{
LowerPEPCall(call);
}
#endif // TARGET_WASM

if (varTypeIsStruct(call))
{
LowerCallStruct(call);
Expand Down
7 changes: 5 additions & 2 deletions src/coreclr/jit/lower.h
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,11 @@ class Lowering final : public Phase
bool LowerCallMemcmp(GenTreeCall* call, GenTree** next);
bool LowerCallMemset(GenTreeCall* call, GenTree** next);
void LowerCFGCall(GenTreeCall* call);
void MovePutArgNodesUpToCall(GenTreeCall* call);
void MovePutArgUpToCall(GenTreeCall* call, GenTree* node);
#ifdef TARGET_WASM
void LowerPEPCall(GenTreeCall* call);
#endif
void MovePutArgNodesUpToCall(GenTreeCall* call);
void MovePutArgUpToCall(GenTreeCall* call, GenTree* node);
#ifndef TARGET_64BIT
GenTree* DecomposeLongCompare(GenTree* cmp);
#endif
Expand Down
65 changes: 65 additions & 0 deletions src/coreclr/jit/lowerwasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,71 @@ bool Lowering::IsCallTargetInRange(void* addr)
return true;
}

//---------------------------------------------------------------------------------------------
// LowerPEPCall: Lower a call node dispatched through a PortableEntryPoint (PEP)
//
// Given a call node with gtControlExpr representing a call target which is the address of a portable entrypoint,
// this function lowers the call to appropriately dispatch through the portable entrypoint using the Portable
// entrypoint calling convention.
// To do this, it:
// 1. Introduces a new local variable to hold the PEP address
// 2. Adds a new well-known argument to the call passing this local
// 3. Rewrites the control expression to indirect through the new local, since for PEP's, the actual call target
// must be loaded from the portable entry point address.
//
// Arguments:
// call - The call node to lower. It is expected that the call node has gtControlExpr set to the original
// call target and that the call does not have a PEP arg already.
//
// Return Value:
// None. The call node is modified in place.
//
void Lowering::LowerPEPCall(GenTreeCall* call)
{
JITDUMP("Begin lowering PEP call\n");
DISPTREERANGE(BlockRange(), call);

// PEP call must always have a control expression
assert(call->gtControlExpr != nullptr);
LIR::Use callTargetUse(BlockRange(), &call->gtControlExpr, call);

JITDUMP("Creating new local variable for PEP");
unsigned int callTargetLclNum = callTargetUse.ReplaceWithLclVar(m_compiler);
GenTreeLclVar* callTargetLclForArg = m_compiler->gtNewLclvNode(callTargetLclNum, TYP_I_IMPL);
DISPTREE(call);

JITDUMP("Add new arg to call arg list corresponding to PEP target");
NewCallArg pepTargetArg =
NewCallArg::Primitive(callTargetLclForArg).WellKnown(WellKnownArg::WasmPortableEntryPoint);
CallArg* pepArg = call->gtArgs.PushBack(m_compiler, pepTargetArg);

pepArg->SetEarlyNode(nullptr);
pepArg->SetLateNode(callTargetLclForArg);
call->gtArgs.PushLateBack(pepArg);

// Set up ABI information for this arg; PEP's should be passed as the last param to a wasm function
unsigned pepIndex = call->gtArgs.CountArgs() - 1;
regNumber pepReg = MakeWasmReg(pepIndex, WasmValueType::I);
pepArg->AbiInfo =
ABIPassingInformation::FromSegmentByValue(m_compiler,
ABIPassingSegment::InRegister(pepReg, 0, TARGET_POINTER_SIZE));
BlockRange().InsertBefore(call, callTargetLclForArg);

// Lower the new PEP arg now that the call abi info is updated and lcl var is inserted
LowerArg(call, pepArg);
DISPTREE(call);

JITDUMP("Rewrite PEP call's control expression to indirect through the new local variable\n");
// Rewrite the call's control expression to have an additional load from the PEP local
GenTree* controlExpr = call->gtControlExpr;
GenTree* target = Ind(controlExpr);
BlockRange().InsertAfter(controlExpr, target);
call->gtControlExpr = target;

JITDUMP("Finished lowering PEP call\n");
DISPTREERANGE(BlockRange(), call);
}

//------------------------------------------------------------------------
// IsContainableImmed: Is an immediate encodable in-place?
//
Expand Down
21 changes: 12 additions & 9 deletions src/coreclr/jit/morph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1756,19 +1756,27 @@ void CallArgs::AddFinalArgsAndDetermineABIInfo(Compiler* comp, GenTreeCall* call
// That's ok; after making something a tailcall, we will invalidate this information
// and reconstruct it if necessary. The tailcalling decision does not change since
// this is a non-standard arg in a register.
bool needsIndirectionCell = call->IsR2RRelativeIndir() && !call->IsDelegateInvoke();
#ifndef TARGET_WASM
bool needsIndirectionCellArg = call->IsR2RRelativeIndir() && !call->IsDelegateInvoke();

#if defined(TARGET_XARCH)
needsIndirectionCell &= call->IsFastTailCall();
needsIndirectionCellArg &= call->IsFastTailCall();
#endif

#else
// TARGET_WASM does not use an explicit indirection cell arg for the R2R calling convention,
// the address of the indirection cell is recoverable from the portable entrypoint which
// we pass as part of the Wasm managed calling convention (See LowerPEPCall).
bool needsIndirectionCellArg = false;
#endif
Comment thread
adamperlin marked this conversation as resolved.

if (needsIndirectionCell)
if (needsIndirectionCellArg)
{
assert(call->gtEntryPoint.addr != nullptr);

size_t addrValue = (size_t)call->gtEntryPoint.addr;
GenTree* indirectCellAddress = comp->gtNewIconHandleNode(addrValue, GTF_ICON_FTN_ADDR);
INDEBUG(indirectCellAddress->AsIntCon()->gtTargetHandle = (size_t)call->gtCallMethHnd);

#ifdef TARGET_ARM
// TODO-ARM: We currently do not properly kill this register in LSRA
// (see getKillSetForCall which does so only for VSD calls).
Expand All @@ -1781,12 +1789,7 @@ void CallArgs::AddFinalArgsAndDetermineABIInfo(Compiler* comp, GenTreeCall* call
// Push the stub address onto the list of arguments.
NewCallArg indirCellAddrArg =
NewCallArg::Primitive(indirectCellAddress).WellKnown(WellKnownArg::R2RIndirectionCell);
#ifdef TARGET_WASM
// On wasm we need to ensure we put the indirection cell address last in LIR, after the SP and formal args.
PushBack(comp, indirCellAddrArg);
#else
InsertAfterThisOrFirst(comp, indirCellAddrArg);
#endif // TARGET_WASM
}
#endif // defined(FEATURE_READYTORUN)

Expand Down
Loading