Introduce numeric and record widening

This commit introduces implicit numeric widening from Int to Float and a
corresponding record promotion mechanism.

When pushing values into a series, if the series' element type is a
TypeVar,
and the pushed value is of a different numeric type (e.g., Int and
Float),
the TypeVar will be unified with the wider type (Float). This ensures
that
operations on series elements handle type differences gracefully.

The record promotion handles cases where two records are involved, and
their
fields have compatible types, either identical or through numeric
widening.
This allows for more flexible type inference in complex structures.
This commit is contained in:
2026-03-28 14:52:24 +01:00
parent 350b7b49b6
commit 6cab8f649b
2 changed files with 206 additions and 6 deletions
+134
View File
@@ -65,6 +65,140 @@ fn test_series_typevar_resolved_through_while_upvalue() {
}
}
// ── Numeric promotion (Int → Float widening) ────────────────────────────────
/// Push int first, then float: series element type should widen to Float.
/// The int value must be readable back as a float (no truncation).
#[test]
fn test_series_promote_int_then_float() {
let env = Environment::new();
let source = r#"
(do
(def s (series 5))
(push s 1)
(push s 2.5)
(+ (s 0) (s 1))
)
"#;
let result = env.run_script(source);
match result {
Ok(Value::Float(v)) if (v - 3.5).abs() < 1e-9 => {}
other => panic!("Expected Float(3.5), got {:?}", other),
}
}
/// Push float first, then int: same widening, other direction.
#[test]
fn test_series_promote_float_then_int() {
let env = Environment::new();
let source = r#"
(do
(def s (series 5))
(push s 2.5)
(push s 1)
(+ (s 0) (s 1))
)
"#;
let result = env.run_script(source);
match result {
Ok(Value::Float(v)) if (v - 3.5).abs() < 1e-9 => {}
other => panic!("Expected Float(3.5), got {:?}", other),
}
}
/// `if` branches with int/float: the join type must be Float, not Any.
/// A Float series is expected — the pushed value must be accessible as float.
#[test]
fn test_series_promote_if_int_float_branch() {
let env = Environment::new();
let source = r#"
(do
(def s (series 5))
(push s (if true 1 2.5))
(push s (if false 1 2.5))
(+ (s 0) (s 1))
)
"#;
let result = env.run_script(source);
match result {
Ok(Value::Float(v)) if (v - 3.5).abs() < 1e-9 => {}
other => panic!("Expected Float(3.5), got {:?}", other),
}
}
/// Int variable pushed into a float-inferred series must be promoted.
#[test]
fn test_series_promote_int_variable_into_float_series() {
let env = Environment::new();
let source = r#"
(do
(def s (series 5))
(def n 3)
(push s 1.5)
(push s n)
(+ (s 0) (s 1))
)
"#;
let result = env.run_script(source);
match result {
Ok(Value::Float(v)) if (v - 4.5).abs() < 1e-9 => {}
other => panic!("Expected Float(4.5), got {:?}", other),
}
}
/// Record field promotion: pushing records where one field is int, another float.
/// The field type must widen to Float.
#[test]
fn test_series_promote_record_field_int_float() {
let env = Environment::new();
let source = r#"
(do
(def s (series 5))
(push s {:v 1})
(push s {:v 2.5})
(def v (.v s))
(+ (v 0) (v 1))
)
"#;
let result = env.run_script(source);
match result {
Ok(Value::Float(v)) if (v - 3.5).abs() < 1e-9 => {}
other => panic!("Expected Float(3.5), got {:?}", other),
}
}
/// Sanity check: Bool → Int is NOT a valid promotion (different semantic type).
#[test]
fn test_series_no_promotion_bool_int() {
let env = Environment::new();
let source = r#"
(do
(def s (series 5))
(push s true)
(push s 1)
(s 0)
)
"#;
let result = env.compile(source).into_result();
assert!(result.is_err(), "Bool->Int promotion must not be allowed");
}
/// Sanity check: Float → Bool is NOT a valid promotion.
#[test]
fn test_series_no_promotion_float_bool() {
let env = Environment::new();
let source = r#"
(do
(def s (series 5))
(push s 1.5)
(push s true)
(s 0)
)
"#;
let result = env.compile(source).into_result();
assert!(result.is_err(), "Float->Bool promotion must not be allowed");
}
#[test]
fn test_record_structural_equality_order() {
let env = Environment::new();