v5 Parsing: modules, imports, and member access

Lesson, slides, and applied problem sets.

View Slides

Lesson

v5 Parsing: modules, imports, exports, and member access

v5 introduces compilation-unit structure. Before this stage, every token stream is a plain statement sequence. In v5, the parser must decide between:

  • explicit multi-module files (module blocks), or
  • implicit module main for files without a leading module.

This decision happens at parse start and is never flipped later.


New grammar

program     -> moduleDecl* | stmts (implicit main)
moduleDecl  -> "module" IDENT "{" moduleItem* "}"
moduleItem  -> importStmt | exportDecl | stmt
importStmt  -> "import" IDENT ("as" IDENT)? ";"
exportDecl  -> "export" (fnDecl | varDecl)

postfix     -> primary (call | index | member)*
member      -> "." IDENT

stmt        -> varDecl
            | ifStmt
            | whileStmt
            | forStmt
            | breakStmt
            | continueStmt
            | returnStmt
            | block
            | fnDecl
            | assignOrExpr

ifStmt      -> "if" "(" expr ")" block ("else" block)?
forStmt     -> "for" "(" forInit? ";" expr? ";" forPost? ")" block
forInit     -> varDeclNoSemi | assignOrExprNoSemi
forPost     -> assignOrExprNoSemi

Parse-mode contract (important)

1) Parse entrypoint rule:

  • If first token is TokenModule, parse repeated moduleDecl until EOF.
  • Otherwise, parse all statements and wrap in one module named main.

2) Module mode implications:

  • Every top-level item is processed through moduleItem.
  • Imports and exports are only recognized in this mode.
  • parseExport is only reached from module items, not from plain statement mode.

3) Implicit-main mode implication:

  • export and import are no longer meaningful syntax in practice because only parseStmt is active. Unrecognized top-level tokens should surface as normal parse errors.

This boundary is where many “why did parser accept/reject this file” bugs hide.


Module and import semantics in the parser

  • moduleDecl requires:
    • module keyword
    • module name identifier
    • opening {
    • zero or more module items
    • closing }
  • import syntax:
    • import name; and alias default = name
    • import name as alias; for explicit rename
    • required trailing semicolon for each import declaration
  • Empty module bodies are legal when only braces close immediately.

Subtle edge: once in module mode, the parser stays in module mode until EOF, so mixing module ... and implicit-main statements in one file is rejected at token-level by grammar mismatch.


Export declaration contract

export applies only to declarations, and only where module items are expected.

  • valid forms:
    • export fn ...
    • export let|var|const ...
  • invalid forms should fail as: expected declaration after export

No other exported node kind exists in v5.


Postfix chain as parser strategy

a.b()[0].c(1) is parsed by:

1) parsing a primary (a) 2) repeatedly consuming one of: (), [], . 3) each step producing a new expression node (ExprCall, ExprIndex, ExprMember)

The chain is therefore left-to-right by construction.

Chain details:

  • member access uses ExprMember with Left + Value = identifier string.
  • a.(...) is illegal because after . an identifier is mandatory.
  • this stage treats member access as syntactic shape only; name resolution is a later stage.

Assignment target rule in v5

parseAssignOrExpr accepts assignment only when the left side is an L-value:

  • ExprIdent or ExprIndex are valid
  • ExprMember is not a valid target in v5 parser

So:

  • x = 1 -> valid
  • x[0] = 1 -> valid
  • x.y = 1 -> rejected as invalid assignment target

This is intentional: member writes become a later semantic/runtime concern.


Practical diagnostics to preserve

Keep these parse errors stable where possible:

  • expected module name
  • expected '{' after module name / expected '}' after module body
  • expected ';' after import
  • expected declaration after export
  • expected member name after .
  • expected ';' after assignment or expression

Also keep early EOF detection strict: parse should not silently accept trailing garbage once EOF conditions are mismatched.


Deep practice checkpoints (almost solved)

1) Explicit module dispatch

Input tokens start with module and include two module blocks.

Expected:

  • parse mode remains module mode for entire input
  • both blocks become distinct entries in Program.Modules

2) Import aliasing

Parse sequence:

import core as rt;

Expected:

  • one import entry
  • Name == "core", Alias == "rt"

3) Export gate in module mode

Parse:

export fn f() {}

Expected:

  • StmtExported true on that function
  • function node otherwise structurally valid

4) Export restriction

Parse:

export if (x) {}

Expected:

  • parse error: expected declaration after export

5) Member/access chains

Parse expression:

m.add(1)[0].name;

Expected:

  • postfix chain built left-to-right
  • ending node is ExprMember
  • child path includes call then index then member

6) Invalid member read as target

Parse:

obj.field = 1;

Expected:

  • parse error invalid assignment target

7) Valid index assignment target

Parse:

arr[0] = 1;

Expected:

  • assignment target accepted
  • target node kind ExprIndex

Module Items

  • Program Parser v5

    Parse v5 programs with modules, imports, and member access.

    hard Upgrade to Pro to access hard problems
Join Discord