Section 1: From Characters to Trees

The front end is the part of a language implementation that turns text into structure. It is the only part that ever sees your source code, and it is the only part that can produce a good error message — because it is the only part that still knows where things were written.

This section covers Milestones M1 and M2. By the end you have a working calculator language with real values, real type errors, and error messages that point at the exact characters that were wrong.

$ echo '10 + 20 * 3' > t.ember && ember run t.ember
70

That looks small. Getting there requires a scanner, a grammar, a precedence-correct parser, a typed tree, a span discipline, and a value representation you can live with for the next fifteen weeks. Three of those five decisions are hard to change later.


What You Build

ModuleWhat it doesLab
src/token.rsTokenKind, Token { kind, span }1
src/lexer.rs&str → Vec<Token>, with errors that have spans1
src/ast.rsExpr, Stmt, Block — every node carries a Span2
src/parser.rstokens → AST: recursive descent for statements, Pratt for expressions2
src/interp/eval.rsthe first evaluator: AST → Value3
src/value.rsthe tagged union, truthiness, equality, the coercion rules4
src/error.rsDiagnostic rendering: source line, caret, message1–4
src/bin/ember.rsember tokens, ember ast, ember run1–3

The Layer You Are Building

   "10 + 20 * 3"                                          ← you never see this again
        │
        ▼
  ┌─────────────────────────────────────────────────────────────┐
  │  LEXER                                       lexer.rs       │
  │  a loop over bytes with one character of lookahead          │
  │  decides: is this a number, a name, an operator, a comment? │
  │  ATTACHES A SPAN TO EVERYTHING                              │
  └─────────────────────────────────────────────────────────────┘
        │  [Int(10)@0..2, Plus@3..4, Int(20)@5..7, Star@8..9, Int(3)@10..11, Eof]
        ▼
  ┌─────────────────────────────────────────────────────────────┐
  │  PARSER                                     parser.rs       │
  │  statements: recursive descent (one fn per production)      │
  │  expressions: Pratt / precedence climbing (one loop,        │
  │               a binding-power table, and recursion)         │
  │  PRECEDENCE BECOMES TREE SHAPE — this is the whole job      │
  └─────────────────────────────────────────────────────────────┘
        │  Binary(Add, Int(10), Binary(Mul, Int(20), Int(3)))
        ▼
  ┌─────────────────────────────────────────────────────────────┐
  │  EVALUATOR                              interp/eval.rs      │
  │  a recursive match over the tree. Post-order: children      │
  │  first, then the node. That IS the evaluation order.        │
  └─────────────────────────────────────────────────────────────┘
        │
        ▼
     Value::Integer(70)

Note: Notice what the parser does to precedence. 10 + 20 * 3 and (10 + 20) * 3 produce different trees, and once the tree exists there is no precedence left to get wrong — the evaluator just walks it. Precedence is a parsing problem that stops existing after parsing. That is the single most useful thing to understand about front ends.


The Concepts, and Where Each Is Treated

ConceptChapterWhy it matters later
Lexical analysis, tokens, maximal munch, lexer errorsLexical AnalysisThe lexer is the only place raw bytes exist; every later layer trusts its classification
Grammars, ambiguity, precedence, associativityGrammars and PrecedenceYou will design bytecode operand orders from the same reasoning
Recursive descent, Pratt parsing, binding powerRecursive Descent and Pratt ParsingThe parser is the second-largest file you write; its shape is a choice
The AST: node design, spans, ownership, visitorsThe ASTThe compiler in §3 consumes exactly this shape
Spans, source maps, line/column, diagnosticsSpans and Source Maps§6's diagnostics are impossible without a discipline established here

The Labs

LabTitleMilestoneYou end able to run
1The LexerM1ember tokens t.ember
2The Pratt ParserM1ember ast t.ember
3The First EvaluatorM1ember run t.ember → 70
4Values and Type ErrorsM2ember run -e '"x" * 2' → a diagnostic with a caret

Crates: What You Are Allowed to Use, and When

CrateStatusReason
logos, lexgenBanned in Section 1A generated lexer is 20 lines of macro and teaches you nothing about maximal munch, lookahead, or error spans. Write it by hand; you may benchmark against logos in §7 as a challenge.
chumsky, nom, lalrpop, pestBanned in Section 1Same reason, more so. Precedence handling is the lesson. A parser combinator hands you the answer and hides the question.
codespan-reporting, ariadne, mietteBanned until §6Rendering a caret under a span is forty lines and you should write them once. Then, in §6, compare yours to ariadne's and adopt whichever you prefer — with an ADR.
thiserror, anyhowBannedanyhow erases types, and this crate's errors are part of its public API. thiserror is fine in an application; here, writing the Display impl by hand keeps the message text under review.
std onlyRequired[dependencies] stays empty through Section 4.

The rule is not asceticism. It is that you cannot evaluate a library for a job you have never done. After Lab 2 you will have opinions about parser libraries. Before it you would only have preferences.


Deliverables

  • ember tokens, ember ast, and ember run all work on 10 + 20 * 3.
  • 2 ^ 3 ^ 2 evaluates to 512 and -2 ^ 2 to -4, each with a test naming the rule.
  • Every Token and every Expr/Stmt node carries a Span, checked by a test that walks the tree and asserts no node has Span::EMPTY.
  • A lexer error, a parser error, and a type error each render as: message, source line, caret under the exact span.
  • Value is defined, Copy, and size_of::<Value>() is asserted in a test.
  • The integer/float coercion table is implemented and tested for every operator/type pair.
  • tests/golden/ exists with at least 12 programs and expected outputs.
  • docs/learning/01-lexer.md, 02-parser.md, 03-ast.md written.
  • docs/adr/ADR-004-value-representation.md and ADR-005-integer-float-split.md written.

Common Mistakes in This Section

MistakeSymptomCorrection
Skipping spans "for now"Error messages say error: unexpected token with no location, foreverSpans are one u32 pair. Add them in Lab 1 or pay for them in Lab 24 with a rewrite.
Lexing .. as two . tokens"a" .. "b" becomes a parse error, or worse, parses as field accessMaximal munch: always take the longest matching token. Test .., ==, ~=, <=, >=, //.
Handling precedence with a nest of functionsSix near-identical parse_term/parse_factor functions; adding an operator means adding a functionThat is the textbook recursive-descent-only approach and it works, but Pratt does it in one loop and a table. Write the naive one, feel it, then replace it — the diff is the lesson.
Right-associativity by accident2 ^ 3 ^ 2 gives 64In the Pratt loop, recurse with bp for right-associative operators and bp + 1 for left-associative ones. One character. Test it.
Unary minus binding too loosely-2 ^ 2 gives 4Unary operators get their own binding power, higher than * but lower than ^. Check against lua -e 'print(-2^2)'.
f64 for all numbers1e17 + 1 == 1e17; array indices become 2.0You chose Lua 5.3+'s model in ADR-005. Integers are i64. Feel the difference in Lab 4.
Evaluating in the parserparse() returns 70Constant folding is an optimization in §7, and doing it in the parser destroys the tree the compiler needs. Parse, then evaluate.
Panicking on bad inputunwrap() on a malformed numberEvery fallible path returns Result<_, EmberError> from Lab 1. This is a production-readiness criterion, and the fuzzer in §6 will find every one you missed.

How to Verify Success

# 1. The pipeline, one representation at a time.
echo '10 + 20 * 3' > /tmp/t.ember
ember tokens /tmp/t.ember     # 6 tokens, spans contiguous and covering the input
ember ast    /tmp/t.ember     # Add at the root, Mul as its RIGHT child
ember run    /tmp/t.ember     # 70

# 2. Precedence, against the reference implementation.
for e in '10 + 20 * 3' '(10 + 20) * 3' '2 ^ 3 ^ 2' '-2 ^ 2' '1 - 2 - 3' '7 // 2' '7 % -3'; do
  printf '%-16s ember=%-8s lua=%s\n' "$e" \
    "$(ember run -e "return $e")" "$(lua -e "print($e)")"
done
# Every row must match. If one does not, YOU decide which is right and write it
# down in appendix/lua-differences.md — but you must notice first.

# 3. Diagnostics point at the right characters.
ember run -e 'return 1 + * 2'     # caret under the '*'
ember run -e 'return "x" * 2'     # caret under the '"x"', not the whole expression
ember run -e 'return 0x'          # lexer error, caret under '0x'

# 4. No panics.
ember tokens /dev/urandom 2>/dev/null | head -1   # an error, not a crash

Section Profile: What a Section 1 Graduate Can Do

  • Write a hand-rolled lexer for any small language in an afternoon, with correct maximal munch and spans on every token.
  • Explain precedence and associativity in terms of tree shape, and implement either with a one-line change in a Pratt loop.
  • Read the grammar section of a language reference manual and predict what its parser looks like.
  • Design an AST that a later compiler pass can consume without re-deriving information.
  • Produce a diagnostic with a source line and a caret, and explain why it required a decision made in the first hour of the project.
  • State the four common value representations and defend the choice of one.

Next: Lexical Analysis.