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.