Operator precedence parsing
How a parser decides the grouping of an expression: precedence and associativity of operators, the relations between terminals that drive an operator precedence parser (operator grammars, the push and reduce relations, the parser run step by step), how the same rules are written in a grammar or declared to yacc, how yacc resolves shift-reduce conflicts of an ambiguous grammar, and how the value of an expression depends on the table.
Why it exists
The string 5 + 3 * 2 could mean or . A grammar such as says nothing about it: it is ambiguous. Compilers settle the question with two rules for each operator, its precedence (which operator binds tighter) and its associativity (how a chain of equal operators groups: is ). There are three ways to put these rules into a parser: write them into the grammar (one nonterminal per precedence level), declare them to a parser generator (yacc's %left and %right), or use a parser driven directly by a table of precedence relations between terminals: the operator precedence parser. GATE asks to evaluate or parse an expression under an unusual precedence table, to read precedence and associativity off a grammar or a parse tree, and what yacc does with an ambiguous expression grammar.
Prerequisites
You need the stack, the handle and the shift and reduce moves of shift-reduce parsing, and the idea of an ambiguous grammar.
Precise definitions
Precedence and associativity. If operator has higher precedence than , then in the operator is applied first: . If is left associative, ; if right associative, . Operators of equal precedence are assumed to share one associativity. The precedence and associativity of the operators in a language are a table, and the value of an expression depends on it.
In a grammar. One nonterminal per precedence level, and recursion on the side of the associativity:
Left recursion () makes the operator left associative; right recursion () makes it right associative; an operator introduced in a lower level (here in , below in ) has higher precedence, because the parser must complete a before the can use it. The same grammar read from a parse tree: the operator nearest the root is applied last.
Operator grammar. An operator grammar has no production with an empty right side and none with two adjacent nonterminals. is one; with is not (two nonterminals side by side), and neither is . Operator precedence parsing is defined only for these.
The relations. An operator precedence parser looks only at terminals. For two terminals (the topmost terminal on the stack) and (the next input) the table gives one of
- : binds tighter, push ;
- : and belong to one handle (a pair of brackets), push ;
- : binds tighter (or equally, on the left), reduce first;
- empty: syntax error.
Built from the operator table: for two operators, if has higher precedence than , if lower; at equal precedence if they are left associative and if right associative. Also every operator, ) and $; every operator id and (; every operator ) and $; ( operators, id and (; ( ); $ operators, id and (; two operands in a row (id id), id followed by (, ) followed by id: error.
The parser. Stack of terminals and the nonterminal , starting with $; input followed by $. Repeat: let be the topmost terminal on the stack and the next input terminal. If a and b are both $ and the stack is $ E, accept. If or : shift . If : reduce: pop the handle, the symbols above the last terminal that is related to the one below it by (with an directly below included), and replace it by . If there is no relation: error. The handles are id, E op E and ( E ).
Properties. It is simple and fast, and handles the ambiguous-looking expression grammar with a small table (one entry per pair of terminals, not per state). It accepts only operator grammars; it can miss a syntax error in a grammar that is not carefully written, because it never looks at nonterminals; it handles unary minus awkwardly (the same token is unary and binary). Bottom-up parsers for a more general class of grammars are LR parsers.
Yacc and ambiguous grammars. Given , a parser generator builds an LALR(1) table with shift-reduce conflicts: after with lookahead * it can reduce by or shift the *. Yacc reports each conflict and resolves it by rules:
- If the production and the lookahead token both have declared precedence (
%left,%right,%nonassoc; later declarations bind tighter; a production takes the precedence of its last terminal): the higher wins: reduce if the production is higher, shift if the token is higher. - If equal:
%leftmeans reduce,%rightmeans shift,%nonassocmeans error. - If either has no declared precedence: shift (the default). A reduce-reduce conflict is resolved in favour of the production that appears first.
So an expression grammar with no declarations parses everything as if all operators were right associative and the rightmost operator tightest: becomes .
How it works
The relation table for (left), (left, tighter) and (right, tightest), built from precedence and associativity alone. Row: the topmost terminal on the stack; column: the next input.
Operator precedence relations for + (left), * (left, tighter) and ^ (right, tightest): start
Computed by a script, not typed| stack top ↓ / next input → | + | * | ^ | id | ( | ) | $ |
|---|---|---|---|---|---|---|---|
| + | |||||||
| * | |||||||
| ^ | |||||||
| id | |||||||
| ( | |||||||
| ) | |||||||
| $ |
Step 0
⋖: the input terminal binds tighter, so push it. ≐: push, the two belong to one handle (brackets). ⋗: the stack top binds tighter or equally on the left, so reduce first. Equal precedence: left associative gives ⋗, right associative gives ⋖. Empty cell: syntax error.
A parse of : the stack, the input, the relation that was consulted (outlined in the table), and the tree that grows. Each forces a reduce, so is reduced first, then , then .
Operator precedence parse of id + id * id ^ id: start
Parser run by a script, not typedOperator precedence parsing: compare the topmost terminal on the stack with the next input terminal in the table; ⋖ and ≐ push, ⋗ reduces the handle.
- 1E → E + E | E * E | E ^ E | ( E ) | id
| stack ↓ / input → | + | * | ^ | id | ( | ) | $ |
|---|---|---|---|---|---|---|---|
| + | ⋗ | ⋖ | ⋖ | ⋖ | ⋖ | ⋗ | ⋗ |
| * | ⋗ | ⋗ | ⋖ | ⋖ | ⋖ | ⋗ | ⋗ |
| ^ | ⋗ | ⋗ | ⋖ | ⋖ | ⋖ | ⋗ | ⋗ |
| id | ⋗ | ⋗ | ⋗ | ⋗ | ⋗ | ||
| ( | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ | ≐ | |
| ) | ⋗ | ⋗ | ⋗ | ⋗ | ⋗ | ||
| $ | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ |
Step 0
The stack holds $; the input is followed by $. The parser looks at the topmost TERMINAL on the stack and the next input terminal, and the table says push (⋖ or ≐) or reduce (⋗). Watch the tree: * is reduced before +, and ^ before *.
Right associativity. : since , the second is pushed instead of reducing the first, and the tree leans to the right.
Operator precedence parse of id ^ id ^ id (^ is right associative): start
Parser run by a script, not typedOperator precedence parsing: compare the topmost terminal on the stack with the next input terminal in the table; ⋖ and ≐ push, ⋗ reduces the handle.
- 1E → E + E | E * E | E ^ E | ( E ) | id
| stack ↓ / input → | + | * | ^ | id | ( | ) | $ |
|---|---|---|---|---|---|---|---|
| + | ⋗ | ⋖ | ⋖ | ⋖ | ⋖ | ⋗ | ⋗ |
| * | ⋗ | ⋗ | ⋖ | ⋖ | ⋖ | ⋗ | ⋗ |
| ^ | ⋗ | ⋗ | ⋖ | ⋖ | ⋖ | ⋗ | ⋗ |
| id | ⋗ | ⋗ | ⋗ | ⋗ | ⋗ | ||
| ( | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ | ≐ | |
| ) | ⋗ | ⋗ | ⋗ | ⋗ | ⋗ | ||
| $ | ⋖ | ⋖ | ⋖ | ⋖ | ⋖ |
Step 0
^ is right associative, so ^ ⋖ ^ in the table: the second ^ is pushed instead of reducing the first. The tree leans to the right.
The same string under four tables. Precedence and associativity decide the value.
The expression 5 + 3 * 2 - 8 / 4 + 1 under four precedence tables: start
Computed by a script, not typed| precedence table | tree the parser builds | value |
|---|---|---|
| the usual: * and / tighter than + and -, all left associative | ||
| all four operators equal, left associative (a pocket calculator) | ||
| all four equal, right associative | ||
| + tightest (left), then - (right), then * (right), then / (right) |
Step 0
The same string of tokens has different values under different precedence and associativity rules: the rules, not the string, define the meaning. Each value was computed three ways (operator precedence parser, precedence climbing, shunting-yard).
Yacc. The ambiguous grammar has four conflicts:
The LALR(1) table of E → E + E | E * E | id has four conflicts: start
Computed by a script, not typed| cell of the table | entries | meaning |
|---|---|---|
| state 5, next token * | ||
| state 5, next token + | ||
| state 6, next token * | ||
| state 6, next token + |
Step 0
Both states hold a completed E op E and E op . E items; every operator as lookahead can both reduce and shift. A parser generator reports shift-reduce conflicts and resolves them by its rules.
and the parse tree of four inputs depends on the declarations:
How yacc resolves the conflicts: the parse tree of four inputs under three sets of declarations: start
Computed by a script, not typed| input | no declarations: yacc shifts | %left '+' then %left '*' (+ looser) | %right '+' then %left '*' |
|---|---|---|---|
| id + id * id | |||
| id * id + id | |||
| id + id + id | |||
| id * id * id |
Step 0
Rule used: a shift-reduce conflict is decided by comparing the precedence of the production (that of its last terminal) with that of the next token: higher wins; equal: %left reduces, %right shifts; without precedence on either side: shift. Run by script on the LALR(1) table.
Grammars. Associativity and precedence read off a grammar by listing all its parse trees for a string, and which grammars are operator grammars:
Reading associativity and precedence off a grammar (all parse trees of a sample string): start
Computed by a script, not typed| grammar | string | trees | the parse tree (brackets show the grouping) |
|---|---|---|---|
| E → E + T | T, T → T * F | F, F → id | |||
| E → T + E | T, T → F * T | F, F → id | |||
| E → E + E | E * E | id (ambiguous) | |||
| S → T * P, T → U | T * U, P → Q + P | Q, Q → id, U → id |
Step 0
All parse trees of the string for the grammar were enumerated by script (memoised recursive derivation); an unambiguous grammar gives exactly one.
Which grammars are operator grammars?: start
Computed by a script, not typed| grammar | operator grammar? | reason |
|---|---|---|
| E → E + E | E * E | id | ||
| E → E A E | id, A → + | * | ||
| S → a S b | ε | ||
| E → E + T | T, T → T * F | F, F → ( E ) | id |
Step 0
Operator grammars may use any number of nonterminals but no ε-productions and no production with two adjacent nonterminals. Checked by script.
Worked examples
EX 1The value depends on the table
EasyEvaluate (integer division). With the usual table ( and tighter, all left associative) it is . If all four operators have equal precedence and are left associative (a pocket calculator) it is . If they are equal and right associative it is . For a table in which binds tightest, then , then , then (the last three right associative) it is (division truncates towards zero). A C program computes all four with the shunting-yard algorithm:
#include <stdio.h>
/* operators + - * / are indexes 0..3; prec: higher binds tighter; right[i]: 1 if right associative */
static int apply(char op, int a, int b) {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
default: return a / b;
}
}
static int index_of(char op) {
return op == '+' ? 0 : op == '-' ? 1 : op == '*' ? 2 : 3;
}
static int eval(const int *vals, const char *ops, int n, const int *prec, const int *right) {
int out[16], top = -1;
char stack[16];
int sp = -1;
out[++top] = vals[0];
for (int i = 0; i < n - 1; i++) {
int p = prec[index_of(ops[i])], r = right[index_of(ops[i])];
while (sp >= 0 && (prec[index_of(stack[sp])] > p || (prec[index_of(stack[sp])] == p && !r))) {
int b = out[top--], a = out[top--];
out[++top] = apply(stack[sp--], a, b);
}
stack[++sp] = ops[i];
out[++top] = vals[i + 1];
}
while (sp >= 0) {
int b = out[top--], a = out[top--];
out[++top] = apply(stack[sp--], a, b);
}
return out[0];
}
int main(void) {
int vals[6] = {5, 3, 2, 8, 4, 1};
char ops[5] = {'+', '*', '-', '/', '+'};
/* order of the columns: + - * / */
int p1[4] = {1, 1, 2, 2}, r1[4] = {0, 0, 0, 0};
int p2[4] = {1, 1, 1, 1}, r2[4] = {0, 0, 0, 0};
int p3[4] = {1, 1, 1, 1}, r3[4] = {1, 1, 1, 1};
int p4[4] = {4, 3, 2, 1}, r4[4] = {0, 1, 1, 1};
printf("usual table: %d\n", eval(vals, ops, 6, p1, r1));
printf("all equal, left associative: %d\n", eval(vals, ops, 6, p2, r2));
printf("all equal, right associative: %d\n", eval(vals, ops, 6, p3, r3));
printf("+ tightest, then -, *, / (right): %d\n", eval(vals, ops, 6, p4, r4));
return 0;
}usual table: 10
all equal, left associative: 3
all equal, right associative: 8
+ tightest, then -, *, / (right): -9Reading it. The algorithm keeps a stack of operators: before pushing an operator it first applies every stacked operator that binds tighter, or equally tight when the new operator is left associative. That is exactly the relation table: means "apply the one on the stack first". Same tokens, four tables, four values.
EX 2The parser run in C
MediumThe operator precedence parser itself, with the relation table of the page, parsing . Each line shows the stack, the remaining input and the move.
#include <stdio.h>
#include <string.h>
/* terminals in the order + * ^ i ( ) $ ; i stands for id; the stack also holds E */
static const char *T = "+*^i()$";
static const char rel[7][8] = { /* rel[a][b]: stack top a, next input b; '<' push, '=' push, '>' reduce, ' ' error */
{'>', '<', '<', '<', '<', '>', '>'},
{'>', '>', '<', '<', '<', '>', '>'},
{'>', '>', '<', '<', '<', '>', '>'},
{'>', '>', '>', ' ', ' ', '>', '>'},
{'<', '<', '<', '<', '<', '=', ' '},
{'>', '>', '>', ' ', ' ', '>', '>'},
{'<', '<', '<', '<', '<', ' ', ' '}};
static int idx(char c) { return (int)(strchr(T, c) - T); }
static void show(const char *stack, int n, const char *in) {
printf("stack:");
for (int i = 0; i < n; i++) printf(stack[i] == 'i' ? " id" : " %c", stack[i]);
printf(" input:");
for (const char *p = in; *p; p++) printf(*p == 'i' ? " id" : " %c", *p);
}
int main(void) {
char stack[32] = "$";
int n = 1;
const char *in = "i+i*i^i$";
show(stack, n, in);
printf(" start\n");
for (;;) {
int top = n - 1;
while (stack[top] == 'E') top--;
char a = stack[top], b = *in;
if (a == '$' && b == '$' && n == 2 && stack[1] == 'E') {
printf("accept\n");
return 0;
}
char r = rel[idx(a)][idx(b)];
if (r == ' ') {
printf("error\n");
return 1;
}
if (r == '<' || r == '=') {
stack[n++] = b;
in++;
show(stack, n, in);
printf(" shift %s\n", b == 'i' ? "id" : (char[]){b, 0});
} else {
int k = n; /* find the start of the handle */
for (;;) {
k--;
if (stack[k] == 'E') continue;
int below = k - 1;
while (below >= 0 && stack[below] == 'E') below--;
if (below < 0 || rel[idx(stack[below])][idx(stack[k])] == '<') break;
}
if (k > 0 && stack[k - 1] == 'E') k--;
int len = n - k;
n = k;
stack[n++] = 'E';
show(stack, n, in);
printf(" reduce %d symbol%s to E\n", len, len == 1 ? "" : "s");
}
}
}stack: $ input: id + id * id ^ id $ start
stack: $ id input: + id * id ^ id $ shift id
stack: $ E input: + id * id ^ id $ reduce 1 symbol to E
stack: $ E + input: id * id ^ id $ shift +
stack: $ E + id input: * id ^ id $ shift id
stack: $ E + E input: * id ^ id $ reduce 1 symbol to E
stack: $ E + E * input: id ^ id $ shift *
stack: $ E + E * id input: ^ id $ shift id
stack: $ E + E * E input: ^ id $ reduce 1 symbol to E
stack: $ E + E * E ^ input: id $ shift ^
stack: $ E + E * E ^ id input: $ shift id
stack: $ E + E * E ^ E input: $ reduce 1 symbol to E
stack: $ E + E * E input: $ reduce 3 symbols to E
stack: $ E + E input: $ reduce 3 symbols to E
stack: $ E input: $ reduce 3 symbols to E
acceptReading it. The first move shifts id (since $ ), the second reduces it (). When the stack holds $ E + E and the next input is *, the topmost terminal is + and : the * is pushed, not reduced; the same happens for and . Only when the input ends do the reductions run from the inside out: , then , then . The parse tree is . A reduce takes the handle from the last on the stack, which is why E op E is replaced as a whole.
EX 3What does the compiler itself do?
MediumA real C compiler follows the same kind of table: the C operators , , are tighter than , ; both groups are left associative; assignment is right associative. The program prints what the compiler computes.
#include <stdio.h>
int main(void) {
int a, b;
printf("5 + 3 * 2 - 8 / 4 + 1 = %d\n", 5 + 3 * 2 - 8 / 4 + 1);
printf("8 - 4 - 2 = %d (left associative: (8 - 4) - 2)\n", 8 - 4 - 2);
printf("100 / 10 / 5 = %d (left associative: (100 / 10) / 5)\n", 100 / 10 / 5);
printf("2 * 3 %% 4 = %d (same level, left to right: (2 * 3) %% 4)\n", 2 * 3 % 4);
printf("-2 * -3 = %d (unary minus binds tighter than *)\n", -2 * -3);
a = b = 7;
printf("a = b = 7: a is %d, b is %d (assignment is right associative: a = (b = 7))\n", a, b);
printf("1 + 2 * 3 > 6 && 4 < 5 = %d (arithmetic, then comparison, then &&)\n", 1 + 2 * 3 > 6 && 4 < 5);
return 0;
}5 + 3 * 2 - 8 / 4 + 1 = 10
8 - 4 - 2 = 2 (left associative: (8 - 4) - 2)
100 / 10 / 5 = 2 (left associative: (100 / 10) / 5)
2 * 3 % 4 = 2 (same level, left to right: (2 * 3) % 4)
-2 * -3 = 6 (unary minus binds tighter than *)
a = b = 7: a is 7, b is 7 (assignment is right associative: a = (b = 7))
1 + 2 * 3 > 6 && 4 < 5 = 1 (arithmetic, then comparison, then &&)Reading it. The first line is the same as in the first example. Subtraction and division are left associative: , not . The assignment is right associative, which is why it works. A question that gives a table different from C's asks you to apply its table, not the one you know.
EX 4Reading precedence and associativity from a grammar
GATE levelFor the grammar , , , , : the production is left recursive, so is left associative ( groups to the left); is right recursive, so is right associative. The table of the page lists the single parse tree of : : the stars group left, the pluses right, and the top-level joins the two parts. The statement " is right associative while is left associative" is true; "both are left associative" and "both right" are false. To decide, ask which side the recursion is on.
To read the same from a given parse tree of an expression with two operators, say a # b @ c @ d # e # f: if the tree groups the @ operators before the # operators (the @ subtrees sit lower), then @ is applied first and has the higher precedence; and the way a chain of one operator leans tells its associativity: ((b @ c) @ d) is left associative, (b @ (c @ d)) right. Always find which operator sits nearer the root (applied last: lower precedence) and which way each chain leans.
EX 5What does yacc do with the ambiguous grammar?
GATE levelThe grammar is fed to yacc with no precedence declarations. The LALR(1) table has shift-reduce conflicts (four of them: in the state after and after , on lookahead and ). Yacc reports them, and resolves each in favour of the shift: so is parsed as (14), which happens to be right, but as (14 instead of 10), and as : every operator behaves as if right associative and the rightmost tightest. There is no reduce-reduce conflict here, and nothing is eliminated (yacc does not remove recursion). With %left '+' and then %left '*' yacc compares precedences instead and gets the usual tree: the declarations replace the grammar's missing levels. The table of the page computes all of this.
Cheat box
Cheat box
- Precedence: which binds tighter. Associativity: how a chain groups. The value of an expression depends on both.
- Grammar: left recursion left associative, right recursion right associative; lower level higher precedence.
- Operator grammar: no , no two adjacent nonterminals.
- Relations between terminals: push (input binds tighter), brackets, reduce. Equal precedence: left , right .
$everything that starts an expression;idoperators. - Handle: between the last and the top;
id,E op E,( E ). - Yacc: shift-reduce conflict resolved by comparing precedence of the production (its last terminal) with the lookahead; equal:
%leftreduces,%rightshifts; no precedence: shift. Reduce-reduce: first production. - Ambiguous : 4 conflicts; operators give .
Traps and GATE patterns
Connections
Across subjects
No cross-subject links yet.
An operator precedence parser is a shift-reduce parser whose handle-finding is replaced by a table over terminals. The same information is hidden in the extra nonterminals of the layered grammar that removes the ambiguity, and in yacc's declarations for the LALR(1) parser of CLR and LALR; compare the classes of grammars in the parser comparison.
PYQs
5 previous year questions from your book. I re-solved each one myself (3 by running or simulating, 2 by reasoning only); the book's key is shown next to my answer. All PYQs
Consider the operator precedence and associativity
rules for the integer arithmetic operators given in
the table below.
Operator
Precedence
Associativity
+
Highest
Left
-
High
Right
*
Medium
Right
/
Low
Right
The value of the expression 3 + 1 + 5 *2/7 +
2 - 4 -7 -6/2 as per the above rules is ___Consider the following parse tree for the expression a#b$c$d#e#f, involving two binary operators $ and #. Which one of the following is correct for the given parse tree?
- (a)$ has higher precedence and is left associative; # is right associative
- (b)# has higher precedence and is left associative; $ is right associative
- (c)$ has higher precedence and is left associative; # is left associative
- (d)# has higher precedence and is right associative; $ is left associative
Consider the grammar defined by the following production rules, with two operators * and + S → T * P T → U | T * U P → Q + P | Q Q → Id U → Id Which one of the following is TRUE?
- (a)+ is left associative, while * is right associative
- (b)+ is right associative, while * is left associative
- (c)Both + and * are right associative
- (d)Both + and * are left associative
Consider two binary operators '↑' and '↓' with the precedence of operator ↓ being lower than that of the operator ↑. Operator ↑ is right associative while operator ↓ is left associative. Which one of the following represents the parse tree for expression (7↓3↑4↑3↓2) ?
- (a)
- (b)
- (c)
- (d)COMPILER DESIGN
Consider the following expression grammar. The
seman-tic rules for expression calculation are stated
next to each grammar production.
E → number
E.val = number. val
| E '+' E
E(1).val = E(2).val + E(3).val
| E '×' E
E(1).val = E(2).val × E(3).val
The above grammar and the semantic rules are fed to
a Yacc tool (which is an LALR (1) parser generator)
for parsing and evaluating arithmetic expressions.
Which one of the following is true about the action of
Yacc for the given grammar?- (a)It detects recursion and eliminates recursion
- (b)It detects reduce-reduce conflict, and resolves
- (c)It detects shift-reduce conflict, and resolves the conflict in favor of a shift over a reduce action
- (d)It detects shift-reduce conflict, and resolves the conflict in favor of a reduce over a shift action
Self-check
Question 1
has higher precedence than , both left associative. Value of ?
Show answerHide
Apply first: , with left associative: .
Question 2
Grammar , . Is left or right associative?
Show answerHide
Right: the production is right recursive, .
Question 3
In an operator precedence table, what relation is if has higher precedence, and ?
Show answerHide
(push the ), (reduce the first).
Question 4
Is an operator grammar? Is , , ?
Show answerHide
The first: yes (no , no two adjacent nonterminals). The second: no ( adjacent).
Question 5
A yacc grammar for has no declarations. How does yacc parse ?
Show answerHide
The conflict after with lookahead is resolved by shift: , not the usual . %left '-' fixes it.
Revision card
One-screen revision
- Precedence: tighter first. Associativity: chain grouping. Grammar: left recursion = left associative; lower level = higher precedence.
- Operator grammar: no , no adjacent nonterminals. Relations on terminals: push, , reduce; equal precedence: left , right .
- Yacc: precedence of production vs lookahead; equal: left reduces, right shifts; none: shift.
- The same tokens have different values under different tables.
References
- ·Aho, Lam, Sethi, Ullman, Compilers: Principles, Techniques, and Tools (2nd edition), Chapter 4, Section 4.8 (Using ambiguous grammars: precedence and associativity in yacc); operator precedence parsing is Section 4.6 of the 1st edition
- ·Aho, Ullman, Principles of Compiler Design, Chapter 5 (operator precedence grammars)
- ·Grune, Jacobs, Parsing Techniques, operator precedence grammars
Verification
The operator precedence parser, the relation table, the yacc-style conflict resolution on the LALR(1) table and the enumeration of parse trees are in scripts/cd_prec.py; scripts/enum-checks/cd_prec.py recomputes them independently: 1500 random flat expressions under random tables (parser = precedence climbing = shunting-yard = the unique bracketing of all Catalan-many that the table allows), 3000 bracketed renderings parsed back to the tree they came from, every one of the 97656 token strings up to length 7 over five symbols (the parser accepts exactly the 60 strings of the expression language), the relation table's properties, yacc's resolution against the operator table with k * k conflict cells for k operators, and the Catalan counts of the ambiguous grammar; it also compares the C programs printed on the page (the shunting-yard values under four tables, the operator precedence parser's steps) and tests/cd-prec-traces.test.ts recomputes the tables. The C operator results come from the real compiler. The relations follow the textbook construction (higher precedence binds tighter; equal precedence: left associative gives the reduce relation, right associative the push relation); a textbook may print the symbols differently. Unary operators and the ambiguity of operator precedence parsing on grammars that are not operator grammars are only mentioned. The 1st-edition section numbers are from memory. The independent second pass has not been run, so the page stays draft. SECOND PASS (2026-11-02, done by the assistant, not a human check): every Self-check question was solved before the printed answer was read and all agreed; the definitions, worked examples, cheat box and traps were read claim by claim and the numbers re-derived by hand or by a small script (token counts, FIRST and FOLLOW sets, LL(1) tables and moves, LR item sets, state counts and parses, operator-precedence values, attribute evaluation and printing schemes, activation-record counts and access links, parameter-passing results, three-address code, jumping and backpatched code, leaders and blocks, value numbering and DAGs, gen/kill and data flow sets, Sethi-Ullman labels and live ranges); long C programs were not re-read line by line (they are compiled and compared by npm run verify:c, and their printed outputs were checked against the prose); tables and animations are machine output and were read, not re-run; No other defect found. Status raised to self-checked; verified needs a textbook or human check.
last verified: never
My notes (saved in this browser)