Step 04 · Typecheck and scope
The typechecker in typecheck.cpp is a single AST walk. Three responsibilities:
- Function table — collect every
fn nameand remember its arity. mainrequirements — must exist (E0411), must take zero parameters (E0412), no duplicate definitions (E0410).- Per-function scope walk:
- Variables must be
let-introduced before use (E0300). - Assignment requires prior declaration (E0302).
- Calls must resolve to a known function with matching arity (E0420, E0421).
- Variables must be
That's it. There's no actual type checking because everything is
i64. That's the right starting point: it disentangles "is it
syntactically and semantically well-formed?" from "is it
type-correct?". You can graft a real type system on top later
(introduce i64/bool/str, unify across operators, infer
generics) without touching the parser.
Scopes are sets, not stacks
std::unordered_set<std::string> scope(f.params.begin(), f.params.end());
Snapshotting the scope before each branch (auto sc1 = scope; ...)
gives the right semantics without a linked structure. It's O(n × m)
in pathological deep nesting but blazingly fast in practice.
A production typechecker uses a stack of scopes for shadowing
(let x = 1; { let x = 2; print x; } print x; → 2 then 1). Our
language doesn't allow shadowing — let x = 1; let x = 2; would
quietly clobber, which is a UX bug we'd fix by adding an E0303
diagnostic.
Why typecheck before IR emission
- Better errors. "Unknown function
fbn" with caret at the call site is far nicer thanllc: undefined symbol _fbn. - Performance. We don't waste time generating IR for code that's going to be rejected.
- Layering. IR emission can assume the AST is well-formed —
fewer
ifchecks, simpler code.
The typechecker is also reused unmodified by the check subcommand,
which is the building block for IDEs.