Refactor TypeChecker into modules
The TypeChecker implementation was becoming quite large, so it has been refactored into several modules: - `context`: Handles type checking contexts and inference access. - `inference`: Contains the core Hindley-Milner inference logic (unification, generalization, etc.). - `finalize`: Manages the finalization step, applying substitutions and dispatching hooks. - `check`: Implements the main type checking logic for AST nodes. This modularization improves code organization and maintainability.
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,845 @@
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::nodes::{
|
||||
Address, AssignBinding, BoundLike, DefBinding, IdentifierBinding, LambdaBinding,
|
||||
Node, NodeKind, TypedNode, TypedPhase,
|
||||
};
|
||||
use crate::ast::types::{Keyword, RecordLayout, Signature, StaticType};
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::context::{CheckerInferenceAccess, TypeContext, extract_lambda_param_hints};
|
||||
use super::TypeChecker;
|
||||
|
||||
impl TypeChecker {
|
||||
/// Types a lambda node using externally provided parameter type hints,
|
||||
/// while preserving the current scope's upvalue types.
|
||||
/// Unlike `check_node_as_bound`, this keeps the enclosing `TypeContext` as parent,
|
||||
/// so captured variables retain their inferred types.
|
||||
pub(super) fn check_lambda_with_param_hints<P: BoundLike>(
|
||||
&self,
|
||||
node: &Node<P>,
|
||||
param_hints: &[StaticType],
|
||||
ctx: &mut TypeContext,
|
||||
diag: &mut Diagnostics,
|
||||
) -> TypedNode {
|
||||
let NodeKind::Lambda {
|
||||
params,
|
||||
body,
|
||||
info,
|
||||
} = &node.kind
|
||||
else {
|
||||
return self.check_node(node, ctx, diag);
|
||||
};
|
||||
|
||||
let upvalues = &info.upvalues;
|
||||
let positional_count = info.positional_count;
|
||||
|
||||
let mut upvalue_types = Vec::with_capacity(upvalues.len());
|
||||
for &addr in upvalues {
|
||||
upvalue_types.push(ctx.get_type(addr));
|
||||
}
|
||||
|
||||
let mut lambda_ctx = TypeContext::new(64, upvalue_types, ctx.root_types, Some(ctx));
|
||||
|
||||
let hint_ty = StaticType::Tuple(param_hints.to_vec());
|
||||
let params_typed = self.check_params(params.as_ref(), &hint_ty, &mut lambda_ctx, diag);
|
||||
|
||||
lambda_ctx.current_params_ty = Some(params_typed.ty.clone());
|
||||
|
||||
let body_typed = self.check_node(body, &mut lambda_ctx, diag);
|
||||
let ret_ty = body_typed.ty.clone();
|
||||
|
||||
let fn_ty = StaticType::Function(Box::new(Signature {
|
||||
params: params_typed.ty.clone(),
|
||||
ret: ret_ty,
|
||||
}));
|
||||
|
||||
Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Lambda {
|
||||
params: Rc::new(params_typed),
|
||||
body: Rc::new(body_typed),
|
||||
info: LambdaBinding {
|
||||
upvalues: upvalues.clone(),
|
||||
positional_count,
|
||||
},
|
||||
},
|
||||
ty: fn_ty,
|
||||
comments: node.comments.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_params<P: BoundLike>(
|
||||
&self,
|
||||
node: &Node<P>,
|
||||
specialized_ty: &StaticType,
|
||||
ctx: &mut TypeContext,
|
||||
diag: &mut Diagnostics,
|
||||
) -> TypedNode {
|
||||
let (kind, ty): (NodeKind<TypedPhase>, StaticType) = match &node.kind {
|
||||
NodeKind::Def {
|
||||
pattern,
|
||||
info,
|
||||
..
|
||||
} => {
|
||||
if let NodeKind::Identifier {
|
||||
symbol,
|
||||
binding: IdentifierBinding::Declaration { addr, kind: decl_kind },
|
||||
} = &pattern.kind
|
||||
{
|
||||
ctx.set_type(*addr, specialized_ty.clone());
|
||||
(
|
||||
NodeKind::Def {
|
||||
pattern: Rc::new(Node {
|
||||
identity: pattern.identity.clone(),
|
||||
kind: NodeKind::Identifier {
|
||||
symbol: symbol.clone(),
|
||||
binding: IdentifierBinding::Declaration {
|
||||
addr: *addr,
|
||||
kind: *decl_kind,
|
||||
},
|
||||
},
|
||||
ty: specialized_ty.clone(),
|
||||
comments: pattern.comments.clone(),
|
||||
}),
|
||||
value: Rc::new(Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Nop,
|
||||
ty: specialized_ty.clone(),
|
||||
comments: Rc::from([]),
|
||||
}),
|
||||
info: DefBinding {
|
||||
captured_by: info.captured_by.clone(),
|
||||
},
|
||||
},
|
||||
specialized_ty.clone(),
|
||||
)
|
||||
} else {
|
||||
// Destructuring def in params — fall through to tuple handling
|
||||
// if pattern is a Tuple, handle elements
|
||||
if let NodeKind::Tuple { elements } = &pattern.kind {
|
||||
return self.check_params_tuple(node, elements, specialized_ty, ctx, diag);
|
||||
}
|
||||
diag.push_error(
|
||||
"Invalid pattern in parameter definition",
|
||||
Some(node.identity.clone()),
|
||||
);
|
||||
(NodeKind::Error, StaticType::Error)
|
||||
}
|
||||
}
|
||||
NodeKind::Assign {
|
||||
info,
|
||||
..
|
||||
} => {
|
||||
(
|
||||
NodeKind::Assign {
|
||||
target: Rc::new(Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Nop,
|
||||
ty: specialized_ty.clone(),
|
||||
comments: Rc::from([]),
|
||||
}),
|
||||
value: Rc::new(Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Nop,
|
||||
ty: specialized_ty.clone(),
|
||||
comments: Rc::from([]),
|
||||
}),
|
||||
info: AssignBinding {
|
||||
addr: info.addr,
|
||||
},
|
||||
},
|
||||
specialized_ty.clone(),
|
||||
)
|
||||
}
|
||||
NodeKind::Identifier {
|
||||
symbol,
|
||||
binding: IdentifierBinding::Declaration { addr, kind: decl_kind },
|
||||
} => {
|
||||
ctx.set_type(*addr, specialized_ty.clone());
|
||||
(
|
||||
NodeKind::Identifier {
|
||||
symbol: symbol.clone(),
|
||||
binding: IdentifierBinding::Declaration {
|
||||
addr: *addr,
|
||||
kind: *decl_kind,
|
||||
},
|
||||
},
|
||||
specialized_ty.clone(),
|
||||
)
|
||||
}
|
||||
NodeKind::Tuple { elements } => {
|
||||
return self.check_params_tuple(node, elements, specialized_ty, ctx, diag);
|
||||
}
|
||||
NodeKind::Nop => (NodeKind::Nop, StaticType::Void),
|
||||
NodeKind::Error => (NodeKind::Error, StaticType::Error),
|
||||
_ => {
|
||||
diag.push_error(
|
||||
"Invalid node in parameter list",
|
||||
Some(node.identity.clone()),
|
||||
);
|
||||
(NodeKind::Error, StaticType::Error)
|
||||
}
|
||||
};
|
||||
|
||||
Node {
|
||||
identity: node.identity.clone(),
|
||||
kind,
|
||||
ty,
|
||||
comments: node.comments.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_params_tuple<P: BoundLike>(
|
||||
&self,
|
||||
node: &Node<P>,
|
||||
elements: &[Rc<Node<P>>],
|
||||
specialized_ty: &StaticType,
|
||||
ctx: &mut TypeContext,
|
||||
diag: &mut Diagnostics,
|
||||
) -> TypedNode {
|
||||
match specialized_ty {
|
||||
StaticType::Any
|
||||
| StaticType::TypeVar(_) // TypeVar may resolve to any destructurable type
|
||||
| StaticType::Tuple(_)
|
||||
| StaticType::Vector(_, _)
|
||||
| StaticType::Matrix(_, _)
|
||||
| StaticType::List(_)
|
||||
| StaticType::Record(_)
|
||||
| StaticType::Error => {}
|
||||
_ => {
|
||||
diag.push_error(
|
||||
format!(
|
||||
"Cannot destructure type {} as a tuple/vector",
|
||||
specialized_ty.display_compact()
|
||||
),
|
||||
Some(node.identity.clone()),
|
||||
);
|
||||
return Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Error,
|
||||
ty: StaticType::Error,
|
||||
comments: node.comments.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut typed_elements = Vec::new();
|
||||
let mut elem_types = Vec::new();
|
||||
|
||||
for (i, el) in elements.iter().enumerate() {
|
||||
let sub_ty = match specialized_ty {
|
||||
StaticType::Tuple(t) => t.get(i).cloned().unwrap_or(StaticType::Any),
|
||||
StaticType::Vector(inner, _) => (**inner).clone(),
|
||||
StaticType::Matrix(inner, _) => (**inner).clone(),
|
||||
StaticType::List(inner) => (**inner).clone(),
|
||||
StaticType::Record(layout) => layout
|
||||
.fields
|
||||
.get(i)
|
||||
.map(|(_, ty): &(Keyword, StaticType)| ty.clone())
|
||||
.unwrap_or(StaticType::Any),
|
||||
StaticType::Error => StaticType::Error,
|
||||
_ => StaticType::Any,
|
||||
};
|
||||
let t = self.check_params(el.as_ref(), &sub_ty, ctx, diag);
|
||||
elem_types.push(t.ty.clone());
|
||||
typed_elements.push(Rc::new(t));
|
||||
}
|
||||
Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Tuple {
|
||||
elements: typed_elements,
|
||||
},
|
||||
ty: StaticType::Tuple(elem_types),
|
||||
comments: node.comments.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn check_node<P: BoundLike>(
|
||||
&self,
|
||||
node: &Node<P>,
|
||||
ctx: &mut TypeContext,
|
||||
diag: &mut Diagnostics,
|
||||
) -> TypedNode {
|
||||
let (kind, ty): (NodeKind<TypedPhase>, StaticType) = match &node.kind {
|
||||
NodeKind::Nop => (NodeKind::Nop, StaticType::Void),
|
||||
|
||||
NodeKind::Constant(v) => {
|
||||
let ty = v.static_type();
|
||||
(NodeKind::Constant(v.clone()), ty)
|
||||
}
|
||||
|
||||
NodeKind::Def {
|
||||
pattern,
|
||||
value,
|
||||
info,
|
||||
} => {
|
||||
let val_typed = self.check_node(value, ctx, diag);
|
||||
let ty = val_typed.ty.clone();
|
||||
|
||||
// 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 {
|
||||
binding: IdentifierBinding::Declaration { addr, .. },
|
||||
..
|
||||
} = &pattern.kind
|
||||
{
|
||||
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
|
||||
if let NodeKind::Tuple { .. } = &pattern.kind {
|
||||
let pat_typed = self.check_params(pattern.as_ref(), &val_typed.ty, ctx, diag);
|
||||
let ty = val_typed.ty.clone();
|
||||
return Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Def {
|
||||
pattern: Rc::new(pat_typed),
|
||||
value: Rc::new(val_typed),
|
||||
info: DefBinding {
|
||||
captured_by: info.captured_by.clone(),
|
||||
},
|
||||
},
|
||||
ty,
|
||||
comments: node.comments.clone(),
|
||||
};
|
||||
}
|
||||
|
||||
// Simple def — reconstruct the pattern node with the new type
|
||||
let new_pattern: TypedNode = Node {
|
||||
identity: pattern.identity.clone(),
|
||||
kind: match &pattern.kind {
|
||||
NodeKind::Identifier { symbol, binding } => NodeKind::Identifier {
|
||||
symbol: symbol.clone(),
|
||||
binding: match binding {
|
||||
IdentifierBinding::Declaration { addr, kind: decl_kind } => {
|
||||
IdentifierBinding::Declaration {
|
||||
addr: *addr,
|
||||
kind: *decl_kind,
|
||||
}
|
||||
}
|
||||
IdentifierBinding::Reference(addr) => {
|
||||
IdentifierBinding::Reference(*addr)
|
||||
}
|
||||
},
|
||||
},
|
||||
_ => NodeKind::Error,
|
||||
},
|
||||
ty: ty.clone(),
|
||||
comments: pattern.comments.clone(),
|
||||
};
|
||||
|
||||
(
|
||||
NodeKind::Def {
|
||||
pattern: Rc::new(new_pattern),
|
||||
value: Rc::new(val_typed),
|
||||
info: DefBinding {
|
||||
captured_by: info.captured_by.clone(),
|
||||
},
|
||||
},
|
||||
ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Identifier { symbol, binding } => {
|
||||
if let IdentifierBinding::Reference(addr) = binding {
|
||||
// Apply the current HM substitution so that TypeVars resolved in
|
||||
// nested scopes (e.g. inside a `while` body) are visible here even
|
||||
// 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.instantiate(ty);
|
||||
(
|
||||
NodeKind::Identifier {
|
||||
symbol: symbol.clone(),
|
||||
binding: IdentifierBinding::Reference(*addr),
|
||||
},
|
||||
ty,
|
||||
)
|
||||
} else if let IdentifierBinding::Declaration { addr, kind: decl_kind } = binding {
|
||||
let ty = Self::apply_subst(ctx.get_type(*addr), &self.subst.borrow());
|
||||
(
|
||||
NodeKind::Identifier {
|
||||
symbol: symbol.clone(),
|
||||
binding: IdentifierBinding::Declaration {
|
||||
addr: *addr,
|
||||
kind: *decl_kind,
|
||||
},
|
||||
},
|
||||
ty,
|
||||
)
|
||||
} else {
|
||||
(NodeKind::Error, StaticType::Error)
|
||||
}
|
||||
}
|
||||
|
||||
NodeKind::FieldAccessor(k) => {
|
||||
(NodeKind::FieldAccessor(*k), StaticType::FieldAccessor(*k))
|
||||
}
|
||||
|
||||
NodeKind::GetField { rec, field } => {
|
||||
let rec_typed = self.check_node(rec, ctx, diag);
|
||||
let field_ty = match &rec_typed.ty {
|
||||
StaticType::Record(layout) => {
|
||||
if let Some(idx) = layout.index_of(*field) {
|
||||
layout.fields[idx].1.clone()
|
||||
} else {
|
||||
diag.push_error(
|
||||
format!("Record does not have field :{}", field.name()),
|
||||
Some(rec_typed.identity.clone()),
|
||||
);
|
||||
StaticType::Error
|
||||
}
|
||||
}
|
||||
StaticType::Any => StaticType::Any,
|
||||
StaticType::Error => StaticType::Error,
|
||||
_ => {
|
||||
diag.push_error(
|
||||
format!(
|
||||
"Cannot access field :{} on non-record type {}",
|
||||
field.name(),
|
||||
rec_typed.ty.display_compact()
|
||||
),
|
||||
Some(rec_typed.identity.clone()),
|
||||
);
|
||||
StaticType::Error
|
||||
}
|
||||
};
|
||||
(
|
||||
NodeKind::GetField {
|
||||
rec: Rc::new(rec_typed),
|
||||
field: *field,
|
||||
},
|
||||
field_ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Assign { target, value, info } => {
|
||||
let val_typed = self.check_node(value, ctx, diag);
|
||||
let ty = val_typed.ty.clone();
|
||||
if let Some(addr) = info.addr {
|
||||
ctx.set_type(addr, ty.clone());
|
||||
}
|
||||
|
||||
// For destructuring assigns (addr = None), preserve the target pattern
|
||||
// so the VM can unpack values. For simple assigns, the target is unused.
|
||||
let target_typed = if info.addr.is_none() {
|
||||
Rc::new(self.check_node(target, ctx, diag))
|
||||
} else {
|
||||
Rc::new(Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Nop,
|
||||
ty: ty.clone(),
|
||||
comments: Rc::from([]),
|
||||
})
|
||||
};
|
||||
|
||||
(
|
||||
NodeKind::Assign {
|
||||
target: target_typed,
|
||||
value: Rc::new(val_typed),
|
||||
info: AssignBinding {
|
||||
addr: info.addr,
|
||||
},
|
||||
},
|
||||
ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::If {
|
||||
cond,
|
||||
then_br,
|
||||
else_br,
|
||||
} => {
|
||||
let cond_typed = self.check_node(cond, ctx, diag);
|
||||
let then_typed = self.check_node(then_br, ctx, diag);
|
||||
|
||||
let mut else_typed = None;
|
||||
let mut final_ty = then_typed.ty.clone();
|
||||
|
||||
if let Some(e) = else_br {
|
||||
let et = self.check_node(e, ctx, diag);
|
||||
if et.ty != final_ty {
|
||||
final_ty = Self::numeric_widen(&final_ty, &et.ty)
|
||||
.or_else(|| Self::record_promote(&final_ty, &et.ty))
|
||||
.unwrap_or(StaticType::Any);
|
||||
}
|
||||
else_typed = Some(Rc::new(et));
|
||||
} else {
|
||||
final_ty = StaticType::Optional(Box::new(then_typed.ty.clone()));
|
||||
}
|
||||
|
||||
(
|
||||
NodeKind::If {
|
||||
cond: Rc::new(cond_typed),
|
||||
then_br: Rc::new(then_typed),
|
||||
else_br: else_typed,
|
||||
},
|
||||
final_ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Block { exprs } => {
|
||||
let mut typed_exprs = Vec::new();
|
||||
let mut last_ty = StaticType::Void;
|
||||
|
||||
for e in exprs {
|
||||
let t = self.check_node(e, ctx, diag);
|
||||
last_ty = t.ty.clone();
|
||||
typed_exprs.push(Rc::new(t));
|
||||
}
|
||||
|
||||
(NodeKind::Block { exprs: typed_exprs }, last_ty)
|
||||
}
|
||||
|
||||
NodeKind::Lambda {
|
||||
params,
|
||||
body,
|
||||
info,
|
||||
} => {
|
||||
let upvalues = &info.upvalues;
|
||||
let positional_count = info.positional_count;
|
||||
|
||||
let mut upvalue_types = Vec::with_capacity(upvalues.len());
|
||||
for &addr in upvalues {
|
||||
upvalue_types.push(ctx.get_type(addr));
|
||||
}
|
||||
|
||||
let mut lambda_ctx =
|
||||
TypeContext::new(64, upvalue_types, ctx.root_types, Some(ctx));
|
||||
|
||||
// Generate a fresh TypeVar per positional parameter so that HM
|
||||
// constraint propagation works across nested closures.
|
||||
// `check_lambda_with_hints` (used at call sites) overrides these with
|
||||
// concrete types; this path only fires for lambdas typed as values
|
||||
// (e.g. returned from another lambda, stored in a def).
|
||||
let param_hint_ty = StaticType::Tuple(
|
||||
(0..positional_count.unwrap_or(0)).map(|_| self.fresh_var()).collect(),
|
||||
);
|
||||
let params_typed = self.check_params(
|
||||
params.as_ref(),
|
||||
¶m_hint_ty,
|
||||
&mut lambda_ctx,
|
||||
diag,
|
||||
);
|
||||
|
||||
lambda_ctx.current_params_ty = Some(params_typed.ty.clone());
|
||||
|
||||
let body_typed = self.check_node(body, &mut lambda_ctx, diag);
|
||||
let ret_ty = body_typed.ty.clone();
|
||||
|
||||
let fn_ty = StaticType::Function(Box::new(Signature {
|
||||
params: params_typed.ty.clone(),
|
||||
ret: ret_ty,
|
||||
}));
|
||||
|
||||
(
|
||||
NodeKind::Lambda {
|
||||
params: Rc::new(params_typed),
|
||||
body: Rc::new(body_typed),
|
||||
info: LambdaBinding {
|
||||
upvalues: upvalues.clone(),
|
||||
positional_count,
|
||||
},
|
||||
},
|
||||
fn_ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Call { callee, args } => {
|
||||
let callee_typed = self.check_node(callee, ctx, diag);
|
||||
|
||||
let args_typed = if let NodeKind::Tuple { elements } = &args.kind {
|
||||
let arg_count = elements.len();
|
||||
let mut typed_elements: Vec<Option<Rc<TypedNode>>> = vec![None; arg_count];
|
||||
let mut known_types: Vec<Option<StaticType>> = vec![None; arg_count];
|
||||
let mut lambda_indices = Vec::new();
|
||||
|
||||
// Phase 1: Type non-lambda arguments first
|
||||
for (i, e) in elements.iter().enumerate() {
|
||||
if matches!(e.kind, NodeKind::Lambda { .. }) {
|
||||
lambda_indices.push(i);
|
||||
} else {
|
||||
let t = self.check_node(e, ctx, diag);
|
||||
known_types[i] = Some(t.ty.clone());
|
||||
typed_elements[i] = Some(Rc::new(t));
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Type lambda arguments with parameter hints (if available)
|
||||
for i in lambda_indices {
|
||||
let hints = extract_lambda_param_hints(
|
||||
&callee_typed.ty,
|
||||
i,
|
||||
&known_types,
|
||||
);
|
||||
let t = if let Some(param_types) = hints {
|
||||
self.check_lambda_with_param_hints(
|
||||
&elements[i],
|
||||
¶m_types,
|
||||
ctx,
|
||||
diag,
|
||||
)
|
||||
} else {
|
||||
self.check_node(&elements[i], ctx, diag)
|
||||
};
|
||||
known_types[i] = Some(t.ty.clone());
|
||||
typed_elements[i] = Some(Rc::new(t));
|
||||
}
|
||||
|
||||
let final_elements: Vec<Rc<TypedNode>> = typed_elements
|
||||
.into_iter()
|
||||
.map(|e| e.expect("all args should be typed"))
|
||||
.collect();
|
||||
let elem_types: Vec<StaticType> =
|
||||
final_elements.iter().map(|e| e.ty.clone()).collect();
|
||||
|
||||
Node {
|
||||
identity: args.identity.clone(),
|
||||
kind: NodeKind::Tuple {
|
||||
elements: final_elements,
|
||||
},
|
||||
ty: StaticType::Tuple(elem_types),
|
||||
comments: args.comments.clone(),
|
||||
}
|
||||
} else {
|
||||
self.check_node(args, ctx, diag)
|
||||
};
|
||||
|
||||
let mut ret_ty = match callee_typed.ty.resolve_call(&args_typed.ty) {
|
||||
Some(ty) => ty,
|
||||
None => {
|
||||
let callee_name = match &callee_typed.kind {
|
||||
NodeKind::Identifier { symbol, .. } => format!("'{}'", symbol.name),
|
||||
_ => "function".to_string(),
|
||||
};
|
||||
diag.push_error(
|
||||
format!(
|
||||
"{}: no matching overload for ({})",
|
||||
callee_name, args_typed.ty.display_compact()
|
||||
),
|
||||
Some(node.identity.clone()),
|
||||
);
|
||||
StaticType::Error
|
||||
}
|
||||
};
|
||||
|
||||
// HM: propagate TypeVar constraints through overloaded calls so that
|
||||
// e.g. `(+ Float TypeVar(1))` resolves TypeVar(1) = Float.
|
||||
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 existing = self.index_constraints.borrow().get(n).copied();
|
||||
if let Some(existing_ret_id) = existing {
|
||||
// Same TypeVar indexed again — all index results on the same
|
||||
// parameter must share one element TypeVar. Unify the fresh
|
||||
// var with the existing one so they resolve together.
|
||||
let elem_var = self.fresh_var();
|
||||
self.unify(elem_var.clone(), StaticType::TypeVar(existing_ret_id), diag);
|
||||
ret_ty = Self::apply_subst(elem_var, &self.subst.borrow());
|
||||
} else {
|
||||
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 compiler hooks registered by the RTL (keyed by global slot index).
|
||||
// Hooks handle type-inference extensions such as:
|
||||
// - series: inject a fresh TypeVar for the element type
|
||||
// - push: unify the series element TypeVar with the pushed value type
|
||||
if let NodeKind::Identifier {
|
||||
binding: IdentifierBinding::Reference(Address::Global(idx)),
|
||||
..
|
||||
} = &callee_typed.kind
|
||||
&& let Some(hook) = self.compiler_hooks.get(&idx.0) {
|
||||
let hook_ctx = CheckerInferenceAccess { checker: self, ctx };
|
||||
ret_ty = hook.post_call(&args_typed, ret_ty, &hook_ctx, diag);
|
||||
}
|
||||
|
||||
(
|
||||
NodeKind::Call {
|
||||
callee: Rc::new(callee_typed),
|
||||
args: Rc::new(args_typed),
|
||||
},
|
||||
ret_ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Again { args } => {
|
||||
let args_typed = if let NodeKind::Tuple { elements } = &args.kind {
|
||||
let mut typed_elements = Vec::new();
|
||||
let mut elem_types = Vec::new();
|
||||
for e in elements {
|
||||
let t = self.check_node(e, ctx, diag);
|
||||
elem_types.push(t.ty.clone());
|
||||
typed_elements.push(Rc::new(t));
|
||||
}
|
||||
Node {
|
||||
identity: args.identity.clone(),
|
||||
kind: NodeKind::Tuple {
|
||||
elements: typed_elements,
|
||||
},
|
||||
ty: StaticType::Tuple(elem_types),
|
||||
comments: args.comments.clone(),
|
||||
}
|
||||
} else {
|
||||
self.check_node(args, ctx, diag)
|
||||
};
|
||||
|
||||
if let Some(expected_ty) = &ctx.current_params_ty {
|
||||
self.unify(expected_ty.clone(), args_typed.ty.clone(), diag);
|
||||
}
|
||||
|
||||
(
|
||||
NodeKind::Again {
|
||||
args: Rc::new(args_typed),
|
||||
},
|
||||
StaticType::Any,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Tuple { elements } => {
|
||||
let mut typed_elements = Vec::new();
|
||||
for e in elements {
|
||||
typed_elements.push(Rc::new(self.check_node(e, ctx, diag)));
|
||||
}
|
||||
|
||||
let ty = if typed_elements.is_empty() {
|
||||
StaticType::Vector(Box::new(StaticType::Any), 0)
|
||||
} else {
|
||||
let first_ty = &typed_elements[0].ty;
|
||||
let all_same = typed_elements.iter().all(|e| e.ty == *first_ty);
|
||||
|
||||
if all_same {
|
||||
match first_ty {
|
||||
StaticType::Vector(inner, len) => {
|
||||
StaticType::Matrix(inner.clone(), vec![typed_elements.len(), *len])
|
||||
}
|
||||
StaticType::Matrix(inner, shape) => {
|
||||
let mut new_shape = vec![typed_elements.len()];
|
||||
new_shape.extend(shape);
|
||||
StaticType::Matrix(inner.clone(), new_shape)
|
||||
}
|
||||
_ => {
|
||||
StaticType::Vector(Box::new(first_ty.clone()), typed_elements.len())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
StaticType::Tuple(typed_elements.iter().map(|e| e.ty.clone()).collect())
|
||||
}
|
||||
};
|
||||
|
||||
(
|
||||
NodeKind::Tuple {
|
||||
elements: typed_elements,
|
||||
},
|
||||
ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Record { fields, layout } => {
|
||||
let mut typed_fields = Vec::with_capacity(fields.len());
|
||||
let mut fields_ty = Vec::with_capacity(fields.len());
|
||||
|
||||
for (i, (key_node, val_node)) in fields.iter().enumerate() {
|
||||
let kt = self.check_node(key_node, ctx, diag);
|
||||
let vt = self.check_node(val_node, ctx, diag);
|
||||
fields_ty.push((layout.fields[i].0, vt.ty.clone()));
|
||||
typed_fields.push((Rc::new(kt), Rc::new(vt)));
|
||||
}
|
||||
|
||||
let new_layout = RecordLayout::get_or_create(fields_ty);
|
||||
(
|
||||
NodeKind::Record {
|
||||
fields: typed_fields,
|
||||
layout: new_layout.clone(),
|
||||
},
|
||||
StaticType::Record(new_layout),
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Expansion {
|
||||
original_call,
|
||||
expanded,
|
||||
} => {
|
||||
let expanded_typed = self.check_node(expanded, ctx, diag);
|
||||
let ty = expanded_typed.ty.clone();
|
||||
(
|
||||
NodeKind::Expansion {
|
||||
original_call: original_call.clone(),
|
||||
expanded: Rc::new(expanded_typed),
|
||||
},
|
||||
ty,
|
||||
)
|
||||
}
|
||||
|
||||
NodeKind::Extension(_ext) => {
|
||||
diag.push_error(
|
||||
format!(
|
||||
"TypeChecking for extension '{}' not implemented",
|
||||
_ext.display_name()
|
||||
),
|
||||
Some(node.identity.clone()),
|
||||
);
|
||||
(NodeKind::Error, StaticType::Error)
|
||||
}
|
||||
NodeKind::Error => (NodeKind::Error, StaticType::Error),
|
||||
|
||||
// Syntax-only variants should not appear in bound phases
|
||||
NodeKind::MacroDecl { .. }
|
||||
| NodeKind::Template(_)
|
||||
| NodeKind::Placeholder(_)
|
||||
| NodeKind::Splice(_) => {
|
||||
diag.push_error(
|
||||
"Unexpected syntax-only node in type checking",
|
||||
Some(node.identity.clone()),
|
||||
);
|
||||
(NodeKind::Error, StaticType::Error)
|
||||
}
|
||||
};
|
||||
|
||||
Node {
|
||||
identity: node.identity.clone(),
|
||||
kind,
|
||||
ty,
|
||||
comments: node.comments.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
use crate::ast::compiler::call_hooks::InferenceAccess;
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::nodes::{Address, VirtualId};
|
||||
use crate::ast::types::{Signature, StaticType};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::TypeChecker;
|
||||
|
||||
/// Manages the types of locals and upvalues during a single type-checking pass.
|
||||
pub(super) struct TypeContext<'a> {
|
||||
pub(super) _parent: Option<&'a TypeContext<'a>>,
|
||||
/// Maps slot index -> Inferred Type
|
||||
pub(super) slots: HashMap<u32, StaticType>,
|
||||
/// Types of captured variables (passed from outer scope)
|
||||
pub(super) upvalue_types: Vec<StaticType>,
|
||||
/// Access to root types for unified resolution
|
||||
pub(super) root_types: &'a std::cell::RefCell<Vec<StaticType>>,
|
||||
/// The expected parameters of the current function (for 'again' validation)
|
||||
pub(super) current_params_ty: Option<StaticType>,
|
||||
}
|
||||
|
||||
impl<'a> TypeContext<'a> {
|
||||
pub(super) fn new(
|
||||
_slot_count: u32,
|
||||
upvalue_types: Vec<StaticType>,
|
||||
root_types: &'a std::cell::RefCell<Vec<StaticType>>,
|
||||
parent: Option<&'a TypeContext<'a>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
_parent: parent,
|
||||
slots: HashMap::new(),
|
||||
upvalue_types,
|
||||
root_types,
|
||||
current_params_ty: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn get_type(&self, addr: Address<VirtualId>) -> StaticType {
|
||||
match addr {
|
||||
Address::Local(slot) => self
|
||||
.slots
|
||||
.get(&slot.0)
|
||||
.cloned()
|
||||
.unwrap_or(StaticType::Any),
|
||||
Address::Global(idx) => self
|
||||
.root_types
|
||||
.borrow()
|
||||
.get(idx.0 as usize)
|
||||
.cloned()
|
||||
.unwrap_or(StaticType::Any),
|
||||
Address::Upvalue(idx) => self
|
||||
.upvalue_types
|
||||
.get(idx.0 as usize)
|
||||
.cloned()
|
||||
.unwrap_or(StaticType::Any),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_type(&mut self, addr: Address<VirtualId>, ty: StaticType) {
|
||||
match addr {
|
||||
Address::Local(slot) => {
|
||||
self.slots.insert(slot.0, ty);
|
||||
}
|
||||
Address::Global(idx) => {
|
||||
let mut rt = self.root_types.borrow_mut();
|
||||
if (idx.0 as usize) < rt.len() {
|
||||
rt[idx.0 as usize] = ty;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Temporary wrapper that gives call-hooks unified access to both the
|
||||
/// type-checker's inference state and the current scope's slot types.
|
||||
/// Created on the stack at each hook dispatch site; zero allocation cost.
|
||||
pub(super) struct CheckerInferenceAccess<'a, 'b> {
|
||||
pub(super) checker: &'a TypeChecker,
|
||||
pub(super) ctx: &'b TypeContext<'a>,
|
||||
}
|
||||
|
||||
impl InferenceAccess for CheckerInferenceAccess<'_, '_> {
|
||||
fn fresh_var(&self) -> StaticType {
|
||||
self.checker.fresh_var()
|
||||
}
|
||||
|
||||
fn unify(&self, a: StaticType, b: StaticType, diag: &mut Diagnostics) {
|
||||
self.checker.unify(a, b, diag);
|
||||
}
|
||||
|
||||
fn bind_typevar(&self, id: u32, ty: StaticType) {
|
||||
self.checker.bind_var(id, ty);
|
||||
}
|
||||
|
||||
fn apply_subst_ty(&self, ty: StaticType) -> StaticType {
|
||||
TypeChecker::apply_subst(ty, &self.checker.subst.borrow())
|
||||
}
|
||||
|
||||
fn try_numeric_widen(&self, a: &StaticType, b: &StaticType) -> Option<StaticType> {
|
||||
TypeChecker::numeric_widen(a, b)
|
||||
}
|
||||
|
||||
fn try_record_promote(&self, a: &StaticType, b: &StaticType) -> Option<StaticType> {
|
||||
TypeChecker::record_promote(a, b)
|
||||
}
|
||||
|
||||
fn get_slot_type(&self, addr: Address<VirtualId>) -> StaticType {
|
||||
self.ctx.get_type(addr)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts expected lambda parameter types from the callee type for a specific argument position.
|
||||
/// Used for bidirectional type inference: when a Call's callee expects a function at `arg_index`,
|
||||
/// this returns the expected parameter types for that function, derived from the callee's signature
|
||||
/// and the already-known types of non-lambda arguments.
|
||||
pub(super) fn extract_lambda_param_hints(
|
||||
callee_ty: &StaticType,
|
||||
arg_index: usize,
|
||||
known_arg_types: &[Option<StaticType>],
|
||||
) -> Option<Vec<StaticType>> {
|
||||
/// Helper: given the expected type at a specific parameter position in a signature,
|
||||
/// extract the lambda parameter types if it expects a function.
|
||||
fn hints_from_param_type(param_ty: &StaticType) -> Option<Vec<StaticType>> {
|
||||
if let StaticType::Function(sig) = param_ty {
|
||||
if let StaticType::Tuple(params) = &sig.params {
|
||||
return Some(params.clone());
|
||||
}
|
||||
return Some(vec![sig.params.clone()]);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Helper: extract the expected type at `arg_index` from a signature's params tuple.
|
||||
fn param_at(sig: &Signature, arg_index: usize) -> Option<&StaticType> {
|
||||
if let StaticType::Tuple(params) = &sig.params {
|
||||
params.get(arg_index)
|
||||
} else if arg_index == 0 {
|
||||
Some(&sig.params)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
match callee_ty {
|
||||
StaticType::Function(sig) => {
|
||||
let expected = param_at(sig, arg_index)?;
|
||||
hints_from_param_type(expected)
|
||||
}
|
||||
StaticType::FunctionOverloads(sigs) => {
|
||||
// Try each overload — return hints from the first one that has a function at this position
|
||||
for sig in sigs {
|
||||
if let Some(expected) = param_at(sig, arg_index)
|
||||
&& let Some(hints) = hints_from_param_type(expected)
|
||||
{
|
||||
return Some(hints);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
StaticType::PolymorphicFn {
|
||||
resolve_arg_hints: Some(resolver),
|
||||
..
|
||||
} => resolver(arg_index, known_arg_types),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
use crate::ast::nodes::{
|
||||
Address, IdentifierBinding, Node, NodeKind, TypedNode, TypedPhase,
|
||||
};
|
||||
use crate::ast::types::{NodeIdentity, StaticType};
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use super::TypeChecker;
|
||||
|
||||
impl TypeChecker {
|
||||
/// Applies the final HM substitution to all type annotations in the tree and
|
||||
/// elaborates `(series n)` calls with their inferred schema arguments.
|
||||
/// Must be called after `check()` or `check_node_as_bound()` completes.
|
||||
pub fn finalize(&self, node: TypedNode) -> TypedNode {
|
||||
let subst = self.subst.borrow().clone();
|
||||
self.finalize_node(node, &subst)
|
||||
}
|
||||
|
||||
/// Walks the typed AST, applies the HM substitution to every type annotation,
|
||||
/// and dispatches finalization hooks (e.g. schema injection for `series`).
|
||||
fn finalize_node(&self, node: TypedNode, subst: &HashMap<u32, StaticType>) -> TypedNode {
|
||||
let new_ty = Self::apply_subst(node.ty, subst);
|
||||
let new_kind = self.finalize_kind(node.kind, subst, &new_ty, &node.identity);
|
||||
Node { kind: new_kind, ty: new_ty, identity: node.identity, comments: node.comments }
|
||||
}
|
||||
|
||||
fn finalize_kind(
|
||||
&self,
|
||||
kind: NodeKind<TypedPhase>,
|
||||
subst: &HashMap<u32, StaticType>,
|
||||
node_ty: &StaticType,
|
||||
_identity: &Rc<NodeIdentity>,
|
||||
) -> NodeKind<TypedPhase> {
|
||||
match kind {
|
||||
// Leaf nodes — nothing to recurse into
|
||||
NodeKind::Nop => NodeKind::Nop,
|
||||
NodeKind::Constant(v) => NodeKind::Constant(v),
|
||||
NodeKind::FieldAccessor(k) => NodeKind::FieldAccessor(k),
|
||||
NodeKind::Error => NodeKind::Error,
|
||||
NodeKind::Identifier { symbol, binding } => NodeKind::Identifier { symbol, binding },
|
||||
NodeKind::Extension(ext) => NodeKind::Extension(ext),
|
||||
|
||||
// Call: dispatch finalize hook registered by RTL (keyed by global slot index).
|
||||
// Hooks may rewrite the call node — e.g. series injects a schema argument.
|
||||
NodeKind::Call { callee, args } => {
|
||||
let callee_fin = Rc::new(self.finalize_node((*callee).clone(), subst));
|
||||
let args_fin = Rc::new(self.finalize_node((*args).clone(), subst));
|
||||
if let NodeKind::Identifier {
|
||||
binding: IdentifierBinding::Reference(Address::Global(idx)),
|
||||
..
|
||||
} = &callee_fin.kind
|
||||
&& let Some(hook) = self.compiler_hooks.get(&idx.0)
|
||||
&& let Some(new_kind) = hook.finalize(
|
||||
Rc::clone(&callee_fin),
|
||||
Rc::clone(&args_fin),
|
||||
node_ty,
|
||||
subst,
|
||||
) {
|
||||
return new_kind;
|
||||
}
|
||||
NodeKind::Call { callee: callee_fin, args: args_fin }
|
||||
}
|
||||
|
||||
NodeKind::If { cond, then_br, else_br } => NodeKind::If {
|
||||
cond: Rc::new(self.finalize_node((*cond).clone(), subst)),
|
||||
then_br: Rc::new(self.finalize_node((*then_br).clone(), subst)),
|
||||
else_br: else_br.map(|e| Rc::new(self.finalize_node((*e).clone(), subst))),
|
||||
},
|
||||
NodeKind::Def { pattern, value, info } => NodeKind::Def {
|
||||
pattern: Rc::new(self.finalize_node((*pattern).clone(), subst)),
|
||||
value: Rc::new(self.finalize_node((*value).clone(), subst)),
|
||||
info,
|
||||
},
|
||||
NodeKind::Assign { target, value, info } => NodeKind::Assign {
|
||||
target: Rc::new(self.finalize_node((*target).clone(), subst)),
|
||||
value: Rc::new(self.finalize_node((*value).clone(), subst)),
|
||||
info,
|
||||
},
|
||||
NodeKind::Lambda { params, body, info } => NodeKind::Lambda {
|
||||
params: Rc::new(self.finalize_node((*params).clone(), subst)),
|
||||
body: Rc::new(self.finalize_node((*body).clone(), subst)),
|
||||
info,
|
||||
},
|
||||
NodeKind::Again { args } => NodeKind::Again {
|
||||
args: Rc::new(self.finalize_node((*args).clone(), subst)),
|
||||
},
|
||||
NodeKind::Block { exprs } => NodeKind::Block {
|
||||
exprs: exprs
|
||||
.into_iter()
|
||||
.map(|e| Rc::new(self.finalize_node((*e).clone(), subst)))
|
||||
.collect(),
|
||||
},
|
||||
NodeKind::Tuple { elements } => NodeKind::Tuple {
|
||||
elements: elements
|
||||
.into_iter()
|
||||
.map(|e| Rc::new(self.finalize_node((*e).clone(), subst)))
|
||||
.collect(),
|
||||
},
|
||||
NodeKind::Record { fields, layout } => NodeKind::Record {
|
||||
fields: fields
|
||||
.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
Rc::new(self.finalize_node((*k).clone(), subst)),
|
||||
Rc::new(self.finalize_node((*v).clone(), subst)),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
layout,
|
||||
},
|
||||
NodeKind::GetField { rec, field } => NodeKind::GetField {
|
||||
rec: Rc::new(self.finalize_node((*rec).clone(), subst)),
|
||||
field,
|
||||
},
|
||||
NodeKind::Expansion { original_call, expanded } => NodeKind::Expansion {
|
||||
original_call,
|
||||
expanded: Rc::new(self.finalize_node((*expanded).clone(), subst)),
|
||||
},
|
||||
NodeKind::MacroDecl { name, params, body } => NodeKind::MacroDecl {
|
||||
name,
|
||||
params: Rc::new(self.finalize_node((*params).clone(), subst)),
|
||||
body: Rc::new(self.finalize_node((*body).clone(), subst)),
|
||||
},
|
||||
NodeKind::Template(inner) => {
|
||||
NodeKind::Template(Rc::new(self.finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
NodeKind::Placeholder(inner) => {
|
||||
NodeKind::Placeholder(Rc::new(self.finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
NodeKind::Splice(inner) => {
|
||||
NodeKind::Splice(Rc::new(self.finalize_node((*inner).clone(), subst)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::types::{RecordLayout, Signature, StaticType};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::context::TypeContext;
|
||||
use super::TypeChecker;
|
||||
|
||||
impl TypeChecker {
|
||||
/// Creates a fresh, unique type variable.
|
||||
pub(super) fn fresh_var(&self) -> StaticType {
|
||||
let id = self.var_counter.get();
|
||||
self.var_counter.set(id + 1);
|
||||
StaticType::TypeVar(id)
|
||||
}
|
||||
|
||||
/// Binds a TypeVar to a concrete type in the substitution, then propagates
|
||||
/// any pending index-call constraints registered for the series lookback pattern.
|
||||
///
|
||||
/// When `index_constraints[id]` is set and `ty` is a `Series`, the result TypeVar
|
||||
/// is bound to the element type — connecting the callee TypeVar to its element type
|
||||
/// without the eager over-constraint of unification. Non-indexable types leave the
|
||||
/// result TypeVar unresolved.
|
||||
pub(super) 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), StaticType::Series(elem)) = (ret_id, &ty) {
|
||||
self.subst.borrow_mut().insert(ret_id, *elem.clone());
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively applies the substitution map to a type, replacing all
|
||||
/// resolved `TypeVar`s with their concrete types.
|
||||
pub(super) fn apply_subst(ty: StaticType, subst: &HashMap<u32, StaticType>) -> StaticType {
|
||||
match ty {
|
||||
StaticType::TypeVar(n) => {
|
||||
if let Some(resolved) = subst.get(&n) {
|
||||
// Follow the chain (handles transitive substitutions)
|
||||
Self::apply_subst(resolved.clone(), subst)
|
||||
} else {
|
||||
StaticType::TypeVar(n)
|
||||
}
|
||||
}
|
||||
StaticType::Series(inner) => {
|
||||
StaticType::Series(Box::new(Self::apply_subst(*inner, subst)))
|
||||
}
|
||||
StaticType::Stream(inner) => {
|
||||
StaticType::Stream(Box::new(Self::apply_subst(*inner, subst)))
|
||||
}
|
||||
StaticType::Optional(inner) => {
|
||||
StaticType::Optional(Box::new(Self::apply_subst(*inner, subst)))
|
||||
}
|
||||
StaticType::Function(sig) => StaticType::Function(Box::new(Signature {
|
||||
params: Self::apply_subst(sig.params, subst),
|
||||
ret: Self::apply_subst(sig.ret, subst),
|
||||
})),
|
||||
StaticType::Tuple(elems) => {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `TypeVar(var_id)` appears anywhere in `ty` under the
|
||||
/// current substitution. Used to prevent infinite types (occurs check).
|
||||
fn occurs(var_id: u32, ty: &StaticType, subst: &HashMap<u32, StaticType>) -> bool {
|
||||
match ty {
|
||||
StaticType::TypeVar(n) => {
|
||||
if *n == var_id {
|
||||
return true;
|
||||
}
|
||||
// Follow chain in substitution
|
||||
if let Some(resolved) = subst.get(n) {
|
||||
Self::occurs(var_id, resolved, subst)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
StaticType::Series(inner)
|
||||
| StaticType::Stream(inner)
|
||||
| StaticType::Optional(inner) => Self::occurs(var_id, inner, subst),
|
||||
StaticType::Function(sig) => {
|
||||
Self::occurs(var_id, &sig.params, subst)
|
||||
|| Self::occurs(var_id, &sig.ret, 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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Unifies two types under the current substitution.
|
||||
/// 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.
|
||||
pub(super) fn unify(&self, ty1: StaticType, ty2: StaticType, diag: &mut Diagnostics) {
|
||||
let subst = self.subst.borrow_mut();
|
||||
let ty1 = Self::apply_subst(ty1, &subst);
|
||||
let ty2 = Self::apply_subst(ty2, &subst);
|
||||
|
||||
match (ty1, ty2) {
|
||||
(a, b) if a == b => {}
|
||||
(StaticType::TypeVar(n), ty) | (ty, StaticType::TypeVar(n)) => {
|
||||
if Self::occurs(n, &ty, &subst) {
|
||||
diag.push_error(format!("Infinite type: ?{} = {}", n, ty.display_compact()), None);
|
||||
return;
|
||||
}
|
||||
// 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)) => {
|
||||
drop(subst);
|
||||
self.unify(*a, *b, diag);
|
||||
}
|
||||
(StaticType::Stream(a), StaticType::Stream(b)) => {
|
||||
drop(subst);
|
||||
self.unify(*a, *b, diag);
|
||||
}
|
||||
(StaticType::Optional(a), StaticType::Optional(b)) => {
|
||||
drop(subst);
|
||||
self.unify(*a, *b, diag);
|
||||
}
|
||||
// Unify element-wise so that overload resolution can propagate TypeVar
|
||||
// constraints: e.g. `(+ Float TypeVar(1))` matched against `(Float, Float)`
|
||||
// triggers `unify(Float, TypeVar(1))` → `subst[1] = Float`.
|
||||
(StaticType::Tuple(a), StaticType::Tuple(b)) => {
|
||||
drop(subst);
|
||||
for (ta, tb) in a.into_iter().zip(b.into_iter()) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// Variadic(T) unifies with a Tuple by unifying each element with T.
|
||||
(StaticType::Variadic(inner), StaticType::Tuple(elems))
|
||||
| (StaticType::Tuple(elems), StaticType::Variadic(inner)) => {
|
||||
drop(subst);
|
||||
for e in elems {
|
||||
self.unify(e, (*inner).clone(), diag);
|
||||
}
|
||||
}
|
||||
// Any and Error are already handled by is_assignable_from — silently succeed
|
||||
(StaticType::Any, _) | (_, StaticType::Any) => {}
|
||||
(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) => {
|
||||
diag.push_error(format!("Type mismatch: expected {}, got {}", a.display_compact(), b.display_compact()), None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// After a `FunctionOverloads` call resolves successfully, unify the matched
|
||||
/// overload's concrete parameter types with the actual argument types.
|
||||
///
|
||||
/// This propagates TypeVar constraints through overloaded calls — for example,
|
||||
/// `(+ Float TypeVar(1))` matched against `(Float, Float) → Float` binds
|
||||
/// `TypeVar(1) = Float` so that nested closures can resolve series element types.
|
||||
///
|
||||
/// The "concrete anchor" guard ensures we only unify when at least one argument
|
||||
/// is a known concrete type. Without it, `(+ TypeVar TypeVar)` would
|
||||
/// spuriously pick the first overload (e.g. Int) and bind both TypeVars to Int.
|
||||
pub(super) fn unify_matched_overload(
|
||||
&self,
|
||||
callee_ty: &StaticType,
|
||||
args_ty: &StaticType,
|
||||
diag: &mut Diagnostics,
|
||||
) {
|
||||
let StaticType::FunctionOverloads(sigs) = callee_ty else { return };
|
||||
// Require both a concrete anchor (to pin the overload choice) and at
|
||||
// least one TypeVar (something to actually bind). Pure concrete calls
|
||||
// like `(- DateTime DateTime)` have nothing to unify and would
|
||||
// incorrectly trigger errors from coercion-only compatible types.
|
||||
if !Self::has_concrete_component(args_ty) || !Self::has_typevar_component(args_ty) {
|
||||
return;
|
||||
}
|
||||
// Only unify when EXACTLY ONE non-variadic overload matches.
|
||||
// If multiple overloads match (e.g. `(* TypeVar Int)` matches both
|
||||
// `(Int,Int)→Int` and `(Float,Float)→Float`), the choice is ambiguous
|
||||
// and binding the TypeVar to the first match would be incorrect.
|
||||
let is_variadic = |sig: &Signature| matches!(sig.params, StaticType::Any | StaticType::Variadic(_));
|
||||
let mut unique_match: Option<&Signature> = None;
|
||||
for sig in sigs {
|
||||
if !is_variadic(sig) && sig.params.is_assignable_from(args_ty) {
|
||||
if unique_match.is_some() {
|
||||
return; // Ambiguous — more than one specific overload matches
|
||||
}
|
||||
unique_match = Some(sig);
|
||||
}
|
||||
}
|
||||
if let Some(sig) = unique_match {
|
||||
self.unify(sig.params.clone(), args_ty.clone(), diag);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `ty` (or any element of a Tuple) is a concrete type —
|
||||
/// i.e. not `Any`, `TypeVar`, or `Error`.
|
||||
pub(super) fn has_concrete_component(ty: &StaticType) -> bool {
|
||||
match ty {
|
||||
StaticType::Tuple(elems) => elems.iter().any(Self::is_concrete),
|
||||
other => Self::is_concrete(other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `ty` contains a TypeVar anywhere in its structure.
|
||||
pub(super) fn has_typevar_component(ty: &StaticType) -> bool {
|
||||
match ty {
|
||||
StaticType::TypeVar(_) => true,
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_concrete(ty: &StaticType) -> bool {
|
||||
!matches!(ty, StaticType::Any | StaticType::TypeVar(_) | StaticType::Error)
|
||||
}
|
||||
|
||||
/// Returns the widened numeric type when one side is `Int` and the other `Float`.
|
||||
/// This is the only implicit numeric promotion in Myc: Int is a subtype of Float.
|
||||
pub(super) fn numeric_widen(a: &StaticType, b: &StaticType) -> Option<StaticType> {
|
||||
match (a, b) {
|
||||
(StaticType::Int, StaticType::Float) | (StaticType::Float, StaticType::Int) => {
|
||||
Some(StaticType::Float)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// If both types are Records with the same field names and compatible types
|
||||
/// (same type or Int→Float widening), returns the promoted Record type.
|
||||
pub(super) fn record_promote(a: &StaticType, b: &StaticType) -> Option<StaticType> {
|
||||
let (StaticType::Record(la), StaticType::Record(lb)) = (a, b) else {
|
||||
return None;
|
||||
};
|
||||
if la == lb {
|
||||
return None;
|
||||
}
|
||||
if la.fields.len() != lb.fields.len() {
|
||||
return None;
|
||||
}
|
||||
let mut promoted_fields = Vec::with_capacity(la.fields.len());
|
||||
for ((ka, ta), (kb, tb)) in la.fields.iter().zip(lb.fields.iter()) {
|
||||
if ka != kb {
|
||||
return None;
|
||||
}
|
||||
let t = if ta == tb {
|
||||
ta.clone()
|
||||
} else if let Some(w) = Self::numeric_widen(ta, tb) {
|
||||
w
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
promoted_fields.push((*ka, t));
|
||||
}
|
||||
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.
|
||||
pub(super) 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.
|
||||
pub(super) 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
mod context;
|
||||
mod inference;
|
||||
mod finalize;
|
||||
mod check;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use crate::ast::compiler::call_hooks::RtlCompilerHook;
|
||||
use crate::ast::diagnostics::Diagnostics;
|
||||
use crate::ast::nodes::{
|
||||
BoundLike, BoundPhase, LambdaBinding,
|
||||
Node, NodeKind, TypedNode,
|
||||
};
|
||||
use crate::ast::types::{Signature, StaticType};
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use context::TypeContext;
|
||||
|
||||
pub struct TypeChecker {
|
||||
pub(crate) root_types: Rc<std::cell::RefCell<Vec<StaticType>>>,
|
||||
/// Monotonic counter for generating unique type variable IDs.
|
||||
pub(crate) var_counter: Cell<u32>,
|
||||
/// Global substitution map: TypeVar ID → resolved StaticType.
|
||||
/// Shared across all scopes within a single type-checking pass.
|
||||
pub(crate) 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`: when the callee TypeVar is bound to a concrete type,
|
||||
/// the element type is extracted directly from the `Series` variant.
|
||||
pub(crate) index_constraints: std::cell::RefCell<HashMap<u32, u32>>,
|
||||
/// Compiler hooks keyed by global slot index.
|
||||
/// Populated from the frozen RTL snapshot; empty for non-RTL call sites.
|
||||
pub(crate) compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>,
|
||||
}
|
||||
|
||||
impl TypeChecker {
|
||||
pub fn new(
|
||||
root_types: Rc<std::cell::RefCell<Vec<StaticType>>>,
|
||||
compiler_hooks: Rc<HashMap<u32, Rc<dyn RtlCompilerHook>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
root_types,
|
||||
var_counter: Cell::new(0),
|
||||
subst: std::cell::RefCell::new(HashMap::new()),
|
||||
index_constraints: std::cell::RefCell::new(HashMap::new()),
|
||||
compiler_hooks,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check(
|
||||
&self,
|
||||
node: &Node<BoundPhase>,
|
||||
arg_types: &[StaticType],
|
||||
diag: &mut Diagnostics,
|
||||
) -> TypedNode {
|
||||
let typed = self.check_node_as_bound(node, arg_types, diag);
|
||||
self.finalize(typed)
|
||||
}
|
||||
|
||||
/// Allows re-checking a node from any phase as if it were a bound node.
|
||||
/// This is useful for specialization where we re-type a TypedNode with more specific info.
|
||||
pub fn check_node_as_bound<P: BoundLike>(
|
||||
&self,
|
||||
node: &Node<P>,
|
||||
arg_types: &[StaticType],
|
||||
diag: &mut Diagnostics,
|
||||
) -> TypedNode {
|
||||
match &node.kind {
|
||||
NodeKind::Lambda {
|
||||
params,
|
||||
body,
|
||||
info,
|
||||
} => {
|
||||
let upvalues = &info.upvalues;
|
||||
let positional_count = info.positional_count;
|
||||
|
||||
let mut upvalue_types = Vec::with_capacity(upvalues.len());
|
||||
for &_addr in upvalues {
|
||||
upvalue_types.push(StaticType::Any);
|
||||
}
|
||||
|
||||
let root_ctx = TypeContext::new(0, vec![], &self.root_types, None);
|
||||
let mut lambda_ctx =
|
||||
TypeContext::new(64, upvalue_types, &self.root_types, Some(&root_ctx));
|
||||
|
||||
let arg_tuple_ty = if arg_types.is_empty() {
|
||||
StaticType::Any
|
||||
} else {
|
||||
StaticType::Tuple(arg_types.to_vec())
|
||||
};
|
||||
|
||||
let params_typed = self.check_params(
|
||||
params.as_ref(),
|
||||
&arg_tuple_ty,
|
||||
&mut lambda_ctx,
|
||||
diag,
|
||||
);
|
||||
|
||||
let body_typed = self.check_node(body, &mut lambda_ctx, diag);
|
||||
let ret_ty = body_typed.ty.clone();
|
||||
let final_params_ty = params_typed.ty.clone();
|
||||
|
||||
let fn_ty = StaticType::Function(Box::new(Signature {
|
||||
params: final_params_ty,
|
||||
ret: ret_ty,
|
||||
}));
|
||||
|
||||
Node {
|
||||
identity: node.identity.clone(),
|
||||
kind: NodeKind::Lambda {
|
||||
params: Rc::new(params_typed),
|
||||
body: Rc::new(body_typed),
|
||||
info: LambdaBinding {
|
||||
upvalues: upvalues.clone(),
|
||||
positional_count,
|
||||
},
|
||||
},
|
||||
ty: fn_ty,
|
||||
comments: node.comments.clone(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let mut root_ctx = TypeContext::new(0, vec![], &self.root_types, None);
|
||||
self.check_node(node, &mut root_ctx, diag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
use super::*;
|
||||
use crate::ast::environment::Environment;
|
||||
use crate::ast::types::StaticType;
|
||||
|
||||
fn check_source(source: &str) -> TypedNode {
|
||||
let env = Environment::new();
|
||||
env.compile(source).into_result().unwrap()
|
||||
}
|
||||
|
||||
fn get_ret_type(node: &TypedNode) -> StaticType {
|
||||
if let StaticType::Function(sig) = &node.ty {
|
||||
sig.ret.clone()
|
||||
} else {
|
||||
node.ty.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_constants() {
|
||||
assert_eq!(get_ret_type(&check_source("10")), StaticType::Int);
|
||||
assert_eq!(get_ret_type(&check_source("10.5")), StaticType::Float);
|
||||
assert_eq!(get_ret_type(&check_source("true")), StaticType::Bool);
|
||||
assert_eq!(get_ret_type(&check_source("\"hello\"")), StaticType::Text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_variable_propagation() {
|
||||
// (do (def x 10) x) -> The last 'x' must be Int
|
||||
let typed = check_source("(do (def x 10) x)");
|
||||
// Outer is Lambda, Body is Block
|
||||
if let NodeKind::Lambda { body, .. } = &typed.kind {
|
||||
if let NodeKind::Block { exprs } = &body.kind {
|
||||
let last_expr = exprs.last().unwrap();
|
||||
assert_eq!(
|
||||
last_expr.ty,
|
||||
StaticType::Int,
|
||||
"Variable 'x' should be inferred as Int"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected block in lambda body");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Lambda wrapper");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_block_type() {
|
||||
// Block type = last expression type
|
||||
assert_eq!(get_ret_type(&check_source("(do 1 2.5)")), StaticType::Float);
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("(do 1.5 \"test\")")),
|
||||
StaticType::Text
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_lambda_return() {
|
||||
// (fn [a] 10) -> fn(any) -> Int
|
||||
// Since it's already a Lambda, it's NOT wrapped further.
|
||||
let typed = check_source("(fn [a] 10)");
|
||||
if let StaticType::Function(sig) = &typed.ty {
|
||||
assert_eq!(sig.ret, StaticType::Int);
|
||||
} else {
|
||||
panic!("Expected function type, got {:?}", typed.ty);
|
||||
}
|
||||
|
||||
// Nested: (fn [] (do 1 2.5)) -> fn() -> Float
|
||||
let typed_nested = check_source("(fn [] (do 1 2.5))");
|
||||
if let StaticType::Function(sig) = &typed_nested.ty {
|
||||
assert_eq!(sig.ret, StaticType::Float);
|
||||
} else {
|
||||
panic!("Expected function type");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_assignment_updates_type() {
|
||||
// (do (def x 10) (assign x 20.5) x) -> x becomes Float after assignment
|
||||
let typed = check_source("(do (def x 10) (assign x 20.5) x)");
|
||||
if let NodeKind::Lambda { body, .. } = &typed.kind {
|
||||
if let NodeKind::Block { exprs } = &body.kind {
|
||||
let last_expr = exprs.last().unwrap();
|
||||
assert_eq!(
|
||||
last_expr.ty,
|
||||
StaticType::Float,
|
||||
"Variable 'x' should be specialized to Float after assignment"
|
||||
);
|
||||
} else {
|
||||
panic!("Expected block");
|
||||
}
|
||||
} else {
|
||||
panic!("Expected Lambda");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_operator_overloading_inference() {
|
||||
assert_eq!(get_ret_type(&check_source("(+ 1 2)")), StaticType::Int);
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("(+ 1.0 2.0)")),
|
||||
StaticType::Float
|
||||
);
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("(+ \"a\" \"b\")")),
|
||||
StaticType::Text
|
||||
);
|
||||
assert_eq!(get_ret_type(&check_source("(/ 1 2)")), StaticType::Float);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_datetime_inference() {
|
||||
// date("2023-01-01") -> DateTime
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("(date \"2023-01-01\")")),
|
||||
StaticType::DateTime
|
||||
);
|
||||
// DateTime + Int -> DateTime
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("(+ (date \"2023-01-01\") 86400000)")),
|
||||
StaticType::DateTime
|
||||
);
|
||||
// DateTime - DateTime -> Int (Duration)
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source(
|
||||
"(- (date \"2023-01-02\") (date \"2023-01-01\"))"
|
||||
)),
|
||||
StaticType::Int
|
||||
);
|
||||
// DateTime comparison -> Bool
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source(
|
||||
"(> (date \"2023-01-02\") (date \"2023-01-01\"))"
|
||||
)),
|
||||
StaticType::Bool
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_tuple_vector_matrix() {
|
||||
// Heterogeneous -> Tuple
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("[1 3.14 \"text\"]")),
|
||||
StaticType::Tuple(vec![StaticType::Int, StaticType::Float, StaticType::Text])
|
||||
);
|
||||
|
||||
// Homogeneous -> Vector
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("[10 20 30]")),
|
||||
StaticType::Vector(Box::new(StaticType::Int), 3)
|
||||
);
|
||||
|
||||
// Nested Homogeneous -> Matrix
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("[[1 2] [3 4]]")),
|
||||
StaticType::Matrix(Box::new(StaticType::Int), vec![2, 2])
|
||||
);
|
||||
|
||||
// Deep Matrix
|
||||
assert_eq!(
|
||||
get_ret_type(&check_source("[[[1 2]] [[3 4]]]")),
|
||||
StaticType::Matrix(Box::new(StaticType::Int), vec![2, 1, 2])
|
||||
);
|
||||
|
||||
// Shape mismatch -> Tuple of Vectors
|
||||
let mixed = get_ret_type(&check_source("[[1 2] [3 4 5]]"));
|
||||
if let StaticType::Tuple(elements) = mixed {
|
||||
assert_eq!(
|
||||
elements[0],
|
||||
StaticType::Vector(Box::new(StaticType::Int), 2)
|
||||
);
|
||||
assert_eq!(
|
||||
elements[1],
|
||||
StaticType::Vector(Box::new(StaticType::Int), 3)
|
||||
);
|
||||
} else {
|
||||
panic!("Expected Tuple for shape mismatch, got {:?}", mixed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_record() {
|
||||
use crate::ast::types::Keyword;
|
||||
let typed = check_source("{:x 1 :y 0.3}");
|
||||
let ty = get_ret_type(&typed);
|
||||
if let StaticType::Record(layout) = ty {
|
||||
assert_eq!(layout.fields.len(), 2);
|
||||
assert_eq!(layout.fields[0], (Keyword::intern("x"), StaticType::Int));
|
||||
assert_eq!(layout.fields[1], (Keyword::intern("y"), StaticType::Float));
|
||||
} else {
|
||||
panic!("Expected Record, got {:?}", ty);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user