Regular expressions describe a set of strings, and finite automata determine whether an input belongs to that set. Lexical analyzers must additionally answer: starting from the current position, how long should we capture, and which token type does it belong to? Understanding the distinction between these three problems is essential for applying automata theory to real-world lexers.
Regular Languages: Remembering a Finite Number of Cases
Concatenation (ab), alternation (a|b), repetition (a*), along with the empty string and the empty language, constitute the basic operations of classic regular expressions. Character classes and operators like + and ? are typically convenient syntactic sugar. For example, [0-9]+ represents one or more decimal digits.
The "finite" in finite automata refers to a fixed number of states that does not grow with the input. It can remember whether a decimal point has appeared in the current number or whether the string ends with a specific fixed suffix; generally, it cannot remember arbitrarily deep bracket nesting with a fixed number of states. Language rules such as nested comments can be handled by lexers using additional counters, but this goes beyond pure finite automata.
The syntax of modern regex libraries is not exactly equivalent to classic regular languages. Capture groups record match positions but do not themselves push the language beyond the regular class; backreferences require re-matching arbitrary-length content previously matched, which may describe non-regular languages. RE2 supports capture groups but does not support backreferences. RE2 Syntax
NFA: Preserving All Still-Possible Paths in Parallel
An NFA allows a single state to transition to multiple states on the same character, and also allows ε-transitions that do not consume input. An input is accepted if there exists at least one accepting path; it is not required that all paths succeed.
Thompson's construction transforms each sub-expression of a regular expression into a fragment with an entry and exit, then composes these fragments: concatenation connects the exit of the previous fragment to the entry of the next; alternation adds ε-branches and merges; the Kleene star adds paths to "skip the entire fragment" and to "return from the exit to the entry for repetition." A graph without back-edges can only represent optional occurrences, not arbitrary repetitions.
For the expanded basic regular expression, the number of constructed states is proportional to the size of the expression. This conclusion discusses the size of the automaton and cannot be used directly to compare the matching time of backtracking engines.
NFA simulation maintains a set of active states, updating the set for each character read and filling in states reachable via ε. Deduplicating states in each round avoids repeatedly exploring the same "state, input position" combinations. Let the NFA have m states and the input length be n; the time complexity of simple Thompson simulation is O(mn), with active set space complexity O(m). For fixed patterns, this is often referred to as linear in the input size. Russ Cox: NFA Simulation
DFA: Treating a Set of NFA States as a Single State
A DFA has exactly one next state for each state and input symbol. Subset construction pre-encodes "which NFA states might we be in" into a DFA state.
In the diagram, after reading ab, the set contains q2, so it accepts; if we continue reading a, we return to set B, and the current entire string no longer ends with ab. The state represents the information that must be remembered after reading the entire current prefix.
The steps of subset construction are:
The initial set is ε-closure({start}), where the closure includes the set itself.
For a set S and character c, compute T = ε-closure(move(S, c)).
If T has no number yet, assign a new state and add it to the work queue; record the transition from S on c to T.
Until no new sets remain. Sets containing any NFA accepting state are marked as accepting.
The empty set can be represented as an explicit dead state or by missing transitions. Practical implementations often handle character equivalence classes or ranges, without enumerating all Unicode code points.
If the NFA has m states, there can be up to 2m potential subsets; generating only reachable subsets can still result in exponential growth. There is no guarantee that the NFA and DFA state counts are similar for real-world lexical rules.
An intuitive example is (0|1)*1(0|1){k-1}: it requires the k-th-to-last character to be 1. The NFA can guess which 1 is the target, while the DFA needs to distinguish combinations of the most recent k bits; different k-bit suffixes can be distinguished by subsequent input, requiring at least 2k states. Conversely, (a*)* and a* describe the same language, so this is not an example of minimal DFA exponential growth.
Implementing Subset Construction: Computing All Three Sets in the Graph
The NFA in the graph does not require ε-edges, so the set after move is itself the next set. Below, we directly simulate the NFA, construct the reachable DFA, and finally compare results for all 127 strings of length 0 to 6 over the alphabet {a,b}.
The transition table shows why an accepting state is not "always successful": C transitions to B on a, and back to A on b. Here, we are checking if the entire input ends with ab; if the requirement is to search for ab appearing at any position, the control logic or automaton after acceptance must be defined separately.
After adding ε-edges, the start point and the result of each move must take the closure. The closure model below allows ε-cycles, ensuring each state is expanded only once via a visited set:
Distinguish taking one NFA path from storing one name for a whole set. The two views below represent the same prefix: the NFA keeps all possibilities, while A/B/C name entire sets. Edit input or select a prefix to compare acceptance. Reading a after ab leaves acceptance; having accepted an earlier prefix is not permanent acceptance.
Sk+1=ε-closure(move(Sk,ck))
Preparing the visual
Which NFA states live inside one DFA state?
Each character updates the whole active set. q0 remains; a adds q1 and a following b reaches q2. DFA states name these sets; acceptance can disappear after more input.
{"id":"automaton-state-subsets","title":"Which NFA states live inside one DFA state?","summary":"Each character updates the whole active set. q0 remains; a adds q1 and a following b reaches q2. DFA states name these sets; acceptance can disappear after more input.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"form-field\"><span data-i18n=\"input\"></span><input type=\"text\" id=\"input\" value=\"aabab\" maxlength=\"8\" autocomplete=\"off\" spellcheck=\"false\"></label><div class=\"control\"><label for=\"prefix\" data-i18n=\"prefix\"></label><output for=\"prefix\" id=\"prefix-value\"></output><input id=\"prefix\" type=\"range\" min=\"0\" max=\"5\" value=\"5\" step=\"1\"></div><div id=\"chars\" class=\"track\"></div><svg id=\"nfa\" class=\"diagram\" viewBox=\"0 0 360 180\" role=\"img\"></svg><div class=\"metrics\"><div><span data-i18n=\"active\"></span><strong id=\"active\"></strong></div><div><span data-i18n=\"dfa\"></span><strong id=\"dfa\"></strong></div></div><div id=\"transitions\" 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=\"accept\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"reject\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"invalid\"></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.metrics strong{font-size:19px}.table .tr{grid-template-columns:repeat(3,minmax(0,1fr))}","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;}));}function draw(){const src=$('#input').value,valid=/^[ab]{0,8}$/.test(src);$('#prefix').max=src.length;const n=Math.min(src.length,+$('#prefix').value);set('prefix-value',n);let S=new Set([0]),prev=S;for(const c of src.slice(0,n)){prev=S;const next=new Set([0]);if(c==='a'&&S.has(0))next.add(1);if(c==='b'&&S.has(1))next.add(2);S=next;}cells('chars',Array.from({length:8},(_,i)=>({text:src[i]||'·',cls:i<n?'on':'empty'})));graph('nfa',[0,1,2].map((i)=>({x:55+i*125,y:80,name:'q'+i,cls:valid&&S.has(i)?'on':'',sub:i===2?'accept':''})),[[0,1,n>0&&src[n-1]==='a'],[1,2,n>0&&src[n-1]==='b'&&prev.has(1)]]);$('#nfa').insertAdjacentHTML('beforeend','<path class=\"edge '+(valid&&n>0?'on':'')+'\" d=\"M37 63 C0 5 110 5 73 63\" marker-end=\"url(#arrow-nfa)\"/><text x=\"55\" y=\"14\" text-anchor=\"middle\">a,b</text><text x=\"117\" y=\"70\" text-anchor=\"middle\">a</text><text x=\"242\" y=\"70\" text-anchor=\"middle\">b</text>');table('transitions',[['q0 → q0','a, b','↺'],['q0 → q1','a','→'],['q1 → q2','b','→']]);set('active',valid?fmt([...S].map(i=>'q'+i)):'—');set('dfa',valid?(S.has(2)?'C = {q0,q2}':S.has(1)?'B = {q0,q1}':'A = {q0}'):'—');set('equation',n?'move('+fmt([...prev])+', '+src[n-1]+') = '+fmt([...S]):'S₀ = {0}');set('explanation',t(!valid?'invalid':S.has(2)?'accept':'reject'));$('#dfa').dataset.states=valid?[...S].join(','):'';}$('#input').oninput=()=>{$('#prefix').max=$('#input').value.length;$('#prefix').value=$('#input').value.length;draw()};$('#prefix').oninput=draw;draw();","audio":false,"strings":{"accept":"This prefix ends in ab. Further input still changes the state.","active":"NFA active set","dfa":"Matching DFA state","heading":"Which NFA states live inside one DFA state?","input":"Input string","intro":"Enter a/b or select a prefix. The NFA keeps every possible state; the DFA names that same set.","invalid":"Only a and b are accepted here; edit the input.","prefix":"Read prefix length","reject":"This prefix does not end in ab. Unread input has not participated.","reset":"Reset","scope":"Whole-string recognition of suffix ab, alphabet a/b, at most eight characters. No epsilon edges, so closure adds nothing. Not substring search.","scopeLabel":"Model scope and assumptions"}}
Minimization: Merge Only If Future Behavior Is Identical
Two DFA states are equivalent if, starting from them, any subsequent string yields the same acceptance result. Minimization first classifies by output, then continuously splits states with different future behaviors.
A simple recognizer can first divide into accepting and non-accepting. A lexer must further distinguish acceptance actions: Identifier and Number, even though both are accepting, cannot be merged just by looking at boolean acceptance, as this would lose token types and rule priorities.
Hopcroft's algorithm refines partitions using splitters and their transition predecessors, controlling repeated processing via a work queue and smaller-subset strategies. Simply writing "scan repeatedly until no changes occur" is general partition refinement and is insufficient to prove Hopcroft's complexity. The common formulation for n states and alphabet size k is O(knlogn); for a fixed alphabet, this is abbreviated as O(nlogn). This is also not a claim of a unified optimal lower bound for all computational models.
The minimum number of states does not equal the fastest machine code. Branch layout, table size, cache hits, and vectorization also affect throughput; whether to minimize and the specific generation format depend on the tool and configuration.
Longest Match and Rule Priority
After composing multiple token rules into a recognizer, a single scan must retain the most recent acceptance position and action until no further progress can be made, then return to that position to output the token. If no acceptance position is ever reached, lexical error handling is triggered.
Input Prefix
Possible Rules
Result
if followed by space
Both keyword and identifier match 2 characters
Choose keyword based on equal-length priority
ifx followed by space
Keyword matches 2, identifier matches 3
Identifier is longer; priority cannot let if take precedence
==
= and ==
Choose the operator of length 2
re2c's rule selection first compares match length, then selects the earlier rule in case of ties. Keywords can also be scanned as identifiers first and then classified via a lookup table; both approaches are valid. re2c Manual
"Single DFA transition in constant time" does not automatically guarantee linear time for the entire longest-match lexer. For example, with rules a and a*b, and input consisting of many as followed by c: a naive scanner scans to the end each time looking for b, then backs up to accept only a single a, repeating this process can lead to quadratic time. Overall complexity must be discussed in conjunction with rule backtracking distance, implementation memoization, or other scanning strategies.
Quantifying Backtracking Cost: Transitions Are Cheap, But The Same Character May Be Visited Many Times
For rules a and a*b, with input aaaaac. Starting from position 0, we read 5 as to try the longer a*b, only confirming failure when c is reached; the most recent acceptance position allows outputting only one a. The next round restarts from position 1, reading 4 more as, and so on. Just the advancement on a amounts to 5+4+3+2+1=15 visits.
defrescans(n):source="a"*n+"c"visits=0forstartinrange(n):cursor=startwhilesource[cursor]=="a":visits+=1cursor+=1# a*b fails; back up to the acceptance position of a single a, next round starts at start+1.
returnvisitsassertrescans(5)==15fornin(1,10,100):assertrescans(n)==n*(n+1)//2
Here, we count separately the successful crossings of a, excluding the failed checks on c each round. The quadratic term already comes from repeated suffix scanning; there is no need to make each state transition itself an expensive operation. Improvement requires constraining rule backtracking, recording failure suffix information, or adopting appropriate scanning algorithms; one cannot use "using DFA" as proof that the entire token stream processing is linear.
Why can cheap transitions still yield quadratic scanning?
Count how often each input position is revisited. One row per scan exposes the triangle for final c: n traversals, then n−1, down to one. A final b lets a*b cover the whole string in one scan. The figure counts a traversals only; handling the invalid final c is separate.
W(n)=i=1∑ni=2n(n+1)
Preparing the visual
Why can cheap transitions still yield quadratic scanning?
A final c forces one-token fallback after rescanning each suffix: n(n+1)/2 traversals of a. A final b accepts the entire token in one scan.
{"id":"maximal-munch-work","title":"Why can cheap transitions still yield quadratic scanning?","summary":"A final c forces one-token fallback after rescanning each suffix: n(n+1)/2 traversals of a. A final b accepts the entire token in one scan.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"n\" data-i18n=\"n\"></label><output for=\"n\" id=\"n-value\"></output><input id=\"n\" type=\"range\" min=\"1\" max=\"12\" value=\"6\" step=\"1\"></div><div class=\"presets\" role=\"group\" data-i18n-label=\"ending\"><button data-mode=\"c\" data-i18n=\"bad\"></button><button data-mode=\"b\" data-i18n=\"good\"></button></div><p id=\"source\" class=\"source\"></p><div id=\"workgrid\"></div><div class=\"metrics\"><div><span data-i18n=\"work\"></span><strong id=\"work\"></strong></div><div><span data-i18n=\"tokens\"></span><strong id=\"tokens\"></strong></div></div><p id=\"equation\" class=\"formula\"></p><p class=\"hint\" data-i18n=\"hint\"></p><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#workgrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px}#workgrid i{height:12px;background:var(--surface);border-radius:2px}#workgrid i.on{background:var(--second)}","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 ending='c';function draw(){const n=+$('#n').value;set('n-value',n);set('source','a'.repeat(n)+ending);$('#workgrid').replaceChildren(...Array.from({length:144},(_,i)=>{const e=document.createElement('i'),row=Math.floor(i/12),col=i%12;e.className=(ending==='c'?row<n&&col>=row&&col<n:row===0&&col<n)?'on':'';return e}));const work=ending==='c'?n*(n+1)/2:n;set('work',work);set('tokens',ending==='c'?n:1);set('equation',ending==='c'?'1 + … + '+n+' = '+n+'×'+(n+1)+'/2 = '+work:n+' = '+work);pressed(ending);}$('#n').oninput=draw;document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{ending=b.dataset.mode;draw()});draw();","audio":false,"strings":{"bad":"Final c: repeated fallback","ending":"Final character","good":"Final b: full token","heading":"Why can cheap transitions still yield quadratic scanning?","hint":"Rows are token scans; columns are input positions. Gray cells are unvisited.","intro":"Rules are a and a*b. Change the run length and final character; each gold cell counts one traversal of a.","n":"Number of a characters n","reset":"Reset","scope":"Naive maximal-munch scanner; counts only successful a traversals, excluding terminal checks, automaton construction and optimized strategies. n≤12.","scopeLabel":"Model scope and assumptions","tokens":"Accepted token count","work":"Total a traversals"}}
Comparing Automata and Backtracking Engines
Method
Information Retained
Primary Cost
Backtracking Search
Path selection stack and capture states
Certain patterns and failing inputs cause massive repeated exploration
Thompson NFA Simulation
Current set of active states
Updating the set per character; pattern size affects constants
Full DFA
Enumerated set states and transitions
Direct scanning, but compilation time and table space can be large
Lazy DFA
Construct and cache states on demand
Cache capacity, eviction, and reconstruction costs
Real-world libraries may combine multiple strategies. Capturing positions and greedy disambiguation require additional information but do not force the implementation to use exponential backtracking. For untrusted patterns, compilation memory limits should be considered; for untrusted inputs, matching workload must be considered. These two cannot be conflated into a single "linear safe" label.
Continue Reading
Lexer Design implements these rules into token, position, buffer, and error recovery interfaces; Recursive Descent and LL Analysis continues to handle nested structures between tokens.