@@ -1447,6 +1447,14 @@ impl Memory {
14471447pub 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
14521460unsafe 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+
28753010fn 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 {
0 commit comments