Files
RustAst/tests/hm_poly.rs
T
Brummel 84abbd9721 Add Forall type for polymorphism
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)`.
2026-03-28 22:49:44 +01:00

218 lines
7.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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()
);
}