Skip to content

Step 1 of 3 · Reading · ~3 min

Adding Type Annotations to the Language

Type System & Optimization

Parsing Types Before Checking Types

Every static type system starts the same way: extend the grammar to accept type annotations, before writing a single line of type-checking logic. This lesson is deliberately narrow — parse var x: number = 42; and fun add(a: number, b: number): number { ... } and attach the annotation to the AST, but don't validate anything yet. That separation matters: parsing is about syntax (can this program even be read?), checking is a semantic pass on top of an already-valid parse tree. Mixing the two makes both harder to get right.

Extending the Grammar

The core addition is a single new token, :, appearing in two places:

varDecl  -> "var" IDENT ( ":" IDENT )? "=" expression ";"
funDecl  -> "fun" IDENT "(" ( param ( "," param )* )? ")" ( ":" IDENT )? block
param    -> IDENT ( ":" IDENT )?

Both the variable's type and the return type are optional — this is a gradually typed extension, not a redesign. Existing untyped code (var x = 42;) must keep parsing exactly as before; the annotation is pure addition.

python

Function parameters follow the same pattern, just repeated per parameter, plus one more optional : type after the closing ) for the return type:

python

Why Store, Not Enforce, Right Now

Notice the parser never asks "is number a valid type name?" or "does 42 actually look like a number?" — it just records the name of whatever identifier follows the colon. That's intentional: type names are just syntax at this stage (they could even be misspelled — nubmer parses fine). The next lesson's type checker is the pass that assigns meaning to these names and rejects mismatches. Keeping parsing "dumb" here means the grammar work is fully done and testable in isolation, before any semantic rules exist to get wrong.

What to build

Extend your tokenizer to recognize : as its own token, then extend var and fun parsing to optionally consume a type annotation, defaulting to "any" when omitted. Output a readable dump of what was parsed (e.g. decl x : number, fun add(a: number, b: number) : number) so you can confirm the annotations round-trip correctly before the checker in the next lesson ever runs against them. Make sure omitted annotations (var y = 1;) still parse without error — that's the case most likely to break if the colon-handling isn't fully optional.

Up nextType Checking — Catching Errors at Compile TimeType System & Optimization

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…