Skip to content

Commit af78743

Browse files
deuszxclaude
andcommitted
fix(singlepass): read float return values from SIMD registers per C ABI
The singlepass compiler's `emit_call_native` reads all return values from GPR registers (RAX/RDX on x86_64, X0-X7 on ARM64, X10+ on RISC-V). However, the platform C ABIs return floating-point values in SIMD registers (XMM0-XMM1 on x86_64, V0-V7 on ARM64, FA0-FA1 on RISC-V). This causes f32/f64 return values from imported host functions to contain garbage. This regression was introduced when return value handling was moved from the type-aware `Operator::Call` handler into `emit_call_native`, which uses `get_return_value_location()` — a function that unconditionally returns GPR locations regardless of the value type. The fix adds `get_simd_return_register()` to the Machine trait, which returns the correct SIMD register for float return values per the platform's C ABI. After building the return value location list in `emit_call_native`, float entries are replaced with the correct SIMD register. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 492ef3b commit af78743

File tree

6 files changed

+104
-1
lines changed

6 files changed

+104
-1
lines changed

lib/compiler-singlepass/src/codegen.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -663,7 +663,8 @@ impl<'a, M: Machine> FuncGen<'a, M> {
663663
_ => Size::S64,
664664
};
665665
let param_sizes = params_type.map(get_size).collect_vec();
666-
let return_value_sizes = return_types.map(get_size).collect_vec();
666+
let return_wptypes = return_types.collect_vec();
667+
let return_value_sizes = return_wptypes.iter().map(|t| get_size(*t)).collect_vec();
667668

668669
/* We're going to reuse the memory param locations for the return values. Any extra needed slots will be allocated on stack. */
669670
let used_stack_params = stack_params
@@ -712,6 +713,18 @@ impl<'a, M: Machine> FuncGen<'a, M> {
712713
self.calling_convention,
713714
));
714715
}
716+
// The C ABI returns floating-point values in SIMD registers (XMM0 on x86_64,
717+
// V0 on ARM64, FA0 on RISC-V), not in GPRs. Fix up the return value locations
718+
// so that we read float results from the correct registers.
719+
let mut float_idx = 0usize;
720+
for (i, wp_type) in return_wptypes.iter().enumerate() {
721+
if matches!(wp_type, WpType::F32 | WpType::F64) {
722+
if let Some(simd_loc) = self.machine.get_simd_return_register(float_idx) {
723+
return_args[i] = simd_loc;
724+
}
725+
float_idx += 1;
726+
}
727+
}
715728

716729
// Allocate space for arguments relative to SP.
717730
let mut args = Vec::with_capacity(params.len());

lib/compiler-singlepass/src/machine.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ pub trait Machine {
247247
idx: usize,
248248
calling_convention: CallingConvention,
249249
) -> Location<Self::GPR, Self::SIMD>;
250+
/// Get the SIMD register used for the nth floating-point return value in the
251+
/// platform's C ABI. Returns `None` if the value would be on the stack.
252+
fn get_simd_return_register(&self, float_idx: usize) -> Option<Location<Self::GPR, Self::SIMD>>;
250253
/// move a location to another
251254
fn move_location(
252255
&mut self,

lib/compiler-singlepass/src/machine_arm64.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1830,6 +1830,17 @@ impl Machine for MachineARM64 {
18301830
)
18311831
}
18321832

1833+
fn get_simd_return_register(&self, float_idx: usize) -> Option<AbstractLocation<Self::GPR, Self::SIMD>> {
1834+
// AAPCS64: floating-point return values are in V0-V7.
1835+
const AAPCS64_FLOAT_RETURN_REGISTERS: [NEON; 8] = [
1836+
NEON::V0, NEON::V1, NEON::V2, NEON::V3,
1837+
NEON::V4, NEON::V5, NEON::V6, NEON::V7,
1838+
];
1839+
AAPCS64_FLOAT_RETURN_REGISTERS
1840+
.get(float_idx)
1841+
.map(|&reg| AbstractLocation::SIMD(reg))
1842+
}
1843+
18331844
// move a location to another
18341845
fn move_location(
18351846
&mut self,

lib/compiler-singlepass/src/machine_riscv.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2304,6 +2304,14 @@ impl Machine for MachineRiscv {
23042304
)
23052305
}
23062306

2307+
fn get_simd_return_register(&self, float_idx: usize) -> Option<AbstractLocation<Self::GPR, Self::SIMD>> {
2308+
// RISC-V: floating-point return values are in FA0 (F10) and FA1 (F11).
2309+
const RISCV_FLOAT_RETURN_REGISTERS: [FPR; 2] = [FPR::F10, FPR::F11];
2310+
RISCV_FLOAT_RETURN_REGISTERS
2311+
.get(float_idx)
2312+
.map(|&reg| AbstractLocation::SIMD(reg))
2313+
}
2314+
23072315
// move a location to another
23082316
fn move_location(
23092317
&mut self,

lib/compiler-singlepass/src/machine_x64.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,6 +2397,15 @@ impl Machine for MachineX86_64 {
23972397
)
23982398
}
23992399

2400+
fn get_simd_return_register(&self, float_idx: usize) -> Option<Location> {
2401+
// System V AMD64 ABI: floating-point return values are in XMM0-XMM1.
2402+
// Windows x64: floating-point return value is in XMM0 only.
2403+
const SYSV_FLOAT_RETURN_REGISTERS: [XMM; 2] = [XMM::XMM0, XMM::XMM1];
2404+
SYSV_FLOAT_RETURN_REGISTERS
2405+
.get(float_idx)
2406+
.map(|&reg| Location::SIMD(reg))
2407+
}
2408+
24002409
// move a location to another
24012410
fn move_location(
24022411
&mut self,

tests/compilers/imports.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,62 @@ fn instance_local_memory_lifetime(config: crate::Config) -> Result<()> {
513513

514514
Ok(())
515515
}
516+
517+
/// Test that floating-point return values from imported host functions are
518+
/// correctly propagated back to WASM. This covers f32 and f64 return values
519+
/// which use SIMD registers (XMM0 on x86_64, V0 on ARM64, FA0 on RISC-V)
520+
/// per the platform's C ABI, whereas singlepass internally expects all return
521+
/// values in GPR registers.
522+
#[compiler_test(imports)]
523+
fn float_return_values_from_imports(config: crate::Config) -> Result<()> {
524+
let mut store = config.store();
525+
526+
// Test f32 return from import
527+
let wat_f32 = r#"(module
528+
(import "host" "get" (func $get (result f32)))
529+
(func (export "test") (result f32) call $get)
530+
)"#;
531+
let module = Module::new(&store, wat_f32)?;
532+
let get_f32 = Function::new_typed(&mut store, || -> f32 { -0.125 });
533+
let imports = imports! { "host" => { "get" => get_f32 } };
534+
let instance = Instance::new(&mut store, &module, &imports)?;
535+
let test: TypedFunction<(), f32> = instance.exports.get_typed_function(&store, "test")?;
536+
assert_eq!(test.call(&mut store)?, -0.125f32);
537+
538+
// Test f64 return from import
539+
let wat_f64 = r#"(module
540+
(import "host" "get" (func $get (result f64)))
541+
(func (export "test") (result f64) call $get)
542+
)"#;
543+
let module = Module::new(&store, wat_f64)?;
544+
let get_f64 = Function::new_typed(&mut store, || -> f64 { 128.25 });
545+
let imports = imports! { "host" => { "get" => get_f64 } };
546+
let instance = Instance::new(&mut store, &module, &imports)?;
547+
let test: TypedFunction<(), f64> = instance.exports.get_typed_function(&store, "test")?;
548+
assert_eq!(test.call(&mut store)?, 128.25f64);
549+
550+
// Verify i32 and i64 still work (regression guard)
551+
let wat_i32 = r#"(module
552+
(import "host" "get" (func $get (result i32)))
553+
(func (export "test") (result i32) call $get)
554+
)"#;
555+
let module = Module::new(&store, wat_i32)?;
556+
let get_i32 = Function::new_typed(&mut store, || -> i32 { 42 });
557+
let imports = imports! { "host" => { "get" => get_i32 } };
558+
let instance = Instance::new(&mut store, &module, &imports)?;
559+
let test: TypedFunction<(), i32> = instance.exports.get_typed_function(&store, "test")?;
560+
assert_eq!(test.call(&mut store)?, 42);
561+
562+
let wat_i64 = r#"(module
563+
(import "host" "get" (func $get (result i64)))
564+
(func (export "test") (result i64) call $get)
565+
)"#;
566+
let module = Module::new(&store, wat_i64)?;
567+
let get_i64 = Function::new_typed(&mut store, || -> i64 { 123456789 });
568+
let imports = imports! { "host" => { "get" => get_i64 } };
569+
let instance = Instance::new(&mut store, &module, &imports)?;
570+
let test: TypedFunction<(), i64> = instance.exports.get_typed_function(&store, "test")?;
571+
assert_eq!(test.call(&mut store)?, 123456789i64);
572+
573+
Ok(())
574+
}

0 commit comments

Comments
 (0)