Recursive descent expresses grammar structure through function calls: expression functions call sub-expression functions, and statement functions call expression functions. LL analysis explains when you can select a production rule by looking at only a limited amount of lookahead; Pratt parsing organizes the decision-making for expression operators based on binding strength.
Hand-writing a recursive descent parser does not necessarily mean it is LL(1). Implementations may also use multi-token lookahead, backtracking, or semantic predicates; each added mechanism requires re-evaluating complexity, error recovery, and side effects.
From Grammar to Functions
Below, four arithmetic operations are described using EBNF repetition notation:
expr = term, { ("+" | "-"), term } ;
term = factor, { ("*" | "/"), factor } ;
factor = NUMBER | "(", expr, ")" ;
expr first parses a term, then loops to consume + or - operators followed by the next term; term handles * and / in the same way; factor handles numbers and parentheses. Higher precedence operators reside in deeper call layers, so 2 + 3 * 4 treats multiplication as the right subtree of addition.
A parsing function should define what it consumes, when it stops (before which tokens), and what it returns on success or failure. The entry point for a complete expression should also check for EOF; otherwise, 1 2 might be incorrectly parsed as a successful parse of 1.
Left-Recursion Elimination and Left-Associativity Are Not the Same Thing
Naive recursive descent directly implementing E → E - T | T will cause infinite recursion because it calls E again before consuming any input. Direct left-recursion can be rewritten as E → T E' and E' → - T E' | ε. Indirect left-recursion requires separate handling based on dependencies between non-terminals; some extended parsing algorithms also support left-recursion directly.
The right-recursive tail in the rewritten grammar does not imply that subtraction should be right-associative. When constructing the AST, use the current result as the left child of the new node, folding sequentially: after reading 8, you get Number; after reading - 3, you get Sub(8,3); after reading - 2, you get Sub(Sub(8,3),2).
FIRST, FOLLOW, and Predictive Sets
FIRST(α) is the set of terminal symbols that can begin strings derived from α; if α can derive the empty string, it also includes ε. FOLLOW(A) is the set of terminal symbols that can immediately follow A in some sentential form; the FOLLOW set of the start symbol includes the input end marker $.
When computing FIRST, propagate from left to right along the production rules; only continue to the next symbol if the previous one can be empty. For A → α B β, add FIRST(β) − {ε} to FOLLOW(B); if β can be empty, also add FOLLOW(A). For cyclic grammars, iterate until the sets no longer change.
Consider this simplified grammar:
E → T E'
E' → + T E' | ε
T → id
Non-terminal
FIRST
FOLLOW
E
{id}
{$}
E'
{+, ε}
{$}
T
{id}
{+, $}
For a production A → α, the predictive set is FIRST(α) − {ε}; if α can be empty, merge in FOLLOW(A). The predictive sets for different alternatives of the same non-terminal must be pairwise disjoint to build an unambiguous LL(1) selection. Checking only that FIRST sets are disjoint will miss conflicts involving empty productions.
In the example, when E' sees +, it chooses to continue; when it sees $, it chooses ε; when it sees id, it should report an error and cannot unconditionally take ε. The predictive table simply tabulates these decisions, while recursive descent implements them as function branches.
Why Empty Productions Compete for the Same Token as Regular Alternatives
Consider S → A a and A → a | ε, where lowercase a is a terminal. The FIRST sets for the two right-hand sides of A are {a} and {ε}, which appear disjoint; however, FOLLOW(A) contains a, so the predictive set for the empty production is also {a}. When the parser sees a at A, it cannot decide solely based on this single token whether to consume it or leave it for the end of S.
For an input of length 1 (a), the choice should be A→ε; for an input of length 2 (aa), the choice should be A→a. This small grammar is not ambiguous, but it does not satisfy the LL(1) prediction condition for this grammar; you can rewrite the grammar corresponding to the language, or use more lookahead. Do not equate predictive table conflicts directly with language ambiguity.
defpredict(first,nullable,follow):# first already excludes ε; nullable records whether it can be empty separately.
returnset(first)|(set(follow)ifnullableelseset())deftable(candidates):cells={}forrule,lookaheadsincandidates:fortokeninlookaheads:iftokenincells:raiseValueError(f"conflict on {token}: {cells[token]} / {rule}")cells[token]=rulereturncellsasserttable([("E' -> + T E'",predict({"+"},False,{"$"})),("E' -> empty",predict(set(),True,{"$"})),])=={"+":"E' -> + T E'","$":"E' -> empty"}try:table([("A -> a",predict({"a"},False,{"a"})),("A -> empty",predict(set(),True,{"a"})),])exceptValueErroraserror:assert"conflict on a"instr(error)else:raiseAssertionError("FIRST/FOLLOW conflict missed")
The code only constructs a predictive table for one non-terminal and is not responsible for computing the complete FIRST/FOLLOW sets for the entire grammar. It preserves the sources of the two conflicting productions to prevent later-inserted alternatives from silently overwriting existing ones; a real parser generator should also point out the relevant paths involving empty-string derivation and FOLLOW propagation to help locate grammar issues.
Why can epsilon compete for a lookahead?
Compare what production a consumes with what epsilon leaves for the caller. Under S→Aa both may see lookahead a: the short string a leaves it outside; aa consumes one inside. Changing S’s final terminal to b separates predictions. This changes the grammar’s prediction condition, not epsilon’s input consumption.
The empty production predicts FOLLOW(A). If S also ends with a, both candidates respond to a. The grammar is not LL(1), without implying ambiguity.
{"id":"nullable-prediction","title":"Why can epsilon compete for a lookahead?","summary":"The empty production predicts FOLLOW(A). If S also ends with a, both candidates respond to a. The grammar is not LL(1), without implying ambiguity.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"tail\"><button data-mode=\"a\" data-i18n=\"ta\"></button><button data-mode=\"b\" data-i18n=\"tb\"></button></div><div class=\"form-field\"><label for=\"look\" data-i18n=\"look\"></label><select id=\"look\"><option>a</option><option>b</option><option>$</option></select></div><p id=\"grammar\" class=\"source\"></p><div id=\"prediction\" class=\"table\"></div><p id=\"equation\" class=\"formula\"></p><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"conflict\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"unique\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"none\"></p></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#grammar{min-height:3.6em}#prediction .tr{grid-template-columns:1fr 1fr 40px}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let tail='a';function draw(){const look=$('#look').value,n=+(look==='a')+(look===tail);set('grammar','S → A '+tail+'\\nA → a | ε');table('prediction',[['A → a','FIRST(a) = {a}',{text:look==='a'?'✓':'—',cls:look==='a'?'on':''}],['A → ε','FOLLOW(A) = {'+tail+'}',{text:look===tail?'✓':'—',cls:look===tail?'gold':''}]]);set('equation','PRED(a) ∩ PRED(ε) = '+(tail==='a'?'{a}':'∅'));set('explanation',t(n===2?'conflict':n===1?'unique':'none'));$('#prediction').dataset.candidates=n;pressed(tail);}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{tail=b.dataset.mode;draw()});$('#look').onchange=draw;draw();","audio":false,"strings":{"conflict":"Both match: one lookahead cannot choose.","heading":"Why can epsilon compete for a lookahead?","intro":"S → A t; A → a or epsilon. Change t and lookahead to see which productions compete.","look":"Current lookahead","none":"No candidate: report an error rather than defaulting to epsilon.","prediction":"Prediction sets and matches","reset":"Reset","scope":"Two fixed A productions; S ends in a or b. A prediction-table model, not a general FIRST/FOLLOW solver. $ denotes EOF.","scopeLabel":"Model scope and assumptions","ta":"Final a","tail":"Final terminal t in S","tb":"Final b","unique":"Exactly one production matches this lookahead."}}
Common Prefixes, Ambiguity, and Backtracking
Left-factoring transforms A → αβ | αγ into A → αA' and A' → β | γ, delaying the choice until after the common prefix. However, this does not guarantee that any grammar becomes LL(1), nor does it automatically eliminate ambiguity.
The dangling else problem concerns which if block it should bind to. A language can specify matching the nearest unmatched if, require explicit end markers, or use grammar rules to distinguish matched and unmatched statements. Specific rules need to be stated; it is insufficient to simply call it "insufficient lookahead."
If speculative parsing backtracks, it must simultaneously restore the cursor, saved diagnostics, and other mutable state. Frequent retries may re-parse the same prefix; memoization increases memory usage and requires results to be correctly included in cache keys.
Pratt: Left and Right Binding Strength Control the Tree
Below is a runnable Python teaching implementation. The input is a list of already-tokenized tokens; numbers are non-negative decimal integers, and unary minus is parsed separately. Here, ^ is defined by this example language as exponentiation, unrelated to Python's bitwise XOR ^.
classParser:# (left binding power, right recursion threshold)
INFIX={"+":(10,11),"-":(10,11),"*":(20,21),"/":(20,21),"^":(30,30)}def__init__(self,tokens):self.tokens=list(tokens)+[None]self.pos=0defpeek(self):returnself.tokens[self.pos]deftake(self):token=self.peek()iftokenisNone:raiseSyntaxError("expected expression before EOF")self.pos+=1returntokendefexpr(self,minimum=0):token=self.take()iftoken.isascii()andtoken.isdecimal():left=("number",int(token))eliftoken=="-":left=("negate",self.expr(30))eliftoken=="(":left=self.expr()ifself.peek()!=")":raiseSyntaxError("expected closing parenthesis")self.take()else:raiseSyntaxError(f"expected expression, got {token!r}")whileself.peek()inself.INFIX:op=self.peek()lbp, rbp=self.INFIX[op]iflbp<minimum:breakself.take()left=(op,left,self.expr(rbp))returnleftdefparse(self):result=self.expr()ifself.peek()isnotNone:raiseSyntaxError(f"unexpected trailing token {self.peek()!r}")returnresultassertParser(["8","-","3","-","2"]).parse()==("-",("-",("number",8),("number",3)),("number",2))
This implementation allows operators with lbp >= minimum to enter the current call. Left-associative subtraction uses 11 for the right side, so the right side does not consume the next minus sign with lbp 10, leaving it for the outer loop. Right-associative exponentiation has both left and right thresholds at 30, allowing the right side to continue absorbing subsequent exponentiation.
Unary minus starts its operand at 30, so - 2 ^ 2 is parsed as -(2^2). This is a deliberately chosen language rule; modifying the thresholds can define different rules. Parentheses and EOF are not in the infix table, naturally terminating the loop, and should not be assigned a fictitious minimum infix priority.
A complete Pratt implementation can also configure prefix, postfix, and infix handlers for tokens, adding function calls, member access, or ternary expressions. This example focuses on binding direction and does not include source positions, resource limits, or error recovery; deep nesting in actual implementations also requires avoiding stack overflow. The author's tutorial demonstrates this token-organized approach to expression compilation. Crafting Interpreters Chapter 17
Tracking Thresholds to Observe Which Call Consumes Which Token
For 8 - 3 - 2, the outer level sees the first minus sign and recursively parses the right side with a threshold of 11. The right side reads 3, then sees the second minus sign with a left binding power of 10, which is less than 11, so it returns without consuming the minus sign. The outer level first forms 8−3, then continues to consume the second minus sign, finally resulting in (8−3)−2.
Expression Fragment
Current Threshold
Next Operator Strength
Decision
3 to the right of the first minus
11
- is 10
Leave for outer level, forming left-associativity
3 to the right of the first power
30
^ is 30
Current level continues, forming right-associativity
2 to the right of the prefix minus
30
^ is 30
Include power in the minus operand first
End of expression inside parentheses
Any
) is not infix
Return, consumed by parenthesis handler
Append the following assertions after the Parser definition to check the AST directly, without masking tree differences with evaluation results:
Empty input, trailing tokens, missing right operand, missing closing parenthesis, and illegal prefixes originate from different failure points. Checking these entry and exit conditions reveals more about whether the parser fully consumes input than just observing whether a single expression yields the correct number.
How does one binding threshold change the tree?
Binding power is a consumption threshold for a recursive parse, not evaluation speed. A restricted Pratt parser produces the tree, parenthesized form and result below. Changing subtraction’s right threshold changes associativity; lowering multiplication below addition changes grouping. The decision log explains the transformation rather than merely displaying different answers.
Preparing the visual
How does one binding threshold change the tree?
A right threshold above the next operator’s left binding power returns control outward; equality allows consumption and right association.
{"id":"pratt-binding-tree","title":"How does one binding threshold change the tree?","summary":"A right threshold above the next operator’s left binding power returns control outward; equality allows consumption and right association.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"expr\"><button data-mode=\"sub\" data-i18n=\"sub\"></button><button data-mode=\"pow\" data-i18n=\"pow\"></button><button data-mode=\"mix\" data-i18n=\"mix\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"right\"><span data-i18n=\"right\"></span></label><div class=\"control\"><label for=\"multiply\" data-i18n=\"multiply\"></label><output for=\"multiply\" id=\"multiply-value\"></output><input id=\"multiply\" type=\"range\" min=\"5\" max=\"25\" value=\"20\" step=\"1\"></div><svg id=\"treeplot\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\"></svg><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></strong></div></div><p id=\"tree\" class=\"source\"></p><section class=\"panel\"><h3 data-i18n=\"decisions\"></h3><div id=\"decisions\"></div></section><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n#treeplot{max-height:250px}#tree{min-height:3.6em}#decisions{font:12px/1.8 var(--viz-font-ui);white-space:pre-line;min-height:9em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}let mode='sub';function draw(){const tokens={sub:['8','-','3','-','2'],pow:['2','^','3','^','2'],mix:['2','+','3','*','4']}[mode],mul=+$('#multiply').value;set('multiply-value',mul);const bp={'+':[10,11],'-':[10,$('#right').checked?10:11],'*':[mul,mul+1],'^':[30,30]},log=[];let i=0;function parse(min){let left={v:tokens[i++]};while(i<tokens.length){const op=tokens[i],[l,r]=bp[op];log.push(op+': '+l+(l<min?' < ':' ≥ ')+min+' → '+(l<min?'return':'consume'));if(l<min)break;i++;left={op,a:left,b:parse(r)}}return left}const tree=parse(0);function format(n){return n.op?'('+format(n.a)+' '+n.op+' '+format(n.b)+')':n.v}function value(n){if(!n.op)return+n.v;const a=value(n.a),b=value(n.b);return n.op==='+'?a+b:n.op==='-'?a-b:n.op==='*'?a*b:a**b}const nodes=[],edges=[];function walk(n,x,y,w){const k=nodes.length;nodes.push({name:n.op||n.v,x,y,cls:n.op?'on':''});if(n.op){const a=walk(n.a,x-w,y+65,w/2),b=walk(n.b,x+w,y+65,w/2);edges.push([k,a],[k,b])}return k}walk(tree,180,30,90);graph('treeplot',nodes,edges);set('tree',format(tree));set('result',value(tree));set('decisions',log.join('\\n'));$('#tree').dataset.ast=JSON.stringify(tree);pressed(mode);}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});$('#right').onchange=draw;$('#multiply').oninput=draw;draw();","audio":false,"strings":{"decisions":"Recursive threshold decisions","expr":"Expression","heading":"How does one binding threshold change the tree?","intro":"Choose an expression, then change subtraction associativity or multiplication precedence. A Pratt parser shows the tree, fully parenthesized form and threshold decisions.","mix":"2 + 3 * 4","multiply":"Multiplication left binding power","pow":"2 ^ 3 ^ 2","reset":"Reset","result":"Evaluated result","right":"Right-associative subtraction","scope":"Finite examples of integer +, −, *, ^; ^ is right-associative exponentiation. No input parentheses or unary operators. Trees come from parsing; no cost or overflow model.","scopeLabel":"Model scope and assumptions","sub":"8 − 3 − 2","tree":"Fully parenthesized form"}}
Selection and Next Steps
Layered recursive descent is suitable for directly mapping language rules; Pratt is suitable for centrally managing expression operators. Generators can also provide good diagnostics, while hand-written parsers may have poor recovery; quality depends on implementation effort. For more general deterministic grammar handling, see LR Parsing; for the resulting tree structure, see AST Design and Error Recovery.
Keywords: recursive descent, LL(1), FIRST, FOLLOW, prediction set, left recursion, left factoring, Pratt parser, binding power, associativity