v6 Parsing: type annotations and signatures

Lesson, slides, and applied problem sets.

View Slides

Lesson

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:
    1. parse declaration-level annotations,
    2. parse function signatures (-> and typed params),
    3. 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.

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

  • fn types contain nested parameter/return type parsing.
  • generic-like forms are represented as syntax, not generics runtime:
    • array<number> is valid
    • map<string, number> is valid
    • array<fn(number)->number> is valid with nested parse recursion

4) Valid primitive names are constrained

  • only number, bool, string, and nil are accepted when token is TokenIdent.
  • anything else is an unknown type parse 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 parseVarDeclNoSemi for loop init.
  • assignment targets are still identifier or index only.

Parser structure for typed nodes

Implement with small helpers:

  • parseType() returns *Type and centralizes all type syntax.
  • parseParams() returns []string + []*Type in lockstep.
  • parseVarDecl() / parseVarDeclNoSemi() fill Stmt.TypeAnn.
  • parseFnDecl() fills Stmt.ParamTypes and Stmt.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 name
  • expected '<' after array
  • expected ',' after map key type
  • expected '->' after parameters
  • expected '->' after fn params
  • unknown 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 == TypeNumber
  • ParamTypes both TypeNumber

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 == TypeArray
  • TypeAnn.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 parseType with 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

    Parse v6 programs with type annotations and signatures.

    hard Upgrade to Pro to access hard problems
Join Discord