TRT
Sign in
Operating Systems

How a Parser Turns Tokens Into Structure

A lexer groups characters into tokens such as identifiers, numbers, and punctuation.

The Runtime Theory Team8 min read#lexer#parser#ast

A lexer groups characters into tokens such as identifiers, numbers, and punctuation; a parser consumes that token stream according to a grammar and builds a structured representation, usually an abstract syntax tree. The split exists because the two jobs have different difficulty: the lexer handles characters (encodings, escapes, comments, longest match), the parser handles structure (nesting, precedence, associativity). Keeping them apart is what lets an editor highlight strings without knowing anything about statements.

From characters to tokens

Scanning is rule-driven: start a token, consume characters while the rule still matches, stop at the first character that cannot extend it. The convention is maximal munch — the longest valid match wins, so price1 is one identifier, not price and 1, and 0x1F is a single integer literal rather than 0, x1F. Keywords are ordinary identifiers recognized afterward, which is why ifx is a variable and if is not.

text
$ echo 'foo(1, 2+bar);' | tokenize
IDENT foo   LPAREN   INT 1   COMMA   INT 2   PLUS   IDENT bar   RPAREN   SEMI

A hand-written scanner on a modern core runs at hundreds of megabytes per second — a 1 MB file is tokenized in well under a millisecond — so lexing is essentially never the bottleneck in a build. Its errors are byte-level: an unexpected character, an unterminated string, a number with a trailing 0x.

From tokens to a tree

The parser enforces the grammar: which sequences of tokens form a valid program. Two decisions shape the tree:

Precedence decides grouping. 2 + 3 * 4 must parse as +(2, *(3, 4)) so it evaluates to 14 rather than 20; 2 * 3 + 4 parses as +(*(2, 3), 4) = 10. Associativity decides ties: subtraction is left-associative, so a - b - c is (a - b) - c; had the parser built it right-associative the result would be a - (b - c), a different number for the same input.

A compact way to encode both is precedence climbing, which is what most hand-written expression parsers actually do:

js
function parseExpr(tokens, minPrec = 0) {
  let left = parsePrimary(tokens);
  while (prec(peek(tokens)) >= minPrec) {
    const op = next(tokens);
    const right = parseExpr(tokens, prec(op) + (rightAssoc(op) ? 0 : 1));
    left = { type: "bin", op, left, right };
  }
  return left;
}

Recursive descent like this is a direct transcription of a grammar into mutually recursive functions — readable, debuggable, and fast enough that parsing cost never shows up in a build.

Syntax is not meaning

A parser proves only that the shape is legal. foo(bar) parses whether or not foo is a function or bar exists; x + y parses whether or not + is defined for those types. Name resolution, type checking and other language rules belong to semantic analysis afterward — which is why those errors come from a later phase with source locations pointing at declarations, not at the token that failed.

Errors, recovery, and the AST you don't keep

Good parsers report the first error precisely and then recover: skip to a statement boundary (; or }), synchronize the token stream, and continue, so one typo yields one diagnostic instead of thirty. Editors depend on this — the language server re-parses the file on every keystroke and must return a usable tree even when the file is temporarily broken. That usable tree is often a concrete syntax tree that keeps comments and punctuation; the compact, whitespace-free AST used for evaluation or codegen is derived from it, dropping everything that does not affect meaning.

Key takeaways

  • Lexing is maximal-munch character matching; parsing is grammar enforcement over the resulting token stream — different layers, different error classes.
  • Precedence and associativity are properties of the grammar, not of the operators: they decide tree shape, and tree shape decides the answer.
  • Recursive-descent parsers (one function per nonterminal plus precedence climbing for expressions) are simple, fast, and the default choice for tools and compilers.
  • Syntactic acceptance says nothing about names or types; error recovery is what makes a parser usable inside an editor rather than only inside a build.

Production cost model and latency numbers

To apply How a Parser Turns Tokens Into Structure effectively in production systems, engineers must move beyond qualitative diagrams and quantify the exact hardware and network costs at each boundary:

Operation / BoundaryTypical LatencyDominant BottleneckProduction Mitigation
L1 / L2 Cache Access1 ns – 4 nsRegister dependency chains & pointer chasingContiguous memory layouts, cache-line alignment (64 B), and batch iteration
Main Memory (DRAM) Fetch60 ns – 100 nsTLB misses, random heap indirection, NUMA hopsHugePages (2 MB), arena/slab allocators, and pre-allocated ring buffers
Kernel Syscall / Context Switch1 µs – 5 µsUser-to-kernel mode switch, page-table flush, lock contentionNon-blocking readiness loops (epoll / io_uring), batching, and zero-copy I/O
NVMe SSD Random Read / fsync50 µs – 500 µsBlock I/O queue depth, write amplification, WAL flushesGroup commit, sequential append logs, and bounded buffer-pool pinning
Intra-Region RPC Roundtrip0.5 ms – 2.5 msSerialization, TCP/TLS framing, tail queueing ($L = \lambda W$)Connection pooling, deadline propagation, and p95 hedged requests

Failure modes, debugging, and operational checklist

In real incidents, systems rarely fail at average (p50) load—they fail when a slow dependency, skewed hot key, or resource leak pushes concurrency across a non-linear saturation cliff. When reviewing code or architecture that relies on How a Parser Turns Tokens Into Structure, walk through three verification questions:

  1. What is the invariant under partial failure or contention? If a worker crashes mid-operation, a network packet is delayed and reordered, or two threads race on the same state transition, prove which atomic primitive, version number, or idempotency key prevents silent corruption.
  2. How is backpressure enforced when demand exceeds capacity? Unbounded in-memory queues, unthrottled retries, and blocking calls without explicit deadlines turn a localized 100 ms slowdown into a fleet-wide Out-Of-Memory (OOM) cascade. Always bound queue depths, propagate caller deadlines, and shed excess load early.
  3. Which metrics and traces isolate the bottleneck in under 60 seconds? Instrument saturation, queue wait time vs service execution time, cache hit/miss ratios, and error rates by failure reason so on-call engineers can immediately distinguish a compute bottleneck from an I/O or lock-contention stall.

Spotted a mistake or something broken? Report a problem

Not started

Sign in to save your learning progress.

Sign in to save