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 fact test) keep working unchanged, while fully-annotated programs get strict checks.

What's new vs cp-04

Aspectcp-04cp-05
Phaseslex → parse → resolve → interpretlex → parse → resolve → typecheck → interpret
Tokensadds : and ->
Annotationsnonelet x: int, fn f(a: int, b: int) -> int { ... }
Type ADTint / bool / string / nil / fn(...) -> T / any
Errors caughtresolution-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

  1. 01-why-static-types.md — what dynamic-only typing costs
  2. 02-type-syntax.md: annotations, -> returns, function types
  3. 03-type-representation.md — the Type ADT and Any wildcard
  4. 04-the-checker-walk.md — visitor, scope stack, currentReturn
  5. 05-inference-vs-annotation.md — when the checker fills the gaps
  6. 06-function-types-and-calls.md — arity, arg-by-arg, return matching
  7. 07-error-recovery.md — collecting diagnostics like the resolver