Skip to content

Conversation

@kg
Copy link
Member

@kg kg commented Jan 9, 2026

Depends on #123021

Starting to put together all the code we need for helper calls and calls in general.

@kg kg added arch-wasm WebAssembly architecture area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI labels Jan 9, 2026
@dotnet-policy-service
Copy link
Contributor

Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.

Copy link
Member

@AndyAyersMS AndyAyersMS left a comment

Choose a reason for hiding this comment

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

We still need to figure out in detail how to specify what method we want to call.

We know roughly what kind of Wasm we want to produce (per the calling convention doc we'll be making lots of indirect calls) but we need help from the JIT host to make this all work out.

I think it might be ok for now to just defer this part of the work and try and get everything else lined up?

// 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

@kg kg force-pushed the wasm-genemithelpercall branch from b95ce18 to 499127d Compare January 16, 2026 04:26
@kg
Copy link
Member Author

kg commented Jan 16, 2026

This branch now hits this in crossgen2:

Z:\runtime\src\coreclr\jit\codegenwasm.cpp:1123
Assertion failed 'NYI_WASM: load call target from indirection cell in register' in 'Program:callVoidFunc()' during 'Generate code' (IL size 8; hash 0x4bd04ee2; MinOpts)

@kg
Copy link
Member Author

kg commented Jan 16, 2026

For this:

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void voidFunc () {
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static void callVoidFunc () {
        voidFunc();
    }

R2R now compiles without crashing or asserting, and disasmo generates this:

; Method Program:callVoidFunc() (FullOpts)
G_M45341_IG01:  ;; offset=0x0000
            local.cnt 4
            local[0] type=i32
            local[1] type=i64
            local[2] type=f32
            local[3] type=f64
						;; size=9 bbWeight=1 PerfScore 5.00

G_M45341_IG02:  ;; offset=0x0009
            i32.const 140722950757336
            i32.load 0 0
            call_indirect 0 0
						;; size=14 bbWeight=1 PerfScore 3.00

G_M45341_IG03:  ;; offset=0x0017
            end
						;; size=1 bbWeight=1 PerfScore 1.00
; Total bytes of code: 24

It's not correct but it's a start.

@kg
Copy link
Member Author

kg commented Jan 16, 2026

cc @dotnet/jit-contrib need to figure out how this should actually be factored and shaped and implemented, now that it "works". I don't understand 75% of the code I touched here, for example whether we use EA_8BYTE or a different one still makes no sense to me, and the way we historically separate out emitNewInstrXxx from emitIns_Xxx seems arbitrary. But this is my best guess at how everything should be arranged to make sense for wasm.

@AndyAyersMS
Copy link
Member

@kg what do we need to do to unblock this?

I don't think we'll have the crossgen2 relocation stuff around for a bit, so emitting long-form placeholder values for the various indices/offsets is the best we can do. If you want to try getting the reloc machinery started with this PR then we can look into that too.

@kg
Copy link
Member Author

kg commented Jan 22, 2026

@kg what do we need to do to unblock this?

I don't think we'll have the crossgen2 relocation stuff around for a bit, so emitting long-form placeholder values for the various indices/offsets is the best we can do. If you want to try getting the reloc machinery started with this PR then we can look into that too.

As far as I'm concerned it's done, but I was waiting for someone to review it on the presumption that there's a bunch of things wrong in this PR since I didn't really know what I was doing.

@AndyAyersMS
Copy link
Member

@kg what do we need to do to unblock this?
I don't think we'll have the crossgen2 relocation stuff around for a bit, so emitting long-form placeholder values for the various indices/offsets is the best we can do. If you want to try getting the reloc machinery started with this PR then we can look into that too.

As far as I'm concerned it's done, but I was waiting for someone to review it on the presumption that there's a bunch of things wrong in this PR since I didn't really know what I was doing.

We should be able to do some testing, especially now that #123262 is in. What happens if we try and compile a method that has a call (eg fact or something equally simple).

@kg
Copy link
Member Author

kg commented Jan 22, 2026

@kg what do we need to do to unblock this?
I don't think we'll have the crossgen2 relocation stuff around for a bit, so emitting long-form placeholder values for the various indices/offsets is the best we can do. If you want to try getting the reloc machinery started with this PR then we can look into that too.

As far as I'm concerned it's done, but I was waiting for someone to review it on the presumption that there's a bunch of things wrong in this PR since I didn't really know what I was doing.

We should be able to do some testing, especially now that #123262 is in. What happens if we try and compile a method that has a call (eg fact or something equally simple).

Is this what you mean? #123044 (comment)

break;

case GT_PUTARG_REG:
genPutArgReg(treeNode->AsOp());
Copy link
Contributor

Choose a reason for hiding this comment

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

Should be removed.

Copy link
Member Author

Choose a reason for hiding this comment

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

I needed this for builds to work, did we remove putarg_reg in another PR?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, we removed it in #123262.

Comment on lines +995 to +1001
// Insert a null check on "this" pointer if asked.
if (call->NeedsNullCheck())
{
// TODO-WASM: Ensure that when the this-arg is pushed onto the stack earlier, we perform the null check there.
// Once that's done this NYI can go away.
NYI_WASM("this ptr null check in genCall");
}
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
// Insert a null check on "this" pointer if asked.
if (call->NeedsNullCheck())
{
// TODO-WASM: Ensure that when the this-arg is pushed onto the stack earlier, we perform the null check there.
// Once that's done this NYI can go away.
NYI_WASM("this ptr null check in genCall");
}
if (call->NeedsNullCheck())
{
NYI_WASM("Insert nullchecks for calls that need it in lowering");
}

Copy link
Member Author

Choose a reason for hiding this comment

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

I think NeedsNullCheck will still be true even if we inserted it, right? Or will we clear that flag when inserting it?

Copy link
Contributor

Choose a reason for hiding this comment

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

We will clear it. This is similar to what happens when we discover there is a natural dominating null check.

Comment on lines +1003 to +1019
// TODO-WASM: Handle "fast tail calls" here if we decide to implement them.

genCallInstruction(call);

// TODO-WASM: Disabled in WASM currently. Not sure if we need it
// genDefinePendingCallLabel(call);

var_types returnType = call->TypeGet();
if (returnType != TYP_VOID)
{
if (call->HasMultiRegRetVal())
{
NYI_WASM("multi-reg return values");
}

genProduceReg(call);
}
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: Handle "fast tail calls" here if we decide to implement them.
genCallInstruction(call);
// TODO-WASM: Disabled in WASM currently. Not sure if we need it
// genDefinePendingCallLabel(call);
var_types returnType = call->TypeGet();
if (returnType != TYP_VOID)
{
if (call->HasMultiRegRetVal())
{
NYI_WASM("multi-reg return values");
}
genProduceReg(call);
}
assert(!call->IsTailCall());
genCallInstruction(call);
genProduceReg(call);

I suspect the logic that skips genProduceReg for VOID calls is not entirely sound either, but in a way that's 'mostly fine' (the reason it should nominally be needed is because calls can induce liveness changes with return buffer locals).

genDefinePendingCallLabel is used to implement GT_LABEL and things that rely on the return address being available, so, not going to be needed for WASM.

Copy link
Member Author

Choose a reason for hiding this comment

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

We're not going to do tail calls? Or should I make the assert a NYI_WASM?

Copy link
Contributor

Choose a reason for hiding this comment

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

Currently, we do not expect to see tailcalls here because we don't have fast tailcalls and without that morph will fall back to the "portable" tailcalling mechanism, which is not implemented for CG2, which in turn means that explicit (.tail) tailcalls will silently be converted into regular tailcalls.

For this kind of case, an assert I think is better since seeing a tailcall here is an "unexpected" scenario, not an "expected but not handled" one.

In the long run, we will have two kinds of tailcalls:

  1. 'Fast' / 'implicit': discards the caller's linear memory frame, emits call.
  2. 'Explicit' (.tail): emits return_call, a "true" tailcall.

Currently though we only have one concept for a tailcall, so, this long term thing will need some work.

EC_MANAGED_PTR, // call_indirect to an unknown managed method or helper (two levels of indirection - ptr to
// arbitrary cell)
// should generate: <push args>; call <func-index>
EC_FUNCTION_INDEX, // call to a known wasm import or function from same module, no indirection
Copy link
Contributor

@SingleAccretion SingleAccretion Jan 22, 2026

Choose a reason for hiding this comment

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

For WASM we only have direct call and indirect call_indirect, which already map to EC_FUNC_TOKEN and EC_INDIR_R, so I don't think we need these kinds to be under ifdefs.

Copy link
Member Author

Choose a reason for hiding this comment

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

We don't have the concept of function pointers in wasm? Or is the idea that the extra level of indirection will be handled somewhere else?

Copy link
Contributor

Choose a reason for hiding this comment

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

The proposal is to use EC_FUNC_TOKEN for what is currently called EC_FUNCTION_INDEX, remove EC_MANAGED_CELL and EC_MANAGED_PTR by handling the indirections they represent in the caller(s), and using EC_INDIR_R to represent "emit call_indirect".

The overall principle is to have emitIns_Call only emit one WASM instruction, the call itself. It is (more) consistent with how other targets represent these things.

// cell to support interpretation)
// should generate: <push args>; <push address-of-cell-address>; i32.load 0 0; i32.load 0 0; call_indirect
// <signature-type>
EC_MANAGED_PTR, // call_indirect to an unknown managed method or helper (two levels of indirection - ptr to
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this should be an emitter concept. The design philosophy for the WASM emitter so far has been "one WASM instruction <=> one emitIns_XYZ method". It means handling this at the caller level.

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(ULEB128_X2, IS_NONE, NONE) // <opcode> <ULEB128 immediate> <ULEB128 immediate>
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we need this for indirect calls. The table index/symbol will always be __indirect_function_table_table, we can just hardcode it.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can you think of any other cases where we need it? If not, I'll remove it. Maybe catch since it takes a tag idx and a label idx? table.copy/table.init? memory.copy/memory.init? Out of these only memory.init seems like we'll use it with two nonzero values, and it's possible one of them will always be zero (the second one).

Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see anything that would require it immediately. Of the ones you list, I think catch is the only candidate, but it's a part of try_table, not a standalone instruction.

I view this case as being very similar to IF_BLOCK: you still need the new format (to append the table index), but you don't need a separate instrDesc kind (since the index is not variable).

Comment on lines +1535 to +1540
#ifdef TARGET_WASM
// We have no special registers, so it's not correct for this function to ever return true,
// and callers will just break if it does.
return false;
#endif

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
#ifdef TARGET_WASM
// We have no special registers, so it's not correct for this function to ever return true,
// and callers will just break if it does.
return false;
#endif

I don't think this is needed anymore (#if HAS_FIXED_REGISTER_SET below should be taking care of it).

Copy link
Member Author

Choose a reason for hiding this comment

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

It was needed before, but I'll check, thanks.

@AndyAyersMS
Copy link
Member

We should be able to do some testing, especially now that #123262 is in. What happens if we try and compile a method that has a call (eg fact or something equally simple).

Is this what you mean? #123044 (comment)

Yes --make sure that basic cases look plausible

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants