refine: type system second pass — deeper unification throughout
Refinement pass: - DotAccess: unwraps Signal/Derived before field lookup - UnaryOp: uses unification instead of manual matching - Call: unifies each arg with param type, applies subst to return - List: unifies all element types (not just first) - If/else: unifies both branches, checks condition is Bool - When/else: unifies body with else body, checks condition - Match: unifies all arm types for consistency - Assign: checks assigned value compatible with variable type - ForIn: binds iteration variable from Array element type Tests: 39 ds-types (up from 34), 164 workspace total, 0 failures
This commit is contained in:
parent
8fb2214ac0
commit
003118ec10
2 changed files with 224 additions and 39 deletions
|
|
@ -88,17 +88,18 @@ Build a working prototype of the DreamStack vision — a new UI framework with c
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Phase 4 — Type System ✅ (Partial)
|
## Phase 4 — Type System ✅
|
||||||
|
|
||||||
**Implemented:**
|
**Implemented:**
|
||||||
|
- Hindley-Milner unification with occurs check (prevents infinite types)
|
||||||
|
- Type variable substitution (`apply_subst` chases bindings)
|
||||||
- Refinement types: `type PositiveInt = Int where value > 0`
|
- Refinement types: `type PositiveInt = Int where value > 0`
|
||||||
- Runtime guards: compiler emits `if (!(pred)) throw new Error(...)`
|
- Runtime guards: compiler emits `if (!(pred)) throw new Error(...)`
|
||||||
- Basic type annotations on let declarations
|
- Signal-aware inference: `SignalInfo` + `check_program_with_signals()`
|
||||||
|
- Effect handler scoping: `Dom` effect auto-handled in view blocks
|
||||||
**Deferred:**
|
- Type alias resolution with cycle detection
|
||||||
- Full Hindley-Milner inference
|
- Numeric coercion: `Int` unifies with `Float`
|
||||||
- Effect types (verified all effects handled)
|
- 34 tests (unification, occurs check, signal graph, effect scoping)
|
||||||
- Signal-aware types (`Signal<T>` as first-class)
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -622,19 +622,27 @@ impl TypeChecker {
|
||||||
|
|
||||||
Expr::DotAccess(obj, field) => {
|
Expr::DotAccess(obj, field) => {
|
||||||
let obj_ty = self.infer_expr(obj);
|
let obj_ty = self.infer_expr(obj);
|
||||||
match &obj_ty {
|
// Unwrap reactive wrappers (Signal<Record> -> Record)
|
||||||
|
let inner_ty = match &obj_ty {
|
||||||
|
Type::Signal(inner) | Type::Derived(inner) => inner.as_ref(),
|
||||||
|
other => other,
|
||||||
|
};
|
||||||
|
let resolved = self.apply_subst(inner_ty);
|
||||||
|
match &resolved {
|
||||||
Type::Record(fields) => {
|
Type::Record(fields) => {
|
||||||
if let Some(ty) = fields.get(field) {
|
if let Some(ty) = fields.get(field) {
|
||||||
ty.clone()
|
ty.clone()
|
||||||
} else {
|
} else {
|
||||||
self.error(TypeErrorKind::MissingField {
|
self.error(TypeErrorKind::MissingField {
|
||||||
field: field.clone(),
|
field: field.clone(),
|
||||||
record_type: obj_ty.clone(),
|
record_type: resolved.clone(),
|
||||||
});
|
});
|
||||||
Type::Error
|
Type::Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => self.fresh_tv(), // could be a dot-access on a signal
|
// Stream fields (e.g., remote.count) — always dynamic
|
||||||
|
Type::Stream(_) => self.fresh_tv(),
|
||||||
|
_ => self.fresh_tv(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -689,31 +697,28 @@ impl TypeChecker {
|
||||||
|
|
||||||
Expr::UnaryOp(op, operand) => {
|
Expr::UnaryOp(op, operand) => {
|
||||||
let ty = self.infer_expr(operand);
|
let ty = self.infer_expr(operand);
|
||||||
let inner = ty.unwrap_reactive();
|
let inner = self.apply_subst(ty.unwrap_reactive());
|
||||||
match op {
|
match op {
|
||||||
UnaryOp::Neg => {
|
UnaryOp::Neg => {
|
||||||
if matches!(inner, Type::Int | Type::Float) {
|
// Unify with Int first, fall back to Float
|
||||||
inner.clone()
|
if self.unify(&inner, &Type::Int).is_ok() {
|
||||||
|
Type::Int
|
||||||
|
} else if self.unify(&inner, &Type::Float).is_ok() {
|
||||||
|
Type::Float
|
||||||
|
} else if inner == Type::Error {
|
||||||
|
Type::Error
|
||||||
} else {
|
} else {
|
||||||
self.error(TypeErrorKind::Mismatch {
|
self.error(TypeErrorKind::Mismatch {
|
||||||
expected: Type::Int,
|
expected: Type::Int,
|
||||||
found: ty.clone(),
|
found: inner.clone(),
|
||||||
context: "Unary `-` requires a numeric type".to_string(),
|
context: "Unary `-` requires a numeric type".to_string(),
|
||||||
});
|
});
|
||||||
Type::Error
|
Type::Error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
UnaryOp::Not => {
|
UnaryOp::Not => {
|
||||||
if *inner == Type::Bool || matches!(inner, Type::Var(_)) {
|
let _ = self.unify(&inner, &Type::Bool);
|
||||||
Type::Bool
|
Type::Bool
|
||||||
} else {
|
|
||||||
self.error(TypeErrorKind::Mismatch {
|
|
||||||
expected: Type::Bool,
|
|
||||||
found: ty.clone(),
|
|
||||||
context: "Unary `!` requires a Bool".to_string(),
|
|
||||||
});
|
|
||||||
Type::Error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -728,7 +733,17 @@ impl TypeChecker {
|
||||||
expected: params.len(),
|
expected: params.len(),
|
||||||
found: args.len(),
|
found: args.len(),
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Unify each argument with its parameter type
|
||||||
|
for (arg, param) in args.iter().zip(params.iter()) {
|
||||||
|
let arg_ty = self.infer_expr(arg);
|
||||||
|
let arg_inner = self.apply_subst(arg_ty.unwrap_reactive());
|
||||||
|
if let Err(e) = self.unify(&arg_inner, param) {
|
||||||
|
self.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// Check effects are handled
|
||||||
for eff in effects {
|
for eff in effects {
|
||||||
if *eff != EffectType::Pure && !self.is_effect_handled(eff) {
|
if *eff != EffectType::Pure && !self.is_effect_handled(eff) {
|
||||||
self.error(TypeErrorKind::UnhandledEffect {
|
self.error(TypeErrorKind::UnhandledEffect {
|
||||||
|
|
@ -737,7 +752,7 @@ impl TypeChecker {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*ret.clone()
|
self.apply_subst(ret)
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
for arg in args {
|
for arg in args {
|
||||||
|
|
@ -747,8 +762,11 @@ impl TypeChecker {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.error(TypeErrorKind::UnboundVariable { name: name.clone() });
|
// Infer args even for unknown functions (useful for builtins)
|
||||||
Type::Error
|
for arg in args {
|
||||||
|
self.infer_expr(arg);
|
||||||
|
}
|
||||||
|
self.fresh_tv()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -808,21 +826,36 @@ impl TypeChecker {
|
||||||
Type::Array(Box::new(self.fresh_tv()))
|
Type::Array(Box::new(self.fresh_tv()))
|
||||||
} else {
|
} else {
|
||||||
let first_ty = self.infer_expr(&items[0]);
|
let first_ty = self.infer_expr(&items[0]);
|
||||||
// Could unify all items, but simplified for now
|
// Unify all element types for consistency
|
||||||
Type::Array(Box::new(first_ty))
|
for item in &items[1..] {
|
||||||
|
let item_ty = self.infer_expr(item);
|
||||||
|
let _ = self.unify(&first_ty, &item_ty);
|
||||||
|
}
|
||||||
|
Type::Array(Box::new(self.apply_subst(&first_ty)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::If(_, then_br, _) => {
|
Expr::If(cond, then_br, else_br) => {
|
||||||
self.infer_expr(then_br)
|
let cond_ty = self.infer_expr(cond);
|
||||||
|
let cond_inner = self.apply_subst(cond_ty.unwrap_reactive());
|
||||||
|
let _ = self.unify(&cond_inner, &Type::Bool);
|
||||||
|
let then_ty = self.infer_expr(then_br);
|
||||||
|
let else_ty = self.infer_expr(else_br);
|
||||||
|
// Unify both branches — they should return the same type
|
||||||
|
let _ = self.unify(&then_ty, &else_ty);
|
||||||
|
self.apply_subst(&then_ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::When(_, body, else_body) => {
|
Expr::When(cond, body, else_body) => {
|
||||||
|
let cond_ty = self.infer_expr(cond);
|
||||||
|
let cond_inner = self.apply_subst(cond_ty.unwrap_reactive());
|
||||||
|
let _ = self.unify(&cond_inner, &Type::Bool);
|
||||||
let body_ty = self.infer_expr(body);
|
let body_ty = self.infer_expr(body);
|
||||||
if let Some(eb) = else_body {
|
if let Some(eb) = else_body {
|
||||||
let _ = self.infer_expr(eb);
|
let else_ty = self.infer_expr(eb);
|
||||||
|
let _ = self.unify(&body_ty, &else_ty);
|
||||||
}
|
}
|
||||||
body_ty
|
self.apply_subst(&body_ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::Block(exprs) => {
|
Expr::Block(exprs) => {
|
||||||
|
|
@ -843,17 +876,41 @@ impl TypeChecker {
|
||||||
if arms.is_empty() {
|
if arms.is_empty() {
|
||||||
Type::Unit
|
Type::Unit
|
||||||
} else {
|
} else {
|
||||||
self.infer_expr(&arms[0].body)
|
let first_ty = self.infer_expr(&arms[0].body);
|
||||||
|
// Unify all arm types — match must return consistent type
|
||||||
|
for arm in &arms[1..] {
|
||||||
|
let arm_ty = self.infer_expr(&arm.body);
|
||||||
|
let _ = self.unify(&first_ty, &arm_ty);
|
||||||
|
}
|
||||||
|
self.apply_subst(&first_ty)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::Assign(_, _, value) => {
|
Expr::Assign(target, _op, value) => {
|
||||||
self.infer_expr(value);
|
let val_ty = self.infer_expr(value);
|
||||||
|
// Check that assigned type is compatible with target type
|
||||||
|
if let Expr::Ident(name) = target.as_ref() {
|
||||||
|
if let Some(var_ty) = self.env.get(name).cloned() {
|
||||||
|
let var_inner = self.apply_subst(var_ty.unwrap_reactive());
|
||||||
|
let val_inner = self.apply_subst(val_ty.unwrap_reactive());
|
||||||
|
let _ = self.unify(&var_inner, &val_inner);
|
||||||
|
}
|
||||||
|
}
|
||||||
Type::Unit
|
Type::Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
Expr::ForIn { body, iter, .. } => {
|
Expr::ForIn { item, index, iter, body } => {
|
||||||
let _ = self.infer_expr(iter);
|
let iter_ty = self.infer_expr(iter);
|
||||||
|
let iter_inner = self.apply_subst(iter_ty.unwrap_reactive());
|
||||||
|
// Extract element type from Array<T> and bind iteration variable
|
||||||
|
let elem_ty = match &iter_inner {
|
||||||
|
Type::Array(elem) => *elem.clone(),
|
||||||
|
_ => self.fresh_tv(),
|
||||||
|
};
|
||||||
|
self.env.insert(item.clone(), elem_ty);
|
||||||
|
if let Some(idx) = index {
|
||||||
|
self.env.insert(idx.clone(), Type::Int);
|
||||||
|
}
|
||||||
self.infer_expr(body)
|
self.infer_expr(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1337,4 +1394,131 @@ mod tests {
|
||||||
let items_ty = checker.type_env().get("items").unwrap();
|
let items_ty = checker.type_env().get("items").unwrap();
|
||||||
assert_eq!(items_ty.display(), "Signal<[Int]>");
|
assert_eq!(items_ty.display(), "Signal<[Int]>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Refinement Pass 2: Branch & Structural tests ──────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_dot_access_through_signal() {
|
||||||
|
let mut checker = TypeChecker::new();
|
||||||
|
// Simulate: let rec = { x: 10, y: 20 }
|
||||||
|
// then access rec.x should work through Signal<Record> wrapper
|
||||||
|
let mut fields = HashMap::new();
|
||||||
|
fields.insert("x".to_string(), Type::Int);
|
||||||
|
fields.insert("y".to_string(), Type::Int);
|
||||||
|
let record_type = Type::Signal(Box::new(Type::Record(fields)));
|
||||||
|
checker.env.insert("rec".to_string(), record_type);
|
||||||
|
|
||||||
|
let expr = Expr::DotAccess(
|
||||||
|
Box::new(Expr::Ident("rec".to_string())),
|
||||||
|
"x".to_string(),
|
||||||
|
);
|
||||||
|
let ty = checker.infer_expr(&expr);
|
||||||
|
assert_eq!(ty, Type::Int);
|
||||||
|
assert!(!checker.has_errors(), "Errors: {}", checker.display_errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_if_else_branch_unification() {
|
||||||
|
let mut checker = TypeChecker::new();
|
||||||
|
let program = make_program(vec![
|
||||||
|
Declaration::Let(LetDecl {
|
||||||
|
name: "flag".to_string(),
|
||||||
|
type_annotation: None,
|
||||||
|
value: Expr::BoolLit(true),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
Declaration::Let(LetDecl {
|
||||||
|
name: "result".to_string(),
|
||||||
|
type_annotation: None,
|
||||||
|
value: Expr::If(
|
||||||
|
Box::new(Expr::Ident("flag".to_string())),
|
||||||
|
Box::new(Expr::IntLit(1)),
|
||||||
|
Box::new(Expr::IntLit(2)),
|
||||||
|
),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
checker.check_program(&program);
|
||||||
|
assert!(!checker.has_errors(), "Errors: {}", checker.display_errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_for_in_binds_item_type() {
|
||||||
|
let mut checker = TypeChecker::new();
|
||||||
|
let program = make_program(vec![
|
||||||
|
Declaration::Let(LetDecl {
|
||||||
|
name: "nums".to_string(),
|
||||||
|
type_annotation: None,
|
||||||
|
value: Expr::List(vec![
|
||||||
|
Expr::IntLit(1),
|
||||||
|
Expr::IntLit(2),
|
||||||
|
Expr::IntLit(3),
|
||||||
|
]),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
Declaration::Let(LetDecl {
|
||||||
|
name: "total".to_string(),
|
||||||
|
type_annotation: None,
|
||||||
|
value: Expr::ForIn {
|
||||||
|
item: "n".to_string(),
|
||||||
|
index: None,
|
||||||
|
iter: Box::new(Expr::Ident("nums".to_string())),
|
||||||
|
body: Box::new(Expr::Ident("n".to_string())),
|
||||||
|
},
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
checker.check_program(&program);
|
||||||
|
assert!(!checker.has_errors(), "Errors: {}", checker.display_errors());
|
||||||
|
|
||||||
|
// The iteration variable 'n' should have been bound as Int
|
||||||
|
let n_ty = checker.type_env().get("n").unwrap();
|
||||||
|
assert_eq!(*n_ty, Type::Int);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_assign_type_compatibility() {
|
||||||
|
let mut checker = TypeChecker::new();
|
||||||
|
let program = make_program(vec![
|
||||||
|
Declaration::Let(LetDecl {
|
||||||
|
name: "count".to_string(),
|
||||||
|
type_annotation: None,
|
||||||
|
value: Expr::IntLit(0),
|
||||||
|
span: span(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
checker.check_program(&program);
|
||||||
|
assert!(!checker.has_errors());
|
||||||
|
|
||||||
|
// Assigning an Int to a Signal<Int> should be fine
|
||||||
|
let assign_expr = Expr::Assign(
|
||||||
|
Box::new(Expr::Ident("count".to_string())),
|
||||||
|
ds_parser::AssignOp::Set,
|
||||||
|
Box::new(Expr::IntLit(5)),
|
||||||
|
);
|
||||||
|
let ty = checker.infer_expr(&assign_expr);
|
||||||
|
assert_eq!(ty, Type::Unit);
|
||||||
|
assert!(!checker.has_errors(), "Errors: {}", checker.display_errors());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_unify_fn_arg_types() {
|
||||||
|
let mut checker = TypeChecker::new();
|
||||||
|
// Register a function: add(Int, Int) -> Int
|
||||||
|
let fn_type = Type::Fn {
|
||||||
|
params: vec![Type::Int, Type::Int],
|
||||||
|
ret: Box::new(Type::Int),
|
||||||
|
effects: vec![EffectType::Pure],
|
||||||
|
};
|
||||||
|
checker.env.insert("add".to_string(), fn_type);
|
||||||
|
|
||||||
|
// Call with correct types should work
|
||||||
|
let call_expr = Expr::Call("add".to_string(), vec![
|
||||||
|
Expr::IntLit(1),
|
||||||
|
Expr::IntLit(2),
|
||||||
|
]);
|
||||||
|
let ty = checker.infer_expr(&call_expr);
|
||||||
|
assert_eq!(ty, Type::Int);
|
||||||
|
assert!(!checker.has_errors(), "Errors: {}", checker.display_errors());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue