Step 02 · Reusing the cp-15 frontend
The source/diag/lex/parse modules are nearly verbatim copies of cp-15's, extended for the multi-function language. This is intentional: the capstone proves the cp-15 design generalises.
What grew:
| concern | cp-15 | cp-16 |
|---|---|---|
| top-level | flat statement list | function definitions |
| keywords | let, print | + fn, return, if, else, while |
| operators | +, -, *, / | + %, comparisons |
| expressions | numeric arithmetic | + function calls |
| typecheck | none | scope, arity, main |
The lexer's structure didn't change — just more keywords and
two-character operators (==, <=, …).
The parser gained parseFunc, parseBlock, control-flow statements,
parseCall, and a parseCmp precedence layer above parseAdd. Every
new feature followed the same recipe:
- Define the AST node.
- Add the parser rule.
- Extend
typecheckto validate it. - Extend
emitLLVMIRto lower it.
Lessons from doing it twice
- Spans, not positions. Spans carry through every transformation; positions become stale the moment you concatenate AST nodes.
- Synchronisation tokens scale. cp-15 used
;. cp-16 uses;and brace-balanced sync inside blocks — seeParser::syncin parse.cpp. The principle is identical. - Diagnostic codes are forever. Once you ship
E0202, you don't renumber it. Our scheme (E01xxlex,E02xxparse,E03xxeval,E04xxsemantic) is just enough structure. std::optional<Stmt>requires<optional>. Compilers may include it transitively today; relying on that is a portability bug waiting to happen.