84abbd9721
Introduce `StaticType::Forall` to represent universally quantified types. This enables Hindley-Milner's let-polymorphism, allowing functions to be generalized and reused with different concrete types. - **Generalization at `def`:** Function values are now generalized using `self.generalize` when they are defined, wrapping their type in `Forall`. This occurs at `def` boundaries. - **Instantiation at use:** Function types are instantiated with fresh type variables at each call site using `self.instantiate`. This ensures that different uses of the same polymorphic function do not interfere with each other's type inference. - **Value restriction:** Only function-typed definitions are generalized. Mutable state like series and scalars remain monomorphic to prevent unexpected behavior. - **Type context awareness:** The `generalize` function considers the current type context to avoid quantifying type variables that are still in use elsewhere. Feat: Add Forall type for polymorphism Introduces the `Forall` type to support polymorphic functions and enable let-polymorphism. - `Forall(vars, body)`: Represents a universally quantified type where `vars` are the type variables bound by this quantification, and `body` is the type itself. - `TypeChecker::generalize`: Wraps a type in `Forall` when a `def` boundary is encountered for function-typed values. This ensures that each use site of a polymorphic function gets its own instantiation. - `TypeChecker::instantiate`: Replaces the type variables within a `Forall` type with fresh ones. This is performed at each call site of a polymorphic function. - Updates `TypeChecker::unify` and `TypeChecker::apply_subst` to correctly handle `Forall` types, ensuring that bound variables within a `Forall` are not affected by global substitutions and that `Forall` types are instantiated before unification. - Adds a new example script `examples/Propa.myc` to demonstrate basic usage. - Includes a regression test `test_let_poly_non_series_callable_no_false_positive` to ensure that passing non-series callables to generic functions does not lead to type errors. - Introduces `TypeChecker::bind_var` to handle lazy propagation of index-call constraints, crucial for correctly typing series lookups like `(s 0)`.
218 lines
7.5 KiB
Rust
218 lines
7.5 KiB
Rust
use myc::ast::environment::Environment;
|
||
use myc::ast::types::Value;
|
||
|
||
// ── Stage B: HM Let-Polymorphism ─────────────────────────────────────────────
|
||
//
|
||
// These tests verify that user-defined generic functions (e.g. `last`, `prev`)
|
||
// work correctly across multiple series element types without type conflicts.
|
||
//
|
||
// Before Stage B: TypeVars are unified once per call site. The optimizer
|
||
// hides failures by inlining, but diagnostics may report false conflicts and
|
||
// return types remain `Any` instead of the precise inferred type.
|
||
//
|
||
// After Stage B: `generalize` wraps function types in `Forall`; `instantiate`
|
||
// gives each call site fresh TypeVars. No conflicts, precise return types.
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
/// A generic accessor function must compile without any type-conflict diagnostics
|
||
/// when called with two series of different element types.
|
||
///
|
||
/// This is the primary Stage B regression test. Before let-polymorphism, the
|
||
/// TypeVar from the first call may leak into the second, producing a spurious
|
||
/// type-conflict error.
|
||
#[test]
|
||
fn test_let_poly_no_conflict_diagnostic() {
|
||
let env = Environment::new();
|
||
let source = r#"
|
||
(do
|
||
(def last (fn [s] (s 0)))
|
||
(def a (series 5))
|
||
(push a 1.5)
|
||
(def b (series 5))
|
||
(push b "hello")
|
||
(last a)
|
||
(last b)
|
||
)
|
||
"#;
|
||
let result = env.compile(source);
|
||
assert!(
|
||
!result.diagnostics.has_errors(),
|
||
"Generic function used with two series types must not produce type errors: {}",
|
||
result.diagnostics.format_errors()
|
||
);
|
||
let has_type_conflict = result
|
||
.diagnostics
|
||
.items
|
||
.iter()
|
||
.any(|d| d.message.contains("type") || d.message.contains("conflict") || d.message.contains("mismatch"));
|
||
assert!(
|
||
!has_type_conflict,
|
||
"No type-conflict diagnostics expected for a polymorphic function: {:?}",
|
||
result.diagnostics.items
|
||
);
|
||
}
|
||
|
||
/// Runtime correctness: `last` retrieves the correct value from Float and Text series.
|
||
#[test]
|
||
fn test_let_poly_two_series_types_runtime() {
|
||
let env = Environment::new();
|
||
let source = r#"
|
||
(do
|
||
(def last (fn [s] (s 0)))
|
||
(def a (series 5))
|
||
(push a 1.5)
|
||
(push a 2.5)
|
||
(def b (series 5))
|
||
(push b "hello")
|
||
(push b "world")
|
||
[(last a) (last b)]
|
||
)
|
||
"#;
|
||
let result = env.run_script(source);
|
||
match result {
|
||
Ok(Value::Tuple(elems)) if elems.len() == 2 => {
|
||
match (&elems[0], &elems[1]) {
|
||
(Value::Float(f), Value::Text(t))
|
||
if (*f - 2.5).abs() < 1e-9 && t.as_ref() == "world" => {}
|
||
other => panic!("Expected [Float(2.5), Text(\"world\")], got {:?}", other),
|
||
}
|
||
}
|
||
other => panic!("Expected tuple result, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
/// A generic function works on record series as well as scalar series.
|
||
#[test]
|
||
fn test_let_poly_record_series() {
|
||
let env = Environment::new();
|
||
let source = r#"
|
||
(do
|
||
(def last (fn [s] (s 0)))
|
||
(def prices (series 5))
|
||
(push prices {:close 42.0 :vol 100})
|
||
(def names (series 5))
|
||
(push names {:name "Alice"})
|
||
[
|
||
((.close prices) 0)
|
||
((.name names) 0)
|
||
]
|
||
)
|
||
"#;
|
||
let result = env.run_script(source);
|
||
match result {
|
||
Ok(Value::Tuple(elems)) if elems.len() == 2 => {
|
||
match (&elems[0], &elems[1]) {
|
||
(Value::Float(f), Value::Text(t))
|
||
if (*f - 42.0).abs() < 1e-9 && t.as_ref() == "Alice" => {}
|
||
other => panic!("Expected [Float(42.0), Text(\"Alice\")], got {:?}", other),
|
||
}
|
||
}
|
||
other => panic!("Expected tuple result, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
/// Value restriction: a series binding must NOT be generalized.
|
||
/// Pushing incompatible types into the same series must remain a type error.
|
||
#[test]
|
||
fn test_let_poly_value_restriction_series() {
|
||
let env = Environment::new();
|
||
let source = r#"
|
||
(do
|
||
(def s (series 5))
|
||
(push s 1.5)
|
||
(push s "hello")
|
||
(s 0)
|
||
)
|
||
"#;
|
||
let result = env.compile(source);
|
||
assert!(
|
||
result.diagnostics.has_errors(),
|
||
"Pushing incompatible types into the same series must produce a type error"
|
||
);
|
||
}
|
||
|
||
/// Higher-order function: `apply-f` passes a generic function to different series.
|
||
/// Both calls must compile cleanly and return the correct values.
|
||
#[test]
|
||
fn test_let_poly_higher_order() {
|
||
let env = Environment::new();
|
||
let source = r#"
|
||
(do
|
||
(def last (fn [s] (s 0)))
|
||
(def apply-f (fn [f s] (f s)))
|
||
(def a (series 5))
|
||
(push a 1.5)
|
||
(push a 2.5)
|
||
(def b (series 5))
|
||
(push b "hello")
|
||
(push b "world")
|
||
[(apply-f last a) (apply-f last b)]
|
||
)
|
||
"#;
|
||
let result = env.run_script(source);
|
||
match result {
|
||
Ok(Value::Tuple(elems)) if elems.len() == 2 => {
|
||
match (&elems[0], &elems[1]) {
|
||
(Value::Float(f), Value::Text(t))
|
||
if (*f - 2.5).abs() < 1e-9 && t.as_ref() == "world" => {}
|
||
other => panic!("Expected [Float(2.5), Text(\"world\")], got {:?}", other),
|
||
}
|
||
}
|
||
other => panic!("Expected tuple result, got {:?}", other),
|
||
}
|
||
}
|
||
|
||
/// When a generic function returns `Any` (because its TypeVar is not resolved),
|
||
/// pushing the result into a new series leaves that series with an unresolved
|
||
/// element type. Field access on `Series(TypeVar)` then fails because the type
|
||
/// checker cannot find the record layout.
|
||
///
|
||
/// After Stage B + call-site unification: the return type of `(get-first prices)`
|
||
/// is correctly inferred as `Record({:price Float})`, the pushed series gets
|
||
/// `Series(Record({:price Float}))`, and field access works.
|
||
#[test]
|
||
fn test_let_poly_generic_return_used_in_field_series() {
|
||
let env = Environment::new();
|
||
let source = r#"
|
||
(do
|
||
(def get-first (fn [s] (s 0)))
|
||
(def prices (series 5))
|
||
(push prices {:price 42.0})
|
||
(def out (series 3))
|
||
(push out (get-first prices))
|
||
((.price out) 0)
|
||
)
|
||
"#;
|
||
let result = env.run_script(source);
|
||
match result {
|
||
Ok(Value::Float(v)) if (v - 42.0).abs() < 1e-9 => {}
|
||
other => panic!("Expected Float(42.0), got {:?}", other),
|
||
}
|
||
}
|
||
|
||
/// Regression test: passing a non-series callable to a generic `last` function
|
||
/// must NOT produce a type error. Before the index-constraint fix, Step 9 would
|
||
/// eagerly unify `TypeVar = Series(elem)`, making `last` Series-only and causing
|
||
/// a spurious conflict when a Function-typed variable was passed.
|
||
///
|
||
/// After the fix, `last` is inferred as `∀α β. fn(α) → β` with a lazy constraint:
|
||
/// only when `α` resolves to `Series(inner)` does `β` also resolve to `inner`.
|
||
/// For any other callable, `β` stays unbound and the return type is `Any`.
|
||
#[test]
|
||
fn test_let_poly_non_series_callable_no_false_positive() {
|
||
let env = Environment::new();
|
||
let source = r#"
|
||
(do
|
||
(def f (fn [n] (n)))
|
||
(def last (fn [s] (s 0)))
|
||
(last f)
|
||
)
|
||
"#;
|
||
let result = env.compile(source);
|
||
assert!(
|
||
!result.diagnostics.has_errors(),
|
||
"Passing a non-series callable to a generic function must not produce a type error: {}",
|
||
result.diagnostics.format_errors()
|
||
);
|
||
}
|