Skip to content

Commit e13cf01

Browse files
committed
lowering: add implicit numeric casts and type-aware cast ops
1 parent 3dd5933 commit e13cf01

4 files changed

Lines changed: 217 additions & 10 deletions

File tree

crates/compiler/src/cranelift_backend.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5566,7 +5566,9 @@ impl CraneliftBackend {
55665566
result
55675567
}
55685568
crate::hir::CastOp::Trunc => builder.ins().ireduce(target_ty, val),
5569+
crate::hir::CastOp::FpToUi => builder.ins().fcvt_to_uint(target_ty, val),
55695570
crate::hir::CastOp::FpToSi => builder.ins().fcvt_to_sint(target_ty, val),
5571+
crate::hir::CastOp::UiToFp => builder.ins().fcvt_from_uint(target_ty, val),
55705572
crate::hir::CastOp::SiToFp => builder.ins().fcvt_from_sint(target_ty, val),
55715573
crate::hir::CastOp::FpExt => builder.ins().fpromote(target_ty, val),
55725574
crate::hir::CastOp::FpTrunc => builder.ins().fdemote(target_ty, val),

crates/compiler/src/lowering.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4276,8 +4276,81 @@ impl LoweringContext {
42764276
None
42774277
}
42784278

4279+
fn can_implicitly_convert_numeric(
4280+
from: zyntax_typed_ast::PrimitiveType,
4281+
to: zyntax_typed_ast::PrimitiveType,
4282+
) -> bool {
4283+
use zyntax_typed_ast::PrimitiveType::*;
4284+
4285+
if from == to {
4286+
return true;
4287+
}
4288+
4289+
match (from, to) {
4290+
// Widening signed integer conversions
4291+
(I8, I16 | I32 | I64 | I128) => true,
4292+
(I16, I32 | I64 | I128) => true,
4293+
(I32, I64 | I128) => true,
4294+
(I64, I128) => true,
4295+
4296+
// Widening unsigned integer conversions
4297+
(U8, U16 | U32 | U64 | U128) => true,
4298+
(U16, U32 | U64 | U128) => true,
4299+
(U32, U64 | U128) => true,
4300+
(U64, U128) => true,
4301+
4302+
// Float widening
4303+
(F32, F64) => true,
4304+
4305+
// Integer to float promotions
4306+
(I8 | I16 | I32, F32 | F64) => true,
4307+
(I64, F64) => true,
4308+
(U8 | U16 | U32, F32 | F64) => true,
4309+
(U64, F64) => true,
4310+
4311+
// Platform-sized integers (conservative defaults)
4312+
(ISize, I64 | I128 | F64) => true,
4313+
(USize, U64 | U128 | F64) => true,
4314+
4315+
_ => false,
4316+
}
4317+
}
4318+
4319+
fn try_implicit_numeric_conversion(
4320+
&self,
4321+
expr: zyntax_typed_ast::TypedNode<zyntax_typed_ast::TypedExpression>,
4322+
expected_type: &Type,
4323+
) -> Option<zyntax_typed_ast::TypedNode<zyntax_typed_ast::TypedExpression>> {
4324+
use zyntax_typed_ast::{typed_ast::typed_node, TypedCast, TypedExpression};
4325+
4326+
let source_prim = match expr.ty {
4327+
Type::Primitive(p) if p.is_numeric() => p,
4328+
_ => return None,
4329+
};
4330+
4331+
let target_prim = match expected_type {
4332+
Type::Primitive(p) if p.is_numeric() => *p,
4333+
_ => return None,
4334+
};
4335+
4336+
if !Self::can_implicitly_convert_numeric(source_prim, target_prim) {
4337+
return None;
4338+
}
4339+
4340+
let span = expr.span;
4341+
Some(typed_node(
4342+
TypedExpression::Cast(TypedCast {
4343+
expr: Box::new(expr),
4344+
target_type: expected_type.clone(),
4345+
}),
4346+
expected_type.clone(),
4347+
span,
4348+
))
4349+
}
4350+
42794351
/// Try to insert an implicit From conversion if types don't match
4280-
/// Returns a new expression wrapped in Type::from() if conversion is available
4352+
/// Returns a new expression wrapped in either an implicit numeric cast
4353+
/// or Type::from() call when available.
42814354
fn try_implicit_from_conversion(
42824355
&self,
42834356
expr: zyntax_typed_ast::TypedNode<zyntax_typed_ast::TypedExpression>,
@@ -4297,6 +4370,11 @@ impl LoweringContext {
42974370
return None;
42984371
}
42994372

4373+
// Prefer direct numeric casts for primitive numeric conversions.
4374+
if let Some(cast_expr) = self.try_implicit_numeric_conversion(expr.clone(), expected_type) {
4375+
return Some(cast_expr);
4376+
}
4377+
43004378
// Check if there's a From<expr.ty> impl for expected_type
43014379
if let Some(from_func) = self.find_from_impl(&expr.ty, expected_type) {
43024380
eprintln!(

crates/compiler/src/ssa.rs

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1531,6 +1531,84 @@ impl SsaBuilder {
15311531
self.translate_expression(block_id, &lowered_expr)
15321532
}
15331533

1534+
fn int_type_width_and_sign(ty: &HirType) -> Option<(u8, bool)> {
1535+
match ty {
1536+
HirType::I8 => Some((8, true)),
1537+
HirType::I16 => Some((16, true)),
1538+
HirType::I32 => Some((32, true)),
1539+
HirType::I64 => Some((64, true)),
1540+
HirType::I128 => Some((128, true)),
1541+
HirType::U8 => Some((8, false)),
1542+
HirType::U16 => Some((16, false)),
1543+
HirType::U32 => Some((32, false)),
1544+
HirType::U64 => Some((64, false)),
1545+
HirType::U128 => Some((128, false)),
1546+
_ => None,
1547+
}
1548+
}
1549+
1550+
fn float_type_width(ty: &HirType) -> Option<u8> {
1551+
match ty {
1552+
HirType::F32 => Some(32),
1553+
HirType::F64 => Some(64),
1554+
_ => None,
1555+
}
1556+
}
1557+
1558+
fn select_cast_op(&self, source_ty: &HirType, target_ty: &HirType) -> CastOp {
1559+
use CastOp::*;
1560+
1561+
if source_ty == target_ty {
1562+
return Bitcast;
1563+
}
1564+
1565+
if let (Some((src_bits, src_signed)), Some((dst_bits, _dst_signed))) = (
1566+
Self::int_type_width_and_sign(source_ty),
1567+
Self::int_type_width_and_sign(target_ty),
1568+
) {
1569+
if src_bits > dst_bits {
1570+
return Trunc;
1571+
}
1572+
if src_bits < dst_bits {
1573+
return if src_signed { SExt } else { ZExt };
1574+
}
1575+
return Bitcast;
1576+
}
1577+
1578+
if let (Some(src_bits), Some(dst_bits)) = (
1579+
Self::float_type_width(source_ty),
1580+
Self::float_type_width(target_ty),
1581+
) {
1582+
if src_bits < dst_bits {
1583+
return FpExt;
1584+
}
1585+
if src_bits > dst_bits {
1586+
return FpTrunc;
1587+
}
1588+
return Bitcast;
1589+
}
1590+
1591+
if let (Some((_src_bits, src_signed)), Some(_dst_float_bits)) = (
1592+
Self::int_type_width_and_sign(source_ty),
1593+
Self::float_type_width(target_ty),
1594+
) {
1595+
return if src_signed { SiToFp } else { UiToFp };
1596+
}
1597+
1598+
if let (Some(_src_float_bits), Some((_dst_bits, dst_signed))) = (
1599+
Self::float_type_width(source_ty),
1600+
Self::int_type_width_and_sign(target_ty),
1601+
) {
1602+
return if dst_signed { FpToSi } else { FpToUi };
1603+
}
1604+
1605+
match (source_ty, target_ty) {
1606+
(HirType::Ptr(_), HirType::I64 | HirType::U64) => PtrToInt,
1607+
(HirType::I64 | HirType::U64, HirType::Ptr(_)) => IntToPtr,
1608+
_ => Bitcast,
1609+
}
1610+
}
1611+
15341612
/// Translate expression to SSA value
15351613
fn translate_expression(
15361614
&mut self,
@@ -2535,17 +2613,10 @@ impl SsaBuilder {
25352613

25362614
TypedExpression::Cast(cast) => {
25372615
let operand_val = self.translate_expression(block_id, &cast.expr)?;
2616+
let source_ty = self.convert_type(&cast.expr.ty);
25382617
let target_ty = self.convert_type(&cast.target_type);
25392618
let result = self.create_value(target_ty.clone(), HirValueKind::Instruction);
2540-
2541-
// NOTE: Proper cast operation selection requires type information.
2542-
// Should determine: IntToInt, IntToFloat, FloatToInt, FloatToFloat, Bitcast, etc.
2543-
// Needs source type width, target type width, signedness information.
2544-
//
2545-
// WORKAROUND: Always uses Bitcast (works for pointer casts and same-size conversions)
2546-
// FUTURE (v2.0): Add type-aware cast selection logic
2547-
// Estimated effort: 4-5 hours (needs type width/signedness utilities)
2548-
let cast_op = CastOp::Bitcast;
2619+
let cast_op = self.select_cast_op(&source_ty, &target_ty);
25492620

25502621
self.add_instruction(
25512622
block_id,

crates/zynml/tests/e2e_tests.rs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3639,6 +3639,62 @@ mod execution {
36393639
);
36403640
}
36413641

3642+
#[test]
3643+
fn test_execute_implicit_numeric_casts_in_assignments() {
3644+
let Some(mut zynml) = create_runtime_with_plugins() else {
3645+
println!("Skipping: plugins not available");
3646+
return;
3647+
};
3648+
3649+
let source = r#"
3650+
fn numeric_assignment_casts() {
3651+
let a: i64 = 41
3652+
let b: f64 = a
3653+
let c: i16 = 7
3654+
let d: i64 = c
3655+
let e: f64 = c
3656+
}
3657+
"#;
3658+
3659+
let functions = zynml
3660+
.load_source(source)
3661+
.expect("numeric assignment casts should compile");
3662+
assert!(
3663+
functions.iter().any(|f| f == "numeric_assignment_casts"),
3664+
"numeric_assignment_casts should be exported"
3665+
);
3666+
}
3667+
3668+
#[test]
3669+
fn test_execute_implicit_numeric_casts_in_call_args() {
3670+
let Some(mut zynml) = create_runtime_with_plugins() else {
3671+
println!("Skipping: plugins not available");
3672+
return;
3673+
};
3674+
3675+
let source = r#"
3676+
fn takes_i64(x: i64) {
3677+
let _copy = x
3678+
}
3679+
3680+
fn callsite() {
3681+
takes_i64(5)
3682+
}
3683+
"#;
3684+
3685+
let functions = zynml
3686+
.load_source(source)
3687+
.expect("numeric call-arg casts should compile");
3688+
assert!(
3689+
functions.iter().any(|f| f == "takes_i64"),
3690+
"takes_i64 should be exported"
3691+
);
3692+
assert!(
3693+
functions.iter().any(|f| f == "callsite"),
3694+
"callsite should be exported"
3695+
);
3696+
}
3697+
36423698
// Regression coverage for the full hello example (prelude + tensor + dynamic println).
36433699
// Keep panic isolation via catch_unwind so runtime regressions produce actionable test output.
36443700
#[test]

0 commit comments

Comments
 (0)