Skip to content

Commit 4022a61

Browse files
committed
fix: register zyntax_box_* runtime symbols + correct LLVM Call::Symbol return type
When the SSA-lowering autobox / autounbox path for `Type::Any` fields emits `Call::Symbol("zyntax_box_X")` or `..._get_X`, two things had to be in place that weren't: 1. The runtime had to know those symbols. `ZyntaxRuntime::new` already registers `__zyntax_effect_*` up front; mirror that for the box helpers in a new `register_box_runtime_symbols` in `effect_runtime.rs`. Each symbol goes to (a) the Cranelift backend's runtime-symbol table via `register_function_typed`, (b) the plugin-signatures table (so the LLVMBackend's per-symbol boxing decisions see the right param types), and (c) the BC interp's FFI table via `register_zrtl_symbols`. Without (c) the interp tier panicked with "unknown function 'zyntax_box_X'" the moment a hot path resolved through it. Plus added the missing `zyntax_box_get_{i32,f32,f64,bool}` runtime helpers in `zrtl.rs` alongside the existing `_i64`. 2. The LLVMBackend's `Call::Symbol` path defaulted to `void(args) → i32 0` whenever the symbol's signature wasn't in `symbol_signatures` — and the interp_runtime LLVM install path only forwarded the raw symbol pointers, not the signatures. Two changes: - `install_jit_with` now calls `be.register_symbol_signatures(&box_infos)` so the LLVM JIT sees the box helpers' real types. - `LLVMBackend::compile_call`'s Symbol arm now respects `sig_info.return_type` when present — picks an LLVM scalar type matching the `TypeTag` category/size (mirrors `type_tag_to_cranelift_type`). Without this, unboxing an f64 still surfaced an "i32 0" placeholder that crashed downstream FAdd / FSub. `bench_any_field` round-trips an f64 through an `Any` field 1M times under the LLVM tier and returns the correct `Int(1500000)`. All other kernels unchanged. Followup: explicit `X as Any` / `Any as T` cast plumbing is a separate piece — wired the helpers but the typed-AST surfacing of `cast.expr.ty` made it double-convert in the current shape. The field-load autobox/autounbox already covers the `let v: T = b.payload` ergonomic the user actually hits.
1 parent efe009e commit 4022a61

6 files changed

Lines changed: 606 additions & 24 deletions

File tree

crates/compiler/src/llvm_backend.rs

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3235,19 +3235,71 @@ impl<'ctx> LLVMBackend<'ctx> {
32353235
})
32363236
.collect();
32373237

3238-
// Declare the function (assume void return for now)
3239-
let fn_type = self.context.void_type().fn_type(&param_types, false);
3238+
// Pick the return type from the registered signature
3239+
// when present. Without this the function declaration
3240+
// defaulted to `void(args)` and the caller — which had
3241+
// already typed the call result based on the SSA value
3242+
// type — would consume an "i32 0" dummy and panic the
3243+
// first time it tried to use the result as an f64 /
3244+
// f32 / bool. Mirrors `type_tag_to_cranelift_type` in
3245+
// the Cranelift backend.
3246+
let returns_void = sig_info
3247+
.as_ref()
3248+
.map(|s| matches!(s.return_type.category(), crate::zrtl::TypeCategory::Void))
3249+
.unwrap_or(true);
3250+
let call_name = if returns_void { "" } else { symbol_name };
3251+
let fn_type = if let Some(ref sig) = sig_info {
3252+
use crate::zrtl::{PrimitiveSize, TypeCategory};
3253+
let bits = sig.return_type.type_id();
3254+
match sig.return_type.category() {
3255+
TypeCategory::Void => self.context.void_type().fn_type(&param_types, false),
3256+
TypeCategory::Bool => self.context.bool_type().fn_type(&param_types, false),
3257+
TypeCategory::Int | TypeCategory::UInt => {
3258+
if bits == PrimitiveSize::Bits8 as u16 {
3259+
self.context.i8_type().fn_type(&param_types, false)
3260+
} else if bits == PrimitiveSize::Bits16 as u16 {
3261+
self.context.i16_type().fn_type(&param_types, false)
3262+
} else if bits == PrimitiveSize::Bits32 as u16 {
3263+
self.context.i32_type().fn_type(&param_types, false)
3264+
} else {
3265+
self.context.i64_type().fn_type(&param_types, false)
3266+
}
3267+
}
3268+
TypeCategory::Float => {
3269+
if bits == PrimitiveSize::Bits32 as u16 {
3270+
self.context.f32_type().fn_type(&param_types, false)
3271+
} else {
3272+
self.context.f64_type().fn_type(&param_types, false)
3273+
}
3274+
}
3275+
// Pointers / opaques / closures: ptr return.
3276+
_ => self
3277+
.context
3278+
.ptr_type(AddressSpace::default())
3279+
.fn_type(&param_types, false),
3280+
}
3281+
} else {
3282+
self.context.void_type().fn_type(&param_types, false)
3283+
};
32403284
let func = self
32413285
.module
32423286
.get_function(symbol_name)
32433287
.unwrap_or_else(|| self.module.add_function(symbol_name, fn_type, None));
32443288

32453289
// Build call
3246-
self.builder
3247-
.build_call(func, &final_arg_values, symbol_name)?;
3248-
3249-
// Return a dummy value (void functions don't return anything meaningful)
3250-
Ok(self.context.i32_type().const_zero().into())
3290+
let call_site = self
3291+
.builder
3292+
.build_call(func, &final_arg_values, call_name)?;
3293+
if returns_void {
3294+
Ok(self.context.i32_type().const_zero().into())
3295+
} else {
3296+
match call_site.try_as_basic_value() {
3297+
ValueKind::Basic(val) => Ok(val),
3298+
ValueKind::Instruction(_) => {
3299+
Ok(self.context.i32_type().const_zero().into())
3300+
}
3301+
}
3302+
}
32513303
}
32523304
HirCallable::FuncRef(_) => Err(CompilerError::CodeGen(
32533305
"HirCallable::FuncRef is not callable directly — \

crates/compiler/src/ssa.rs

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4470,7 +4470,7 @@ impl SsaBuilder {
44704470
if let Some(field_hir_ty) = field_ty {
44714471
let object_hir_ty =
44724472
self.function.values.get(&object_val).map(|v| v.ty.clone());
4473-
if object_hir_ty.as_ref() != Some(&field_hir_ty) {
4473+
let raw = if object_hir_ty.as_ref() != Some(&field_hir_ty) {
44744474
// Rebind with the field's HirType via Bitcast.
44754475
let rebind =
44764476
self.create_value(field_hir_ty.clone(), HirValueKind::Instruction);
@@ -4484,10 +4484,23 @@ impl SsaBuilder {
44844484
},
44854485
);
44864486
self.add_use(object_val, rebind);
4487-
return Ok(rebind);
4488-
}
4489-
// Types already match — no rebind needed.
4490-
return Ok(object_val);
4487+
rebind
4488+
} else {
4489+
object_val
4490+
};
4491+
// Even on the single-field shortcut, the field
4492+
// may be declared `Type::Any` (box-pointer
4493+
// slot) — apply the same autounbox the
4494+
// ExtractValue path does so
4495+
// `let v: f64 = b.payload` round-trips when
4496+
// `b` is a single-field struct.
4497+
return Ok(self.maybe_unbox_for_any_field(
4498+
block_id,
4499+
raw,
4500+
&object_type,
4501+
field_index as usize,
4502+
&expr.ty,
4503+
));
44914504
}
44924505
// Fall through to ExtractValue when we couldn't resolve
44934506
// the field type.
@@ -7505,10 +7518,6 @@ impl SsaBuilder {
75057518
/// `zyntax_box_get_X` call to unwrap the box pointer. Leaves
75067519
/// boxes intact when the use-site is also `Type::Any` (the box
75077520
/// 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`.
75127521
fn maybe_unbox_for_any_field(
75137522
&mut self,
75147523
block_id: HirId,
@@ -7517,7 +7526,6 @@ impl SsaBuilder {
75177526
field_index: usize,
75187527
use_site_ty: &Type,
75197528
) -> HirId {
7520-
// Field-side gate: only act on fields declared `Type::Any`.
75217529
let field_is_any = self
75227530
.get_field_typed_types(object_ty)
75237531
.and_then(|fs| fs.get(field_index).cloned())
@@ -7526,11 +7534,24 @@ impl SsaBuilder {
75267534
if !field_is_any {
75277535
return boxed_value;
75287536
}
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.
7537+
self.try_emit_any_downcast(block_id, boxed_value, use_site_ty)
7538+
.unwrap_or(boxed_value)
7539+
}
7540+
7541+
/// Emit a `zyntax_box_get_X` call to downcast a `*mut DynamicBox`
7542+
/// (HIR i64) to the requested concrete primitive type. Returns
7543+
/// `Some(unboxed)` when the target type has a matching getter,
7544+
/// `None` otherwise (caller falls through to whatever default
7545+
/// it had). Shared between the Field-load unbox path and the
7546+
/// explicit-cast (`X as T`) unbox path.
7547+
fn try_emit_any_downcast(
7548+
&mut self,
7549+
block_id: HirId,
7550+
boxed_value: HirId,
7551+
target_ty: &Type,
7552+
) -> Option<HirId> {
75327553
use zyntax_typed_ast::PrimitiveType;
7533-
let (get_symbol, result_hir_ty) = match use_site_ty {
7554+
let (symbol, result_hir_ty) = match target_ty {
75347555
Type::Primitive(PrimitiveType::I8)
75357556
| Type::Primitive(PrimitiveType::I16)
75367557
| Type::Primitive(PrimitiveType::I32)
@@ -7543,22 +7564,22 @@ impl SsaBuilder {
75437564
Type::Primitive(PrimitiveType::F32) => ("zyntax_box_get_f32", HirType::F32),
75447565
Type::Primitive(PrimitiveType::F64) => ("zyntax_box_get_f64", HirType::F64),
75457566
Type::Primitive(PrimitiveType::Bool) => ("zyntax_box_get_bool", HirType::Bool),
7546-
_ => return boxed_value,
7567+
_ => return None,
75477568
};
75487569
let unboxed = self.create_value(result_hir_ty, HirValueKind::Instruction);
75497570
self.add_instruction(
75507571
block_id,
75517572
HirInstruction::Call {
75527573
result: Some(unboxed),
7553-
callee: crate::hir::HirCallable::Symbol(get_symbol.to_string()),
7574+
callee: crate::hir::HirCallable::Symbol(symbol.to_string()),
75547575
args: vec![boxed_value],
75557576
type_args: vec![],
75567577
const_args: vec![],
75577578
is_tail: false,
75587579
},
75597580
);
75607581
self.add_use(boxed_value, unboxed);
7561-
unboxed
7582+
Some(unboxed)
75627583
}
75637584

75647585
/// Convert frontend type to HIR type

crates/compiler/src/zrtl.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1760,6 +1760,32 @@ pub fn type_tag_for_hir_type(ty: &crate::hir::HirType) -> TypeTag {
17601760
}
17611761
}
17621762

1763+
/// Return the list of `zyntax_box_*` runtime helpers as `(name, fn
1764+
/// pointer, arity)` triples. The runtime (BC interp + Cranelift JIT
1765+
/// + LLVM AOT) registers these by default so any module that
1766+
/// reaches the autobox / autounbox code paths in SSA lowering finds
1767+
/// the symbol resolved.
1768+
///
1769+
/// Mirrors the per-symbol `register_function_typed` shape that the
1770+
/// effect-runtime registration uses, but flat — the box helpers are
1771+
/// pure FFI with no effect bookkeeping needed.
1772+
pub fn box_runtime_symbols() -> Vec<(&'static str, *const u8, u8)> {
1773+
vec![
1774+
("zyntax_box_i32", zyntax_box_i32 as *const u8, 1),
1775+
("zyntax_box_i64", zyntax_box_i64 as *const u8, 1),
1776+
("zyntax_box_f32", zyntax_box_f32 as *const u8, 1),
1777+
("zyntax_box_f64", zyntax_box_f64 as *const u8, 1),
1778+
("zyntax_box_bool", zyntax_box_bool as *const u8, 1),
1779+
("zyntax_box_free", zyntax_box_free as *const u8, 1),
1780+
("zyntax_box_get_i32", zyntax_box_get_i32 as *const u8, 1),
1781+
("zyntax_box_get_i64", zyntax_box_get_i64 as *const u8, 1),
1782+
("zyntax_box_get_f32", zyntax_box_get_f32 as *const u8, 1),
1783+
("zyntax_box_get_f64", zyntax_box_get_f64 as *const u8, 1),
1784+
("zyntax_box_get_bool", zyntax_box_get_bool as *const u8, 1),
1785+
("zyntax_box_get_tag", zyntax_box_get_tag as *const u8, 1),
1786+
]
1787+
}
1788+
17631789
#[cfg(test)]
17641790
mod tests {
17651791
use super::*;

0 commit comments

Comments
 (0)