From 134b0254437957fae7d7491152d2689e2dcf5e4a Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 22 Jun 2026 21:01:44 +0200 Subject: [PATCH 1/3] [mono] AOT-compile methods that use the IL jmp opcode The IL `jmp` opcode (CEE_JMP) is compiled to a real tail call (OP_TAILCALL) on the non-llvm_only path, which disables AOT for the whole method (DISABLE_AOT). Under full-AOT (aot-only) the method is then left out of the AOT image, so the first call to it fails at runtime with "Attempting to JIT compile method ... while running in aot-only mode". The llvm_only path already avoids this by emitting `jmp` as a normal call followed by a return. Extend that to AOT compilation: when compiling for AOT, emit the jmp as a call + return instead of a real tail call, so the method can be AOT compiled. This is observably equivalent for jmp (transfer to the target with the current arguments and return its result); it just does not reuse the caller's stack frame. The JIT keeps the real tail call. Fixes the Mono full-AOT failures of the JIT/jit64/localloc/call/call05_large and call05_small tests, which were crashing with ExecutionEngineException under full-AOT because their `jmp` method was excluded from the AOT image. --- src/mono/mono/mini/method-to-ir.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/mono/mono/mini/method-to-ir.c b/src/mono/mono/mini/method-to-ir.c index f4e661edd4eea2..5bf2ef495d4c19 100644 --- a/src/mono/mono/mini/method-to-ir.c +++ b/src/mono/mono/mini/method-to-ir.c @@ -7493,13 +7493,21 @@ mono_method_to_ir (MonoCompile *cfg, MonoMethod *method, MonoBasicBlock *start_b fsig = mono_method_signature_internal (cmethod); int nargs = fsig->param_count + fsig->hasthis; - if (cfg->llvm_only) { + if (cfg->llvm_only || cfg->compile_aot) { MonoInst **args; + /* + * A real tailcall (OP_TAILCALL) disables AOT for the method (see DISABLE_AOT + * below), which under aot-only/full-AOT leaves the method out of the image and + * crashes at runtime. For AOT (and llvm_only) emit the jmp as a normal call + * followed by a return instead. This is observably equivalent for jmp (transfer + * to the target with the current arguments and return its result); it just does + * not reuse the caller's stack frame. Keep the real tailcall for the JIT. + */ args = (MonoInst **)mono_mempool_alloc (cfg->mempool, sizeof (MonoInst*) * nargs); for (int i = 0; i < nargs; ++i) EMIT_NEW_ARGLOAD (cfg, args [i], i); - ins = mini_emit_method_call_full (cfg, cmethod, fsig, TRUE, args, NULL, NULL, NULL); + ins = mini_emit_method_call_full (cfg, cmethod, fsig, cfg->llvm_only, args, NULL, NULL, NULL); /* * The code in mono-basic-block.c treats the rest of the code as dead, but we * have to emit a normal return since llvm expects it. From 525162ac03eed52af874cba04fe6e3856e66829d Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Mon, 22 Jun 2026 21:16:06 +0200 Subject: [PATCH 2/3] Re-enable call05_large/call05_small on Mono These tests use the IL jmp opcode from a method that also uses localloc; with the AOT compiler now emitting such methods, they pass under Mono full-AOT. Remove the ActiveIssue annotation so CI exercises the fix. --- src/tests/JIT/jit64/localloc/call/call05_large.il | 4 ---- src/tests/JIT/jit64/localloc/call/call05_small.il | 4 ---- 2 files changed, 8 deletions(-) diff --git a/src/tests/JIT/jit64/localloc/call/call05_large.il b/src/tests/JIT/jit64/localloc/call/call05_large.il index c4b74924e68fa6..d68c92f0d227a7 100644 --- a/src/tests/JIT/jit64/localloc/call/call05_large.il +++ b/src/tests/JIT/jit64/localloc/call/call05_large.il @@ -21,10 +21,6 @@ .custom instance void [xunit.core]Xunit.FactAttribute::.ctor() = ( 01 00 00 00 ) - .custom instance void [Microsoft.DotNet.XUnitExtensions]Xunit.ActiveIssueAttribute::.ctor(string, valuetype [Microsoft.DotNet.XUnitExtensions]Xunit.TestRuntimes) = { - string('https://github.com/dotnet/runtime/issues/129508') - int32(0x2) // Mono - } .entrypoint .maxstack 11 .locals (int32* intArray1, diff --git a/src/tests/JIT/jit64/localloc/call/call05_small.il b/src/tests/JIT/jit64/localloc/call/call05_small.il index d78aaeb6e2882c..7b11708e44b8ff 100644 --- a/src/tests/JIT/jit64/localloc/call/call05_small.il +++ b/src/tests/JIT/jit64/localloc/call/call05_small.il @@ -21,10 +21,6 @@ .custom instance void [xunit.core]Xunit.FactAttribute::.ctor() = ( 01 00 00 00 ) - .custom instance void [Microsoft.DotNet.XUnitExtensions]Xunit.ActiveIssueAttribute::.ctor(string, valuetype [Microsoft.DotNet.XUnitExtensions]Xunit.TestRuntimes) = { - string('https://github.com/dotnet/runtime/issues/129508') - int32(0x2) // Mono - } .entrypoint .maxstack 11 .locals (int32* intArray1, From eeb91ef7a2dff543aef9e712c5f93e9e47161e94 Mon Sep 17 00:00:00 2001 From: pavelsavara Date: Tue, 23 Jun 2026 13:24:30 +0200 Subject: [PATCH 3/3] [mono] Re-enable iltests.il jmp test under full-AOT; refine jmp comment test_5_jmp was tagged !FULLAOT because jmp could not be AOT-compiled; this PR fixes that, so run it under full-AOT for regression coverage. Also soften the lowering comment: emitting jmp as call+return is not a true tailcall (it adds a stack frame, so stack traces can differ). --- src/mono/mono/mini/iltests.il | 3 --- src/mono/mono/mini/method-to-ir.c | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/mono/mono/mini/iltests.il b/src/mono/mono/mini/iltests.il index 12e7a845589c54..b96973d9c00adb 100644 --- a/src/mono/mono/mini/iltests.il +++ b/src/mono/mono/mini/iltests.il @@ -743,9 +743,6 @@ COND: ldloc.0 } .method public static int32 test_5_jmp () cil managed { - // Some platforms might not be able to AOT tail calls - .custom instance void class [TestDriver]CategoryAttribute::'.ctor'(string) = (01 00 08 21 46 55 4C 4C 41 4F 54 00 00 ) // ...!FULLAOT.. - ldc.i4.1 ldc.i4.2 call int32 Tests::jmp2 (int32, int32) diff --git a/src/mono/mono/mini/method-to-ir.c b/src/mono/mono/mini/method-to-ir.c index 5bf2ef495d4c19..847466dc18da04 100644 --- a/src/mono/mono/mini/method-to-ir.c +++ b/src/mono/mono/mini/method-to-ir.c @@ -7500,9 +7500,10 @@ mono_method_to_ir (MonoCompile *cfg, MonoMethod *method, MonoBasicBlock *start_b * A real tailcall (OP_TAILCALL) disables AOT for the method (see DISABLE_AOT * below), which under aot-only/full-AOT leaves the method out of the image and * crashes at runtime. For AOT (and llvm_only) emit the jmp as a normal call - * followed by a return instead. This is observably equivalent for jmp (transfer - * to the target with the current arguments and return its result); it just does - * not reuse the caller's stack frame. Keep the real tailcall for the JIT. + * followed by a return instead. This preserves the jmp semantics that matter + * here (transfer to the target with the current arguments and return its + * result), but it is not a true tailcall: it adds a stack frame, so observable + * details such as stack traces can differ. Keep the real tailcall for the JIT. */ args = (MonoInst **)mono_mempool_alloc (cfg->mempool, sizeof (MonoInst*) * nargs); for (int i = 0; i < nargs; ++i)