Write down the expression "one plus two times three". Already you feel the itch: does it mean
We split syntax into two layers. Concrete syntax is the surface: the actual strings of characters, with their brackets, keywords, whitespace, and operator-precedence rules — everything a parser must wrestle with. Abstract syntax is what is left once parsing has done its job: a tree that records the essential structure and throws away the incidental punctuation. Theory — semantics, type systems, proofs — works almost entirely on the tree, because the tree is where the meaning lives. The string is merely how humans key the tree in.
Concrete syntax is defined by a context-free grammar, traditionally written in BNF (Backus–Naur Form). A grammar for our arithmetic language that builds in precedence and associativity needs several nonterminals — one "level" per precedence tier — plus parentheses for grouping:
This grammar is doing a lot of quiet work. Because expr recurses on its left
(expr "+" term), 1 - 2 - 3 groups as (1-2)-3. Because * lives on a
deeper tier than +, multiplication binds tighter, so
1 + 2 * 3 is forced to mean 1 + (2*3). And "(" expr ")" lets you
override both. All of this machinery — precedence tiers, associativity direction, the parentheses — is
concrete-syntax bureaucracy. It exists to make an unambiguous, comfortable-to-type surface
language. None of it survives into the tree.
Parse 1 + 2 * 3 and you get exactly one tree — and that tree, all by itself, resolves the
ambiguity we started with. The
This is an abstract syntax tree (AST). Notice what it omits: there are no
parentheses, no whitespace, no hint of which precedence tier each node came from — that scaffolding did
its job during parsing and was discarded. Notice too what it guarantees: the tree for
1 + 2 * 3 and the tree for the fully-parenthesised 1 + (2 * 3) are
identical. Two different strings, one abstract syntax — because they mean the same
thing. Conversely, (1 + 2) * 3 parses to a different tree (a
Here is the pivotal move. We can define the set of ASTs directly, with no strings and no parser, as an inductively-defined datatype. This is the grammar we actually reason about:
Read as an inductive definition, this says: the set
The inductive definition transcribes directly into a TypeScript algebraic datatype — one
variant per production — and any function over expressions is written by structural recursion,
one case per variant. Below, the same tree is built two ways (from the ambiguous-looking
1 + 2 * 3 and from the explicit 1 + (2*3)) and shown to be structurally equal,
while (1+2)*3 differs. Press Run:
The output makes the whole point concrete: two different strings collapse to one tree, and a third string that looks similar is a genuinely different tree. Once you are holding the tree, the string's ambiguity is simply gone — which is why every subsequent phase of a compiler, and every proof in this course, operates on the AST.
Working on abstract syntax is not a convenience — it is what makes the mathematics tractable. Three payoffs stand out:
+? at which
precedence tier?). Over trees it is a single, unambiguous clause.
This is why a language-theory paper will write
The notation is a direct fossil of the ALGOL era. John Backus (who had just led the FORTRAN project) proposed the metasyntax for the 1959 ALGOL 58 report; Peter Naur, editing the landmark 1960 ALGOL 60 report, refined and popularised it — hence Backus–Naur Form. It was arguably the first time a real programming language's syntax was defined with full mathematical precision, and it set the template every language since has followed. The phrase abstract syntax was crystallised a little later by John McCarthy (of Lisp fame), who pointed out that a compiler really cares about the abstract structure of a program, not its concrete character-level form — and Lisp took the idea to its logical extreme by letting you write the AST directly as nested parentheses (S-expressions), erasing the concrete/abstract gap almost entirely. That is why Lisp has "no syntax": you are typing the tree.
A frequent confusion is to think the abstract grammar 1+2*3 has two parse trees under it. That is not a bug; it is the whole point. An abstract
grammar is not a description of strings and is not handed to a parser. It is an
inductive definition of trees, and a tree cannot be ambiguous about its own grouping — it simply
is a particular shape. Disambiguation (precedence, associativity, parentheses) is the
responsibility of the concrete grammar, and it happens once, during parsing, before the
tree ever exists.
The mirror-image mistake is to drag concrete details into the abstract world — to store parentheses, source whitespace, or "which precedence level produced this node" inside AST nodes and then let a proof depend on them. Resist it. If a piece of information does not affect meaning, it does not belong in the abstract syntax. (Real compilers do keep source positions for error messages, but they hang them off to the side as annotations — the semantic shape of the tree stays pristine.)