Files
RustAst/tests/rtl_series.rs
T
Brummel 6cab8f649b 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.
2026-03-28 14:52:24 +01:00

213 lines
5.9 KiB
Rust

use myc::ast::environment::Environment;
use myc::ast::types::Value;
#[test]
fn test_series_api_with_limit() {
let env = Environment::new();
// (series limit) — schema is inferred from push calls
let source = "(series 100)";
let result = env.compile(source).into_result();
assert!(result.is_ok(), "Series creation with limit should work: {:?}", result.err());
}
/// Regression test for HM type inference through nested closures.
///
/// `series` is created in an outer closure, `push` happens in an inner closure.
/// The pushed value type must propagate back through the TypeVar chain so that
/// the compiler can inject the `:float` schema — no explicit schema given.
#[test]
fn test_series_infer_type_from_nested_closure() {
let env = Environment::new();
let source = r#"
(do
(def make-acc
(fn [n]
(do
(def s (series n))
(def total 0.0)
(fn [x]
(do
(assign total (+ total x))
(push s x)
(s 0))))))
(def acc (make-acc 5))
(acc 1.5))
"#;
let result = env.run_script(source);
match result {
Ok(Value::Float(v)) if (v - 1.5).abs() < 1e-9 => {}
other => panic!("Expected Float(1.5), got {:?}", other),
}
}
/// Regression test: TypeVar on a series used as an upvalue inside a `while` loop
/// (zero-parameter lambda created by the macro) must be resolved via the global
/// substitution even when `ctx.set_type` silently ignores the upvalue address.
#[test]
fn test_series_typevar_resolved_through_while_upvalue() {
let env = Environment::new();
let source = r#"
(do
(def ticks (series 10))
(def i 0)
(while (< i 3)
(do
(push ticks {:price (* i 1.0)})
(assign i (+ i 1))))
(def p (.price ticks))
(p 0)
)
"#;
let result = env.run_script(source);
match result {
Ok(Value::Float(v)) if (v - 2.0).abs() < 1e-9 => {}
other => panic!("Expected Float(2.0), got {:?}", other),
}
}
// ── 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();
// Known design choice: currently order dependent.
let source = "(= {:a 1 :b 2} {:b 2 :a 1})";
let result = env.run_script(source);
match result {
Ok(Value::Bool(false)) => {},
_ => panic!("Expected false (current implementation behavior)"),
}
}