v6 Parsing: type annotations and signatures

Work through examples, try the reasoning yourself, then put it into practice.

View Slides

Before 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 read

v6 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.

Join Discord