A
The word that matters is abstract. A concrete syntax tree (another name for the parse tree) mirrors the grammar exactly, warts and all. An abstract syntax tree mirrors the meaning. Two grammars that describe the same language — one factored to encode precedence, one using explicit parentheses — should, and do, produce the same AST. The AST is the point in the pipeline where the compiler stops caring how you wrote something and starts caring what you wrote.
Compare the two trees for the humble arithmetic expression. A grammar that enforces precedence with the
classic
| Kept in the AST | Dropped from the AST |
|---|---|
| operators & their operand structure | parentheses (structure already encodes grouping) |
| which node is the root of a subexpression | single-child "chain" productions ( |
| literals, identifiers, their values | separators, semicolons, layout, comments |
| the shape that determines evaluation order | the particular grammar rules that were fired |
The payoff is not merely smaller trees — it is uniformity. Every downstream pass can assume that
a Binary node has exactly a left child, an operator, and a right child, with no interleaved
precedence scaffolding to skip over. The precedence work was done once, by the parser, and is now baked
silently into the tree's shape.
Watch the concrete parse tree for = at the root, a + and a
× beneath, and four leaves. Same meaning, a third of the nodes.
Notice how the × sits below the + on the right subtree — precedence is
now purely positional. To evaluate, a post-order walk multiplies
An AST is a family of node types, one variant per language construct. In a typed language the natural
encoding is a discriminated union (a "sum type"): a tag field says which
variant this is, and the rest of the fields are specific to that variant. Traversals switch on the tag;
the compiler's exhaustiveness checker then guarantees you handled every kind of node.
Two design instincts guide the shape. First, make illegal states unrepresentable: a
binary node always has two operand fields, so you can never build a half-formed +.
Second, keep only semantic children: there is no field for the parentheses in
if, while, call,
lambda — and often a span field carrying the source line/column so later error
messages can point back at the original text.
You do not build the parse tree and then convert it — that would waste the very memory the AST exists to
save. Instead each parser routine returns an AST node directly. In a recursive-descent
parser this is delightfully natural: the function that recognises a term builds and returns a
binary node; the function that recognises a factor returns a num or a
var. The precedence encoded in the grammar becomes the nesting of the returned nodes — this
is a first taste of
The loop makes the tree left-leaning for binary node.
Once you have an AST, nearly everything you do to it is a traversal. You will want to walk it many times — to type-check, to constant-fold, to emit code, to pretty-print. Rather than scatter that recursion, compilers centralise it in a visitor: one dispatch on the node's tag, one handler per variant. New passes are new visitors; new node kinds add one case to each visitor (and the type checker reminds you where).
Two visitors, one tree. The same structure that evaluated to 16 pretty-prints with
the multiplication correctly nested inside the addition — because the nesting is the precedence.
Add an if node tomorrow and TypeScript's exhaustiveness check will flag both switches until
you handle it. That safety is why discriminated unions are the AST representation of choice.
Surprisingly close to it. Many production compilers define their AST as the boundary between a language-specific front end and everything after. Clang builds a C/C++/Objective-C AST that its analyses share; Rust and Swift both lower a surface AST into a simpler intermediate form. The trick that makes this work is that the AST already discarded the syntactic quirks — commas, layout, keyword spellings — and kept only the semantic skeleton, and skeletons across imperative languages look remarkably alike. It is why "a expression tree with typed nodes" is one of the most reused ideas in all of compiler engineering.
The classic misunderstanding is to think the AST is "the parse tree, tidied up" — same information, fewer keystrokes. It is not. The AST deliberately discards information the parse tree carried: the exact derivation, the single-child precedence chains, the parentheses. That is a feature, not a lossy accident. What it must preserve is structure — which operator governs which operands, in what order they nest. Get that backwards (drop a needed grouping, or keep the derivation "just in case") and you have either a wrong program or a bloated one. The rule of thumb: if two source texts mean the same computation, they should produce the same AST; if they mean different computations, they must not.