v6 Parsing: type annotations and signatures
Work through examples, try the reasoning yourself, then put it into practice.
View SlidesBefore you start
- Parse v5 modules and postfix expressions.
- Understand recursive array/map/function type shapes.
What you will practice
- Build recursive Type trees and aligned parameter annotations.
- Enforce function signature syntax while keeping optional local annotations.
- Separate malformed type syntax from incompatible initializer semantics.
In this lesson
Lesson
3 min readv6 Parsing: type annotations and signatures
The v6 parser adds optional variable annotations and mandatory function
signatures. let x=1; still parses without an annotation. A function parameter
must include : type, and the parameter list must be followed by -> type.
The body is still a block of ordinary statements.
Worked reconstruction
Parse this type annotation:
let transform: fn(array<number>, bool) -> map<string,number>;
| Type node | Children |
|---|---|
| function | Params[0]=array, Params[1]=bool; Return=map |
| array | Elem=number |
| map | Key=string; Value=number |
Store the complete tree in Stmt.TypeAnn. For a function declaration, keep
ParamTypes aligned by index with Params and store ReturnType separately.
fn f(x:array<number>)->number { return x[0]; } has one parameter, not the
three type-name tokens array/number/number as separate parameters.
Recursive type parsing
A type parser consumes one complete type then returns at its delimiter. array requires one nested type inside angle brackets; map requires two separated by comma; fn requires a parenthesized type list, arrow, and return type. Primitive type spellings are restricted; vector is an unknown type and fails parsing. Internal unknown is not accepted as a user annotation.
Nested array<array<number>> works because the lexer emits two greater-than
tokens, not a shift token. Type parsing interprets them as delimiters, while
expression parsing still uses the same token kind for numeric comparison.
Typed declarations are also allowed in for-init through the NoSemi variant.
Separate a type's closing > from the initializer = with whitespace:
let xs:array<number> = [];. Without that space, >= is one greater-or-equal
token, so the type parser never receives its closing delimiter. This lexer
uses longest-match operators even inside a type annotation.
Parsing does not prove compatibility
let x:number="text"; has valid type syntax and reaches analysis, where the
initializer mismatch is rejected. Likewise a syntactically complete function
can have an invalid return expression. Conversely fn f(x) {} fails parsing
before analysis can infer or complain about its signature. Preserve the
existing fixed module/implicit-main parse mode and l-value rules.
Debugging lab
A type parser stops map<string,array<number>> after reading string, and a function parser appends types in a separate unordered map. Which two representation invariants are broken?
Compare your diagnosis after attempting the lab
The map parser must recursively consume both key and value type trees and both closing delimiters. Parameter types must stay positionally aligned with parameter names; an unordered map loses call-slot correspondence.
Transfer task
Draw the Type tree for fn(fn(number)->bool)->array<string>. Then make one unknown type, one missing arrow, and one valid annotation with an incompatible initializer; classify parse versus semantic failure.
Carry forward
Carry complete Type trees into analysis. Do not erase them during parsing or treat a missing required signature as an inference request.
Build program-parser-v6. Keep a before/after trace of one failing test. Use the checkpoint to diagnose specific skills; passing it does not replace implementing and testing the artifact.
Practice sequence
Use the order below for a guided path, or choose an item to revisit. A pass records a successful attempt; try a fresh example later to check what you can reconstruct.