Luce / engineering
Learn Luce LuciaOS

Parsing and the AST

Tokens are flat; programs are nested. The parser builds that nesting: multiplication belongs inside a binding, the binding belongs inside a function, and the indented returns belong to different branches.

The AST preserves what was written

let total = price + tax * 2Precedence shapes the tree
  • binding let total
    • binary +
      • name price
      • binary *
        • name tax
        • integer 2

The nodes are arena-owned, untyped, and source-spanned. A call node carries the written positional and named arguments. A class node carries fields, methods, init, and deinit. Semantic analysis uses this structure to resolve names and match calls.

How Pratt parsing handles expressions

  1. Read a prefixA literal, name, grouped expression, lambda, unary operator, or collection literal starts the left side.
  2. Read postfix formsCalls, field access, indexing, and slicing bind most tightly and extend the current expression.
  3. Compare binding powerAn infix operator joins only when its precedence is high enough for the current parse.
  4. Recurse for the right sideAssociativity sets the minimum binding power used for the recursive right-hand parse.

Parentheses resolve two ambiguous forms

A chained comparison and not directly before a comparison both admit plausible trees to readers from different languages. Luce requires parentheses so the source carries one visible grouping.

Explicit grouping

not (a == b)
(a < b) and (b < c)

Ambiguous grouping

not a == b
a < b < c

Recovery follows the reader

A broken construct reports at the offending token, then resumes at the next line in the same block. If a broken header owns an indented suite, recovery consumes that body with the header to preserve the surrounding block structure. An unclosed list points to its opening bracket, and a missing comma points to the item it separates.

For an unambiguous mistake such as if x = 1:, the recovery stream uses the intended comparison so the body can also be checked. The compile result remains a diagnostic, and the temporary repair improves the rest of that report.

The small amount of early sugar

Most source sugar survives into typed HIR. The parser currently expands two forms early: an f-string becomes concatenation through str(...), and an elif chain becomes nested conditionals. Moving those expansions into typed lowering would give them access to resolved meaning.

Robustness is part of the grammar

Statements, conditions, expression nesting, prefix chains, and type arguments share a recursion budget. Diagnostic volume is capped. When the lexer truncates a structurally excessive stream, the parser recognizes the synthetic tail. The grammar therefore defines recovery and termination for malformed input alongside valid syntax.