Skip to content

Commit 1f438e8

Browse files
committed
fix(ssa): autobox values stored into Type::Any struct fields
`Type::Any` lowers to `HirType::I64` because the slot is meant to hold a `*mut DynamicBoxRepr` (the runtime tagged-box used for type-erased storage and FFI). The runtime already exposes `zyntax_box_i32`, `_i64`, `_f32`, `_f64`, `_bool`; the LLVM and Cranelift backends already emit calls to them when an extern parameter is declared `DynamicBox`. The struct literal lowering just wasn't using them — it tried to raw-store the value into the i64 slot, which crashed the LLVM backend at `Cannot select: i64 = bitcast Constant:i32<42>` whenever the value's HIR type wasn't already i64-shaped. Now: in `TypedExpression::Struct` lowering (both value-type and `@reference` paths), every field marked `Type::Any` in the declaring class's `TypeDefinition` gets its value routed through `zyntax_box_X` before the `InsertValue` / `Store`. Values that are already i64-shaped (`I64`, `U64`, any `Ptr`) pass through unchanged — the slot just holds them as the raw pointer / 64-bit value. Unsupported value types fall through to the prior behaviour (which we'll formalise as the unbox-on-read path lands). Test: `struct Box { value: Any }` followed by `Box { value: 42 }`, `Box { value: 3.14 }`, `Box { value: <ptr> }` — all now compile and execute cleanly via the LLVM tier. Previously the i32 case crashed at codegen, the f64 case silently mis-stored the bit pattern, and the pointer case worked by accident. Existing kernels unaffected — none of them use `Any` fields, so the autobox helper is a no-op for them. 248 compiler tests + all embed tests pass; clippy clean. Followup: insert matching unbox calls on `Type::Any` field reads when the use-site has a known target type. Without that, reading `b.value` returns the raw box pointer (i64) and the user has to manually call `zyntax_box_get_X` — which works but isn't ergonomic.
1 parent d9e1180 commit 1f438e8

1 file changed

Lines changed: 83 additions & 2 deletions

File tree

crates/compiler/src/ssa.rs

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4912,8 +4912,15 @@ impl SsaBuilder {
49124912
// index, exactly the shape
49134913
// `aggregate_split` / `scalar_replace_alloc`
49144914
// expect.
4915+
let field_typed_types = self.get_field_typed_types(&expr.ty);
49154916
for (i, field) in struct_lit.fields.iter().enumerate() {
4916-
let field_val = self.translate_expression(block_id, &field.value)?;
4917+
let mut field_val =
4918+
self.translate_expression(block_id, &field.value)?;
4919+
if let Some(types) = field_typed_types.as_ref() {
4920+
if matches!(types.get(i), Some(Type::Any)) {
4921+
field_val = self.maybe_box_for_any_field(block_id, field_val);
4922+
}
4923+
}
49174924
let offset = offsets[i] as i64;
49184925
let field_ty = hir_struct.fields[i].clone();
49194926

@@ -4972,12 +4979,27 @@ impl SsaBuilder {
49724979
}
49734980
}
49744981

4982+
// For `Any` field autoboxing — when a field is declared
4983+
// `Type::Any`, the slot is `HirType::I64` (pointer-sized,
4984+
// intended to hold a `DynamicBox*`). If the value being
4985+
// stored isn't already i64-shaped, we have to wrap it
4986+
// through `zyntax_box_X(value)` so the slot ends up with
4987+
// a real box pointer rather than a raw bit-pattern
4988+
// (which currently crashes the LLVM backend at the
4989+
// `bitcast i32 to i64` step).
4990+
let field_typed_types = self.get_field_typed_types(&expr.ty);
4991+
49754992
// Start with an undefined struct value
49764993
let mut current_struct = self.create_value(struct_ty.clone(), HirValueKind::Undef);
49774994

49784995
// Insert each field value into the struct
49794996
for (i, field) in struct_lit.fields.iter().enumerate() {
4980-
let field_val = self.translate_expression(block_id, &field.value)?;
4997+
let mut field_val = self.translate_expression(block_id, &field.value)?;
4998+
if let Some(types) = field_typed_types.as_ref() {
4999+
if matches!(types.get(i), Some(Type::Any)) {
5000+
field_val = self.maybe_box_for_any_field(block_id, field_val);
5001+
}
5002+
}
49815003

49825004
log::trace!("[SSA STRUCT LIT] Inserting field {}", i);
49835005

@@ -7400,6 +7422,65 @@ impl SsaBuilder {
74007422
)))
74017423
}
74027424

7425+
/// For a struct-typed expression, return the per-field `Type` from
7426+
/// the [`type_registry`] in declaration order. Used by struct
7427+
/// literal lowering to detect `Type::Any` fields that need
7428+
/// autoboxing via `zyntax_box_X` before the field value is stored.
7429+
/// Returns `None` if the struct's type can't be resolved (the
7430+
/// field-store path then falls through to the standard raw-coerce
7431+
/// behaviour, which is what every non-Any field already does).
7432+
fn get_field_typed_types(&self, expr_ty: &Type) -> Option<Vec<Type>> {
7433+
let type_def = match expr_ty {
7434+
Type::Named { id, .. } => self.type_registry.get_type_by_id(*id)?,
7435+
Type::Unresolved(name) => self.type_registry.get_type_by_name(*name)?,
7436+
_ => return None,
7437+
};
7438+
Some(type_def.fields.iter().map(|f| f.ty.clone()).collect())
7439+
}
7440+
7441+
/// Wrap `value` in a heap-allocated `DynamicBox` via the
7442+
/// appropriate `zyntax_box_X` runtime symbol so it can be stored
7443+
/// into a `Type::Any` field slot. The slot is `HirType::I64`
7444+
/// (pointer-sized); the box call returns a `*mut DynamicBoxRepr`
7445+
/// which is i64-shaped on the platforms we support.
7446+
///
7447+
/// Values that are already i64-shaped (`I64`, `U64`, any `Ptr(_)`)
7448+
/// pass through unchanged — the slot will hold the raw value /
7449+
/// pointer directly. Box pointers, plain `i64`, and pointers all
7450+
/// alias the same shape and can be stored verbatim.
7451+
///
7452+
/// Unsupported value types fall through; downstream codegen may
7453+
/// reject them. The current bench surface only stores i32, i64,
7454+
/// f32, f64, bool, and pointers — all covered.
7455+
fn maybe_box_for_any_field(&mut self, block_id: HirId, value: HirId) -> HirId {
7456+
let value_ty = match self.function.values.get(&value).map(|v| v.ty.clone()) {
7457+
Some(t) => t,
7458+
None => return value,
7459+
};
7460+
let box_symbol = match value_ty {
7461+
HirType::I8 | HirType::U8 | HirType::Bool => "zyntax_box_bool",
7462+
HirType::I16 | HirType::U16 | HirType::I32 | HirType::U32 => "zyntax_box_i32",
7463+
HirType::F32 => "zyntax_box_f32",
7464+
HirType::F64 => "zyntax_box_f64",
7465+
HirType::I64 | HirType::U64 | HirType::Ptr(_) => return value,
7466+
_ => return value,
7467+
};
7468+
let boxed = self.create_value(HirType::I64, HirValueKind::Instruction);
7469+
self.add_instruction(
7470+
block_id,
7471+
HirInstruction::Call {
7472+
result: Some(boxed),
7473+
callee: crate::hir::HirCallable::Symbol(box_symbol.to_string()),
7474+
args: vec![value],
7475+
type_args: vec![],
7476+
const_args: vec![],
7477+
is_tail: false,
7478+
},
7479+
);
7480+
self.add_use(value, boxed);
7481+
boxed
7482+
}
7483+
74037484
/// Convert frontend type to HIR type
74047485
fn convert_type(&self, ty: &Type) -> HirType {
74057486
use zyntax_typed_ast::PrimitiveType;

0 commit comments

Comments
 (0)