Step 03 · A multi-function language

cp-15's language was a calculator. cp-16's is a real (if tiny) language with functions, control flow, and recursion. The grammar changes that mattered:

Top-level is functions only

program := func+

A program is a list of fn declarations. There's no top-level "main scope" — that's fn main(). This rule is enforced by the parser (error E0210) and by typecheck (error E0411 if no main).

Blocks introduce scope

case Stmt::K::If: {
    auto sc1 = scope; checkBlock(s.body, sc1);
    auto sc2 = scope; checkBlock(s.elseBody, sc2);
    return;
}

We snapshot the scope before each branch so a let inside a branch doesn't leak out. This is the simplest form of lexical scoping; real languages use a linked stack of scopes for efficiency and shadowing rules.

Calls

parseCall runs after parsePrimary and wraps the result in zero or more ( args ) suffixes:

while (peek().kind == Tok::LParen) { ... }

This lets f(1)(2) parse (even though we don't have first-class functions). It also makes adding methods (obj.method(arg)) a small extension.

Control flow lowers to branches

if/while are compiled to plain LLVM basic blocks; we don't use select or phi. The IR for while (cond) body is:

  br label %cond
cond:
  %v = ... evaluate cond ...
  %t = icmp ne i64 %v, 0
  br i1 %t, label %body, label %end
body:
  ... body ...
  br label %cond
end:

That's the canonical "structured control flow → CFG" lowering. Optimisation passes (mem2reg, jump threading) clean up the alloca traffic introduced by let/assign.