03 — Type Annotations in the AST and Parser

Type annotations let programmers express intent:

let x: Num = 42;
fn add(a: Num, b: Num): Num { return a + b; }

The parser must recognise the : Type syntax and store the annotation in the AST.

AST changes

The LetStmt and function parameter nodes gain an optional type annotation:

struct LetStmt {
    std::string name;
    bool        immutable;
    ExprPtr     init;
    TypePtr     annotation;  // nullptr if absent
    int         line;
};

struct Param {
    std::string name;
    TypePtr     annotation;  // nullptr if absent
};

struct FnExpr {
    std::vector<Param> params;
    TypePtr            retAnnotation;  // nullptr if absent
    StmtPtr            body;
    int                line;
};

Parsing type annotations

A parseType helper handles the type grammar:

// Type ::= "Num" | "Bool" | "Str" | "Nil" | "Any"
//         | "Fn" "(" TypeList ")" "->" Type
TypePtr Parser::parseType() {
    Token t = advance();
    if (t.lexeme == "Num")  return mkNum();
    if (t.lexeme == "Bool") return mkBool();
    if (t.lexeme == "Str")  return mkStr();
    if (t.lexeme == "Nil")  return mkNil();
    if (t.lexeme == "Any")  return mkAny();
    if (t.lexeme == "Fn") {
        expect(LParen);
        std::vector<TypePtr> params;
        while (peek().kind != RParen) {
            params.push_back(parseType());
            if (!match(Comma)) break;
        }
        expect(RParen);
        expect(Arrow);    // "->"
        auto ret = parseType();
        return mkFn(std::move(params), std::move(ret));
    }
    throw ParseError("[line " + std::to_string(t.line) +
        "] Expected type annotation, got '" + t.lexeme + "'.");
}

Parsing let with annotation

StmtPtr Parser::parseLet() {
    int line = advance().line;  // consume 'let' / 'var'
    bool immutable = (previous().kind == Let);
    auto name = expect(Ident).lexeme;
    TypePtr ann;
    if (match(Colon)) ann = parseType();   // optional ": Type"
    expect(Eq);
    auto init = parseExpr(0);
    expect(Semicolon);
    return std::make_unique<LetStmt>(name, immutable, std::move(init), std::move(ann), line);
}

Parsing function parameters with annotations

std::vector<Param> Parser::parseParams() {
    expect(LParen);
    std::vector<Param> params;
    while (peek().kind != RParen) {
        auto name = expect(Ident).lexeme;
        TypePtr ann;
        if (match(Colon)) ann = parseType();
        params.push_back({name, std::move(ann)});
        if (!match(Comma)) break;
    }
    expect(RParen);
    return params;
}

Token additions

The lexer needs two new token kinds:

  • Colon for : separating name from type.
  • Arrow for -> separating parameter types from return type.

-> is a two-character token; the lexer handles it in the - branch:

case '-':
    return makeToken(match('>') ? Arrow : Minus);

Annotations are optional

All annotations are TypePtr defaulting to nullptr. Code without any annotations is valid — it's treated as fully Any-typed (step 06). This means cp-03/cp-04 programs are valid cp-05 programs without modification.