Skip to content

Commit 2dd0823

Browse files
committed
[cc] aarch64 varargs fixes; new CLI arg
Summary Root Cause of CI Failures The failing tests (varargs_caller_comprehensive and varargs_float_args) were caused by a calling convention mismatch on Apple ARM64 (Darwin/macOS). The Apple ARM64 ABI differs from standard AAPCS64 for variadic functions: - AAPCS64 (Linux): Variadic arguments use normal register allocation (x0-x7 for integers, d0-d7 for floats) - Apple ARM64 (Darwin): ALL variadic arguments must be passed on the stack, not in registers Our codegen was using the same register-based passing for all platforms, which worked on Linux but failed on the macOS CI runners (Apple Silicon). Fix Applied Modified cc/arch/aarch64/codegen.rs emit_call() function to: 1. Detect Darwin variadic calls via insn.variadic_arg_start and target.os == MacOS 2. For Darwin variadic calls: Pass fixed args in registers as normal, but push all variadic arguments onto the stack 3. For Linux/FreeBSD: Keep existing behavior (registers for all args) Assembly Comparison Before (incorrect for Darwin): mov w2, #42 ; variadic arg in register - WRONG bl _sprintf After (correct for Darwin): mov w9, #42 str x9, [sp, #-16]! ; variadic arg on stack - CORRECT bl _sprintf add sp, sp, #16 Additional Feature Added --print-targets CLI argument that displays available target architectures, matching clang's behavior: $ ./target/release/pcc --print-targets Registered Targets: aarch64 - AArch64 (little endian) x86-64 - 64-bit X86: EM64T and AMD64 Files Modified - cc/arch/aarch64/codegen.rs - Fixed variadic argument passing for Darwin - cc/main.rs - Added --print-targets argument Test Results - All 313 tests pass locally on x86-64 - The varargs tests should now pass on the macOS aarch64 CI runner Sources: - https://github.com/ARM-software/abi-aa/blob/main/aapcs64/aapcs64.rst - https://dyncall.org/docs/manual/manualse11.html
1 parent 5485bd7 commit 2dd0823

File tree

2 files changed

+149
-51
lines changed

2 files changed

+149
-51
lines changed

cc/arch/aarch64/codegen.rs

Lines changed: 136 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2984,13 +2984,22 @@ impl Aarch64CodeGen {
29842984
// - Floating-point arguments: V0-V7 (8 registers)
29852985
// - Indirect result (large struct return): X8
29862986
// Each class has its own register allocation
2987+
//
2988+
// Apple ARM64 (Darwin) divergence for variadic functions:
2989+
// ALL variadic arguments (after the last named parameter) must be passed
2990+
// on the stack, not in registers. This differs from standard AAPCS64.
29872991
let int_arg_regs = Reg::arg_regs();
29882992
let fp_arg_regs = VReg::arg_regs();
29892993

29902994
let mut int_arg_idx = 0;
29912995
let mut fp_arg_idx = 0;
29922996
let mut stack_args = 0;
29932997

2998+
// For Darwin variadic calls, determine where variadic args start
2999+
let variadic_start = insn.variadic_arg_start.unwrap_or(usize::MAX);
3000+
let is_darwin_variadic =
3001+
self.target.os == crate::target::Os::MacOS && insn.variadic_arg_start.is_some();
3002+
29943003
// Check if this call returns a large struct
29953004
// If so, the first argument is the sret pointer and goes in X8 (not X0)
29963005
let returns_large_struct = insn.typ.as_ref().is_some_and(|t| {
@@ -3018,71 +3027,148 @@ impl Aarch64CodeGen {
30183027
0
30193028
};
30203029

3021-
// Move arguments to registers
3022-
for (i, &arg) in insn.src.iter().enumerate().skip(args_start) {
3023-
// Get argument type if available, otherwise fall back to location-based detection
3024-
let arg_type = insn.arg_types.get(i);
3025-
let is_fp = if let Some(typ) = arg_type {
3026-
typ.is_float()
3027-
} else {
3028-
// Fall back to location-based detection for backwards compatibility
3029-
let arg_loc = self.get_location(arg);
3030-
matches!(arg_loc, Loc::VReg(_) | Loc::FImm(_))
3031-
};
3032-
3033-
// Get argument size from type, with minimum 32-bit for register ops
3034-
let arg_size = if let Some(typ) = arg_type {
3035-
typ.size_bits().max(32)
3036-
} else {
3037-
64 // Default for backwards compatibility
3038-
};
3030+
// For Darwin variadic calls, we need to:
3031+
// 1. First pass fixed args in registers as normal
3032+
// 2. Then push ALL variadic args onto the stack (in reverse order for correct layout)
3033+
if is_darwin_variadic {
3034+
// Collect variadic args to push in reverse order
3035+
let mut variadic_args: Vec<(PseudoId, bool, u32)> = Vec::new();
3036+
3037+
// Process all arguments
3038+
for (i, &arg) in insn.src.iter().enumerate().skip(args_start) {
3039+
let arg_type = insn.arg_types.get(i);
3040+
let is_fp = if let Some(typ) = arg_type {
3041+
typ.is_float()
3042+
} else {
3043+
let arg_loc = self.get_location(arg);
3044+
matches!(arg_loc, Loc::VReg(_) | Loc::FImm(_))
3045+
};
30393046

3040-
if is_fp {
3041-
// FP size from type (32 for float, 64 for double)
3042-
let fp_size = if let Some(typ) = arg_type {
3043-
typ.size_bits()
3047+
let arg_size = if let Some(typ) = arg_type {
3048+
typ.size_bits().max(32)
30443049
} else {
30453050
64
30463051
};
3047-
if fp_arg_idx < fp_arg_regs.len() {
3048-
self.emit_fp_move(arg, fp_arg_regs[fp_arg_idx], fp_size, frame_size);
3049-
fp_arg_idx += 1;
3052+
3053+
if i >= variadic_start {
3054+
// Variadic arg - collect for stack pushing
3055+
variadic_args.push((arg, is_fp, arg_size));
30503056
} else {
3051-
// FP arg on stack
3052-
self.emit_fp_move(arg, VReg::V16, fp_size, frame_size);
3053-
// LIR: store FP reg to stack with pre-decrement
3054-
let fp_sz = if fp_size == 32 {
3055-
FpSize::Single
3056-
} else {
3057-
FpSize::Double
3058-
};
3057+
// Fixed arg - use registers as normal
3058+
if is_fp {
3059+
let fp_size = if let Some(typ) = arg_type {
3060+
typ.size_bits()
3061+
} else {
3062+
64
3063+
};
3064+
if fp_arg_idx < fp_arg_regs.len() {
3065+
self.emit_fp_move(arg, fp_arg_regs[fp_arg_idx], fp_size, frame_size);
3066+
fp_arg_idx += 1;
3067+
}
3068+
// Fixed args shouldn't overflow to stack in well-formed calls
3069+
} else if int_arg_idx < int_arg_regs.len() {
3070+
self.emit_move(arg, int_arg_regs[int_arg_idx], arg_size, frame_size);
3071+
int_arg_idx += 1;
3072+
}
3073+
}
3074+
}
3075+
3076+
// Push variadic args in reverse order so first variadic arg ends up at [sp]
3077+
for (arg, is_fp, arg_size) in variadic_args.into_iter().rev() {
3078+
if is_fp {
3079+
// Load FP value to temp register, then store to stack
3080+
// For variadic calls on Darwin, floats are passed as 8-byte values on stack
3081+
self.emit_fp_move(arg, VReg::V16, arg_size, frame_size);
30593082
self.push_lir(Aarch64Inst::StrFp {
3060-
size: fp_sz,
3083+
size: FpSize::Double, // Always 8 bytes on stack for Darwin variadic
30613084
src: VReg::V16,
30623085
addr: MemAddr::PreIndex {
30633086
base: Reg::SP,
30643087
offset: -16,
30653088
},
30663089
});
3067-
stack_args += 1;
3090+
} else {
3091+
// Integer/pointer arg
3092+
self.emit_move(arg, Reg::X9, arg_size, frame_size);
3093+
self.push_lir(Aarch64Inst::Str {
3094+
size: OperandSize::B64,
3095+
src: Reg::X9,
3096+
addr: MemAddr::PreIndex {
3097+
base: Reg::SP,
3098+
offset: -16,
3099+
},
3100+
});
30683101
}
3069-
} else if int_arg_idx < int_arg_regs.len() {
3070-
self.emit_move(arg, int_arg_regs[int_arg_idx], arg_size, frame_size);
3071-
int_arg_idx += 1;
3072-
} else {
3073-
// Integer arg on stack - always store 8 bytes on aarch64
3074-
self.emit_move(arg, Reg::X9, arg_size, frame_size);
3075-
// LIR: store GP reg to stack with pre-decrement
3076-
self.push_lir(Aarch64Inst::Str {
3077-
size: OperandSize::B64,
3078-
src: Reg::X9,
3079-
addr: MemAddr::PreIndex {
3080-
base: Reg::SP,
3081-
offset: -16,
3082-
},
3083-
});
30843102
stack_args += 1;
30853103
}
3104+
} else {
3105+
// Standard AAPCS64 (Linux, FreeBSD) or non-variadic calls
3106+
// Move arguments to registers
3107+
for (i, &arg) in insn.src.iter().enumerate().skip(args_start) {
3108+
// Get argument type if available, otherwise fall back to location-based detection
3109+
let arg_type = insn.arg_types.get(i);
3110+
let is_fp = if let Some(typ) = arg_type {
3111+
typ.is_float()
3112+
} else {
3113+
// Fall back to location-based detection for backwards compatibility
3114+
let arg_loc = self.get_location(arg);
3115+
matches!(arg_loc, Loc::VReg(_) | Loc::FImm(_))
3116+
};
3117+
3118+
// Get argument size from type, with minimum 32-bit for register ops
3119+
let arg_size = if let Some(typ) = arg_type {
3120+
typ.size_bits().max(32)
3121+
} else {
3122+
64 // Default for backwards compatibility
3123+
};
3124+
3125+
if is_fp {
3126+
// FP size from type (32 for float, 64 for double)
3127+
let fp_size = if let Some(typ) = arg_type {
3128+
typ.size_bits()
3129+
} else {
3130+
64
3131+
};
3132+
if fp_arg_idx < fp_arg_regs.len() {
3133+
self.emit_fp_move(arg, fp_arg_regs[fp_arg_idx], fp_size, frame_size);
3134+
fp_arg_idx += 1;
3135+
} else {
3136+
// FP arg on stack
3137+
self.emit_fp_move(arg, VReg::V16, fp_size, frame_size);
3138+
// LIR: store FP reg to stack with pre-decrement
3139+
let fp_sz = if fp_size == 32 {
3140+
FpSize::Single
3141+
} else {
3142+
FpSize::Double
3143+
};
3144+
self.push_lir(Aarch64Inst::StrFp {
3145+
size: fp_sz,
3146+
src: VReg::V16,
3147+
addr: MemAddr::PreIndex {
3148+
base: Reg::SP,
3149+
offset: -16,
3150+
},
3151+
});
3152+
stack_args += 1;
3153+
}
3154+
} else if int_arg_idx < int_arg_regs.len() {
3155+
self.emit_move(arg, int_arg_regs[int_arg_idx], arg_size, frame_size);
3156+
int_arg_idx += 1;
3157+
} else {
3158+
// Integer arg on stack - always store 8 bytes on aarch64
3159+
self.emit_move(arg, Reg::X9, arg_size, frame_size);
3160+
// LIR: store GP reg to stack with pre-decrement
3161+
self.push_lir(Aarch64Inst::Str {
3162+
size: OperandSize::B64,
3163+
src: Reg::X9,
3164+
addr: MemAddr::PreIndex {
3165+
base: Reg::SP,
3166+
offset: -16,
3167+
},
3168+
});
3169+
stack_args += 1;
3170+
}
3171+
}
30863172
}
30873173

30883174
// Call the function

cc/main.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@ use token::{preprocess, show_token, token_type_name, StreamTable, Tokenizer};
4343
#[command(version, about = gettext("pcc - compile standard C programs"))]
4444
struct Args {
4545
/// Input files
46-
#[arg(required = true)]
46+
#[arg(required_unless_present = "print_targets")]
4747
files: Vec<String>,
4848

49+
/// Print registered targets
50+
#[arg(long = "print-targets", help = gettext("Display available target architectures"))]
51+
print_targets: bool,
52+
4953
/// Dump tokens (for debugging tokenizer)
5054
#[arg(
5155
short = 'T',
@@ -331,6 +335,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
331335

332336
let args = Args::parse();
333337

338+
// Handle --print-targets
339+
if args.print_targets {
340+
println!(" Registered Targets:");
341+
println!(" aarch64 - AArch64 (little endian)");
342+
println!(" x86-64 - 64-bit X86: EM64T and AMD64");
343+
return Ok(());
344+
}
345+
334346
// Detect target (use --target if specified, otherwise detect host)
335347
let target = if let Some(ref triple) = args.target {
336348
match Target::from_triple(triple) {

0 commit comments

Comments
 (0)