A lexer slices source code into tokens for the parser while retaining enough information to support error reporting, formatting, and editing. The design focuses on clarifying input encoding, rule priority, position units, error behavior, and who owns the referenced source text.
Tokens: Classification, Raw Text, and Position Responsibilities
A representation suitable for immutable source snapshots is as follows. The integers in the example are decimal digit strings; only two operators are listed; actual languages can extend this further.
The raw text can be sliced from a still-living snapshot via the span, eliminating the need to store a string copy for each token. Alternatively, one can borrow the text directly or intern identifiers as SymbolId; the key is to ensure references do not outlive the source buffer's lifetime. The raw spelling of strings and their escaped values should be distinguished, and syntactic recognition of numbers and target type overflow checks can be performed in separate phases.
The parser can determine it is an addition operator via Operator(Add); it does not require every operator to be a top-level enum variant. Granularity should serve the consumer, not be deduced inversely from the number of DFA states.
Matching Rules: Keywords Are Just One Classification Strategy
Prioritize the longest match, then resolve ties for equal-length matches. ifx should be treated as a complete identifier, not split into the keyword if and x.
Keywords can either be written directly into generator rules or recognized first as identifiers and then looked up in a keyword table. The former allows the automaton to share common prefixes; the latter facilitates centralized management of language versions and reserved words. Regular hash tables, perfect hash functions, sorted tables, or length-based branching all have their use cases; a small number of keywords does not necessarily lead to state explosion.
Contextual keywords might initially be retained as Identifier and then checked for positional validity by the parser. Raw identifiers, string interpolation, indentation tokens, and nested comments may require lexical states, a state stack, or counters. The boundary between the lexer and parser is a matter of language design and implementation choice; not all rules can be handled by a single regular expression.
Spans: Binding to Snapshots, Using Half-Open Intervals
[start, end) facilitates length calculation and slicing; an empty interval can also represent the insertion point for a missing token. Byte offsets are only valid within the source version they belong to: inserting a character at the beginning shifts all subsequent offsets. An IDE must re-parse using the new snapshot or translate via edit mappings; old offsets cannot be treated as stable identities across versions.
The line-start table stores the byte offset where each line begins, typically starting with 0. To find the line containing a position offset, you should find the last line start that is less than or equal to offset, rather than requiring offset to exactly hit a line start.
frombisectimportbisect_rightdefbyte_line_column(line_starts,offset):# Precondition: the table belongs to the same snapshot, and 0 <= offset <= file byte count
row=bisect_right(line_starts,offset)-1returnrow+1, offset-line_starts[row]+1assertbyte_line_column([0,6],4)==(1,5)assertbyte_line_column([0,6],6)==(2,1)
This is the byte column, not the Unicode code-point column, UTF-16 column, or screen column. Tabs, combining characters, and wide characters also affect display width. When exchanging positions with editor protocols, convert according to the negotiated encoding; see the Language Server Protocol for relevant conventions. How CRLF counts as a single newline and how empty lines at the end of a file are represented must also be unified at the source file management layer.
Iteration Interface and Lookahead Buffer
The basic interface can simply be next_token(). The parser's token buffering layer then provides peek, advance, and expect. The first peek can trigger scanning, and subsequent peeks reuse the cache; there is no need to force all scanning to happen upfront just to claim peek is O(1).
Fixed-k lookahead can use a small queue. When supporting speculative parsing, checkpoints should cover the token cursor and relevant parser state; if the lexer is switched modes by the parser, rollback also requires restoring the mode or ensuring cache compatibility. You cannot assume all state is restored by simply rolling back the byte cursor.
EOF should be a stable terminal state. Repeatedly peeking at EOF from the upper layer should not trigger out-of-bounds errors; the recovery loop should not mistake repeatedly receiving EOF as making progress.
Input Management: Reading the Whole File or Streaming Windows Are Both Reasonable
Ordinary source files are often read entirely into an immutable buffer, facilitating span slicing, multiple analyses, and diagnostics. Reading the entire file is a valid design; whether it consumes too much memory depends on project scale, concurrent file count, and retention policies.
Streaming scanning reduces resident input, but tokens can still cross buffer boundaries. The implementation must retain incomplete tokens and rollback intervals, expanding, moving, or copying as necessary; long strings do not naturally lose validity just because they exceed 4 KiB.
Double buffering does not automatically imply parallel I/O and scanning; parallelism requires explicit asynchronous or threading mechanisms. mmap provides on-demand mapping but does not exempt you from considering file lifecycle, mapping invalidation, and random access costs. re2c leaves buffer refilling and boundary checking to integration code configuration. re2c Input Handling
End of Input Block Is Not Necessarily End of Token, Nor End of Character
Assume the first input block is exactly if. You cannot immediately output the keyword, because the next block might start with x, making the complete token ifx. The input block might also stop in the middle of the three UTF-8 bytes of the character 中; the decoder needs to wait for continuation bytes, and the lexer needs to wait for enough characters to determine the token boundary. The two layers of state have distinct responsibilities.
The following implementation uses a small batch feed interface: identifiers are limited to ASCII, keywords are only if, integers accept only decimal digits; operators are = and ==, and whitespace is only space/tab/CR/LF. Other valid Unicode code points produce Invalid, and illegal UTF-8 terminates and throws a decoding error. This is not treating 中 or emoji as "illegal UTF-8".
importcodecsimportreclassStreamingLexer:def__init__(self):self.decoder=codecs.getincrementaldecoder("utf-8")("strict")self.pending=""self.offset=0self.closed=Falsedeffeed(self,data,final=False):ifself.closed:raiseValueError("input already finalized")self.pending+=self.decoder.decode(data,final=final)result=[]whileself.pending:first=self.pending[0]match=re.match(r"[A-Za-z_][A-Za-z_0-9]*|[0-9]+",self.pending)ifmatch:text=match.group()iflen(text)==len(self.pending)andnotfinal:break# Might have more identifier/digit characters
kind="If"iftext=="if"else"Integer"iftext[0].isdigit()else"Identifier"eliffirst=="=":iflen(self.pending)==1andnotfinal:break# Next block might extend = to ==
text="=="ifself.pending.startswith("==")else"="kind="Equal"iftext=="=="else"Assign"else:text=firstkind="Whitespace"iffirstin"\t\r\n"else"Invalid"end=self.offset+len(text.encode("utf-8"))ifkind!="Whitespace":result.append((kind,text,self.offset,end))self.offset=endself.pending=self.pending[len(text):]iffinal:self.closed=Trueresult.append(("Eof","",self.offset,self.offset))returnresultdeflex_chunks(chunks):lexer, result=StreamingLexer(), []forchunkinchunks:result.extend(lexer.feed(chunk))returnresult+lexer.feed(b"",final=True)raw="ifx == 中 😀 if 12".encode("utf-8")expected=lex_chunks([raw])assert[(kind,text)forkind,text,start,endinexpected]==[("Identifier","ifx"),("Equal","=="),("Invalid","中"),("Invalid","😀"),("If","if"),("Integer","12"),("Eof","")]forcutinrange(len(raw)+1):assertlex_chunks([raw[:cut],raw[cut:]])==expectedassertlex_chunks([bytes([b])forbinraw])==expectedforkind,text,start,endinexpected:assertraw[start:end].decode("utf-8")==textforinvalidin(b"\xff",b"\xe4\xb8",b"\xc0\xaf"):try:lex_chunks([invalid])exceptUnicodeDecodeError:passelse:raiseAssertionError("invalid UTF-8 accepted")
Trying every possible byte cut point simultaneously covers cuts within if/x, =/=, and inside 中 and emoji. The raw text and byte intervals of the output tokens should be completely consistent, regardless of I/O chunking. The ending final=True is crucial: it classifies the pending if as a keyword and requires the decoder to report incomplete UTF-8 sequences. Python Incremental Decoder Interface
Currently Received
Can Immediately Determine
Evidence Needed
if, not yet EOF
Cannot determine token end
Subsequent delimiter or final end
Single =, not yet EOF
Cannot determine length
Whether the next character is also =
First two UTF-8 bytes of 中
Cannot produce complete code point
Third continuation byte or final truncation error
==
Can output immediately in this case
No longer equality operator exists
This teaching implementation copies the pending string and re-matches from the start for long tokens that haven't ended, so it has no linear time or fixed memory guarantees; a real streaming lexer should save scan state and cursor, and set reasonable resource budgets. It also skips whitespace, failing to form a lossless token stream. After feed is closed, it refuses to accept more data; a peek/next wrapper layer for the parser can cache the final Eof to implement stable termination queries.
When Bytes Arrive, Are Characters and Tokens Complete?
Reception progress cannot be directly equated with scanning completion progress. The following deliberately stops the slider after if, after a single equals sign, in the middle of the Chinese character's bytes, and in the middle of the emoji's bytes: these positions lack either token boundary evidence or complete characters. Confirmed tokens use raw byte intervals; bytes not yet decoded are not disguised as illegal Unicode characters. EOF is an additional fact; declaring EOF early might yield a valid short input or a truncation error.
Preparing the visual
When Bytes Arrive, Are Characters and Tokens Complete?
Byte boundaries, character boundaries, and token boundaries are three different things. `if` needs subsequent characters to be classified, and half a UTF-8 character needs continuation bytes; only true EOF can terminate pending input.
{"id":"streaming-lexer-boundaries","title":"When Bytes Arrive, Are Characters and Tokens Complete?","summary":"Byte boundaries, character boundaries, and token boundaries are three different things. `if` needs subsequent characters to be classified, and half a UTF-8 character needs continuation bytes; only true EOF can terminate pending input.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"control\"><label for=\"received\" data-i18n=\"received\"></label><output for=\"received\" id=\"received-value\"></output><input id=\"received\" type=\"range\" min=\"0\" max=\"14\" value=\"2\" step=\"1\"></div><label class=\"check\"><input type=\"checkbox\" id=\"eof\"><span data-i18n=\"eof\"></span></label><div id=\"bytes\" class=\"tile-grid\"></div><section class=\"panel\"><h3 data-i18n=\"decoded\"></h3><div id=\"decoded\"></div></section><section class=\"panel\"><h3 data-i18n=\"pending\"></h3><div id=\"pending\"></div></section><section class=\"panel\"><h3 data-i18n=\"tokens\"></h3><div id=\"tokens\"></div></section><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"ok\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"error\"></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#bytes{grid-template-columns:repeat(7,minmax(0,1fr))}#bytes .cell{font-size:10px}#decoded,#pending{font:13px/1.8 var(--viz-font-ui);white-space:pre-wrap;min-height:3.6em;overflow-wrap:anywhere}#tokens{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;min-height:12.6em}","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;}));}const source='ifx == 中😀',bytes=new TextEncoder().encode(source),enc=new TextEncoder;function draw(){const n=+$('#received').value,final=$('#eof').checked;set('received-value',n);cells('bytes',[...bytes].map((b,i)=>({text:b.toString(16).padStart(2,'0'),cls:i<n?'on':'empty'})));let decoded='',error=false;try{decoded=new TextDecoder('utf-8',{fatal:true}).decode(bytes.slice(0,n),{stream:!final});}catch{error=true;decoded=new TextDecoder('utf-8',{fatal:true}).decode(bytes.slice(0,n),{stream:true});}set('decoded',decoded||'∅');let pos=0,tokens=[],pending='';while(pos<decoded.length){const rest=decoded.slice(pos);if(/^\\s/.test(rest)){pos++;continue}let match=/^[A-Za-z_][A-Za-z_0-9]*|^[0-9]+|^==|^=/.exec(rest),raw=match?match[0]:String.fromCodePoint(rest.codePointAt(0));const needs=match&&raw!=='=='&&pos+raw.length===decoded.length&&!final;if(needs){pending=raw;break}const start=enc.encode(decoded.slice(0,pos)).length,end=start+enc.encode(raw).length;tokens.push((match?raw==='if'?'If':/^[A-Za-z_]/.test(raw)?'Identifier':/^[0-9]/.test(raw)?'Integer':'Operator':'Invalid')+' '+raw+' ['+start+','+end+')');pos+=raw.length;}const incomplete=n-enc.encode(decoded).length;set('pending',(pending||'∅')+' / '+incomplete+' byte(s)');set('tokens',tokens.join('\\n')||'∅');set('explanation',t(error?'error':'ok'));$('#tokens').dataset.count=tokens.length;$('#decoded').dataset.error=String(error);}$('#received').oninput=draw;$('#eof').onchange=draw;draw();","audio":false,"strings":{"decoded":"Decoded Text","eof":"Input has truly ended (EOF)","error":"EOF falls inside a UTF-8 character, rejecting truncated input.","heading":"When Bytes Arrive, Are Characters and Tokens Complete?","intro":"Fixed input `ifx == 中😀`. Drag the number of received bytes to observe decoded text, confirmed tokens, and content still waiting.","ok":"Confirmed tokens will not change due to subsequent input. Half-open intervals are counted by UTF-8 bytes.","pending":"Waiting for Token Boundary / Incomplete Bytes","received":"Number of Received Bytes","reset":"Reset","scope":"ASCII identifiers and integers, keyword `if`, operators `=`/`==`; other valid Unicode characters output `Invalid`. Illegal or truncated UTF-8 throws a decoding error. The slider compares independent reception snapshots; EOF can simulate truncated connections.","scopeLabel":"Model Scope and Conventions","tokens":"Confirmed Tokens and Byte Intervals"}}
Unicode: Decoding Rules and Identifier Rules Are Separate
UTF-8 decoding requires checking continuation bytes, overlong encodings, surrogate code points, and code point limits; you cannot blindly skip bytes based solely on the first byte's length. If the input type already guarantees valid UTF-8, you can leverage standard library iteration boundaries; otherwise, first decide on the diagnostic and recovery strategy for illegal bytes.
After decoding to code points, you must also determine if the identifier is valid according to the language specification. Rust uses rules related to XID_Start and XID_Continue, allows underscores, and specifies requirements such as NFC normalization; single underscores and raw identifiers have additional rules. \w is not a universal language identifier specification. Rust Identifiers
Normalized symbol comparison and raw source spelling can be retained simultaneously: name resolution uses the normalized form, while diagnostics still highlight user input. Unicode versions are also part of language compatibility.
Error Recovery: Consumption Strategy Must Match Current State
Illegal characters can generate an Invalid token and consume one valid code point; illegal UTF-8 advances according to the established byte recovery strategy. Unclosed strings, comments, or interpolations need their own termination conditions and cannot uniformly "skip one character". When multi-line strings are valid, newlines are certainly not error boundaries.
A single recovery step should consume input, change state, and guarantee that the next step can proceed, or explicitly stop at EOF. Generating placeholder tokens but staying in the same position repeatedly reporting the same error leads to infinite loops. Diagnostics can be returned separately from tokens, letting the parser know the error interval without having to repeatedly report the same lexical issue.
Hand-Written vs. Generator
Concern
Hand-Written
Generator
Rule Maintenance
Control flow is direct, but requires manual consistency
Regex and actions are centralized, generation step needs integration
Special Contexts
State changes can be encoded directly
Depends on tool support for states and user code
Diagnostic Quality
Easy to customize per branch, but recovery might be missed
Custom actions can be written; not limited by generator identity
Performance
Depends on branch layout, scan strategy, and implementation
Depends on generated form, optimization options, and integration method
The choice should be supported by language rules and measurement results. Both must verify longest match, cross-buffer tokens, Unicode boundaries, EOF, and recovery progress. For how to compose tokens into a tree retaining error information, see AST Design and Error Recovery.