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)`.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
(do
|
||||||
|
(def last (fn [s] (s 0)))
|
||||||
|
|
||||||
|
(def f (fn [n] n))
|
||||||
|
|
||||||
|
(def r (series 5))
|
||||||
|
(push r 4)
|
||||||
|
|
||||||
|
[(last f) (last r)]
|
||||||
|
)
|
||||||
@@ -95,7 +95,7 @@ impl TypeInferenceAccess for TypeChecker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bind_typevar(&self, id: u32, ty: StaticType) {
|
fn bind_typevar(&self, id: u32, ty: StaticType) {
|
||||||
self.subst.borrow_mut().insert(id, ty);
|
self.bind_var(id, ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_subst_ty(&self, ty: StaticType) -> StaticType {
|
fn apply_subst_ty(&self, ty: StaticType) -> StaticType {
|
||||||
@@ -174,6 +174,17 @@ pub struct TypeChecker {
|
|||||||
/// Global substitution map: TypeVar ID → resolved StaticType.
|
/// Global substitution map: TypeVar ID → resolved StaticType.
|
||||||
/// Shared across all scopes within a single type-checking pass.
|
/// Shared across all scopes within a single type-checking pass.
|
||||||
subst: std::cell::RefCell<HashMap<u32, StaticType>>,
|
subst: std::cell::RefCell<HashMap<u32, StaticType>>,
|
||||||
|
/// Index-call constraints: callee_var → result_var.
|
||||||
|
///
|
||||||
|
/// When a `TypeVar` is used as a callable with a single `Int` argument —
|
||||||
|
/// the series lookback pattern `(s 0)` — we record the pairing here instead
|
||||||
|
/// of eagerly unifying `TypeVar = Series(elem)`. The constraint is evaluated
|
||||||
|
/// lazily in `bind_var`: only when the callee TypeVar is bound to `Series(inner)`
|
||||||
|
/// is the result TypeVar also bound to `inner`.
|
||||||
|
///
|
||||||
|
/// **Extension point**: other indexable types (e.g. `Stream`, future `Map`)
|
||||||
|
/// can participate by being matched in `bind_var`'s propagation arm.
|
||||||
|
index_constraints: std::cell::RefCell<HashMap<u32, u32>>,
|
||||||
/// Type-inference and finalization hooks keyed by global slot index.
|
/// Type-inference and finalization hooks keyed by global slot index.
|
||||||
/// Populated from the frozen RTL snapshot; empty for non-RTL call sites.
|
/// Populated from the frozen RTL snapshot; empty for non-RTL call sites.
|
||||||
call_hooks: Rc<HashMap<u32, CallHooks>>,
|
call_hooks: Rc<HashMap<u32, CallHooks>>,
|
||||||
@@ -188,6 +199,7 @@ impl TypeChecker {
|
|||||||
root_types,
|
root_types,
|
||||||
var_counter: Cell::new(0),
|
var_counter: Cell::new(0),
|
||||||
subst: std::cell::RefCell::new(HashMap::new()),
|
subst: std::cell::RefCell::new(HashMap::new()),
|
||||||
|
index_constraints: std::cell::RefCell::new(HashMap::new()),
|
||||||
call_hooks,
|
call_hooks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -199,6 +211,28 @@ impl TypeChecker {
|
|||||||
StaticType::TypeVar(id)
|
StaticType::TypeVar(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Binds a TypeVar to a concrete type in the substitution, then propagates
|
||||||
|
/// any pending index-call constraints registered by Step 9.
|
||||||
|
///
|
||||||
|
/// When `ty` is `Series(inner)` and `index_constraints[id]` is set, the
|
||||||
|
/// result TypeVar is also bound to `inner` — connecting the callee TypeVar
|
||||||
|
/// to its element type without the eager over-constraint of unification.
|
||||||
|
///
|
||||||
|
/// To add support for another indexable type (e.g. `Stream`), extend the
|
||||||
|
/// `match &ty` arm in the propagation block below.
|
||||||
|
fn bind_var(&self, id: u32, ty: StaticType) {
|
||||||
|
self.subst.borrow_mut().insert(id, ty.clone());
|
||||||
|
let ret_id = self.index_constraints.borrow().get(&id).copied();
|
||||||
|
if let Some(ret_id) = ret_id {
|
||||||
|
match &ty {
|
||||||
|
StaticType::Series(inner) => {
|
||||||
|
self.subst.borrow_mut().insert(ret_id, *inner.clone());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Recursively applies the substitution map to a type, replacing all
|
/// Recursively applies the substitution map to a type, replacing all
|
||||||
/// resolved `TypeVar`s with their concrete types.
|
/// resolved `TypeVar`s with their concrete types.
|
||||||
fn apply_subst(ty: StaticType, subst: &HashMap<u32, StaticType>) -> StaticType {
|
fn apply_subst(ty: StaticType, subst: &HashMap<u32, StaticType>) -> StaticType {
|
||||||
@@ -227,6 +261,13 @@ impl TypeChecker {
|
|||||||
StaticType::Tuple(elems) => {
|
StaticType::Tuple(elems) => {
|
||||||
StaticType::Tuple(elems.into_iter().map(|t| Self::apply_subst(t, subst)).collect())
|
StaticType::Tuple(elems.into_iter().map(|t| Self::apply_subst(t, subst)).collect())
|
||||||
}
|
}
|
||||||
|
// Substitute into the body but skip over the bound vars: they are local to
|
||||||
|
// this schema and must not be replaced by the global substitution.
|
||||||
|
StaticType::Forall(vars, body) => {
|
||||||
|
let filtered: HashMap<u32, StaticType> =
|
||||||
|
subst.iter().filter(|(k, _)| !vars.contains(k)).map(|(k, v)| (*k, v.clone())).collect();
|
||||||
|
StaticType::Forall(vars, Box::new(Self::apply_subst(*body, &filtered)))
|
||||||
|
}
|
||||||
other => other,
|
other => other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,6 +295,10 @@ impl TypeChecker {
|
|||||||
|| Self::occurs(var_id, &sig.ret, subst)
|
|| Self::occurs(var_id, &sig.ret, subst)
|
||||||
}
|
}
|
||||||
StaticType::Tuple(elems) => elems.iter().any(|t| Self::occurs(var_id, t, subst)),
|
StaticType::Tuple(elems) => elems.iter().any(|t| Self::occurs(var_id, t, subst)),
|
||||||
|
// A bound var inside Forall does not count as a free occurrence.
|
||||||
|
StaticType::Forall(vars, body) => {
|
||||||
|
!vars.contains(&var_id) && Self::occurs(var_id, body, subst)
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,7 +307,7 @@ impl TypeChecker {
|
|||||||
/// On success, the substitution is extended so that `ty1` and `ty2` become equal.
|
/// On success, the substitution is extended so that `ty1` and `ty2` become equal.
|
||||||
/// On failure (type mismatch or occurs check), a diagnostic error is emitted.
|
/// On failure (type mismatch or occurs check), a diagnostic error is emitted.
|
||||||
fn unify(&self, ty1: StaticType, ty2: StaticType, diag: &mut Diagnostics) {
|
fn unify(&self, ty1: StaticType, ty2: StaticType, diag: &mut Diagnostics) {
|
||||||
let mut subst = self.subst.borrow_mut();
|
let subst = self.subst.borrow_mut();
|
||||||
let ty1 = Self::apply_subst(ty1, &subst);
|
let ty1 = Self::apply_subst(ty1, &subst);
|
||||||
let ty2 = Self::apply_subst(ty2, &subst);
|
let ty2 = Self::apply_subst(ty2, &subst);
|
||||||
|
|
||||||
@@ -273,7 +318,10 @@ impl TypeChecker {
|
|||||||
diag.push_error(format!("Infinite type: ?{} = {}", n, ty), None);
|
diag.push_error(format!("Infinite type: ?{} = {}", n, ty), None);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
subst.insert(n, ty);
|
// Release the borrow before routing through bind_var so that
|
||||||
|
// constraint propagation can re-borrow subst without a panic.
|
||||||
|
drop(subst);
|
||||||
|
self.bind_var(n, ty);
|
||||||
}
|
}
|
||||||
(StaticType::Series(a), StaticType::Series(b)) => {
|
(StaticType::Series(a), StaticType::Series(b)) => {
|
||||||
drop(subst);
|
drop(subst);
|
||||||
@@ -296,9 +344,30 @@ impl TypeChecker {
|
|||||||
self.unify(ta, tb, diag);
|
self.unify(ta, tb, diag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A homogeneous Vector is assignable from a Tuple — unify each element
|
||||||
|
// with the vector's inner type. Mirrors the `is_assignable_from` coercion.
|
||||||
|
(StaticType::Tuple(elems), StaticType::Vector(inner, len))
|
||||||
|
| (StaticType::Vector(inner, len), StaticType::Tuple(elems)) => {
|
||||||
|
if elems.len() != len {
|
||||||
|
diag.push_error(
|
||||||
|
format!("Type mismatch: expected tuple of length {}, got {}", len, elems.len()),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(subst);
|
||||||
|
for e in elems {
|
||||||
|
self.unify(e, (*inner).clone(), diag);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Any and Error are already handled by is_assignable_from — silently succeed
|
// Any and Error are already handled by is_assignable_from — silently succeed
|
||||||
(StaticType::Any, _) | (_, StaticType::Any) => {}
|
(StaticType::Any, _) | (_, StaticType::Any) => {}
|
||||||
(StaticType::Error, _) | (_, StaticType::Error) => {}
|
(StaticType::Error, _) | (_, StaticType::Error) => {}
|
||||||
|
// Forall should be instantiated before unification; delegate to the body.
|
||||||
|
(StaticType::Forall(_, body), other) | (other, StaticType::Forall(_, body)) => {
|
||||||
|
drop(subst);
|
||||||
|
self.unify(*body, other, diag);
|
||||||
|
}
|
||||||
(a, b) => {
|
(a, b) => {
|
||||||
diag.push_error(format!("Type mismatch: expected {}, got {}", a, b), None);
|
diag.push_error(format!("Type mismatch: expected {}, got {}", a, b), None);
|
||||||
}
|
}
|
||||||
@@ -357,11 +426,17 @@ impl TypeChecker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if `ty` (or any element of a Tuple) contains a TypeVar.
|
/// Returns true if `ty` contains a TypeVar anywhere in its structure.
|
||||||
fn has_typevar_component(ty: &StaticType) -> bool {
|
fn has_typevar_component(ty: &StaticType) -> bool {
|
||||||
match ty {
|
match ty {
|
||||||
StaticType::TypeVar(_) => true,
|
StaticType::TypeVar(_) => true,
|
||||||
StaticType::Tuple(elems) => elems.iter().any(|e| matches!(e, StaticType::TypeVar(_))),
|
StaticType::Tuple(elems) => elems.iter().any(Self::has_typevar_component),
|
||||||
|
StaticType::Series(inner) | StaticType::Stream(inner) | StaticType::Optional(inner) => {
|
||||||
|
Self::has_typevar_component(inner)
|
||||||
|
}
|
||||||
|
StaticType::Function(sig) => {
|
||||||
|
Self::has_typevar_component(&sig.params) || Self::has_typevar_component(&sig.ret)
|
||||||
|
}
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -410,6 +485,117 @@ impl TypeChecker {
|
|||||||
Some(StaticType::Record(RecordLayout::get_or_create(promoted_fields)))
|
Some(StaticType::Record(RecordLayout::get_or_create(promoted_fields)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Collects all free TypeVar IDs that appear in `ty`, following the substitution
|
||||||
|
/// chain and skipping variables bound by `Forall`. Deduplicates via `out`.
|
||||||
|
fn collect_free_tvars(ty: &StaticType, subst: &HashMap<u32, StaticType>, out: &mut Vec<u32>) {
|
||||||
|
match ty {
|
||||||
|
StaticType::TypeVar(n) => {
|
||||||
|
if let Some(resolved) = subst.get(n) {
|
||||||
|
Self::collect_free_tvars(resolved, subst, out);
|
||||||
|
} else if !out.contains(n) {
|
||||||
|
out.push(*n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StaticType::Series(inner) | StaticType::Stream(inner) | StaticType::Optional(inner) => {
|
||||||
|
Self::collect_free_tvars(inner, subst, out);
|
||||||
|
}
|
||||||
|
StaticType::Function(sig) => {
|
||||||
|
Self::collect_free_tvars(&sig.params, subst, out);
|
||||||
|
Self::collect_free_tvars(&sig.ret, subst, out);
|
||||||
|
}
|
||||||
|
StaticType::Tuple(elems) => {
|
||||||
|
for e in elems {
|
||||||
|
Self::collect_free_tvars(e, subst, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Recurse into the body but skip the locally bound vars.
|
||||||
|
StaticType::Forall(vars, body) => {
|
||||||
|
let mut body_free = Vec::new();
|
||||||
|
Self::collect_free_tvars(body, subst, &mut body_free);
|
||||||
|
for v in body_free {
|
||||||
|
if !vars.contains(&v) && !out.contains(&v) {
|
||||||
|
out.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Collects all free TypeVar IDs that are currently visible in `ctx`.
|
||||||
|
/// Used by `generalize` to avoid quantifying TypeVars that are still shared
|
||||||
|
/// with other live bindings (series, scalars, outer function params).
|
||||||
|
fn ctx_free_tvars(ctx: &TypeContext, subst: &HashMap<u32, StaticType>) -> Vec<u32> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for ty in ctx.slots.values() {
|
||||||
|
Self::collect_free_tvars(ty, subst, &mut out);
|
||||||
|
}
|
||||||
|
for ty in &ctx.upvalue_types {
|
||||||
|
Self::collect_free_tvars(ty, subst, &mut out);
|
||||||
|
}
|
||||||
|
for ty in ctx.root_types.borrow().iter() {
|
||||||
|
Self::collect_free_tvars(ty, subst, &mut out);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generalizes a type at a `def` boundary (Algorithm W `gen` step).
|
||||||
|
/// Wraps all TypeVars that are free in `ty` but NOT free in `ctx` into a
|
||||||
|
/// `Forall`. Value restriction: only call this for `Function`-typed values.
|
||||||
|
fn generalize(&self, ty: StaticType, ctx: &TypeContext) -> StaticType {
|
||||||
|
let subst = self.subst.borrow();
|
||||||
|
let resolved = Self::apply_subst(ty, &subst);
|
||||||
|
let mut tvars_in_ty = Vec::new();
|
||||||
|
Self::collect_free_tvars(&resolved, &subst, &mut tvars_in_ty);
|
||||||
|
let ctx_tvars = Self::ctx_free_tvars(ctx, &subst);
|
||||||
|
let quantified: Vec<u32> = tvars_in_ty.into_iter()
|
||||||
|
.filter(|v| !ctx_tvars.contains(v))
|
||||||
|
.collect();
|
||||||
|
if quantified.is_empty() {
|
||||||
|
resolved
|
||||||
|
} else {
|
||||||
|
StaticType::Forall(quantified, Box::new(resolved))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Instantiates a `Forall` type by replacing each bound TypeVar with a
|
||||||
|
/// fresh one (Algorithm W `inst` step). No-op for non-`Forall` types.
|
||||||
|
///
|
||||||
|
/// Also remaps any index-call constraints: if `index_constraints[old] = ret`
|
||||||
|
/// and both `old` and `ret` are bound vars, the fresh copies inherit the
|
||||||
|
/// same pairing so that `bind_var` propagation keeps working at each call site.
|
||||||
|
fn instantiate(&self, ty: StaticType) -> StaticType {
|
||||||
|
let StaticType::Forall(vars, body) = ty else { return ty };
|
||||||
|
let mut local_subst: HashMap<u32, StaticType> = HashMap::new();
|
||||||
|
let mut var_mapping: HashMap<u32, u32> = HashMap::new();
|
||||||
|
for v in &vars {
|
||||||
|
let fresh = self.fresh_var();
|
||||||
|
if let StaticType::TypeVar(fresh_id) = &fresh {
|
||||||
|
var_mapping.insert(*v, *fresh_id);
|
||||||
|
}
|
||||||
|
local_subst.insert(*v, fresh);
|
||||||
|
}
|
||||||
|
// Copy index-call constraints for the freshly created TypeVars.
|
||||||
|
// Both the callee-var and its result-var must be in vars for the
|
||||||
|
// mapping to apply; partial remaps are dropped (they can't occur in
|
||||||
|
// a well-formed Forall, but the guard is cheap insurance).
|
||||||
|
let new_constraints: Vec<(u32, u32)> = {
|
||||||
|
let constraints = self.index_constraints.borrow();
|
||||||
|
vars.iter()
|
||||||
|
.filter_map(|v| {
|
||||||
|
let new_v = *var_mapping.get(v)?;
|
||||||
|
let old_ret = *constraints.get(v)?;
|
||||||
|
let new_ret = *var_mapping.get(&old_ret)?;
|
||||||
|
Some((new_v, new_ret))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
for (new_v, new_ret) in new_constraints {
|
||||||
|
self.index_constraints.borrow_mut().insert(new_v, new_ret);
|
||||||
|
}
|
||||||
|
Self::apply_subst(*body, &local_subst)
|
||||||
|
}
|
||||||
|
|
||||||
/// Walks the typed AST, applies the HM substitution to every type annotation,
|
/// Walks the typed AST, applies the HM substitution to every type annotation,
|
||||||
/// and dispatches finalization hooks (e.g. schema injection for `series`).
|
/// and dispatches finalization hooks (e.g. schema injection for `series`).
|
||||||
fn finalize_node(&self, node: TypedNode, subst: &HashMap<u32, StaticType>) -> TypedNode {
|
fn finalize_node(&self, node: TypedNode, subst: &HashMap<u32, StaticType>) -> TypedNode {
|
||||||
@@ -881,13 +1067,20 @@ impl TypeChecker {
|
|||||||
let val_typed = self.check_node(value, ctx, diag);
|
let val_typed = self.check_node(value, ctx, diag);
|
||||||
let ty = val_typed.ty.clone();
|
let ty = val_typed.ty.clone();
|
||||||
|
|
||||||
// Extract addr from pattern to register the type
|
// Extract addr from pattern to register the type.
|
||||||
|
// Value restriction: only Function-typed values are generalized to Forall.
|
||||||
|
// Mutable state (Series, scalars) must remain monomorphic.
|
||||||
if let NodeKind::Identifier {
|
if let NodeKind::Identifier {
|
||||||
binding: IdentifierBinding::Declaration { addr, .. },
|
binding: IdentifierBinding::Declaration { addr, .. },
|
||||||
..
|
..
|
||||||
} = &pattern.kind
|
} = &pattern.kind
|
||||||
{
|
{
|
||||||
ctx.set_type(*addr, ty.clone());
|
let stored_ty = if matches!(ty, StaticType::Function(..)) {
|
||||||
|
self.generalize(ty.clone(), ctx)
|
||||||
|
} else {
|
||||||
|
ty.clone()
|
||||||
|
};
|
||||||
|
ctx.set_type(*addr, stored_ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// For destructuring defs, check params on the pattern
|
// For destructuring defs, check params on the pattern
|
||||||
@@ -949,7 +1142,10 @@ impl TypeChecker {
|
|||||||
// Apply the current HM substitution so that TypeVars resolved in
|
// Apply the current HM substitution so that TypeVars resolved in
|
||||||
// nested scopes (e.g. inside a `while` body) are visible here even
|
// nested scopes (e.g. inside a `while` body) are visible here even
|
||||||
// when ctx.set_type could not propagate back through an upvalue address.
|
// when ctx.set_type could not propagate back through an upvalue address.
|
||||||
|
// Instantiate Forall types: each use site gets fresh TypeVars so that
|
||||||
|
// calls with different argument types remain independent.
|
||||||
let ty = Self::apply_subst(ctx.get_type(*addr), &self.subst.borrow());
|
let ty = Self::apply_subst(ctx.get_type(*addr), &self.subst.borrow());
|
||||||
|
let ty = self.instantiate(ty);
|
||||||
(
|
(
|
||||||
NodeKind::Identifier {
|
NodeKind::Identifier {
|
||||||
symbol: symbol.clone(),
|
symbol: symbol.clone(),
|
||||||
@@ -1225,6 +1421,42 @@ impl TypeChecker {
|
|||||||
// e.g. `(+ Float TypeVar(1))` resolves TypeVar(1) = Float.
|
// e.g. `(+ Float TypeVar(1))` resolves TypeVar(1) = Float.
|
||||||
self.unify_matched_overload(&callee_typed.ty, &args_typed.ty, diag);
|
self.unify_matched_overload(&callee_typed.ty, &args_typed.ty, diag);
|
||||||
|
|
||||||
|
// HM step 9: when a TypeVar is called with a single Int argument
|
||||||
|
// (series lookback indexing pattern), record a lazy index-call constraint
|
||||||
|
// instead of eagerly unifying `TypeVar = Series(elem)`.
|
||||||
|
//
|
||||||
|
// The constraint pair (callee_var → result_var) is stored in
|
||||||
|
// `index_constraints`. When `bind_var` later resolves callee_var to
|
||||||
|
// `Series(inner)`, it automatically binds result_var to `inner`.
|
||||||
|
//
|
||||||
|
// This avoids over-constraining the function to Series-only: passing
|
||||||
|
// any other callable (e.g. a Function) simply leaves result_var
|
||||||
|
// unresolved and the call returns `Any` — no spurious type error.
|
||||||
|
if let StaticType::TypeVar(n) = &callee_typed.ty {
|
||||||
|
let is_index_call = matches!(&args_typed.ty,
|
||||||
|
StaticType::Tuple(elems) if elems.len() == 1
|
||||||
|
&& matches!(&elems[0], StaticType::Int)
|
||||||
|
);
|
||||||
|
if is_index_call && matches!(ret_ty, StaticType::Any) {
|
||||||
|
let elem_var = self.fresh_var();
|
||||||
|
let StaticType::TypeVar(elem_id) = &elem_var else { unreachable!() };
|
||||||
|
self.index_constraints.borrow_mut().insert(*n, *elem_id);
|
||||||
|
ret_ty = elem_var;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HM step 10: unify Function parameter types with actual argument types,
|
||||||
|
// but only when the signature still contains TypeVars to resolve.
|
||||||
|
// Skip for fully concrete signatures (e.g. fn([any any])) to avoid
|
||||||
|
// false conflicts between Tuple and Vector representations.
|
||||||
|
if let StaticType::Function(sig) = &callee_typed.ty
|
||||||
|
&& Self::has_typevar_component(&sig.params)
|
||||||
|
{
|
||||||
|
let params = sig.params.clone();
|
||||||
|
self.unify(params, args_typed.ty.clone(), diag);
|
||||||
|
ret_ty = Self::apply_subst(ret_ty, &self.subst.borrow());
|
||||||
|
}
|
||||||
|
|
||||||
// Dispatch post-call hooks registered by the RTL (keyed by global slot index).
|
// Dispatch post-call hooks registered by the RTL (keyed by global slot index).
|
||||||
// Hooks handle type-inference extensions such as:
|
// Hooks handle type-inference extensions such as:
|
||||||
// - series: inject a fresh TypeVar for the element type
|
// - series: inject a fresh TypeVar for the element type
|
||||||
|
|||||||
@@ -376,6 +376,12 @@ pub enum StaticType {
|
|||||||
/// An unresolved type variable used during Hindley-Milner type inference.
|
/// An unresolved type variable used during Hindley-Milner type inference.
|
||||||
/// The `u32` is a unique ID assigned by the type checker. Resolved via substitution.
|
/// The `u32` is a unique ID assigned by the type checker. Resolved via substitution.
|
||||||
TypeVar(u32),
|
TypeVar(u32),
|
||||||
|
/// A universally quantified type (let-polymorphism / HM generalization).
|
||||||
|
/// Created at `def` boundaries for function values; instantiated with fresh TypeVars
|
||||||
|
/// at each use site so calls with different types remain independent.
|
||||||
|
/// Value restriction: only `Function`-typed defs are generalized — mutable state
|
||||||
|
/// (series, scalars) must remain monomorphic.
|
||||||
|
Forall(Vec<u32>, Box<StaticType>),
|
||||||
/// A diagnostic poison type, allowing type-checking to continue after an error.
|
/// A diagnostic poison type, allowing type-checking to continue after an error.
|
||||||
Error,
|
Error,
|
||||||
}
|
}
|
||||||
@@ -436,6 +442,14 @@ impl fmt::Display for StaticType {
|
|||||||
StaticType::Object(name) => write!(f, "{}", name),
|
StaticType::Object(name) => write!(f, "{}", name),
|
||||||
StaticType::PolymorphicFn { .. } => write!(f, "<polymorphic-fn>"),
|
StaticType::PolymorphicFn { .. } => write!(f, "<polymorphic-fn>"),
|
||||||
StaticType::TypeVar(n) => write!(f, "?{}", n),
|
StaticType::TypeVar(n) => write!(f, "?{}", n),
|
||||||
|
StaticType::Forall(vars, body) => {
|
||||||
|
write!(f, "∀")?;
|
||||||
|
for (i, v) in vars.iter().enumerate() {
|
||||||
|
if i > 0 { write!(f, ",")?; }
|
||||||
|
write!(f, "?{}", v)?;
|
||||||
|
}
|
||||||
|
write!(f, ". {}", body)
|
||||||
|
}
|
||||||
StaticType::Error => write!(f, "<error>"),
|
StaticType::Error => write!(f, "<error>"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -485,6 +499,8 @@ impl StaticType {
|
|||||||
|| matches!(other, StaticType::Error)
|
|| matches!(other, StaticType::Error)
|
||||||
|| matches!(self, StaticType::TypeVar(_))
|
|| matches!(self, StaticType::TypeVar(_))
|
||||||
|| matches!(other, StaticType::TypeVar(_))
|
|| matches!(other, StaticType::TypeVar(_))
|
||||||
|
|| matches!(self, StaticType::Forall(..))
|
||||||
|
|| matches!(other, StaticType::Forall(..))
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -599,6 +615,9 @@ impl StaticType {
|
|||||||
.map(|sig| sig.ret.clone()),
|
.map(|sig| sig.ret.clone()),
|
||||||
StaticType::PolymorphicFn { resolve_return, .. } => resolve_return(args_ty),
|
StaticType::PolymorphicFn { resolve_return, .. } => resolve_return(args_ty),
|
||||||
StaticType::TypeVar(_) => Some(StaticType::Any),
|
StaticType::TypeVar(_) => Some(StaticType::Any),
|
||||||
|
// Forall should be instantiated before resolve_call is reached.
|
||||||
|
// This fallback delegates to the body as a safety net.
|
||||||
|
StaticType::Forall(_, body) => body.resolve_call(args_ty),
|
||||||
// Lookback indexing: (my-series 0) → element type
|
// Lookback indexing: (my-series 0) → element type
|
||||||
StaticType::Series(inner) => {
|
StaticType::Series(inner) => {
|
||||||
let is_int = matches!(
|
let is_int = matches!(
|
||||||
|
|||||||
@@ -189,3 +189,29 @@ fn test_let_poly_generic_return_used_in_field_series() {
|
|||||||
other => panic!("Expected Float(42.0), got {:?}", other),
|
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()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user