From 345266deec69b9fde0e97a28154de5012628ebaf Mon Sep 17 00:00:00 2001 From: darmie Date: Wed, 29 Jul 2026 10:58:47 +0100 Subject: [PATCH 1/3] fix(jit/win64): use platform call conv for closure indirect calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CallClosure handler built its indirect-call signature with a hard- coded CallConv::SystemV. On x86_64-pc-windows-msvc the callee is compiled WindowsFastcall, so arguments landed in SysV registers (rdi, rsi) while the callee read MS x64 registers (rcx, rdx). The async poll fn is reached via CreateClosure -> CallClosure, so every JIT-executed async function on Windows read a garbage state-machine pointer and returned garbage (async_execution_tests / promise_combinator_tests: double(21) -> garbage instead of 42). Build the signature from module.make_signature() so it carries the platform default calling convention, matching the callee — the same fix already applied to the sibling indirect-call site. No-op on Unix (default is SystemV there); async + effect suites still green on macOS/Linux. --- crates/compiler/src/cranelift_backend.rs | 35 +++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/compiler/src/cranelift_backend.rs b/crates/compiler/src/cranelift_backend.rs index da0179b..843641b 100644 --- a/crates/compiler/src/cranelift_backend.rs +++ b/crates/compiler/src/cranelift_backend.rs @@ -3871,26 +3871,29 @@ impl CraneliftBackend { call_args.push(arg_val); } - // Step 5: Create function signature for indirect call - // Build signature: first parameter is always self (data pointer) - let mut param_types: Vec = vec![AbiParam::new(ptr_type)]; // self + // Step 5: Create function signature for indirect call. + // Start from the module's default calling convention + // (the platform ABI: SystemV on Unix x86_64, + // WindowsFastcall on x86_64-msvc, AppleAarch64 on ARM + // macOS) so the indirect call matches how the callee + // was compiled. Hard-coding `SystemV` here made the + // async poll fn — reached via `CreateClosure` — read a + // garbage state-machine pointer on Windows (args landed + // in SysV registers like rdi while the WindowsFastcall + // callee read rcx), returning garbage for every async + // execution. Same root cause and fix as the indirect + // call site above. + let mut sig = self.module.make_signature(); + sig.params.push(AbiParam::new(ptr_type)); // self (data ptr) for _ in args { // TODO: Use actual types from method_sig.params - param_types.push(AbiParam::new(ptr_type)); + sig.params.push(AbiParam::new(ptr_type)); } - - let return_types = if matches!(return_ty, HirType::Void) { - vec![] - } else { + if !matches!(return_ty, HirType::Void) { // TODO: Translate return_ty from method_sig - vec![AbiParam::new(ptr_type)] - }; - - let sig = builder.func.import_signature(Signature { - params: param_types, - returns: return_types, - call_conv: CallConv::SystemV, - }); + sig.returns.push(AbiParam::new(ptr_type)); + } + let sig = builder.func.import_signature(sig); // Step 6: Perform indirect call let call_inst = builder.ins().call_indirect(sig, func_ptr, &call_args); From 94e7339a0a50c075f1e45d0a01b83f9eee336db6 Mon Sep 17 00:00:00 2001 From: darmie Date: Wed, 29 Jul 2026 11:20:28 +0100 Subject: [PATCH 2/3] fix(async/win64): pin generated async entry/poll fns to C calling convention MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The real cause of the Windows async garbage: the krio_adapter-generated async entry (`generate_promise_entry`), sync entry (`generate_sync_entry`), and poll fn (`reshape_to_poll_abi`) inherited HirFunction::new's default `CallingConvention::Fast`. On x86_64 Fast lowers to Cranelift's Fast, which happens to match `extern "C"` (SystemV) on Unix — so Linux/macOS worked — but on x86_64-pc-windows-msvc it does NOT match Win64. The runtime calls these functions through `extern "C"` transmutes, so on Windows the entry read its first argument (and the poll fn its state-machine pointer) from the wrong register, returning garbage for every JIT-executed async function. Pin the three runtime-facing generated functions to `CallingConvention::C` (= platform default = WindowsFastcall on Windows). No-op on Unix. Complements the CallClosure indirect-call fix in the same branch (inner- promise polls). --- crates/passes/krio_adapter/src/abi_emit.rs | 25 +++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/passes/krio_adapter/src/abi_emit.rs b/crates/passes/krio_adapter/src/abi_emit.rs index f917f5e..175e2ff 100644 --- a/crates/passes/krio_adapter/src/abi_emit.rs +++ b/crates/passes/krio_adapter/src/abi_emit.rs @@ -25,9 +25,9 @@ use std::collections::{HashMap, HashSet}; use indexmap::IndexMap; use krio_async::StateMachineLayout; use zyntax_compiler::hir::{ - BinaryOp, CastOp, HirBlock, HirCallable, HirConstant, HirFunction, HirFunctionSignature, - HirFunctionType, HirId, HirInstruction, HirParam, HirStructType, HirTerminator, HirType, - HirValue, HirValueKind, Intrinsic, ParamAttributes, + BinaryOp, CallingConvention, CastOp, HirBlock, HirCallable, HirConstant, HirFunction, + HirFunctionSignature, HirFunctionType, HirId, HirInstruction, HirParam, HirStructType, + HirTerminator, HirType, HirValue, HirValueKind, Intrinsic, ParamAttributes, }; use zyntax_typed_ast::InternedString; @@ -1912,6 +1912,15 @@ pub fn reshape_to_poll_abi( layout: &StateMachineLayout, param_slots: &[(HirId, u32)], ) -> InternedString { + // The runtime invokes the poll fn through an `extern "C"` transmute + // (`fn(*mut u8) -> i64`), so it must carry the platform default + // calling convention. `CallingConvention::Fast` lowers to Cranelift's + // `Fast` (SystemV-flavoured on x86_64 — matches `extern "C"` on Unix + // by coincidence) but on x86_64-msvc it does NOT match Win64, so the + // poll fn would read its state-machine pointer from the wrong + // register. Pin it to `C` (= WindowsFastcall on Windows). + function.calling_convention = CallingConvention::C; + let original_return_ty = function .signature .returns @@ -2255,6 +2264,12 @@ pub fn generate_promise_entry( }; let mut entry = HirFunction::new(original_name, entry_sig); + // The runtime calls this entry through an `extern "C"` transmute + // (`call_with_signature`), so it must use the platform default ABI. + // `HirFunction::new` defaults to `Fast`, which mismatches Win64 on + // x86_64-msvc (args land in SysV registers while the caller uses MS + // x64), making the entry read a garbage first argument on Windows. + entry.calling_convention = CallingConvention::C; // Re-mint param SSA values so they're owned by THIS function. // The new param HirIds match the entry's signature.params.id. @@ -2522,6 +2537,10 @@ pub fn generate_sync_entry( }; let mut entry = HirFunction::new(original_name, entry_sig); + // Runtime-called via `extern "C"` transmute → must use the platform + // default ABI, not `HirFunction::new`'s `Fast` default (mismatches + // Win64 on x86_64-msvc). See `generate_promise_entry`. + entry.calling_convention = CallingConvention::C; // Re-mint params for this function. for (i, p) in original_signature.params.iter().enumerate() { From ae4fdf017af979ed0a90385e5ccab814cd5ef97f Mon Sep 17 00:00:00 2001 From: darmie Date: Wed, 29 Jul 2026 11:36:47 +0100 Subject: [PATCH 3/3] test(async/win64): assert generated async entries use C calling convention Regression guard for the Windows async-garbage fix: the async promise entry and sync entry must carry CallingConvention::C (platform default), not HirFunction::new's Fast default, or JIT-executed async functions read arguments from the wrong registers on x86_64-pc-windows-msvc. --- crates/passes/krio_adapter/src/abi_emit.rs | 74 ++++++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/crates/passes/krio_adapter/src/abi_emit.rs b/crates/passes/krio_adapter/src/abi_emit.rs index 175e2ff..affdd06 100644 --- a/crates/passes/krio_adapter/src/abi_emit.rs +++ b/crates/passes/krio_adapter/src/abi_emit.rs @@ -2874,3 +2874,77 @@ fn mint_const_i64(values: &mut IndexMap, val: i64) -> HirId { HirValueKind::Constant(HirConstant::I64(val)), ) } + +#[cfg(test)] +mod calling_convention_tests { + use super::*; + + /// A minimal `async fn f(x: i32) -> i32` signature plus its param id. + fn one_i32_param_sig() -> (HirFunctionSignature, HirId) { + let pid = HirId::new(); + let sig = HirFunctionSignature { + params: vec![HirParam { + id: pid, + name: InternedString::new_global("x"), + ty: HirType::I32, + attributes: ParamAttributes::default(), + }], + returns: vec![HirType::I32], + type_params: vec![], + const_params: vec![], + lifetime_params: vec![], + is_variadic: false, + is_async: true, + is_fiber: false, + effects: vec![], + is_pure: false, + }; + (sig, pid) + } + + // The runtime invokes async entry/poll functions through `extern "C"` + // transmutes, so they must carry the platform-default calling + // convention (`C`), NOT `HirFunction::new`'s `Fast` default. On x86_64 + // `Fast` is SystemV-flavoured and matches `extern "C"` on Unix by + // coincidence, but on x86_64-pc-windows-msvc it does not match Win64 — + // args land in the wrong registers and every JIT-executed async + // function returns garbage. These guard that fix from silently + // reverting if the generators are refactored. + + #[test] + fn generated_promise_entry_uses_c_calling_convention() { + let (sig, pid) = one_i32_param_sig(); + let entry = generate_promise_entry( + InternedString::new_global("double"), + &sig, + HirId::new(), // poll_fn_id + 4, // num_slots + &[(pid, 1)], // param_slots (param at slot 1) + 0, // state_slot + ); + assert_eq!( + entry.calling_convention, + CallingConvention::C, + "promise entry must use the platform-default ABI (see Win64 fix)" + ); + } + + #[test] + fn generated_sync_entry_uses_c_calling_convention() { + let (sig, pid) = one_i32_param_sig(); + let entry = generate_sync_entry( + InternedString::new_global("double"), + &sig, + HirId::new(), // poll_fn_id + 5, // num_slots + &[(pid, 1)], // param_slots + 0, // state_slot + 2, // refcount_slot + ); + assert_eq!( + entry.calling_convention, + CallingConvention::C, + "sync entry must use the platform-default ABI (see Win64 fix)" + ); + } +}