Skip to content

Commit a25098d

Browse files
committed
fix(hir_interp): typed FFI marshalling — float args via float ABI
`call_extern_symbol` routed every arg through `value_to_i64`, which bit-truncated `ZyntaxValue::Float(2.5)` to `2` before the call. The callee's f64 parameter then read xmm0/v0 (whatever the int register file happened to leave there), producing garbage. Any FFI symbol with a float parameter — `zyntax_box_f64`, plugin math helpers, custom runtime hooks — was silently broken on the BC interp tier. `SymbolEntry` gains an optional `ZrtlSymbolSig`. When set, `Op::CallSym` dispatches through a new `call_extern_symbol_typed` that consults the TypeTag for each arg and the return — float args ride the float ABI, ints / pointers stay on the int register file. `register_zrtl_symbols` in interp_runtime now prefers the typed path when a sig is present. Surfaces every Any cast through the box helpers correctly on the BC interp tier — bench_any_cast and bench_any_field both return correct values across all four tiers.
1 parent 5e9113b commit a25098d

2 files changed

Lines changed: 154 additions & 8 deletions

File tree

crates/compiler/src/hir_interp.rs

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,6 +1447,14 @@ impl Memory {
14471447
pub struct SymbolEntry {
14481448
pub ptr: *const u8,
14491449
pub param_count: u8,
1450+
/// Optional ZRTL signature describing arg / return TypeTags. When
1451+
/// `Some`, `Op::CallSym` dispatches through a typed-marshalling
1452+
/// path that respects the platform's float ABI (float args ride
1453+
/// xmm/v registers, not the int register file). Required for any
1454+
/// FFI symbol whose signature includes f32 / f64 — without it,
1455+
/// `value_to_i64` truncates floats to integers and the callee
1456+
/// reads garbage.
1457+
pub sig: Option<crate::zrtl::ZrtlSymbolSig>,
14501458
}
14511459

14521460
unsafe impl Send for SymbolEntry {}
@@ -1638,8 +1646,35 @@ impl HirInterpreter {
16381646
}
16391647

16401648
pub fn register_symbol(&mut self, name: impl Into<String>, ptr: *const u8, param_count: u8) {
1641-
self.symbols
1642-
.insert(name.into(), SymbolEntry { ptr, param_count });
1649+
self.symbols.insert(
1650+
name.into(),
1651+
SymbolEntry {
1652+
ptr,
1653+
param_count,
1654+
sig: None,
1655+
},
1656+
);
1657+
}
1658+
1659+
/// Typed registration variant — stores a ZRTL signature alongside
1660+
/// the function pointer so the BC interp's `Op::CallSym` can
1661+
/// route float arguments through the platform float ABI instead
1662+
/// of bit-truncating them.
1663+
pub fn register_symbol_typed(
1664+
&mut self,
1665+
name: impl Into<String>,
1666+
ptr: *const u8,
1667+
sig: crate::zrtl::ZrtlSymbolSig,
1668+
) {
1669+
let param_count = sig.param_count;
1670+
self.symbols.insert(
1671+
name.into(),
1672+
SymbolEntry {
1673+
ptr,
1674+
param_count,
1675+
sig: Some(sig),
1676+
},
1677+
);
16431678
}
16441679

16451680
/// Snapshot of every registered FFI symbol — name → `(ptr,
@@ -2306,9 +2341,25 @@ impl HirInterpreter {
23062341
.ok_or_else(|| InterpError::UnknownFunction(name.clone()))?
23072342
}
23082343
};
2309-
let raw = call_extern_symbol(entry.ptr, &arg_vals);
2310-
let ty = &cf.type_pool[*ret_ty as usize];
2311-
value_from_i64_as(ty, raw)
2344+
// Typed marshalling path. When the symbol was
2345+
// registered with a ZRTL signature
2346+
// (e.g. via `register_zrtl_symbols` for the
2347+
// `zyntax_box_*` family) the dispatch routes
2348+
// float args through the platform float ABI
2349+
// and reads the return register matching the
2350+
// declared return TypeTag. Without this, f64
2351+
// args bit-truncate through `value_to_i64` —
2352+
// `zyntax_box_f64(2.5)` would arrive at the
2353+
// callee with `xmm0 == 0.0`, silently
2354+
// poisoning every Any cast on the BC interp
2355+
// tier.
2356+
if let Some(sig) = entry.sig {
2357+
call_extern_symbol_typed(entry.ptr, &arg_vals, &sig)
2358+
} else {
2359+
let raw = call_extern_symbol(entry.ptr, &arg_vals);
2360+
let ty = &cf.type_pool[*ret_ty as usize];
2361+
value_from_i64_as(ty, raw)
2362+
}
23122363
};
23132364

23142365
if *has_dst {
@@ -2872,6 +2923,90 @@ pub fn jit_float_mask(params: &[HirType]) -> u8 {
28722923
mask
28732924
}
28742925

2926+
/// Typed marshalling of an FFI symbol call.
2927+
///
2928+
/// Routes each argument through the correct register file based on
2929+
/// its declared `TypeTag` — float categories take the float ABI
2930+
/// (`f64` / `f32` register), everything else flows through the
2931+
/// integer register file via `value_to_i64`. The return is decoded
2932+
/// from the matching register according to `sig.return_type`.
2933+
///
2934+
/// Only the 1-arg shapes used by the `zyntax_box_*` family are
2935+
/// covered today; broader shapes fall through to the legacy untyped
2936+
/// path, which is fine because the only signatures currently
2937+
/// registered typed are box constructors / unboxers (param_count = 1)
2938+
/// plus the void `zyntax_box_free`.
2939+
///
2940+
/// Cross-frontend: any DSL that registers FFI symbols with float
2941+
/// parameters benefits from this path automatically — there is no
2942+
/// ZynML-specific logic here.
2943+
fn call_extern_symbol_typed(
2944+
ptr: *const u8,
2945+
args: &[ZyntaxValue],
2946+
sig: &crate::zrtl::ZrtlSymbolSig,
2947+
) -> ZyntaxValue {
2948+
use crate::hir::HirType;
2949+
use crate::zrtl::TypeCategory;
2950+
2951+
let pcount = sig.param_count as usize;
2952+
let arg_is_float = |i: usize| matches!(sig.params[i].category(), TypeCategory::Float);
2953+
let ret_cat = sig.return_type.category();
2954+
let ret_hir = match ret_cat {
2955+
TypeCategory::Void => HirType::Void,
2956+
TypeCategory::Bool => HirType::Bool,
2957+
TypeCategory::Int | TypeCategory::UInt => HirType::I64,
2958+
TypeCategory::Float => HirType::F64,
2959+
TypeCategory::Pointer | TypeCategory::Opaque => HirType::I64,
2960+
_ => HirType::I64,
2961+
};
2962+
2963+
// The supported shape (1 param, float-or-int → int-or-float).
2964+
// Everything else falls through to the integer-register path.
2965+
if pcount == 1 {
2966+
let a0_float = arg_is_float(0);
2967+
let raw_int = || value_to_i64(&args[0]).unwrap_or(0);
2968+
let raw_f64 = || value_to_f64(&args[0]).unwrap_or(0.0);
2969+
unsafe {
2970+
return match (a0_float, ret_cat) {
2971+
(false, TypeCategory::Void) => {
2972+
let f: extern "C" fn(i64) = core::mem::transmute(ptr);
2973+
f(raw_int());
2974+
ZyntaxValue::Void
2975+
}
2976+
(false, TypeCategory::Float) => {
2977+
let f: extern "C" fn(i64) -> f64 = core::mem::transmute(ptr);
2978+
ZyntaxValue::Float(f(raw_int()))
2979+
}
2980+
(false, _) => {
2981+
let f: extern "C" fn(i64) -> i64 = core::mem::transmute(ptr);
2982+
value_from_i64_as(&ret_hir, f(raw_int()))
2983+
}
2984+
(true, TypeCategory::Void) => {
2985+
let f: extern "C" fn(f64) = core::mem::transmute(ptr);
2986+
f(raw_f64());
2987+
ZyntaxValue::Void
2988+
}
2989+
(true, TypeCategory::Float) => {
2990+
let f: extern "C" fn(f64) -> f64 = core::mem::transmute(ptr);
2991+
ZyntaxValue::Float(f(raw_f64()))
2992+
}
2993+
(true, _) => {
2994+
let f: extern "C" fn(f64) -> i64 = core::mem::transmute(ptr);
2995+
value_from_i64_as(&ret_hir, f(raw_f64()))
2996+
}
2997+
};
2998+
}
2999+
}
3000+
3001+
// Fallback: integer-register marshalling. Safe as long as no
3002+
// parameter is `Float` — the caller is expected to only register
3003+
// typed signatures with shapes this fn can route, but the
3004+
// fallback keeps the call sound (no UB) even if a wider sig
3005+
// sneaks in.
3006+
let raw = call_extern_symbol(ptr, args);
3007+
value_from_i64_as(&ret_hir, raw)
3008+
}
3009+
28753010
fn call_extern_symbol(ptr: *const u8, args: &[ZyntaxValue]) -> i64 {
28763011
let raw_args: Vec<i64> = args.iter().map(|v| value_to_i64(v).unwrap_or(0)).collect();
28773012
unsafe {

crates/zyntax_embed/src/interp_runtime.rs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -418,9 +418,20 @@ impl InterpRuntime {
418418
/// otherwise defaults to 0.
419419
pub fn register_zrtl_symbols(&mut self, symbols: &[zyntax_compiler::zrtl::RuntimeSymbolInfo]) {
420420
for sym in symbols {
421-
let param_count = sym.sig.map(|s| s.param_count).unwrap_or(0);
422-
self.interp
423-
.register_symbol(sym.name.to_string(), sym.ptr, param_count);
421+
// Prefer the typed registration path when a signature is
422+
// present — that wires the BC interp's `Op::CallSym`
423+
// marshalling through the platform float ABI so float
424+
// params don't bit-truncate. The untyped fallback stays
425+
// for symbols registered without a sig (legacy plugin
426+
// surface).
427+
match sym.sig {
428+
Some(sig) => self
429+
.interp
430+
.register_symbol_typed(sym.name.to_string(), sym.ptr, sig),
431+
None => self
432+
.interp
433+
.register_symbol(sym.name.to_string(), sym.ptr, 0),
434+
}
424435
}
425436
}
426437

0 commit comments

Comments
 (0)