cp-05 — Static Type Checker (gradual)
Status: ✅ Built · all tests passing
A third compiler pass added between resolver and interpreter:
lex → parse → resolve → typecheck → interpret
The checker walks the AST a second time, this time with a TypePtr
visitor. It computes a static type for every expression and validates
operands, conditions, arities, return values, and assignments.
Gradual typing
Annotations are optional. A bare var x = 1; is fine; so is
let y: int = 1;. Wherever the source omits a type, the slot is
any, a wildcard that:
- satisfies any constraint (
any + int = int,if (anyVal)accepted), - propagates through unknown operations (
any * any = any), - lets cp-04 programs (and the recursive
facttest) keep working unchanged, while fully-annotated programs get strict checks.
What's new vs cp-04
| Aspect | cp-04 | cp-05 |
|---|---|---|
| Phases | lex → parse → resolve → interpret | lex → parse → resolve → typecheck → interpret |
| Tokens | — | adds : and -> |
| Annotations | none | let x: int, fn f(a: int, b: int) -> int { ... } |
| Type ADT | — | int / bool / string / nil / fn(...) -> T / any |
| Errors caught | resolution-only (undef, redecl, …) | + operand types, arity, arg types, return type, condition type, assign-T |
Source layout (src/cpp/)
src/
token.hpp # adds Colon, Arrow
lexer.{hpp,cpp} # handles ':' and '->'
value.hpp # unchanged
type.hpp # NEW — Type ADT and tyInt/tyBool/...
ast.hpp # adds declaredType, paramTypes, returnType, checkedType
# and a second `accept` overload returning TypePtr
parser.{hpp,cpp} # parseType() + optional annotations
environment.hpp # unchanged
resolver.{hpp,cpp} # unchanged
typecheck.{hpp,cpp} # NEW — gradual type checker
interpreter.{hpp,cpp} # unchanged
main.cpp # invokes TypeChecker between resolver and interpreter
tests/test_typecheck.cpp # regression + new annotated programs + 11 negative cases
Build & test
cd src/cpp
cmake -S . -B build -G "Unix Makefiles"
cmake --build build -j
ctest --test-dir build --output-on-failure
Sample diagnostics
$ cat > bad.ml <<'EOF'
let x: int = true;
if (5) print 1;
fn h(a: int) -> int { return true; }
h(true, 2);
EOF
$ build/mli bad.ml
[line 1] type error: initializer of 'x' has type bool, expected int
[line 2] type error: if-condition must be bool (got int)
[line 3] type error: return type mismatch: function returns int, got bool
[line 4] type error: call to function expected 1 arg(s), got 2
All diagnostics are collected before the program is rejected — same recovery model as the resolver.
See src/cpp/steps/ for the walk-through
01-why-static-types.md— what dynamic-only typing costs02-type-syntax.md—:annotations,->returns, function types03-type-representation.md— theTypeADT andAnywildcard04-the-checker-walk.md— visitor, scope stack, currentReturn05-inference-vs-annotation.md— when the checker fills the gaps06-function-types-and-calls.md— arity, arg-by-arg, return matching07-error-recovery.md— collecting diagnostics like the resolver