v6 Parsing: type annotations and signatures
Lesson, slides, and applied problem sets.
View SlidesLesson
v6 Parsing: type annotations and function signatures
v6 makes declarations type-aware at parse time and makes signature parsing a new failure surface.
Why v6 parsing is a bigger jump
- Earlier versions only tracked syntax shapes.
- v6 introduces recursive type syntax in declarations, which means parser must now parse and validate type trees, not only identifier names.
- This includes three coupled responsibilities:
- parse declaration-level annotations,
- parse function signatures (
->and typed params), - keep previous statement/expr grammar stable.
Parse-mode reminder
Parse still uses the same compile-unit boundary:
- If the first token is
module, parse module mode (moduleDecl*). - Otherwise parse implicit
module main.
This matters because all module-aware behavior (imports/exports) remains in the same spot before we get to typed constructs.
New grammar additions
simpleType -> number | bool | string | nil
-> array < type >
-> map < type , type >
fnType -> fn ( typeList? ) -> type
type -> simpleType | fnType
varDecl -> ("let"|"var"|"const") IDENT (":" type)? ("=" expr)? ";"
fnDecl -> "fn" IDENT "(" params? ")" "->" type block
params -> param ("," param)*
param -> IDENT ":" type
stmt -> varDecl
| fnDecl
| ...
Key parser rules to preserve
1) Function return type is required
fn add(a: number) {}is invalid without->.- A function declaration must parse in this order:
- fn, name,
(, params,),->, return type, body block.
- fn, name,
2) Parameter annotations are mandatory at this level
- each parameter must be
IDENT : type fn f(a, b: number) -> number {}should fail at the first unannotated param.
3) parseType is recursive and token-driven
fntypes contain nested parameter/return type parsing.- generic-like forms are represented as syntax, not generics runtime:
array<number>is validmap<string, number>is validarray<fn(number)->number>is valid with nested parse recursion
4) Valid primitive names are constrained
- only
number,bool,string, andnilare accepted when token isTokenIdent. - anything else is an
unknown typeparse error.
5) Optional annotations on variables
let x: number = 1;is valid.let x = 1;remains valid and stores nil/empty type annotation.
6) For-loop and l-value behavior unchanged from v5
- typed declarations still reuse
parseVarDeclNoSemifor loop init. - assignment targets are still identifier or index only.
Parser structure for typed nodes
Implement with small helpers:
parseType()returns*Typeand centralizes all type syntax.parseParams()returns[]string+[]*Typein lockstep.parseVarDecl()/parseVarDeclNoSemi()fillStmt.TypeAnn.parseFnDecl()fillsStmt.ParamTypesandStmt.ReturnType.
This keeps semantic/typing checks orthogonal: parser ensures shape first, type-checker can enforce compatibility later.
Parse diagnostics to keep stable
Keep canonical messages stable where possible:
expected ':' after parameter nameexpected '<' after arrayexpected ',' after map key typeexpected '->' after parametersexpected '->' after fn paramsunknown type: ...expected type
Deep practice checkpoints (almost solved)
1) Function signature requirement
Input: fn add(a: number, b: number) { return a + b; }
Expected:
- parse error on missing
-> type.
2) Return type parsing
Input: fn add(a: number, b: number) -> number { return a + b; }
Expected:
- function parse succeeds
ReturnType.Kind == TypeNumberParamTypesbothTypeNumber
3) Optional variable annotation
Inputs:
let x: number = 1;let y = 2;
Expected:
- first sets
Stmt.TypeAnn != nil - second sets
Stmt.TypeAnn == nil
4) Recursive type parsing
Input: let f: fn(array<number>, fn(string)->string) -> map<string, array<string>> = 1;
Expected:
- nested type tree parses fully
- function type
Kind==TypeFunc, two params, map return
5) Map type arity
Input: let bad: map<number> = 1;
Expected:
- error on missing comma/value in map type.
6) Array type shape
Input: let a: array<number> = [];
Expected:
Stmt.TypeAnn.Kind == TypeArrayTypeAnn.Elem.Kind == TypeNumber
7) Variable declaration in for loop
Input: for (let i: number = 0;;) {}
Expected:
- init statement still valid in typed parser.
8) Unknown type rejection
Input: let a: bigint = 1;
Expected:
- parse error
unknown type.
9) Nested fn type parse order
Input: let cb: fn(string)->fn(number)->string = f;
Expected:
- returns from
parseTypewith right-associative behavior via recursion in fn parser path.
Design takeaway
v6 parsing is not “just syntax sugar.” It is the first point where parser has to become grammar-strict about typing syntax. That means malformed annotations must fail early and deterministically, before any runtime or semantic rule fires.
Module Items
Program Parser v6