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); diff --git a/crates/passes/krio_adapter/src/abi_emit.rs b/crates/passes/krio_adapter/src/abi_emit.rs index f917f5e..affdd06 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() { @@ -2855,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)" + ); + } +}