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:

concerncp-15cp-16
top-levelflat statement listfunction definitions
keywordslet, print+ fn, return, if, else, while
operators+, -, *, /+ %, comparisons
expressionsnumeric arithmetic+ function calls
typechecknonescope, 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:

  1. Define the AST node.
  2. Add the parser rule.
  3. Extend typecheck to validate it.
  4. Extend emitLLVMIR to 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 — see Parser::sync in parse.cpp. The principle is identical.
  • Diagnostic codes are forever. Once you ship E0202, you don't renumber it. Our scheme (E01xx lex, E02xx parse, E03xx eval, E04xx semantic) 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.