Skip to content

Commit efe009e

Browse files
committed
fix(ssa): autounbox values read from Type::Any struct fields
Counterpart to the autobox-on-store fix in `1f438e8`. When user code accesses a field whose declared type is `Type::Any` AND the use-site has a concrete target type (let binding annotation, function parameter, return statement, …), the field-load now routes through the matching `zyntax_box_get_X` runtime helper so the user gets the stored value back instead of the raw `*mut DynamicBox` pointer. Together with the autobox-on-store side this closes the round-trip loop: `let bag = Bag { payload: 1.5 } ; let v: f64 = bag.payload` now does the right thing — `Bag` literal boxes `1.5` via `zyntax_box_f64`, field read uses `zyntax_box_get_f64` because the let binding constrains the use-site type. Before this commit, the read returned an i64 box pointer that the user would have to manually unwrap, which was — to use the technical term — silly. Runtime side: added the missing getter helpers `zyntax_box_get_{i32,f32,f64,bool}` in `zrtl.rs` mirroring the existing `_i64` shape. Sizes are read from the box's `size` field so source values that were narrower than the requested type widen losslessly (i8 → i32, f32 → f64 etc.). Test: new `bench_any_field` kernel exercises 1M round-trips of storing f64 1.5 through an `Any` field and reading back as f64, sums them, and asserts `Int(1500000)`. The bench harness's value gate catches any regression that would re-introduce silent miscompiles. Existing kernels unchanged — none use `Any` fields, so both the autobox and autounbox helpers are no-ops for them.
1 parent 1f438e8 commit efe009e

4 files changed

Lines changed: 170 additions & 2 deletions

File tree

crates/compiler/src/ssa.rs

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4405,7 +4405,17 @@ impl SsaBuilder {
44054405
},
44064406
);
44074407
self.add_use(field_ptr, load_result);
4408-
return Ok(load_result);
4408+
// Unbox if the field is `Type::Any` and the
4409+
// use-site has a concrete target type. See
4410+
// `maybe_unbox_for_any_field` for the
4411+
// `zyntax_box_get_X` selection table.
4412+
return Ok(self.maybe_unbox_for_any_field(
4413+
block_id,
4414+
load_result,
4415+
&object_type,
4416+
field_index as usize,
4417+
&expr.ty,
4418+
));
44094419
}
44104420
}
44114421
}
@@ -4510,7 +4520,15 @@ impl SsaBuilder {
45104520
self.add_instruction(block_id, inst);
45114521
self.add_use(object_val, result);
45124522

4513-
Ok(result)
4523+
// Unbox if the declared field type is `Type::Any` and
4524+
// the use-site has a concrete target type.
4525+
Ok(self.maybe_unbox_for_any_field(
4526+
block_id,
4527+
result,
4528+
&object_type,
4529+
field_index as usize,
4530+
&expr.ty,
4531+
))
45144532
}
45154533

45164534
TypedExpression::Index(index_expr) => {
@@ -7481,6 +7499,68 @@ impl SsaBuilder {
74817499
boxed
74827500
}
74837501

7502+
/// Counterpart to [`maybe_box_for_any_field`]: when reading a
7503+
/// field whose DECLARED type is `Type::Any` and the use-site's
7504+
/// inferred type (`use_site_ty`) is a concrete primitive, emit a
7505+
/// `zyntax_box_get_X` call to unwrap the box pointer. Leaves
7506+
/// boxes intact when the use-site is also `Type::Any` (the box
7507+
/// pointer flows through as-is to whatever next consumes it).
7508+
///
7509+
/// Pairs with the autobox-on-store rule so that
7510+
/// `let x: f64 = b.value` round-trips a stored f64 cleanly
7511+
/// instead of leaving the user holding a raw `*mut DynamicBox`.
7512+
fn maybe_unbox_for_any_field(
7513+
&mut self,
7514+
block_id: HirId,
7515+
boxed_value: HirId,
7516+
object_ty: &Type,
7517+
field_index: usize,
7518+
use_site_ty: &Type,
7519+
) -> HirId {
7520+
// Field-side gate: only act on fields declared `Type::Any`.
7521+
let field_is_any = self
7522+
.get_field_typed_types(object_ty)
7523+
.and_then(|fs| fs.get(field_index).cloned())
7524+
.map(|t| matches!(t, Type::Any))
7525+
.unwrap_or(false);
7526+
if !field_is_any {
7527+
return boxed_value;
7528+
}
7529+
// Use-site gate: only unbox when there's a concrete target
7530+
// type. If the surrounding context also wants `Any`, the box
7531+
// pointer is exactly the right thing to forward unchanged.
7532+
use zyntax_typed_ast::PrimitiveType;
7533+
let (get_symbol, result_hir_ty) = match use_site_ty {
7534+
Type::Primitive(PrimitiveType::I8)
7535+
| Type::Primitive(PrimitiveType::I16)
7536+
| Type::Primitive(PrimitiveType::I32)
7537+
| Type::Primitive(PrimitiveType::U8)
7538+
| Type::Primitive(PrimitiveType::U16)
7539+
| Type::Primitive(PrimitiveType::U32) => ("zyntax_box_get_i32", HirType::I32),
7540+
Type::Primitive(PrimitiveType::I64) | Type::Primitive(PrimitiveType::U64) => {
7541+
("zyntax_box_get_i64", HirType::I64)
7542+
}
7543+
Type::Primitive(PrimitiveType::F32) => ("zyntax_box_get_f32", HirType::F32),
7544+
Type::Primitive(PrimitiveType::F64) => ("zyntax_box_get_f64", HirType::F64),
7545+
Type::Primitive(PrimitiveType::Bool) => ("zyntax_box_get_bool", HirType::Bool),
7546+
_ => return boxed_value,
7547+
};
7548+
let unboxed = self.create_value(result_hir_ty, HirValueKind::Instruction);
7549+
self.add_instruction(
7550+
block_id,
7551+
HirInstruction::Call {
7552+
result: Some(unboxed),
7553+
callee: crate::hir::HirCallable::Symbol(get_symbol.to_string()),
7554+
args: vec![boxed_value],
7555+
type_args: vec![],
7556+
const_args: vec![],
7557+
is_tail: false,
7558+
},
7559+
);
7560+
self.add_use(boxed_value, unboxed);
7561+
unboxed
7562+
}
7563+
74847564
/// Convert frontend type to HIR type
74857565
fn convert_type(&self, ty: &Type) -> HirType {
74867566
use zyntax_typed_ast::PrimitiveType;

crates/compiler/src/zrtl.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1587,6 +1587,64 @@ pub unsafe extern "C" fn zyntax_box_get_i64(boxed: *const DynamicBoxRepr) -> i64
15871587
}
15881588
}
15891589

1590+
/// Get the value from a DynamicBox as i32. Reads through the box's
1591+
/// `data` pointer using the recorded `size` so source values stored
1592+
/// as i8/i16 widen losslessly. Returns 0 on null box.
1593+
#[no_mangle]
1594+
pub unsafe extern "C" fn zyntax_box_get_i32(boxed: *const DynamicBoxRepr) -> i32 {
1595+
if boxed.is_null() || (*boxed).data.is_null() {
1596+
return 0;
1597+
}
1598+
match (*boxed).size {
1599+
1 => *((*boxed).data as *const i8) as i32,
1600+
2 => *((*boxed).data as *const i16) as i32,
1601+
4 => *((*boxed).data as *const i32),
1602+
_ => 0,
1603+
}
1604+
}
1605+
1606+
/// Get the value from a DynamicBox as f32. Source f64 narrows.
1607+
#[no_mangle]
1608+
pub unsafe extern "C" fn zyntax_box_get_f32(boxed: *const DynamicBoxRepr) -> f32 {
1609+
if boxed.is_null() || (*boxed).data.is_null() {
1610+
return 0.0;
1611+
}
1612+
match (*boxed).size {
1613+
4 => *((*boxed).data as *const f32),
1614+
8 => *((*boxed).data as *const f64) as f32,
1615+
_ => 0.0,
1616+
}
1617+
}
1618+
1619+
/// Get the value from a DynamicBox as f64. Source f32 widens.
1620+
#[no_mangle]
1621+
pub unsafe extern "C" fn zyntax_box_get_f64(boxed: *const DynamicBoxRepr) -> f64 {
1622+
if boxed.is_null() || (*boxed).data.is_null() {
1623+
return 0.0;
1624+
}
1625+
match (*boxed).size {
1626+
4 => *((*boxed).data as *const f32) as f64,
1627+
8 => *((*boxed).data as *const f64),
1628+
_ => 0.0,
1629+
}
1630+
}
1631+
1632+
/// Get the value from a DynamicBox as bool (returned as i32 for FFI).
1633+
#[no_mangle]
1634+
pub unsafe extern "C" fn zyntax_box_get_bool(boxed: *const DynamicBoxRepr) -> i32 {
1635+
if boxed.is_null() || (*boxed).data.is_null() {
1636+
return 0;
1637+
}
1638+
if (*boxed).size == 0 {
1639+
return 0;
1640+
}
1641+
if *((*boxed).data as *const u8) != 0 {
1642+
1
1643+
} else {
1644+
0
1645+
}
1646+
}
1647+
15901648
/// Get the TypeTag from a DynamicBox
15911649
#[no_mangle]
15921650
pub unsafe extern "C" fn zyntax_box_get_tag(boxed: *const DynamicBoxRepr) -> u32 {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Type::Any field roundtrip — autobox-on-store + unbox-on-read.
2+
//
3+
// Stores an f64 into an `Any` field via `zyntax_box_f64`, then reads
4+
// it back with use-site type `f64` (the let binding annotation) so
5+
// the lowering inserts `zyntax_box_get_f64`. A clean round-trip
6+
// gets you the stored value; a broken pipeline either crashes at
7+
// codegen (the i32 case used to LLVM-panic on bitcast i32→i64) or
8+
// silently returns the box pointer cast to i64.
9+
//
10+
// The loop body re-creates the box and unboxes 1M times — a noisy
11+
// per-iteration heap allocation, so this benchmarks malloc churn
12+
// more than arithmetic. The point isn't the number, it's the
13+
// correctness gate.
14+
15+
struct Bag {
16+
payload: Any
17+
}
18+
19+
def main(): i64 {
20+
let mut sum: f64 = 0.0
21+
let mut i: i64 = 0
22+
while i < 1000000 {
23+
let bag = Bag { payload: 1.5 }
24+
let v: f64 = bag.payload
25+
sum = sum + v
26+
i = i + 1
27+
}
28+
return sum as i64
29+
}

crates/zynml/examples/bench_runner.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ const KERNELS: &[(&str, &str)] = &[
196196
// → sum 210000000.
197197
("bench_op_overload", "Int(210000000)"),
198198
("bench_op_overload_ref", "Int(21000000)"),
199+
("bench_any_field", "Int(1500000)"),
199200
];
200201

201202
/// Each target produces one [`TargetResult`] per kernel.

0 commit comments

Comments
 (0)