Skip to content

Commit 8b0a6e2

Browse files
committed
runtime: stabilize dyn boxing tags and opaque println formatting
1 parent 356cf2b commit 8b0a6e2

4 files changed

Lines changed: 215 additions & 138 deletions

File tree

crates/compiler/src/cranelift_backend.rs

Lines changed: 143 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,106 @@ fn type_tag_to_cranelift_type(tag: &crate::zrtl::TypeTag) -> types::Type {
7878
}
7979
}
8080

81+
/// Deterministically derive a non-zero opaque sub-id from a type name.
82+
///
83+
/// We reserve `0` for unknown/legacy opaque values and keep `0xFFFF` for
84+
/// DynamicBox signature markers, so generated IDs stay in a safe range.
85+
fn stable_opaque_type_sub_id(type_name: &str) -> u16 {
86+
let clean_name = type_name.trim_start_matches('$');
87+
if clean_name.is_empty() {
88+
return 1;
89+
}
90+
91+
// FNV-1a (32-bit), deterministic across processes/platforms.
92+
let mut hash: u32 = 0x811C_9DC5;
93+
for byte in clean_name.as_bytes() {
94+
hash ^= *byte as u32;
95+
hash = hash.wrapping_mul(0x0100_0193);
96+
}
97+
98+
let mut id = (hash as u16) & 0x7FFF;
99+
if id == 0 {
100+
id = 1;
101+
}
102+
id
103+
}
104+
105+
fn dynamic_box_opaque_tag(opaque_name: &str) -> u32 {
106+
crate::zrtl::TypeTag::new(
107+
crate::zrtl::TypeCategory::Opaque,
108+
stable_opaque_type_sub_id(opaque_name),
109+
crate::zrtl::TypeFlags::NONE,
110+
)
111+
.0
112+
}
113+
114+
fn dynamic_box_tag_and_size_for_hir_type(ty: &HirType) -> (u32, u32) {
115+
match ty {
116+
HirType::I8 => (crate::zrtl::TypeTag::I8.0, 1),
117+
HirType::I16 => (crate::zrtl::TypeTag::I16.0, 2),
118+
HirType::I32 => (crate::zrtl::TypeTag::I32.0, 4),
119+
HirType::I64 => (crate::zrtl::TypeTag::I64.0, 8),
120+
HirType::U8 => (crate::zrtl::TypeTag::U8.0, 1),
121+
HirType::U16 => (crate::zrtl::TypeTag::U16.0, 2),
122+
HirType::U32 => (crate::zrtl::TypeTag::U32.0, 4),
123+
HirType::U64 => (crate::zrtl::TypeTag::U64.0, 8),
124+
HirType::F32 => (crate::zrtl::TypeTag::F32.0, 4),
125+
HirType::F64 => (crate::zrtl::TypeTag::F64.0, 8),
126+
HirType::Bool => (crate::zrtl::TypeTag::BOOL.0, 1),
127+
HirType::Ptr(inner) if matches!(inner.as_ref(), HirType::I8) => {
128+
(crate::zrtl::TypeTag::STRING.0, 8)
129+
}
130+
HirType::Opaque(type_name) => {
131+
let type_name_str = type_name.resolve_global().unwrap_or_default();
132+
(dynamic_box_opaque_tag(&type_name_str), 8)
133+
}
134+
HirType::Ptr(inner) if matches!(inner.as_ref(), HirType::Opaque(_)) => {
135+
if let HirType::Opaque(type_name) = inner.as_ref() {
136+
let type_name_str = type_name.resolve_global().unwrap_or_default();
137+
(dynamic_box_opaque_tag(&type_name_str), 8)
138+
} else {
139+
unreachable!("checked by match guard");
140+
}
141+
}
142+
HirType::Ptr(_) => (
143+
crate::zrtl::TypeTag::new(
144+
crate::zrtl::TypeCategory::Pointer,
145+
crate::zrtl::PrimitiveSize::Pointer as u16,
146+
crate::zrtl::TypeFlags::NONE,
147+
)
148+
.0,
149+
8,
150+
),
151+
other => {
152+
log::warn!(
153+
"[Boxing] Unhandled type: {:?}, defaulting to opaque tag",
154+
other
155+
);
156+
default_dynamic_box_opaque_tag_and_size()
157+
}
158+
}
159+
}
160+
161+
fn default_dynamic_box_opaque_tag_and_size() -> (u32, u32) {
162+
(
163+
crate::zrtl::TypeTag::new(
164+
crate::zrtl::TypeCategory::Opaque,
165+
1,
166+
crate::zrtl::TypeFlags::NONE,
167+
)
168+
.0,
169+
8,
170+
)
171+
}
172+
173+
fn dynamic_box_uses_direct_pointer(ty: &HirType) -> bool {
174+
match ty {
175+
HirType::Opaque(_) => true,
176+
HirType::Ptr(inner) => matches!(inner.as_ref(), HirType::Opaque(_) | HirType::I8),
177+
_ => false,
178+
}
179+
}
180+
81181
/// Cranelift backend for JIT compilation
82182
pub struct CraneliftBackend {
83183
/// JIT module for code generation
@@ -1402,65 +1502,27 @@ impl CraneliftBackend {
14021502
if needs_boxing {
14031503
// Apply DynamicBox wrapping - same logic as HirCallable::Symbol
14041504
let arg_hir_id = args[param_index];
1405-
// Check if this is a pointer type (opaque or string) - pass value directly
1406-
let is_pointer_type = if let Some(hir_value) =
1407-
function.values.get(&arg_hir_id)
1408-
{
1409-
match &hir_value.ty {
1410-
HirType::Opaque(_) => true,
1411-
HirType::Ptr(inner) => matches!(
1412-
inner.as_ref(),
1413-
HirType::Opaque(_) | HirType::I8
1414-
),
1415-
_ => false,
1416-
}
1417-
} else {
1418-
false
1419-
};
1420-
1421-
let (tag_value, size_value) =
1422-
if let Some(hir_value) =
1423-
function.values.get(&arg_hir_id)
1424-
{
1425-
// TypeTag format: (type_id << 8) | category
1426-
// PrimitiveSize: Bits8=1, Bits16=2, Bits32=3, Bits64=4
1427-
match &hir_value.ty {
1428-
HirType::I8 => (0x0102u32, 1u32), // Int(Bits8)
1429-
HirType::I16 => (0x0202u32, 2u32), // Int(Bits16)
1430-
HirType::I32 => (0x0302u32, 4u32), // Int(Bits32)
1431-
HirType::I64 => (0x0402u32, 8u32), // Int(Bits64)
1432-
HirType::U8 => (0x0103u32, 1u32), // UInt(Bits8)
1433-
HirType::U16 => (0x0203u32, 2u32), // UInt(Bits16)
1434-
HirType::U32 => (0x0303u32, 4u32), // UInt(Bits32)
1435-
HirType::U64 => (0x0403u32, 8u32), // UInt(Bits64)
1436-
HirType::F32 => (0x0304u32, 4u32), // Float(Bits32)
1437-
HirType::F64 => (0x0404u32, 8u32), // Float(Bits64)
1438-
HirType::Bool => (0x0001u32, 1u32), // Bool
1439-
HirType::Ptr(inner)
1440-
if matches!(
1441-
inner.as_ref(),
1442-
HirType::I8
1443-
) =>
1444-
{
1445-
(0x0005u32, 8u32)
1446-
} // String
1447-
HirType::Opaque(_) => (0x0012u32, 8u32), // Opaque
1448-
HirType::Ptr(inner)
1449-
if matches!(
1450-
inner.as_ref(),
1451-
HirType::Opaque(_)
1452-
) =>
1453-
{
1454-
(0x0012u32, 8u32)
1455-
}
1456-
other => {
1457-
(0x0012u32, 8u32)
1458-
// Opaque
1459-
}
1460-
}
1461-
} else {
1462-
(0x0012u32, 8u32) // Opaque
1463-
};
1505+
let is_pointer_type = function
1506+
.values
1507+
.get(&arg_hir_id)
1508+
.map(|hir_value| {
1509+
dynamic_box_uses_direct_pointer(
1510+
&hir_value.ty,
1511+
)
1512+
})
1513+
.unwrap_or(false);
1514+
1515+
let (tag_value, size_value) = function
1516+
.values
1517+
.get(&arg_hir_id)
1518+
.map(|hir_value| {
1519+
dynamic_box_tag_and_size_for_hir_type(
1520+
&hir_value.ty,
1521+
)
1522+
})
1523+
.unwrap_or_else(
1524+
default_dynamic_box_opaque_tag_and_size,
1525+
);
14641526

14651527
let data_ptr_value = if is_pointer_type {
14661528
// Pointer types (opaque, string): value IS the pointer
@@ -1776,65 +1838,30 @@ impl CraneliftBackend {
17761838
// Allocate stack space for DynamicBox (32 bytes on 64-bit)
17771839
// Determine TypeTag and size based on HIR type
17781840
let arg_hir_id = args[param_index];
1779-
let (tag_value, size_value) = if let Some(hir_value) =
1780-
function.values.get(&arg_hir_id)
1781-
{
1782-
// TypeTag format: (type_id << 8) | category
1783-
// PrimitiveSize: Bits8=1, Bits16=2, Bits32=3, Bits64=4
1784-
match &hir_value.ty {
1785-
HirType::I8 => (0x0102u32, 1u32), // Int(Bits8)
1786-
HirType::I16 => (0x0202u32, 2u32), // Int(Bits16)
1787-
HirType::I32 => (0x0302u32, 4u32), // Int(Bits32)
1788-
HirType::I64 => (0x0402u32, 8u32), // Int(Bits64)
1789-
HirType::U8 => (0x0103u32, 1u32), // UInt(Bits8)
1790-
HirType::U16 => (0x0203u32, 2u32), // UInt(Bits16)
1791-
HirType::U32 => (0x0303u32, 4u32), // UInt(Bits32)
1792-
HirType::U64 => (0x0403u32, 8u32), // UInt(Bits64)
1793-
HirType::F32 => (0x0304u32, 4u32), // Float(Bits32)
1794-
HirType::F64 => (0x0404u32, 8u32), // Float(Bits64)
1795-
HirType::Bool => (0x0001u32, 1u32), // Bool
1796-
HirType::Ptr(inner)
1797-
if matches!(
1798-
inner.as_ref(),
1799-
HirType::I8
1800-
) =>
1801-
{
1802-
(0x0005u32, 8u32) // String
1803-
}
1804-
HirType::Opaque(_) => (0x0012u32, 8u32), // Opaque
1805-
HirType::Ptr(inner)
1806-
if matches!(
1807-
inner.as_ref(),
1808-
HirType::Opaque(_)
1809-
) =>
1810-
{
1811-
(0x0012u32, 8u32) // Ptr to Opaque
1812-
}
1813-
other => {
1814-
log::warn!("[Boxing] Unhandled type: {:?}, defaulting to Opaque", other);
1815-
(0x0012u32, 8u32) // Opaque
1816-
}
1817-
}
1818-
} else {
1819-
log::warn!("[Boxing] No HirValue found for arg_hir_id {:?}", arg_hir_id);
1820-
(0x0012u32, 8u32) // Opaque
1821-
};
1841+
let (tag_value, size_value) = function
1842+
.values
1843+
.get(&arg_hir_id)
1844+
.map(|hir_value| {
1845+
dynamic_box_tag_and_size_for_hir_type(
1846+
&hir_value.ty,
1847+
)
1848+
})
1849+
.unwrap_or_else(|| {
1850+
log::warn!(
1851+
"[Boxing] No HirValue found for arg_hir_id {:?}",
1852+
arg_hir_id
1853+
);
1854+
default_dynamic_box_opaque_tag_and_size()
1855+
});
18221856

18231857
// Check if this is a pointer type (opaque types or strings that should be passed directly)
1824-
let is_pointer_type = if let Some(hir_value) =
1825-
function.values.get(&args[param_index])
1826-
{
1827-
match &hir_value.ty {
1828-
HirType::Opaque(_) => true,
1829-
HirType::Ptr(inner) => matches!(
1830-
inner.as_ref(),
1831-
HirType::Opaque(_) | HirType::I8
1832-
),
1833-
_ => false,
1834-
}
1835-
} else {
1836-
false
1837-
};
1858+
let is_pointer_type = function
1859+
.values
1860+
.get(&args[param_index])
1861+
.map(|hir_value| {
1862+
dynamic_box_uses_direct_pointer(&hir_value.ty)
1863+
})
1864+
.unwrap_or(false);
18381865

18391866
// For pointer types, the value IS already a pointer - store directly
18401867
// For primitives, allocate stack space and store a pointer to the value

crates/compiler/src/zrtl.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1627,6 +1627,25 @@ extern "C" fn drop_box_u8(ptr: *mut u8) {
16271627
///
16281628
/// This is called at compile time (during lowering) to get the
16291629
/// ZRTL TypeTag for a given type.
1630+
fn stable_opaque_type_sub_id(type_name: &str) -> u16 {
1631+
let clean_name = type_name.trim_start_matches('$');
1632+
if clean_name.is_empty() {
1633+
return 1;
1634+
}
1635+
1636+
let mut hash: u32 = 0x811C_9DC5;
1637+
for byte in clean_name.as_bytes() {
1638+
hash ^= *byte as u32;
1639+
hash = hash.wrapping_mul(0x0100_0193);
1640+
}
1641+
1642+
let mut id = (hash as u16) & 0x7FFF;
1643+
if id == 0 {
1644+
id = 1;
1645+
}
1646+
id
1647+
}
1648+
16301649
pub fn type_tag_for_hir_type(ty: &crate::hir::HirType) -> TypeTag {
16311650
use crate::hir::HirType;
16321651

@@ -1653,7 +1672,14 @@ pub fn type_tag_for_hir_type(ty: &crate::hir::HirType) -> TypeTag {
16531672
HirType::Union(_) => TypeTag::new(TypeCategory::Union, 0, TypeFlags::NONE),
16541673
HirType::Function(_) => TypeTag::new(TypeCategory::Function, 0, TypeFlags::NONE),
16551674
HirType::Closure(_) => TypeTag::new(TypeCategory::Function, 1, TypeFlags::NONE), // Closure
1656-
HirType::Opaque(_) => TypeTag::new(TypeCategory::Opaque, 0, TypeFlags::NONE),
1675+
HirType::Opaque(type_name) => {
1676+
let type_name_str = type_name.resolve_global().unwrap_or_default();
1677+
TypeTag::new(
1678+
TypeCategory::Opaque,
1679+
stable_opaque_type_sub_id(&type_name_str),
1680+
TypeFlags::NONE,
1681+
)
1682+
}
16571683
HirType::ConstGeneric(_) => TypeTag::new(TypeCategory::Opaque, 1, TypeFlags::NONE),
16581684
HirType::Generic { .. } => TypeTag::new(TypeCategory::Custom, 0, TypeFlags::NONE),
16591685
HirType::TraitObject { .. } => TypeTag::new(TypeCategory::TraitObject, 0, TypeFlags::NONE),

crates/zynml/tests/e2e_tests.rs

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3590,13 +3590,9 @@ mod execution {
35903590
}
35913591
}
35923592

3593-
// Ignored: this test crashes the test process with SIGABRT (stack overflow in Grammar2
3594-
// interpreter parsing the complex prelude.zynml) or SIGSEGV (tensor runtime bug in
3595-
// println_any). Both are pre-existing issues that kill the test runner via signal.
3596-
// Run manually with a larger stack:
3597-
// RUST_MIN_STACK=134217728 cargo test -p zynml --test e2e_tests -- --include-ignored test_execute_hello
3593+
// Regression coverage for the full hello example (prelude + tensor + dynamic println).
3594+
// Keep panic isolation via catch_unwind so runtime regressions produce actionable test output.
35983595
#[test]
3599-
#[ignore]
36003596
fn test_execute_hello_example() {
36013597
let Some(mut zynml) = create_runtime_with_plugins() else {
36023598
println!("Skipping: plugins not available");

0 commit comments

Comments
 (0)