From 535558bdf847ee1bb7f4c8bb0789053cc1c8c712 Mon Sep 17 00:00:00 2001 From: playX18 Date: Tue, 21 Jul 2026 07:18:10 +0700 Subject: [PATCH 1/4] feat: GHC calling convention --- cranelift/codegen/src/isa/aarch64/abi.rs | 186 +++++++++++- cranelift/codegen/src/isa/call_conv.rs | 19 +- .../codegen/src/isa/pulley_shared/abi.rs | 3 +- cranelift/codegen/src/isa/riscv64/abi.rs | 113 ++++++- cranelift/codegen/src/isa/s390x/abi.rs | 5 + cranelift/codegen/src/isa/x64/abi.rs | 156 +++++++++- cranelift/codegen/src/isa/x64/inst/emit.rs | 23 +- .../codegen/src/isa/x64/inst/emit_state.rs | 18 +- cranelift/codegen/src/machinst/abi.rs | 12 +- cranelift/codegen/src/verifier/mod.rs | 8 + cranelift/docs/ir.md | 67 +++- .../filetests/filetests/isa/aarch64/ghc.clif | 186 ++++++++++++ .../filetests/filetests/isa/riscv64/ghc.clif | 286 ++++++++++++++++++ .../filetests/filetests/isa/x64/ghc.clif | 167 ++++++++++ .../filetests/filetests/verifier/ghc-abi.clif | 22 ++ cranelift/fuzzgen/src/cranelift_arbitrary.rs | 20 +- 16 files changed, 1260 insertions(+), 31 deletions(-) create mode 100644 cranelift/filetests/filetests/isa/aarch64/ghc.clif create mode 100644 cranelift/filetests/filetests/isa/riscv64/ghc.clif create mode 100644 cranelift/filetests/filetests/isa/x64/ghc.clif create mode 100644 cranelift/filetests/filetests/verifier/ghc-abi.clif diff --git a/cranelift/codegen/src/isa/aarch64/abi.rs b/cranelift/codegen/src/isa/aarch64/abi.rs index 806ad1566a92..3fb58052a622 100644 --- a/cranelift/codegen/src/isa/aarch64/abi.rs +++ b/cranelift/codegen/src/isa/aarch64/abi.rs @@ -160,6 +160,7 @@ impl ABIMachineSpec for AArch64MachineDeps { ) -> CodegenResult<(u32, Option)> { let is_apple_cc = call_conv == isa::CallConv::AppleAarch64; let is_winch_return = call_conv == isa::CallConv::Winch && args_or_rets == ArgsOrRets::Rets; + let is_ghc = call_conv == isa::CallConv::Ghc; // See AArch64 ABI (https://github.com/ARM-software/abi-aa/blob/2021Q1/aapcs64/aapcs64.rst#64parameter-passing), sections 6.4. // @@ -190,6 +191,13 @@ impl ABIMachineSpec for AArch64MachineDeps { 0 }; let mut next_vreg = 0; + // GHC uses separate STG register pools (indices into fixed lists), + // matching LLVM CC_AArch64_GHC: ints, f32, f64, and optional 128-bit + // vectors each have their own counter. + let mut next_ghc_x: usize = 0; + let mut next_ghc_f32: usize = 0; + let mut next_ghc_f64: usize = 0; + let mut next_ghc_vec: usize = 0; let mut next_stack: u32 = 0; // Note on return values: on the regular ABI, we may return values @@ -202,6 +210,8 @@ impl ABIMachineSpec for AArch64MachineDeps { let ret_area_ptr = if add_ret_area_ptr { debug_assert_eq!(args_or_rets, ArgsOrRets::Args); + // GHC has void returns only; a return-area pointer is never used. + assert!(!is_ghc); if call_conv != isa::CallConv::Winch { // In the AAPCS64 calling convention the return area pointer is // stored in x8. @@ -243,9 +253,19 @@ impl ABIMachineSpec for AArch64MachineDeps { "support for StructReturn parameters is not implemented for the `tail` \ calling convention yet", ); + if is_ghc { + return Err(crate::CodegenError::Unsupported( + "StructReturn not supported with ghc calling convention".to_owned(), + )); + } } if let ir::ArgumentPurpose::StructArgument(_) = param.purpose { + if is_ghc { + return Err(crate::CodegenError::Unsupported( + "StructArgument not supported with ghc calling convention".to_owned(), + )); + } panic!( "StructArgument parameters are not supported on arm64. \ Use regular pointer arguments instead." @@ -301,6 +321,46 @@ impl ABIMachineSpec for AArch64MachineDeps { "Unable to handle non i64 regs" ); + if is_ghc { + // GHC: void returns only; args must fit in STG registers. + let regs = match args_or_rets { + ArgsOrRets::Rets => None, + ArgsOrRets::Args => { + match ( + get_ghc_intreg_for_arg(next_ghc_x), + get_ghc_intreg_for_arg(next_ghc_x + 1), + ) { + (Some(lo), Some(hi)) => Some((lo, hi)), + _ => None, + } + } + }; + let Some((lower_reg, upper_reg)) = regs else { + return Err(crate::CodegenError::Unsupported( + "Too many arguments for ghc calling convention; \ + all arguments must fit in STG registers" + .to_owned(), + )); + }; + args.push(ABIArg::Slots { + slots: smallvec![ + ABIArgSlot::Reg { + reg: lower_reg.to_real_reg().unwrap(), + ty: reg_types[0], + extension: param.extension, + }, + ABIArgSlot::Reg { + reg: upper_reg.to_real_reg().unwrap(), + ty: reg_types[1], + extension: param.extension, + }, + ], + purpose: param.purpose, + }); + next_ghc_x += 2; + continue; + } + let reg_class_space = max_per_class_reg_vals - next_xreg; let reg_space = remaining_reg_vals; @@ -336,6 +396,66 @@ impl ABIMachineSpec for AArch64MachineDeps { remaining_reg_vals -= 2; continue; } + } else if is_ghc { + // GHC: void returns only; args must fit in STG registers. + // Integer / f32 / f64 / 128-bit vectors use separate pools + // (LLVM CC_AArch64_GHC). + let rc = rcs[0]; + let reg_ty = reg_types[0]; + let next_reg = match args_or_rets { + ArgsOrRets::Rets => None, + ArgsOrRets::Args => match rc { + RegClass::Int => { + let r = get_ghc_intreg_for_arg(next_ghc_x); + if r.is_some() { + next_ghc_x += 1; + } + r + } + RegClass::Float if reg_ty == F32 => { + let r = get_ghc_f32reg_for_arg(next_ghc_f32); + if r.is_some() { + next_ghc_f32 += 1; + } + r + } + RegClass::Float if reg_ty == F64 || reg_ty.bits() == 64 => { + let r = get_ghc_f64reg_for_arg(next_ghc_f64); + if r.is_some() { + next_ghc_f64 += 1; + } + r + } + RegClass::Float if reg_ty.bits() == 128 => { + let r = get_ghc_vec128reg_for_arg(next_ghc_vec); + if r.is_some() { + next_ghc_vec += 1; + } + r + } + RegClass::Float => None, + RegClass::Vector => unreachable!(), + }, + }; + if let Some(reg) = next_reg { + let ty = if param.value_type.is_dynamic_vector() { + dynamic_to_fixed(param.value_type) + } else { + param.value_type + }; + args.push(ABIArg::reg( + reg.to_real_reg().unwrap(), + ty, + param.extension, + param.purpose, + )); + continue; + } + return Err(crate::CodegenError::Unsupported( + "Too many arguments for ghc calling convention; \ + all arguments must fit in STG registers" + .to_owned(), + )); } else { // Single Register parameters let rc = rcs[0]; @@ -378,6 +498,7 @@ impl ABIMachineSpec for AArch64MachineDeps { } // Spill to the stack + debug_assert!(!is_ghc, "GHC must not spill arguments to the stack"); if args_or_rets == ArgsOrRets::Rets && !flags.enable_multi_ret_implicit_sret() { return Err(crate::CodegenError::Unsupported( @@ -1191,6 +1312,9 @@ impl ABIMachineSpec for AArch64MachineDeps { (isa::CallConv::Tail, true) => ALL_CLOBBERS, (isa::CallConv::Winch, true) => ALL_CLOBBERS, (isa::CallConv::Winch, false) => WINCH_CLOBBERS, + // GHC defines no callee-saved registers; treat calls like + // an all-clobbers / Winch-style callsite. + (isa::CallConv::Ghc, _) => ALL_CLOBBERS, // Note that "PreserveAll" actually preserves nothing at // the callsite if used for a `try_call`, because the // unwinder ABI for `try_call`s is still "no clobbered @@ -1300,7 +1424,11 @@ impl ABIMachineSpec for AArch64MachineDeps { | isa::CallConv::Tail | isa::CallConv::PreserveAll | isa::CallConv::AppleAarch64 => PAYLOAD_REGS, - _ => &[], + isa::CallConv::Ghc + | isa::CallConv::Fast + | isa::CallConv::WindowsFastcall + | isa::CallConv::Probestack + | isa::CallConv::Winch => &[], } } } @@ -1389,6 +1517,11 @@ fn is_reg_saved_in_prologue( if call_conv == isa::CallConv::PreserveAll { return true; } + // GHC defines no callee-saved registers (STG state lives in the usual + // AAPCS callee-saves and must not be spilled/restored by the prologue). + if call_conv == isa::CallConv::Ghc { + return false; + } // FIXME: We need to inspect whether a function is returning Z or P regs too. let save_z_regs = sig @@ -1427,6 +1560,57 @@ fn is_reg_saved_in_prologue( } } +/// GHC integer STG registers: Base, Sp, Hp, R1–R6, SpLim +/// (x19–x28). Note x29 is the frame pointer and is intentionally not +/// in this set. +fn get_ghc_intreg_for_arg(idx: usize) -> Option { + match idx { + 0 => Some(xreg(19)), + 1 => Some(xreg(20)), + 2 => Some(xreg(21)), + 3 => Some(xreg(22)), + 4 => Some(xreg(23)), + 5 => Some(xreg(24)), + 6 => Some(xreg(25)), + 7 => Some(xreg(26)), + 8 => Some(xreg(27)), + 9 => Some(xreg(28)), + _ => None, + } +} + +/// GHC f32 STG registers: F1–F4 (s8–s11 = v8–v11). +fn get_ghc_f32reg_for_arg(idx: usize) -> Option { + match idx { + 0 => Some(vreg(8)), + 1 => Some(vreg(9)), + 2 => Some(vreg(10)), + 3 => Some(vreg(11)), + _ => None, + } +} + +/// GHC f64 STG registers: D1–D4 (d12–d15 = v12–v15). +fn get_ghc_f64reg_for_arg(idx: usize) -> Option { + match idx { + 0 => Some(vreg(12)), + 1 => Some(vreg(13)), + 2 => Some(vreg(14)), + 3 => Some(vreg(15)), + _ => None, + } +} + +/// GHC 128-bit / v2f64 STG registers (q4–q5 = v4–v5), matching LLVM +/// CC_AArch64_GHC. +fn get_ghc_vec128reg_for_arg(idx: usize) -> Option { + match idx { + 0 => Some(vreg(4)), + 1 => Some(vreg(5)), + _ => None, + } +} + const fn default_aapcs_clobbers() -> PRegSet { PRegSet::empty() // x0 - x17 inclusive are caller-saves. diff --git a/cranelift/codegen/src/isa/call_conv.rs b/cranelift/codegen/src/isa/call_conv.rs index 6eb1a5e012ad..19d36065a423 100644 --- a/cranelift/codegen/src/isa/call_conv.rs +++ b/cranelift/codegen/src/isa/call_conv.rs @@ -56,6 +56,21 @@ pub enum CallConv { /// respective platform. It does not support tail-calls. It also /// does not support return values. PreserveAll, + /// GHC (Glasgow Haskell Compiler) calling convention. + /// + /// Passes arguments only in fixed STG registers, defines no + /// callee-saved registers, and does not support return values. + /// Used for register-pinning style ABIs similar to LLVM's + /// `ghccc`. Supported on x86_64, aarch64, and riscv64. + /// + /// Supports tail calls between `ghc` callers and callees. Does not + /// support exceptions. + /// + /// On x86_64, STG Sp is `%rbp`, so Cranelift `system_v` code cannot + /// pass a full STG register set into `ghc`; enter from Rust via a + /// naked assembly trampoline. See the `ghc` section in + /// `cranelift/docs/ir.md`. + Ghc, } impl CallConv { @@ -87,7 +102,7 @@ impl CallConv { /// Does this calling convention support tail calls? pub fn supports_tail_calls(&self) -> bool { match self { - CallConv::Tail => true, + CallConv::Tail | CallConv::Ghc => true, _ => false, } } @@ -140,6 +155,7 @@ impl fmt::Display for CallConv { Self::Probestack => "probestack", Self::Winch => "winch", Self::PreserveAll => "preserve_all", + Self::Ghc => "ghc", }) } } @@ -156,6 +172,7 @@ impl str::FromStr for CallConv { "probestack" => Ok(Self::Probestack), "winch" => Ok(Self::Winch), "preserve_all" => Ok(Self::PreserveAll), + "ghc" => Ok(Self::Ghc), _ => Err(()), } } diff --git a/cranelift/codegen/src/isa/pulley_shared/abi.rs b/cranelift/codegen/src/isa/pulley_shared/abi.rs index e077ef04a3f8..d386da20532d 100644 --- a/cranelift/codegen/src/isa/pulley_shared/abi.rs +++ b/cranelift/codegen/src/isa/pulley_shared/abi.rs @@ -578,7 +578,8 @@ where | isa::CallConv::WindowsFastcall | isa::CallConv::AppleAarch64 | isa::CallConv::Probestack - | isa::CallConv::Winch => &[], + | isa::CallConv::Winch + | isa::CallConv::Ghc => &[], } } } diff --git a/cranelift/codegen/src/isa/riscv64/abi.rs b/cranelift/codegen/src/isa/riscv64/abi.rs index ab815bbdf952..4cc39944fa88 100644 --- a/cranelift/codegen/src/isa/riscv64/abi.rs +++ b/cranelift/codegen/src/isa/riscv64/abi.rs @@ -91,7 +91,7 @@ impl ABIMachineSpec for Riscv64MachineDeps { add_ret_area_ptr: bool, mut args: ArgsAccumulator, ) -> CodegenResult<(u32, Option)> { - // This implements the LP64D RISC-V ABI. + // This implements the LP64D RISC-V ABI (and CC_RISCV_GHC for Ghc). assert_ne!( call_conv, @@ -99,6 +99,8 @@ impl ABIMachineSpec for Riscv64MachineDeps { "riscv64 does not support the 'winch' calling convention yet" ); + let is_ghc = call_conv == isa::CallConv::Ghc; + // All registers that can be used as parameters or rets. // both start and end are included. let (x_start, x_end, f_start, f_end) = match args_or_rets { @@ -107,11 +109,17 @@ impl ABIMachineSpec for Riscv64MachineDeps { }; let mut next_x_reg = x_start; let mut next_f_reg = f_start; + // GHC uses separate STG register pools (indices into fixed lists). + let mut next_ghc_x: usize = 0; + let mut next_ghc_f32: usize = 0; + let mut next_ghc_f64: usize = 0; // Stack space. let mut next_stack: u32 = 0; let ret_area_ptr = if add_ret_area_ptr { assert!(ArgsOrRets::Args == args_or_rets); + // GHC has void returns only; a return-area pointer is never used. + assert!(!is_ghc); next_x_reg += 1; Some(ABIArg::reg( x_reg(x_start).to_real_reg().unwrap(), @@ -125,6 +133,11 @@ impl ABIMachineSpec for Riscv64MachineDeps { for param in params { if let ir::ArgumentPurpose::StructArgument(_) = param.purpose { + if is_ghc { + return Err(crate::CodegenError::Unsupported( + "StructArgument not supported with ghc calling convention".to_owned(), + )); + } panic!( "StructArgument parameters are not supported on riscv64. \ Use regular pointer arguments instead." @@ -135,7 +148,37 @@ impl ABIMachineSpec for Riscv64MachineDeps { let (rcs, reg_tys) = Inst::rc_for_type(param.value_type)?; let mut slots = ABIArgSlotVec::new(); for (rc, reg_ty) in rcs.iter().zip(reg_tys.iter()) { - let next_reg = if (next_x_reg <= x_end) && *rc == RegClass::Int { + let next_reg = if is_ghc { + // GHC: void returns only; args must fit in STG registers. + // Integer / f32 / f64 use separate pools (LLVM CC_RISCV_GHC). + match args_or_rets { + ArgsOrRets::Rets => None, + ArgsOrRets::Args => match *rc { + RegClass::Int => { + let r = get_ghc_intreg_for_arg(next_ghc_x); + if r.is_some() { + next_ghc_x += 1; + } + r + } + RegClass::Float if *reg_ty == F32 => { + let r = get_ghc_f32reg_for_arg(next_ghc_f32); + if r.is_some() { + next_ghc_f32 += 1; + } + r + } + RegClass::Float if *reg_ty == F64 => { + let r = get_ghc_f64reg_for_arg(next_ghc_f64); + if r.is_some() { + next_ghc_f64 += 1; + } + r + } + _ => None, + }, + } + } else if (next_x_reg <= x_end) && *rc == RegClass::Int { let x = Some(x_reg(next_x_reg)); next_x_reg += 1; x @@ -153,6 +196,13 @@ impl ABIMachineSpec for Riscv64MachineDeps { extension: param.extension, }); } else { + if is_ghc { + return Err(crate::CodegenError::Unsupported( + "Too many arguments for ghc calling convention; \ + all arguments must fit in STG registers" + .to_owned(), + )); + } if args_or_rets == ArgsOrRets::Rets && !flags.enable_multi_ret_implicit_sret() { return Err(crate::CodegenError::Unsupported( "Too many return values to fit in registers. \ @@ -631,6 +681,9 @@ impl ABIMachineSpec for Riscv64MachineDeps { // Wasmtime). isa::CallConv::PreserveAll if is_exception => ALL_CLOBBERS, isa::CallConv::PreserveAll => NO_CLOBBERS, + // GHC defines no callee-saved registers; treat calls like + // an all-clobbers / Winch-style callsite. + isa::CallConv::Ghc => ALL_CLOBBERS, _ => DEFAULT_CLOBBERS, } } @@ -649,6 +702,8 @@ impl ABIMachineSpec for Riscv64MachineDeps { ) -> FrameLayout { let is_callee_saved = |reg: &Writable| match call_conv { isa::CallConv::PreserveAll => true, + // GHC defines no callee-saved registers. + isa::CallConv::Ghc => false, _ => DEFAULT_CALLEE_SAVES.contains(reg.to_reg().into()), }; let mut regs: Vec> = @@ -731,11 +786,63 @@ impl ABIMachineSpec for Riscv64MachineDeps { isa::CallConv::SystemV | isa::CallConv::Tail | isa::CallConv::PreserveAll => { PAYLOAD_REGS } - _ => &[], + // GHC does not support exceptions. + isa::CallConv::Ghc + | isa::CallConv::Fast + | isa::CallConv::WindowsFastcall + | isa::CallConv::AppleAarch64 + | isa::CallConv::Probestack + | isa::CallConv::Winch => &[], } } } +/// GHC integer STG registers: Base, Sp, Hp, R1–R7, SpLim +/// (s1, s2–s11 = x9, x18–x27). Note x8/s0 is the frame pointer and is +/// intentionally not in this set. +fn get_ghc_intreg_for_arg(idx: usize) -> Option { + match idx { + 0 => Some(x_reg(9)), + 1 => Some(x_reg(18)), + 2 => Some(x_reg(19)), + 3 => Some(x_reg(20)), + 4 => Some(x_reg(21)), + 5 => Some(x_reg(22)), + 6 => Some(x_reg(23)), + 7 => Some(x_reg(24)), + 8 => Some(x_reg(25)), + 9 => Some(x_reg(26)), + 10 => Some(x_reg(27)), + _ => None, + } +} + +/// GHC f32 STG registers: F1–F6 (fs0–fs5 = f8, f9, f18–f21). +fn get_ghc_f32reg_for_arg(idx: usize) -> Option { + match idx { + 0 => Some(f_reg(8)), + 1 => Some(f_reg(9)), + 2 => Some(f_reg(18)), + 3 => Some(f_reg(19)), + 4 => Some(f_reg(20)), + 5 => Some(f_reg(21)), + _ => None, + } +} + +/// GHC f64 STG registers: D1–D6 (fs6–fs11 = f22–f27). +fn get_ghc_f64reg_for_arg(idx: usize) -> Option { + match idx { + 0 => Some(f_reg(22)), + 1 => Some(f_reg(23)), + 2 => Some(f_reg(24)), + 3 => Some(f_reg(25)), + 4 => Some(f_reg(26)), + 5 => Some(f_reg(27)), + _ => None, + } +} + // NOTE: no V regs are callee save. const DEFAULT_CALLEE_SAVES: PRegSet = PRegSet::empty() // X Regs diff --git a/cranelift/codegen/src/isa/s390x/abi.rs b/cranelift/codegen/src/isa/s390x/abi.rs index 42bbe3d23d99..d4e7dd33656e 100644 --- a/cranelift/codegen/src/isa/s390x/abi.rs +++ b/cranelift/codegen/src/isa/s390x/abi.rs @@ -312,6 +312,11 @@ impl ABIMachineSpec for S390xMachineDeps { isa::CallConv::Winch, "s390x does not support the 'winch' calling convention yet" ); + assert_ne!( + call_conv, + isa::CallConv::Ghc, + "s390x does not support the 'ghc' calling convention" + ); let mut next_gpr = 0; let mut next_fpr = 0; diff --git a/cranelift/codegen/src/isa/x64/abi.rs b/cranelift/codegen/src/isa/x64/abi.rs index 81e87f8e350a..b49ff0369a9a 100644 --- a/cranelift/codegen/src/isa/x64/abi.rs +++ b/cranelift/codegen/src/isa/x64/abi.rs @@ -100,6 +100,7 @@ impl ABIMachineSpec for X64ABIMachineSpec { ) -> CodegenResult<(u32, Option)> { let is_fastcall = call_conv == CallConv::WindowsFastcall; let is_tail = call_conv == CallConv::Tail; + let is_ghc = call_conv == CallConv::Ghc; let mut next_gpr = 0; let mut next_vreg = 0; @@ -154,6 +155,11 @@ impl ABIMachineSpec for X64ABIMachineSpec { let last_param = ix == params.len() - 1; if let ir::ArgumentPurpose::StructArgument(size) = param.purpose { + if is_ghc { + return Err(crate::CodegenError::Unsupported( + "StructArgument not supported with ghc calling convention".to_owned(), + )); + } let offset = next_stack as i64; let size = size; assert!(size % 8 == 0, "StructArgument size is not properly aligned"); @@ -199,6 +205,7 @@ impl ABIMachineSpec for X64ABIMachineSpec { && !(param.value_type.is_vector() || param.value_type.is_float()) && !flags.enable_llvm_abi_extensions() && !is_tail + && !is_ghc { panic!( "i128 args/return values not supported unless LLVM ABI extensions are enabled" @@ -342,6 +349,13 @@ impl ABIMachineSpec for X64ABIMachineSpec { extension: param.extension, }); } else { + if is_ghc { + return Err(crate::CodegenError::Unsupported( + "Too many arguments for ghc calling convention; \ + all arguments must fit in STG registers" + .to_owned(), + )); + } if args_or_rets == ArgsOrRets::Rets && !flags.enable_multi_ret_implicit_sret() { return Err(crate::CodegenError::Unsupported( "Too many return values to fit in registers. \ @@ -536,11 +550,16 @@ impl ABIMachineSpec for X64ABIMachineSpec { } fn gen_prologue_frame_setup( - _call_conv: isa::CallConv, + call_conv: isa::CallConv, flags: &settings::Flags, _isa_flags: &x64_settings::Flags, frame_layout: &FrameLayout, ) -> SmallInstVec { + // GHC maps STG Sp to %rbp; do not set up a frame pointer. + if call_conv == CallConv::Ghc { + return smallvec![]; + } + let r_rsp = Gpr::RSP; let r_rbp = Gpr::RBP; let w_rbp = Writable::from_reg(r_rbp); @@ -569,11 +588,16 @@ impl ABIMachineSpec for X64ABIMachineSpec { } fn gen_epilogue_frame_restore( - _call_conv: isa::CallConv, + call_conv: isa::CallConv, _flags: &settings::Flags, _isa_flags: &x64_settings::Flags, _frame_layout: &FrameLayout, ) -> SmallInstVec { + // GHC uses an FP-less frame. + if call_conv == CallConv::Ghc { + return smallvec![]; + } + let rbp = Gpr::RBP; let rsp = Gpr::RSP; @@ -650,7 +674,7 @@ impl ABIMachineSpec for X64ABIMachineSpec { } fn gen_clobber_save( - _call_conv: isa::CallConv, + call_conv: isa::CallConv, flags: &settings::Flags, frame_layout: &FrameLayout, ) -> SmallVec<[Self::I; 16]> { @@ -660,6 +684,10 @@ impl ABIMachineSpec for X64ABIMachineSpec { // present, resize the incoming argument area of the frame to accommodate those arguments. let incoming_args_diff = frame_layout.tail_args_size - frame_layout.incoming_args_size; if incoming_args_diff > 0 { + // GHC has no stack arguments, so this path must not run (it would + // clobber STG Sp in %rbp). + debug_assert_ne!(call_conv, CallConv::Ghc); + // Decrement the stack pointer to make space for the new arguments. let rsp = Writable::from_reg(regs::rsp()); insts.push(Inst::subq_mi( @@ -872,8 +900,13 @@ impl ABIMachineSpec for X64ABIMachineSpec { } } - fn get_machine_env(flags: &settings::Flags, _call_conv: isa::CallConv) -> &MachineEnv { - if flags.enable_pinned_reg() { + fn get_machine_env(flags: &settings::Flags, call_conv: isa::CallConv) -> &MachineEnv { + if call_conv == CallConv::Ghc { + // GHC needs %rbp allocatable (STG Sp) and does not use the pinned + // register scheme. + static MACHINE_ENV: MachineEnv = create_reg_env_ghc(); + &MACHINE_ENV + } else if flags.enable_pinned_reg() { static MACHINE_ENV: MachineEnv = create_reg_env_systemv(true); &MACHINE_ENV } else { @@ -895,6 +928,7 @@ impl ABIMachineSpec for X64ABIMachineSpec { // Wasmtime). (isa::CallConv::PreserveAll, true) => ALL_CLOBBERS, (isa::CallConv::Winch, _) => ALL_CLOBBERS, + (isa::CallConv::Ghc, _) => GHC_CLOBBERS, (isa::CallConv::SystemV, _) => SYSV_CLOBBERS, (isa::CallConv::WindowsFastcall, false) => WINDOWS_CLOBBERS, (isa::CallConv::PreserveAll, _) => NO_CLOBBERS, @@ -925,9 +959,9 @@ impl ABIMachineSpec for X64ABIMachineSpec { debug_assert!(tail_args_size >= incoming_args_size); let mut regs: Vec> = match call_conv { - // The `winch` calling convention doesn't have any callee-save - // registers. - CallConv::Winch => vec![], + // The `winch` and `ghc` calling conventions don't have any + // callee-save registers. + CallConv::Winch | CallConv::Ghc => vec![], CallConv::Fast | CallConv::SystemV | CallConv::Tail => regs .iter() .cloned() @@ -950,8 +984,13 @@ impl ABIMachineSpec for X64ABIMachineSpec { // Compute clobber size. let clobber_size = compute_clobber_size(®s); - // Compute setup area size. - let setup_area_size = 16; // RBP, return address + // Compute setup area size. GHC leaves %rbp free for STG Sp, so the + // setup area is only the return address. + let setup_area_size = if call_conv == CallConv::Ghc { + 8 // return address only + } else { + 16 // RBP, return address + }; // Return FrameLayout structure. FrameLayout { @@ -1019,6 +1058,23 @@ impl From for SyntheticAmode { } fn get_intreg_for_arg(call_conv: CallConv, idx: usize, arg_idx: usize) -> Option { + if call_conv == CallConv::Ghc { + // STG registers: Base, Sp, Hp, R1, R2, R3, R4, R5, R6, SpLim + return match idx { + 0 => Some(regs::r13()), + 1 => Some(regs::rbp()), + 2 => Some(regs::r12()), + 3 => Some(regs::rbx()), + 4 => Some(regs::r14()), + 5 => Some(regs::rsi()), + 6 => Some(regs::rdi()), + 7 => Some(regs::r8()), + 8 => Some(regs::r9()), + 9 => Some(regs::r15()), + _ => None, + }; + } + let is_fastcall = call_conv == CallConv::WindowsFastcall; // Fastcall counts by absolute argument number; SysV counts by argument of @@ -1040,6 +1096,19 @@ fn get_intreg_for_arg(call_conv: CallConv, idx: usize, arg_idx: usize) -> Option } fn get_fltreg_for_arg(call_conv: CallConv, idx: usize, arg_idx: usize) -> Option { + if call_conv == CallConv::Ghc { + // STG registers: F1, F2, F3, F4, D1, D2 + return match idx { + 0 => Some(regs::xmm1()), + 1 => Some(regs::xmm2()), + 2 => Some(regs::xmm3()), + 3 => Some(regs::xmm4()), + 4 => Some(regs::xmm5()), + 5 => Some(regs::xmm6()), + _ => None, + }; + } + let is_fastcall = call_conv == CallConv::WindowsFastcall; // Fastcall counts by absolute argument number; SysV counts by argument of @@ -1096,6 +1165,7 @@ fn get_intreg_for_retval( }, CallConv::Winch => is_last.then(|| regs::rax()), + CallConv::Ghc => None, CallConv::Probestack => todo!(), CallConv::AppleAarch64 => unreachable!(), } @@ -1124,6 +1194,7 @@ fn get_fltreg_for_retval(call_conv: CallConv, fltreg_idx: usize, is_last: bool) _ => None, }, CallConv::Winch => is_last.then(|| regs::xmm0()), + CallConv::Ghc => None, CallConv::Probestack => todo!(), CallConv::AppleAarch64 => unreachable!(), } @@ -1185,6 +1256,7 @@ fn compute_clobber_size(clobbers: &[Writable]) -> u32 { const WINDOWS_CLOBBERS: PRegSet = windows_clobbers(); const SYSV_CLOBBERS: PRegSet = sysv_clobbers(); pub(crate) const ALL_CLOBBERS: PRegSet = all_clobbers(); +const GHC_CLOBBERS: PRegSet = ghc_clobbers(); const NO_CLOBBERS: PRegSet = PRegSet::empty(); const fn windows_clobbers() -> PRegSet { @@ -1277,6 +1349,12 @@ const fn all_clobbers() -> PRegSet { .with(regs::fpr_preg(XMM15)) } +/// GHC clobbers every allocatable register, including %rbp (STG Sp). +const fn ghc_clobbers() -> PRegSet { + use asm::gpr::enc::*; + all_clobbers().with(regs::gpr_preg(RBP)) +} + const fn create_reg_env_systemv(enable_pinned_reg: bool) -> MachineEnv { const fn preg(r: Reg) -> PReg { r.to_real_reg().unwrap().preg() @@ -1343,6 +1421,64 @@ const fn create_reg_env_systemv(enable_pinned_reg: bool) -> MachineEnv { env } +/// Register environment for the GHC calling convention. +/// +/// Unlike SysV, %rbp is allocatable because it holds STG Sp. %r15 is also +/// always allocatable (STG SpLim); the pinned-register feature is not used. +const fn create_reg_env_ghc() -> MachineEnv { + const fn preg(r: Reg) -> PReg { + r.to_real_reg().unwrap().preg() + } + + MachineEnv { + preferred_regs_by_class: [ + PRegSet::empty() + .with(preg(regs::rax())) + .with(preg(regs::rcx())) + .with(preg(regs::rdx())) + .with(preg(regs::rsi())) + .with(preg(regs::rdi())) + .with(preg(regs::r8())) + .with(preg(regs::r9())) + .with(preg(regs::r10())) + .with(preg(regs::r11())), + PRegSet::empty() + .with(preg(regs::xmm0())) + .with(preg(regs::xmm1())) + .with(preg(regs::xmm2())) + .with(preg(regs::xmm3())) + .with(preg(regs::xmm4())) + .with(preg(regs::xmm5())) + .with(preg(regs::xmm6())) + .with(preg(regs::xmm7())), + PRegSet::empty(), + ], + non_preferred_regs_by_class: [ + // STG integer registers that are callee-saved under SysV, plus + // %rbp which is normally reserved as the frame pointer. + PRegSet::empty() + .with(preg(regs::rbx())) + .with(preg(regs::rbp())) + .with(preg(regs::r12())) + .with(preg(regs::r13())) + .with(preg(regs::r14())) + .with(preg(regs::r15())), + PRegSet::empty() + .with(preg(regs::xmm8())) + .with(preg(regs::xmm9())) + .with(preg(regs::xmm10())) + .with(preg(regs::xmm11())) + .with(preg(regs::xmm12())) + .with(preg(regs::xmm13())) + .with(preg(regs::xmm14())) + .with(preg(regs::xmm15())), + PRegSet::empty(), + ], + fixed_stack_slots: vec![], + scratch_by_class: [None, None, None], + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/cranelift/codegen/src/isa/x64/inst/emit.rs b/cranelift/codegen/src/isa/x64/inst/emit.rs index 7f6f1aeb763d..aa3b3220e45a 100644 --- a/cranelift/codegen/src/isa/x64/inst/emit.rs +++ b/cranelift/codegen/src/isa/x64/inst/emit.rs @@ -1871,22 +1871,32 @@ fn emit_return_call_common_sequence( state: &mut EmitState, call_info: &ReturnCallInfo, ) { - assert!( - info.flags.preserve_frame_pointers(), - "frame pointers aren't fundamentally required for tail calls, \ + let call_conv = state.call_conv; + match call_conv { + CallConv::Ghc => { + // GHC frames are FP-less; %rbp holds STG Sp and must not be + // restored as a frame pointer. + } + CallConv::Tail => { + assert!( + info.flags.preserve_frame_pointers(), + "frame pointers aren't fundamentally required for tail calls, \ but the current implementation relies on them being present" - ); + ); + } + other => panic!("return_call is only supported for Tail and Ghc, got {other:?}"), + } let tmp = call_info.tmp.to_writable_reg(); for inst in - X64ABIMachineSpec::gen_clobber_restore(CallConv::Tail, &info.flags, state.frame_layout()) + X64ABIMachineSpec::gen_clobber_restore(call_conv, &info.flags, state.frame_layout()) { inst.emit(sink, info, state); } for inst in X64ABIMachineSpec::gen_epilogue_frame_restore( - CallConv::Tail, + call_conv, &info.flags, &info.isa_flags, state.frame_layout(), @@ -1896,6 +1906,7 @@ fn emit_return_call_common_sequence( let incoming_args_diff = state.frame_layout().tail_args_size - call_info.new_stack_arg_size; if incoming_args_diff > 0 { + debug_assert_ne!(call_conv, CallConv::Ghc); // Move the saved return address up by `incoming_args_diff`. let addr = Amode::imm_reg(0, regs::rsp()); asm::inst::movq_rm::new(tmp, addr).emit(sink, info, state); diff --git a/cranelift/codegen/src/isa/x64/inst/emit_state.rs b/cranelift/codegen/src/isa/x64/inst/emit_state.rs index 426a17a2803b..1a3da5d8a095 100644 --- a/cranelift/codegen/src/isa/x64/inst/emit_state.rs +++ b/cranelift/codegen/src/isa/x64/inst/emit_state.rs @@ -3,7 +3,7 @@ use crate::ir; use cranelift_control::ControlPlane; /// State carried between emissions of a sequence of instructions. -#[derive(Default, Clone, Debug)] +#[derive(Clone, Debug)] pub struct EmitState { /// The user stack map for the upcoming instruction, as provided to /// `pre_safepoint()`. @@ -16,6 +16,21 @@ pub struct EmitState { /// A copy of the frame layout, used during the emission of `Inst::ReturnCallKnown` and /// `Inst::ReturnCallUnknown` instructions and exception callsites. pub frame_layout: FrameLayout, + + /// The function's calling convention, used for return-call epilogue + /// teardown (e.g. FP-less GHC frames). + pub call_conv: CallConv, +} + +impl Default for EmitState { + fn default() -> Self { + EmitState { + user_stack_map: None, + ctrl_plane: ControlPlane::default(), + frame_layout: FrameLayout::default(), + call_conv: CallConv::SystemV, + } + } } impl MachInstEmitState for EmitState { @@ -24,6 +39,7 @@ impl MachInstEmitState for EmitState { user_stack_map: None, ctrl_plane, frame_layout: abi.frame_layout().clone(), + call_conv: abi.call_conv(), } } diff --git a/cranelift/codegen/src/machinst/abi.rs b/cranelift/codegen/src/machinst/abi.rs index c7b42116b5d6..0d68d676dcc3 100644 --- a/cranelift/codegen/src/machinst/abi.rs +++ b/cranelift/codegen/src/machinst/abi.rs @@ -1231,7 +1231,8 @@ impl Callee { || call_conv == isa::CallConv::WindowsFastcall || call_conv == isa::CallConv::AppleAarch64 || call_conv == isa::CallConv::Winch - || call_conv == isa::CallConv::PreserveAll, + || call_conv == isa::CallConv::PreserveAll + || call_conv == isa::CallConv::Ghc, "Unsupported calling convention: {call_conv:?}" ); @@ -1822,10 +1823,11 @@ impl Callee { let word_bits = M::word_bits() as usize; if is_tail_call { - debug_assert_eq!( - self.call_conv, - isa::CallConv::Tail, - "Can only do `return_call`s from within a `tail` calling convention function" + debug_assert!( + self.call_conv.supports_tail_calls(), + "Can only do `return_call`s from within a calling convention \ + that supports tail calls (got {:?})", + self.call_conv ); } diff --git a/cranelift/codegen/src/verifier/mod.rs b/cranelift/codegen/src/verifier/mod.rs index 51485478e8b1..3278133b621f 100644 --- a/cranelift/codegen/src/verifier/mod.rs +++ b/cranelift/codegen/src/verifier/mod.rs @@ -2085,6 +2085,14 @@ impl<'a> Verifier<'a> { ))?; } } + CallConv::Ghc => { + if !sig.returns.is_empty() { + errors.fatal(( + entity, + "Signature with `ghc` ABI cannot have return values".to_string(), + ))?; + } + } _ => {} } Ok(()) diff --git a/cranelift/docs/ir.md b/cranelift/docs/ir.md index d5e56355f2b9..7632ecc32860 100644 --- a/cranelift/docs/ir.md +++ b/cranelift/docs/ir.md @@ -352,7 +352,8 @@ param : type [paramext] [paramspecial] paramext : "uext" | "sext" paramspecial : "sarg" ( num ) | "sret" | "vmctx" | "stack_limit" callconv : "fast" | "cold" | "system_v" | "windows_fastcall" - | "apple_aarch64" | "probestack" | "winch" + | "apple_aarch64" | "probestack" | "winch" | "preserve_all" + | "tail" | "ghc" ``` A function's calling convention determines exactly how arguments and return @@ -374,12 +375,76 @@ system, a function's calling convention is only fully determined by a | cold | not-ABI-stable convention for infrequently executed code | | system_v | System V-style convention used on many platforms | | fastcall | Windows "fastcall" convention, also used for x64 and ARM | +| ghc | GHC STG register-pinning convention (x86_64, aarch64, riscv64) | The "not-ABI-stable" conventions do not follow an external specification and may change between versions of Cranelift. The "fastcall" convention is not yet implemented. +### The `ghc` calling convention + +The `ghc` convention mirrors LLVM's `ghccc`: arguments are passed only in +fixed STG (Spineless Tagless G-machine) registers, there are no callee-saved +registers, return values are not supported, and `return_call` is allowed +between `ghc` callers and callees. Excess arguments that do not fit in the +STG register set are a compile-time error (they are not spilled to the stack). + +STG integer / floating-point argument registers: + +| ISA | Integer (in order) | Floating-point (in order) | +| --- | --- | --- | +| x86_64 | `%r13`, `%rbp`, `%r12`, `%rbx`, `%r14`, `%rsi`, `%rdi`, `%r8`, `%r9`, `%r15` | `%xmm1`–`%xmm6` | +| aarch64 | `x19`–`x28` | `s8`–`s11` (`f32`), `d12`–`d15` (`f64`) | +| riscv64 | `x9`, `x18`–`x27` | `f8`, `f9`, `f18`–`f21` (`f32`); `f22`–`f27` (`f64`) | + +On x86_64 the second integer argument is STG Sp and is pinned to `%rbp`. +Cranelift `ghc` functions therefore use an FP-less frame. A Cranelift +`system_v` function cannot place Sp in `%rbp` while using `%rbp` as its own +frame pointer, so **entry from Rust / SysV must go through a small naked +assembly trampoline** (not through CLIF `system_v` that `call`s `ghc` with +Sp). On aarch64 and riscv64, Sp is not the hardware frame pointer, so +`system_v` → `ghc` calls with a full STG argument list work without asm. + +#### x86_64 entry stub (SysV → `ghc`) + +Compile the GHC body with `ghc`. Call it from Rust via a naked stub that +moves SysV argument registers into the STG pins and jumps (typical for +continuation-style STG code that does not return like C): + +```rust +use std::arch::naked_asm; + +// SysV args: rdi=Base, rsi=Sp, rdx=Hp, rcx=R1, r8=R2, r9=R3 +// STG pins: r13, rbp, r12, rbx, r14, rsi, rdi, r8, r9, r15 +#[unsafe(naked)] +pub unsafe extern "C" fn enter_ghc( + _base: u64, + _sp: u64, + _hp: u64, + _r1: u64, + _r2: u64, + _r3: u64, +) -> ! { + naked_asm!( + "mov r13, rdi", // Base + "mov rbp, rsi", // Sp (safe here: no frame pointer) + "mov r12, rdx", // Hp + "mov rbx, rcx", // R1 + "mov r14, r8", // R2 + "mov rsi, r9", // R3 + // Optional: load R4..SpLim from the SysV stack into rdi, r8, r9, r15. + "jmp {target}", + target = sym your_ghc_entry, // Cranelift-compiled `ghc` function + ) +} +``` + +Use `call` instead of `jmp` only if the `ghc` callee is guaranteed to return +to this stub with a SysV-compatible machine state (uncommon for STG). Exit +back to Rust through a second, similarly explicit stub or a known +continuation, not by assuming a normal SysV return through Sp-in-`%rbp`. + Parameters and return values have flags whose meaning is mostly target dependent. These flags support interfacing with code produced by other compilers. diff --git a/cranelift/filetests/filetests/isa/aarch64/ghc.clif b/cranelift/filetests/filetests/isa/aarch64/ghc.clif new file mode 100644 index 000000000000..3607f8081f2f --- /dev/null +++ b/cranelift/filetests/filetests/isa/aarch64/ghc.clif @@ -0,0 +1,186 @@ +test compile precise-output +target aarch64 + +;; GHC STG integer args: x19-x28 +;; f32: s8-s11; f64: d12-d15 + +function %ghc_leaf(i64, i64, i64) ghc { +block0(v0: i64, v1: i64, v2: i64): + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +function %ghc_call_ghc(i64, i64) ghc { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64, v1: i64): + call fn0(v0, v1, v0) + return +} + +; VCode: +; stp fp, lr, [sp, #-16]! +; mov fp, sp +; block0: +; load_ext_name_far x3, TestCase(%ghc_leaf)+0 +; mov x21, x19 +; blr x3 +; ldp fp, lr, [sp], #16 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; stp x29, x30, [sp, #-0x10]! +; mov x29, sp +; block1: ; offset 0x8 +; ldr x3, #0x10 +; b #0x18 +; udf #0 ; reloc_external Abs8 %ghc_leaf 0 +; udf #0 +; mov x21, x19 +; blr x3 +; ldp x29, x30, [sp], #0x10 +; ret + +function %ghc_call_system_v(i64) ghc { + fn0 = %sysv(i64) -> i64 system_v +block0(v0: i64): + v1 = call fn0(v0) + return +} + +; VCode: +; stp fp, lr, [sp, #-16]! +; mov fp, sp +; block0: +; load_ext_name_far x3, TestCase(%sysv)+0 +; mov x0, x19 +; blr x3 +; ldp fp, lr, [sp], #16 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; stp x29, x30, [sp, #-0x10]! +; mov x29, sp +; block1: ; offset 0x8 +; ldr x3, #0x10 +; b #0x18 +; udf #0 ; reloc_external Abs8 %sysv 0 +; udf #0 +; mov x0, x19 +; blr x3 +; ldp x29, x30, [sp], #0x10 +; ret + +function %system_v_call_ghc(i64) system_v { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64): + call fn0(v0, v0, v0) + return +} + +; VCode: +; stp fp, lr, [sp, #-16]! +; mov fp, sp +; stp x27, x28, [sp, #-16]! +; stp x25, x26, [sp, #-16]! +; stp x23, x24, [sp, #-16]! +; stp x21, x22, [sp, #-16]! +; stp x19, x20, [sp, #-16]! +; stp d14, d15, [sp, #-16]! +; stp d12, d13, [sp, #-16]! +; stp d10, d11, [sp, #-16]! +; stp d8, d9, [sp, #-16]! +; block0: +; load_ext_name_far x2, TestCase(%ghc_leaf)+0 +; mov x21, x0 +; mov x19, x21 +; mov x20, x21 +; blr x2 +; ldp d8, d9, [sp], #16 +; ldp d10, d11, [sp], #16 +; ldp d12, d13, [sp], #16 +; ldp d14, d15, [sp], #16 +; ldp x19, x20, [sp], #16 +; ldp x21, x22, [sp], #16 +; ldp x23, x24, [sp], #16 +; ldp x25, x26, [sp], #16 +; ldp x27, x28, [sp], #16 +; ldp fp, lr, [sp], #16 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; stp x29, x30, [sp, #-0x10]! +; mov x29, sp +; stp x27, x28, [sp, #-0x10]! +; stp x25, x26, [sp, #-0x10]! +; stp x23, x24, [sp, #-0x10]! +; stp x21, x22, [sp, #-0x10]! +; stp x19, x20, [sp, #-0x10]! +; stp d14, d15, [sp, #-0x10]! +; stp d12, d13, [sp, #-0x10]! +; stp d10, d11, [sp, #-0x10]! +; stp d8, d9, [sp, #-0x10]! +; block1: ; offset 0x2c +; ldr x2, #0x34 +; b #0x3c +; udf #0 ; reloc_external Abs8 %ghc_leaf 0 +; udf #0 +; mov x21, x0 +; mov x19, x21 +; mov x20, x21 +; blr x2 +; ldp d8, d9, [sp], #0x10 +; ldp d10, d11, [sp], #0x10 +; ldp d12, d13, [sp], #0x10 +; ldp d14, d15, [sp], #0x10 +; ldp x19, x20, [sp], #0x10 +; ldp x21, x22, [sp], #0x10 +; ldp x23, x24, [sp], #0x10 +; ldp x25, x26, [sp], #0x10 +; ldp x27, x28, [sp], #0x10 +; ldp x29, x30, [sp], #0x10 +; ret + +function %ghc_return_call(i64, i64) ghc { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64, v1: i64): + return_call fn0(v0, v1, v0) +} + +; VCode: +; block0: +; load_ext_name_far x1, TestCase(%ghc_leaf)+0 +; mov x21, x19 +; return_call_ind x1 new_stack_arg_size:0 x19=x19 x20=x20 x21=x21 +; +; Disassembled: +; block0: ; offset 0x0 +; ldr x1, #8 +; b #0x10 +; udf #0 ; reloc_external Abs8 %ghc_leaf 0 +; udf #0 +; mov x21, x19 +; br x1 + +function %ghc_float_args(f32, f32, f64) ghc { +block0(v0: f32, v1: f32, v2: f64): + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + diff --git a/cranelift/filetests/filetests/isa/riscv64/ghc.clif b/cranelift/filetests/filetests/isa/riscv64/ghc.clif new file mode 100644 index 000000000000..d69bb5dfe579 --- /dev/null +++ b/cranelift/filetests/filetests/isa/riscv64/ghc.clif @@ -0,0 +1,286 @@ +test compile precise-output +target riscv64 + +;; GHC STG integer args: x9, x18-x27 +;; f32: fs0-fs5; f64: fs6-fs11 + +function %ghc_leaf(i64, i64, i64) ghc { +block0(v0: i64, v1: i64, v2: i64): + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + +function %ghc_call_ghc(i64, i64) ghc { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64, v1: i64): + call fn0(v0, v1, v0) + return +} + +; VCode: +; addi sp,sp,-16 +; sd ra,8(sp) +; sd fp,0(sp) +; mv fp,sp +; block0: +; load_ext_name_far a0,%ghc_leaf+0 +; mv s3,s1 +; callind a0 +; ld ra,8(sp) +; ld fp,0(sp) +; addi sp,sp,16 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; addi sp, sp, -0x10 +; sd ra, 8(sp) +; sd s0, 0(sp) +; mv s0, sp +; block1: ; offset 0x10 +; auipc a0, 0 +; ld a0, 0xc(a0) +; j 0xc +; .byte 0x00, 0x00, 0x00, 0x00 ; reloc_external Abs8 %ghc_leaf 0 +; .byte 0x00, 0x00, 0x00, 0x00 +; mv s3, s1 +; jalr a0 +; ld ra, 8(sp) +; ld s0, 0(sp) +; addi sp, sp, 0x10 +; ret + +function %ghc_call_system_v(i64) ghc { + fn0 = %sysv(i64) -> i64 system_v +block0(v0: i64): + v1 = call fn0(v0) + return +} + +; VCode: +; addi sp,sp,-16 +; sd ra,8(sp) +; sd fp,0(sp) +; mv fp,sp +; block0: +; load_ext_name_far a1,%sysv+0 +; mv a0,s1 +; callind a1 +; ld ra,8(sp) +; ld fp,0(sp) +; addi sp,sp,16 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; addi sp, sp, -0x10 +; sd ra, 8(sp) +; sd s0, 0(sp) +; mv s0, sp +; block1: ; offset 0x10 +; auipc a1, 0 +; ld a1, 0xc(a1) +; j 0xc +; .byte 0x00, 0x00, 0x00, 0x00 ; reloc_external Abs8 %sysv 0 +; .byte 0x00, 0x00, 0x00, 0x00 +; mv a0, s1 +; jalr a1 +; ld ra, 8(sp) +; ld s0, 0(sp) +; addi sp, sp, 0x10 +; ret + +function %system_v_call_ghc(i64) system_v { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64): + call fn0(v0, v0, v0) + return +} + +; VCode: +; addi sp,sp,-16 +; sd ra,8(sp) +; sd fp,0(sp) +; mv fp,sp +; addi sp,sp,-192 +; sd fp,184(sp) +; sd s1,176(sp) +; sd s2,168(sp) +; sd s3,160(sp) +; sd s4,152(sp) +; sd s5,144(sp) +; sd s6,136(sp) +; sd s7,128(sp) +; sd s8,120(sp) +; sd s9,112(sp) +; sd s10,104(sp) +; sd s11,96(sp) +; fsd fs0,88(sp) +; fsd fs2,80(sp) +; fsd fs3,72(sp) +; fsd fs4,64(sp) +; fsd fs5,56(sp) +; fsd fs6,48(sp) +; fsd fs7,40(sp) +; fsd fs8,32(sp) +; fsd fs9,24(sp) +; fsd fs10,16(sp) +; fsd fs11,8(sp) +; block0: +; load_ext_name_far a1,%ghc_leaf+0 +; mv s3,a0 +; mv s1,s3 +; mv s2,s3 +; callind a1 +; ld fp,184(sp) +; ld s1,176(sp) +; ld s2,168(sp) +; ld s3,160(sp) +; ld s4,152(sp) +; ld s5,144(sp) +; ld s6,136(sp) +; ld s7,128(sp) +; ld s8,120(sp) +; ld s9,112(sp) +; ld s10,104(sp) +; ld s11,96(sp) +; fld fs0,88(sp) +; fld fs2,80(sp) +; fld fs3,72(sp) +; fld fs4,64(sp) +; fld fs5,56(sp) +; fld fs6,48(sp) +; fld fs7,40(sp) +; fld fs8,32(sp) +; fld fs9,24(sp) +; fld fs10,16(sp) +; fld fs11,8(sp) +; addi sp,sp,192 +; ld ra,8(sp) +; ld fp,0(sp) +; addi sp,sp,16 +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; addi sp, sp, -0x10 +; sd ra, 8(sp) +; sd s0, 0(sp) +; mv s0, sp +; addi sp, sp, -0xc0 +; sd s0, 0xb8(sp) +; sd s1, 0xb0(sp) +; sd s2, 0xa8(sp) +; sd s3, 0xa0(sp) +; sd s4, 0x98(sp) +; sd s5, 0x90(sp) +; sd s6, 0x88(sp) +; sd s7, 0x80(sp) +; sd s8, 0x78(sp) +; sd s9, 0x70(sp) +; sd s10, 0x68(sp) +; sd s11, 0x60(sp) +; fsd fs0, 0x58(sp) +; fsd fs2, 0x50(sp) +; fsd fs3, 0x48(sp) +; fsd fs4, 0x40(sp) +; fsd fs5, 0x38(sp) +; fsd fs6, 0x30(sp) +; fsd fs7, 0x28(sp) +; fsd fs8, 0x20(sp) +; fsd fs9, 0x18(sp) +; fsd fs10, 0x10(sp) +; fsd fs11, 8(sp) +; block1: ; offset 0x70 +; auipc a1, 0 +; ld a1, 0xc(a1) +; j 0xc +; .byte 0x00, 0x00, 0x00, 0x00 ; reloc_external Abs8 %ghc_leaf 0 +; .byte 0x00, 0x00, 0x00, 0x00 +; mv s3, a0 +; mv s1, s3 +; mv s2, s3 +; jalr a1 +; ld s0, 0xb8(sp) +; ld s1, 0xb0(sp) +; ld s2, 0xa8(sp) +; ld s3, 0xa0(sp) +; ld s4, 0x98(sp) +; ld s5, 0x90(sp) +; ld s6, 0x88(sp) +; ld s7, 0x80(sp) +; ld s8, 0x78(sp) +; ld s9, 0x70(sp) +; ld s10, 0x68(sp) +; ld s11, 0x60(sp) +; fld fs0, 0x58(sp) +; fld fs2, 0x50(sp) +; fld fs3, 0x48(sp) +; fld fs4, 0x40(sp) +; fld fs5, 0x38(sp) +; fld fs6, 0x30(sp) +; fld fs7, 0x28(sp) +; fld fs8, 0x20(sp) +; fld fs9, 0x18(sp) +; fld fs10, 0x10(sp) +; fld fs11, 8(sp) +; addi sp, sp, 0xc0 +; ld ra, 8(sp) +; ld s0, 0(sp) +; addi sp, sp, 0x10 +; ret + +function %ghc_return_call(i64, i64) ghc { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64, v1: i64): + return_call fn0(v0, v1, v0) +} + +; VCode: +; addi sp,sp,-16 +; sd ra,8(sp) +; sd fp,0(sp) +; mv fp,sp +; block0: +; load_ext_name_far t0,%ghc_leaf+0 +; mv s3,s1 +; return_call_ind t0 new_stack_arg_size:0 s1=s1 s2=s2 s3=s3 +; +; Disassembled: +; block0: ; offset 0x0 +; addi sp, sp, -0x10 +; sd ra, 8(sp) +; sd s0, 0(sp) +; mv s0, sp +; block1: ; offset 0x10 +; auipc t0, 0 +; ld t0, 0xc(t0) +; j 0xc +; .byte 0x00, 0x00, 0x00, 0x00 ; reloc_external Abs8 %ghc_leaf 0 +; .byte 0x00, 0x00, 0x00, 0x00 +; mv s3, s1 +; ld ra, 8(sp) +; ld s0, 0(sp) +; addi sp, sp, 0x10 +; jr t0 + +function %ghc_float_args(f32, f32, f64) ghc { +block0(v0: f32, v1: f32, v2: f64): + return +} + +; VCode: +; block0: +; ret +; +; Disassembled: +; block0: ; offset 0x0 +; ret + diff --git a/cranelift/filetests/filetests/isa/x64/ghc.clif b/cranelift/filetests/filetests/isa/x64/ghc.clif new file mode 100644 index 000000000000..efa845f0bef1 --- /dev/null +++ b/cranelift/filetests/filetests/isa/x64/ghc.clif @@ -0,0 +1,167 @@ +test compile precise-output +target x86_64 + +;; GHC STG integer args: r13, rbp, r12, rbx, r14, rsi, rdi, r8, r9, r15 +;; No frame-pointer setup (rbp holds Sp). +;; +;; Note: a SystemV caller cannot place Sp in %rbp while using %rbp as its +;; frame pointer, so SystemV→GHC calls are limited to args that fit in +;; allocatable SysV regs (e.g. the first STG int arg in %r13). + +function %ghc_leaf(i64, i64, i64) ghc { +block0(v0: i64, v1: i64, v2: i64): + return +} + +; VCode: +; block0: +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; retq + +function %ghc_one(i64) ghc { +block0(v0: i64): + return +} + +; VCode: +; block0: +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; retq + +function %ghc_call_ghc(i64, i64) ghc { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64, v1: i64): + call fn0(v0, v1, v0) + return +} + +; VCode: +; block0: +; load_ext_name %ghc_leaf+0, %rsi +; movq %r13, %r12 +; call *%rsi +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; movabsq $0, %rsi ; reloc_external Abs8 %ghc_leaf 0 +; movq %r13, %r12 +; callq *%rsi +; retq + +function %ghc_call_system_v(i64) ghc { + fn0 = %sysv(i64) -> i64 system_v +block0(v0: i64): + v1 = call fn0(v0) + return +} + +; VCode: +; block0: +; load_ext_name %sysv+0, %rsi +; movq %r13, %rdi +; call *%rsi +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; movabsq $0, %rsi ; reloc_external Abs8 %sysv 0 +; movq %r13, %rdi +; callq *%rsi +; retq + +function %system_v_call_ghc(i64) system_v { + fn0 = %ghc_one(i64) ghc +block0(v0: i64): + call fn0(v0) + return +} + +; VCode: +; pushq %rbp +; movq %rsp, %rbp +; subq $0x30, %rsp +; movq %rbx, (%rsp) +; movq %rbp, 8(%rsp) +; movq %r12, 0x10(%rsp) +; movq %r13, 0x18(%rsp) +; movq %r14, 0x20(%rsp) +; movq %r15, 0x28(%rsp) +; block0: +; load_ext_name %ghc_one+0, %rdx +; movq %rdi, %r13 +; call *%rdx +; movq (%rsp), %rbx +; movq 8(%rsp), %rbp +; movq 0x10(%rsp), %r12 +; movq 0x18(%rsp), %r13 +; movq 0x20(%rsp), %r14 +; movq 0x28(%rsp), %r15 +; addq $0x30, %rsp +; movq %rbp, %rsp +; popq %rbp +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; pushq %rbp +; movq %rsp, %rbp +; subq $0x30, %rsp +; movq %rbx, (%rsp) +; movq %rbp, 8(%rsp) +; movq %r12, 0x10(%rsp) +; movq %r13, 0x18(%rsp) +; movq %r14, 0x20(%rsp) +; movq %r15, 0x28(%rsp) +; block1: ; offset 0x25 +; movabsq $0, %rdx ; reloc_external Abs8 %ghc_one 0 +; movq %rdi, %r13 +; callq *%rdx +; movq (%rsp), %rbx +; movq 8(%rsp), %rbp +; movq 0x10(%rsp), %r12 +; movq 0x18(%rsp), %r13 +; movq 0x20(%rsp), %r14 +; movq 0x28(%rsp), %r15 +; addq $0x30, %rsp +; movq %rbp, %rsp +; popq %rbp +; retq + +function %ghc_return_call(i64, i64) ghc { + fn0 = %ghc_leaf(i64, i64, i64) ghc +block0(v0: i64, v1: i64): + return_call fn0(v0, v1, v0) +} + +; VCode: +; block0: +; load_ext_name %ghc_leaf+0, %r10 +; movq %r13, %r12 +; return_call_unknown %r10 (0) tmp=%r11 %r13=%r13 %rbp=%rbp %r12=%r12 +; +; Disassembled: +; block0: ; offset 0x0 +; movabsq $0, %r10 ; reloc_external Abs8 %ghc_leaf 0 +; movq %r13, %r12 +; jmpq *%r10 + +function %ghc_float_args(f32, f32, f64) ghc { +block0(v0: f32, v1: f32, v2: f64): + return +} + +; VCode: +; block0: +; retq +; +; Disassembled: +; block0: ; offset 0x0 +; retq + diff --git a/cranelift/filetests/filetests/verifier/ghc-abi.clif b/cranelift/filetests/filetests/verifier/ghc-abi.clif new file mode 100644 index 000000000000..abf1ba69536c --- /dev/null +++ b/cranelift/filetests/filetests/verifier/ghc-abi.clif @@ -0,0 +1,22 @@ +test verifier + +function %f0(i8) -> i8 ghc { ; error: Signature with `ghc` ABI cannot have return values +block0(v0: i8): + return v0 +} + +function %f1(i64) { + sig0 = () -> i8 ghc ; error: Signature with `ghc` ABI cannot have return values + +block0(v0: i64): + v1 = call_indirect sig0, v0() + return +} + +function %f2() { + fn0 = %g() -> i8 ghc ; error: Signature with `ghc` ABI cannot have return values + +block0(): + v1 = call fn0() + return +} diff --git a/cranelift/fuzzgen/src/cranelift_arbitrary.rs b/cranelift/fuzzgen/src/cranelift_arbitrary.rs index 7375f1ad9c8c..73f52a48e99a 100644 --- a/cranelift/fuzzgen/src/cranelift_arbitrary.rs +++ b/cranelift/fuzzgen/src/cranelift_arbitrary.rs @@ -68,6 +68,14 @@ impl<'a> CraneliftArbitrary for &mut Unstructured<'a> { allowed_callconvs.push(CallConv::Winch); } + // GHC calling convention is supported on x64, aarch64, and riscv64. + if matches!( + architecture, + Architecture::X86_64 | Architecture::Aarch64(_) | Architecture::Riscv64(_) + ) { + allowed_callconvs.push(CallConv::Ghc); + } + Ok(*self.choose(&allowed_callconvs[..])?) } @@ -107,11 +115,19 @@ impl<'a> CraneliftArbitrary for &mut Unstructured<'a> { simd_enabled = false; } - // We can't have any returns in the `preserve_all` calling convention. - if callconv == CallConv::PreserveAll { + // We can't have any returns in the `preserve_all` or `ghc` calling + // conventions. + if callconv == CallConv::PreserveAll || callconv == CallConv::Ghc { max_rets = 0; } + // Cap GHC params so fuzzing does not exceed the fixed STG register set. + let max_params = if callconv == CallConv::Ghc { + max_params.min(6) + } else { + max_params + }; + let mut sig = Signature::new(callconv); for _ in 0..max_params { From 3807c8413cdb0212c2e34342b4817b2d17483b1c Mon Sep 17 00:00:00 2001 From: playX18 Date: Tue, 21 Jul 2026 07:32:00 +0700 Subject: [PATCH 2/4] fmt --- cranelift/codegen/src/isa/x64/inst/emit.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cranelift/codegen/src/isa/x64/inst/emit.rs b/cranelift/codegen/src/isa/x64/inst/emit.rs index aa3b3220e45a..2853e2fead2d 100644 --- a/cranelift/codegen/src/isa/x64/inst/emit.rs +++ b/cranelift/codegen/src/isa/x64/inst/emit.rs @@ -1889,8 +1889,7 @@ fn emit_return_call_common_sequence( let tmp = call_info.tmp.to_writable_reg(); - for inst in - X64ABIMachineSpec::gen_clobber_restore(call_conv, &info.flags, state.frame_layout()) + for inst in X64ABIMachineSpec::gen_clobber_restore(call_conv, &info.flags, state.frame_layout()) { inst.emit(sink, info, state); } From 569e23b8fe78ba90b60324238689e9b39df86d55 Mon Sep 17 00:00:00 2001 From: playX18 Date: Tue, 21 Jul 2026 08:49:22 +0700 Subject: [PATCH 3/4] fix(ghc): pad x86_64 frames so SysV call sites are 16-byte aligned MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GHC entries have RSP≡8 (return address only). A 16-aligned frame would leave RSP≡8 before `call`, violating the SysV ABI. Pad by 8 when the function makes any regular call. Co-authored-by: Cursor --- cranelift/codegen/src/isa/x64/abi.rs | 12 ++++++++++++ cranelift/filetests/filetests/isa/x64/ghc.clif | 4 ++++ 2 files changed, 16 insertions(+) diff --git a/cranelift/codegen/src/isa/x64/abi.rs b/cranelift/codegen/src/isa/x64/abi.rs index b49ff0369a9a..54ae0afba847 100644 --- a/cranelift/codegen/src/isa/x64/abi.rs +++ b/cranelift/codegen/src/isa/x64/abi.rs @@ -992,6 +992,18 @@ impl ABIMachineSpec for X64ABIMachineSpec { 16 // RBP, return address }; + // SysV requires RSP ≡ 0 (mod 16) immediately before a `call`. A GHC + // entry has RSP ≡ 8 (retaddr only, no `push %rbp`). A 16-aligned frame + // would leave RSP ≡ 8 at call sites — pad by 8 when this function makes + // any non-tail call. + let mut fixed_frame_storage_size = fixed_frame_storage_size; + if call_conv == CallConv::Ghc && matches!(function_calls, FunctionCalls::Regular) { + let frame = clobber_size + fixed_frame_storage_size + outgoing_args_size; + if frame % 16 == 0 { + fixed_frame_storage_size += 8; + } + } + // Return FrameLayout structure. FrameLayout { word_bytes: 8, diff --git a/cranelift/filetests/filetests/isa/x64/ghc.clif b/cranelift/filetests/filetests/isa/x64/ghc.clif index efa845f0bef1..320249d167be 100644 --- a/cranelift/filetests/filetests/isa/x64/ghc.clif +++ b/cranelift/filetests/filetests/isa/x64/ghc.clif @@ -63,17 +63,21 @@ block0(v0: i64): } ; VCode: +; subq $8, %rsp ; block0: ; load_ext_name %sysv+0, %rsi ; movq %r13, %rdi ; call *%rsi +; addq $8, %rsp ; retq ; ; Disassembled: ; block0: ; offset 0x0 +; subq $8, %rsp ; movabsq $0, %rsi ; reloc_external Abs8 %sysv 0 ; movq %r13, %rdi ; callq *%rsi +; addq $8, %rsp ; retq function %system_v_call_ghc(i64) system_v { From 96ff13c7bc6d7a3657ee186fc530e5174212997e Mon Sep 17 00:00:00 2001 From: playX18 Date: Tue, 21 Jul 2026 09:33:47 +0700 Subject: [PATCH 4/4] fix(filtetests): x86 now aligns the stack to 16 --- cranelift/filetests/filetests/isa/x64/ghc.clif | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cranelift/filetests/filetests/isa/x64/ghc.clif b/cranelift/filetests/filetests/isa/x64/ghc.clif index 320249d167be..5e6d6c280e61 100644 --- a/cranelift/filetests/filetests/isa/x64/ghc.clif +++ b/cranelift/filetests/filetests/isa/x64/ghc.clif @@ -42,17 +42,22 @@ block0(v0: i64, v1: i64): } ; VCode: +; subq $0x8, %rsp ; block0: ; load_ext_name %ghc_leaf+0, %rsi ; movq %r13, %r12 ; call *%rsi +; addq $0x8, %rsp ; retq ; ; Disassembled: ; block0: ; offset 0x0 +; subq $8, %rsp +; block1: ; offset 0x4 ; movabsq $0, %rsi ; reloc_external Abs8 %ghc_leaf 0 ; movq %r13, %r12 ; callq *%rsi +; addq $8, %rsp ; retq function %ghc_call_system_v(i64) ghc { @@ -63,17 +68,18 @@ block0(v0: i64): } ; VCode: -; subq $8, %rsp +; subq $0x8, %rsp ; block0: ; load_ext_name %sysv+0, %rsi ; movq %r13, %rdi ; call *%rsi -; addq $8, %rsp +; addq $0x8, %rsp ; retq ; ; Disassembled: ; block0: ; offset 0x0 ; subq $8, %rsp +; block1: ; offset 0x4 ; movabsq $0, %rsi ; reloc_external Abs8 %sysv 0 ; movq %r13, %rdi ; callq *%rsi