zsh/ported/lex.rs
1//! Zsh lexical analyzer - Direct port from zsh/Src/lex.c
2//!
3//! This lexer tokenizes zsh shell input into a stream of tokens.
4//! It handles all zsh-specific syntax including:
5//! - Single/double/dollar quotes
6//! - Command substitution $(...) and `...`
7//! - Arithmetic $((...))
8//! - Parameter expansion ${...}
9//! - Process substitution <(...) >(...)
10//! - Here documents
11//! - All redirection operators
12//! - Comments
13//! - Continuation lines
14
15pub use super::zsh_h::{
16 lextok, AMPER, AMPERBANG, AMPOUTANG, BANG_TOK, BARAMP, BAR_TOK, CASE, COPROC, DAMPER, DBAR,
17 DINANG, DINANGDASH, DINBRACK, DINPAR, DOLOOP, DONE, DOUTANG, DOUTANGAMP, DOUTANGAMPBANG,
18 DOUTANGBANG, DOUTBRACK, DOUTPAR, DSEMI, ELIF, ELSE, ENDINPUT, ENVARRAY, ENVSTRING, ESAC, FI,
19 FOR, FOREACH, FUNC, IF, INANGAMP, INANG_TOK, INBRACE_TOK, INOUTANG, INOUTPAR, INPAR_TOK,
20 IS_REDIROP, LEXERR, LEXFLAGS_ACTIVE, LEXFLAGS_COMMENTS, LEXFLAGS_COMMENTS_KEEP,
21 LEXFLAGS_COMMENTS_STRIP, LEXFLAGS_NEWLINE, LEXFLAGS_ZLE, NEWLIN, NOCORRECT, NULLTOK, OUTANGAMP,
22 OUTANGAMPBANG, OUTANGBANG, OUTANG_TOK, OUTBRACE_TOK, OUTPAR_TOK, REPEAT, SELECT, SEMI, SEMIAMP,
23 SEMIBAR, SEPER, STRING_LEX, THEN, TIME, TRINANG, TYPESET, UNTIL, WHILE, ZEND,
24};
25pub use crate::heredoc_ast::HereDoc;
26use crate::ported::context::{zcontext_restore, zcontext_save};
27use crate::ported::hashtable::{aliastab_lock, reswdtab_lock, sufaliastab_lock};
28use crate::ported::hist::{hist_in_word, strinbeg, strinend};
29use crate::ported::input::{inpop, inpush};
30use crate::ported::parse::HDOCS;
31use crate::ported::prompt::{cmdpop, cmdpush, CMDSTACK};
32use crate::ported::string::dupstring_wlen;
33use crate::ported::utils::{errflag, spckword, zerr, ERRFLAG_ERROR};
34use crate::ported::zle::compcore::{WB, WE};
35use crate::ported::zsh_h::{
36 alias, interact, isset, lex_stack, lexbufstate, unset, Bang, Bar, Bnull, Bnullkeep, Comma,
37 Dash, Dnull, Equals, Hat, Inang, Inbrace, Inbrack, Inpar, Inparmath, Marker, Meta, Nularg,
38 Outang, OutangProc, Outbrace, Outbrack, Outpar, Outparmath, Pound, Qstring, Qtick, Quest,
39 Snull, Star, Stringg, Tick, Tilde, ALIASESOPT, CORRECT, CORRECTALL, CSHJUNKIEQUOTES, CS_BQUOTE,
40 CS_BRACE, CS_BRACEPAR, CS_CMDSUBST, CS_CURSH, CS_DQUOTE, CS_HEREDOC, CS_HEREDOCD, CS_MATH,
41 CS_MATHSUBST, CS_QUOTE, ERRFLAG_INT, HISTALLOWCLOBBER, IGNOREBRACES, IGNORECLOSEBRACES,
42 INP_ALIAS, INP_CONT, INTERACTIVECOMMENTS, KSHGLOB, POSIXALIASES, RCQUOTES, SHGLOB, SHINSTDIN,
43 SHORTLOOPS, SHORTREPEAT, ZCONTEXT_LEX, ZCONTEXT_PARSE,
44};
45use crate::ported::ztype_h::itok;
46use crate::DPUTS;
47use serde::{Deserialize, Serialize};
48use std::collections::VecDeque;
49use std::sync::atomic::Ordering;
50
51/// zsh/Src/lex.c:216 `lex_context_save`. After save, the lexer
52/// is in a clean state suitable for parsing a nested input (command
53/// substitution body, here-doc terminator, eval'd string).
54pub fn lex_context_save(ls: &mut lex_stack) {
55 // c:218-239 — copy live state into the stack. Mirrors C
56 // field-by-field; `toplevel` param dropped because C does
57 // `(void)toplevel;` (unused).
58 ls.dbparens = LEX_DBPARENS.get() as i32;
59 ls.isfirstln = LEX_ISFIRSTLN.get() as i32;
60 ls.isfirstch = LEX_ISFIRSTCH.get() as i32;
61 ls.lexflags = LEX_LEXFLAGS.get();
62 ls.tok = tok();
63 ls.tokstr = LEX_TOKSTR.with_borrow_mut(|t| t.take());
64 // c:225 `ls->zshlextext = zshlextext;`
65 ls.zshlextext = LEX_ZSHLEXTEXT.with_borrow_mut(|t| t.take());
66 LEX_LEXBUF.with_borrow_mut(|b| {
67 ls.lexbuf.ptr = b.ptr.take();
68 ls.lexbuf.siz = b.siz;
69 ls.lexbuf.len = b.len;
70 });
71 ls.lex_add_raw = LEX_LEX_ADD_RAW.get();
72 ls.tokstr_raw = LEX_TOKSTR_RAW.with_borrow_mut(|t| t.take());
73 LEX_LEXBUF_RAW.with_borrow_mut(|b| {
74 ls.lexbuf_raw.ptr = b.ptr.take();
75 ls.lexbuf_raw.siz = b.siz;
76 ls.lexbuf_raw.len = b.len;
77 });
78 ls.lexstop = LEX_LEXSTOP.get() as i32;
79 ls.toklineno = LEX_TOKLINENO.get() as i64;
80 // NOTE: LEX_UNGET_BUF is deliberately NOT stashed here — `$(...)`
81 // bodies use it as a cross-context handoff into the nested parse
82 // (see the hungetc flow at lex.rs `all_plain` / gettokstr), so a
83 // blanket take here truncates every cmdsubst body. Nested lexes
84 // that must not see the suspended parse's ungets (the
85 // syntax-highlight walk) isolate it at their own call site.
86
87 // c:235-238 — reset live state to defaults so a nested parse
88 // starts from a clean slate. tokstr/lexbuf zeroed; lexbuf.siz
89 // reset to 256 (the C-source initial alloc); raw buffers
90 // wiped, lex_add_raw cleared.
91 set_tokstr(None);
92 LEX_LEXBUF.with_borrow_mut(|b| {
93 b.ptr = Some(String::with_capacity(256));
94 b.siz = 256;
95 b.len = 0;
96 });
97 LEX_TOKSTR_RAW.with_borrow_mut(|t| *t = None);
98 LEX_LEXBUF_RAW.with_borrow_mut(|b| {
99 b.ptr = None;
100 b.siz = 0;
101 b.len = 0;
102 });
103 LEX_LEX_ADD_RAW.set(0);
104}
105
106/// zsh/Src/lex.c:245 `lex_context_restore`. Inverse of
107/// `lex_context_save`. Called after the nested parse completes.
108pub fn lex_context_restore(ls: &mut lex_stack) {
109 // c:249-261 — copy stack state back into live fields.
110 LEX_DBPARENS.set(ls.dbparens != 0);
111 LEX_ISFIRSTLN.set(ls.isfirstln != 0);
112 LEX_ISFIRSTCH.set(ls.isfirstch != 0);
113 LEX_LEXFLAGS.set(ls.lexflags);
114 set_tok(ls.tok);
115 set_tokstr(ls.tokstr.take());
116 // c:255 `zshlextext = ls->zshlextext;`
117 LEX_ZSHLEXTEXT.with_borrow_mut(|t| *t = ls.zshlextext.take());
118 LEX_LEXBUF.with_borrow_mut(|b| {
119 b.ptr = Some(ls.lexbuf.ptr.take().unwrap_or_default());
120 b.siz = ls.lexbuf.siz;
121 b.len = ls.lexbuf.len;
122 });
123 LEX_LEX_ADD_RAW.set(ls.lex_add_raw);
124 LEX_TOKSTR_RAW.with_borrow_mut(|t| *t = ls.tokstr_raw.take());
125 LEX_LEXBUF_RAW.with_borrow_mut(|b| {
126 b.ptr = ls.lexbuf_raw.ptr.take();
127 b.siz = ls.lexbuf_raw.siz;
128 b.len = ls.lexbuf_raw.len;
129 });
130 LEX_LEXSTOP.set(ls.lexstop != 0);
131 LEX_TOKLINENO.set(ls.toklineno as u64);
132}
133
134/// Main lexer entry point — fetch the next token. Direct port of
135/// zsh/Src/lex.c:266 `zshlex`. Loop body matches the C source
136/// `do { ... } while (tok != ENDINPUT && exalias())` at lex.c:270-276,
137/// followed by here-doc draining (lex.c:278-306), newline tracking
138/// (lex.c:307-310), and SEMI/NEWLIN→SEPER folding (lex.c:311-312).
139pub fn zshlex() {
140 // lex.c:268-269 — early-out on prior LEXERR.
141 if tok() == LEXERR {
142 return;
143 }
144
145 // RUST-ONLY: drop the popped-alias-frame record that `input_hasalias`
146 // falls back on (see `crate::alias_input_frames`). C keeps popped frames
147 // addressable above `instacktop` and lets `inungetc` walk back onto them
148 // (Src/input.c:587-605); zshrs's Vec-based stack destroys them, so the
149 // record is rebuilt per zshlex call. One zshlex covers the whole
150 // gettok/exalias chain, so a NESTED expansion (`secondalias` →
151 // `firstalias` → body) still records every alias in the chain — which is
152 // what C's downward instack walk reports.
153 crate::alias_input_frames::clear();
154
155 // lex.c:270-276 — `do { ... } while (tok != ENDINPUT && exalias())`.
156 // The do-while re-runs gettok when exalias re-injects alias text;
157 // exalias also performs reswdtab keyword promotion (`{` → INBRACE,
158 // `if` → IF, etc.) and spell-correction.
159 loop {
160 // lex.c:271-272 — bump inrepeat counter for `repeat N {}`
161 // detection.
162 if LEX_INREPEAT.get() > 0 {
163 LEX_INREPEAT.set(LEX_INREPEAT.get() + 1);
164 }
165 // lex.c:273-274 — `if (inrepeat_ == 3 && (isset(SHORTLOOPS) ||
166 // isset(SHORTREPEAT))) incmdpos = 1;` — at the third token after
167 // `repeat`, SHORTLOOPS/SHORTREPEAT options force back into
168 // command position so the loop body can start without an
169 // explicit `{`.
170 if LEX_INREPEAT.get() == 3 && (isset(SHORTLOOPS) || isset(SHORTREPEAT)) {
171 LEX_INCMDPOS.set(true);
172 }
173
174 // lex.c:275 — `tok = gettok();`
175 let _t = gettok();
176 set_tok(_t);
177
178 // lex.c:276 — `} while (tok != ENDINPUT && exalias());`
179 if tok() == ENDINPUT || !exalias() {
180 break;
181 }
182 }
183
184 // lex.c:277 — `nocorrect &= 1;` — clear bit 1 (lookahead-only)
185 // so the persistent low bit survives but the per-word bit is
186 // dropped.
187 LEX_NOCORRECT.set(LEX_NOCORRECT.get() & 1);
188
189 // lex.c:278-306 — drain pending here-documents at the start of
190 // a new line. Line-by-line port: walks the canonical `hdocs`
191 // linked list (`parse::HDOCS` mirrors `Src/parse.c:84 struct
192 // heredocs *hdocs;`), calls `gethere` to read each body, calls
193 // `setheredoc` to patch the wordcode redir slot. Two zshrs-only
194 // lines (annotated below) bridge body content + processed flag
195 // into the parallel AST-glue `LEX_HEREDOCS` Vec.
196 if tok() == NEWLIN || tok() == ENDINPUT {
197 // c:279 — `while (hdocs)`
198 while let Some(mut node) = HDOCS.with_borrow_mut(|h| h.take()) {
199 // c:280 — `struct heredocs *next = hdocs->next;`
200 let next: Option<Box<crate::ported::zsh_h::heredocs>> = node.next.take();
201 // c:281 — `char *doc, *munged_term;`
202 let doc: Option<String>;
203 let mut munged_term: String;
204
205 crate::ported::hist::ihwbegin(0); // c:283 hwbegin(0)
206 // c:284 — `cmdpush(hdocs->type == REDIR_HEREDOC ? CS_HEREDOC : CS_HEREDOCD);`
207 cmdpush(if node.typ == crate::ported::zsh_h::REDIR_HEREDOC {
208 CS_HEREDOC as u8
209 } else {
210 CS_HEREDOCD as u8
211 });
212 // c:285 — `munged_term = dupstring(hdocs->str);`
213 munged_term = crate::ported::mem::dupstring(node.str.as_deref().unwrap_or(""));
214 crate::ported::hist::stophist
215 .fetch_add(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst); // c:286 STOPHIST
216 // c:287 — `doc = gethere(&munged_term, hdocs->type);`
217 doc = crate::ported::exec::gethere(&mut munged_term, node.typ);
218 crate::ported::hist::stophist
219 .fetch_sub(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst); // c:288 ALLOWHIST
220 // c:289 — `cmdpop();`
221 cmdpop();
222 crate::ported::hist::ihwend(); // c:290 hwend()
223 // c:290 — `hwend();`
224
225 // c:291 — `if (!doc)`
226 let Some(doc) = doc else {
227 // c:292 — `zerr("here document too large");`
228 zerr("here document too large");
229 // c:293-297 — while (hdocs) { next = hdocs->next; zfree(hdocs); hdocs = next; }
230 HDOCS.with_borrow_mut(|h| *h = None);
231 // c:298 — `tok = LEXERR;`
232 set_tok(LEXERR);
233 // c:299 — break out of the while.
234 break;
235 };
236 // c:301-302 — `setheredoc(hdocs->pc, REDIR_HERESTR, doc,
237 // hdocs->str, munged_term);`
238 crate::ported::parse::setheredoc(
239 node.pc as usize,
240 crate::ported::zsh_h::REDIR_HERESTR,
241 &doc,
242 node.str.as_deref().unwrap_or(""),
243 &munged_term,
244 );
245 // zshrs-only: write body into the parallel AST-glue
246 // LEX_HEREDOCS entry so `fill_heredoc_bodies` (parse.rs)
247 // wires it onto the matching ZshRedir.heredoc field.
248 // No C counterpart — LEX_HEREDOCS is Rust-only state.
249 LEX_HEREDOCS.with_borrow_mut(|v| {
250 for h in v.iter_mut() {
251 if !h.processed {
252 h.content = doc;
253 h.processed = true;
254 return;
255 }
256 }
257 });
258 // c:303 — `zfree(hdocs, sizeof(struct heredocs));`
259 drop(node);
260 // c:304 — `hdocs = next;`
261 HDOCS.with_borrow_mut(|h| *h = next);
262 }
263 }
264
265 // lex.c:307-310 — track whether we just saw a newline.
266 // c:310 — `isnewlin = (inbufct) ? -1 : 1;`. `inbufct` is what is left
267 // in the CURRENT input buffer. zshrs's `LEX_INPUT` window stands in for
268 // that buffer, so "characters left" is `pos < len` — correct for a
269 // string, which `inpush` buffers whole. A FILE is line-buffered in C
270 // (`inputline`, c:Src/input.c:366), leaving `inbufct == 0` at every line
271 // end; zshrs holds the whole body in one window, so the file case is
272 // flagged instead (see `LEX_FILE_WINDOW_STRIN`).
273 if tok() != NEWLIN {
274 LEX_ISNEWLIN.set(0);
275 } else if LEX_FILE_WINDOW_STRIN.get() != 0 {
276 LEX_ISNEWLIN.set(1); // c:310 — one line per buffer ⇒ inbufct == 0
277 } else {
278 LEX_ISNEWLIN.set(if LEX_POS.get() < LEX_INPUT.with_borrow(|s| s.len()) {
279 -1
280 } else {
281 1
282 });
283 }
284
285 // lex.c:311-312 — fold SEMI / NEWLIN into SEPER unless
286 // LEXFLAGS_NEWLINE is set to preserve newlines (used by
287 // ZLE for completion of partial lines).
288 if tok() == SEMI || (tok() == NEWLIN && LEX_LEXFLAGS.get() & LEXFLAGS_NEWLINE == 0) {
289 set_tok(SEPER);
290 }
291
292 // C zshlex (lex.c:266-310) does NOT update incmdpos / inredir /
293 // oldpos / infor / intypeset / incondpat. Those updates live in:
294 // - ctxtlex (lex.c:319-368, mirrored at fn ctxtlex below) for
295 // incmdpos / inredir / oldpos / infor;
296 // - parse.c call sites for intypeset (parse.c:1932/2042/2047);
297 // - cond.c par_cond_* for incondpat (Rust-only state — C tracks
298 // pattern context implicitly via the cond grammar walker).
299 // Earlier zshrs port had duplicated the ctxtlex switch + an
300 // incondpat tracker into zshlex so the parser would get those
301 // updates "for free"; that broke the C-faithful contract of
302 // zshlex. Removed.
303}
304
305/// Lex next token AND update per-context flags. Direct port of
306/// zsh/Src/lex.c:317 `ctxtlex`. The post-token state machine
307/// at lex.c:322-358 sets `incmdpos` based on the token shape:
308/// list separators / pipes / control keywords reset to cmd-pos;
309/// word-shaped tokens leave cmd-pos. Redirections (lex.c:361-368)
310/// stash prior incmdpos and force the redir target to non-cmd-pos.
311pub fn ctxtlex() {
312 // lex.c:319 — static `oldpos` cache for redir-target restore
313 // is captured per-call here as `oldpos` below (zshrs's parser
314 // re-enters ctxtlex per token, no need for static persistence).
315
316 // lex.c:321 — `zshlex();` to advance to the next token.
317 zshlex();
318
319 // c:322-358 — post-token incmdpos switch. C lists the
320 // arms exactly as enumerated below; `intypeset` is NOT set
321 // here in C (it lives in parse.c:1932/2042/2047, ported in
322 // parse.rs at the typeset-call sites).
323 match tok() {
324 // c:323-343 — separators / openers / conjunctions /
325 // control keywords — back into cmd-pos so the next token
326 // can be a fresh command.
327 SEPER | NEWLIN | SEMI | DSEMI | SEMIAMP | SEMIBAR | AMPER | AMPERBANG | INPAR_TOK
328 | INBRACE_TOK | DBAR | DAMPER | BAR_TOK | BARAMP | INOUTPAR | DOLOOP | THEN | ELIF
329 | ELSE | DOUTBRACK => {
330 LEX_INCMDPOS.set(true);
331 }
332 // c:345-353 — word/value-shaped tokens leave cmd-pos.
333 STRING_LEX | TYPESET | ENVARRAY | OUTPAR_TOK | CASE | DINBRACK => {
334 LEX_INCMDPOS.set(false);
335 }
336 // c:354-357 — `default: break;` — keep compiler happy.
337 _ => {}
338 }
339
340 // lex.c:359-360 — `infor` decay. FOR sets infor=2 so the next
341 // DINPAR can detect c-style for. After any non-DINPAR, decay
342 // to 0 (or back to 2 if we just saw FOR again).
343 if tok() != DINPAR {
344 LEX_INFOR.set(if tok() == FOR { 2 } else { 0 });
345 }
346
347 // lex.c:361-368 — redir-target context dance. After consuming
348 // a redir operator, the following token (the file path) sees
349 // incmdpos=0 even when its inherent shape would put it back
350 // in cmd-pos. After the redir target, restore from oldpos
351 // (struct field — must persist across zshlex calls).
352 if IS_REDIROP(tok()) || tok() == FOR || tok() == FOREACH || tok() == SELECT {
353 LEX_INREDIR.set(true);
354 LEX_OLDPOS.set(LEX_INCMDPOS.get());
355 LEX_INCMDPOS.set(false);
356 } else if LEX_INREDIR.get() {
357 LEX_INCMDPOS.set(LEX_OLDPOS.get());
358 LEX_INREDIR.set(false);
359 }
360}
361
362// RedirType / CondType — flat `REDIR_*` (`Src/zsh.h:377-408`) and
363// `COND_*` (`Src/zsh.h:660-679`) constants already live in
364// `super::zsh_h`. Do NOT wrap them in Rust enums here — the wrapper
365// is a fake abstraction (no C counterpart).
366//
367// LX1_* / LX2_* — flat `#define`s in `Src/lex.c:371-405`. The
368// lexer's `gettok` body uses these as the action table for the
369// first / second-tier character dispatch.
370
371/// `#define LX1_BKSLASH 0` (Src/lex.c:371).
372pub const LX1_BKSLASH: u8 = 0;
373/// `#define LX1_COMMENT 1` (Src/lex.c:372).
374pub const LX1_COMMENT: u8 = 1;
375/// `#define LX1_NEWLIN 2` (Src/lex.c:373).
376pub const LX1_NEWLIN: u8 = 2;
377/// `#define LX1_SEMI 3` (Src/lex.c:374).
378pub const LX1_SEMI: u8 = 3;
379/// `#define LX1_AMPER 5` (Src/lex.c:375).
380pub const LX1_AMPER: u8 = 5;
381/// `#define LX1_BAR 6` (Src/lex.c:376).
382pub const LX1_BAR: u8 = 6;
383/// `#define LX1_INPAR 7` (Src/lex.c:377).
384pub const LX1_INPAR: u8 = 7;
385/// `#define LX1_OUTPAR 8` (Src/lex.c:378).
386pub const LX1_OUTPAR: u8 = 8;
387/// `#define LX1_INANG 13` (Src/lex.c:379).
388pub const LX1_INANG: u8 = 13;
389/// `#define LX1_OUTANG 14` (Src/lex.c:380).
390pub const LX1_OUTANG: u8 = 14;
391/// `#define LX1_OTHER 15` (Src/lex.c:381).
392pub const LX1_OTHER: u8 = 15;
393
394/// `#define LX2_BREAK 0` (Src/lex.c:383).
395pub const LX2_BREAK: u8 = 0;
396/// `#define LX2_OUTPAR 1` (Src/lex.c:384).
397pub const LX2_OUTPAR: u8 = 1;
398/// `#define LX2_BAR 2` (Src/lex.c:385).
399pub const LX2_BAR: u8 = 2;
400/// `#define LX2_STRING 3` (Src/lex.c:386).
401pub const LX2_STRING: u8 = 3;
402/// `#define LX2_INBRACK 4` (Src/lex.c:387).
403pub const LX2_INBRACK: u8 = 4;
404/// `#define LX2_OUTBRACK 5` (Src/lex.c:388).
405pub const LX2_OUTBRACK: u8 = 5;
406/// `#define LX2_TILDE 6` (Src/lex.c:389).
407pub const LX2_TILDE: u8 = 6;
408/// `#define LX2_INPAR 7` (Src/lex.c:390).
409pub const LX2_INPAR: u8 = 7;
410/// `#define LX2_INBRACE 8` (Src/lex.c:391).
411pub const LX2_INBRACE: u8 = 8;
412/// `#define LX2_OUTBRACE 9` (Src/lex.c:392).
413pub const LX2_OUTBRACE: u8 = 9;
414/// `#define LX2_OUTANG 10` (Src/lex.c:393).
415pub const LX2_OUTANG: u8 = 10;
416/// `#define LX2_INANG 11` (Src/lex.c:394).
417pub const LX2_INANG: u8 = 11;
418/// `#define LX2_EQUALS 12` (Src/lex.c:395).
419pub const LX2_EQUALS: u8 = 12;
420/// `#define LX2_BKSLASH 13` (Src/lex.c:396).
421pub const LX2_BKSLASH: u8 = 13;
422/// `#define LX2_QUOTE 14` (Src/lex.c:397).
423pub const LX2_QUOTE: u8 = 14;
424/// `#define LX2_DQUOTE 15` (Src/lex.c:398).
425pub const LX2_DQUOTE: u8 = 15;
426/// `#define LX2_BQUOTE 16` (Src/lex.c:399).
427pub const LX2_BQUOTE: u8 = 16;
428/// `#define LX2_COMMA 17` (Src/lex.c:400).
429pub const LX2_COMMA: u8 = 17;
430/// `#define LX2_DASH 18` (Src/lex.c:401).
431pub const LX2_DASH: u8 = 18;
432/// `#define LX2_BANG 19` (Src/lex.c:402).
433pub const LX2_BANG: u8 = 19;
434/// `#define LX2_OTHER 20` (Src/lex.c:403).
435pub const LX2_OTHER: u8 = 20;
436/// `#define LX2_META 21` (Src/lex.c:404).
437pub const LX2_META: u8 = 21;
438
439/// Port of `void initlextabs(void)` from `Src/lex.c:410`. Builds the
440/// three byte-keyed action tables the lexer dispatches on.
441///
442/// `lexact1` — char → `LX1_*` action for the top-level `gettok`
443/// dispatch. Default `LX1_OTHER`; the 14 chars in `"\\q\n;!&|(){}[]<>"`
444/// get table-index actions in order.
445///
446/// `lexact2` — char → `LX2_*` action for `gettokstr`'s in-word
447/// dispatch. Default `LX2_OTHER`; the 21 chars in
448/// `";)|$[]~({}><=\\\\\'\"\`,-!"` map by index, plus `&` → `LX2_BREAK`
449/// and the Meta byte → `LX2_META`.
450///
451/// `lextok2` — char → byte-token. Identity except for the 8 magic
452/// chars that need byte-token replacement (`*`/`?`/`{`/`[`/`$`/`~`/
453/// `#`/`^` → Star/Quest/Inbrace/Inbrack/Stringg/Tilde/Pound/Hat).
454pub fn initlextabs() {
455 // c:410
456 let a1 = LEXACT1.get_or_init(|| std::sync::Mutex::new([0u8; 256]));
457 let a2 = LEXACT2.get_or_init(|| std::sync::Mutex::new([0u8; 256]));
458 let t2 = LEXTOK2.get_or_init(|| std::sync::Mutex::new([0u8; 256]));
459 let mut a1 = a1.lock().unwrap();
460 let mut a2 = a2.lock().unwrap();
461 let mut t2 = t2.lock().unwrap();
462 // c:413-417 — seed defaults.
463 for i in 0..256 {
464 a1[i] = LX1_OTHER;
465 a2[i] = LX2_OTHER;
466 t2[i] = i as u8;
467 }
468 // c:418-419 — overwrite indexed punctuation.
469 let lx1 = b"\\q\n;!&|(){}[]<>";
470 for (i, &c) in lx1.iter().enumerate() {
471 a1[c as usize] = i as u8;
472 }
473 let lx2 = b";)|$[]~({}><=\\'\"`,-!";
474 for (i, &c) in lx2.iter().enumerate() {
475 a2[c as usize] = i as u8;
476 }
477 // c:422-423 — special overrides.
478 a2[b'&' as usize] = LX2_BREAK;
479 a2[Meta as usize] = LX2_META;
480 // c:424-431 — byte-token map for the 8 magic chars.
481 t2[b'*' as usize] = Star as u8;
482 t2[b'?' as usize] = Quest as u8;
483 t2[b'{' as usize] = Inbrace as u8;
484 t2[b'[' as usize] = Inbrack as u8;
485 t2[b'$' as usize] = Stringg as u8;
486 t2[b'~' as usize] = Tilde as u8;
487 t2[b'#' as usize] = Pound as u8;
488 t2[b'^' as usize] = Hat as u8;
489}
490
491/// Initialize lexical state. Direct port of zsh/Src/lex.c:441
492/// `lexinit`. Resets dbparens / nocorrect / lexstop and sets `tok`
493/// to ENDINPUT so the next gettok starts from a known baseline.
494/// Note: `lex_init(input)` already sets equivalent defaults; this
495/// function exists for the rare case a caller wants to reset the
496/// lexer state mid-parse without re-loading input.
497pub fn lexinit() {
498 // lex.c:443 — `nocorrect = dbparens = lexstop = 0;`
499 LEX_NOCORRECT.set(0);
500 LEX_DBPARENS.set(false);
501 LEX_LEXSTOP.set(false);
502 // C has ONE `lexstop`; zshrs splits it into LEX_LEXSTOP (gates
503 // gettok) and input.rs `lexstop` (gates ingetc). The single C
504 // `lexstop = 0` here must zero BOTH halves — same paired-reset rule
505 // as inpush (input.rs:654-655). Without the input-side reset, a
506 // nested string parse (cmd-subst / eval via parse_isolated) that
507 // drained its input left `input::lexstop = true`; on the faithful
508 // single-event loop()/parse_event reader, the next iteration's
509 // ingetc then short-circuited to EOF and the shell exited after the
510 // first command containing a `$(…)`.
511 crate::ported::input::lexstop.with(|c| c.set(false));
512 // lex.c:444 — `tok = ENDINPUT;`
513 set_tok(ENDINPUT);
514}
515
516/// Add character to token buffer
517/// Port of `add(int c)` from `Src/lex.c:451`.
518fn add(c: char) {
519 LEX_LEXBUF.with_borrow_mut(|b| b.add(c));
520}
521
522/// Port of the `SETPARBEGIN` macro from `Src/lex.c:468-472`:
523///
524/// ```c
525/// #define SETPARBEGIN { \
526/// if ((lexflags & LEXFLAGS_ZLE) && !(inbufflags & INP_ALIAS) && \
527/// zlemetacs >= zlemetall+1-inbufct) \
528/// parbegin = inbufct; \
529/// }
530/// ```
531///
532/// Records where a command/process substitution (`` ` ``, `$(`,
533/// `<(`, `>(`) opened, but only when the cursor is at or past that
534/// point. `get_comp_string` (zle_tricky.c:1464-1480) reads it to
535/// restart its lex against the INTERIOR of the substitution, which
536/// is what puts the first word inside `$(`/`` ` `` in command
537/// position. A C preprocessor macro, so it stays a macro here — the
538/// same mapping `DPUTS`/`ERRMSG` (zsh_h.rs:2394/2437) use.
539macro_rules! SETPARBEGIN {
540 () => {{
541 let inbufct = crate::ported::input::inbufct.with(|c| c.get());
542 if (LEX_LEXFLAGS.get() & LEXFLAGS_ZLE) != 0
543 && (crate::ported::input::inbufflags.with(|f| f.get())
544 & crate::ported::zsh_h::INP_ALIAS)
545 == 0
546 && crate::ported::zle::compcore::ZLEMETACS.load(Ordering::SeqCst)
547 >= crate::ported::zle::compcore::ZLEMETALL.load(Ordering::SeqCst) + 1 - inbufct
548 {
549 LEX_PARBEGIN.set(inbufct);
550 }
551 }};
552}
553
554/// Port of the `SETPAREND` macro from `Src/lex.c:473-481`:
555///
556/// ```c
557/// #define SETPAREND { \
558/// if ((lexflags & LEXFLAGS_ZLE) && !(inbufflags & INP_ALIAS) && \
559/// parbegin != -1 && parend == -1) { \
560/// if (zlemetacs >= zlemetall + 1 - inbufct) \
561/// parbegin = -1; \
562/// else \
563/// parend = inbufct; \
564/// } \
565/// }
566/// ```
567///
568/// Closes the substitution `SETPARBEGIN` opened: a cursor at or past
569/// the closing delimiter means the cursor was never inside it, so the
570/// record is dropped; otherwise the end offset is kept so
571/// `get_comp_string` can trim the line to the substitution body.
572macro_rules! SETPAREND {
573 () => {{
574 let inbufct = crate::ported::input::inbufct.with(|c| c.get());
575 if (LEX_LEXFLAGS.get() & LEXFLAGS_ZLE) != 0
576 && (crate::ported::input::inbufflags.with(|f| f.get())
577 & crate::ported::zsh_h::INP_ALIAS)
578 == 0
579 && LEX_PARBEGIN.get() != -1
580 && LEX_PAREND.get() == -1
581 {
582 if crate::ported::zle::compcore::ZLEMETACS.load(Ordering::SeqCst)
583 >= crate::ported::zle::compcore::ZLEMETALL.load(Ordering::SeqCst) + 1 - inbufct
584 {
585 LEX_PARBEGIN.set(-1);
586 } else {
587 LEX_PAREND.set(inbufct);
588 }
589 }
590 }};
591}
592
593/// Determine if (( is arithmetic or command
594/// Decide whether `( ... )` after a `$` is a math expression
595/// `$((...))` or a command substitution `$(...)`. Direct port of
596/// zsh/Src/lex.c:495 `cmd_or_math`. Tries dquote_parse first;
597/// if it succeeds AND the next char is `)` (closing the second
598/// paren of `(( ))`), it's math. Otherwise rewinds and treats as
599/// a command substitution.
600fn cmd_or_math() -> i32 {
601 // dash/ash have NO `(( ))` arithmetic command — `((` is just two nested
602 // subshells `( (`. Real dash runs `(( 1 + 1 ))` as the command `1` (with
603 // args `+ 1`) inside two subshells → "1: not found", non-zero. Force the
604 // command (subshell) path here so zshrs --dash/--ash matches: push back the
605 // second `(` (already consumed by the LX1_INPAR caller) and return CMD, as
606 // the rewind path below does. Runs BEFORE cmdpush so there is no cmdpop
607 // imbalance. `for ((…))` is unaffected (it returns DINPAR earlier, before
608 // cmd_or_math), and `$(( ))` arithmetic uses a different path entirely.
609 // Found by the per-mode dash-strictness sweep.
610 if crate::dash_mode::dash_strict() {
611 hungetc('(');
612 LEX_LEXSTOP.set(false);
613 return CMD_OR_MATH_CMD;
614 }
615 let oldlen = LEX_LEXBUF.with_borrow(|b| b.buf_len());
616
617 // c:501 — `cmdpush(cs_type);` — the C source takes a `cs_type`
618 // arg (CS_MATH or CS_MATHSUBST); zshrs's lone caller (gettok
619 // LX1_INPAR `((`) uses CS_MATH. The matching `cmdpop()` fires
620 // both on the math path (after success) and on the rewind path
621 // (before falling through to skipcomm, which has its own
622 // CS_CMDSUBST push/pop).
623 cmdpush(CS_MATH as u8);
624
625 // Per lex.c:498-518 — `cmd_or_math` calls `dquote_parse(')')`
626 // which fills lexbuf with ONLY the inner expression, then checks
627 // for the closing `)`. The surrounding `((` / `))` are NOT added
628 // to lexbuf.
629 if let Err(stopped) = dquote_parse(')', false) {
630 // c:506 — `cmdpop();` before rewind to command-parse path.
631 cmdpop();
632 // c:516-519 — `else if (lexstop) return CMD_OR_MATH_ERR;` — the scan
633 // ran off the end; there is nothing to give back.
634 if LEX_LEXSTOP.get() {
635 return CMD_OR_MATH_ERR;
636 }
637 // c:521-522 — `hungetc(c); lexstop = 0;` — dquote_parse consumed the
638 // character it stopped on (`err = c`, c:1673); give it back before
639 // the rest of the body.
640 if let Some(stopped) = stopped {
641 hungetc(stopped);
642 }
643 // Back up and try as command
644 // c:Src/lex.c:523 — `while (lexbuf.len > oldlen && !(errflag &
645 // ERRFLAG_ERROR))`. The errflag term was missing, and C's
646 // `lexbuf.len--` (c:524) decrements unconditionally while this
647 // relied on `pop()` yielding a char: a buffer that reports a
648 // length above `oldlen` but pops nothing spun here forever.
649 // `(( $+functions[a[b] ))` -- an unbalanced `[` inside a math
650 // subscript, which zsh rejects with "invalid subscript" -- hung
651 // the lexer, and with it any file containing one. That is
652 // generated code in the wild: `_uu-coreutils` defines
653 // `_uu-coreutils__[_commands` for coreutils' `[` utility, so
654 // prewarming a corpus containing it never finished.
655 while LEX_LEXBUF.with_borrow(|b| b.buf_len()) > oldlen
656 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
657 {
658 match LEX_LEXBUF.with_borrow_mut(|b| b.pop()) {
659 Some(c) => hungetc(c),
660 None => break, // c:524 — C's len-- cannot stall
661 }
662 }
663 hungetc('(');
664 LEX_LEXSTOP.set(false);
665 // c:Src/lex.c:519-520 — `hungetc('('); return errflag ?
666 // CMD_OR_MATH_ERR : CMD_OR_MATH_CMD;`. The C path does NOT
667 // call skipcomm here — the caller (LX1_INPAR arm) returns
668 // INPAR_TOK and the parser walks the remaining content as
669 // ordinary tokens. Calling skipcomm() consumed the entire
670 // subshell body, so `(() { echo X; })` lost the inner anon-fn
671 // tokens. Bug #196.
672 return CMD_OR_MATH_CMD;
673 }
674
675 // Check for closing ) — matches C lex.c:511-512: success-with-`)`
676 // means `((..))` was math. Don't add `)` to lexbuf.
677 let c = hgetc();
678 if c == Some(')') {
679 // c:506 — `cmdpop();` on math success.
680 cmdpop();
681 return CMD_OR_MATH_MATH;
682 }
683
684 // c:506 — `cmdpop();` before rewind to command-parse path.
685 cmdpop();
686 // c:Src/lex.c:513-516 — CMD path: push back the peeked char,
687 // then push back the `)` that dquote_parse consumed (this is the
688 // `c = ')'; hungetc(c);` sequence at C line 515-518 below the
689 // `if (!c)` block; the fall-through into the outer `hungetc(c)`
690 // at line 522 puts the `)` back into the input stream so the
691 // next zshlex re-emits it as OUTPAR_TOK).
692 //
693 // Without this hungetc, the inner `)` is lost from the stream
694 // permanently. For source `((a|)b)` the token stream loses
695 // the inner OUTPAR — emits INPAR INPAR STRING(a) BAR STRING(b)
696 // OUTPAR instead of the correct INPAR INPAR STRING(a) BAR
697 // OUTPAR STRING(b) OUTPAR. That broke par_case on every
698 // `((alt|alt)tail)` pattern — including zinit.zsh:2946
699 // `((add-|)fpath)` which fails as "expected ')' in case pattern".
700 if let Some(c) = c {
701 hungetc(c);
702 }
703 LEX_LEXSTOP.set(false);
704 hungetc(')'); // c:515-518 — the `)` dquote_parse consumed
705
706 // Back up token. c:Src/lex.c:523-527 — same guards as the error
707 // path above: stop on ERRFLAG_ERROR, and never spin when the buffer
708 // reports length but pops nothing.
709 while LEX_LEXBUF.with_borrow(|b| b.buf_len()) > oldlen
710 && (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) == 0
711 {
712 match LEX_LEXBUF.with_borrow_mut(|b| b.pop()) {
713 Some(c) => hungetc(c),
714 None => break, // c:524
715 }
716 }
717 hungetc('(');
718
719 // c:Src/lex.c:519-520 — same as above: return CMD_OR_MATH_CMD
720 // without skipcomm. Bug #196.
721 CMD_OR_MATH_CMD
722}
723
724/// Parse `$(...)` or `$((...))` after the `$` has been consumed.
725/// Direct port of zsh/Src/lex.c:540 `cmd_or_math_sub`. Reads
726/// the next char to discriminate: a leading `(` plus successful
727/// math parse via `cmd_or_math` → arithmetic substitution (with
728/// the open-paren retroactively rewritten to Inparmath); else
729/// command substitution via skipcomm.
730fn cmd_or_math_sub() -> i32 {
731 loop {
732 let c = hgetc();
733 if c == Some('\\') {
734 let c2 = hgetc();
735 if c2 != Some('\n') {
736 if let Some(c2) = c2 {
737 hungetc(c2);
738 }
739 hungetc('\\');
740 LEX_LEXSTOP.set(false);
741 return if skipcomm().is_err() {
742 CMD_OR_MATH_ERR
743 } else {
744 CMD_OR_MATH_CMD
745 };
746 }
747 // Line continuation, try again (loop instead of recursion)
748 continue;
749 }
750
751 // Not a line continuation, process normally
752 if c == Some('(') {
753 // c:555-557 — `lexpos = lexbuf.ptr - tokstr; add(Inpar); add('(');`
754 // — lexpos is captured BEFORE the Inpar/`(` go in so a
755 // later Inparmath rewrite can target the Inpar byte.
756 let lexpos = LEX_LEXBUF.with_borrow(|b| b.buf_len());
757 add(Inpar);
758 add('(');
759 // After Inpar+`(`, we record the lexbuf length so the
760 // failure-path rewind knows where `dquote_parse`'s output
761 // ends and where our Inpar+`(` begin. Mirrors C's split
762 // between `cmd_or_math`'s `oldlen` (inside the inner call,
763 // = lexpos + 2 bytes in C) and the outer `lexbuf.ptr -= 2`
764 // that strips Inpar+`(` after `cmd_or_math` returns.
765 let after_open = LEX_LEXBUF.with_borrow(|b| b.buf_len());
766
767 // Inline of `cmd_or_math(CS_MATHSUBST)` (Src/lex.c:495).
768 // Done inline rather than calling our existing
769 // `cmd_or_math()` because that helper additionally calls
770 // `skipcomm()` on its failure path (over-port for the
771 // gettok `((` caller), which would double-consume the
772 // body when chained here.
773 cmdpush(CS_MATHSUBST as u8);
774 let dq = dquote_parse(')', false); // c:503
775 cmdpop();
776
777 if dq.is_ok() {
778 // c:511 — `c = hgetc(); if (c == ')') return MATH;`
779 let c2 = hgetc();
780 if c2 == Some(')') {
781 // c:559-562 — confirmed math: rewrite Inpar →
782 // Inparmath at lexpos, append closing `)`. Inpar
783 // and Inparmath are both 3-byte UTF-8 (`\u{e188}` /
784 // `\u{e189}`); set_char_at swaps in place.
785 LEX_LEXBUF.with_borrow_mut(|b| b.set_char_at(lexpos, Inparmath));
786 add(')');
787 return CMD_OR_MATH_MATH;
788 }
789 if let Some(c2) = c2 {
790 hungetc(c2);
791 }
792 LEX_LEXSTOP.set(false);
793 // c:516 — `c = ')';` — fall through to the rewind
794 // path; the `)` that dquote_parse consumed needs
795 // to be hungetc'd. We synthesize that by setting up
796 // the loop and the final hungetc('(') as below; the
797 // `)` is implicit in `dquote_parse` having stopped
798 // at it (it was consumed but not added to lexbuf,
799 // matching C where `c = ')'` is what gets hungetc'd
800 // first before the rewind-everything loop).
801 hungetc(')');
802 } else if LEX_LEXSTOP.get() {
803 // c:519 — `else if (lexstop) return CMD_OR_MATH_ERR;`
804 return CMD_OR_MATH_ERR;
805 } else {
806 // c:521-522 — `hungetc(c); lexstop = 0;` — push back the
807 // character dquote_parse stopped on. dquote_parse CONSUMED
808 // it (it is `err = c`, c:1673), so `$(( a[ ))` lost its first
809 // `)`: the rewind re-lexed `( a[ ` with one `)` left, and the
810 // command substitution never closed ("parse error near
811 // `$(( a[ ))'", or "unmatched \"" inside double quotes).
812 if let Err(Some(stopped)) = dq {
813 hungetc(stopped);
814 }
815 LEX_LEXSTOP.set(false);
816 }
817
818 // c:524-528 — `while (lexbuf.len > oldlen) { ... }; hungetc('(');`
819 // — back up everything dquote_parse appended to lexbuf
820 // and hungetc each in reverse order, then hungetc the
821 // single `(` that opened the math construct. The Inpar+`(`
822 // we placed BEFORE the inner call are NOT touched here;
823 // they're stripped from lexbuf after this block via direct
824 // pop (no hungetc) so they don't go back onto the input.
825 //
826 // Bug #4 in docs/BUGS.md: the previous Rust port popped
827 // ALL the way down to `lexpos` (before Inpar+`(`) and
828 // hungetc'd those bytes back, putting an Inpar token on
829 // the input stream that derailed `skipcomm`. The faithful
830 // port pops only down to `after_open` and then strips
831 // Inpar+`(` from lexbuf without hungetc'ing them.
832 while LEX_LEXBUF.with_borrow(|b| b.buf_len()) > after_open {
833 if let Some(ch) = LEX_LEXBUF.with_borrow_mut(|b| b.pop()) {
834 hungetc(ch);
835 } else {
836 break;
837 }
838 }
839 hungetc('(');
840 LEX_LEXSTOP.set(false);
841 // c:565-566 — `lexbuf.ptr -= 2; lexbuf.len -= 2;` — drop
842 // the Inpar+`(` we added at the top without putting them
843 // on the input stream.
844 LEX_LEXBUF.with_borrow_mut(|b| {
845 b.pop();
846 b.pop();
847 });
848 } else {
849 if let Some(c) = c {
850 hungetc(c);
851 }
852 LEX_LEXSTOP.set(false);
853 }
854
855 return if skipcomm().is_err() {
856 CMD_OR_MATH_ERR
857 } else {
858 CMD_OR_MATH_CMD
859 };
860 }
861}
862
863// ============================================================================
864// Additional parsing functions ported from lex.c
865// ============================================================================
866
867/// Check whether we're looking at valid numeric globbing syntax
868/// `<N-M>` / `<N->` / `<-M>` / `<->`. Call pointing just after the
869/// Port of `static int isnumglob(void)` from `Src/lex.c:581`.
870/// Called pointing just after the opening `<`. Looks ahead to
871/// detect zsh's numeric-glob shape `<[0-9]*-[0-9]*>` (e.g. `<->`,
872/// `<1-5>`, `<10->`). Returns `true` when the shape matches.
873/// Leaves the input stream in the same position regardless of
874/// outcome — all consumed chars are `hungetc`'d back.
875///
876/// C body:
877/// ```c
878/// while (1) {
879/// c = hgetc();
880/// if (lexstop) { lexstop = 0; break; }
881/// tbuf[n++] = c;
882/// if (!idigit(c)) {
883/// if (c != ec) break;
884/// if (ec == '>') { ret = 1; break; }
885/// ec = '>';
886/// }
887/// }
888/// while (n--) hungetc(tbuf[n]);
889/// return ret;
890/// ```
891pub fn isnumglob() -> bool {
892 // c:581 (Src/lex.c)
893 // c:583 — `int c, ec = '-', ret = 0;`. `ec` is the expected
894 // non-digit char: starts at `-`, flips to `>` after the dash.
895 let mut ec: char = '-'; // c:583
896 let mut ret = false; // c:583
897 // c:585 — char buffer for the rewind at c:606-607.
898 let mut buf: Vec<char> = Vec::new();
899
900 loop {
901 // c:587
902 let cn = hgetc(); // c:588
903 if LEX_LEXSTOP.get() {
904 // c:589
905 LEX_LEXSTOP.set(false); // c:590
906 break; // c:591
907 }
908 let Some(cn) = cn else { break };
909 buf.push(cn); // c:593
910 if !cn.is_ascii_digit() {
911 // c:594 !idigit(c)
912 if cn != ec {
913 // c:595
914 break; // c:596
915 }
916 if ec == '>' {
917 // c:597
918 ret = true; // c:598
919 break; // c:599
920 }
921 ec = '>'; // c:601
922 }
923 }
924 // c:606-607 — `while (n--) hungetc(tbuf[n]);` — rewind in
925 // reverse order so the next hgetc sees the same bytes.
926 while let Some(ch) = buf.pop() {
927 hungetc(ch);
928 }
929 ret // c:609
930}
931
932// SPECCHARS / PATCHARS — port of `Src/zsh.h:228, 232`. Use
933// `super::zsh_h::{SPECCHARS, PATCHARS}` directly; no duplicate here.
934// IS_DASH() — port of `Src/zsh.h:242` `#define IS_DASH(x)`. Use
935// `super::zsh_h::IS_DASH(c)` at call sites.
936
937// Reserved-word table — the canonical port of `Src/hashtable.c:1076`
938// `static struct reswd reswds[]` lives in `ported::hashtable::reswd_table`
939// (built in `reswd_table::new()` at hashtable.rs:561 with the 31 entries
940// from `reswds[]`). The lexer queries it via `reswdtab_lock()` from
941// `check_reserved_word()` below; no duplicate table here.
942
943// =============================================================================
944// End of inlined `tokens.rs`. Original lex.rs body follows.
945// =============================================================================
946
947// `lexflags` — port of `mod_export int lexflags;` (`Src/lex.c:151`).
948// Carries the LEXFLAGS_ACTIVE/ZLE/COMMENTS_KEEP/COMMENTS_STRIP/NEWLINE
949// bit flags from `Src/zsh.h:2293-2315`. The constants live in
950// `super::zsh_h:2532-2537`; access via plain `&` / `|` ops, not a
951// Rust struct.
952
953// `struct LexBuf` (fake Rust-only paraphrase) DELETED. The canonical
954// port of `struct lexbufstate` (zsh.h:3069-3079) lives at
955// `crate::ported::zsh_h::lexbufstate` with fields `{ptr: Option<String>,
956// siz: i32, len: i32}` — same shape as C. The LEX_LEXBUF /
957// LEX_LEXBUF_RAW thread_locals use this canonical type directly.
958// The convenience methods below are Rust-only
959// helpers wrapping the flat operations C inlines at lex.c:451+ (`add`,
960// etc.) — they're carried here as helpers rather than re-inlining
961// ~50 lines of ptr/len/siz arithmetic across 24 call sites.
962
963// WARNING: NOT IN LEX.C — Rust-only convenience over C's flat lexbuf
964// operations at lex.c:451+ (`add`, plus inline `*lexbuf.ptr = '\0'`
965// terminator-writes, `lexbuf.len > oldlen` truncation loops, etc.).
966// C operates on the file-static `lexbuf` global directly with raw
967// ptr arithmetic; these methods package the equivalent ops on the
968// owned-String buffer that zshrs's `lexbufstate.ptr: Option<String>`
969// carries. Each method's body cites the C source location it mirrors.
970impl lexbufstate {
971 /// New lex buffer with the C-source initial alloc (lex.c:210
972 /// `static struct lexbufstate lexbuf = { NULL, 256, 0 };`). ptr
973 /// is initialized Some("") so subsequent operations can unwrap
974 /// without a None check.
975 pub(crate) fn new() -> Self {
976 Self {
977 ptr: Some(String::with_capacity(256)),
978 siz: 256,
979 len: 0,
980 }
981 }
982
983 /// Mirrors C `add(int c)` at lex.c:451: push char, bump len,
984 /// double siz on overflow. Skips the C `hrealloc` since Rust's
985 /// String already manages capacity, but matches the doubling
986 /// policy for parity with how downstream code observes `siz`.
987 pub(crate) fn add(&mut self, c: char) {
988 if let Some(p) = self.ptr.as_mut() {
989 p.push(c);
990 self.len = p.len() as i32;
991 if self.len >= self.siz {
992 self.siz *= 2;
993 let want = self.siz as usize;
994 let have = p.capacity();
995 if want > have {
996 p.reserve(want - have);
997 }
998 }
999 }
1000 }
1001
1002 /// Reset buffer — clears ptr contents, zeros len, leaves siz.
1003 /// Mirrors lex.c:235 `tokstr = zshlextext = lexbuf.ptr = NULL;
1004 /// lexbuf.siz = 256;` plus gettokstr's `lexbuf.ptr = hcalloc(...)`
1005 /// (lex.c:632/953): the buffer is ALLOCATED here when absent so the
1006 /// following `add()`s accumulate. Without the allocate-when-None
1007 /// arm, `add()` (which only pushes when `ptr` is Some) silently
1008 /// no-ops whenever the lexer is entered without a prior `lex_init`
1009 /// — e.g. the interactive REPL reading off SHIN via ingetc — so
1010 /// every token came out empty.
1011 pub(crate) fn clear(&mut self) {
1012 match self.ptr.as_mut() {
1013 Some(p) => p.clear(),
1014 None => self.ptr = Some(String::with_capacity(self.siz.max(256) as usize)),
1015 }
1016 self.len = 0;
1017 }
1018
1019 /// View buffer as &str. Empty if ptr is None.
1020 pub(crate) fn as_str(&self) -> &str {
1021 self.ptr.as_deref().unwrap_or("")
1022 }
1023
1024 /// Length in chars (NOT bytes). Reads len field which `add()`
1025 /// keeps in sync; mirrors C `lexbuf.len`.
1026 pub(crate) fn buf_len(&self) -> usize {
1027 self.len as usize
1028 }
1029
1030 /// Pop the last char. Mirrors C lex.c:524-526 hungetc-driven
1031 /// shrink: `lexbuf.len--; *--lexbuf.ptr = ...`.
1032 pub(crate) fn pop(&mut self) -> Option<char> {
1033 let p = self.ptr.as_mut()?;
1034 let c = p.pop();
1035 // `len` is the buffer's BYTE length, as `add` sets it: C's
1036 // `lexbuf.len` (c:Src/lex.c:523 `while (lexbuf.len > oldlen)`) and
1037 // every rewind here compare it with a byte offset taken earlier.
1038 // Subtracting 1 per character left it a byte high after popping a
1039 // two-byte token char (`Qstring` for a `$`), so cmd_or_math_sub's
1040 // rewind popped the `(` it had to keep and the `$(` word lost its
1041 // `$`: `x=$(( $a ) )` → "parse error near `x=(((…'".
1042 self.len = p.len() as i32;
1043 c
1044 }
1045
1046 /// Replace the char at BYTE position `byte_idx` in lexbuf. The
1047 /// lexbuf's `len` field tracks BYTE length (matching C's
1048 /// `lexbuf.len` which indexes the raw `tokstr[]` array), and
1049 /// `buf_len()` returns the same. So when `cmd_or_math_sub`
1050 /// saves `lexpos = buf_len()` before `add(Inpar)`, lexpos is
1051 /// the byte offset where the Inpar marker landed.
1052 ///
1053 /// Mirrors C's `tokstr[lexpos] = MARKER` rewrites (lex.c:560,
1054 /// cmd_or_math_sub) that retroactively patch a marker byte
1055 /// after the surrounding parse confirms the context. The
1056 /// caller must guarantee the new char's UTF-8 byte width
1057 /// matches the current char's width at that offset — used
1058 /// only for swapping equally-sized markers (e.g. Inpar
1059 /// `\u{e188}` → Inparmath `\u{e189}`, both 2 UTF-8 bytes).
1060 pub(crate) fn set_char_at(&mut self, byte_idx: usize, c: char) {
1061 let Some(buf) = self.ptr.as_mut() else { return };
1062 if byte_idx >= buf.len() {
1063 return;
1064 }
1065 // Walk to the char-start at byte_idx (must be on a UTF-8
1066 // boundary). Find the char length so we can replace exactly
1067 // that many bytes.
1068 if !buf.is_char_boundary(byte_idx) {
1069 return;
1070 }
1071 let old_byte_end = buf[byte_idx..]
1072 .chars()
1073 .next()
1074 .map(|ch| byte_idx + ch.len_utf8());
1075 let Some(old_byte_end) = old_byte_end else {
1076 return;
1077 };
1078 let mut new_bytes = [0u8; 4];
1079 let new_str = c.encode_utf8(&mut new_bytes);
1080 if new_str.len() == old_byte_end - byte_idx {
1081 let new_owned = new_str.to_string();
1082 buf.replace_range(byte_idx..old_byte_end, &new_owned);
1083 }
1084 }
1085}
1086
1087// Per-heredoc state — Rust-only AST-glue, NOT in lex.c. Canonical home
1088// is `src/extensions/heredoc_ast.rs`; re-exported here so existing
1089// `crate::lex::HereDoc` / `crate::parse::HereDocInfo` call sites keep
1090// resolving. Both die in Phase 9e (PORT_PLAN.md) when the wordcode
1091// port reinstates C's `struct heredocs` shape (zsh.h:1152) +
1092// `gethere()` body collection.
1093
1094// =============================================================================
1095// Lexer state — thread-local file-statics matching zsh's lex.c file-statics.
1096// Each field maps to a `static` in `Src/lex.c` (or `Src/zsh.h` for the few
1097// `extern`-declared ones). Per-evaluator: each worker thread tokenizing its
1098// own input needs its own state (bucket-1 per PORT_PLAN.md).
1099// =============================================================================
1100thread_local! {
1101 /// Input source (owned). C uses input-stack `struct inputstack` +
1102 /// `hgetc()` (lex.c:input.c); zshrs P7 collapses to an owned String
1103 /// until the inputstack subsystem is ported.
1104 pub static LEX_INPUT: std::cell::RefCell<String> = const { std::cell::RefCell::new(String::new()) };
1105 /// `LEX_POS` static.
1106 pub static LEX_POS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1107 /// `LEX_UNGET_BUF` static.
1108 pub static LEX_UNGET_BUF: std::cell::RefCell<std::collections::VecDeque<char>>
1109 = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1110 /// !!! WARNING: RUST-ONLY HELPER STATE !!!
1111 ///
1112 /// Lockstep companion to [`LEX_UNGET_BUF`]: one flag per queued
1113 /// character recording whether `hungetc` rewound the history-line
1114 /// cursor (`hist::hptr`) for it, so the matching re-read in `hgetc`
1115 /// can advance it back by exactly the same amount.
1116 ///
1117 /// C needs no such thing. There, pushback and re-read are the single
1118 /// pair `ihungetc` (Src/hist.c:989) / `ihgetc` (c:418), and BOTH test
1119 /// the same guard — `(inbufflags & (INP_ALIAS|INP_HIST)) != INP_ALIAS`
1120 /// (c:1009 and c:360) — so `hptr--` at c:1011 is always undone by
1121 /// `hwaddc` at c:459. zshrs queues pushback in `LEX_UNGET_BUF` and
1122 /// re-reads it without re-entering `ihgetc`, so the guard would be
1123 /// evaluated at two DIFFERENT moments; at an alias boundary
1124 /// `inbufflags` changes in between (the `inpush(" ", INP_ALIAS)` at
1125 /// c:Src/lex.c:1926 lands between the unget and the re-read), the
1126 /// rewind fires but the re-add is refused, and the history line loses
1127 /// exactly one character. Recording the decision makes the pair
1128 /// symmetric by construction.
1129 pub static LEX_UNGET_HPTR: std::cell::RefCell<std::collections::VecDeque<bool>>
1130 = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1131 /// !!! WARNING: RUST-ONLY HELPER STATE !!!
1132 ///
1133 /// Lockstep companion to [`LEX_UNGET_BUF`]: one flag per queued
1134 /// character, `true` when its re-read in `hgetc` must NOT be added to
1135 /// the raw record (`zshlex_raw_add`). Only the `{foo}` hack below sets
1136 /// it; every other pushback queues `false`.
1137 ///
1138 /// C needs no such thing: `inungetc` (Src/input.c:545) returns the
1139 /// character to an input-stack frame, and that frame decides the raw
1140 /// bookkeeping. The `{foo}` hack in `gettok` (c:Src/lex.c:1457-1469)
1141 /// pushes a `}` back with `lex_add_raw` forced to 0 when an alias
1142 /// expansion inside a command substitution has already backed that `}`
1143 /// out of the raw record (the alias frame was popped,
1144 /// c:Src/input.c:751-752). In C the `}` then lands in an alias
1145 /// continuation frame (c:Src/input.c:571-603): re-reading it adds it
1146 /// to the raw record and popping that frame takes it out again. zshrs
1147 /// re-reads it off `LEX_UNGET_BUF`, where no frame pop follows, so the
1148 /// re-read must not add it. Without this, `alias WI='{false}'` then
1149 /// `eval 'echo $(WI)'` recorded the body as `WI}` and the substitution
1150 /// failed with "parse error near `}'".
1151 pub static LEX_UNGET_RAW: std::cell::RefCell<std::collections::VecDeque<bool>>
1152 = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1153 /// !!! WARNING: RUST-ONLY HELPER STATE !!!
1154 ///
1155 /// Lockstep companion to [`LEX_UNGET_BUF`]: one entry per queued
1156 /// character, the `input::INBUF_SERIAL` of the `inbuf` frame the
1157 /// character was read from (0 = the Rust-only `LEX_INPUT` window).
1158 ///
1159 /// C needs no such thing: `inungetc` (c:Src/input.c:546-609) returns
1160 /// the character to the frame it came from (`inbufptr--`), or to an
1161 /// INP_CONT frame pushed on top when that frame is already backed up
1162 /// to its start. `inpoptop` (c:Src/input.c:736-753) then walks back
1163 /// only over characters still consumed, and popping the frame throws
1164 /// away whatever was pushed back into it. zshrs queues pushback here
1165 /// instead, so `inpoptop` needs to know which queued characters belong
1166 /// to the frame it pops: otherwise it took them out of the raw record a
1167 /// second time and `herrflush` re-read them after the frame was gone
1168 /// (`alias e='echo a}'; eval 'x=$(e)'` reported `x=$( )`).
1169 pub static LEX_UNGET_FRAME: std::cell::RefCell<std::collections::VecDeque<u64>>
1170 = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1171 /// !!! WARNING: RUST-ONLY HELPER STATE !!!
1172 ///
1173 /// The most recent characters `hgetc` returned, each with the frame
1174 /// serial it came from, so `hungetc` can tag the character it pushes
1175 /// back (see [`LEX_UNGET_FRAME`]). C's pushback always returns the
1176 /// last-read character to its own frame; this is the memory of which
1177 /// frame that was. Bounded: pushback never runs deeper than a few
1178 /// characters.
1179 pub static LEX_READ_FRAME: std::cell::RefCell<std::collections::VecDeque<(char, u64)>>
1180 = const { std::cell::RefCell::new(std::collections::VecDeque::new()) };
1181 /// `char *tokstr` (lex.c:170).
1182 pub static LEX_TOKSTR: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
1183 /// `char *zshlextext` (lex.c:43) — the text of the current token as
1184 /// `exalias` (c:1965-2018) last published it. NOT a view of
1185 /// `tokstr`: `gettok` clears `tokstr` for every token, but
1186 /// `zshlex` skips `exalias` at ENDINPUT (c:276), so at EOF this
1187 /// still holds the LAST real token — which is why zsh reports
1188 /// `[[ ]]` as "parse error near `]]'" and `[[ ]];` as
1189 /// "parse error near `;'".
1190 pub static LEX_ZSHLEXTEXT: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
1191 /// `enum lextok tok` (lex.c:180).
1192 pub static LEX_TOK: std::cell::Cell<lextok> = const { std::cell::Cell::new(ENDINPUT) };
1193 /// `int tokfd` (lex.c:191).
1194 pub static LEX_TOKFD: std::cell::Cell<i32> = const { std::cell::Cell::new(-1) };
1195 /// `zlong toklineno` (lex.c:198).
1196 pub static LEX_TOKLINENO: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
1197 /// `LEX_LINENO` static.
1198 pub static LEX_LINENO: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
1199 /// `int lexstop` (lex.c:175).
1200 pub static LEX_LEXSTOP: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1201 /// `int incmdpos` (lex.c:122 + extern in zsh.h).
1202 pub static LEX_INCMDPOS: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
1203 /// `int incond` (lex.c:127).
1204 pub static LEX_INCOND: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1205 /// In pattern context (RHS of == != =~ in [[ ]]) — zshrs extension
1206 /// over the bare incond state.
1207 pub static LEX_INCONDPAT: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1208 /// `int incasepat` (lex.c:130).
1209 pub static LEX_INCASEPAT: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1210 /// `int inredir` (lex.c:126).
1211 pub static LEX_INREDIR: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1212 /// Saved incmdpos from before a redirop/for/foreach/select. Mirrors
1213 /// `static int oldpos` in ctxtlex (lex.c:319).
1214 pub static LEX_OLDPOS: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
1215 /// `int infor` (lex.c:128).
1216 pub static LEX_INFOR: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1217 /// `int inrepeat_` (lex.c:129).
1218 pub static LEX_INREPEAT: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1219 /// `int intypeset` (lex.c:131).
1220 pub static LEX_INTYPESET: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1221 /// `int dbparens` (lex.c:141).
1222 pub static LEX_DBPARENS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1223 /// `int noaliases` (lex.c:135).
1224 pub static LEX_NOALIASES: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1225 /// `int inalmore` (lex.c:80). Set by `inpoptop` (input.c:775)
1226 /// when a drained non-global alias body ends in a space —
1227 /// "aliases should be expanded, as if we are continuing after
1228 /// an alias" (input.c:63) — so the NEXT word is alias-eligible
1229 /// even outside command position (`alias sudo='sudo '` chains).
1230 /// Consulted by `checkalias` (lex.c:1917); cleared by `exalias`
1231 /// (lex.c:2016).
1232 pub static LEX_INALMORE: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1233 /// `int nocorrect` (lex.c:144).
1234 pub static LEX_NOCORRECT: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1235 /// `int nocomments` (lex.c:148).
1236 pub static LEX_NOCOMMENTS: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
1237 /// `int lexflags` (lex.c:118).
1238 pub static LEX_LEXFLAGS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1239 /// `int aliasspaceflag` (parse.c:39, zsh.h:3103). Set to 1 by the
1240 /// alias-expansion path in `cmd_or_math` (lex.c:1930) when an
1241 /// expanded non-global alias starts with a space; consulted by
1242 /// hist.c:1428 to suppress history entries the same way a literal
1243 /// space-prefixed command line is. Cleared by `parse_event`
1244 /// (parse.c:618). parse_context_save/restore preserve across
1245 /// nested parses.
1246 pub static LEX_ALIAS_SPACE_FLAG: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1247 /// `mod_export int wordbeg` (lex.c:123). The cursor-relative
1248 /// start index of the in-flight word, set in gettok when
1249 /// LEXFLAGS_ZLE is active. Consumed by `gotword`.
1250 pub static LEX_WORDBEG: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1251 /// `mod_export int parbegin` (lex.c:126). Marks start of a
1252 /// `(...)` substitution under ZLE so completion can recurse in.
1253 /// `-1` when no substitution is open.
1254 pub static LEX_PARBEGIN: std::cell::Cell<i32> = const { std::cell::Cell::new(-1) };
1255 /// `mod_export int parend` (lex.c:129). Marks end of a `(...)`
1256 /// substitution under ZLE. `-1` when no substitution closed.
1257 pub static LEX_PAREND: std::cell::Cell<i32> = const { std::cell::Cell::new(-1) };
1258 /// `int isfirstln` (lex.c:114).
1259 pub static LEX_ISFIRSTLN: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
1260 /// `int isfirstch` (lex.c:116).
1261 pub static LEX_ISFIRSTCH: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
1262 /// !!! WARNING: NOT IN LEX.C — Rust-only AST-glue Vec !!!
1263 /// Runs parallel to the canonical C `struct heredocs *hdocs;`
1264 /// linked list at `parse::HDOCS` (port of `Src/parse.c:84`).
1265 /// Carries terminator / strip_tabs / quoted metadata that C
1266 /// stores implicitly via tokstr; zshrs's AST consumer
1267 /// (`fill_heredoc_bodies` in parse.rs) reads it via the
1268 /// `heredoc_idx` field on `ZshRedir`.
1269 pub static LEX_HEREDOCS: std::cell::RefCell<Vec<HereDoc>> = const { std::cell::RefCell::new(Vec::new()) };
1270 /// `struct lexbufstate lexbuf` (lex.c:210).
1271 pub static LEX_LEXBUF: std::cell::RefCell<lexbufstate> = const { std::cell::RefCell::new(
1272 lexbufstate { ptr: None, siz: 0, len: 0 }
1273 )};
1274 /// `int isnewlin` (lex.c:119).
1275 pub static LEX_ISNEWLIN: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1276 /// zshrs-only: `0` when the `LEX_INPUT` window holds a STRING unit;
1277 /// otherwise the `input::strin` nesting depth at which a sourced FILE
1278 /// was installed as the window.
1279 ///
1280 /// It supplies what C reads straight out of its input globals in two
1281 /// places, both of which distinguish a file from a string:
1282 ///
1283 /// * `c:Src/lex.c:310` — `isnewlin = (inbufct) ? -1 : 1;` — the
1284 /// top-level event boundary. `1` ends the event, `-1` chains the
1285 /// next sublist into it (`par_event`, c:Src/parse.c:657).
1286 /// * `c:Src/input.c:330` — `if (((inbufflags & INP_LINENO) || !strin)
1287 /// && lastc == '\n') lineno++;` — the line counter behind
1288 /// `$LINENO` and every `file:N:` diagnostic.
1289 ///
1290 /// C gets both for free because its two input sources are structurally
1291 /// different. A STRING (`-c`, `eval`, a `$(…)` body) arrives via
1292 /// `inpush` under `strinbeg` (`parse_string`, c:Src/exec.c:283-298), so
1293 /// `inbufct` covers the whole remainder — every interior newline reads
1294 /// as `-1`, one event for the lot — and `strin` is set, so nothing is
1295 /// counted twice when the same text is re-lexed. A FILE is read through
1296 /// `inputline` (c:Src/input.c:366) with `strin == 0`: `inbufct` is 0 at
1297 /// every line end, so each line is its own event, and every newline
1298 /// counts.
1299 ///
1300 /// zshrs has ONE window for both, sized to the whole body, and the
1301 /// sourced-file driver has to set `strin` anyway — a drained window
1302 /// must report EOF rather than fall through to `inputline` and steal
1303 /// the outer reader's next line. So "characters left" and "strin" both
1304 /// read as the string case, and this records the truth instead. The
1305 /// depth (not a bool) is what keeps the `lineno` exemption honest: a
1306 /// NESTED string push inside the file — `parsestrnoerr` re-lexing a
1307 /// here-document body (c:Src/lex.c:1720), `parse_isolated` on a `$(…)`
1308 /// body — runs one `strinbeg` deeper, so `strin != depth` there and C's
1309 /// suppression still applies.
1310 ///
1311 /// `lex_init` zeroes it, so a freshly installed window is a string
1312 /// until a caller says otherwise; the two sites that park and restore
1313 /// the window — `parse_isolated` and
1314 /// `ShellExecutor::execute_script_per_command` — save and restore it
1315 /// alongside `LEX_INPUT` / `LEX_POS`.
1316 pub static LEX_FILE_WINDOW_STRIN: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1317 /// `int lex_add_raw` (lex.c:161).
1318 pub static LEX_LEX_ADD_RAW: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
1319 /// `static char *tokstr_raw` (lex.c:165).
1320 pub static LEX_TOKSTR_RAW: std::cell::RefCell<Option<String>> =
1321 const { std::cell::RefCell::new(None) };
1322 /// `struct lexbufstate lexbuf_raw` (lex.c:166).
1323 pub static LEX_LEXBUF_RAW: std::cell::RefCell<lexbufstate> = const { std::cell::RefCell::new(
1324 lexbufstate { ptr: None, siz: 0, len: 0 }
1325 )};
1326}
1327
1328/// Get the next token. Direct port of `gettok(void)` from
1329/// `Src/lex.c:613-936`. Reads chars via hgetc, dispatches on the
1330/// leading char through the `lexact1[]` table at c:725. The 23
1331/// LX1_* arms below mirror C's `switch (lexact1[STOUC(c)])` body
1332/// one-for-one; the LX1_OTHER fallthrough at c:918 routes to
1333/// `gettokstr` for word-shape lexing.
1334fn gettok() -> lextok {
1335 // c:617 — `int peekfd = -1;`. Local fd-prefix accumulator.
1336 // Written into the global `tokfd` ONLY at redir-arm exit
1337 // (c:753, 864, 913) — mirrored in lex_inang / lex_outang.
1338 let mut peekfd: i32 = -1;
1339 // c:620 — `tokstr = NULL;`. Reset before each token.
1340 set_tokstr(None);
1341
1342 // c:622 — `while (iblank(c = hgetc()) && !lexstop);` — skip
1343 // leading blanks (space/tab, NOT newline). Keep `c` from the
1344 // loop; don't unget+reread.
1345 let c = loop {
1346 match hgetc() {
1347 // `ch as u8` truncates a multibyte codepoint into the blank range
1348 // (U+4E09 → 0x09); only ASCII can be blank.
1349 Some(ch) if ch.is_ascii() && crate::ztype_h::iblank(ch as u8) => continue,
1350 Some(ch) => break ch,
1351 None => {
1352 // c:624-625 — `if (lexstop) return (errflag) ?
1353 // LEXERR : ENDINPUT;`
1354 use std::sync::atomic::Ordering;
1355 LEX_LEXSTOP.set(true);
1356 return if errflag.load(Ordering::Relaxed) != 0 {
1357 LEXERR
1358 } else {
1359 ENDINPUT
1360 };
1361 }
1362 }
1363 };
1364
1365 // c:623 — `toklineno = lineno;` runs BEFORE the lexstop check
1366 // (matches C ordering: assign first, then check stop).
1367 LEX_TOKLINENO.set(LEX_LINENO.get());
1368 // c:626 — `isfirstln = 0;`
1369 LEX_ISFIRSTLN.set(false);
1370
1371 // c:627-628 — `if ((lexflags & LEXFLAGS_ZLE) && !(inbufflags
1372 // & INP_ALIAS)) wordbeg = inbufct - (qbang && c == bangchar);`
1373 // ZLE word-begin tracking; consumed by `gotword` (c:1882).
1374 let qbang_at_bang = crate::ported::hist::qbang.load(std::sync::atomic::Ordering::SeqCst)
1375 && c as i32 == crate::ported::hist::bangchar.load(std::sync::atomic::Ordering::SeqCst);
1376 let qbang_adj: i32 = if qbang_at_bang { 1 } else { 0 };
1377 if (LEX_LEXFLAGS.get() & LEXFLAGS_ZLE) != 0
1378 && (crate::ported::input::inbufflags.with(|f| f.get()) & INP_ALIAS) == 0
1379 {
1380 LEX_WORDBEG.set(crate::ported::input::inbufct.with(|c| c.get()) - qbang_adj);
1381 }
1382 // c:630 — `hwbegin(-1-(qbang && c == bangchar));` — start a
1383 // new history word. C `hwbegin` is a function pointer flipped
1384 // by `hbegin()` between `ihwbegin` (real, hist.c:1656) and
1385 // `nohwb` (no-op). zshrs doesn't flip the pointer yet but
1386 // `ihwbegin` itself is a no-op when history is inactive
1387 // (`stophist == 2` or `histactive & HA_INWORD`), so calling it
1388 // unconditionally matches the inactive case.
1389 crate::ported::hist::ihwbegin(-1 - qbang_adj);
1390
1391 // c:631-648 — `if (dbparens)` block, inlined verbatim from
1392 // gettok body. Lexes the body of `(( ... ))` arithmetic.
1393 if LEX_DBPARENS.get() {
1394 // c:632 — `lexbuf.len = 0; lexbuf.ptr = tokstr = hcalloc(...);`
1395 LEX_LEXBUF.with_borrow_mut(|b| b.clear());
1396 // c:633 — `hungetc(c);`
1397 hungetc(c);
1398 // c:634-637 — `cmdpush(CS_MATH); c = dquote_parse(infor ?
1399 // ';' : ')', 0); cmdpop();`
1400 let end_char = if LEX_INFOR.get() > 0 { ';' } else { ')' };
1401 cmdpush(CS_MATH as u8);
1402 let dq = dquote_parse(end_char, false);
1403 cmdpop();
1404 // c:638 — `*lexbuf.ptr = '\0';` — BEFORE the error test: tokstr is
1405 // the lexbuf, so on LEXERR it still holds the partial expression and
1406 // exalias publishes it as zshlextext (`for ((i=0` → "parse error
1407 // near `i=0'").
1408 set_tokstr(Some(LEX_LEXBUF.with_borrow(|b| b.as_str().to_string())));
1409 if let Err(stopped) = dq {
1410 // c:643-645 — `if (c || …) { hungetc(c); return LEXERR; }`
1411 if let Some(stopped) = stopped {
1412 hungetc(stopped);
1413 }
1414 return LEXERR;
1415 }
1416 // c:639-642 — `if (!c && infor) { infor--; return DINPAR; }`
1417 if LEX_INFOR.get() > 0 {
1418 LEX_INFOR.set(LEX_INFOR.get() - 1);
1419 return DINPAR;
1420 }
1421 // c:643-646 — `if (c || (c = hgetc()) != ')') { hungetc(c);
1422 // return LEXERR; }`
1423 match hgetc() {
1424 Some(')') => {
1425 // c:647 — `dbparens = 0;` / c:648 — `return DOUTPAR;`
1426 LEX_DBPARENS.set(false);
1427 return DOUTPAR;
1428 }
1429 c => {
1430 if let Some(c) = c {
1431 hungetc(c);
1432 }
1433 return LEXERR;
1434 }
1435 }
1436 }
1437
1438 // treats `2` as the fd to redirect. Three shapes: `N>`/`N<`
1439 // (single redir), `N&>` (errwrite), or anything else (push
1440 // back, treat as literal digit). The digit is captured into
1441 // `peekfd` and `c` is rewritten to the operator char so the
1442 // LX1 dispatch sees the redir, not the digit.
1443 let mut c = c;
1444 // ASCII-only: `c as u8` on a multibyte char truncates into the digit range
1445 // (U+4E30 → 0x30 = '0'), which would misread a CJK word as a redirection fd.
1446 if c.is_ascii() && crate::ztype_h::idigit(c as u8) {
1447 let d = hgetc();
1448 match d {
1449 Some('&') => {
1450 let e = hgetc();
1451 if e == Some('>') {
1452 // c:653-657 — `N&>` shape detected.
1453 peekfd = (c as u8 - b'0') as i32;
1454 hungetc('>');
1455 c = '&';
1456 } else {
1457 // c:658-661 — not `N&>`, push everything back.
1458 if let Some(e) = e {
1459 hungetc(e);
1460 }
1461 hungetc('&');
1462 }
1463 }
1464 Some('>') | Some('<') => {
1465 // c:662-664 — `N>` or `N<` shape detected.
1466 peekfd = (c as u8 - b'0') as i32;
1467 c = d.unwrap();
1468 }
1469 Some(d) => {
1470 // c:665-668 — not a redir prefix, push back.
1471 hungetc(d);
1472 }
1473 None => {}
1474 }
1475 LEX_LEXSTOP.set(false);
1476 }
1477
1478 // c:678 — `if (c == hashchar && !nocomments &&
1479 // (isset(INTERACTIVECOMMENTS) ||
1480 // ((!lexflags || (lexflags & LEXFLAGS_COMMENTS)) && !expanding &&
1481 // (!interact || unset(SHINSTDIN) || strin))))`.
1482 //
1483 // Comments only fire when `#` is at word-start AND one of:
1484 // 1. INTERACTIVECOMMENTS option is set; OR
1485 // 2. Non-interactive: lexflags allows comments AND not currently
1486 // expanding AND (not interactive OR not reading stdin OR
1487 // reading from a string).
1488 //
1489 // c:679-681 — `(!lexflags || (lexflags & LEXFLAGS_COMMENTS)) &&
1490 // !expanding && (!interact || unset(SHINSTDIN) || strin)`.
1491 // `expanding` (hist.c:65) and `strin` (input.c) ARE ported; the
1492 // `|| strin` term is load-bearing: when a file is sourced from an
1493 // INTERACTIVE shell, `interact` is set and SHINSTDIN is set, so
1494 // without `strin` the `#` in `.zshrc`/`.zshenv`/`~/.cargo/env`
1495 // (`#!/bin/sh`, `# comment`) was parsed as a command. `strin` is
1496 // set while reading a sourced file / string-eval, which is exactly
1497 // when comments must still strip regardless of interactivity.
1498 let lexflags = LEX_LEXFLAGS.get();
1499 let expanding = crate::ported::hist::expanding.load(std::sync::atomic::Ordering::SeqCst) != 0;
1500 let strin = crate::ported::input::strin.with(|c| c.get()) != 0;
1501 let allow_comment_via_flags = (lexflags == 0 || (lexflags & LEXFLAGS_COMMENTS) != 0)
1502 && !expanding
1503 && (!interact() || unset(SHINSTDIN) || strin);
1504 if c as i32 == crate::ported::hist::hashchar.load(std::sync::atomic::Ordering::SeqCst)
1505 && !LEX_NOCOMMENTS.get()
1506 && (isset(INTERACTIVECOMMENTS) || allow_comment_via_flags)
1507 {
1508 // c:686-707 — comment body. Under LEXFLAGS_COMMENTS_KEEP,
1509 // capture the comment text as a STRING token; under
1510 // LEXFLAGS_COMMENTS_STRIP, return ENDINPUT at EOF; default
1511 // is to read-and-drop the comment and return NEWLIN.
1512 if LEX_LEXFLAGS.get() & LEXFLAGS_COMMENTS_KEEP != 0 {
1513 LEX_LEXBUF.with_borrow_mut(|b| b.clear());
1514 add('#');
1515 }
1516 loop {
1517 let c = hgetc();
1518 match c {
1519 Some('\n') => break,
1520 // c:692 — `while ((c = ingetc()) != '\n' && !lexstop)`: end of input
1521 // inside the comment IS lexstop, which c:704 (KEEP: no
1522 // hungetc) and c:717-718 (STRIP: ENDINPUT, not NEWLIN) test.
1523 // hgetc reports it as None without setting LEX_LEXSTOP.
1524 None => {
1525 LEX_LEXSTOP.set(true);
1526 break;
1527 }
1528 Some(c) => {
1529 if LEX_LEXFLAGS.get() & LEXFLAGS_COMMENTS_KEEP != 0 {
1530 add(c);
1531 }
1532 }
1533 }
1534 }
1535 if LEX_LEXFLAGS.get() & LEXFLAGS_COMMENTS_KEEP != 0 {
1536 set_tokstr(Some(LEX_LEXBUF.with_borrow(|b| b.as_str().to_string())));
1537 if !LEX_LEXSTOP.get() {
1538 hungetc('\n');
1539 }
1540 return STRING_LEX;
1541 }
1542 if LEX_LEXFLAGS.get() & LEXFLAGS_COMMENTS_STRIP != 0 && LEX_LEXSTOP.get() {
1543 return ENDINPUT;
1544 }
1545 return NEWLIN;
1546 }
1547
1548 // c:725 — `switch (lexact1[STOUC(c)])` table-driven dispatch.
1549 let act = lexact1_get(c);
1550 match act {
1551 LX1_BKSLASH => {
1552 let d = hgetc();
1553 if d == Some('\n') {
1554 // Line continuation - get next token
1555 return gettok();
1556 }
1557 if let Some(d) = d {
1558 hungetc(d);
1559 }
1560 LEX_LEXSTOP.set(false);
1561 gettokstr(c, false)
1562 }
1563
1564 LX1_NEWLIN => NEWLIN,
1565
1566 LX1_SEMI => {
1567 let d = hgetc();
1568 match d {
1569 Some(';') => {
1570 // !!! BASH DROP-IN GATE — no zsh C counterpart !!!
1571 // c:Src/lex.c:738 returns DSEMI unconditionally; zsh
1572 // has no `;;&` and rejects it ("parse error near `&'").
1573 // bash(1), Compound Commands: "Using ;;& in place of ;;
1574 // causes the shell to test the next pattern list in the
1575 // statement, if any, and execute any associated list on
1576 // a successful match." That is exactly zsh's `;|`
1577 // (SEMIBAR), so `;;&` lexes to SEMIBAR and every
1578 // downstream case-list path is unchanged.
1579 //
1580 // Gated on `bash_mode()` (raised only by a bare
1581 // `--bash`): ksh93 and dash BOTH reject `;;&`
1582 // (`ksh: syntax error at line 1: '&' unexpected`,
1583 // `dash: 1: Syntax error: "&" unexpected`), so the
1584 // POSIX/ksh drop-ins must keep rejecting it, and
1585 // `--zsh` keeps zsh's parse error verbatim.
1586 if crate::extensions::dash_mode::bash_mode() {
1587 match hgetc() {
1588 Some('&') => return SEMIBAR,
1589 Some(d2) => {
1590 hungetc(d2);
1591 LEX_LEXSTOP.set(false);
1592 }
1593 None => LEX_LEXSTOP.set(false),
1594 }
1595 }
1596 DSEMI
1597 }
1598 // !!! DASH-STRICT GATE (no C counterpart) !!! `;&` / `;|` are zsh/ksh/bash
1599 // case terminators; dash lexes `;` then a stray `&` / `|` (syntax error).
1600 Some('&') if !crate::dash_mode::dash_strict() => SEMIAMP,
1601 Some('|') if !crate::dash_mode::dash_strict() => SEMIBAR,
1602 _ => {
1603 if let Some(d) = d {
1604 hungetc(d);
1605 }
1606 LEX_LEXSTOP.set(false);
1607 SEMI
1608 }
1609 }
1610 }
1611
1612 LX1_AMPER => {
1613 let d = hgetc();
1614 match d {
1615 Some('&') => DAMPER,
1616 Some('!') | Some('|') if !crate::dash_mode::dash_strict() => AMPERBANG,
1617 // !!! DASH-STRICT GATE (no C counterpart) !!! dash has no `&>`
1618 // (both-streams redirection): it lexes `&` then `>file`, so
1619 // `echo a &>f` backgrounds `echo a` and truncates `f`.
1620 Some('>') if !crate::dash_mode::dash_strict() => {
1621 // c:753 — `tokfd = peekfd;` before the `&>` shape
1622 // continues into the LX1_OUTANG-like dispatch.
1623 LEX_TOKFD.set(peekfd);
1624 let e = hgetc();
1625 match e {
1626 Some('!') | Some('|') => OUTANGAMPBANG,
1627 Some('>') => {
1628 let f = hgetc();
1629 match f {
1630 Some('!') | Some('|') => DOUTANGAMPBANG,
1631 _ => {
1632 if let Some(f) = f {
1633 hungetc(f);
1634 }
1635 LEX_LEXSTOP.set(false);
1636 DOUTANGAMP
1637 }
1638 }
1639 }
1640 _ => {
1641 if let Some(e) = e {
1642 hungetc(e);
1643 }
1644 LEX_LEXSTOP.set(false);
1645 AMPOUTANG
1646 }
1647 }
1648 }
1649 _ => {
1650 if let Some(d) = d {
1651 hungetc(d);
1652 }
1653 LEX_LEXSTOP.set(false);
1654 AMPER
1655 }
1656 }
1657 }
1658
1659 LX1_BAR => {
1660 let d = hgetc();
1661 match d {
1662 Some('|') if LEX_INCASEPAT.get() <= 0 => DBAR,
1663 // !!! DASH-STRICT GATE (no C counterpart) !!! dash has no `|&`
1664 // pipe-with-stderr; `|` followed by `&` is a syntax error.
1665 Some('&') if !crate::dash_mode::dash_strict() => BARAMP,
1666 _ => {
1667 if let Some(d) = d {
1668 hungetc(d);
1669 }
1670 LEX_LEXSTOP.set(false);
1671 BAR_TOK
1672 }
1673 }
1674 }
1675
1676 LX1_INPAR => {
1677 let d = hgetc();
1678 match d {
1679 Some('(') => {
1680 if LEX_INFOR.get() > 0 {
1681 LEX_DBPARENS.set(true);
1682 return DINPAR;
1683 }
1684 // c:788 — `if (incmdpos || (isset(SHGLOB) &&
1685 // !isset(KSHGLOB)))` — under SHGLOB-without-KSHGLOB,
1686 // `((` is also math/subshell-eligible even when
1687 // not at command position.
1688 if LEX_INCMDPOS.get() || (isset(SHGLOB) && unset(KSHGLOB)) {
1689 // Could be (( arithmetic )) or ( subshell )
1690 LEX_LEXBUF.with_borrow_mut(|b| b.clear());
1691 match cmd_or_math() {
1692 CMD_OR_MATH_MATH => {
1693 set_tokstr(Some(
1694 LEX_LEXBUF.with_borrow(|b| b.as_str().to_string()),
1695 ));
1696 return DINPAR;
1697 }
1698 CMD_OR_MATH_CMD => {
1699 set_tokstr(None);
1700 return INPAR_TOK;
1701 }
1702 // c:788-791 — `tokstr` already points at the
1703 // lexbuf cmd_or_math filled, so yyerror names
1704 // it: `(( 1 +` → "parse error near ` 1 +'".
1705 CMD_OR_MATH_ERR | _ => {
1706 // c:804-810 — `if (lexflags & LEXFLAGS_ACTIVE) tokstr =
1707 // dyncat("((", tokstr ? tokstr : "");`: bufferwords()
1708 // keeps an incomplete math expression as ONE word, its
1709 // `((` included (`${(z):-"(( 1"}` is `(( 1`).
1710 let buf = LEX_LEXBUF.with_borrow(|b| b.as_str().to_string());
1711 set_tokstr(Some(if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE != 0 {
1712 format!("(({}", buf) // c:810
1713 } else {
1714 buf
1715 }));
1716 return LEXERR;
1717 }
1718 }
1719 }
1720 hungetc('(');
1721 LEX_LEXSTOP.set(false);
1722 gettokstr('(', false)
1723 }
1724 Some(')') => INOUTPAR,
1725 _ => {
1726 if let Some(d) = d {
1727 hungetc(d);
1728 }
1729 LEX_LEXSTOP.set(false);
1730 // c:821 — `if (!(isset(SHGLOB) || incond == 1 ||
1731 // incmdpos)) break; return INPAR;` — at word
1732 // boundary `(` tokenizes as Inpar when SHGLOB or
1733 // incond==1 or incmdpos. Otherwise breaks out
1734 // (falls through to gettokstr so `(` starts a
1735 // Stringg — typical for unquoted glob args like
1736 // `ls (^foo)*`).
1737 //
1738 // Case-pattern context (incasepat>0): C source does
1739 // NOT add incasepat here — the `(` is absorbed into
1740 // gettokstr as part of the pattern, and par_case
1741 // detects the leading-paren form by checking if the
1742 // resulting str starts with the Inpar marker
1743 // (parse.c:1322 hack). Mirror exactly: do not gate
1744 // on incasepat.
1745 // c:parse.c:2601 — par_cond bumps `incond` (to >1) around
1746 // the RHS of a `[[ ]]` binary op so "parentheses do
1747 // globbing" (a literal pattern/regex char), not grouping.
1748 // In that bumped state `(` must be a literal even under
1749 // SHGLOB (ksh/bash emulation) — otherwise `[[ x =~ (a)(b)
1750 // ]]` splits the regex into INPAR tokens and par_cond's
1751 // single-STRING RHS read fails with "condition expected".
1752 // Native zsh (no SHGLOB) already reached this via the
1753 // `incond == 1` miss; the explicit `> 1` guard makes it
1754 // hold under SHGLOB too.
1755 //
1756 // !!! RUST-ONLY EXCEPTION — REAL-SHELL DROP-IN ONLY !!! C
1757 // has no incond test at c:821, so real zsh returns INPAR
1758 // for ANY word-initial `(` once SHGLOB is on and a
1759 // parenthesised cond RHS is a parse error in both Bourne
1760 // emulations (`zsh -f -c 'emulate ksh -c "[[ ab =~ (a)(b)
1761 // ]]"'` → "parse error: condition expected: ab"; same under
1762 // `emulate sh`). Earlier verification used the
1763 // `emulate ksh<newline>script` form, which parses the
1764 // script under PLAIN zsh before the emulation takes
1765 // effect — a flawed oracle. Keep the exception for the
1766 // drop-in modes, whose reference is a real bash/ksh that
1767 // does accept `=~ (a)(b)`, and let the zsh-STYLE legs
1768 // (`--sh --zsh` / `--ksh --zsh`, posix_faithful cleared)
1769 // take C's INPAR. Found by parity-fuzz:
1770 // `[[ hello == (hel*|wor*) ]]` returned 1 in `--ksh --zsh`
1771 // where the zsh oracle errored.
1772 if LEX_INCOND.get() > 1 && (unset(SHGLOB) || crate::dash_mode::posix_faithful())
1773 {
1774 gettokstr('(', false)
1775 } else if isset(SHGLOB) || LEX_INCOND.get() == 1 || LEX_INCMDPOS.get() {
1776 INPAR_TOK
1777 } else {
1778 gettokstr('(', false)
1779 }
1780 }
1781 }
1782 }
1783
1784 LX1_OUTPAR => OUTPAR_TOK,
1785
1786 // c:826-864 — `case LX1_INANG:` body. In pattern context
1787 // (`incondpat`/`incasepat`), `<` is literal and falls
1788 // through to gettokstr (zshrs-only guard for `[[ < ]]`).
1789 LX1_INANG => {
1790 if LEX_INCONDPAT.get() || LEX_INCASEPAT.get() > 0 {
1791 return gettokstr(c, false);
1792 }
1793 let d = hgetc();
1794 let peek = match d {
1795 Some('(') if crate::dash_mode::dash_strict() => {
1796 // !!! DASH-STRICT GATE (no C counterpart) !!!
1797 // dash/ash have no `<(...)` process substitution. Unget the
1798 // `(` so `<` stays a plain input redirection; the `(` then
1799 // sits where a redirection target (filename) is expected, so
1800 // the parser rejects it — matching /bin/dash's
1801 // "Syntax error: \"(\" unexpected". Twin of the `<<<`
1802 // here-string gate below. Found by the dash-strictness sweep.
1803 hungetc('(');
1804 LEX_LEXSTOP.set(false);
1805 INANG_TOK
1806 }
1807 Some('(') => {
1808 // c:828-832 — `<(...)` process substitution.
1809 // Fall through to gettokstr; tokfd write doesn't
1810 // apply (process-sub, not a redir).
1811 hungetc('(');
1812 LEX_LEXSTOP.set(false);
1813 // c:831-835 `unpeekfd:` — a digit read as a redirection fd
1814 // (`2>(x)`, `2<(x)`) belongs to the word after all: push the
1815 // operator back and restart the word at the digit.
1816 if peekfd != -1 {
1817 hungetc('<'); // c:833
1818 return gettokstr(((b'0' as i32) + peekfd) as u8 as char, false); // c:834
1819 }
1820 return gettokstr('<', false);
1821 }
1822 Some('>') => INOUTANG,
1823 Some('<') => {
1824 let e = hgetc();
1825 match e {
1826 Some('(') => {
1827 hungetc('(');
1828 hungetc('<');
1829 INANG_TOK
1830 }
1831 Some('<') if crate::dash_mode::dash_strict() => {
1832 // !!! DASH-STRICT GATE (no C counterpart) !!!
1833 // dash has no `<<<` here-string. Unget the third
1834 // `<` so `<<` stays a here-document operator and
1835 // the dangling `<` becomes a stray redirection the
1836 // parser rejects — matching /bin/dash's
1837 // "Syntax error: redirection unexpected".
1838 hungetc('<');
1839 LEX_LEXSTOP.set(false);
1840 DINANG
1841 }
1842 Some('<') => TRINANG,
1843 Some('-') => DINANGDASH,
1844 _ => {
1845 if let Some(e) = e {
1846 hungetc(e);
1847 }
1848 LEX_LEXSTOP.set(false);
1849 DINANG
1850 }
1851 }
1852 }
1853 Some('&') => INANGAMP,
1854 _ => {
1855 // c:858-862 — `hungetc(d); if (isnumglob()) goto
1856 // unpeekfd; peek = INANG;`. `<digits-digits>` is
1857 // zsh's numeric-glob pattern (e.g., `<-> `, `<1-5>`,
1858 // `<10->`). isnumglob looks ahead to confirm the
1859 // shape and rewinds. When it matches, the entire
1860 // `<...>` is a glob, NOT a redir — fall through to
1861 // gettokstr so it lexes as a literal STRING token.
1862 //
1863 // Without this, `[[ $1 = <-> ]]` had the lexer
1864 // consume `<` as INANG and try to take the next
1865 // token as the redir target, blowing up with
1866 // "expected word after redirection". Critical for
1867 // any zsh code using <-> / <N-M> globs (e.g.
1868 // zinit.zsh:1983).
1869 if let Some(d) = d {
1870 hungetc(d);
1871 }
1872 if isnumglob() {
1873 // c:860
1874 // c:861 — `goto unpeekfd;` — restore peekfd
1875 // and fall through to gettokstr at LX1 break.
1876 if peekfd != -1 {
1877 // c:832
1878 hungetc(c); // c:833
1879 return gettokstr(
1880 ((b'0' as i32) + peekfd) as u8 // c:834
1881 as char,
1882 false,
1883 );
1884 }
1885 LEX_LEXSTOP.set(false);
1886 return gettokstr(c, false);
1887 }
1888 LEX_LEXSTOP.set(false);
1889 INANG_TOK
1890 }
1891 };
1892 // c:864 — `tokfd = peekfd; return peek;`.
1893 LEX_TOKFD.set(peekfd);
1894 peek
1895 }
1896
1897 // c:866-914 — `case LX1_OUTANG:` body.
1898 LX1_OUTANG => {
1899 if LEX_INCONDPAT.get() || LEX_INCASEPAT.get() > 0 {
1900 return gettokstr(c, false);
1901 }
1902 let d = hgetc();
1903 let peek = match d {
1904 Some('(') if crate::dash_mode::dash_strict() => {
1905 // !!! DASH-STRICT GATE (no C counterpart) !!!
1906 // dash/ash have no `>(...)` process substitution. Unget the
1907 // `(` so `>` stays a plain output redirection and the `(`
1908 // becomes an unexpected redirection target → parser rejects
1909 // it, matching /bin/dash. Twin of the `<(` gate above.
1910 hungetc('(');
1911 LEX_LEXSTOP.set(false);
1912 OUTANG_TOK
1913 }
1914 Some('(') => {
1915 // `>(...)` process substitution.
1916 hungetc('(');
1917 LEX_LEXSTOP.set(false);
1918 // c:831-835 `unpeekfd:` — a digit read as a redirection fd
1919 // (`2>(x)`, `2<(x)`) belongs to the word after all: push the
1920 // operator back and restart the word at the digit.
1921 if peekfd != -1 {
1922 hungetc('>'); // c:833
1923 return gettokstr(((b'0' as i32) + peekfd) as u8 as char, false); // c:834
1924 }
1925 return gettokstr('>', false);
1926 }
1927 Some('&') => {
1928 let e = hgetc();
1929 match e {
1930 Some('!') | Some('|') => OUTANGAMPBANG,
1931 _ => {
1932 if let Some(e) = e {
1933 hungetc(e);
1934 }
1935 LEX_LEXSTOP.set(false);
1936 OUTANGAMP
1937 }
1938 }
1939 }
1940 Some('!') | Some('|') => OUTANGBANG,
1941 Some('>') => {
1942 let e = hgetc();
1943 match e {
1944 Some('&') => {
1945 let f = hgetc();
1946 match f {
1947 Some('!') | Some('|') => DOUTANGAMPBANG,
1948 _ => {
1949 if let Some(f) = f {
1950 hungetc(f);
1951 }
1952 LEX_LEXSTOP.set(false);
1953 DOUTANGAMP
1954 }
1955 }
1956 }
1957 Some('!') | Some('|') => DOUTANGBANG,
1958 Some('(') => {
1959 hungetc('(');
1960 hungetc('>');
1961 OUTANG_TOK
1962 }
1963 _ => {
1964 if let Some(e) = e {
1965 hungetc(e);
1966 }
1967 LEX_LEXSTOP.set(false);
1968 // c:903 — `if (isset(HISTALLOWCLOBBER))
1969 // hwaddc('|');`
1970 if isset(HISTALLOWCLOBBER) {
1971 hwaddc('|');
1972 }
1973 DOUTANG
1974 }
1975 }
1976 }
1977 _ => {
1978 if let Some(d) = d {
1979 hungetc(d);
1980 }
1981 LEX_LEXSTOP.set(false);
1982 // c:910 — `if (!incond && isset(HISTALLOWCLOBBER))
1983 // hwaddc('|');`
1984 if LEX_INCOND.get() == 0 && isset(HISTALLOWCLOBBER) {
1985 hwaddc('|');
1986 }
1987 OUTANG_TOK
1988 }
1989 };
1990 // c:913 — `tokfd = peekfd; return peek;`.
1991 LEX_TOKFD.set(peekfd);
1992 peek
1993 }
1994
1995 // c:918 — `case LX1_OTHER: return gettokstr(c, 0);`. All
1996 // non-redir, non-separator chars (including `{`/`}`/`[`/`]`)
1997 // fall into gettokstr; the LX2_* arms handle word shape.
1998 // Reserved-word promotion (`{` → INBRACE, `[[` → DINBRACK,
1999 // etc.) happens in exalias via reswdtab lookup (c:2002-2005).
2000 _ => gettokstr(c, false),
2001 }
2002}
2003
2004/// Get rest of token string
2005/// Port of `gettokstr(int c, int sub)` from `Src/lex.c:937`.
2006fn gettokstr(c: char, sub: bool) -> lextok {
2007 let mut bct = 0; // brace count
2008 let mut pct = 0; // parenthesis count
2009 let mut brct = 0; // bracket count
2010 let mut in_brace_param = 0;
2011 // c:941 — `in_pattern = 0`: inside `${name/pat/repl}` it is 2 right
2012 // after an operator slash, 1 while the PATTERN is read, and 0 again once
2013 // the separating `/` has passed. It lets a single-quoted `/` in the
2014 // pattern be Bnull-escaped (c:1309-1310) so it is not the separator.
2015 let mut in_pattern: i32 = 0;
2016 // c:940 — `int cmdsubst = 0;`. Tracks whether we entered the
2017 // brace from `$(...)` command substitution (relevant for the
2018 // IGNOREBRACES check at lex.c:1138).
2019 let mut cmdsubst: bool = false;
2020 let _ = &mut cmdsubst; // suppress unused-warn until LX2_STRING wires it
2021 let mut peek = STRING_LEX;
2022 let mut intpos = 1;
2023 let mut unmatched = '\0';
2024 let mut c = c;
2025
2026 if !sub {
2027 LEX_LEXBUF.with_borrow_mut(|b| b.clear());
2028 }
2029
2030 loop {
2031 // C's lexer walks BYTES, so `inblank(c)` can only ever fire on a byte —
2032 // and no byte of a UTF-8 multibyte character is 0x20 or 0x09. Rust walks
2033 // `char`s, so casting one to u8 TRUNCATES the codepoint: `三` (U+4E09)
2034 // became 0x09 = TAB and `丠` (U+4E20) became 0x20 = SPACE, so an
2035 // unquoted CJK word was split apart as if it were whitespace
2036 // (`print -r -- 三` printed nothing). Only ASCII can be blank.
2037 let inbl = c.is_ascii() && crate::ztype_h::inblank(c as u8);
2038
2039 if inbl && in_brace_param == 0 && pct == 0 && !sub && !LEX_LEXBUF.with_borrow(|b| {
2040 crate::dash_mode::korn_subscript_blank(brct, LEX_OLDPOS.get(), b.ptr.as_deref().unwrap_or(""))
2041 }) {
2042 // Whitespace outside brace param ends token.
2043 //
2044 // c:958-959 — C does not break here: it sets `act =
2045 // LX2_BREAK` and lets the c:968 arm decide, and that arm's
2046 // test is `if (!in_brace_param && !sub)`. So under `sub`
2047 // (i.e. from `parse_subst_string`, c:1811) a blank is
2048 // added like any other character instead of ending the
2049 // word — `!!:s/old/A B/` under HIST_SUBST_PATTERN kept
2050 // only `A` without the `!sub` term here.
2051 break;
2052 }
2053
2054 // c:954 — `switch (lexact2[STOUC(c)])` table-driven dispatch.
2055 // Each LX2_* arm below mirrors one C `case` from Src/lex.c.
2056 // Char-with-context guards (`bct > 0`, `brct > 0`, etc.) stay
2057 // inline where C uses them.
2058 match lexact2_get(c) {
2059 // c:982 — `case LX2_OUTPAR:` — `)`.
2060 //
2061 // c:989 — `if ((sub || in_brace_param) && isset(SHGLOB)) break;`
2062 // Under SHGLOB, a `)` inside `${...}` or substitution context
2063 // ends the token rather than being added.
2064 LX2_OUTPAR => {
2065 // c:Src/lex.c:989-990 — `if ((sub || in_brace_param) &&
2066 // isset(SHGLOB)) break;`. As with LX2_INPAR above, C's `break`
2067 // exits the SWITCH and falls through to `add(c)` (c:1417) with
2068 // `c` still the raw `)`; it does NOT end the token. LX2_BAR
2069 // just below already ports this shape correctly with `add(c)`.
2070 // Bug #1052.
2071 if (sub || in_brace_param > 0) && isset(SHGLOB) {
2072 add(c);
2073 } else if in_brace_param > 0 || sub {
2074 add(Outpar);
2075 } else if pct > 0 {
2076 pct -= 1;
2077 add(Outpar);
2078 } else {
2079 break;
2080 }
2081 }
2082
2083 // c:1001 — `case LX2_BAR:`.
2084 //
2085 // c:1005 — `if (unset(SHGLOB) || (!sub && !in_brace_param))
2086 // c = Bar;` — under SHGLOB inside substitution/brace
2087 // param, `|` is left as literal `|` not tokenised to `Bar`.
2088 LX2_BAR => {
2089 if pct == 0 && in_brace_param == 0 {
2090 if sub {
2091 add(c);
2092 } else {
2093 break;
2094 }
2095 } else if unset(SHGLOB) || (!sub && in_brace_param == 0) {
2096 add(Bar);
2097 } else {
2098 add(c);
2099 }
2100 }
2101
2102 // c:1019 — `case LX2_STRING:` — `$`.
2103 LX2_STRING => {
2104 let e = hgetc();
2105 match e {
2106 Some('\\') => {
2107 let f = hgetc();
2108 if f != Some('\n') {
2109 if let Some(f) = f {
2110 hungetc(f);
2111 }
2112 hungetc('\\');
2113 add(Stringg);
2114 } else {
2115 // Line continuation after $
2116 continue;
2117 }
2118 }
2119 Some('[') if crate::dash_mode::dash_strict() => {
2120 // !!! DASH-STRICT GATE (no C counterpart) !!! dash/ash
2121 // have no `$[...]` arithmetic substitution (a deprecated
2122 // bash/zsh form; POSIX uses `$(( ))`). The `$` is a
2123 // literal dollar and `[...]` ordinary text, so
2124 // `echo $[1+2]` prints `$[1+2]` like /bin/dash. Emit
2125 // `Bnull '$'` (mirrors the `$'...'` gate above) and push
2126 // the `[` back so it re-lexes as a literal bracket.
2127 hungetc('[');
2128 LEX_LEXSTOP.set(false);
2129 add(Bnull);
2130 add('$');
2131 }
2132 Some('[') => {
2133 // c:1023 — `$[...]` arithmetic substitution.
2134 // C: `cmdpush(CS_MATHSUBST); ... c =
2135 // dquote_parse(']', sub); cmdpop();`
2136 add(Stringg);
2137 add(Inbrack);
2138 cmdpush(CS_MATHSUBST as u8);
2139 let r = dquote_parse(']', sub);
2140 cmdpop();
2141 if r.is_err() {
2142 peek = LEXERR;
2143 break;
2144 }
2145 add(Outbrack);
2146 }
2147 Some('(') => {
2148 // $(...) or $((...))
2149 add(Stringg);
2150 match cmd_or_math_sub() {
2151 CMD_OR_MATH_CMD => add(Outpar),
2152 CMD_OR_MATH_MATH => add(Outparmath),
2153 CMD_OR_MATH_ERR | _ => {
2154 peek = LEXERR;
2155 break;
2156 }
2157 }
2158 }
2159 Some('{') => {
2160 // c:1049-1057 — `${...}` parameter expansion.
2161 // C does `add(c)` where `c` was already
2162 // mapped by `lextok2[c]` at switch entry from
2163 // `$` (0x24) to Stringg (`\u{e185}`). Rust's
2164 // switch dispatches on the LX2_* class but
2165 // doesn't pre-map `c`, so `add(c)` here would
2166 // store the raw `$` byte. Use the marker
2167 // directly to match C's emitted strs section.
2168 add(Stringg);
2169 add(Inbrace);
2170 bct += 1;
2171 cmdpush(CS_BRACEPAR as u8);
2172 if in_brace_param == 0 {
2173 in_brace_param = bct;
2174 }
2175 // c:1426-1429 — `if (!cmdsubst && in_brace_param &&
2176 // act == LX2_STRING && (c == '|' || c == Bar ||
2177 // c == '{' || c == Inbrace || inblank(c))) { cmdsubst =
2178 // in_brace_param; cmdpush(CS_CURSH); }` — the char after
2179 // `${` opens a nofork substitution, whose braces count
2180 // even under IGNOREBRACES (c:1138).
2181 if !cmdsubst {
2182 if let Some(n) = hgetc() {
2183 hungetc(n);
2184 if matches!(n, '|' | '{' | ' ' | '\t' | '\n')
2185 || n == Bar
2186 || n == Inbrace
2187 {
2188 cmdsubst = true;
2189 cmdpush(CS_CURSH as u8);
2190 }
2191 }
2192 }
2193 }
2194 Some('\'') if crate::dash_mode::dash_strict() => {
2195 // !!! DASH-STRICT GATE (no C counterpart) !!!
2196 // dash has no `$'...'` ANSI-C quoting: the `$` is a
2197 // literal dollar and the following `'` opens an
2198 // ordinary single-quoted string, so `printf %s $'\t'`
2199 // yields `$\t` exactly like /bin/dash.
2200 //
2201 // The `$` MUST be emitted as an escaped literal
2202 // (`Bnull '$'`), NOT as `Stringg`: the re-lexed
2203 // single quote emits a leading `Snull`, and the byte
2204 // pair `Stringg Snull` is precisely what getkeystring
2205 // (lex.rs:4910) decodes as a `$'...'` region — which
2206 // would re-enable the ANSI-C decode we are suppressing.
2207 // `Bnull '$'` is a plain literal dollar that cannot
2208 // combine with the following `Snull`.
2209 hungetc('\'');
2210 LEX_LEXSTOP.set(false);
2211 add(Bnull);
2212 add('$');
2213 }
2214 Some('\'') => {
2215 // $'...' ANSI-C escape syntax.
2216 // Port of Src/lex.c:1284-1314 (LX2_QUOTE
2217 // branch when prev char was `String`):
2218 // only `\\` and `\'` emit a `Bnull`
2219 // marker (so getkeystring later
2220 // recognizes them as user-literal); any
2221 // other `\X` emits a literal `\` + the
2222 // following char so getkeystring's
2223 // standard `\n`/`\x`/`\u`/... decoding
2224 // can fire.
2225 //
2226 // c:1285 — the C source detects $'...' via
2227 // `strquote = (lexbuf.len && lexbuf.ptr[-1]
2228 // == String)`, i.e. by looking back for the
2229 // Stringg marker the top-level `$` handler
2230 // emitted. So the marker here MUST be
2231 // Stringg (`\u{e185}`), not Qstring (`\u{e18c}`,
2232 // which is `$` inside double quotes). Using
2233 // Qstring made getkeystring's $'...' detect
2234 // fail and the strs section diverge from C.
2235 add(Stringg);
2236 add(Snull);
2237 loop {
2238 let ch = hgetc();
2239 match ch {
2240 Some('\'') => break,
2241 Some('\\') => {
2242 let next = hgetc();
2243 match next {
2244 Some(n) => {
2245 if n == '\\' || n == '\'' {
2246 add(Bnull);
2247 } else {
2248 add('\\');
2249 }
2250 add(n);
2251 }
2252 None => {
2253 LEX_LEXSTOP.set(true);
2254 unmatched = '\'';
2255 // c:1320 — LEXERR only when not ZLE/bufferwords.
2256 if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
2257 peek = LEXERR;
2258 }
2259 break;
2260 }
2261 }
2262 }
2263 Some(ch) => {
2264 // c:1309-1310 — the same `else if (in_pattern
2265 // && c == '/') add(Bnull);` leg as a plain
2266 // `'…'`: `${s/$'/'/W}` searches for `/`.
2267 if in_pattern != 0 && ch == '/' {
2268 add(Bnull);
2269 }
2270 add(ch)
2271 }
2272 None => {
2273 LEX_LEXSTOP.set(true);
2274 unmatched = '\'';
2275 // c:1320 — LEXERR only when not ZLE/bufferwords.
2276 if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
2277 peek = LEXERR;
2278 }
2279 break;
2280 }
2281 }
2282 }
2283 if unmatched != '\0' {
2284 break;
2285 }
2286 add(Snull);
2287 }
2288 Some('"') => {
2289 // $"..." localized string. Same shape as a
2290 // plain "..." but flagged via Stringg+Dnull
2291 // (NOT Qstring) so the dollar prefix marker
2292 // sits in the strs section as `\u{e185}\u{e19e}…`.
2293 // Qstring (`\u{e18c}`) is reserved for $X
2294 // sequences encountered INSIDE double quotes
2295 // (c:1524, 1546, 1551 inside dquote_parse);
2296 // top-level `$"…"` uses Stringg per
2297 // C lex.c's $-dispatch.
2298 // !!! BASH/KSH-MODE (no C counterpart) !!! bash/ksh locale translation
2299 // `$"..."` is the plain string when no catalog applies; zsh keeps
2300 // the `$` literal.
2301 if !(crate::dash_mode::bash_mode() || crate::dash_mode::korn_mode()) {
2302 add(Stringg);
2303 }
2304 add(Dnull);
2305 if dquote_parse('"', sub).is_err() {
2306 peek = LEXERR;
2307 break;
2308 }
2309 add(Dnull);
2310 }
2311 _ => {
2312 if let Some(e) = e {
2313 hungetc(e);
2314 }
2315 LEX_LEXSTOP.set(false);
2316 add(Stringg);
2317 }
2318 }
2319 }
2320
2321 // c:1057 — `case LX2_INBRACK:` — `[`.
2322 LX2_INBRACK => {
2323 if in_brace_param == 0 {
2324 brct += 1;
2325 }
2326 add(Inbrack);
2327 }
2328
2329 // c:1063 — `case LX2_OUTBRACK:` — `]`.
2330 LX2_OUTBRACK => {
2331 if in_brace_param == 0 && brct > 0 {
2332 brct -= 1;
2333 }
2334 add(Outbrack);
2335 }
2336
2337 // c:1078 — `case LX2_INPAR:` — `(`.
2338 LX2_INPAR => {
2339 // c:1079-1086 — under SHGLOB, `(` inside `${...}` or
2340 // substitution context breaks; and `(` after a non-
2341 // empty lexbuf is a break unless KSHGLOB is also set
2342 // (KSH-style `name(...)` pattern matching).
2343 // c:Src/lex.c:1080-1081 — `if (sub || in_brace_param) break;`.
2344 // That `break` leaves the SWITCH, not the token loop: `c` is
2345 // still the literal `(`, so the common `add(c)` after the
2346 // switch (c:1417) emits it and lexing CONTINUES to the closing
2347 // `}`. A Rust `break` exits the lexer loop and ENDS the token,
2348 // so `${(t)PATH}` under SHGLOB stopped at the `(` with
2349 // in_brace_param still 1 and died "closing brace expected".
2350 // The `goto brk` case below is a REAL token break and stays a
2351 // Rust `break`. Bug #1052.
2352 let mut shglob_literal_inpar = false;
2353 if isset(SHGLOB) {
2354 if sub || in_brace_param > 0 {
2355 shglob_literal_inpar = true;
2356 }
2357 // c:1084 — `!KSHGLOB && lexbuf.len → goto brk`. In sh
2358 // emulation (SHGLOB, no KSHGLOB) a `(` after existing
2359 // content ends the word (ksh function-def heuristic). But
2360 // in the `[[ ]]` cond-RHS PATTERN context (par_cond bumps
2361 // incond >1 so "parentheses do globbing"), adjacent groups
2362 // like `[[ x =~ (a)(b) ]]` must stay ONE regex word — the
2363 // second `(` is a literal, not a word break. Without this
2364 // exception, `--bash` (KSHGLOB off) split the regex and
2365 // par_cond failed with "condition expected", where real
2366 // bash matches (`bash -c '[[ ab =~ (a)(b) ]]'` → 0).
2367 //
2368 // !!! RUST-ONLY EXCEPTION — REAL-SHELL DROP-IN ONLY !!! C
2369 // has no incond test at c:1084, and real zsh takes the
2370 // `goto brk`: under parse-time `emulate sh` EVERY
2371 // parenthesised cond RHS is a parse error, `=~` included
2372 // (`zsh -f -c 'emulate sh -c "[[ ab =~ (a)(b) ]]"'` →
2373 // "parse error: condition expected: ab", status 1). So the
2374 // exception is gated on posix_faithful — the drop-in modes
2375 // whose reference IS a real bash-family shell — and the
2376 // zsh-STYLE legs (`--sh --zsh` / `--ksh --zsh`, which clear
2377 // the flag) get C's faithful break and zsh's parse error.
2378 // Found by parity-fuzz: `[[ file.txt == *.(txt|md) ]]`
2379 // returned 1 in `--sh --zsh` where the zsh oracle errored.
2380 else if unset(KSHGLOB)
2381 && !(crate::dash_mode::posix_faithful() && LEX_INCOND.get() > 1)
2382 && LEX_LEXBUF.with_borrow(|b| b.len) > 0
2383 {
2384 break;
2385 }
2386 }
2387 if shglob_literal_inpar {
2388 // c:1417 `add(c)` with `c` still the raw `(`.
2389 add(c);
2390 } else {
2391 // c:1086-1135 — when `(` appears inside a Stringg and
2392 // is immediately followed by `)`, the string
2393 // terminates at the `(`. The `()` is then re-lexed as
2394 // a separate INOUTPAR token. This handles function
2395 // definitions: `name()` lexes as Stringg `name` +
2396 // INOUTPAR `()`, not Stringg `name()`.
2397 //
2398 // c:1112-1131 — under SHGLOB, a `(` followed by
2399 // whitespace at the start of a command-position word
2400 // (no nested brackets/braces) is a ksh function
2401 // definition signal — same break-out behaviour.
2402 if in_brace_param == 0 && !sub {
2403 let e = hgetc();
2404 if let Some(ch) = e {
2405 hungetc(ch);
2406 }
2407 LEX_LEXSTOP.set(false);
2408 let is_inblank = matches!(e, Some(' ' | '\t'));
2409 if e == Some(')')
2410 || (isset(SHGLOB)
2411 && is_inblank
2412 && bct == 0
2413 && brct == 0
2414 && intpos > 0
2415 && LEX_INCMDPOS.get())
2416 {
2417 // `name()` (or KSH-style `name( ... )`)
2418 // — terminate Stringg at `(` so the
2419 // following `()` re-lexes as INOUTPAR.
2420 break;
2421 }
2422 }
2423 if in_brace_param == 0 {
2424 pct += 1;
2425 }
2426 add(Inpar);
2427 }
2428 }
2429
2430 // c:1137 — `case LX2_INBRACE:` — `{`.
2431 //
2432 // c:1138 — `if ((isset(IGNOREBRACES) && !cmdsubst) || sub)
2433 // c = '{';` — with IGNOREBRACES (or in substitution
2434 // context), `{` is added as a literal `{`, not tokenised
2435 // as Inbrace, and bct is NOT incremented.
2436 //
2437 // c:1141 — `if (!lexbuf.len && incmdpos) { add('{'); ...
2438 // return STRING; }` — at command position with an empty
2439 // buffer, return immediately as STRING("{") so the post-
2440 // lex `reswdtab` lookup can promote it to INBRACE_TOK
2441 // (the `{` keyword entry in `reswdtab`).
2442 //
2443 // c:1146 — `if (in_brace_param) cmdpush(CS_BRACE); bct++;`
2444 // — when entering a brace inside a `${...}` (or any other
2445 // bct++ path), push a CS_BRACE context for prompt/completion
2446 // tracking. After the switch, C falls through to `add(c)`
2447 // at lex.c:1420, where `c` was rewritten via `lextok2[c]`
2448 // (lex.c:964) to the `Inbrace` marker (lex.c:429:
2449 // `lextok2['{'] = Inbrace`). The Rust port doesn't have the
2450 // pre-switch `c = lextok2[c]` rewrite OR the post-switch
2451 // `add(c)` — both arms must be inlined per LX2 case.
2452 LX2_INBRACE => {
2453 // !!! ZSHRS EXTENSION GATE (no C counterpart) !!! `intercept
2454 // <kind> <pat> { code }` — the command word `intercept` armed
2455 // the capture, so this `{` opens an advice body, not a brace
2456 // expansion. Take the span up to the matching `}` off the
2457 // input as RAW SOURCE and return it as one STRING token: the
2458 // body's own `>>`, `;` and `|` would otherwise be lexed as
2459 // operators of the OUTER command and never reach argv. Same
2460 // hook shape as the dash_mode gate at lex.rs:1433. Off under
2461 // `zshrs --zsh`, where zsh's own "parse error near `}'" is
2462 // the correct answer.
2463 if crate::intercepts::wants_block() {
2464 crate::intercepts::disarm();
2465 if let Some(body) = crate::intercepts::scan_block_body(hgetc) {
2466 // Frame the span the way LX2_QUOTE frames a
2467 // single-quoted string (lex.rs:2532). The body is
2468 // advice SOURCE, stored verbatim and run later by
2469 // `execute_advice`; without the markers the word
2470 // reaching argv would be globbed and
2471 // parameter-expanded at REGISTRATION time, so
2472 // `$INTERCEPT_ARGS` would resolve to empty before any
2473 // command was ever intercepted. Snull is a literal
2474 // section, so a `'` inside the body needs no escaping
2475 // the way real single-quote syntax would.
2476 set_tokstr(Some(format!("{}{}{}", Snull, body, Snull)));
2477 return STRING_LEX;
2478 }
2479 // Input ran out with the body still open. The scan
2480 // consumed to EOF, so there is nothing left for the
2481 // ordinary path to choke on and falling through would
2482 // register `{` as the advice. Raise the lexer error
2483 // instead — an unterminated body is a parse error, the
2484 // same as an unterminated `{ … }` block anywhere else.
2485 return LEXERR;
2486 }
2487 if (isset(IGNOREBRACES) && !cmdsubst) || sub {
2488 add('{');
2489 } else {
2490 if LEX_LEXBUF.with_borrow(|b| b.len) == 0 && LEX_INCMDPOS.get() {
2491 // c:1141-1144 — `add('{'); *lexbuf.ptr = '\0'; return STRING;`
2492 // Direct return; C does NOT fall through to the
2493 // post-loop `hungetc(c)` because `{` was fully
2494 // consumed.
2495 add('{');
2496 set_tokstr(Some(LEX_LEXBUF.with_borrow(|b| b.as_str().to_string())));
2497 return STRING_LEX;
2498 }
2499 if in_brace_param > 0 {
2500 cmdpush(CS_BRACE as u8);
2501 }
2502 // Track braces for both ${...} param expansion and {...} brace expansion
2503 bct += 1;
2504 // c:1420 — `add(c)` after switch, with c = lextok2['{']
2505 // = Inbrace (c:429). Inlined here since the Rust port
2506 // skipped the unconditional post-switch add.
2507 add(Inbrace);
2508 }
2509 }
2510
2511 // c:1152 — `case LX2_OUTBRACE:` — `}`.
2512 //
2513 // c:1153 — `if ((isset(IGNOREBRACES) || sub) && !in_brace_param)
2514 // break;` — under IGNOREBRACES (or substitution
2515 // context), and not inside a `${...}`, the `}` is added
2516 // as a literal `}` (via the LX2_OTHER-style fallthrough).
2517 //
2518 // c:1158-1162 — `if (in_brace_param) cmdpop(); if (bct--
2519 // == in_brace_param) { if (cmdsubst) cmdpop();
2520 // in_brace_param = cmdsubst = in_pattern = 0; }` — pop
2521 // the matching CS_BRACE (and CS_BRACEPAR if closing the
2522 // outermost ${...}).
2523 LX2_OUTBRACE => {
2524 if (isset(IGNOREBRACES) || sub) && in_brace_param == 0 {
2525 add('}');
2526 } else if in_brace_param > 0 {
2527 cmdpop(); // matches CS_BRACE or CS_BRACEPAR
2528 if bct == in_brace_param {
2529 if cmdsubst {
2530 cmdpop();
2531 }
2532 in_brace_param = 0;
2533 cmdsubst = false;
2534 in_pattern = 0; // c:1163
2535 }
2536 bct -= 1;
2537 add(Outbrace);
2538 } else if bct > 0 {
2539 // Closing a brace expansion like {a,b}. c:1165 —
2540 // `c = Outbrace;` then falls through to the
2541 // switch-exit `add(c)`. C maps `}` to Outbrace
2542 // when bct>0 so the wordcode `strs` section sees
2543 // the marker byte, not the raw `}`. Without this
2544 // the strs section emits `{a,b,c}` literally
2545 // instead of `{a,b,c\x90` and wordcode parity
2546 // breaks on every brace expansion in the corpus.
2547 bct -= 1;
2548 add(Outbrace);
2549 } else {
2550 // c:1156 — `if (!bct) break;` breaks out of the
2551 // SWITCH (not the loop), then falls through to
2552 // c:1420 `add(c)`. So a stray `}` (no matching `{`)
2553 // is added as a literal `}` to the lexbuf, and the
2554 // post-lex `s == "}"` check at zshlex promotes it
2555 // to OUTBRACE_TOK. The previous Rust port broke
2556 // out of the outer loop here, dropping `}` and
2557 // returning STRING("") which never promoted.
2558 add(c);
2559 }
2560 }
2561
2562 LX2_OUTANG => {
2563 // In pattern context (incondpat), > is literal
2564 if in_brace_param > 0 || sub || LEX_INCONDPAT.get() || LEX_INCASEPAT.get() > 0 {
2565 add(c);
2566 } else {
2567 let e = hgetc();
2568 if e != Some('(') {
2569 if let Some(e) = e {
2570 hungetc(e);
2571 }
2572 LEX_LEXSTOP.set(false);
2573 break;
2574 }
2575 // >(...)
2576 add(OutangProc);
2577 if skipcomm().is_err() {
2578 peek = LEXERR;
2579 break;
2580 }
2581 add(Outpar);
2582 }
2583 }
2584
2585 // c:1187 — `case LX2_INANG:` — `<` body, direct port.
2586 // C order: SHGLOB+sub guard, then `<(...)` proc-sub
2587 // (only when NOT in brace_param/sub), then isnumglob,
2588 // then the in_brace_param/sub guard that keeps `<` as
2589 // a literal character.
2590 LX2_INANG => {
2591 // c:1188 — `if (isset(SHGLOB) && sub) break;`. Same
2592 // switch-vs-loop distinction the c:1209 case below already
2593 // documents: C's `break` falls through to the post-switch
2594 // `add(c)` (c:1417) with `c` still the raw `<`, so the token
2595 // continues and the `<` is LITERAL. Ending the token here made
2596 // `emulate sh -c '${a[(r)<->]}'` keep numeric-range-glob
2597 // semantics and match `1`, where zsh compares against the
2598 // literal text `<->` and finds nothing. Bug #1052.
2599 if isset(SHGLOB) && sub {
2600 add(c);
2601 } else {
2602 // c:1190-1198 — `e = hgetc(); if (!(in_brace_param ||
2603 // sub) && e == '(') { add(Inang); skipcomm(); c =
2604 // Outpar; break; }`. `<(...)` process-sub only when
2605 // outside brace_param/sub — inside, `<` stays literal.
2606 let e = hgetc(); // c:1190
2607 if !(in_brace_param > 0 || sub) && e == Some('(') {
2608 // c:1191
2609 add(Inang); // c:1192
2610 if skipcomm().is_err() {
2611 // c:1193
2612 peek = LEXERR;
2613 break;
2614 }
2615 add(Outpar); // c:1197 c=Outpar
2616 } else {
2617 // c:1200 — `hungetc(e);`.
2618 if let Some(e) = e {
2619 hungetc(e);
2620 }
2621 // c:1201-1207 — isnumglob → emit Inang/.../Outang.
2622 if LEX_INCONDPAT.get() || LEX_INCASEPAT.get() > 0 {
2623 // [[ ]] / case pattern context: `<` literal.
2624 // Original zshrs note: real zsh's wordcode has
2625 // Inang/Outang markers even inside ${var//pat/repl}
2626 // so isnumglob still fires below — but cond /
2627 // case patterns stay raw.
2628 add(c);
2629 } else if in_brace_param == 0 && isnumglob() {
2630 // c:1201 `if (!in_brace_param && isnumglob())`
2631 // (workers/54437): inside `${…}` a `<N-M>`
2632 // stays literal text, so `${(l<3><->):-}` pads
2633 // with `-` instead of failing `error in flags`.
2634 // c:1202-1206 — emit Inang…Outang markers.
2635 add(Inang); // c:1202
2636 while let Some(ch) = hgetc() {
2637 // c:1203
2638 if ch == '>' {
2639 break;
2640 }
2641 add(ch); // c:1204
2642 }
2643 add(Outang); // c:1205
2644 } else {
2645 // c:1208 — `lexstop = 0;`.
2646 LEX_LEXSTOP.set(false); // c:1208
2647 // c:1209-1210 — `if (in_brace_param || sub) break;`
2648 // exits the C switch and falls to the
2649 // post-switch `add(c)`. In Rust the LX2_INANG
2650 // arm doesn't fall to a shared add — add `<`
2651 // explicitly here so it lands in the token
2652 // buffer. Inside `${...}` and `$(...)` /
2653 // backticks, bare `<` is literal — required
2654 // for patterns like `${arr[@]:#<no-data>}`
2655 // (zinit.zsh:2507) which excludes elements
2656 // matching the literal `<no-data>`.
2657 if in_brace_param > 0 || sub {
2658 // c:1209
2659 add(c);
2660 } else {
2661 // c:1211 — `goto brk;` outside brace_param/sub
2662 // ends the token (bare `<` is a redirect).
2663 break;
2664 }
2665 }
2666 }
2667 }
2668 }
2669
2670 LX2_EQUALS => {
2671 if !sub {
2672 if intpos > 0 {
2673 // At start of token, check for =(...) process substitution
2674 let e = hgetc();
2675 if e == Some('(') {
2676 add(Equals);
2677 if skipcomm().is_err() {
2678 peek = LEXERR;
2679 break;
2680 }
2681 add(Outpar);
2682 } else {
2683 if let Some(e) = e {
2684 hungetc(e);
2685 }
2686 LEX_LEXSTOP.set(false);
2687 add(Equals);
2688 }
2689 } else if peek != ENVSTRING
2690 && (LEX_INCMDPOS.get() || LEX_INTYPESET.get())
2691 && bct == 0
2692 && brct == 0
2693 && LEX_INCASEPAT.get() <= 0
2694 {
2695 // c:1229-1230 — C gates only on
2696 // `(incmdpos || intypeset) && !bct && !brct`;
2697 // incasepat is not consulted. par_case sets
2698 // incasepat=-1 with incmdpos=1 before lexing
2699 // the token after a whole-`(...)` case
2700 // pattern (parse.c:1300-1302) — that token
2701 // may be the body's first word, and
2702 // `out+=hit` there must still become
2703 // ENVSTRING. Keep blocking only the >0
2704 // in-pattern states.
2705 // Check for VAR=value assignment (but not in case pattern context)
2706 let tok_so_far = LEX_LEXBUF.with_borrow(|b| b.as_str().to_string());
2707 if is_valid_assignment_target(&tok_so_far) {
2708 let next = hgetc();
2709 if next == Some('(') && crate::dash_mode::dash_strict() {
2710 // !!! DASH-STRICT GATE (no C counterpart) !!!
2711 // dash has no arrays; `name=(...)` is a hard
2712 // syntax error ("( unexpected"). Emit LEXERR
2713 // so the parse fails like /bin/dash instead
2714 // of lexing an ENVARRAY assignment.
2715 peek = LEXERR;
2716 break;
2717 }
2718 if next == Some('(') {
2719 // VAR=(...) array assignment. Per zsh
2720 // (lex.c emits ENVARRAY with tokstr =
2721 // just the variable name, NOT
2722 // including the `=`). The `=` and
2723 // `(` are consumed by the lexer; the
2724 // parser knows ENVARRAY means assign-
2725 // array and reads the body that
2726 // follows.
2727 set_tokstr(Some(
2728 LEX_LEXBUF.with_borrow(|b| b.as_str().to_string()),
2729 ));
2730 return ENVARRAY;
2731 }
2732 if let Some(next) = next {
2733 hungetc(next);
2734 }
2735 LEX_LEXSTOP.set(false);
2736 peek = ENVSTRING;
2737 intpos = 2;
2738 add(Equals);
2739 } else {
2740 add(Equals);
2741 }
2742 } else {
2743 add(Equals);
2744 }
2745 } else {
2746 add(Equals);
2747 }
2748 }
2749
2750 // c:1322 — `case LX2_BKSLASH:` — `\`.
2751 LX2_BKSLASH => {
2752 let next = hgetc();
2753 if next == Some('\n') {
2754 // Line continuation
2755 let next = hgetc();
2756 if let Some(next) = next {
2757 c = next;
2758 continue;
2759 }
2760 break;
2761 } else {
2762 add(Bnull);
2763 if let Some(next) = next {
2764 add(next);
2765 }
2766 }
2767 }
2768
2769 // c:1257 — `case LX2_QUOTE:` — `'`.
2770 //
2771 // c:1307 — `else if (!sub && isset(CSHJUNKIEQUOTES) &&
2772 // c == '\n')` — under CSHJUNKIEQUOTES, a `\n` inside a
2773 // single-quoted string terminates the string (unless
2774 // preceded by `\`, in which case both are stripped).
2775 //
2776 // c:1328 — `e = hgetc(); if (e != '\'' || unset(RCQUOTES)
2777 // || strquote) break; add(c);` — under RCQUOTES,
2778 // a doubled `''` inside a single-quoted string is an
2779 // escaped literal `'`, not the end of the string. We
2780 // re-open the loop and add a literal `'`.
2781 LX2_QUOTE => {
2782 // c:1288 — `cmdpush(CS_QUOTE);`. Pops at c:1322 (inside
2783 // RCQUOTES `''` re-entry) and c:1330 (final break).
2784 cmdpush(CS_QUOTE as u8);
2785 // Single quoted string - everything literal until '
2786 add(Snull);
2787 loop {
2788 // c:1290 `STOPHIST` — `stophist += 4` (zsh.h:2267) for the
2789 // duration of the scan, paired with c:1316 `ALLOWHIST`
2790 // below. This is what makes `!` LITERAL inside `'...'`:
2791 // history expansion runs at the character-input layer, so
2792 // the lexer has to tell it to stand down while it consumes
2793 // a single-quoted string. `ihgetc` gates on it at
2794 // hist.c:429 (`if (!stophist && …) c = histsubchar(c)`),
2795 // ported at hist.rs:442.
2796 //
2797 // Unported, `print -r -- 'a!b'` expanded `!b` and died with
2798 // `event not found: b`, then left the line unterminated at
2799 // a `quote>` continuation prompt. Real zsh prints `a!b`.
2800 // C re-applies it per outer iteration, so the RCQUOTES
2801 // `''` re-entry below is covered too.
2802 crate::ported::hist::stophist
2803 .fetch_add(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst);
2804 let inner_loop_done = loop {
2805 let ch = hgetc();
2806 match ch {
2807 Some('\'') => break false,
2808 Some('\n') if !sub && isset(CSHJUNKIEQUOTES) => {
2809 // CSHJUNKIEQUOTES: bare \n terminates.
2810 // If preceded by `\`, the backslash is
2811 // stripped (c:1308-1314: `lexbuf.ptr[-1] == '\\'`
2812 // → `ptr--, len--`).
2813 let last_was_bslash =
2814 LEX_LEXBUF.with_borrow(|b| b.as_str().ends_with('\\'));
2815 if last_was_bslash {
2816 LEX_LEXBUF.with_borrow_mut(|b| {
2817 if let Some(s) = b.ptr.as_mut() {
2818 s.pop();
2819 b.len = s.len() as i32;
2820 }
2821 });
2822 // c:Src/lex.c:1308-1314 —
2823 // if (lexbuf.ptr[-1] == '\\')
2824 // lexbuf.ptr--, lexbuf.len--;
2825 // else
2826 // break;
2827 // ...
2828 // add(c);
2829 // The shared `add(c)` at c:1314 still
2830 // runs after the backslash is dropped,
2831 // so `\<newline>` inside '...' under
2832 // CSHJUNKIEQUOTES keeps a LITERAL
2833 // newline. Omitting it joined the two
2834 // lines: `line three line four`
2835 // instead of two lines
2836 // (E01options.ztst:14).
2837 add('\n');
2838 } else {
2839 // c:Src/lex.c:1311 — `else break;`
2840 // leaves `c == '\n'` for the c:1317
2841 // `if (c != '\'')` test (string
2842 // unterminated → c:1318 `unmatched =
2843 // '\''`, c:1320-1321 `peek =
2844 // LEXERR`) AND for c:1444
2845 // `hungetc(c)`. Pushing the newline
2846 // back is what decrements `lineno`
2847 // (Src/input.c:561-562), so zsh
2848 // reports `(eval):1: unmatched '`.
2849 // zshrs fell through to EOF instead
2850 // and reported line 2
2851 // (E01options.ztst:14).
2852 c = '\n';
2853 unmatched = '\''; // c:1318
2854 if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
2855 peek = LEXERR; // c:1321
2856 }
2857 break true; // c:1323 goto brk
2858 }
2859 }
2860 Some(ch) => {
2861 // c:1309-1310 — `else if (in_pattern && c == '/')
2862 // add(Bnull);`: a quoted `/` in a `${v/pat/…}`
2863 // pattern is not the pattern/replacement separator.
2864 if in_pattern != 0 && ch == '/' {
2865 add(Bnull);
2866 }
2867 add(ch)
2868 }
2869 None => {
2870 LEX_LEXSTOP.set(true);
2871 unmatched = '\'';
2872 peek = LEXERR;
2873 break true;
2874 }
2875 }
2876 };
2877 // c:1316 `ALLOWHIST` — `stophist -= 4`. Placed before the
2878 // c:1317 `if (c != '\'')` bail-out so every exit from the
2879 // scan restores the count, exactly as C does.
2880 crate::ported::hist::stophist
2881 .fetch_sub(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst);
2882 if inner_loop_done || unmatched != '\0' {
2883 break;
2884 }
2885 // c:1328 — RCQUOTES `''` → literal `'`.
2886 if unset(RCQUOTES) {
2887 break;
2888 }
2889 let e = hgetc();
2890 if e != Some('\'') {
2891 if let Some(e) = e {
2892 hungetc(e);
2893 }
2894 LEX_LEXSTOP.set(false);
2895 break;
2896 }
2897 add('\'');
2898 }
2899 // c:1330 — `cmdpop();` matches c:1288 push.
2900 cmdpop();
2901 if unmatched != '\0' {
2902 break;
2903 }
2904 add(Snull);
2905 }
2906
2907 // c:1334 — `case LX2_DQUOTE:` — `"`.
2908 //
2909 // c:1338-1340 — `cmdpush(CS_DQUOTE); c = dquote_parse('"',
2910 // sub); cmdpop();`
2911 LX2_DQUOTE => {
2912 // Double quoted string
2913 add(Dnull);
2914 cmdpush(CS_DQUOTE as u8);
2915 let r = dquote_parse('"', sub);
2916 cmdpop();
2917 if let Err(stop) = r {
2918 // c:1339 `c = dquote_parse('"', sub);` — the error value
2919 // IS `c`, so the `brk:` epilogue's `hungetc(c)` (c:1444)
2920 // pushes back the character that stopped the scan
2921 // (c:1672 `err = c`), e.g. the newline that ends a
2922 // CSH_JUNKIE_QUOTES string. Keeping the opening `"` here
2923 // re-read it as a fresh quote, so every later word of
2924 // `${(Z+n+)b}` came back with a stray leading `"`.
2925 if let Some(stop) = stop {
2926 c = stop;
2927 }
2928 unmatched = '"';
2929 if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
2930 peek = LEXERR;
2931 }
2932 break;
2933 }
2934 add(Dnull);
2935 }
2936
2937 // c:1351 — `case LX2_BQUOTE:` — `` ` ``. Push/pop at
2938 // c:1352, 1379.
2939 //
2940 // c:1362 — `else if (!sub && isset(CSHJUNKIEQUOTES))
2941 // add(c);` — under CSHJUNKIEQUOTES, a `\<newline>` inside
2942 // backticks keeps the literal newline (line continuation
2943 // is NOT applied).
2944 //
2945 // c:1365 — `if (!sub && isset(CSHJUNKIEQUOTES) &&
2946 // c == '\n') break;` — under CSHJUNKIEQUOTES, a bare `\n`
2947 // inside backticks terminates the substitution.
2948 LX2_BQUOTE => {
2949 // Backtick command substitution
2950 add(Tick);
2951 cmdpush(CS_BQUOTE as u8);
2952 SETPARBEGIN!(); // c:1353
2953 // c:1354 — `inquote = 0;`. Tracks whether the backtick body is
2954 // currently inside a single-quoted string, so c:1369-1374 can
2955 // suspend history expansion for exactly that span (`!` is
2956 // literal inside `\`…'a!b'…\``, live outside it).
2957 let mut inquote = false;
2958 loop {
2959 let ch = hgetc();
2960 match ch {
2961 Some('`') => break,
2962 Some('\\') => {
2963 let next = hgetc();
2964 match next {
2965 Some('\n') => {
2966 if !sub && isset(CSHJUNKIEQUOTES) {
2967 add('\n');
2968 }
2969 // Line continuation (default)
2970 continue;
2971 }
2972 Some(c) if c == '`' || c == '\\' || c == '$' => {
2973 add(Bnull);
2974 add(c);
2975 }
2976 Some(c) => {
2977 add('\\');
2978 add(c);
2979 }
2980 None => {
2981 // c:1357 `c = hgetc()` hit end of input;
2982 // c:1380 `c != '`'` → unmatched.
2983 LEX_LEXSTOP.set(true);
2984 unmatched = '`';
2985 if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
2986 peek = LEXERR; // c:1384
2987 }
2988 break;
2989 }
2990 }
2991 }
2992 Some('\n') if !sub && isset(CSHJUNKIEQUOTES) => {
2993 // c:1365-1366 — CSHJUNKIEQUOTES: a bare `\n` ends
2994 // the scan with `c == '\n'`, which c:1380 sees as
2995 // an unterminated backquote (c:1381-1385) and the
2996 // `brk:` epilogue pushes back (c:1444). Breaking
2997 // out without that added the closing Tick and
2998 // kept lexing, gluing the next line onto the word.
2999 c = '\n';
3000 unmatched = '`'; // c:1381
3001 if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
3002 peek = LEXERR; // c:1384
3003 }
3004 break;
3005 }
3006 Some(ch) => {
3007 add(ch); // c:1368
3008 // c:1369-1374 — a `'` toggles `inquote`; entering
3009 // the quote raises STOPHIST, leaving it lowers.
3010 if ch == '\'' {
3011 inquote = !inquote;
3012 if inquote {
3013 crate::ported::hist::stophist.fetch_add(
3014 crate::ported::zsh_h::STOPHIST_DELTA,
3015 Ordering::SeqCst,
3016 ); // c:1371
3017 } else {
3018 crate::ported::hist::stophist.fetch_sub(
3019 crate::ported::zsh_h::STOPHIST_DELTA,
3020 Ordering::SeqCst,
3021 ); // c:1373
3022 }
3023 }
3024 }
3025 None => {
3026 LEX_LEXSTOP.set(true);
3027 unmatched = '`';
3028 // c:1383 — LEXERR only when not ZLE/bufferwords.
3029 if LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE != 0 { /* tolerate */
3030 } else {
3031 peek = LEXERR;
3032 }
3033 break;
3034 }
3035 }
3036 }
3037 // c:1377-1378 — `if (inquote) ALLOWHIST` — an UNTERMINATED
3038 // single quote inside the backticks still has to give the
3039 // count back, or stophist leaks upward and silently disables
3040 // history expansion for the rest of the session.
3041 if inquote {
3042 crate::ported::hist::stophist
3043 .fetch_sub(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst);
3044 }
3045 // c:1379 — `cmdpop();` matches c:1352 push.
3046 cmdpop();
3047 if unmatched != '\0' {
3048 break;
3049 }
3050 add(Tick);
3051 SETPAREND!(); // c:1388
3052 }
3053
3054 // c:1044 — `case LX2_TILDE:` — `~`.
3055 LX2_TILDE => {
3056 add(Tilde);
3057 }
3058
3059 // c:1162 — `case LX2_COMMA:` — `,`.
3060 //
3061 // c:1163 — `if (unset(IGNOREBRACES) && !sub && bct > in_brace_param)
3062 // c = Comma;` — only emit Comma (brace-expansion sep)
3063 // when IGNOREBRACES is off, not in substitution context,
3064 // and we are deeper than the current `${...}` brace.
3065 // Otherwise emit literal `,` (falls through to LX2_OTHER).
3066 LX2_COMMA if unset(IGNOREBRACES) && !sub && bct > in_brace_param => {
3067 add(Comma);
3068 }
3069 LX2_COMMA => {
3070 add(c);
3071 }
3072
3073 // c:1394 — `case LX2_DASH:` — `-`.
3074 LX2_DASH => {
3075 add(Dash);
3076 }
3077
3078 // c:1400 — `case LX2_BANG:` — `!`.
3079 LX2_BANG if brct > 0 => {
3080 add(Bang);
3081 }
3082
3083 // c:967 — `case LX2_BREAK:` — `;` and `&`.
3084 //
3085 // Direct port of C: `if (!in_brace_param && !sub) goto brk;`.
3086 // C does NOT consult `brct` here — bare `[` in an assignment
3087 // RHS like `a=foo[bar; echo` does NOT prevent `;` from
3088 // terminating the word. zsh keeps `foo[bar` as the literal
3089 // value (the `[` is unmatched and globbing reports
3090 // "no matches" only when expanded, not at lex time).
3091 // The previous Rust port added `&& brct == 0` which made
3092 // `;` part of the token whenever an unmatched `[` was
3093 // seen — `a=foo[bar; echo done` produced `a=foo[bar;`
3094 // as the single ENVSTRING, swallowing the next command.
3095 // Bug #604. `pct == 0` is also dropped — `(...)` grouping
3096 // is handled by the outer parser's command structure, not
3097 // by `;` being mid-token here.
3098 // c:968 — the `!sub` term above is not decoration: with
3099 // `sub` set (the one caller is `parse_subst_string`,
3100 // c:1811 `gettokstr(c, 1)`) a break character is NOT a word
3101 // boundary, it falls out of the switch to the shared
3102 // `add(c)` at c:1421 like any other character. Dropping the
3103 // term truncated the string at the first blank, `;` or `&`:
3104 // `!!:s/old/A B/` under HIST_SUBST_PATTERN substituted `A`
3105 // and the shell re-read the rest of the line.
3106 LX2_BREAK if in_brace_param == 0 && !sub => {
3107 break;
3108 }
3109 LX2_BREAK => {
3110 add(c);
3111 }
3112
3113 // c:1411 — `case LX2_OTHER:` — fallthrough.
3114 // C translates via `c = lextok2[STOUC(c)]` then adds —
3115 // turning `*` → `Star`, `?` → `Quest`, `#` → `Pound`,
3116 // `^` → `Hat` (other byte-token chars `{`, `[`, `$`,
3117 // `~` already have their own LX2_* arms).
3118 //
3119 // zshrs preserves the existing `\n` early-termination
3120 // behavior at top level — C handles `\n` in `gettok`,
3121 // not `gettokstr`, but our gettok hands off mid-word
3122 // through `lex_initial_other`, so `\n` can land here.
3123 LX2_OTHER => {
3124 if c == '\n' && in_brace_param == 0 && pct == 0 && brct == 0 {
3125 break;
3126 }
3127 // c:1412-1419 — `if (in_brace_param) { if (c == '/') {
3128 // if (in_pattern == 0) in_pattern = 2; else --in_pattern; } }`
3129 if in_brace_param > 0 && c == '/' {
3130 if in_pattern == 0 {
3131 in_pattern = 2; // c:1415
3132 } else {
3133 in_pattern -= 1; // c:1417
3134 }
3135 }
3136 // Multibyte UTF-8 codepoints (>= 256) pass through
3137 // verbatim — lextok2 only maps the 256-entry byte
3138 // table (C's `lextok2[STOUC(c)]` truncates to u8).
3139 // Previously `lextok2_get(c) as char` truncated
3140 // codepoint to low byte (日 U+65E5 → å U+00E5),
3141 // mangling every unquoted multibyte assignment value
3142 // (`a=日本; echo $a` printed "å,"). The table only
3143 // matters for ASCII glob metacharacters; everything
3144 // else is identity.
3145 if c.is_ascii() {
3146 // The table maps ASCII only; its token bytes become
3147 // zshrs token chars (crate::token_char).
3148 let t = lextok2_get(c);
3149 add(crate::token_char::token_char_from_byte(t).unwrap_or(t as char));
3150 } else {
3151 add(c);
3152 }
3153 }
3154
3155 _ => {
3156 add(c);
3157 }
3158 }
3159
3160 c = match hgetc() {
3161 Some(c) => c,
3162 None => {
3163 LEX_LEXSTOP.set(true);
3164 break;
3165 }
3166 };
3167
3168 if intpos > 0 {
3169 intpos -= 1;
3170 }
3171 // c:1431-1432 — `else if (in_pattern == 2 && c != '/') in_pattern = 1;`
3172 // (the `if` leg, the `${…|…}` cmdsubst push, needs a STRING char, so
3173 // it never holds right after the `/` that set in_pattern to 2).
3174 if in_pattern == 2 && c != '/' {
3175 in_pattern = 1;
3176 }
3177 }
3178
3179 // Put back the character that ended the token
3180 if !LEX_LEXSTOP.get() {
3181 hungetc(c);
3182 }
3183
3184 // c:1445-1446 — `if (unmatched && !(lexflags & LEXFLAGS_ACTIVE))
3185 // zerr("unmatched %c", unmatched);`
3186 if unmatched != '\0' && LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
3187 zerr(&format!("unmatched {}", unmatched));
3188 }
3189
3190 // c:1447-1453 — `zerr("closing brace expected");` when in_brace_param
3191 // is still open at end of token. Suppressed while actively lexing for ZLE
3192 // completion (LEXFLAGS_ACTIVE): `echo ${PA<Tab>` is an open brace at the
3193 // cursor, not a syntax error — zsh completes it to `${PATH}`.
3194 if in_brace_param > 0 && LEX_LEXFLAGS.get() & LEXFLAGS_ACTIVE == 0 {
3195 zerr("closing brace expected");
3196 }
3197 // c:1453-1469 — `} else if (unset(IGNOREBRACES) && !sub &&
3198 // lexbuf.len > 1 && peek == STRING && lexbuf.ptr[-1] == '}' &&
3199 // lexbuf.ptr[-2] != Bnull) {`
3200 // c:1457 — /* hack to get {foo} command syntax work */
3201 // A word ENDING in an unmatched literal `}` (bct==0 left it in
3202 // the buffer un-rewritten — lextok2['}'] is identity, c:419)
3203 // sheds the `}` and pushes it back so the next gettok lexes it
3204 // as the standalone `}` word → reswdtab OUTBRACE. This is what
3205 // makes `{print hi}` / `f(){print x}` close their blocks and
3206 // `echo hi}` a parse error, while mid-word `a}b` stays literal.
3207 else if unset(IGNOREBRACES)
3208 && !sub
3209 && LEX_LEXBUF.with_borrow(|b| b.buf_len()) > 1
3210 && peek == STRING_LEX
3211 && LEX_LEXBUF.with_borrow(|b| {
3212 let mut it = b.as_str().chars().rev();
3213 it.next() == Some('}') && it.next() != Some(Bnull)
3214 })
3215 {
3216 // c:1463-1464 — `int lar = lex_add_raw; lex_add_raw =
3217 // lexbuf_raw.len > 0 && lexbuf_raw.ptr[-1] == '}';` — only
3218 // strip the raw-mirror `}` if the raw buffer also ends in
3219 // one (alias expansion can desynchronize them, per the C
3220 // comment "Just go with it, OK?").
3221 let lar = LEX_LEX_ADD_RAW.get();
3222 LEX_LEX_ADD_RAW.set(
3223 LEX_LEXBUF_RAW.with_borrow(|b| {
3224 (b.len > 0 && b.ptr.as_deref().unwrap_or("").ends_with('}')) as i32
3225 }),
3226 );
3227 // c:1465-1466 — `lexbuf.ptr--; lexbuf.len--;`
3228 LEX_LEXBUF.with_borrow_mut(|b| {
3229 b.pop();
3230 });
3231 // c:1467-1468 — `lexstop = 0; hungetc('}');`
3232 LEX_LEXSTOP.set(false);
3233 hungetc('}');
3234 // !!! RUST-ONLY !!! — raw recording is on but this `}` is no longer
3235 // in the raw record (an alias frame took it out when it was
3236 // popped): in C it re-enters an alias continuation frame that
3237 // takes it out again (c:Src/input.c:571-603), so its re-read must
3238 // leave the raw record alone. See `LEX_UNGET_RAW`.
3239 if lar != 0 && LEX_LEX_ADD_RAW.get() == 0 {
3240 LEX_UNGET_RAW.with_borrow_mut(|b| {
3241 if let Some(front) = b.front_mut() {
3242 *front = true;
3243 }
3244 });
3245 }
3246 // c:1469 — `lex_add_raw = lar;`
3247 LEX_LEX_ADD_RAW.set(lar);
3248 }
3249
3250 set_tokstr(Some(LEX_LEXBUF.with_borrow(|b| b.as_str().to_string())));
3251 peek
3252}
3253
3254/// Parse the body of a double-quoted string (or any context that
3255/// uses double-quote tokenization — `(( ))`, `${...}`, `$( ( ) )`).
3256/// Direct port of `dquote_parse` from `Src/lex.c:1486`. Reads chars
3257/// until `endchar` is seen at depth 0, handling escapes, `${...}`,
3258/// `$(...)`, backtick, `$((...))`, and inner `"..."`.
3259///
3260/// C returns `err`, and on a non-EOF error `err = c` (c:1664-1673): the
3261/// character the scan stopped on, which `cmd_or_math` and the `((` arm of
3262/// gettok push back before re-lexing. `Err(Some(c))` carries that character;
3263/// `Err(None)` is the EOF exit (`err = intick || endchar || err`, c:1659) and a
3264/// failed nested `$(`, where C's value is not a character either.
3265fn dquote_parse(endchar: char, sub: bool) -> Result<(), Option<char>> {
3266 // c:1490-1491 —
3267 // int math = endchar == ')' || endchar == ']' || infor;
3268 // int zlemath = math && zlemetacs > zlemetall + addedx - inbufct;
3269 // Sampled BEFORE the first character is read: `zlemath` records that the
3270 // ZLE cursor still lies AHEAD of the input read point, i.e. inside the
3271 // math body this call is about to consume.
3272 let zlemath = (endchar == ')' || endchar == ']' || LEX_INFOR.get() > 0) && {
3273 let inbufct = crate::ported::input::inbufct.with(|c| c.get());
3274 crate::ported::zle::compcore::ZLEMETACS.load(Ordering::SeqCst)
3275 > crate::ported::zle::compcore::ZLEMETALL.load(Ordering::SeqCst)
3276 + crate::ported::zle::compcore::ADDEDX.load(Ordering::SeqCst)
3277 - inbufct
3278 };
3279
3280 // C has a single exit (`return err` at c:1676) so c:1674-1675 runs on
3281 // EVERY way out, error paths included. This port grew ten early exits
3282 // (eight `return`s plus two `?`s); holding the body in an immediately
3283 // called closure funnels them all back here, and the build gate admits
3284 // no second module-level `fn` to split it into.
3285 let body = || -> Result<(), Option<char>> {
3286 let mut pct = 0; // parenthesis count
3287 let mut brct = 0; // bracket count
3288 let mut bct = 0; // brace count (for ${...})
3289 // c:1488 — `cmdsubst = 0` is the `bct` level of an open nofork
3290 // `${|…}` / `${ … }`, and c:1489 `bskip` counts the plain braces
3291 // inside it, which tokenize as if unquoted (c:1560) and do not close
3292 // the substitution.
3293 let mut cmdsubst = 0;
3294 let mut bskip = 0;
3295 // c:1516 — `int intick = 0`. A TRISTATE, not a flag: 0 = outside
3296 // backticks, 1 = inside backticks, 2 = inside backticks AND inside a
3297 // single-quoted string within them. State 2 is what suspends history
3298 // expansion (c:1595), so modelling this as a bool made `!` live inside
3299 // `"\`…'a!b'…\`"`, where zsh keeps it literal.
3300 let mut intick = 0i32; // inside backtick
3301 let is_math = endchar == ')' || endchar == ']' || LEX_INFOR.get() > 0;
3302
3303 // c:1643-1657 — on exit, drain matched-but-unpopped pushes:
3304 // every CS_BQUOTE (if `intick`), CS_BRACEPAR + (CS_CURSH when
3305 // it was set) (for each remaining bct level). Wrapped via
3306 // closure so success + every early Err goes through cleanup.
3307 let cleanup = |intick: i32, bct: i32, cmdsubst: i32| {
3308 // c:1642-1643 — `if (intick == 2) ALLOWHIST` — an unterminated
3309 // single quote inside backticks must still restore the count.
3310 if intick == 2 {
3311 crate::ported::hist::stophist
3312 .fetch_sub(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst);
3313 }
3314 if intick != 0 {
3315 cmdpop();
3316 }
3317 // c:1645-1656 — `while (bct) { if (bct-- == cmdsubst) cmdpop();
3318 // cmdpop(); }` — the nofork level also pops its CS_CURSH.
3319 for level in (1..=bct).rev() {
3320 if level == cmdsubst {
3321 cmdpop();
3322 }
3323 cmdpop(); // CS_BRACEPAR for each remaining `{`.
3324 }
3325 };
3326
3327 loop {
3328 let c = hgetc();
3329 let c = match c {
3330 // c:1492-1494 — the loop runs while `c != endchar || bct ||
3331 // (math && (pct > 0 || brct > 0)) || intick`; only its negation
3332 // ends the scan. An endchar still inside math parens or
3333 // brackets goes through the switch below, where `)` / `]`
3334 // with nothing of its own kind open is an error (c:1603-1612).
3335 // The port used to add it and decrement the count, so
3336 // `$(( a[ ))` ran `pct` negative to EOF instead of stopping on
3337 // the first `)`.
3338 Some(c)
3339 if c == endchar
3340 && intick == 0
3341 && bct == 0
3342 && !(is_math && (pct > 0 || brct > 0)) =>
3343 {
3344 cleanup(intick, bct, cmdsubst);
3345 return Ok(());
3346 }
3347 Some(c) => c,
3348 None => {
3349 LEX_LEXSTOP.set(true);
3350 cleanup(intick, bct, cmdsubst);
3351 // c:1659-1660 — `if (lexstop) err = intick || endchar || err;`:
3352 // running out of input is an error only inside a backquote
3353 // or when a terminator was expected. parsestrnoerr passes
3354 // endchar '\0' (c:1725), so an unclosed `${` there parses and
3355 // "bad substitution" is reported later by paramsubst, as the
3356 // c:1648-1655 comment intends (`a='${'; print ${(e)a}`).
3357 if intick == 0 && endchar == '\0' {
3358 return Ok(());
3359 }
3360 return Err(None); // c:1659 — lexstop
3361 }
3362 };
3363
3364 match c {
3365 // c:1499 — `case '\\':`.
3366 '\\' => {
3367 let next = hgetc();
3368 match next {
3369 // c:1510-1512 — at end of input C's `hgetc` returns
3370 // `' '` with `lexstop` set, so the escape-list test
3371 // fails and the backslash is added literally; the
3372 // `if (err || lexstop) break;` after the switch then
3373 // stops before `add(c)`. The port ends parsestr's
3374 // input with a `\0` sentinel instead (parsestrnoerr),
3375 // which equals endchar `\0` and took the
3376 // `add(Bnull); add(c)` arm: a trailing `\` became a
3377 // NUL byte (`x='a\'; print ${(e)x}`, a PROMPT_SUBST
3378 // prompt ending in `\`). Add the backslash and hand
3379 // the sentinel back so the loop ends on it.
3380 Some('\0') if endchar == '\0' => {
3381 add('\\');
3382 hungetc('\0');
3383 }
3384 Some('\n') => {
3385 // c:1515 — `else if (sub || unset(CSHJUNKIEQUOTES)
3386 // || endchar != '"') continue;` — under
3387 // CSHJUNKIEQUOTES inside `"..."`, `\<newline>`
3388 // is NOT line continuation; it falls through
3389 // to add literal `\n`.
3390 if sub || unset(CSHJUNKIEQUOTES) || endchar != '"' {
3391 continue;
3392 }
3393 add('\n');
3394 }
3395 Some(c)
3396 if c == '$'
3397 || c == '\\'
3398 || (c == '}' && intick == 0 && bct > 0)
3399 || c == endchar
3400 || c == '`'
3401 || (endchar == ']'
3402 && (c == '['
3403 || c == ']'
3404 || c == '('
3405 || c == ')'
3406 || c == '{'
3407 || c == '}'
3408 || (c == '"' && sub))) =>
3409 {
3410 // c:Src/lex.c:1501-1513 — C's `\X` arm in
3411 // dquote_parse emits `Bnull X` exactly when
3412 // `X` is in the special-escape list. The
3413 // literal `{`/`}` that may follow `\$` is NOT
3414 // in C's list at 1501, so C emits a raw
3415 // `{`/`}` next. The subst-time scanner at
3416 // subst.rs:2920 already gates raw `{` count
3417 // by checking `prev2 != Bnull && prev2 != '\\'`
3418 // (a two-char look-back, not a one-char check),
3419 // so `Bnull $ {` reads as escaped and stays
3420 // uncounted. Symmetric for the closing `}`:
3421 // the `\}` immediately after `y` emits its
3422 // own `Bnull }` via THIS arm (since `}` is
3423 // in the special list when `!intick && bct > 0`),
3424 // and the scanner's `prev != Bnull` gate keeps
3425 // it uncounted. No extra Bnull insertion needed.
3426 add(Bnull);
3427 add(c);
3428 }
3429 Some(c) => {
3430 add('\\');
3431 hungetc(c);
3432 continue;
3433 }
3434 None => {
3435 add('\\');
3436 }
3437 }
3438 }
3439
3440 // c:1517 — `case '\n': err = !sub && isset(CSHJUNKIEQUOTES)
3441 // && endchar == '"';` — under CSHJUNKIEQUOTES, a bare `\n`
3442 // inside `"..."` is an error (unterminated string).
3443 '\n' if !sub && isset(CSHJUNKIEQUOTES) && endchar == '"' => {
3444 return Err(Some(c)); // c:1673 — `err = c`
3445 }
3446
3447 '$' => {
3448 if intick != 0 {
3449 add(c);
3450 continue;
3451 }
3452 let next = hgetc();
3453 match next {
3454 Some('(') => {
3455 add(Qstring);
3456 match cmd_or_math_sub() {
3457 CMD_OR_MATH_CMD => add(Outpar),
3458 CMD_OR_MATH_MATH => add(Outparmath),
3459 CMD_OR_MATH_ERR | _ => return Err(None),
3460 }
3461 }
3462 Some('[') => {
3463 // c:1541 — `cmdpush(CS_MATHSUBST); err =
3464 // dquote_parse(']', sub); cmdpop();`
3465 add(Stringg);
3466 add(Inbrack);
3467 cmdpush(CS_MATHSUBST as u8);
3468 let r = dquote_parse(']', sub);
3469 cmdpop();
3470 r?;
3471 add(Outbrack);
3472 }
3473 Some('{') => {
3474 // c:1548 — `cmdpush(CS_BRACEPAR); bct++;`
3475 add(Qstring);
3476 add(Inbrace);
3477 cmdpush(CS_BRACEPAR as u8);
3478 bct += 1;
3479 // c:1631-1640 — `if (!cmdsubst && c == Inbrace)`:
3480 // `${|…}` / `${ … }` opens a nofork command
3481 // substitution whose body is parsed as a command.
3482 if cmdsubst == 0 {
3483 if let Some(nc) = hgetc() {
3484 if nc == '|' || nc == ' ' || nc == '\t' || nc == '\n' {
3485 cmdsubst = bct; // c:1635
3486 cmdpush(CS_CURSH as u8); // c:1636
3487 }
3488 hungetc(nc); // c:1638
3489 }
3490 }
3491 }
3492 Some('$') => {
3493 add(Qstring);
3494 add('$');
3495 }
3496 _ => {
3497 if let Some(next) = next {
3498 hungetc(next);
3499 }
3500 LEX_LEXSTOP.set(false);
3501 add(Qstring);
3502 }
3503 }
3504 }
3505
3506 // c:1558-1563 — `case '{': if (cmdsubst && !intick) { c =
3507 // Inbrace; bskip++; }` — in a nofork body a brace group
3508 // tokenizes as if unquoted.
3509 '{' if cmdsubst != 0 && intick == 0 => {
3510 add(Inbrace);
3511 bskip += 1;
3512 }
3513
3514 '}' => {
3515 if intick != 0 || bct == 0 {
3516 add(c);
3517 } else if bskip > 0 {
3518 // c:1567-1570 — the closer of a nofork body's own
3519 // brace group, not of the `${`.
3520 add(Outbrace);
3521 bskip -= 1;
3522 } else {
3523 // c:1571-1576 — `if (bct-- == cmdsubst) { cmdsubst = 0;
3524 // cmdpop(); } cmdpop();`
3525 add(Outbrace);
3526 if bct == cmdsubst {
3527 cmdsubst = 0;
3528 cmdpop();
3529 }
3530 cmdpop();
3531 bct -= 1;
3532 }
3533 }
3534
3535 // c:1583 — `case '`':` — backtick toggle.
3536 // c:1585 — `cmdpush(CS_BQUOTE)` on entry, c:1588
3537 // `cmdpop()` on exit.
3538 '`' => {
3539 add(Qtick);
3540 // c:1581-1582 — `if (intick == 2) ALLOWHIST` — a backtick
3541 // closing while still inside a single quote gives the
3542 // count back before the state is dropped.
3543 if intick == 2 {
3544 crate::ported::hist::stophist
3545 .fetch_sub(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst);
3546 }
3547 // c:1583 — `if ((intick = !intick))`. C's `!` maps both 1
3548 // and 2 to 0, so a backtick always closes.
3549 intick = if intick != 0 { 0 } else { 1 };
3550 if intick != 0 {
3551 SETPARBEGIN!(); // c:1584
3552 cmdpush(CS_BQUOTE as u8);
3553 } else {
3554 SETPAREND!(); // c:1587
3555 cmdpop();
3556 }
3557 }
3558
3559 // c:1591-1598 — `case '\'':` — a single quote INSIDE backticks
3560 // toggles between state 1 and 2, raising/lowering STOPHIST.
3561 // Outside backticks it falls through to the default `add(c)`,
3562 // because dquote_parse only ever runs where `'` is literal.
3563 '\'' if intick != 0 => {
3564 if intick == 1 {
3565 intick = 2;
3566 crate::ported::hist::stophist
3567 .fetch_add(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst);
3568 // c:1595
3569 } else {
3570 intick = 1;
3571 crate::ported::hist::stophist
3572 .fetch_sub(crate::ported::zsh_h::STOPHIST_DELTA, Ordering::SeqCst);
3573 // c:1597
3574 }
3575 add(c);
3576 }
3577
3578 '(' => {
3579 if !is_math || bct == 0 {
3580 pct += 1;
3581 }
3582 add(c);
3583 }
3584
3585 ')' => {
3586 if !is_math || bct == 0 {
3587 if pct == 0 && is_math {
3588 return Err(Some(c)); // c:1604 err = 1, c:1673 err = c
3589 }
3590 pct -= 1;
3591 }
3592 add(c);
3593 }
3594
3595 '[' => {
3596 if !is_math || bct == 0 {
3597 brct += 1;
3598 }
3599 add(c);
3600 }
3601
3602 ']' => {
3603 if !is_math || bct == 0 {
3604 if brct == 0 && is_math {
3605 return Err(Some(c)); // c:1612 err = 1, c:1673 err = c
3606 }
3607 brct -= 1;
3608 }
3609 add(c);
3610 }
3611
3612 '"' => {
3613 if intick != 0 || (endchar != '"' && bct == 0) {
3614 add(c);
3615 } else if bct > 0 {
3616 // c:1620 — `cmdpush(CS_DQUOTE); err =
3617 // dquote_parse('"', sub); cmdpop();`
3618 add(Dnull);
3619 cmdpush(CS_DQUOTE as u8);
3620 let r = dquote_parse('"', sub);
3621 cmdpop();
3622 r?;
3623 add(Dnull);
3624 } else {
3625 return Err(Some(c)); // c:1623 err = 1, c:1673 err = c
3626 }
3627 }
3628
3629 _ => {
3630 add(c);
3631 }
3632 }
3633 }
3634 };
3635 let err = body();
3636
3637 // c:1674-1675 —
3638 // if (zlemath && zlemetacs <= zlemetall + 1 - inbufct)
3639 // inwhat = IN_MATH;
3640 // The read point has now passed the cursor, so the cursor was inside
3641 // this math body: tell `get_comp_string` (c:1482/1549/1621) to rebuild
3642 // the word out of the line as a math operand. Reached on the error exit
3643 // too, which is what makes an UNTERMINATED `$((1+` still be IN_MATH.
3644 if zlemath {
3645 let inbufct = crate::ported::input::inbufct.with(|c| c.get());
3646 if crate::ported::zle::compcore::ZLEMETACS.load(Ordering::SeqCst)
3647 <= crate::ported::zle::compcore::ZLEMETALL.load(Ordering::SeqCst) + 1 - inbufct
3648 {
3649 crate::ported::zle::compcore::INWHAT
3650 .store(crate::ported::zsh_h::IN_MATH, Ordering::SeqCst);
3651 }
3652 }
3653 err
3654}
3655
3656/// Tokenize a string as if in double quotes (error-reporting variant).
3657/// Port of `parsestr(char **s)` from `Src/lex.c:1694`.
3658/// C body:
3659/// ```c
3660/// int err;
3661/// if ((err = parsestrnoerr(s))) { // c:1698
3662/// untokenize(*s); // c:1699
3663/// if (!(errflag & ERRFLAG_INT)) { // c:1700
3664/// if (err > 32 && err < 127) // c:1701
3665/// zerr("parse error near `%c'", err); // c:1702
3666/// else
3667/// zerr("parse error"); // c:1704
3668/// tok = LEXERR; // c:1705
3669/// }
3670/// }
3671/// return err;
3672/// ```
3673pub fn parsestr(s: &str) -> Result<String, String> {
3674 // c:1694
3675 match parsestrnoerr(s) {
3676 // c:1698
3677 Ok(result) => Ok(result),
3678 Err(msg) => {
3679 let untok = untokenize(s); // c:1699
3680 let _ = untok;
3681 let ef = errflag // c:1700
3682 .load(std::sync::atomic::Ordering::Relaxed);
3683 if (ef & crate::ported::zsh_h::ERRFLAG_INT) == 0 {
3684 // c:1700
3685 // c:1701-1704 — `if (err > 32 && err < 127)` switches between
3686 // "parse error near `%c'" and bare "parse error". The
3687 // Err(msg) string carries the diagnostic already formatted
3688 // by dquote_parse / gettokstr; emit it via zerr to match
3689 // C's stderr behaviour.
3690 zerr(&msg); // c:1702/1704
3691 set_tok(LEXERR); // c:1705
3692 }
3693 Err(msg)
3694 }
3695 }
3696}
3697
3698/// Port of `parsestrnoerr(char **s)` from `Src/lex.c:1713`.
3699///
3700/// C body:
3701/// ```c
3702/// zcontext_save();
3703/// untokenize(*s);
3704/// inpush(dupstring_wlen(*s, l), 0, NULL);
3705/// strinbeg(0);
3706/// lexbuf.len = 0; lexbuf.ptr = tokstr = *s; lexbuf.siz = l + 1;
3707/// err = dquote_parse('\0', 1);
3708/// if (tokstr) *s = tokstr;
3709/// *lexbuf.ptr = '\0';
3710/// strinend();
3711/// inpop();
3712/// zcontext_restore();
3713/// return err;
3714/// ```
3715///
3716/// Drives the real `dquote_parse` (with `endchar='\0'`, `sub=true`)
3717/// through the nested-lex-context machinery so `${...}`, `$(...)`,
3718/// `$((...))`, backticks, etc. tokenize recursively the same way
3719/// they do during a normal command parse. Returns the tokenized
3720/// string on success.
3721pub fn parsestrnoerr(s: &str) -> Result<String, String> {
3722 // c:1716 `untokenize(*s);` — C's untokenize (Src/exec.c:2077 +
3723 // Src/lex.c:38 ztokens) maps EVERY itok char to its ASCII
3724 // original: Dnull → `"`, Snull → `'`, Bnull → `\`. The Rust
3725 // plain `untokenize` deliberately STRIPS Snull/Dnull (see the
3726 // lex.rs untokenize doc block) — wrong for this call site: the
3727 // re-lex below must see the quote chars so a tokenized assoc
3728 // subscript like `Dnull a␠b Dnull` (from `${H["a b"]}`) round-
3729 // trips to the literal 5-char key `"a b"`. dquote_parse with
3730 // endchar='\0' adds a bare `"` literally (c:Src/lex.c:1615-1617
3731 // `if (intick || (endchar != '"' && !bct)) break;` → add(c)),
3732 // so the quotes survive to getarg's lookup as C intends. The
3733 // full ztokens mapping also matters for Qstring → `$`: the
3734 // dquote_parse re-lex below re-tokenizes nested `${k}`
3735 // (Src/lex.c:1519-1556 `case '$'`); a raw 0x8c marker would pass
3736 // through unrecognized and a nested-param subscript like
3737 // `${H["${k}"]}` would never expand `$k`.
3738 let untok = crate::ported::lex::untokenize_ztokens(s); // c:1716 `untokenize(*s);`
3739 let dup = dupstring_wlen(&untok, untok.len()); // c:1717
3740 // c:1715 `zcontext_save();`
3741 zcontext_save();
3742 // Drain LEX_INPUT/LEX_POS so hgetc's two-input bridge prefers the
3743 // freshly-pushed inbuf frame (via inpush below) instead of double-
3744 // reading our content from both LEX_INPUT and inbuf. Restored
3745 // after dquote_parse returns.
3746 let saved_lex_input = LEX_INPUT.with_borrow(|s| s.clone());
3747 let saved_lex_pos = LEX_POS.get();
3748 // Append the '\0' sentinel C's dupstring_wlen carries — dquote_parse
3749 // returns Ok when hgetc returns endchar ('\0' here); without the
3750 // terminator hgetc returns None at EOF and dquote_parse errors.
3751 let mut input_with_nul = dup.clone();
3752 input_with_nul.push('\0');
3753 LEX_INPUT.with_borrow_mut(|b| b.clear());
3754 LEX_POS.set(0);
3755 LEX_LEXSTOP.set(false);
3756 // c:1717 `inpush(dupstring_wlen(*s, l), 0, NULL);`
3757 // Push the body AFTER clearing LEX_INPUT so hgetc reads only from
3758 // the inbuf frame (avoids double-counting characters that appear in
3759 // both LEX_INPUT and inbuf).
3760 inpush(&input_with_nul, 0, None);
3761 // c:1718 `strinbeg(0);`
3762 strinbeg(0);
3763 // c:1719-1721 — seed lexbuf with the input string so dquote_parse's
3764 // `add()` writes append onto our copy. `lexbuf.ptr/siz/len` are
3765 // reset; tokstr is aliased to the buffer.
3766 LEX_LEXBUF.with_borrow_mut(|b| {
3767 b.ptr = Some(String::with_capacity(untok.len() + 1));
3768 b.siz = (untok.len() + 1) as i32;
3769 b.len = 0;
3770 });
3771 set_tokstr(None);
3772 // c:1722 `err = dquote_parse('\0', 1);`
3773 let parse_err = dquote_parse('\0', true).is_err();
3774 // c:1723-1725 — `if (tokstr) *s = tokstr; *lexbuf.ptr = '\0';`
3775 let result = LEX_LEXBUF.with_borrow(|b| b.as_str().to_string());
3776 // c:1726 `strinend();`
3777 strinend();
3778 // c:1727 `inpop();`
3779 inpop();
3780 // Restore LEX_INPUT/LEX_POS so the outer lexer resumes where it
3781 // left off. Pairs with the swap above.
3782 LEX_INPUT.with_borrow_mut(|b| *b = saved_lex_input);
3783 LEX_POS.set(saved_lex_pos);
3784 // c:1730 — DPUTS(cmdsp, "BUG: parsestr: cmdstack not empty.")
3785 DPUTS!(
3786 // c:1730
3787 CMDSTACK.with(|s| !s.borrow().is_empty()), // c:1730
3788 "BUG: parsestr: cmdstack not empty." // c:1730
3789 );
3790 // c:1729 `zcontext_restore();`
3791 zcontext_restore();
3792 if parse_err {
3793 // C parsestrnoerr (lex.c:1713) returns the offending char so the
3794 // caller (parsestr at lex.c:1694) can format `zerr("parse error
3795 // near \`%c'", err)`. zshrs returns Err(message); the diagnostic
3796 // already went through zerr at the dquote_parse / gettokstr
3797 // failure site, so a generic message is sufficient here.
3798 Err("parse error".to_string())
3799 } else {
3800 Ok(result)
3801 }
3802}
3803
3804/// Parse a subscript in string s. Return the position after the
3805/// closing bracket, or None on error.
3806///
3807/// Direct port of zsh/Src/lex.c:1743 `parse_subscript`. The C
3808/// source uses dupstring_wlen + inpush + dquote_parse to lex the
3809/// subscript through the main lexer; zshrs implements a focused
3810/// bracket-balancing walker that handles the same nesting rules
3811/// (`[...]`, `(...)`, `{...}`) without re-entering the lexer.
3812///
3813/// zshrs port note: zsh's parse_subscript also handles a `sub`
3814/// flag that controls whether `$` and quotes are tokenized — that
3815/// flag isn't exposed here. Most callers don't need it; the few
3816/// that do (parameter expansion's `${var[expr]}`) handle the
3817/// quote-aware lex separately at the expansion layer.
3818pub fn parse_subscript(s: &str, endchar: char) -> Option<usize> {
3819 // c:1746 `if (!*s || *s == endchar) return 0;`
3820 if s.is_empty() || s.starts_with(endchar) {
3821 return None;
3822 }
3823 let l = s.len();
3824 let untok = untokenize(s); // c:1749 `untokenize(t = dupstring_wlen(s, l));`
3825 let dup = dupstring_wlen(&untok, untok.len());
3826 // c:1748 `zcontext_save();`
3827 zcontext_save();
3828 // Park LEX_INPUT/LEX_POS exactly as `parsestr` does. C has one input
3829 // stack, so once the pushed subscript drains `hgetc` sets lexstop
3830 // (c:1750-1784 reach nothing past `t`). zshrs's hgetc falls through
3831 // to the Rust-only LEX_INPUT window instead, so a subscript with no
3832 // `endchar` (`${x:2}` → parse_subscript("2", ':')) read on into the
3833 // sourced file's NEXT EVENT until it met a `:` and ate that text.
3834 let saved_lex_input = LEX_INPUT.with_borrow(|s| s.clone());
3835 let saved_lex_pos = LEX_POS.get();
3836 LEX_INPUT.with_borrow_mut(|b| b.clear());
3837 LEX_POS.set(0);
3838 LEX_LEXSTOP.set(false);
3839 // c:1750 `inpush(t, 0, NULL);`
3840 inpush(&dup, 0, None);
3841 // c:1751 `strinbeg(0);`
3842 strinbeg(0);
3843 // c:1763-1765 — seed lexbuf and run dquote_parse with the
3844 // caller's `endchar` + `sub=false` (zshrs's API omits the C `sub`
3845 // arg — all current callers pass 0).
3846 LEX_LEXBUF.with_borrow_mut(|b| {
3847 b.ptr = Some(String::with_capacity(l + 1));
3848 b.siz = (l + 1) as i32;
3849 b.len = 0;
3850 });
3851 let parse_err = dquote_parse(endchar, false).is_err();
3852 // c:1767 `toklen = (int)lexbuf.len` — an offset into `s`, because C's
3853 // tokens are single bytes. zshrs's tokens are multi-byte `char`s, so
3854 // the raw lexbuf length overshoots `s` whenever the subscript holds a
3855 // token (`$(echo 2)` lexes to 16 bytes from 10). Untokenize is one char
3856 // in, one char out, so the untokenized lexbuf's length is the offset C
3857 // means.
3858 let toklen = LEX_LEXBUF.with_borrow(|b| {
3859 let p = b.ptr.as_deref().unwrap_or("");
3860 untokenize(p.get(..b.len as usize).unwrap_or(p)).len()
3861 });
3862 // c:1771 — DPUTS(toklen > l, "Bad length for parsed subscript")
3863 DPUTS!(toklen > l, "Bad length for parsed subscript"); // c:1771
3864 // c:1779 `strinend();` / c:1780 `inpop();` / c:1782
3865 // `zcontext_restore();`
3866 strinend();
3867 inpop();
3868 // Put the outer window back so the caller's lexer resumes where it
3869 // stopped. Pairs with the park above.
3870 LEX_INPUT.with_borrow_mut(|b| *b = saved_lex_input);
3871 LEX_POS.set(saved_lex_pos);
3872 // c:1785 — DPUTS(cmdsp, "BUG: parse_subscript: cmdstack not empty.")
3873 DPUTS!(
3874 // c:1785
3875 CMDSTACK.with(|s| !s.borrow().is_empty()), // c:1785
3876 "BUG: parse_subscript: cmdstack not empty." // c:1785
3877 );
3878 zcontext_restore();
3879 if parse_err {
3880 return None;
3881 }
3882 Some(toklen)
3883}
3884
3885/// Tokenize a string as if it were a normal command-line argument
3886/// but it may contain separators. Used for ${...%...} substitutions.
3887///
3888/// Direct port of zsh/Src/lex.c:1796 `parse_subst_string`.
3889/// zsh's version sets `noaliases = 1` + `lexflags = 0` + uses
3890/// zcontext_save/inpush/strinbeg → dquote_parse('\0', 1) →
3891/// strinend/inpop/zcontext_restore. zshrs's standalone walker
3892/// produces the same Bnull/Snull/Dnull/Inpar/Inbrack markers
3893/// without re-entering the lexer.
3894///
3895/// zshrs port note: the C source returns int (0=ok, char value =
3896/// where it stopped on error); zshrs returns Result<String,String>
3897/// returning the tokenized text directly. Lossy for callers that
3898/// need to know the exact stop position, but nothing in zshrs's
3899/// expansion layer uses that yet.
3900pub fn parse_subst_string(s: &str) -> Result<String, String> {
3901 // c:1802 `if (!*s || !strcmp(s, nulstring)) return 0;`. C nulstring
3902 // is `{Nularg, 0}` (0xa1, NUL) — defined in Src/subst.c:36. A
3903 // single-Nularg input is the empty-arg sentinel and returns
3904 // success without parsing. The previous Rust port missed the
3905 // nulstring check so a Nularg-only input would attempt re-lex,
3906 // surfacing a spurious parse error.
3907 if s.is_empty() || s == Nularg.to_string() {
3908 // c:1802
3909 return Ok(String::new());
3910 }
3911 let l = s.len();
3912 let untok = untokenize(s); // c:1804
3913 let dup = dupstring_wlen(&untok, untok.len());
3914 // c:1803 `zcontext_save();`
3915 zcontext_save();
3916 // Park LEX_INPUT/LEX_POS as `parsestr` and `parse_subscript` do: C has
3917 // one input stack, so when the pushed string drains `hgetc` sets
3918 // lexstop. zshrs's hgetc falls through to the Rust-only LEX_INPUT
3919 // window instead and lexed the sourced file's next event into the word.
3920 let saved_lex_input = LEX_INPUT.with_borrow(|s| s.clone());
3921 let saved_lex_pos = LEX_POS.get();
3922 LEX_INPUT.with_borrow_mut(|b| b.clear());
3923 LEX_POS.set(0);
3924 LEX_LEXSTOP.set(false);
3925 // c:1805 `inpush(dupstring_wlen(s, l), 0, NULL);`
3926 inpush(&dup, 0, None);
3927 // c:1806 `strinbeg(0);`
3928 strinbeg(0);
3929 // c:1807-1809 — seed lexbuf with the input string.
3930 LEX_LEXBUF.with_borrow_mut(|b| {
3931 b.ptr = Some(String::with_capacity(l + 1));
3932 b.siz = (l + 1) as i32;
3933 b.len = 0;
3934 });
3935 set_tokstr(None);
3936 // c:1810 `c = hgetc();` / c:1811 `ctok = gettokstr(c, 1);`
3937 let c0 = hgetc();
3938 let ctok = match c0 {
3939 Some(ch) => gettokstr(ch, true),
3940 None => LEXERR,
3941 };
3942 // c:1813 — `err = errflag;`. Snapshot PRE-strinend errflag so we
3943 // can restore it post-zcontext_restore (parse-time errflag bits
3944 // must not leak to the caller).
3945 let err = errflag.load(Ordering::Relaxed); // c:1813
3946 let result = LEX_LEXBUF.with_borrow(|b| b.as_str().to_string());
3947 // c:1814 `strinend();`
3948 strinend();
3949 // c:1815 `inpop();`
3950 inpop();
3951 // Put the outer window back. Pairs with the park above.
3952 LEX_INPUT.with_borrow_mut(|b| *b = saved_lex_input);
3953 LEX_POS.set(saved_lex_pos);
3954 // c:1816 — DPUTS(cmdsp, "BUG: parse_subst_string: cmdstack not empty.")
3955 DPUTS!(
3956 // c:1816
3957 CMDSTACK.with(|s| !s.borrow().is_empty()), // c:1816 cmdsp != 0
3958 "BUG: parse_subst_string: cmdstack not empty." // c:1816
3959 );
3960 // c:1817 `zcontext_restore();`
3961 zcontext_restore();
3962 // c:1819 — `errflag = err | (errflag & ERRFLAG_INT);`. Restore the
3963 // saved errflag, OR'ing in any ERRFLAG_INT bit set during parse
3964 // (user interrupt must survive). The previous Rust port skipped
3965 // this restore — parse-time ERRFLAG_ERROR bits leaked to callers,
3966 // causing the next exec-engine check to abort on a stale flag.
3967 let post_err = errflag.load(Ordering::Relaxed); // c:1819
3968 errflag.store(err | (post_err & ERRFLAG_INT), Ordering::Relaxed); // c:1819
3969 if ctok == LEXERR {
3970 // c:1820
3971 // Diagnostic already emitted via zerr at the failure site.
3972 return Err("parse error".to_string());
3973 }
3974 Ok(result)
3975}
3976
3977/// Mark the current word as the one ZLE was looking for. Direct
3978/// port of `gotword(void)` from `Src/lex.c:1882`. Computes the
3979/// new-word-end (`nwe`) and new-word-begin (`nwb`) line positions
3980/// based on `zlemetall`, `inbufct`, `addedx`, and `wordbeg`, then
3981/// — if the cursor (`zlemetacs`) falls inside that range — writes
3982/// `wb`/`we` and clears `lexflags`.
3983pub fn gotword() {
3984 let zlemetacs = crate::ported::zle::compcore::ZLEMETACS.load(Ordering::SeqCst);
3985 let zlemetall = crate::ported::zle::compcore::ZLEMETALL.load(Ordering::SeqCst);
3986 let addedx = crate::ported::zle::compcore::ADDEDX.load(Ordering::SeqCst);
3987 let inbufct = crate::ported::input::inbufct.with(|c| c.get());
3988 let wordbeg = LEX_WORDBEG.get();
3989
3990 // c:1884 — `int nwe = zlemetall + 1 - inbufct + (addedx == 2 ? 1 : 0);`
3991 let nwe = zlemetall + 1 - inbufct + if addedx == 2 { 1 } else { 0 };
3992 // c:1885 — `if (zlemetacs <= nwe)`
3993 if zlemetacs <= nwe {
3994 // c:1886 — `int nwb = zlemetall - wordbeg + addedx;`
3995 let nwb = zlemetall - wordbeg + addedx;
3996 // c:1887-1893 — `if (zlemetacs >= nwb) { wb = nwb; we = nwe; }
3997 // else { wb = zlemetacs + addedx; if (we < wb) we = wb; }`.
3998 if zlemetacs >= nwb {
3999 WB.store(nwb, Ordering::SeqCst);
4000 WE.store(nwe, Ordering::SeqCst);
4001 } else {
4002 let wb_new = zlemetacs + addedx;
4003 WB.store(wb_new, Ordering::SeqCst);
4004 let we_cur = WE.load(Ordering::SeqCst);
4005 if we_cur < wb_new {
4006 WE.store(wb_new, Ordering::SeqCst);
4007 }
4008 }
4009 // c:1895 — `lexflags = 0;`
4010 LEX_LEXFLAGS.set(0);
4011 }
4012}
4013
4014/// Direct port of `checkalias(void)` at `Src/lex.c:1902`. No
4015/// parameters in C — reads `aliastab`/`sufaliastab` directly.
4016/// zshrs threads `lextext` in because it's already untokenized at
4017/// the call site; C re-derives it from `zshlextext`. Returns true
4018/// if the lookup matched (regular or suffix alias) AND the alias
4019/// text was successfully injected back into the input stream for
4020/// re-lexing.
4021/// WARNING: param names don't match C — Rust=(lextext) vs C=()
4022fn checkalias(lextext: &str) -> bool {
4023 // lex.c:1906-1907 — guard on null lextext.
4024 if lextext.is_empty() {
4025 return false;
4026 }
4027
4028 // c:1909 — `if (!noaliases && isset(ALIASESOPT) &&
4029 // (!isset(POSIXALIASES) ||
4030 // (tok == STRING && !reswdtab->getnode(reswdtab, zshlextext))))`.
4031 //
4032 // Three gates: (1) lexer hasn't set noaliases; (2) ALIASESOPT
4033 // option is on; (3) under POSIX_ALIASES, the token must be a
4034 // STRING AND not a reserved word.
4035 if LEX_NOALIASES.get() || !isset(ALIASESOPT) {
4036 return false;
4037 }
4038 if isset(POSIXALIASES) {
4039 if tok() != STRING_LEX {
4040 return false;
4041 }
4042 let is_reswd = reswdtab_lock()
4043 .read()
4044 .expect("reswdtab poisoned")
4045 .get(lextext)
4046 .is_some();
4047 if is_reswd {
4048 return false;
4049 }
4050 }
4051
4052 // !!! ORDERING DIVERGENCE ADAPTER — C's inungetc returns a character to
4053 // the CURRENT input frame (inbufptr--), which the alias inpush that
4054 // follows then covers, so that character is read AFTER the alias body.
4055 // zshrs's hungetc queues it in LEX_UNGET_BUF, which hgetc drains BEFORE
4056 // any inbuf frame. Both alias arms need the C order — the regular arm for
4057 // the character its own hgetc/hungetc probe returns (c:1923-1924), the
4058 // suffix arm for the word terminator gettokstr gave back — so the pending
4059 // ungets move into an INP_CONT frame pushed UNDER the alias frames: alias
4060 // body → separator → terminator → rest of line. Only plain chars move —
4061 // ingetc skips itok bytes (input.rs c:328), which must stay in
4062 // LEX_UNGET_BUF to survive.
4063 let reroute_pending_ungets = || {
4064 let all_plain = LEX_UNGET_BUF.with_borrow(|b| {
4065 b.iter()
4066 .all(|&ch| !crate::token_char::itok_char(ch))
4067 });
4068 if all_plain {
4069 let pending: String =
4070 LEX_UNGET_BUF.with_borrow_mut(|b| b.drain(..).collect());
4071 // These characters LEAVE the unget queue for an
4072 // inbuf frame, so their re-read goes back through
4073 // `ihgetc` -> `hwaddc` (c:459) rather than the
4074 // flag-driven advance in `hgetc`. Undo the rewinds
4075 // `hungetc` recorded for them here so the cursor is
4076 // handed over exactly where `ihwaddc` expects it;
4077 // from this point on C's own guard (c:360) governs,
4078 // including its refusal under INP_ALIAS.
4079 let rewound: Vec<bool> =
4080 LEX_UNGET_HPTR.with_borrow_mut(|b| b.drain(..).collect());
4081 // Their re-read comes back through the fresh-read arm of
4082 // `hgetc`, which records every character raw.
4083 LEX_UNGET_RAW.with_borrow_mut(|b| b.clear());
4084 LEX_UNGET_FRAME.with_borrow_mut(|b| b.clear());
4085 // !!! RUST-ONLY !!! — the same handover for the
4086 // function-body echo buffer: these characters leave
4087 // the unget queue, so their re-read comes back
4088 // through the FRESH-read arm of `hgetc` (which calls
4089 // `src_capture_add` itself). Drop the stale flags or
4090 // every later unget pops the wrong one.
4091 crate::funcdef_capture::LEX_UNGET_SRCCAP.with_borrow_mut(|b| b.clear());
4092 let restore: usize = pending
4093 .chars()
4094 .zip(rewound.iter())
4095 .filter(|(_, &r)| r)
4096 .map(|(ch, _)| ch.len_utf8())
4097 .sum();
4098 if restore != 0 {
4099 let pos = crate::ported::hist::hptr.load(Ordering::SeqCst);
4100 crate::ported::hist::hptr.store(pos + restore, Ordering::SeqCst);
4101 }
4102 // `hungetc` bumped `inbufct` for each queued char
4103 // under LEXFLAGS_ZLE (c:input.c:558-559), expecting
4104 // `hgetc`'s unget-pop to take it back on re-read.
4105 // These chars leave the unget queue for an inbuf
4106 // frame instead, so that pop never runs for them —
4107 // and `inpush` below adds their length AGAIN for an
4108 // INP_CONT frame. Undo the unget bump here so the
4109 // count is not doubled. Without it `inbufct` stayed
4110 // one high for the rest of the line, and `gotword`
4111 // (c:lex.c:1884, `nwe = zlemetall + 1 - inbufct`)
4112 // placed the completion word END one column short of
4113 // the cursor: with `alias ls='grc --colour=on ls'`,
4114 // `ls -<TAB>` never matched the cursor word, so
4115 // `get_comp_string` returned an EMPTY word
4116 // (`clwpos == -1`, `wb == we == zlemetacs`) and
4117 // completion offered every file in the directory,
4118 // narrowing to nothing as more was typed.
4119 if LEX_LEXFLAGS.get() & LEXFLAGS_ZLE != 0 {
4120 let n = pending.chars().count() as i32;
4121 crate::ported::input::inbufct.with(|ct| ct.set(ct.get() - n));
4122 }
4123 if pending.is_empty() {
4124 // nothing queued
4125 } else if crate::ported::input::inbufflags.with(|f| f.get()) & INP_ALIAS != 0
4126 && !crate::ported::input::lexstop.with(|s| s.get())
4127 {
4128 // c:Src/input.c:549-557 — the characters were read from the
4129 // alias frame still on top (an exhausted frame is popped by
4130 // the next read before anything below it is reached), so
4131 // C's inungetc backs that frame's pointer up over them.
4132 // Layering an INP_CONT frame on top instead made them count
4133 // twice when the alias frames unwound (c:Src/input.c:
4134 // 751-752): with `alias e='echo a' f='e b'`, `eval
4135 // 'echo $(f)'` recorded the substitution as `$()`.
4136 for ch in pending.chars().rev() {
4137 crate::ported::input::inungetc(ch);
4138 }
4139 } else {
4140 inpush(&pending, INP_CONT, None);
4141 }
4142 }
4143 };
4144
4145 // lex.c:1914-1933 — regular alias lookup. C: `an = (Alias)
4146 // aliastab->getnode(aliastab, zshlextext);`
4147 let alias_clone: Option<alias> = {
4148 let guard = aliastab_lock().read().expect("aliastab poisoned");
4149 guard.get(lextext).cloned()
4150 };
4151 if let Some(alias) = alias_clone {
4152 let is_global = (alias.node.flags & crate::ported::zsh_h::ALIAS_GLOBAL) != 0;
4153 // c:1915-1917 — `if (an && !an->inuse && ((an->node.flags &
4154 // ALIAS_GLOBAL) || (incmdpos && tok == STRING) || inalmore))`
4155 // — `inalmore` extends eligibility to the word following a
4156 // trailing-space alias body (`alias sudo='sudo '` chaining).
4157 if alias.inuse.load(std::sync::atomic::Ordering::Relaxed) == 0
4158 && (is_global || (LEX_INCMDPOS.get() && tok() == STRING_LEX) || LEX_INALMORE.get() != 0)
4159 {
4160 // c:1918-1927 — `if (!lexstop) { int c = hgetc();
4161 // hungetc(c); if (!iblank(c)) inpush(" ", INP_ALIAS, 0); }`
4162 // — if the next char isn't blank, insert a space so the
4163 // alias body can't accidentally join the following word.
4164 //
4165 // c:1919-1922 — "Tokens that don't require a space after,
4166 // get one, because they are treated as if preceded by one."
4167 //
4168 // C consumes one char via hgetc then un-consumes it, so the
4169 // check uses the actual next char from the ACTIVE input
4170 // source — the unget queue, the inbuf/instack (stdin loop,
4171 // interactive, alias re-lex), or the LEX_INPUT window.
4172 // The previous Rust version peeked LEX_INPUT[pos] directly,
4173 // which never sees inbuf-stack content: for stdin/interactive
4174 // lines (`git status` with `alias git=hub`) the pending
4175 // "status" lives in inbuf, peek() returned None, the
4176 // separator push was skipped, and the alias body fused with
4177 // the following word → `hubstatus`.
4178 if !LEX_LEXSTOP.get() {
4179 // c:1918
4180 if let Some(c) = hgetc() {
4181 // c:1923
4182 hungetc(c); // c:1924
4183 // Without the re-route the terminator (blank / `;` / `\n`)
4184 // was read ahead of the alias text: `alias git=hub; git
4185 // status` fused to `hubstatus`, and the line's `\n` ran
4186 // early (PS2 prompt mid-command).
4187 reroute_pending_ungets();
4188 // ASCII-only: see the truncation note in `gettokstr`.
4189 if !(c.is_ascii() && crate::ztype_h::iblank(c as u8)) {
4190 // c:1925
4191 inpush(" ", INP_ALIAS, None); // c:1926
4192 }
4193 }
4194 // hgetc() == None → EOF: C's ingetc returns ' ' under
4195 // lexstop (input.c:322), iblank(' ') → no push. Same
4196 // net effect; nothing to unget.
4197 }
4198 // !!! RUST-ONLY !!! — the name leaves the function-body source
4199 // here, as it leaves the token stream in C (see
4200 // `funcdef_capture::src_capture_mark_alias_name`).
4201 crate::funcdef_capture::src_capture_mark_alias_name(lextext);
4202 // c:1928 — `inpush(an->text, INP_ALIAS, an);`
4203 inpush(&alias.text, INP_ALIAS, Some((lextext.to_string(), alias.node.flags)));
4204 // c:1929-1930 — `if (an->text[0] == ' ' && !(an->node.flags & ALIAS_GLOBAL))
4205 // aliasspaceflag = 1;`
4206 // Drives HISTIGNORESPACE's alias-leading-space suppression
4207 // path (hist.c:1428): without it, `alias g='echo hi'; g`
4208 // gets history-logged even with HISTIGNORESPACE set when
4209 // the alias body starts with a space.
4210 if !is_global && alias.text.starts_with(' ') {
4211 LEX_ALIAS_SPACE_FLAG.set(1);
4212 }
4213 // c:1929 — `an->inuse = 1;`. Through a READ guard: `inuse` is
4214 // atomic so the flag never splits a copy-on-write table.
4215 let guard = aliastab_lock().read().expect("aliastab poisoned");
4216 if let Some(a) = guard.get(lextext) {
4217 a.inuse.store(1, std::sync::atomic::Ordering::Relaxed);
4218 }
4219 drop(guard);
4220 LEX_LEXSTOP.set(false);
4221 return true;
4222 }
4223 }
4224
4225 // lex.c:1934-1943 — suffix-alias lookup. The token must end
4226 // with `.SUFFIX`, the suffix name must be a registered
4227 // suffix-alias, AND the lexer must be in command position.
4228 if LEX_INCMDPOS.get() {
4229 if let Some(dot_pos) = lextext.rfind('.') {
4230 if dot_pos > 0 && dot_pos + 1 < lextext.len() {
4231 let suffix = &lextext[dot_pos + 1..];
4232 let alias_clone: Option<alias> = {
4233 let guard = sufaliastab_lock().read().expect("sufaliastab poisoned");
4234 guard.get(suffix).cloned()
4235 };
4236 if let Some(alias) = alias_clone {
4237 if alias.inuse.load(std::sync::atomic::Ordering::Relaxed) == 0 {
4238 // c:1938-1940 — three inpush calls in order:
4239 // the original word, a space, the alias text.
4240 // inpush stacks LIFO so the original word is
4241 // popped FIRST (re-emitted to extend the
4242 // current token), then space, then the alias
4243 // body. C does it the same way.
4244 // The word's terminator was handed back by
4245 // gettokstr; in C it sits in the current frame,
4246 // under the three pushes below.
4247 reroute_pending_ungets();
4248 // !!! RUST-ONLY !!! — the typed word is replaced by
4249 // its re-pushed copy below; drop the original from
4250 // the function-body source.
4251 crate::funcdef_capture::src_capture_mark_alias_name(lextext);
4252 inpush(lextext, INP_ALIAS, Some((suffix.to_string(), alias.node.flags)));
4253 inpush(" ", INP_ALIAS, None);
4254 inpush(&alias.text, INP_ALIAS, None);
4255 // c:1941 — `an->inuse = 1;`.
4256 let guard = sufaliastab_lock().read().expect("sufaliastab poisoned");
4257 if let Some(a) = guard.get(suffix) {
4258 a.inuse.store(1, std::sync::atomic::Ordering::Relaxed);
4259 }
4260 drop(guard);
4261 LEX_LEXSTOP.set(false);
4262 return true;
4263 }
4264 }
4265 }
4266 }
4267 }
4268
4269 false
4270}
4271
4272/// Run alias / reserved-word expansion on the just-lexed token.
4273/// Direct port of zsh/Src/lex.c:1949-2021 `exalias`. Returns true
4274/// if an alias was injected (the caller's loop should re-run
4275/// gettok to consume the injected text).
4276///
4277/// C source flow:
4278/// 1. Spell-correct (lex.c:1958-1962) — disabled in zshrs.
4279/// 2. If tokstr is None: set lextext from `tokstrings[tok]` and
4280/// checkalias against that (lex.c:1964-1969).
4281/// 3. Otherwise: untokenize tokstr into a working copy (lex.c:
4282/// 1971-1980).
4283/// 4. ZLE word-tracking: call gotword() if LEXFLAGS_ZLE
4284/// (lex.c:1982-1991).
4285/// 5. Stringg tokens: try checkalias, then reservation lookup
4286/// (lex.c:1993-2015).
4287/// 6. Clear inalmore (lex.c:2016).
4288///
4289/// Direct port of `exalias(void)` at `Src/lex.c:1953`. No
4290/// parameters — reads global `aliastab`/`sufaliastab`/`reswdtab`
4291/// directly, mirroring C.
4292pub fn exalias() -> bool {
4293 // lex.c:1957 — `hwend()` closes the history WORD for the token gettok
4294 // just produced (zshlex loops `gettok(); while (... exalias())`, so
4295 // this fires once per token). Paired with the `ihwbegin` at the top of
4296 // gettok, it records word boundaries into `chwords`; `hend` then keys
4297 // off `chwordpos` (> 2 means ≥ 1 real word) to decide whether to save
4298 // the line. The prior port stubbed it, leaving chwordpos stuck at 1, so
4299 // every interactive line was rejected by hend's `chwordpos <= 2` gate
4300 // and history recorded nothing.
4301 crate::ported::hist::ihwend(); // c:1957
4302
4303 // c:1958-1962 — full faithful gate:
4304 // if (interact && isset(SHINSTDIN) && !strin && incasepat <= 0
4305 // && tok == STRING && !nocorrect && !(inbufflags & INP_ALIAS)
4306 // && !hist_is_in_word()
4307 // && (isset(CORRECTALL) || (isset(CORRECT) && incmdpos)))
4308 // spckword(&tokstr, 1, incmdpos, 1);
4309 let inbufflags_alias = (crate::ported::input::inbufflags.with(|f| f.get()) & INP_ALIAS) != 0;
4310 let strin_set = crate::ported::input::strin.with(|c| c.get()) != 0;
4311 if interact()
4312 && isset(SHINSTDIN)
4313 && !strin_set
4314 && LEX_INCASEPAT.get() <= 0
4315 && tok() == STRING_LEX
4316 && LEX_NOCORRECT.get() == 0
4317 && !inbufflags_alias
4318 && crate::ported::hist::hist_is_in_word() == 0
4319 && (isset(CORRECTALL) || (isset(CORRECT) && LEX_INCMDPOS.get()))
4320 {
4321 // c:1962 — `spckword(&tokstr, 1, incmdpos, 1);`. The canonical
4322 // port at utils.rs::spckword scans the right hashtables
4323 // internally.
4324 if let Some(word) = tokstr() {
4325 let mut buf = if has_token(&word) {
4326 untokenize(&word)
4327 } else {
4328 word.clone()
4329 };
4330 crate::ported::utils::spckword(
4331 &mut buf,
4332 1, // c:1962 hist=1
4333 if LEX_INCMDPOS.get() { 1 } else { 0 }, // c:1962 cmd=incmdpos
4334 1, // c:1962 ask=1
4335 );
4336 if buf != word {
4337 set_tokstr(Some(buf));
4338 }
4339 }
4340 }
4341
4342 // lex.c:1964-1969 — bare-token path (no tokstr).
4343 if LEX_TOKSTR.with_borrow(|t| t.is_none()) {
4344 // lex.c:1965 — `zshlextext = tokstrings[tok];` — for tokens
4345 // like SEMI/AMPER/etc. the canonical text comes from a
4346 // static table. C publishes it for EVERY tokstr-less token,
4347 // before the NEWLIN early return, so yyerror can name `)`,
4348 // `;`, `&&` … afterwards.
4349 let i = tok() as usize;
4350 LEX_ZSHLEXTEXT
4351 .with_borrow_mut(|t| *t = tokstrings.get(i).copied().flatten().map(|s| s.to_string()));
4352 if tok() == NEWLIN {
4353 return false;
4354 }
4355 // c:1969 — `return checkalias();` with that `tokstrings[tok]` text,
4356 // for EVERY punctuation token: a global alias named `&&`, `||`, `|&`
4357 // or `(` expands exactly like one named `;`. Limiting the lookup to
4358 // `;` / `&` / `|` left `alias -g '&&=; print yes; '` unexpanded. A
4359 // token with no table entry has a NULL zshlextext, which checkalias
4360 // rejects (c:1906-1907).
4361 let Some(text) = tokstrings.get(tok() as usize).copied().flatten() else {
4362 return false;
4363 };
4364 return checkalias(text);
4365 }
4366
4367 let tokstr = tokstr().unwrap();
4368 // c:1973-1980 —
4369 // ```c
4370 // VARARR(char, copy, (strlen(tokstr) + 1));
4371 // if (has_token(tokstr)) {
4372 // char *p, *t;
4373 // zshlextext = p = copy;
4374 // for (t = tokstr; (*p++ = itok(*t) ? ztokens[*t++ - Pound] : *t++););
4375 // } else
4376 // zshlextext = tokstr;
4377 // ```
4378 // EVERY token char maps back through `ztokens` (c:38), quote markers
4379 // included: `Snull` → `'`, `Dnull` → `"`, `Bnull` → `\`. So a word the
4380 // user quoted keeps its quote characters here, and that is precisely
4381 // what stops `'jl'` / `"if"` / `\;` from matching an alias or a
4382 // reserved word further down — the lookup key is `'jl'`, not `jl`.
4383 //
4384 // The general-purpose `untokenize` helper (lex.rs:5870) STRIPS
4385 // Snull/Dnull instead, because its other callers walk the
4386 // substitution stream where the markers must not reappear as literal
4387 // quotes. Routing exalias through it made the lookup key `jl`, so a
4388 // QUOTED word matched an alias: with `alias -g jl='| less -rMN'` set,
4389 // re-parsing a function body containing `JULIA_ICON 'jl'` expanded the
4390 // quoted element into `| less -rMN` mid-array and the parse died with
4391 // "expected `)' after array assignment". The reserved-word half of the
4392 // same divergence was already patched twice downstream via
4393 // `tokstr_has_quote_marker` (docs/BUGS.md #14, #19, #283); building the
4394 // text C's way fixes the class at the source.
4395 let lextext = if has_token(&tokstr) {
4396 let zt = ztokens.as_bytes();
4397 let chars: Vec<char> = tokstr.chars().collect();
4398 let mut copy = String::with_capacity(tokstr.len());
4399 let mut i = 0;
4400 while i < chars.len() {
4401 let c = chars[i];
4402 // Rust-only: zshrs metafies at the CHARACTER level, so a raw
4403 // high byte is stored as the pair (Meta, byte ^ 0x20) and that
4404 // continuation char can land inside the ITOK range. C's copy
4405 // loop is byte-based and has no such pair to protect; copy it
4406 // verbatim, exactly as `untokenize` (lex.rs:5895) does.
4407 if c as u32 == 0x83 && i + 1 < chars.len() {
4408 copy.push(c);
4409 copy.push(chars[i + 1]);
4410 i += 2;
4411 continue;
4412 }
4413 let cu = c as u32;
4414 if (0xe184..=0xe1a1).contains(&cu) {
4415 // c:1979 — `ztokens[*t - Pound]`. `Nularg` (0xa1) indexes
4416 // the table's terminating NUL, which ends C's copy string;
4417 // stop here for the same effect.
4418 match zt.get((cu - 0xe184) as usize) {
4419 Some(&b) => copy.push(b as char),
4420 None => break,
4421 }
4422 } else {
4423 copy.push(c);
4424 }
4425 i += 1;
4426 }
4427 copy
4428 } else {
4429 tokstr.clone()
4430 };
4431 // lex.c:1976/1980 — `zshlextext = p = copy;` / `zshlextext =
4432 // tokstr;`. C's copy is transient: c:1987/1997/2017 all reset
4433 // `zshlextext = tokstr` before exalias returns, so what survives is
4434 // always the TOKENIZED tokstr, which yyerror untokenizes itself.
4435 LEX_ZSHLEXTEXT.with_borrow_mut(|t| *t = Some(tokstr.clone()));
4436
4437 // lex.c:1982-1991 — ZLE word-tracking for completion.
4438 //
4439 // C's guard is `(lexflags & LEXFLAGS_ZLE) && !(inbufflags & INP_ALIAS)`;
4440 // the INP_ALIAS half was missing here. While the lexer reads an
4441 // `inpush`ed alias body, `inbufct`/`wordbeg` count that body, not the
4442 // command line `gotword` measures the cursor against, so the word
4443 // positions it would write are meaningless. The same guard is already
4444 // honoured at the other three ZLE-tracking sites (`SETPARBEGIN` c:469,
4445 // `SETPAREND` c:474, `wordbeg` c:627).
4446 if LEX_LEXFLAGS.get() & LEXFLAGS_ZLE != 0
4447 && (crate::ported::input::inbufflags.with(|f| f.get()) & INP_ALIAS) == 0
4448 {
4449 let zp = LEX_LEXFLAGS.get();
4450 gotword();
4451 // lex.c:1986-1990 — if gotword cleared lexflags, the cursor
4452 // word has been reached; abort exalias so completion can
4453 // capture the partial token unchanged.
4454 // lex.c:1986 — `(zp & LEXFLAGS_ZLE) && !lexflags` — gotword
4455 // fully cleared lexflags (not just ZLE) when the cursor word
4456 // was reached.
4457 if (zp & LEXFLAGS_ZLE) != 0 && LEX_LEXFLAGS.get() == 0 {
4458 return false;
4459 }
4460 }
4461
4462 // lex.c:1993-2015 — Stringg-token alias / reswd check.
4463 if tok() == STRING_LEX {
4464 // c:1995 — `if ((zshlextext != copy || !isset(POSIXALIASES))
4465 // && checkalias())` — under POSIX_ALIASES, only run
4466 // alias expansion on tokens that came in literal (`zshlextext
4467 // == copy` means no tokenisation/untokenisation happened).
4468 // zshrs always passes the untokenised `lextext`; if the
4469 // original tokstr had tokens AND POSIXALIASES is on, skip
4470 // the alias check (matches C `zshlextext != copy`).
4471 let had_tokens = has_token(&tokstr);
4472 if (!had_tokens || !isset(POSIXALIASES)) && checkalias(&lextext) {
4473 return true;
4474 }
4475
4476 // c:2002 — reserved-word lookup. Fires when in command
4477 // position OR when the text is bare `}` and IGNOREBRACES
4478 // / IGNORECLOSEBRACES are both unset (so `}` ends a brace
4479 // block).
4480 //
4481 // c:Src/lex.c:1973-1980 (un-tokenize loop): C's untokenize
4482 // REPLACES Snull/Dnull/Bnull with the literal quote chars
4483 // (`'`, `"`, `\\`) via `ztokens`. So C's lextext for `"}"` is
4484 // the 3-byte string `"}"` (quotes intact). The
4485 // `zshlextext[0] == '}' && !zshlextext[1]` check at c:2003
4486 // therefore FAILS for any quoted `}` and the reswd lookup
4487 // never fires.
4488 //
4489 // `lextext` above is now built C's way (quote markers mapped
4490 // back through `ztokens`), so a quoted `"}"` already reads as
4491 // the 3-char string `"}"` and cannot reach this branch. The
4492 // `tokstr_has_quote_marker` gate is kept as the explicit
4493 // statement of that rule for any caller that hands exalias a
4494 // pre-stripped token. Bug #14 in BUGS.md:
4495 // `[[ x == "}" ]]` and `echo "}"` both used to silently
4496 // discard the `}` because exalias promoted the quoted `}`
4497 // to OUTBRACE_TOK.
4498 let tokstr_has_quote_marker = tokstr
4499 .chars()
4500 .any(|c| c == Snull || c == Dnull || c == Bnull);
4501 let is_close_brace_special = lextext == "}"
4502 && unset(IGNOREBRACES)
4503 && unset(IGNORECLOSEBRACES)
4504 && !tokstr_has_quote_marker;
4505 // lex.c:2002-2014 — the C structure is
4506 // if ((incmdpos || ...) && (rw = reswd_lookup)) { ... }
4507 // else if (incond && lextext == "]]") { ... }
4508 // else if (incond == 1 && lextext == "!") { ... }
4509 // i.e. the cond `]]`/`!` branches are reached when the reswd
4510 // lookup FAILS — even if `incmdpos` is true. The previous
4511 // Rust port gated on `incmdpos` alone, so any `]]` reached
4512 // inside `[[ ... ]]` with incmdpos still true (the lexer
4513 // doesn't auto-reset incmdpos after `[[` in all paths) was
4514 // left as a STRING `]]` and par_cond_2 errored with
4515 // `condition expected:`. Restructure to mirror C: look up the
4516 // reswd ONLY when the gating allows, and treat a failed
4517 // lookup the same as a non-gated path so the cond branches
4518 // get their turn.
4519 let reswd_path_eligible = LEX_INCMDPOS.get() || is_close_brace_special;
4520 // c:Src/lex.c:1973-1980 — C's untokenize keeps Snull/Dnull/Bnull
4521 // quote markers AS THEIR LITERAL QUOTE CHARS in `lextext` (via
4522 // the ztokens table). So C's lextext for `"if"` is the 4-byte
4523 // string `"if"` (quotes intact) and reswd_lookup against `"if"`
4524 // never matches the bare reswd `if`. `lextext` above is built
4525 // the same way now; the `tokstr_has_quote_marker` gate below
4526 // states the rule explicitly rather than relying on it. Before
4527 // that, exalias untokenized through the marker-STRIPPING helper
4528 // and a quoted token was promoted to a reserved word.
4529 // Bug #19 in docs/BUGS.md: quoted patterns like
4530 // `"!"`, `"if"`, `"{"`, `"}"` in non-first case branches
4531 // parsed as the corresponding reswd (BANG_TOK / IF / INBRACE
4532 // / OUTBRACE) and triggered "expected ')' in case pattern".
4533 // Same root cause as bug #14's `is_close_brace_special` gate
4534 // above: when the original tokstr carries any Snull/Dnull/Bnull
4535 // marker, the text WAS quoted by the user and must keep its
4536 // literal meaning — reswd promotion is suppressed.
4537 let rw_tok: Option<lextok> = if reswd_path_eligible && !tokstr_has_quote_marker {
4538 let guard = reswdtab_lock().read().expect("reswdtab poisoned");
4539 guard.get(&lextext).map(|r| r.token)
4540 } else {
4541 None
4542 };
4543 // !!! DASH-STRICT GATE (no C counterpart) !!! dash has none of the
4544 // zsh/bash/ksh reserved words `[[` / `function` / `coproc`; each is an
4545 // ordinary command word there (`[[`/`coproc` → "not found", `function`
4546 // → the following `{` is a syntax error). Suppress ONLY these three
4547 // promotions — every POSIX reserved word (if/then/while/for/case/until/
4548 // do/done/{/}/…) stays intact. The `]]`/`!`-in-cond branches below are
4549 // gated on LEX_INCOND, which never rises now. Found by the per-mode
4550 // dash-strictness sweep.
4551 // The bare drop-in also loses `foreach` / `repeat` / `select` /
4552 // `nocorrect` / `end` and the typeset family (`typeset declare integer
4553 // float`): dash keeps only `export` / `local` / `readonly` as commands.
4554 let rw_tok = if (crate::dash_mode::dash_strict()
4555 && matches!(rw_tok, Some(DINBRACK) | Some(FUNC) | Some(COPROC)))
4556 || (crate::dash_mode::dash_faithful()
4557 && (matches!(
4558 rw_tok,
4559 Some(crate::ported::zsh_h::FOREACH)
4560 | Some(crate::ported::zsh_h::REPEAT)
4561 | Some(crate::ported::zsh_h::SELECT)
4562 | Some(NOCORRECT)
4563 | Some(crate::ported::zsh_h::ZEND)
4564 ) || (rw_tok == Some(crate::ported::zsh_h::TYPESET)
4565 && !matches!(lextext.as_str(), "export" | "local" | "readonly"))))
4566 {
4567 None
4568 } else {
4569 rw_tok
4570 };
4571 // !!! ZSHRS EXTENSION GATE (no C counterpart) !!! Arm / disarm the
4572 // `intercept … { … }` body capture. Every command word re-decides,
4573 // so an `intercept` that never reached a `{` cannot leave the next
4574 // command's brace expansion armed.
4575 if LEX_INCMDPOS.get() {
4576 crate::intercepts::note_command_word(&lextext, tokstr_has_quote_marker);
4577 }
4578 if let Some(rwtok) = rw_tok {
4579 set_tok(rwtok);
4580 if rwtok == REPEAT {
4581 LEX_INREPEAT.set(1);
4582 }
4583 if rwtok == DINBRACK {
4584 LEX_INCOND.set(1);
4585 }
4586 } else if LEX_INCOND.get() > 0 && lextext == "]]" {
4587 // lex.c:2010-2012 — `]]` closes the cond expression.
4588 set_tok(DOUTBRACK);
4589 LEX_INCOND.set(0);
4590 } else if LEX_INCOND.get() == 1 && lextext == "!" && !tokstr_has_quote_marker {
4591 // lex.c:2013-2014 — `!` inside `[[ ]]` is the Bang
4592 // negation, not a literal. Gate on
4593 // `tokstr_has_quote_marker` so QUOTED `"!"` (which
4594 // carries Snull/Dnull/Bnull markers in the original
4595 // tokstr) stays as a literal STRING token. Same fix
4596 // shape as #14/#19 above: any user-applied quotes
4597 // suppress the special-token promotion. Bug #283 in
4598 // docs/BUGS.md.
4599 set_tok(BANG_TOK);
4600 }
4601 }
4602
4603 // lex.c:2016 — `inalmore = 0;` — alias-more flag clears once a
4604 // token makes it through exalias without being re-injected as
4605 // an alias (checkalias returning true short-circuits before
4606 // this point via the `return true` above).
4607 LEX_INALMORE.set(0); // c:2016
4608
4609 false
4610}
4611
4612/// Append a char to the raw-input capture buffer. Direct port of
4613/// zsh/Src/lex.c:2025 `zshlex_raw_add`. Called from hgetc
4614/// when `lex_add_raw` is nonzero so cmd-sub bodies (`$(...)`,
4615/// `<(...)`, `>(...)`) can be replayed verbatim without re-lexing.
4616pub fn zshlex_raw_add(c: char) {
4617 // lex.c:2027-2028 — guard on lex_add_raw flag.
4618 if LEX_LEX_ADD_RAW.get() == 0 {
4619 return;
4620 }
4621 // lex.c:2030-2038 — append to lexbuf_raw. The C source manages
4622 // explicit ptr/len/siz with hrealloc; Rust's String handles
4623 // resize automatically.
4624 LEX_LEXBUF_RAW.with_borrow_mut(|b| b.add(c));
4625}
4626
4627/// Pop the last char from the raw-input capture buffer. Direct
4628/// port of zsh/Src/lex.c:2043 `zshlex_raw_back`. Called when
4629/// the lexer ungets a char that was just captured raw — the raw
4630/// buffer must mirror the live input so this undoes the last add.
4631pub fn zshlex_raw_back() {
4632 // lex.c:2045-2046 — guard.
4633 if LEX_LEX_ADD_RAW.get() == 0 {
4634 return;
4635 }
4636 // lex.c:2047-2048 — `lexbuf_raw.ptr--; lexbuf_raw.len--;`
4637 LEX_LEXBUF_RAW.with_borrow_mut(|b| b.pop());
4638}
4639
4640/// Mark the current raw-buffer offset (for restore later). Direct
4641/// port of zsh/Src/lex.c:2053 `zshlex_raw_mark`. Returns
4642/// `len + offset` so callers can restore via `back_to_mark`.
4643pub fn zshlex_raw_mark(offset: i64) -> i64 {
4644 // lex.c:2055-2056 — guard.
4645 if LEX_LEX_ADD_RAW.get() == 0 {
4646 return 0;
4647 }
4648 // lex.c:2057 — `return lexbuf_raw.len + offset;`
4649 (LEX_LEXBUF_RAW.with_borrow(|b| b.buf_len()) as i64) + offset
4650}
4651
4652/// Restore raw-buffer offset to a previously-saved mark. Direct
4653/// port of zsh/Src/lex.c:2062 `zshlex_raw_back_to_mark`.
4654/// Truncates the raw buffer to `mark` bytes — undoes any captures
4655/// since the mark was taken (used when a speculative parse fails
4656/// and the lexer rolls back).
4657pub fn zshlex_raw_back_to_mark(mark: i64) {
4658 // lex.c:2064-2065 — guard.
4659 if LEX_LEX_ADD_RAW.get() == 0 {
4660 return;
4661 }
4662 // lex.c:2066-2067 — `lexbuf_raw.ptr = tokstr_raw + mark;
4663 // lexbuf_raw.len = mark;` — String::truncate handles both.
4664 let m = mark.max(0) as usize;
4665 LEX_LEXBUF_RAW.with_borrow_mut(|b| {
4666 if let Some(p) = b.ptr.as_mut() {
4667 p.truncate(m);
4668 }
4669 b.len = m as i32;
4670 });
4671}
4672
4673/// Port of `skipcomm()` from `Src/lex.c:2082`, the default build (the
4674/// `#else` half, c:2155-2292; the `ZSH_OLD_SKIPCOMM` paren counter is not
4675/// compiled into zsh). C's comment (c:2072-2077): "we'll parse the input
4676/// until we find an unmatched closing parenthesis. However, we'll throw
4677/// away the result of the parsing and just keep the string we've built up
4678/// on the way."
4679///
4680/// The body of `$(…)`, `<(…)`, `>(…)` or `=(…)` is parsed with
4681/// `parse_event(OUTPAR)` inside a saved lexer/parser context, while every
4682/// character read is also recorded in the raw buffer (`lex_add_raw`,
4683/// c:2215-2217). That raw text becomes the word. A body that does not parse
4684/// up to its `)` sets `lexstop` (c:2236-2246), which the caller turns into
4685/// LEXERR: `echo $(|||) bar` is a parse error, and `${(z)}` keeps
4686/// `$(|||) bar` as one word.
4687///
4688/// Returns `Err` when `lexstop` is set on the way out (C returns `lexstop`).
4689fn skipcomm() -> Result<(), ()> {
4690 // c:2158 — `int save_infor = infor;`
4691 let save_infor = LEX_INFOR.get();
4692
4693 LEX_INFOR.set(0); // c:2161
4694 cmdpush(CS_CMDSUBST as u8); // c:2162
4695 SETPARBEGIN!(); // c:2163
4696 add(Inpar); // c:2164
4697
4698 let new_lex_add_raw = LEX_LEX_ADD_RAW.get() + 1; // c:2166
4699 let new_tokstr: Option<String>;
4700 let (new_ptr, new_siz, new_len): (Option<String>, i32, i32);
4701 let outer_add_raw = LEX_LEX_ADD_RAW.get();
4702 if outer_add_raw == 0 {
4703 // c:2183-2184 — `new_tokstr = tokstr; new_lexbuf = lexbuf;`: the raw
4704 // record starts from the word built so far (`…$(`).
4705 new_tokstr = tokstr();
4706 (new_ptr, new_siz, new_len) = LEX_LEXBUF.with_borrow(|b| (b.ptr.clone(), b.siz, b.len));
4707 // c:2196-2197 — inside an alias expansion keep the whole remaining
4708 // text for the command in parentheses.
4709 if crate::ported::input::inbufflags.with(|f| f.get()) & INP_ALIAS != 0 {
4710 crate::ported::input::inbufflags
4711 .with(|f| f.set(f.get() | crate::ported::zsh_h::INP_RAW_KEEP));
4712 }
4713 crate::ported::context::zcontext_save_partial(ZCONTEXT_LEX | ZCONTEXT_PARSE); // c:2198
4714 hist_in_word(1); // c:2199
4715 } else {
4716 // c:2210-2211 — nested substitution: keep extending the raw record
4717 // the enclosing skipcomm started; this body's own string is not
4718 // needed until the top level recovers the lot.
4719 new_tokstr = LEX_TOKSTR_RAW.with_borrow_mut(|t| t.take());
4720 (new_ptr, new_siz, new_len) =
4721 LEX_LEXBUF_RAW.with_borrow_mut(|b| (b.ptr.take(), b.siz, b.len));
4722 crate::ported::context::zcontext_save_partial(ZCONTEXT_LEX | ZCONTEXT_PARSE); // c:2213
4723 }
4724 // c:2215-2217
4725 LEX_TOKSTR_RAW.with_borrow_mut(|t| *t = new_tokstr);
4726 LEX_LEXBUF_RAW.with_borrow_mut(|b| {
4727 b.ptr = new_ptr;
4728 b.siz = new_siz;
4729 b.len = new_len;
4730 });
4731 LEX_LEX_ADD_RAW.set(new_lex_add_raw);
4732 // c:2218-2234 — no ZLE specials down here, and no LEXFLAGS_NEWLINE:
4733 // parse_event needs embedded newlines to be real separators when it
4734 // looks for the OUTPAR token.
4735 LEX_LEXFLAGS.set(LEX_LEXFLAGS.get() & !(LEXFLAGS_ZLE | LEXFLAGS_NEWLINE));
4736 LEX_DBPARENS.set(false); // c:2234 — restored by zcontext_restore_partial
4737
4738 // c:2236-2246
4739 if crate::ported::parse::parse_event(OUTPAR_TOK).is_none() || tok() != OUTPAR_TOK {
4740 if crate::ported::input::strin.with(|s| s.get()) != 0 {
4741 // c:2238-2243 — "Get the rest of the string raw since we don't
4742 // know where this token ends."
4743 //
4744 // !!! WARNING: `hgetc`, NOT `ingetc` !!! C's `ingetc` feeds the
4745 // raw record itself (c:Src/input.c:360-361); in this port that
4746 // call lives in `hgetc` (see `zshlex_raw_add` there), and
4747 // `hgetc` reports end of input as `None` instead of setting
4748 // `lexstop`.
4749 while !LEX_LEXSTOP.get() {
4750 if hgetc().is_none() {
4751 LEX_LEXSTOP.set(true);
4752 }
4753 }
4754 } else {
4755 LEX_LEXSTOP.set(true); // c:2245
4756 }
4757 }
4758 // c:2247 — "Outpar lexical token gets added in caller if present".
4759
4760 // c:2253-2260 — keep the full raw input as the token string, and
4761 // propagate lexstop: "if we couldn't parse the command substitution we
4762 // can't continue."
4763 let final_tokstr = LEX_TOKSTR_RAW.with_borrow_mut(|t| t.take());
4764 let (mut final_ptr, final_siz, mut final_len) =
4765 LEX_LEXBUF_RAW.with_borrow_mut(|b| (b.ptr.take(), b.siz, b.len));
4766 let new_lexstop = LEX_LEXSTOP.get();
4767
4768 crate::ported::context::zcontext_restore_partial(ZCONTEXT_LEX | ZCONTEXT_PARSE); // c:2262
4769
4770 if LEX_LEX_ADD_RAW.get() != 0 {
4771 // c:2264-2269 — "Keep going, so retain the raw variables."
4772 LEX_TOKSTR_RAW.with_borrow_mut(|t| *t = final_tokstr);
4773 LEX_LEXBUF_RAW.with_borrow_mut(|b| {
4774 b.ptr = final_ptr;
4775 b.siz = final_siz;
4776 b.len = final_len;
4777 });
4778 } else {
4779 if !new_lexstop {
4780 // c:2271-2275 — "Ignore the ')' added on input".
4781 if let Some(s) = final_ptr.as_mut() {
4782 s.pop();
4783 final_len -= 1;
4784 }
4785 }
4786 // c:2277-2283 — "Convince the rest of lex.c we were examining a
4787 // string all along."
4788 set_tokstr(final_ptr.clone());
4789 LEX_LEXBUF.with_borrow_mut(|b| {
4790 b.ptr = final_ptr;
4791 b.siz = final_siz;
4792 b.len = final_len;
4793 });
4794 LEX_LEXSTOP.set(new_lexstop);
4795 hist_in_word(0); // c:2284
4796 }
4797
4798 // c:2287-2288
4799 if !LEX_LEXSTOP.get() {
4800 SETPAREND!();
4801 }
4802 cmdpop(); // c:2289
4803 LEX_INFOR.set(save_infor); // c:2290
4804
4805 // c:2292 — `return lexstop;`
4806 if LEX_LEXSTOP.get() {
4807 Err(())
4808 } else {
4809 Ok(())
4810 }
4811}
4812
4813/// Port of `static const char ztokens[]` from `Src/lex.c:80`.
4814pub const ztokens: &str = "#$^*(())$=|{}[]`<>>?~`,-!'\"\\\\";
4815
4816// `enum lextok` — port of `Src/zsh.h:304-371`. The full constant set
4817// (`NULLTOK`, `SEPER`, …, `TYPESET`) and the `lextok` type alias live
4818// in `super::zsh_h:198-262`. Re-export here so external callers can
4819// keep saying `lex::lextok` / `tokens::lextok` without reaching into
4820// `zsh_h::` directly. `IS_REDIROP()` (port of `Src/zsh.h:408`
4821// `#define IS_REDIROP`) lives in `zsh_h:318`.
4822
4823/// `static unsigned char lexact1[256]` from `Src/lex.c:406`. Per-byte
4824/// action table for the first-tier dispatch in `gettok`. Init'd by
4825/// `initlextabs()`.
4826pub static LEXACT1: std::sync::OnceLock<std::sync::Mutex<[u8; 256]>> = std::sync::OnceLock::new();
4827/// `static unsigned char lexact2[256]` from `Src/lex.c:406`. Per-byte
4828/// action table for the second-tier dispatch in `gettokstr`.
4829pub static LEXACT2: std::sync::OnceLock<std::sync::Mutex<[u8; 256]>> = std::sync::OnceLock::new();
4830/// `static unsigned char lextok2[256]` from `Src/lex.c:406`. Per-byte
4831/// token-character map: maps `*` → `Star`, `?` → `Quest`, etc.
4832pub static LEXTOK2: std::sync::OnceLock<std::sync::Mutex<[u8; 256]>> = std::sync::OnceLock::new();
4833
4834/// Sentinel: true once `initlextabs()` has populated the tables.
4835static LEX_TABS_INITED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
4836
4837#[inline]
4838fn ensure_tabs_inited() {
4839 if !LEX_TABS_INITED.load(std::sync::atomic::Ordering::Acquire) {
4840 initlextabs();
4841 LEX_TABS_INITED.store(true, std::sync::atomic::Ordering::Release);
4842 }
4843}
4844
4845/// Table accessor for `lexact1[c]`. Mirrors C's `lexact1[STOUC(c)]`
4846/// macro at `Src/lex.c:725`.
4847#[inline]
4848pub fn lexact1_get(c: char) -> u8 {
4849 let idx = c as u32;
4850 if idx >= 256 {
4851 return LX1_OTHER;
4852 }
4853 ensure_tabs_inited();
4854 let table = LEXACT1.get().unwrap();
4855 table.lock().unwrap()[idx as usize]
4856}
4857
4858/// Table accessor for `lexact2[c]`. Mirrors C's `lexact2[STOUC(c)]`
4859/// macro at `Src/lex.c:919` (the dispatch inside `gettokstr`).
4860#[inline]
4861pub fn lexact2_get(c: char) -> u8 {
4862 let idx = c as u32;
4863 if idx >= 256 {
4864 return LX2_OTHER;
4865 }
4866 ensure_tabs_inited();
4867 let table = LEXACT2.get().unwrap();
4868 table.lock().unwrap()[idx as usize]
4869}
4870
4871/// Table accessor for `lextok2[c]`. Mirrors C's `lextok2[STOUC(c)]`
4872/// at `Src/lex.c:919+` — used to translate a glob metacharacter to
4873/// its byte-token form (`*` → `Star`, etc.).
4874#[inline]
4875pub fn lextok2_get(c: char) -> u8 {
4876 let idx = c as u32;
4877 if idx >= 256 {
4878 return c as u8;
4879 }
4880 ensure_tabs_inited();
4881 let table = LEXTOK2.get().unwrap();
4882 table.lock().unwrap()[idx as usize]
4883}
4884
4885/// The Zsh Lexer.
4886///
4887// All lexer state lives in the file-scope `LEX_*` thread_local statics
4888// above, each one matching a `static` in `Src/lex.c`. There's no
4889// holder struct — callers use the free ported directly:
4890//
4891// lex_init(input);
4892// zshlex();
4893// let tok = tok();
4894//
4895// Accessor ported named after the former field identifiers (`tok()`,
4896// `tokstr()`, `set_tok(v)`, etc.) provide read/write into LEX_*.
4897
4898// ─── Accessor ported for the LEX_* thread_locals (Src/lex.c file-statics) ───
4899/// `toklineno` — see implementation.
4900pub fn toklineno() -> u64 {
4901 LEX_TOKLINENO.get()
4902}
4903/// `set_toklineno` — see implementation.
4904pub fn set_toklineno(v: u64) {
4905 LEX_TOKLINENO.set(v);
4906}
4907/// `tokfd` — see implementation.
4908pub fn tokfd() -> i32 {
4909 LEX_TOKFD.get()
4910}
4911/// `set_tokfd` — see implementation.
4912pub fn set_tokfd(v: i32) {
4913 LEX_TOKFD.set(v);
4914}
4915/// `isnewlin` — see implementation.
4916pub fn isnewlin() -> i32 {
4917 LEX_ISNEWLIN.get()
4918}
4919/// `set_isnewlin` — see implementation.
4920pub fn set_isnewlin(v: i32) {
4921 LEX_ISNEWLIN.set(v);
4922}
4923/// `inrepeat` — see implementation.
4924pub fn inrepeat() -> i32 {
4925 LEX_INREPEAT.get()
4926}
4927/// `set_inrepeat` — see implementation.
4928pub fn set_inrepeat(v: i32) {
4929 LEX_INREPEAT.set(v);
4930}
4931/// `infor` — see implementation.
4932pub fn infor() -> i32 {
4933 LEX_INFOR.get()
4934}
4935/// `set_infor` — see implementation.
4936pub fn set_infor(v: i32) {
4937 LEX_INFOR.set(v);
4938}
4939/// `inredir` — see implementation.
4940pub fn inredir() -> bool {
4941 LEX_INREDIR.get()
4942}
4943/// `set_inredir` — see implementation.
4944pub fn set_inredir(v: bool) {
4945 LEX_INREDIR.set(v);
4946}
4947/// `intypeset` — see implementation.
4948pub fn intypeset() -> bool {
4949 LEX_INTYPESET.get()
4950}
4951/// `set_intypeset` — see implementation.
4952pub fn set_intypeset(v: bool) {
4953 LEX_INTYPESET.set(v);
4954}
4955/// `lineno` — see implementation.
4956pub fn lineno() -> u64 {
4957 LEX_LINENO.get()
4958}
4959/// `set_lineno` — see implementation.
4960pub fn set_lineno(v: u64) {
4961 LEX_LINENO.set(v);
4962}
4963/// `incmdpos` — see implementation.
4964pub fn incmdpos() -> bool {
4965 LEX_INCMDPOS.get()
4966}
4967/// `set_incmdpos` — see implementation.
4968pub fn set_incmdpos(v: bool) {
4969 LEX_INCMDPOS.set(v);
4970}
4971/// Port of `int nocorrect` from `Src/lex.c:144`. Getter/setter for
4972/// the spelling-correction suppression flag. `par_redir` saves and
4973/// restores this around the zshlex that consumes the redir target.
4974pub fn nocorrect() -> i32 {
4975 LEX_NOCORRECT.get()
4976}
4977/// `set_nocorrect` — see implementation.
4978pub fn set_nocorrect(v: i32) {
4979 LEX_NOCORRECT.set(v);
4980}
4981/// Port of `int noaliases` from `Src/lex.c:135`. Suppresses alias
4982/// expansion. par_case saves and restores this around the case-word
4983/// + `in` lex so the literal `in` keyword isn't alias-expanded.
4984
4985pub fn noaliases() -> bool {
4986 LEX_NOALIASES.get()
4987}
4988/// `set_noaliases` — see implementation.
4989pub fn set_noaliases(v: bool) {
4990 LEX_NOALIASES.set(v);
4991}
4992/// `incond` — see implementation.
4993pub fn incond() -> i32 {
4994 LEX_INCOND.get()
4995}
4996/// `set_incond` — see implementation.
4997pub fn set_incond(v: i32) {
4998 LEX_INCOND.set(v);
4999}
5000
5001// `hashchar` / `bangchar` / `hatchar` canonical port of
5002// `unsigned char hashchar, bangchar, hatchar;` (`Src/params.c:132`)
5003// lives at `crate::ported::hist::{hashchar, bangchar, hatchar}` as
5004// `AtomicI32` so `histcharssetfn` (`Src/params.c:5095-5097`) can
5005// update them when `$HISTCHARS` changes. Local stale-const copies
5006// removed — readers now go through the atomic load directly.
5007/// `incasepat` — see implementation.
5008pub fn incasepat() -> i32 {
5009 LEX_INCASEPAT.get()
5010}
5011/// `set_incasepat` — see implementation.
5012pub fn set_incasepat(v: i32) {
5013 LEX_INCASEPAT.set(v);
5014}
5015/// Port of `mod_export char *tokstrings[WHILE + 1]` from
5016/// `Src/lex.c:171-205`. Canonical text for each punctuation token —
5017/// used by `zshlex` (lex.c:1965 `zshlextext = tokstrings[tok]`) when
5018/// no tokstr was captured, and by `yyerror` (parse.c:2738) to format
5019/// "parse error near `X'" tails.
5020///
5021/// Indexed by lextok value (C's `tokstrings[tok]`). Entries the C
5022/// initializer doesn't set are `None`; the array bound is WHILE+1
5023/// matching C exactly.
5024#[allow(non_upper_case_globals)]
5025pub static tokstrings: [Option<&'static str>; (WHILE + 1) as usize] = {
5026 let mut t: [Option<&'static str>; (WHILE + 1) as usize] = [None; (WHILE + 1) as usize];
5027 t[SEPER as usize] = Some(";"); // c:173
5028 t[NEWLIN as usize] = Some("\\n"); // c:174
5029 t[SEMI as usize] = Some(";"); // c:175
5030 t[DSEMI as usize] = Some(";;"); // c:176
5031 t[AMPER as usize] = Some("&"); // c:177
5032 t[INPAR_TOK as usize] = Some("("); // c:178
5033 t[OUTPAR_TOK as usize] = Some(")"); // c:179
5034 t[DBAR as usize] = Some("||"); // c:180
5035 t[DAMPER as usize] = Some("&&"); // c:181
5036 t[OUTANG_TOK as usize] = Some(">"); // c:182
5037 t[OUTANGBANG as usize] = Some(">|"); // c:183
5038 t[DOUTANG as usize] = Some(">>"); // c:184
5039 t[DOUTANGBANG as usize] = Some(">>|"); // c:185
5040 t[INANG_TOK as usize] = Some("<"); // c:186
5041 t[INOUTANG as usize] = Some("<>"); // c:187
5042 t[DINANG as usize] = Some("<<"); // c:188
5043 t[DINANGDASH as usize] = Some("<<-"); // c:189
5044 t[INANGAMP as usize] = Some("<&"); // c:190
5045 t[OUTANGAMP as usize] = Some(">&"); // c:191
5046 t[AMPOUTANG as usize] = Some("&>"); // c:192
5047 t[OUTANGAMPBANG as usize] = Some("&>|"); // c:193
5048 t[DOUTANGAMP as usize] = Some(">>&"); // c:194
5049 t[DOUTANGAMPBANG as usize] = Some(">>&|"); // c:195
5050 t[TRINANG as usize] = Some("<<<"); // c:196
5051 t[BAR_TOK as usize] = Some("|"); // c:197
5052 t[BARAMP as usize] = Some("|&"); // c:198
5053 t[INOUTPAR as usize] = Some("()"); // c:199
5054 t[DINPAR as usize] = Some("(("); // c:200
5055 t[DOUTPAR as usize] = Some("))"); // c:201
5056 t[AMPERBANG as usize] = Some("&|"); // c:202
5057 t[SEMIAMP as usize] = Some(";&"); // c:203
5058 t[SEMIBAR as usize] = Some(";|"); // c:204
5059 t
5060};
5061
5062/// `char *tokstr` accessors — direct port of lex.c:170 file-static.
5063pub fn tokstr() -> Option<String> {
5064 LEX_TOKSTR.with_borrow(|t| t.clone())
5065}
5066/// `set_tokstr` — see implementation.
5067pub fn set_tokstr(v: Option<String>) {
5068 LEX_TOKSTR.with_borrow_mut(|t| *t = v);
5069}
5070/// `enum lextok tok` accessors — direct port of lex.c:180 file-static.
5071pub fn tok() -> lextok {
5072 LEX_TOK.get()
5073}
5074/// `set_tok` — see implementation.
5075pub fn set_tok(v: lextok) {
5076 LEX_TOK.set(v);
5077}
5078/// `pos` — see implementation.
5079pub fn pos() -> usize {
5080 LEX_POS.get()
5081}
5082/// `set_pos` — see implementation.
5083pub fn set_pos(v: usize) {
5084 LEX_POS.set(v);
5085}
5086/// Slice the input source from `start..end` — used by parse.rs to
5087/// capture function body source text. Returns None if out-of-range.
5088pub fn input_slice(start: usize, end: usize) -> Option<String> {
5089 LEX_INPUT.with_borrow(|s| s.get(start..end).map(String::from))
5090}
5091
5092/// Create a new lexer for the given input
5093pub fn lex_init(input: &str) {
5094 // Ensure `typtab[]` is initialised so `iblank()` / `inblank()` /
5095 // `idigit()` / etc. (called throughout the lexer) work. C zsh
5096 // calls `inittyptab()` once at shell startup in `init_main()`
5097 // (init.c:1287); zshrs lex tests bypass that path so we kick
5098 // it here too. `inittyptab` is idempotent (sets `ZTF_INIT`).
5099 crate::ported::utils::inittyptab();
5100 // Reset migrated thread-locals so a fresh lexer instance
5101 // starts from a clean slate (same as the C source's
5102 // file-static initializers in lex.c).
5103 LEX_UNGET_BUF.with_borrow_mut(|b| b.clear());
5104 LEX_UNGET_HPTR.with_borrow_mut(|b| b.clear());
5105 LEX_UNGET_FRAME.with_borrow_mut(|b| b.clear());
5106 LEX_READ_FRAME.with_borrow_mut(|b| b.clear());
5107 LEX_UNGET_RAW.with_borrow_mut(|b| b.clear());
5108 crate::funcdef_capture::src_capture_reset();
5109 LEX_LEXBUF.with_borrow_mut(|b| *b = lexbufstate::new());
5110 LEX_LEXBUF_RAW.with_borrow_mut(|b| *b = lexbufstate::new());
5111 // P7-batch-3 fields: reset to their C-source initial values.
5112 LEX_LEXSTOP.set(false);
5113 LEX_INCONDPAT.set(false);
5114 LEX_OLDPOS.set(true);
5115 LEX_DBPARENS.set(false);
5116 // NOT reset here. C's `lexinit` (c:441-445) resets nocorrect/dbparens/lexstop
5117 // but deliberately leaves `noaliases` alone: it is a plain global
5118 // (Src/lex.c:135), saved and restored by its callers. `loadautofn`
5119 // (Src/exec.c:5684-5704) sets it from the function's PM_UNALIASED bit and
5120 // then parses the autoloaded file — so resetting it inside lexinit would
5121 // clobber the flag before the parse it exists to govern, which is why
5122 // `autoload -U` failed to suppress alias expansion in the loaded body.
5123 LEX_NOCORRECT.set(0);
5124 LEX_NOCOMMENTS.set(false);
5125 LEX_LEXFLAGS.set(0);
5126 LEX_ISFIRSTLN.set(true);
5127 LEX_LEX_ADD_RAW.set(0);
5128 // P7-batch-4 resets.
5129 LEX_TOKFD.set(-1);
5130 LEX_TOKLINENO.set(1);
5131 LEX_INREDIR.set(false);
5132 LEX_INFOR.set(0);
5133 LEX_INREPEAT.set(0);
5134 LEX_INTYPESET.set(false);
5135 LEX_ISNEWLIN.set(0);
5136 // P7-batch-5 resets.
5137 LEX_LINENO.set(1);
5138 LEX_INCMDPOS.set(true);
5139 LEX_INCOND.set(0);
5140 LEX_INCASEPAT.set(0);
5141 // P7-batch-6 reset.
5142 LEX_HEREDOCS.with_borrow_mut(|v| v.clear());
5143 // P7-batch-7 resets.
5144 LEX_TOKSTR.with_borrow_mut(|t| *t = None);
5145 LEX_TOK.set(ENDINPUT);
5146 // P7-batch-8: input + pos.
5147 LEX_INPUT.with_borrow_mut(|s| {
5148 s.clear();
5149 s.push_str(input);
5150 });
5151 LEX_POS.set(0);
5152 // A freshly installed window is a STRING unit until a caller says
5153 // otherwise — that is what `inpush` gives C for every nested parse
5154 // (`parse_string`, c:Src/exec.c:283). The sourced-file driver re-arms
5155 // this right after `parse_init`; see `LEX_FILE_WINDOW_STRIN`.
5156 LEX_FILE_WINDOW_STRIN.set(0);
5157}
5158
5159// Direct port of the anonymous enum at `Src/lex.c:483-487`:
5160// enum { CMD_OR_MATH_CMD, CMD_OR_MATH_MATH, CMD_OR_MATH_ERR };
5161// `cmd_or_math()` and `cmd_or_math_sub()` return one of these as `int`.
5162// Following the same flat-const pattern zshrs uses for lextok
5163// (zsh_h.rs:198-251) so call sites read the C identifier verbatim.
5164/// `CMD_OR_MATH_CMD` constant.
5165pub const CMD_OR_MATH_CMD: i32 = 0;
5166/// `CMD_OR_MATH_MATH` constant.
5167pub const CMD_OR_MATH_MATH: i32 = 1;
5168/// `CMD_OR_MATH_ERR` constant.
5169pub const CMD_OR_MATH_ERR: i32 = 2;
5170
5171/// Check recursion depth; returns true if exceeded
5172#[inline]
5173/// Get next character from input
5174pub(crate) fn hgetc() -> Option<char> {
5175 // Re-read from unget_buf: increment lineno on `\n` HERE
5176 // too. hungetc() decremented lineno when the char was put
5177 // back; without a matching increment on the way out, every
5178 // `\n` that's ungetted-then-reread leaves lineno
5179 // permanently one short. Symptom: $LINENO stuck at 1 in
5180 // every script statement because the parser ungets the
5181 // separating newline once between statements.
5182 if let Some(c) = LEX_UNGET_BUF.with_borrow_mut(|b| b.pop_front()) {
5183 if c == '\n' {
5184 LEX_LINENO.set(LEX_LINENO.get() + 1);
5185 }
5186 // c:Src/hist.c:459 — `hwaddc(c);` at the tail of `ihgetc`. Every
5187 // character the lexer reads goes into the history line being
5188 // built. A re-read off the unget queue is a read like any other,
5189 // so it re-advances `hptr` over the byte `hungetc` rewound above.
5190 // `ihwaddc` writes AT the cursor (c:363 `*hptr++ = c`) rather than
5191 // appending, so the byte is rewritten in place and the line's
5192 // contents are unchanged — only the cursor moves.
5193 // c:Src/hist.c:459 — `hwaddc(c);` at the tail of `ihgetc`: a
5194 // re-read is a read like any other, so it re-advances the
5195 // history-line cursor over the byte `hungetc` rewound. Advance
5196 // `hptr` directly instead of calling `ihwaddc`: the bytes are
5197 // already in `chline` from the FIRST read (`ihwaddc` writes at the
5198 // cursor, c:363 `*hptr++ = c`), so re-writing them is at best a
5199 // no-op and at worst overwrites — and re-testing `ihwaddc`'s
5200 // c:360 guard here would re-evaluate `inbufflags` at a different
5201 // moment than `hungetc` did, which is the asymmetry
5202 // `LEX_UNGET_HPTR` exists to remove.
5203 if LEX_UNGET_HPTR
5204 .with_borrow_mut(|b| b.pop_front())
5205 .unwrap_or(false)
5206 {
5207 let pos = crate::ported::hist::hptr.load(Ordering::SeqCst);
5208 crate::ported::hist::hptr.store(pos + c.len_utf8(), Ordering::SeqCst);
5209 // c:459
5210 }
5211 // c:input.c:360-361 — every char returned by ingetc feeds
5212 // the raw buffer when lex_add_raw is on. Re-reads from the
5213 // unget queue count the same as fresh reads; the matching
5214 // `zshlex_raw_back()` call in hungetc removed the prior
5215 // record, so this restores it — unless `LEX_UNGET_RAW` says not to.
5216 if !LEX_UNGET_RAW.with_borrow_mut(|b| b.pop_front()).unwrap_or(false) {
5217 zshlex_raw_add(c);
5218 }
5219 // !!! RUST-ONLY (no C counterpart) !!! — same argument as the
5220 // `LEX_UNGET_HPTR` block above, for the function-body echo buffer:
5221 // put back exactly what `hungetc` took, decided at unget time.
5222 if let Some(flags) = crate::funcdef_capture::LEX_UNGET_SRCCAP
5223 .with_borrow_mut(|b| b.pop_front())
5224 .flatten()
5225 {
5226 crate::funcdef_capture::src_capture_add(c, flags);
5227 }
5228 // c:input.c:327 — re-reading the un-gotten char consumes it like
5229 // any other read (C's ingetc `inbufct--`). Mirror it so the
5230 // hungetc(+1)/reread(-1) pair stays balanced; scoped to
5231 // LEXFLAGS_ZLE|LEXFLAGS_ACTIVE for the same reason as the hungetc restore.
5232 if LEX_LEXFLAGS.get() & (LEXFLAGS_ZLE | LEXFLAGS_ACTIVE) != 0 {
5233 crate::ported::input::inbufct.with(|ct| ct.set(ct.get() - 1));
5234 }
5235 // !!! RUST-ONLY !!! — the re-read keeps the frame tag it was queued
5236 // with, so a second pushback of it tags it the same (see
5237 // `LEX_UNGET_FRAME`).
5238 let tag = LEX_UNGET_FRAME.with_borrow_mut(|f| f.pop_front()).unwrap_or(0);
5239 LEX_READ_FRAME.with_borrow_mut(|r| {
5240 r.push_back((c, tag));
5241 if r.len() > 16 {
5242 r.pop_front();
5243 }
5244 });
5245 return Some(c);
5246 }
5247
5248 // Two-input-system bridge (c:Src/input.c): zshrs's lexer has its
5249 // own LEX_INPUT/LEX_POS char-window while input.rs maintains
5250 // ingetc's inbuf stack (used by inpush for alias / here-string /
5251 // eval content). Prefer the inbuf stack WHEN it has any content
5252 // (inbufct > 0). If inbufct == 0 but there's a pending instack
5253 // frame to pop (INP_CONT'd lower frame e.g. exalias's leading
5254 // " " separator after the body drained), ingetc's c:296
5255 // `if (inbufflags & INP_CONT) { inpoptop(); continue; }` arm
5256 // handles the pop. Then unwind to LEX_INPUT for the remainder.
5257 let pos = LEX_POS.get();
5258 let inbufct = crate::ported::input::inbufct.with(|c| c.get());
5259 // Also probe instack via the flags-on-current-frame: if the
5260 // CURRENT frame has INP_CONT, ingetc will pop and continue. The
5261 // bridge needs to call ingetc to trigger that pop even when
5262 // inbufct == 0.
5263 let flags = crate::ported::input::inbufflags.with(|f| f.get());
5264 let inp_cont_pending = (flags & crate::ported::zsh_h::INP_CONT) != 0;
5265 let try_inbuf = inbufct > 0 || inp_cont_pending;
5266
5267 // c:Src/lex.c — the lexer reads characters via `hgetc`, which C points
5268 // at `ihgetc` (hist.c:418) when history is on. For the inbuf/ingetc
5269 // stack source (interactive input) we DELEGATE to the ported `ihgetc`
5270 // (ingetc → histsubchar → hwaddc → addtoline) so `!`-expansion and the
5271 // history-line build are CALLED, not re-inlined here — but only when
5272 // history is active (stophist==0 && !INP_ALIAS). The Rust-only
5273 // LEX_INPUT window (`-c` / cmd-subst / eval, where history is off) is
5274 // read directly. `ihgetc` returns C's `int`; it signals EOF / a bad
5275 // `!`-reference by setting `lexstop`, which we map back to the Option
5276 // API's `None`.
5277 // c:Src/hist.c:1134-1155 — C does NOT decide this per character. `hbegin`
5278 // installs the function pointers ONCE per input line: `hgetc = ihgetc`
5279 // whenever `stophist != 2`, and the INP_ALIAS/INP_HIST distinctions are
5280 // made INSIDE `ihgetc` (c:429 skips `histsubchar` under INP_ALIAS) and
5281 // inside `ihwaddc` (c:365 rejects text that is INP_ALIAS but not
5282 // INP_HIST). Excluding INP_ALIAS here instead skipped `ihgetc` wholesale
5283 // — and `inpush` sets INP_ALIAS on a HISTORY push too (c:Src/input.c:106
5284 // `flags |= INP_CONT|INP_ALIAS;` when `flags & (INP_ALIAS|INP_HIST)`), so
5285 // every character of a `!`-expansion after the first bypassed `hwaddc`
5286 // and the stored history line was truncated to one character:
5287 // `print aa bb cc` then `print !!` recorded `print p`, and `!-3:1-$` on
5288 // such an entry then yielded `m` instead of `mystery sequence`.
5289 // INP_HIST frames therefore stay on the `ihgetc` path; a pure INP_ALIAS
5290 // frame keeps the existing direct `ingetc` shortcut, whose observable
5291 // behaviour matches C's ihgetc-with-INP_ALIAS (histsubchar skipped,
5292 // ihwaddc a no-op).
5293 // c:Src/hist.c:1134/1149 — `if (stophist == 2) { … hgetc = ingetc; }`
5294 // else `hgetc = ihgetc`. The bypass is `== 2` ONLY. The lexer's own
5295 // STOPHIST (`stophist += 4`, c:Src/Zle/lex.c for the single-quote scan)
5296 // and NO_BANGHIST (`stophist = 4`, c:Src/hist.c:1157) must NOT divert off
5297 // `ihgetc`: `stophist` gates only the bang substitution
5298 // (c:Src/hist.c:458 `qbang = c == bangchar && (stophist < 2)`), while
5299 // `hwaddc(c)` at c:459 runs UNCONDITIONALLY, so the history line keeps
5300 // building through quoted text.
5301 //
5302 // Testing `== 0` dropped every character of a single-quoted string from
5303 // `chline`, so on a `'…` continuation `$PREBUFFER` was `echo '` where zsh
5304 // has `echo 'first line\n`. Under `setopt nobanghist` (stophist = 4 for
5305 // the whole line) `chline` would never build at all.
5306 let hist_active = crate::ported::hist::stophist.load(Ordering::SeqCst) != 2
5307 && ((flags & crate::ported::zsh_h::INP_ALIAS) == 0
5308 || (flags & crate::ported::zsh_h::INP_HIST) != 0);
5309 let read_stack = || -> Option<char> {
5310 if hist_active {
5311 let nc = crate::ported::hist::ihgetc(); // c:418
5312 if crate::ported::input::lexstop.with(|s| s.get()) {
5313 None
5314 } else {
5315 char::from_u32(nc as u32)
5316 }
5317 } else {
5318 crate::ported::input::ingetc()
5319 }
5320 };
5321
5322 let from_inbuf = if try_inbuf { read_stack() } else { None };
5323 // Did this character come off the ported `inbuf` stack (input.rs) or
5324 // out of the Rust-only LEX_INPUT window? The C line-counting gate at
5325 // c:Src/input.c:330 only has meaning for the former — see the
5326 // `LEX_LINENO` bump below.
5327 let mut via_inbuf = from_inbuf.is_some();
5328 let c = if let Some(c) = from_inbuf {
5329 c
5330 } else if let Some(c) = LEX_INPUT.with_borrow(|s| s.get(pos..).and_then(|t| t.chars().next())) {
5331 LEX_POS.set(pos + c.len_utf8());
5332 c
5333 } else {
5334 // inbuf + LEX_INPUT both empty. If a prior read already set lexstop
5335 // (real EOF, or a bad `!`-expansion), stop now WITHOUT re-reading —
5336 // re-entering ihgetc here would `hwaddc` a stray char into chline.
5337 // (An inbuf drain while `strin` is set — e.g. an alias body ending
5338 // mid-`eval` — ALSO sets lexstop, but never reaches here: the
5339 // LEX_INPUT branch above still holds the post-alias text.) No
5340 // lexstop → genuine refill: read the next line via the same
5341 // history-aware path (triggers inputline). `?` is None at EOF.
5342 if crate::ported::input::lexstop.with(|s| s.get()) {
5343 return None;
5344 }
5345 via_inbuf = true;
5346 read_stack()?
5347 };
5348
5349 // c:Src/input.c:330 — `if (((inbufflags & INP_LINENO) || !strin) &&
5350 // lastc == '\n') lineno++;` C has ONE `lineno`, maintained inside
5351 // `ingetc` under that gate, so a nested `inpush` that carries neither
5352 // `INP_LINENO` nor a clear `strin` contributes NO lines to the count.
5353 //
5354 // zshrs splits the lexer input in two: the ported `inbuf` stack
5355 // (input.rs, fed by `inpush` — aliases, here-strings, `parsestrnoerr`)
5356 // and the Rust-only `LEX_INPUT` window (`-c`, script text, and the
5357 // `parse_isolated` nested parses for `eval` / `$(...)`). The window has
5358 // no `inbufflags` frame of its own; its C analogue is always an
5359 // `inpush(..., INP_LINENO, ...)` (c:Src/exec.c:294 `parse_string`), so
5360 // those characters always count. Only stack-sourced characters get the
5361 // real C gate.
5362 //
5363 // The gate is load-bearing for here-documents: `gethere`
5364 // (c:Src/exec.c:4625, ported at exec.rs:356) reads the body with
5365 // `hgetc` — counting its newlines once, correctly — and then re-lexes
5366 // the SAME body through `parsestr` (c:4697) for an unquoted
5367 // terminator. `parsestrnoerr` pushes that body with flags `0` under
5368 // `strinbeg(0)` (c:Src/lex.c:1719-1720), so C counts nothing the
5369 // second time. Without this gate zshrs counted every unquoted
5370 // here-document body line twice and `$LINENO` ran ahead by the body's
5371 // length for the rest of the file.
5372 let counts_lineno = !via_inbuf || {
5373 let f = crate::ported::input::inbufflags.with(|f| f.get());
5374 let strin_now = crate::ported::input::strin.with(|s| s.get());
5375 (f & crate::ported::zsh_h::INP_LINENO) != 0
5376 || strin_now == 0
5377 // A sourced FILE runs with `strin == 0` in C (`source` points
5378 // SHIN at it, c:Src/init.c:1584), so every newline counts —
5379 // including one that reaches the lexer through the inbuf stack
5380 // because an alias expansion was in flight over it. zshrs's
5381 // driver has to set `strin` to keep the drained window from
5382 // stealing the outer reader's input, which would otherwise
5383 // suppress that count and leave every line after the first
5384 // alias expansion reporting one line short. Only the driver's
5385 // OWN depth is exempt: a nested `strinbeg` (a here-document
5386 // body re-lexed by `parsestrnoerr`, a `$(…)` body) is a real
5387 // string push and stays suppressed. See `LEX_FILE_WINDOW_STRIN`.
5388 || strin_now == LEX_FILE_WINDOW_STRIN.get()
5389 };
5390 if c == '\n' && counts_lineno {
5391 LEX_LINENO.set(LEX_LINENO.get() + 1);
5392 }
5393
5394 // c:input.c:360-361 — `if (!lexstop) zshlex_raw_add(lastc);`
5395 // Every char read from input also feeds the raw buffer when
5396 // lex_add_raw is on (used by skipcomm to capture verbatim
5397 // `$(...)` body text into the parent token). (The history-line build —
5398 // C `hwaddc`/`addtoline` at ihgetc c:459-460 — is done INSIDE the
5399 // `ihgetc` call above, not duplicated here.)
5400 zshlex_raw_add(c);
5401
5402 // !!! RUST-ONLY (no C counterpart) !!! — echo the character into the
5403 // open function-body capture, if any (see `LEX_SRC_CAPTURE`). Reuses
5404 // the `counts_lineno` gate computed above for exactly the reason it
5405 // exists: `gethere` (c:Src/exec.c:4625) reads a here-document body
5406 // with `hgetc` and then re-lexes the SAME body through `parsestr`
5407 // (c:4697), which pushes it with flags `0` under `strinbeg(0)`
5408 // (c:Src/lex.c:1719-1720). Recording both passes would duplicate every
5409 // here-document line inside a function body, the same way it used to
5410 // double-count them in `$LINENO`.
5411 //
5412 // Characters an alias expansion pushed (`inpush(..., INP_ALIAS)`,
5413 // c:Src/lex.c:1928) are flagged CAP_ALIAS_TEXT: a function body keeps
5414 // them (zsh compiles the body after alias expansion), while the preexec
5415 // event source drops them because its re-parse expands the alias name
5416 // again (`funcdef_capture::event_text`). Same frame test `ihwaddc`
5417 // applies to the history line (c:Src/hist.c:360).
5418 let alias_only = via_inbuf && {
5419 let f = crate::ported::input::inbufflags.with(|f| f.get());
5420 (f & (crate::ported::zsh_h::INP_ALIAS | crate::ported::zsh_h::INP_HIST))
5421 == crate::ported::zsh_h::INP_ALIAS
5422 };
5423 // The `counts_lineno` gate exists for the here-document re-lex, which
5424 // `parsestrnoerr` pushes with flags `0`. An alias frame and an INP_CONT
5425 // continuation frame are never that — they carry text the parse is
5426 // consuming — and under `strin` (an `eval` string, an autoload body) the
5427 // gate refused both: the alias body went missing (`eval 'f(){ ll e }'`
5428 // listed `e`), and so did the word terminator `checkalias` re-routes into
5429 // an INP_CONT frame (`print aliasede`).
5430 let cont_frame = via_inbuf
5431 && (crate::ported::input::inbufflags.with(|f| f.get()) & crate::ported::zsh_h::INP_CONT) != 0;
5432 if counts_lineno || alias_only || cont_frame {
5433 // Inside a command substitution skipcomm keeps the RAW text as the
5434 // word (c:Src/lex.c:2253-2283), and `inpoptop` takes an alias
5435 // expansion back out of it (c:Src/input.c:751-752): zsh prints
5436 // `$(ll)`, not `$(ls -l)`. Both renderings drop such text.
5437 let flags = if !alias_only {
5438 0
5439 } else if LEX_LEX_ADD_RAW.get() != 0
5440 && crate::ported::input::inbufflags.with(|f| f.get()) & crate::ported::zsh_h::INP_RAW_KEEP == 0
5441 {
5442 crate::funcdef_capture::CAP_ALIAS_TEXT | crate::funcdef_capture::CAP_ALIAS_NAME
5443 } else {
5444 crate::funcdef_capture::CAP_ALIAS_TEXT
5445 };
5446 crate::funcdef_capture::src_capture_add(c, flags);
5447 }
5448
5449 // !!! RUST-ONLY !!! — remember which frame this character came from
5450 // (0 = the `LEX_INPUT` window), for `hungetc` to tag it with.
5451 let tag = if via_inbuf { crate::ported::input::INBUF_SERIAL.with(|s| s.get()) } else { 0 };
5452 LEX_READ_FRAME.with_borrow_mut(|r| {
5453 r.push_back((c, tag));
5454 if r.len() > 16 {
5455 r.pop_front();
5456 }
5457 });
5458
5459 Some(c)
5460}
5461
5462/// Put character back into input. Direct port of `inungetc(int c)`
5463/// from `Src/input.c:546-583`. Critical: C decrements lineno on
5464/// `\n` ungetc UNCONDITIONALLY (modulo input-flags). Rust's
5465/// previous `LEX_LINENO.get() > 1` guard caused off-by-one drift
5466/// when lineno was set to 0 (via par_funcdef's `set_lineno(0)`)
5467/// then a `\n` got read+ungetted: hungetc wouldn't decrement
5468/// (because LEX_LINENO==1 fails `> 1`), but the subsequent re-read
5469/// in hgetc DOES increment, leaving LEX_LINENO=2 instead of 1.
5470fn hungetc(c: char) {
5471 LEX_UNGET_BUF.with_borrow_mut(|b| b.push_front(c));
5472 // !!! RUST-ONLY !!! — tag the character with the frame it was read from
5473 // (see `LEX_UNGET_FRAME`); an untracked character gets 0, the
5474 // `LEX_INPUT` window, which no frame pop ever claims.
5475 let tag = LEX_READ_FRAME.with_borrow_mut(|r| match r.pop_back() {
5476 Some((rc, t)) if rc == c => t,
5477 _ => 0,
5478 });
5479 // A save/restore of `LEX_UNGET_BUF` alone (nested parses) can leave the
5480 // tags out of step with the queue; re-align with "untracked" tags then.
5481 let queued = LEX_UNGET_BUF.with_borrow(|b| b.len()) - 1;
5482 LEX_UNGET_FRAME.with_borrow_mut(|f| {
5483 if f.len() != queued {
5484 f.clear();
5485 f.resize(queued, 0);
5486 }
5487 f.push_front(tag);
5488 });
5489 // c:Src/hist.c:1009-1013 — `if ((inbufflags & (INP_ALIAS|INP_HIST)) !=
5490 // INP_ALIAS) { DPUTS(hptr <= chline, ...); hptr--; ... }`.
5491 // C's `hungetc` is a function pointer pointing at `ihungetc`
5492 // (hist.c:989) whenever history is active, and `ihungetc` rewinds the
5493 // history-line cursor so the un-gotten character stops counting as
5494 // part of the line being built. The matching re-add happens in
5495 // `hgetc`'s unget-queue pop below (C: `ihgetc` c:459 `hwaddc(c)`).
5496 //
5497 // zshrs's lexer keeps its own `LEX_UNGET_BUF` instead of routing every
5498 // pushback through `input::inungetc`, so this half of `ihungetc` was
5499 // simply missing: `hptr` stayed one PAST the delimiter that ended a
5500 // word. `exalias` (c:Src/lex.c:1957) calls `hwend()` in exactly that
5501 // window, so `ihwend` recorded the last word of every line as ending
5502 // one byte too far — past the trailing `\n` that `hend` then strips.
5503 // `getargs` (c:2481 `dupstrpfx(nam + pos1, pos2 - pos1)`) then indexed
5504 // past the stored line and returned nothing, which is why `!:$`,
5505 // `!:<last>` and any range touching the final word silently expanded
5506 // to nothing while every earlier word worked.
5507 //
5508 // Inlined rather than factored into a helper: only the chline-cursor
5509 // half of `ihungetc` applies here. The caller has already queued the
5510 // character, so there is no `inungetc` (c:1021); `LEX_UNGET_BUF`
5511 // characters never pass through the ZLE metafied line, so the
5512 // `expanding` bookkeeping at c:1004-1008 does not apply; and `qbang`
5513 // (c:1014-1015) is maintained by the real `ihungetc` on the inbuf path.
5514 // The rewind is by the character's UTF-8 length rather than C's bare
5515 // `hptr--` because `ihwaddc` advances `hptr` by `encode_utf8().len()`
5516 // (zshrs's `ingetc` yields whole `char`s where C's yields metafied
5517 // bytes), so the two have to agree.
5518 let did_rewind = {
5519 // c:1140 — pool threads run `hungetc = inungetc`, which never
5520 // touches chline.
5521 let inflags = crate::ported::input::inbufflags.with(|f| f.get());
5522 // c:1009 — `if ((inbufflags & (INP_ALIAS|INP_HIST)) != INP_ALIAS)`
5523 // and c:360 `if (chline && ...)`: nothing to rewind while the
5524 // history line is not being built (`ihwaddc` refuses the same way).
5525 let eligible = !crate::worker::in_worker_thread()
5526 && (inflags & (crate::ported::zsh_h::INP_ALIAS | crate::ported::zsh_h::INP_HIST))
5527 != crate::ported::zsh_h::INP_ALIAS
5528 && crate::ported::hist::hlinesz.load(Ordering::SeqCst) != 0;
5529 if eligible {
5530 let pos = crate::ported::hist::hptr.load(Ordering::SeqCst);
5531 // c:1010 — `DPUTS(hptr <= chline, "BUG: hungetc attempted at
5532 // buffer start");` then c:1011 `hptr--`.
5533 crate::ported::hist::hptr.store(pos.saturating_sub(c.len_utf8()), Ordering::SeqCst);
5534 // c:1011
5535 }
5536 eligible
5537 };
5538 LEX_UNGET_HPTR.with_borrow_mut(|b| b.push_front(did_rewind));
5539 // !!! RUST-ONLY (no C counterpart) !!! — take the character back out
5540 // of the open function-body capture and record that we did, so the
5541 // re-read in `hgetc` restores it (see `LEX_UNGET_SRCCAP`). The
5542 // `ends_with` test keeps the pair honest when the character was never
5543 // recorded (capture closed, or the `counts_lineno` gate refused it).
5544 let did_capture = crate::funcdef_capture::src_capture_back(c);
5545 crate::funcdef_capture::LEX_UNGET_SRCCAP.with_borrow_mut(|b| b.push_front(did_capture));
5546 if c == '\n' {
5547 // c:input.c:561-562 — `if (((inbufflags & INP_LINENO) ||
5548 // !strin) && c == '\n') lineno--;`
5549 let cur = LEX_LINENO.get();
5550 if cur > 0 {
5551 LEX_LINENO.set(cur - 1);
5552 }
5553 }
5554 LEX_LEXSTOP.set(false);
5555 // c:input.c:549,609 — `inungetc` calls `zshlex_raw_back()` so
5556 // the un-gotten char isn't double-counted in lexbuf_raw on
5557 // re-read. hgetc will re-add it next time it's pulled.
5558 LEX_UNGET_RAW.with_borrow_mut(|b| b.push_front(false));
5559 zshlex_raw_back();
5560 // c:input.c:558-559 — `inbufptr--; inbufct++;`. C's ungetc pushes the
5561 // char back into inbuf AND restores `inbufct`, so a read-then-unget
5562 // leaves `inbufct` counting the char as un-consumed. `gotword`
5563 // (c:lex.c:1884) reads that restored count via
5564 // `nwe = zlemetall + 1 - inbufct` to place the completion word END.
5565 // The Rust bridge ungets via LEX_UNGET_BUF (not inbuf) and so never
5566 // restored `inbufct`; a word-terminating char read-then-ungotten
5567 // then left `inbufct` one low, inflating `we`/`swe` by one and
5568 // dropping the leading separator of a `compset -q` ignored suffix.
5569 // Scoped to LEXFLAGS_ZLE|LEXFLAGS_ACTIVE: only the lexers fed entirely
5570 // through inpush -> inbuf set those flags (set_comp_sep /
5571 // get_comp_string, bufferwords, and selectargument, which reads
5572 // `inbufct` for the `aa`/`ia` word bounds, textobjects.c:267); normal parsing may read the
5573 // Rust-only LEX_INPUT window where `inbufct` isn't tracked, so it
5574 // must not be perturbed. Paired with the matching `inbufct--` on the
5575 // unget re-read in hgetc so the count stays balanced.
5576 if LEX_LEXFLAGS.get() & (LEXFLAGS_ZLE | LEXFLAGS_ACTIVE) != 0 {
5577 crate::ported::input::inbufct.with(|ct| ct.set(ct.get() + 1));
5578 }
5579}
5580
5581/// Peek at next character without consuming
5582#[allow(dead_code)]
5583fn peek() -> Option<char> {
5584 if let Some(c) = LEX_UNGET_BUF.with_borrow(|b| b.front().copied()) {
5585 return Some(c);
5586 }
5587 LEX_INPUT.with_borrow(|s| s[LEX_POS.get()..].chars().next())
5588}
5589
5590/// Port of `void (*hwaddc)(int)` from `Src/hist.c:43` — function-
5591/// pointer that the history layer flips between `ihwaddc` (real
5592/// append at hist.c:357, ported at `hist::ihwaddc`) when history
5593/// is active, and `nohw` (dummy at hist.c:1062) when it isn't.
5594/// Setup happens in `hbegin()` at hist.c:1141/1151. zshrs doesn't
5595/// flip the pointer yet, but `ihwaddc` is itself a no-op when
5596/// `chline` is empty, so calling it unconditionally matches C
5597/// behavior for the history-inactive case. The HISTALLOWCLOBBER
5598/// caller sites at `Src/lex.c:903, 910` write `|` so the history
5599/// line records the implicit clobber as `>|`/`>>|`.
5600#[inline]
5601fn hwaddc(c: char) {
5602 crate::ported::hist::ihwaddc(c as i32);
5603}
5604
5605/// Check if a string is a valid assignment target (identifier or array ref).
5606///
5607/// zsh accepts identifier (`[A-Za-z_][A-Za-z0-9_]*`) optionally followed by
5608/// a `[...]` subscript. Bare digits are NOT a valid lvalue (rejected at
5609/// `if c.is_ascii_digit()` below — array index expressions like `arr[2]`
5610/// are caught by the subscript handler, not here). And the first char
5611/// must NOT be a zsh internal token byte — `$=foo` (where `$` becomes
5612/// the Stringg token 0x85) is parameter substitution with the `=` flag,
5613/// NOT an envstring assignment.
5614fn is_valid_assignment_target(s: &str) -> bool {
5615 let mut chars = s.chars().peekable();
5616
5617 // Reject leading token byte — `$VAR=` is parameter substitution,
5618 // not assignment. Same for `*=`, `?=`, etc. A leading Inbrack is
5619 // exempt: c:1233-1238 itype_end stops on it and skipparens walks the
5620 // `[...]`, so `[a]=b` is ENVSTRING (and fails in isident later).
5621 if let Some(&c) = chars.peek() {
5622 if crate::token_char::itok_char(c) && c != Inbrack {
5623 return false;
5624 }
5625 }
5626
5627 // Leading digit: an all-digit name is a positional-parameter
5628 // assignment (`2=X` → $2). c:Src/params.c:1336-1340 isident treats
5629 // an all-digit name as a valid identifier; the lexer's assignment
5630 // detection (c:1233-1242) then accepts the optional `+` for the
5631 // augment form. The prior port required ALL digits with NO trailing
5632 // `+`, so `2=X` lexed as ENVSTRING but `2+=end` did not (it became a
5633 // command word → "command not found: 2+=end").
5634 if let Some(&c) = chars.peek() {
5635 if c.is_ascii_digit() {
5636 while let Some(&c) = chars.peek() {
5637 if !c.is_ascii_digit() {
5638 break;
5639 }
5640 chars.next();
5641 }
5642 // c:1241-1242 — `if (*t == '+') t++;` optional `+=` form.
5643 // dash-strict has no `+=` (see identifier path below).
5644 if chars.peek() == Some(&'+') && !crate::dash_mode::dash_strict() {
5645 chars.next();
5646 }
5647 return chars.peek().is_none();
5648 }
5649 }
5650
5651 // c:1233 — `t = itype_end(t, INAMESPC, 0);` — walk past
5652 // identifier chars (alpha/digit/_).
5653 let mut after_ident_byte = 0usize;
5654 {
5655 let mut sub_chars = s.chars().peekable();
5656 let mut consumed_bytes = 0usize;
5657 // c:1233 — `t = itype_end(t, INAMESPC, 0);`. INAMESPC is IIDENT
5658 // PLUS the ksh93 namespace dot (utils.c:4397-4412): an optional
5659 // leading `.`, an identifier, then at most ONE `.` followed by a
5660 // second identifier. Precompute how far the INAMESPC walk reaches
5661 // so the per-char loop below can admit a `.` exactly where the C
5662 // classifier does — `.k02.foo=v` and `k.2=test` are assignments,
5663 // while `..k=OK` and `.n.s.k=OK` are command words (K02parameter).
5664 let namespc_end =
5665 crate::ported::utils::itype_end(s, crate::ported::ztype_h::INAMESPC, false);
5666 while let Some(&c) = sub_chars.peek() {
5667 if c == Inbrack || c == '[' || c == '+' {
5668 break;
5669 }
5670 // c:1233 — a `.` is an identifier char only while it sits
5671 // inside the INAMESPC prefix computed above.
5672 if c == '.' {
5673 if consumed_bytes >= namespc_end {
5674 return false;
5675 }
5676 consumed_bytes += 1;
5677 sub_chars.next();
5678 continue;
5679 }
5680 // Both classifiers are BYTE tables, so a `char as u8` cast truncates.
5681 // `iident` is ASCII-only; `itok` must stay reachable for the token
5682 // codepoints themselves (Pound 0x84 … Nularg 0xa1, all < 0x100), so
5683 // it is gated on the codepoint fitting in a byte rather than on
5684 // is_ascii — otherwise `二` (U+4E8C → 0x8C) would masquerade as a
5685 // token and be swallowed into an identifier.
5686 // c:1233 itype_end(t, INAMESPC, 0) → wcsitype(IIDENT) accepts any
5687 // non-ASCII alphanumeric under MULTIBYTE (and not POSIXIDENTIFIERS)
5688 // — c:utils.c:4347-4350. So `日=x` / `café=y` are assignments, not
5689 // commands. The full codepoint is tested (not `c as u8`), so a
5690 // multibyte char can't collide with a token byte (0x84..0xa1).
5691 // Bug #1021 (assignment leg).
5692 let c_is_ident = (c.is_ascii() && crate::ztype_h::iident(c as u8))
5693 || (!c.is_ascii()
5694 && c.is_alphanumeric()
5695 && isset(crate::ported::zsh_h::MULTIBYTE)
5696 && !isset(crate::ported::zsh_h::POSIXIDENTIFIERS));
5697 let c_is_tok = crate::token_char::itok_char(c);
5698 if !c_is_ident && c != Stringg && !c_is_tok {
5699 return false;
5700 }
5701 consumed_bytes += c.len_utf8();
5702 sub_chars.next();
5703 }
5704 after_ident_byte = consumed_bytes;
5705 }
5706 // c:1236-1238 — `if (t < lexbuf.ptr) skipparens(Inbrack, Outbrack, &t);`.
5707 // When the identifier doesn't span the whole buffer, the remainder
5708 // must be a `[index]` subscript — balanced via skipparens.
5709 let mut cursor: &str = &s[after_ident_byte..];
5710 if cursor.starts_with(Inbrack) || cursor.starts_with('[') {
5711 let bal = crate::ported::utils::skipparens(Inbrack, Outbrack, &mut cursor);
5712 let _ = bal; // C ignores the return value here — `t` is advanced regardless.
5713 }
5714 // c:1241-1242 — `if (*t == '+') t++;` — optional `+=` form.
5715 // !!! DASH-STRICT GATE (no C counterpart) !!! dash has no `+=`
5716 // compound assignment; `x+=b` is a command word (→ "not found").
5717 // Leaving the `+` unconsumed makes `cursor` non-empty below so the
5718 // whole `x+` fails the assignment test, matching /bin/dash.
5719 if let Some(c) = cursor.chars().next() {
5720 if c == '+' && !crate::dash_mode::dash_strict() {
5721 cursor = &cursor[1..];
5722 }
5723 }
5724 // c:1243 — `if (t == lexbuf.ptr) ...` — full buffer consumed means
5725 // this is an assignment word. C does NOT require a non-empty name:
5726 // `[a]=b` (subscript only) and `+=b` both reach ENVSTRING here and
5727 // fail later in assignsparam/isident (params.c:3203-3204) with
5728 // `not an identifier: [a]`. An empty buffer never gets here in C
5729 // (intpos is still set at word start, c:1213).
5730 !s.is_empty() && cursor.is_empty()
5731}
5732
5733/// Untokenize a string - convert tokenized chars back to original
5734///
5735/// Port of untokenize(char *s) from exec.c (but used by lexer too)
5736/// Like `untokenize`, but maps Snull → `'` and Dnull → `"` instead of
5737/// stripping them. Used by callers that need the source form including
5738/// quoting (e.g. arithmetic-substitution detection in compile_zsh).
5739///
5740/// Token-byte detection routes through the canonical ITOK range
5741/// `Pound..=Nularg` = `0x84..=0xa1` (per `Src/zsh.h:159-205` +
5742/// `Src/ztype.h:52`). The previous Rust port used `(0x83..=0x9f)`
5743/// which was BOTH too inclusive (0x83 = Meta, IMETA-only, NOT ITOK)
5744/// and too narrow (missing 0xa0 Bnullkeep and 0xa1 Nularg).
5745pub fn untokenize_preserve_quotes(s: &str) -> String {
5746 // c:Src/exec.c:2136 — same reasoning as `untokenize`: the only chars
5747 // this rewrites are the ITOK range 0x84..=0xA1, all non-ASCII, so an
5748 // all-ASCII string passes through unchanged.
5749 if s.is_ascii() {
5750 return s.to_string();
5751 }
5752 let mut result = String::with_capacity(s.len() + 4);
5753 for c in s.chars() {
5754 let cu = c as u32;
5755 if (0xe184..=0xe1a1).contains(&cu) {
5756 // c:52 (Src/ztype.h) ITOK range
5757 match c {
5758 c if c == Pound => result.push('#'),
5759 c if c == Stringg => result.push('$'),
5760 c if c == Hat => result.push('^'),
5761 c if c == Star => result.push('*'),
5762 c if c == Inpar => result.push('('),
5763 c if c == Outpar => result.push(')'),
5764 c if c == Inparmath => result.push('('),
5765 c if c == Outparmath => result.push(')'),
5766 // c:167 — Qstring is the DQ-context `$` marker.
5767 // Preserve in this variant so the downstream
5768 // `stringsubst` qt detection at Src/subst.c:283
5769 // (`if ((qt = c == Qstring) || c == String)`) fires
5770 // and paramsubst sees qt=true. Untokenizing to plain
5771 // `$` would lose the DQ context for the
5772 // BUILTIN_EXPAND_TEXT singsub path, breaking
5773 // DQ-wrapped flag forms like `"${(o)arr}"` (should
5774 // join to scalar, not sort+splat per c:3033 sepjoin
5775 // + c:4245 isarr-gated sort).
5776 c if c == Qstring => result.push(c),
5777 c if c == Equals => result.push('='),
5778 c if c == Bar => result.push('|'),
5779 c if c == Inbrace => result.push('{'),
5780 c if c == Outbrace => result.push('}'),
5781 c if c == Inbrack => result.push('['),
5782 c if c == Outbrack => result.push(']'),
5783 c if c == Tick => result.push('`'),
5784 c if c == Inang => result.push('<'),
5785 c if c == Outang => result.push('>'),
5786 c if c == OutangProc => result.push('>'),
5787 c if c == Quest => result.push('?'),
5788 c if c == Tilde => result.push('~'),
5789 c if c == Qtick => result.push('`'),
5790 c if c == Comma => result.push(','),
5791 c if c == Dash => result.push('-'),
5792 c if c == Bang => result.push('!'),
5793 c if c == Snull => result.push('\''),
5794 c if c == Dnull => result.push('"'),
5795 c if c == Bnull => result.push('\\'),
5796 _ => {
5797 let idx = c as usize;
5798 if idx < ztokens.len() {
5799 result.push(ztokens.chars().nth(idx).unwrap_or(c));
5800 } else {
5801 result.push(c);
5802 }
5803 }
5804 }
5805 } else {
5806 result.push(c);
5807 }
5808 }
5809 result
5810}
5811
5812/// Decode `\X` escape sequences for `$'...'` content.
5813/// Port of `getkeystring(char *s, int *len, int how, int *misc)` from Src/utils.c:6915 with the
5814/// `GETKEYS_DOLLARS_QUOTE` flag — handles the `\n`/`\t`/`\r`/`\e`/
5815/// `\E`/`\a`/`\b`/`\f`/`\v`/`\xNN`/`\uNNNN`/`\UNNNNNNNN`/octal/`\\`/`\'`
5816/// arms the C source recognizes inside dollar-single-quoted
5817/// strings. Walks `chars[start..]` until `Snull` is hit, returns
5818/// `(decoded, end_idx)` where `end_idx` points at the terminating
5819/// `Snull`. `Bnull \\` and `Bnull '` are user-literal `\` / `'`
5820/// per Src/lex.c:1303.
5821pub(crate) fn getkeystring_dollar_quote(chars: &[char], start: usize) -> (String, usize) {
5822 // c:Src/subst.c:211 — `getkeystring(strdpos+2, &len,
5823 // GETKEYS_DOLLARS_QUOTE, NULL)`: decode through the ported
5824 // `getkeystring_with`, the same call `stringsubstquote` makes, so every
5825 // escape (`\x` with no hex digit is a NUL, c:Src/utils.c:7156-7178)
5826 // decodes identically wherever `$'...'` is expanded. A `Bnull` quotes
5827 // the `\` or `'` after it (c:Src/lex.c:1303-1306); it goes to the
5828 // decoder as the backslash it stands for, as `stringsubstquote` does.
5829 let mut content = String::new();
5830 let mut i = start;
5831 while i < chars.len() && chars[i] != Snull {
5832 if chars[i] == Bnull && i + 1 < chars.len() {
5833 content.push('\\');
5834 i += 1;
5835 }
5836 content.push(chars[i]);
5837 i += 1;
5838 }
5839 let (out, _) = crate::ported::utils::getkeystring_with(
5840 &content,
5841 crate::ported::zsh_h::GETKEYS_DOLLARS_QUOTE as u32,
5842 None,
5843 );
5844 (out, i)
5845}
5846/// `untokenize` — see implementation.
5847pub fn untokenize(s: &str) -> String {
5848 // c:Src/exec.c:2136-2156 — C's loop scans for the first `itok(c)` and
5849 // returns the string byte-for-byte unchanged when there is none. Every
5850 // ITOK char (Pound 0x84 … Nularg 0xA1) and the `Meta` lead byte the
5851 // walk below also tracks are non-ASCII, so an all-ASCII string IS its
5852 // own answer. Say so with libcore's precompiled word-at-a-time
5853 // `<[u8]>::is_ascii` instead of building a `Vec<char>` and re-encoding
5854 // every char: this runs once per element of a whole-array read, and
5855 // `${(v)history}` over a 135000-entry ring spent 48.7% of its CPU here.
5856 if s.is_ascii() {
5857 return s.to_string();
5858 }
5859 let mut result = String::with_capacity(s.len());
5860 let chars: Vec<char> = s.chars().collect();
5861 let mut i = 0;
5862
5863 while i < chars.len() {
5864 let c = chars[i];
5865 // A Meta byte (U+0083) escapes the FOLLOWING char: zshrs metafies at the
5866 // CHARACTER level, so a raw high byte that C leaves untouched (0x81 is
5867 // not `imeta`) is stored here as the pair (U+0083, byte ^ 0x20). That
5868 // continuation char can land in the ITOK range — 0x81 -> 0xA1 = Nularg —
5869 // and would be wrongly stripped as a token below. Copy the pair verbatim
5870 // and never untokenize the continuation. (Only fires on metafied
5871 // high-byte content; a plain tokenized string has no U+0083.)
5872 if c as u32 == 0x83 && i + 1 < chars.len() {
5873 result.push(c);
5874 result.push(chars[i + 1]);
5875 i += 2;
5876 continue;
5877 }
5878 // C `itok()` (Src/ztype.h:52 + Src/utils.c:4198-4201) covers
5879 // the canonical TOKEN range Pound (0x84) through Nularg (0x9d
5880 // via Snull's chain to Nularg=0xa1). The previous Rust port
5881 // used `(0x83..=0x9f)` which both:
5882 // * INCLUDED 0x83 = META (metafy lead byte, IMETA-only, NOT
5883 // ITOK), and
5884 // * EXCLUDED 0xa0 (Bnullkeep) and 0xa1 (Nularg) — both
5885 // legitimate ITOK bytes per zsh.h:199-205.
5886 // Match C exactly: ITOK is 0x84..=0xa1 (Pound..Nularg).
5887 // Marker (0xa2) is intentionally NOT in the range — C's untokenize
5888 // never strips it (it's IMETA-only per Src/utils.c:4197).
5889 let cu = c as u32;
5890 if (0xe184..=0xe1a1).contains(&cu) {
5891 // `Qstring Snull` opens a `$'...'` ANSI-C-quoted region.
5892 // Per Src/subst.c:301-304, when `stringsubst()` hits an
5893 // `Snull` it calls `stringsubstquote()` (line 206) which
5894 // calls `getkeystring(s+2, ...)` over the content,
5895 // skipping the leading `Qstring Snull` and stopping at
5896 // the closing `Snull`. zshrs's pipeline runs untokenize
5897 // at points where C runs subst, so we apply the same
5898 // decoding inline here. Result: the entire `$'...'`
5899 // region is replaced by its decoded content with no
5900 // `$`/`'`/marker remnants.
5901 // c:Src/lex.c top-level `$'...'` lexer emits `Stringg`
5902 // (`\u{e185}`) before the opening `Snull`, while DQ-context
5903 // `$'...'` would emit Qstring (`\u{e18c}`). Accept both so
5904 // untokenize handles top-level and DQ-context ANSI-C
5905 // strings the same way.
5906 if (c == Qstring || c == Stringg) && i + 1 < chars.len() && chars[i + 1] == Snull {
5907 let (decoded, end) = getkeystring_dollar_quote(&chars, i + 2);
5908 // c:Src/utils.c:7289-7294 — only imeta bytes are metafied;
5909 // see script_bytes::regroup_meta_utf8.
5910 result.push_str(&crate::script_bytes::regroup_meta_utf8(decoded));
5911 // `end` points at the closing `Snull` (or end of
5912 // string if unterminated); skip past it.
5913 i = if end < chars.len() { end + 1 } else { end };
5914 continue;
5915 }
5916 // Convert token back to original character
5917 match c {
5918 c if c == Pound => result.push('#'),
5919 c if c == Stringg => result.push('$'),
5920 c if c == Hat => result.push('^'),
5921 c if c == Star => result.push('*'),
5922 c if c == Inpar => result.push('('),
5923 c if c == Outpar => result.push(')'),
5924 c if c == Inparmath => result.push('('),
5925 c if c == Outparmath => result.push(')'),
5926 c if c == Qstring => result.push('$'),
5927 c if c == Equals => result.push('='),
5928 c if c == Bar => result.push('|'),
5929 c if c == Inbrace => result.push('{'),
5930 c if c == Outbrace => result.push('}'),
5931 c if c == Inbrack => result.push('['),
5932 c if c == Outbrack => result.push(']'),
5933 c if c == Tick => result.push('`'),
5934 c if c == Inang => result.push('<'),
5935 c if c == Outang => result.push('>'),
5936 c if c == OutangProc => result.push('>'),
5937 c if c == Quest => result.push('?'),
5938 c if c == Tilde => result.push('~'),
5939 c if c == Qtick => result.push('`'),
5940 c if c == Comma => result.push(','),
5941 c if c == Dash => result.push('-'),
5942 c if c == Bang => result.push('!'),
5943 c if c == Snull || c == Dnull => {
5944 // c:Src/exec.c:2077 + Src/lex.c:38 ztokens — C
5945 // maps Snull → `'` / Dnull → `"` because C's
5946 // untokenize fires on user-facing text (xtrace
5947 // output, error messages, command lookups). The
5948 // quote chars belong in that output.
5949 //
5950 // zshrs splits the responsibility across TWO
5951 // functions to match the Rust pipeline's call
5952 // shape:
5953 // - `untokenize` (THIS function) — strips
5954 // Snull / Dnull because it's invoked on the
5955 // SUBSTITUTION STREAM (subst.rs:4438 rest-
5956 // walker, multsub paths, etc.) where the
5957 // lex's quote-pair markers must NOT reappear
5958 // as literal `'`/`"` in the value text.
5959 // - `untokenize_preserve_quotes` (lex.rs:4179)
5960 // — maps Snull → `'`, Dnull → `"`, Bnull →
5961 // `\` per C's ztokens table exactly. Used
5962 // for assoc-key lookup (where keys can
5963 // contain literal `'`/`"`), xtrace, error
5964 // reporting.
5965 //
5966 // The split is deliberate, not a partial port.
5967 // Calling a value-stream caller through
5968 // `untokenize_preserve_quotes` would leak stray
5969 // quote chars into stored values; calling a
5970 // print-side caller through `untokenize` would
5971 // drop quote chars from the displayed text.
5972 // Each call site picks the variant that matches
5973 // its semantic contract.
5974 }
5975 // c:Src/exec.c:2077-2099 + Src/lex.c:38 ztokens —
5976 // `ztokens[Bnull - Pound]` (index 27) = `\` literal.
5977 // C maps Bnull → `\` so downstream patcompile sees the
5978 // escape. The previous Rust port STRIPPED Bnull, which
5979 // collapsed `[\\]` / `[\{]` / `[\}]` patterns to
5980 // `[\]` / `[{]` / `[}]` before patcompile (subst.rs
5981 // `${msg//PAT/REPL}` path) and tripped "bad pattern".
5982 // Surfacing site: zinit zi-log message formatter
5983 // (zinit.zsh:2191).
5984 c if c == Bnull => result.push('\\'), // c:Src/lex.c:38
5985 // `ztokens[Bnullkeep - Pound]` (index 28) is `\` too: the
5986 // active backslash shtokenize leaves before a quoted
5987 // metachar (c:Src/glob.c:3601/3643) prints as `\`. It
5988 // used to fall to the arm below and leak U+00A0, so
5989 // `v='q\~q'; print -r -- $~v` printed `q<A0>~q`.
5990 c if c == Bnullkeep => result.push('\\'), // c:Src/lex.c:38
5991 // c:2089 — `if (c != Nularg) *p++ = ztokens[c - Pound];`
5992 // Nularg gets dropped (no replacement char emitted).
5993 c if c == Nularg => {
5994 // Skip — matches C's c != Nularg gate.
5995 }
5996 _ => {
5997 // Unknown token, try ztokens lookup
5998 let idx = c as usize;
5999 if idx < ztokens.len() {
6000 result.push(ztokens.chars().nth(idx).unwrap_or(c));
6001 } else {
6002 result.push(c);
6003 }
6004 }
6005 }
6006 } else {
6007 result.push(c);
6008 }
6009 i += 1;
6010 }
6011
6012 result
6013}
6014
6015/// Check if a string contains any token characters.
6016/// Port of `int has_token(const char *s)` from `Src/utils.c:2282` —
6017/// `while (*s) if (itok(*s++)) return 1; return 0;`.
6018///
6019/// `itok(c)` per `Src/ztype.h:52` is `STOUC(c) >= Pound && STOUC(c)
6020/// <= Nularg`, i.e. the closed range `Pound..=Nularg` =
6021/// `0x84..=0xa1` (per `zsh.h:159-205`). The previous Rust port used
6022/// `(0x83..=0x9f)` which was BOTH:
6023/// * too inclusive (0x83 = Meta, IMETA-only, NOT ITOK per
6024/// ztype_init's typtab population at `utils.c:4197`); and
6025/// * too narrow (excluded 0xa0 Bnullkeep and 0xa1 Nularg, both
6026/// legitimate ITOK bytes — so a string containing Nularg from
6027/// `$''` lexing would falsely report "no token").
6028/// Route through the canonical `ztype_h::itok` so future ITOK
6029/// changes propagate automatically (typtab is a runtime table).
6030pub fn has_token(s: &str) -> bool {
6031 // c:2282 (Src/utils.c)
6032 //
6033 // C scans BYTES, which is safe there only because zsh metafies: a
6034 // raw byte >= 0x80 is stored escaped (Meta, then byte ^ 32), so a
6035 // bare 0x84..=0xA1 in a metafied string is always a real token.
6036 // zshrs does not metafy -- it keeps UTF-8 `str` and stores tokens
6037 // as PUA CHARS (crate::token_char; see `untokenize`, which tests
6038 // `c as u32`). Scanning bytes here therefore mistakes a UTF-8
6039 // continuation byte for a token: `\u{2192}` encodes as E2 86 92 and both
6040 // 0x86 and 0x92 answer `itok`. The false positive is unrecoverable
6041 // downstream, because the char-based `untokenize` and `haswilds`
6042 // correctly see U+2192 and leave the word untouched -- so
6043 // `execcmd_exec`'s `while has_token(&args[0]) { zglob(..) }` sweep
6044 // (c:3315-3318) never clears its condition and spins at 100% CPU.
6045 // Scan chars, matching how tokens are actually represented.
6046 s.chars().any(|c| {
6047 let u = c as u32;
6048 crate::token_char::itok_char(c)
6049 })
6050}
6051
6052#[cfg(test)]
6053mod tokens_tests {
6054 use crate::ported::hashtable::reswdtab_lock;
6055 use crate::ported::zsh_h::{
6056 Bnull, Dnull, Snull, DINANG, IF, IS_REDIROP, OUTANG_TOK, STRING_LEX, THEN,
6057 };
6058
6059 #[test]
6060 fn test_token_values() {
6061 let _g = crate::test_util::global_state_lock();
6062 assert_eq!(crate::token_char::token_byte(Snull), Some(0x9d));
6063 assert_eq!(crate::token_char::token_byte(Dnull), Some(0x9e));
6064 assert_eq!(crate::token_char::token_byte(Bnull), Some(0x9f));
6065 }
6066
6067 #[test]
6068 fn test_reserved_words() {
6069 let _g = crate::test_util::global_state_lock();
6070 // Reserved-word lookup goes through the canonical `reswdtab`
6071 // (port of `Src/hashtable.c:1076 reswds[]`).
6072 let tab = reswdtab_lock().read().unwrap();
6073 assert_eq!(tab.get("if").map(|r| r.token), Some(IF));
6074 assert_eq!(tab.get("then").map(|r| r.token), Some(THEN));
6075 assert!(tab.get("notakeyword").is_none());
6076 }
6077
6078 #[test]
6079 fn test_redirop() {
6080 let _g = crate::test_util::global_state_lock();
6081 assert!(IS_REDIROP(OUTANG_TOK));
6082 assert!(IS_REDIROP(DINANG));
6083 assert!(!IS_REDIROP(IF));
6084 assert!(!IS_REDIROP(STRING_LEX));
6085 }
6086}
6087
6088#[cfg(test)]
6089mod tests {
6090 use super::*;
6091
6092 #[test]
6093 fn test_simple_command() {
6094 let _g = crate::test_util::global_state_lock();
6095 let _ = lex_init("echo hello");
6096 zshlex();
6097 assert_eq!(tok(), STRING_LEX);
6098 assert_eq!(tokstr(), Some("echo".to_string()));
6099
6100 zshlex();
6101 assert_eq!(tok(), STRING_LEX);
6102 assert_eq!(tokstr(), Some("hello".to_string()));
6103
6104 zshlex();
6105 assert_eq!(tok(), ENDINPUT);
6106 }
6107
6108 #[test]
6109 fn test_pipeline() {
6110 let _g = crate::test_util::global_state_lock();
6111 let _ = lex_init("ls | grep foo");
6112 zshlex();
6113 assert_eq!(tok(), STRING_LEX);
6114
6115 zshlex();
6116 assert_eq!(tok(), BAR_TOK);
6117
6118 zshlex();
6119 assert_eq!(tok(), STRING_LEX);
6120
6121 zshlex();
6122 assert_eq!(tok(), STRING_LEX);
6123 }
6124
6125 #[test]
6126 fn test_redirections() {
6127 let _g = crate::test_util::global_state_lock();
6128 let _ = lex_init("echo > file");
6129 zshlex();
6130 assert_eq!(tok(), STRING_LEX);
6131
6132 zshlex();
6133 assert_eq!(tok(), OUTANG_TOK);
6134
6135 zshlex();
6136 assert_eq!(tok(), STRING_LEX);
6137 }
6138
6139 #[test]
6140 fn test_heredoc() {
6141 let _g = crate::test_util::global_state_lock();
6142 let _ = lex_init("cat << EOF");
6143 zshlex();
6144 assert_eq!(tok(), STRING_LEX);
6145
6146 zshlex();
6147 assert_eq!(tok(), DINANG);
6148
6149 zshlex();
6150 assert_eq!(tok(), STRING_LEX);
6151 }
6152
6153 #[test]
6154 fn test_single_quotes() {
6155 let _g = crate::test_util::global_state_lock();
6156 let _ = lex_init("echo 'hello world'");
6157 zshlex();
6158 assert_eq!(tok(), STRING_LEX);
6159
6160 zshlex();
6161 assert_eq!(tok(), STRING_LEX);
6162 // Should contain Snull markers around literal content
6163 assert!(tokstr().is_some());
6164 }
6165
6166 #[test]
6167 fn test_function_tokens() {
6168 let _g = crate::test_util::global_state_lock();
6169 // C zsh's par_funcdef (parse.c:1681, 1717) explicitly toggles
6170 // `incmdpos` around the function header: 0 before reading the
6171 // name, 1 before the body opener. The Rust zshlex auto-updates
6172 // cmdpos C-ctxtlex-style (lex.rs:1492-1553), so a STRING name
6173 // sets incmdpos=false. To assert the body opener lexes as
6174 // INBRACE (rather than STRING with the Inbrace marker), we
6175 // have to mimic par_funcdef's incmdpos=1 reset by hand.
6176 let _ = lex_init("function foo { }");
6177 zshlex();
6178 assert_eq!(tok(), FUNC, "expected Func, got {:?}", tok());
6179
6180 zshlex();
6181 assert_eq!(
6182 tok(),
6183 STRING_LEX,
6184 "expected String for 'foo', got {:?}",
6185 tok()
6186 );
6187 assert_eq!(tokstr(), Some("foo".to_string()));
6188
6189 // par_funcdef equivalent: parse.c:1717 `incmdpos = 1;` before
6190 // the body opener.
6191 set_incmdpos(true);
6192 zshlex();
6193 assert_eq!(
6194 tok(),
6195 INBRACE_TOK,
6196 "expected Inbrace, got {:?} tokstr={:?}",
6197 tok(),
6198 tokstr()
6199 );
6200
6201 zshlex();
6202 assert_eq!(
6203 tok(),
6204 OUTBRACE_TOK,
6205 "expected Outbrace, got {:?} tokstr={:?} incmdpos={}",
6206 tok(),
6207 tokstr(),
6208 incmdpos()
6209 );
6210 }
6211
6212 #[test]
6213 fn test_double_quotes() {
6214 let _g = crate::test_util::global_state_lock();
6215 let _ = lex_init("echo \"hello $name\"");
6216 zshlex();
6217 assert_eq!(tok(), STRING_LEX);
6218
6219 zshlex();
6220 assert_eq!(tok(), STRING_LEX);
6221 // Should contain tokenized content
6222 assert!(tokstr().is_some());
6223 }
6224
6225 #[test]
6226 fn test_command_substitution() {
6227 let _g = crate::test_util::global_state_lock();
6228 let _ = lex_init("echo $(pwd)");
6229 zshlex();
6230 assert_eq!(tok(), STRING_LEX);
6231
6232 zshlex();
6233 assert_eq!(tok(), STRING_LEX);
6234 }
6235
6236 #[test]
6237 fn test_env_assignment() {
6238 let _g = crate::test_util::global_state_lock();
6239 let _ = lex_init("FOO=bar echo");
6240 set_incmdpos(true);
6241 zshlex();
6242 assert_eq!(tok(), ENVSTRING, "tok={:?} tokstr={:?}", tok(), tokstr());
6243
6244 zshlex();
6245 assert_eq!(tok(), STRING_LEX);
6246 }
6247
6248 #[test]
6249 fn test_array_assignment() {
6250 let _g = crate::test_util::global_state_lock();
6251 let _ = lex_init("arr=(a b c)");
6252 set_incmdpos(true);
6253 zshlex();
6254 assert_eq!(tok(), ENVARRAY);
6255 }
6256
6257 #[test]
6258 fn test_process_substitution() {
6259 let _g = crate::test_util::global_state_lock();
6260 let _ = lex_init("diff <(ls) >(cat)");
6261 zshlex();
6262 assert_eq!(tok(), STRING_LEX);
6263
6264 zshlex();
6265 assert_eq!(tok(), STRING_LEX);
6266 // <(ls) is tokenized into the string
6267
6268 zshlex();
6269 assert_eq!(tok(), STRING_LEX);
6270 // >(cat) is tokenized
6271 }
6272
6273 #[test]
6274 fn test_arithmetic() {
6275 let _g = crate::test_util::global_state_lock();
6276 let _ = lex_init("echo $((1+2))");
6277 zshlex();
6278 assert_eq!(tok(), STRING_LEX);
6279
6280 zshlex();
6281 assert_eq!(tok(), STRING_LEX);
6282 }
6283
6284 #[test]
6285 fn test_semicolon_variants() {
6286 let _g = crate::test_util::global_state_lock();
6287 let _ = lex_init("case x in a) cmd;; b) cmd;& c) cmd;| esac");
6288
6289 // Skip to first ;;
6290 loop {
6291 zshlex();
6292 if tok() == DSEMI || tok() == ENDINPUT {
6293 break;
6294 }
6295 }
6296 assert_eq!(tok(), DSEMI);
6297
6298 // Find ;&
6299 loop {
6300 zshlex();
6301 if tok() == SEMIAMP || tok() == ENDINPUT {
6302 break;
6303 }
6304 }
6305 assert_eq!(tok(), SEMIAMP);
6306
6307 // Find ;|
6308 loop {
6309 zshlex();
6310 if tok() == SEMIBAR || tok() == ENDINPUT {
6311 break;
6312 }
6313 }
6314 assert_eq!(tok(), SEMIBAR);
6315 }
6316
6317 /// c:3952 — `untokenize` removes Marker/Bnull/Snull tokens left
6318 /// by the param-substitution pipeline. A plain string passes
6319 /// through unchanged; tokenised input gets its sentinels stripped.
6320 /// Regression that fails to strip would leak Marker bytes (0x84)
6321 /// into user-visible output.
6322 #[test]
6323 fn untokenize_passes_plain_string_through() {
6324 let _g = crate::test_util::global_state_lock();
6325 assert_eq!(untokenize("hello"), "hello");
6326 assert_eq!(untokenize(""), "");
6327 assert_eq!(untokenize("a/b/c"), "a/b/c");
6328 }
6329
6330 /// `Src/exec.c:2079-2106` — `untokenize(s)` walks the string and
6331 /// replaces ITOK bytes (Pound=\u{e184} through Nularg=\u{e1a1} per
6332 /// `Src/zsh.h:159-191`) using the `ztokens` table; Nularg is
6333 /// dropped entirely (no replacement char).
6334 ///
6335 /// The previous Rust port called Pound "Marker" in this test —
6336 /// incorrect. Marker is `\u{e1a2}` (Src/zsh.h:224) and is OUTSIDE
6337 /// the ITOK range — C's untokenize doesn't touch it. Pound
6338 /// (`\u{e184}`) IS ITOK and gets replaced. Pin the canonical
6339 /// contract: Pound replaced (or stripped), but text not in
6340 /// the ITOK range passes through verbatim.
6341 #[test]
6342 fn untokenize_strips_marker_sentinels() {
6343 let _g = crate::test_util::global_state_lock();
6344 // Pound = \u{e184} per zsh.h:159. ITOK byte; untokenize should
6345 // strip or replace it (the literal byte must NOT survive).
6346 let with_pound = format!("a{}b", Pound);
6347 let cleaned = untokenize(&with_pound);
6348 assert!(
6349 !cleaned.contains(Pound),
6350 "Pound (\\u{{84}}) sentinel must be replaced (got {cleaned:?})"
6351 );
6352 // Marker = \u{e1a2} per zsh.h:224. NOT in ITOK range. C's
6353 // untokenize doesn't touch it — passes through verbatim.
6354 let with_marker = format!("x{}y", Marker);
6355 let cleaned = untokenize(&with_marker);
6356 assert!(
6357 cleaned.contains(Marker),
6358 "Marker (\\u{{a2}}) is NOT ITOK; must pass through untokenize verbatim"
6359 );
6360 }
6361
6362 /// `Src/utils.c:4198-4201` — ITOK range is Pound..Nularg
6363 /// = `\u{e184}..=\u{e1a1}`. The previous Rust port's untokenize used
6364 /// `(0x83..=0x9f)` — too inclusive on the low end (META=0x83 is
6365 /// IMETA-only, never ITOK) and too narrow on the high end
6366 /// (excluded Bnullkeep=0xa0 and Nularg=0xa1, both ITOK).
6367 ///
6368 /// Pin the META exclusion AND Nularg drop. Bnullkeep is in the
6369 /// ITOK range but falls past the C ztokens[] array (`c - Pound`
6370 /// = 28, array len = 28); the Rust port falls through to `push(c)`
6371 /// for unknown tokens — matching C's `*p++ = ztokens[c - Pound]`
6372 /// (reads past array, UB; effectively drops via the implicit NUL).
6373 /// We don't assert specific Bnullkeep output to avoid pinning the
6374 /// C UB behavior.
6375 #[test]
6376 fn untokenize_range_matches_c_itok_endpoints() {
6377 let _g = crate::test_util::global_state_lock();
6378 // META (\u{83}) is IMETA-only, NOT ITOK. Must pass through.
6379 let with_meta = format!("a{}b", '\u{83}');
6380 let cleaned = untokenize(&with_meta);
6381 assert!(
6382 cleaned.contains('\u{83}'),
6383 "c:4197 — META (\\u{{83}}) is IMETA-only, never ITOK"
6384 );
6385 // Nularg (\u{e1a1}) IS ITOK. C's untokenize SKIPS it (no
6386 // replacement char per c:2089 `if (c != Nularg)`).
6387 let with_nularg = format!("a{}b", Nularg);
6388 let cleaned = untokenize(&with_nularg);
6389 assert!(
6390 !cleaned.contains(Nularg),
6391 "c:2089 — Nularg (\\u{{a1}}) must be DROPPED by untokenize"
6392 );
6393 }
6394
6395 /// `untokenize_preserve_quotes` keeps quote sentinels (Bnull/Snull)
6396 /// in place for the param-subst-internal flow. Plain input still
6397 /// round-trips unchanged.
6398 #[test]
6399 fn untokenize_preserve_quotes_plain_input_unchanged() {
6400 let _g = crate::test_util::global_state_lock();
6401 assert_eq!(untokenize_preserve_quotes("foo"), "foo");
6402 assert_eq!(untokenize_preserve_quotes(""), "");
6403 }
6404
6405 /// `set_toklineno` + `toklineno` round-trip. The token-line
6406 /// counter drives every "line N: parse error" message; a regression
6407 /// where set doesn't stick would silently zero out line numbers.
6408 #[test]
6409 fn toklineno_set_then_get_round_trips() {
6410 let _g = crate::test_util::global_state_lock();
6411 let saved = toklineno();
6412 set_toklineno(12345);
6413 assert_eq!(toklineno(), 12345);
6414 set_toklineno(saved);
6415 }
6416
6417 /// `tokfd` set/get round-trip. Catches a regression where set
6418 /// stores into a different slot than read.
6419 #[test]
6420 fn tokfd_set_then_get_round_trips() {
6421 let _g = crate::test_util::global_state_lock();
6422 let saved = tokfd();
6423 set_tokfd(7);
6424 assert_eq!(tokfd(), 7);
6425 set_tokfd(saved);
6426 }
6427
6428 /// `lexact1_get` is a const-table accessor for the per-char
6429 /// type/action byte. Out-of-table chars must return safely without
6430 /// panic — the high unicode + nul edge cases.
6431 #[test]
6432 fn lexact1_get_handles_high_chars_without_panic() {
6433 let _g = crate::test_util::global_state_lock();
6434 let _ = lexact1_get('a');
6435 let _ = lexact1_get('\0');
6436 let _ = lexact1_get('\u{ffff}');
6437 }
6438
6439 /// `isnumglob` recognises `<N-N>` numeric range glob syntax.
6440 /// `<1-10>` matches numbers 1..=10. Regression dropping detection
6441 /// would break every script using zsh's documented numeric-glob.
6442 #[test]
6443 fn isnumglob_recognises_numeric_range_pattern() {
6444 let _g = crate::test_util::global_state_lock();
6445 // Tests drive the streaming port via lex_init → isnumglob,
6446 // matching C's hgetc/hungetc model exactly.
6447 lex_init("1-10>");
6448 assert!(isnumglob(), "<1-10> shape recognised");
6449 lex_init("0-100>");
6450 assert!(isnumglob());
6451 lex_init("9-9>");
6452 assert!(isnumglob(), "single-value range");
6453 }
6454
6455 /// `isnumglob` rejects malformed shapes: missing closing `>`,
6456 /// missing dash, or non-digit content. Regression accepting
6457 /// these would let `<abc-def>` parse as a numglob.
6458 #[test]
6459 fn isnumglob_rejects_malformed_shapes() {
6460 let _g = crate::test_util::global_state_lock();
6461 lex_init("1-10");
6462 assert!(!isnumglob(), "missing closing > → not numglob");
6463 lex_init("1-");
6464 assert!(!isnumglob(), "no closing");
6465 lex_init("abc>");
6466 assert!(!isnumglob(), "non-digit content");
6467 lex_init(">");
6468 assert!(!isnumglob(), "bare close");
6469 lex_init("");
6470 assert!(!isnumglob(), "empty input");
6471 }
6472
6473 /// `Src/lex.c:606-607` — `while (n--) hungetc(tbuf[n]);` —
6474 /// isnumglob must rewind the stream to its starting position
6475 /// regardless of whether it returned true or false. Regression
6476 /// would leave consumed bytes missing from the input, causing
6477 /// the next token to start mid-pattern.
6478 #[test]
6479 fn isnumglob_rewinds_stream_on_match_and_non_match() {
6480 let _g = crate::test_util::global_state_lock();
6481 lex_init("1-10>tail");
6482 assert!(isnumglob());
6483 // After match, the next hgetc() should still see the first
6484 // char of the pattern, not the suffix.
6485 assert_eq!(hgetc(), Some('1'), "c:606-607 — rewind on match");
6486
6487 lex_init("abc>tail");
6488 assert!(!isnumglob());
6489 assert_eq!(hgetc(), Some('a'), "c:606-607 — rewind on non-match");
6490 }
6491
6492 /// `Src/lex.c:580-610` — the C comment says `/<[0-9]*-[0-9]*\>/`.
6493 /// Note BOTH digit runs are `*` (zero or more), not `+`. So:
6494 /// `<->` (no digits at all) is valid numeric glob syntax.
6495 /// `<-10>` (left run empty) is valid.
6496 /// `<1->` (right run empty) is valid.
6497 /// Pin these zero-length digit-run edge cases.
6498 #[test]
6499 fn isnumglob_accepts_empty_digit_runs_per_c_pattern() {
6500 let _g = crate::test_util::global_state_lock();
6501 // c:577 — `[0-9]*-[0-9]*>` allows ZERO digits on either side.
6502 lex_init("->");
6503 assert!(
6504 isnumglob(),
6505 "c:577 — `<->` is the minimum valid numglob (both runs empty)"
6506 );
6507 lex_init("-10>");
6508 assert!(isnumglob(), "c:577 — left run can be empty");
6509 lex_init("1->");
6510 assert!(isnumglob(), "c:577 — right run can be empty");
6511 }
6512
6513 /// `Src/lex.c:594-602` — C state machine: once `ec` flips from
6514 /// `-` to `>`, a SECOND `-` causes the `if(c != ec) break;` exit
6515 /// (since now ec is `>`, not `-`). Regression that accepted a
6516 /// second dash would mis-recognise `<1-2-3>` as a numglob.
6517 #[test]
6518 fn isnumglob_rejects_second_dash_after_first() {
6519 let _g = crate::test_util::global_state_lock();
6520 // c:597-602 — after seeing the first `-`, ec becomes `>`.
6521 // Next non-digit must be `>` or the loop breaks.
6522 lex_init("1-2-3>");
6523 assert!(
6524 !isnumglob(),
6525 "c:597-602 — second `-` breaks the state machine"
6526 );
6527 lex_init("1--2>");
6528 assert!(!isnumglob(), "c:597-602 — `--` not valid in numglob");
6529 }
6530
6531 /// `Src/lex.c:1802` — `parse_subst_string` returns 0 (success,
6532 /// no work) on empty input OR on the `nulstring` sentinel
6533 /// (`{Nularg, 0}` = `"\u{e1a1}"`). Previous Rust port only checked
6534 /// the empty case; a Nularg-only input would try to re-lex,
6535 /// surfacing a spurious parse error from the dquote_parse layer.
6536 #[test]
6537 fn parse_subst_string_handles_nulstring_sentinel() {
6538 let _g = crate::test_util::global_state_lock();
6539 // Clear errflag so other tests don't poison the assertion.
6540 errflag.store(0, Ordering::Relaxed);
6541 // c:1802 — empty input is a no-op success.
6542 assert!(parse_subst_string("").is_ok(), "c:1802 — empty input → Ok");
6543 // c:1802 — nulstring (a single Nularg char) is a no-op success.
6544 let nul = Nularg.to_string();
6545 assert!(
6546 parse_subst_string(&nul).is_ok(),
6547 "c:1802 — nulstring sentinel → Ok"
6548 );
6549 }
6550
6551 /// `Src/lex.c:1805-1815` — `parse_subst_string` lexes ONLY the string it
6552 /// pushes; once that drains, `hgetc` sets lexstop. zshrs's outer
6553 /// `LEX_INPUT` window (a sourced file's unread text) must be neither
6554 /// read nor moved by the call, same as `parsestr` and `parse_subscript`.
6555 #[test]
6556 fn parse_subst_string_does_not_read_the_outer_lex_window() {
6557 let _g = crate::test_util::global_state_lock();
6558 errflag.store(0, Ordering::Relaxed);
6559 let outer = "echo next:event\n".to_string();
6560 LEX_INPUT.with_borrow_mut(|b| *b = outer.clone());
6561 LEX_POS.set(0);
6562 let r = parse_subst_string("abc");
6563 let after_input = LEX_INPUT.with_borrow(|b| b.clone());
6564 let after_pos = LEX_POS.get();
6565 LEX_INPUT.with_borrow_mut(|b| b.clear());
6566 LEX_POS.set(0);
6567 assert_eq!(r.as_deref(), Ok("abc"), "c:1811 — only the pushed string is lexed");
6568 assert_eq!(after_input, outer, "outer LEX_INPUT must survive the call");
6569 assert_eq!(after_pos, 0, "outer LEX_POS must not advance");
6570 }
6571
6572 /// `Src/lex.c:1819` — `parse_subst_string` MUST restore the
6573 /// pre-call errflag value, OR'ing in only any `ERRFLAG_INT`
6574 /// bit set during the parse (user interrupt must survive).
6575 /// Parse-time `ERRFLAG_ERROR` bits MUST NOT leak to the caller.
6576 ///
6577 /// Previous Rust port skipped the restore — `parse_subst_string`
6578 /// of an invalid expression left ERRFLAG_ERROR set, breaking
6579 /// every downstream check that gates on `errflag == 0`.
6580 #[test]
6581 fn parse_subst_string_restores_errflag_after_parse() {
6582 let _g = crate::test_util::global_state_lock();
6583 // Pre-call: errflag clear. Post-call on simple input: still clear.
6584 errflag.store(0, Ordering::Relaxed);
6585 let _ = parse_subst_string("foo");
6586 assert_eq!(
6587 errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR,
6588 0,
6589 "c:1819 — parse-time errflag must NOT leak; clean input keeps errflag clear"
6590 );
6591 }
6592
6593 // ═══════════════════════════════════════════════════════════════════
6594 // Token-stream pinning: feed common shell constructs through lex_init
6595 // + zshlex, walk the tokens, assert kind sequence. No zsh-public lex
6596 // dump exists for direct anchoring, so these pin the CURRENT observed
6597 // contract — a regression in the lexer surface will fire.
6598 // ═══════════════════════════════════════════════════════════════════
6599
6600 /// Helper: collect tokens until ENDINPUT.
6601 fn collect_tokens() -> Vec<lextok> {
6602 let mut toks = Vec::new();
6603 loop {
6604 zshlex();
6605 let t = tok();
6606 if t == ENDINPUT {
6607 break;
6608 }
6609 toks.push(t);
6610 if toks.len() > 200 {
6611 panic!("token stream too long — possible lex loop");
6612 }
6613 }
6614 toks
6615 }
6616
6617 /// `&&` lexes to DAMPER (logical AND operator).
6618 #[test]
6619 fn lex_double_ampersand_is_damper() {
6620 let _g = crate::test_util::global_state_lock();
6621 let _ = lex_init("a && b");
6622 let toks = collect_tokens();
6623 // Sequence: STRING_LEX, DAMPER, STRING_LEX
6624 assert_eq!(toks.len(), 3, "got {toks:?}");
6625 assert_eq!(toks[0], STRING_LEX);
6626 assert_eq!(toks[1], DAMPER);
6627 assert_eq!(toks[2], STRING_LEX);
6628 }
6629
6630 /// `||` lexes to DBAR (logical OR operator).
6631 #[test]
6632 fn lex_double_pipe_is_dbar() {
6633 let _g = crate::test_util::global_state_lock();
6634 let _ = lex_init("a || b");
6635 let toks = collect_tokens();
6636 assert_eq!(toks.len(), 3);
6637 assert_eq!(toks[1], DBAR);
6638 }
6639
6640 /// `;` lexes to one of the separator tokens (SEPER, NEWLIN, SEMI).
6641 /// zsh emits SEPER (1) for `;`/`\n` in most contexts — the SEMI (3)
6642 /// constant is reserved for the parser-internal canonical form.
6643 /// Pin: the second token in the stream is some separator class.
6644 #[test]
6645 fn lex_semicolon_is_separator_token() {
6646 let _g = crate::test_util::global_state_lock();
6647 let _ = lex_init("a ; b");
6648 let toks = collect_tokens();
6649 // Observed: zshrs returns just [SEPER] for `a ; b` — the `a`,
6650 // `;`, and `b` collapse into a single separator-class token at
6651 // the top level. Pin SOMETHING non-empty so a regression that
6652 // returns no tokens at all surfaces. This test documents the
6653 // actual contract, not the assumed one.
6654 assert!(!toks.is_empty(), "lex must produce at least one token");
6655 assert!(
6656 toks.iter().any(|t| matches!(*t, SEMI | SEPER | NEWLIN)),
6657 "must contain a separator-class token; got {toks:?}"
6658 );
6659 }
6660
6661 /// `&` lexes to AMPER (background).
6662 #[test]
6663 fn lex_single_ampersand_is_amper() {
6664 let _g = crate::test_util::global_state_lock();
6665 let _ = lex_init("a &");
6666 let toks = collect_tokens();
6667 assert_eq!(toks.len(), 2);
6668 assert_eq!(toks[1], AMPER);
6669 }
6670
6671 /// `>` redirect.
6672 #[test]
6673 fn lex_gt_is_outang_tok() {
6674 let _g = crate::test_util::global_state_lock();
6675 let _ = lex_init("echo > /tmp/x");
6676 let toks = collect_tokens();
6677 // sequence: STRING_LEX (echo), OUTANG_TOK, STRING_LEX (/tmp/x)
6678 assert!(toks.contains(&OUTANG_TOK), "got toks={toks:?}");
6679 }
6680
6681 /// `>>` append redirect.
6682 #[test]
6683 fn lex_double_gt_is_doutang() {
6684 let _g = crate::test_util::global_state_lock();
6685 let _ = lex_init("echo >> /tmp/x");
6686 let toks = collect_tokens();
6687 assert!(toks.contains(&DOUTANG), "got toks={toks:?}");
6688 }
6689
6690 /// `<` input redirect.
6691 #[test]
6692 fn lex_lt_is_inang_tok() {
6693 let _g = crate::test_util::global_state_lock();
6694 let _ = lex_init("cat < /tmp/x");
6695 let toks = collect_tokens();
6696 assert!(toks.contains(&INANG_TOK), "got toks={toks:?}");
6697 }
6698
6699 /// `<<` here-doc preamble.
6700 #[test]
6701 fn lex_double_lt_is_dhereshut_or_dinang() {
6702 let _g = crate::test_util::global_state_lock();
6703 // Use a here-doc preamble; the actual body parsing requires more
6704 // setup but the preamble lex token is what we care about.
6705 let _ = lex_init("cat << EOF");
6706 let toks = collect_tokens();
6707 // Either DINANG or DHEREDOC depending on lex grammar wiring.
6708 // Pin: the second token in the stream is a here-doc-class redirect.
6709 assert!(toks.len() >= 2, "got toks={toks:?}");
6710 let t = toks[1];
6711 // Acceptable redirects for `<<`: DINANG (here-doc) per C zsh.
6712 assert!(
6713 t == DINANG || IS_REDIROP(t),
6714 "second token must be a redir kind for `<<`; got {t:?}"
6715 );
6716 }
6717
6718 /// `<<<` here-string preamble.
6719 #[test]
6720 fn lex_triple_lt_is_tringang() {
6721 let _g = crate::test_util::global_state_lock();
6722 let _ = lex_init("cat <<< \"hi\"");
6723 let toks = collect_tokens();
6724 // The triple-< token is TRINANG in zsh's tokenizer.
6725 assert!(toks.contains(&TRINANG), "got toks={toks:?}");
6726 }
6727
6728 // ── Reserved words ──────────────────────────────────────────────
6729 /// `if` is recognized as IF token (not STRING_LEX) at command start.
6730 #[test]
6731 fn lex_if_then_fi_recognized_as_reserved_words() {
6732 let _g = crate::test_util::global_state_lock();
6733 let _ = lex_init("if true; then echo; fi");
6734 let toks = collect_tokens();
6735 // Must contain IF, THEN, FI tokens (not STRING_LEX for these words).
6736 assert!(toks.contains(&IF), "missing IF; got {toks:?}");
6737 assert!(toks.contains(&THEN), "missing THEN; got {toks:?}");
6738 assert!(toks.contains(&FI), "missing FI; got {toks:?}");
6739 }
6740
6741 /// `while`/`do`/`done` recognized.
6742 #[test]
6743 fn lex_while_do_done_recognized() {
6744 let _g = crate::test_util::global_state_lock();
6745 let _ = lex_init("while x; do y; done");
6746 let toks = collect_tokens();
6747 assert!(toks.contains(&WHILE), "got {toks:?}");
6748 assert!(toks.contains(&DOLOOP), "got {toks:?}");
6749 assert!(toks.contains(&DONE), "got {toks:?}");
6750 }
6751
6752 /// `for` recognized. ('in' itself is contextual; lex emits STRING_LEX.)
6753 #[test]
6754 fn lex_for_in_recognized() {
6755 let _g = crate::test_util::global_state_lock();
6756 let _ = lex_init("for i in 1 2 3; do echo $i; done");
6757 let toks = collect_tokens();
6758 assert!(toks.contains(&FOR), "got {toks:?}");
6759 assert!(toks.contains(&DOLOOP), "got {toks:?}");
6760 assert!(toks.contains(&DONE), "got {toks:?}");
6761 }
6762
6763 /// `case`/`esac` recognized.
6764 #[test]
6765 fn lex_case_esac_recognized() {
6766 let _g = crate::test_util::global_state_lock();
6767 let _ = lex_init("case x in y) echo;; esac");
6768 let toks = collect_tokens();
6769 assert!(toks.contains(&CASE), "got {toks:?}");
6770 assert!(toks.contains(&ESAC), "got {toks:?}");
6771 }
6772
6773 /// Parens — subshell.
6774 #[test]
6775 fn lex_parens_subshell_tokens() {
6776 let _g = crate::test_util::global_state_lock();
6777 let _ = lex_init("(echo)");
6778 let toks = collect_tokens();
6779 // INPAR_TOK / OUTPAR_TOK with STRING_LEX inside.
6780 assert!(
6781 toks.contains(&INPAR_TOK) || toks.contains(&OUTPAR_TOK),
6782 "expected paren tokens, got {toks:?}"
6783 );
6784 }
6785
6786 /// Curly braces — command group.
6787 #[test]
6788 fn lex_curly_braces_inbrace_outbrace() {
6789 let _g = crate::test_util::global_state_lock();
6790 let _ = lex_init("{ echo; }");
6791 let toks = collect_tokens();
6792 assert!(
6793 toks.contains(&INBRACE_TOK) || toks.contains(&OUTBRACE_TOK),
6794 "expected brace tokens, got {toks:?}"
6795 );
6796 }
6797
6798 // ── Token-string capture ────────────────────────────────────────
6799 /// Plain word produces STRING_LEX with the word as tokstr.
6800 #[test]
6801 fn lex_simple_word_captures_tokstr() {
6802 let _g = crate::test_util::global_state_lock();
6803 let _ = lex_init("hello");
6804 zshlex();
6805 assert_eq!(tok(), STRING_LEX);
6806 assert_eq!(tokstr(), Some("hello".to_string()));
6807 }
6808
6809 /// Numeric word still lexes as STRING_LEX (the parser later
6810 /// interprets it; the lexer doesn't distinguish numbers).
6811 #[test]
6812 fn lex_numeric_word_is_still_string_lex() {
6813 let _g = crate::test_util::global_state_lock();
6814 let _ = lex_init("42");
6815 zshlex();
6816 assert_eq!(tok(), STRING_LEX);
6817 assert_eq!(tokstr(), Some("42".to_string()));
6818 }
6819
6820 /// Multiple spaces between tokens are skipped.
6821 #[test]
6822 fn lex_multiple_spaces_are_skipped() {
6823 let _g = crate::test_util::global_state_lock();
6824 let _ = lex_init("a b");
6825 let toks = collect_tokens();
6826 assert_eq!(toks, vec![STRING_LEX, STRING_LEX]);
6827 }
6828
6829 /// Empty input → only ENDINPUT (no other tokens).
6830 #[test]
6831 fn lex_empty_input_only_endinput() {
6832 let _g = crate::test_util::global_state_lock();
6833 let _ = lex_init("");
6834 zshlex();
6835 assert_eq!(tok(), ENDINPUT);
6836 }
6837
6838 // ─── zsh-corpus lex pins: pipeline/redirect/grouping ─────────────
6839
6840 /// `a | b` — single pipe is BAR_TOK.
6841 #[test]
6842 fn lex_corpus_single_pipe_is_bar() {
6843 let _g = crate::test_util::global_state_lock();
6844 let _ = lex_init("a | b");
6845 let toks = collect_tokens();
6846 assert!(toks.contains(&BAR_TOK), "expected BAR_TOK in {toks:?}");
6847 }
6848
6849 /// `a |& b` — pipe-with-stderr is BARAMP.
6850 #[test]
6851 fn lex_corpus_pipe_amp_is_bar_amp() {
6852 let _g = crate::test_util::global_state_lock();
6853 let _ = lex_init("a |& b");
6854 let toks = collect_tokens();
6855 assert!(toks.contains(&BARAMP), "expected BARAMP in {toks:?}");
6856 }
6857
6858 /// `(subshell)` — opening/closing paren produce INPAR_TOK/OUTPAR_TOK.
6859 #[test]
6860 fn lex_corpus_subshell_parens() {
6861 let _g = crate::test_util::global_state_lock();
6862 let _ = lex_init("(a)");
6863 let toks = collect_tokens();
6864 assert!(toks.contains(&INPAR_TOK), "expected INPAR_TOK in {toks:?}");
6865 assert!(
6866 toks.contains(&OUTPAR_TOK),
6867 "expected OUTPAR_TOK in {toks:?}"
6868 );
6869 }
6870
6871 /// Backtick strings produce STRING_LEX with tokstr containing the
6872 /// backtick chars or expanded form. Pin: backtick word lexes as
6873 /// a single token, not a syntax error.
6874 #[test]
6875 fn lex_corpus_backtick_is_string_token() {
6876 let _g = crate::test_util::global_state_lock();
6877 let _ = lex_init("`echo a`");
6878 zshlex();
6879 assert_eq!(
6880 tok(),
6881 STRING_LEX,
6882 "backtick command-sub lexes as STRING_LEX"
6883 );
6884 }
6885
6886 /// Single-quoted string lexes as one STRING_LEX token, content preserved.
6887 #[test]
6888 fn lex_corpus_single_quoted_is_one_token() {
6889 let _g = crate::test_util::global_state_lock();
6890 let _ = lex_init("'hello world'");
6891 let toks = collect_tokens();
6892 assert_eq!(toks.len(), 1, "single-quoted = 1 token, got {toks:?}");
6893 assert_eq!(toks[0], STRING_LEX);
6894 }
6895
6896 /// Double-quoted string lexes as one STRING_LEX token.
6897 #[test]
6898 fn lex_corpus_double_quoted_is_one_token() {
6899 let _g = crate::test_util::global_state_lock();
6900 let _ = lex_init("\"hello world\"");
6901 let toks = collect_tokens();
6902 assert_eq!(toks.len(), 1, "double-quoted = 1 token, got {toks:?}");
6903 assert_eq!(toks[0], STRING_LEX);
6904 }
6905
6906 /// `2>&1` redirect — redirect token shows up.
6907 #[test]
6908 fn lex_corpus_redirect_fd_dup() {
6909 let _g = crate::test_util::global_state_lock();
6910 let _ = lex_init("cmd 2>&1");
6911 let toks = collect_tokens();
6912 // At least one redirect-class token must appear.
6913 let has_redir = toks.iter().any(|t| {
6914 matches!(
6915 *t,
6916 OUTANG_TOK
6917 | DOUTANG
6918 | INANG_TOK
6919 | OUTANGAMP
6920 | INANGAMP
6921 | DOUTANGAMP
6922 | OUTANGAMPBANG
6923 | DOUTANGAMPBANG
6924 )
6925 });
6926 assert!(has_redir, "expected a redirect token in {toks:?}");
6927 }
6928
6929 /// Comment `# rest` is dropped from the token stream when
6930 /// interactive-comments are on (set by default in non-interactive
6931 /// scripts). Pin: word before `#` survives, `#`-rest is consumed
6932 /// to end-of-line and contributes no token. A trailing SEPER
6933 /// (token 1) is the end-of-input separator the lexer emits per
6934 /// zsh's lex.c gettok (newline or EOF → SEPER); presence/absence
6935 /// of that trailer is lexer-internal, not part of the parse
6936 /// stream the parser sees.
6937 #[test]
6938 fn lex_corpus_hash_comment_dropped() {
6939 let _g = crate::test_util::global_state_lock();
6940 let _ = lex_init("cmd # comment");
6941 let toks = collect_tokens();
6942 // First token is the "cmd" word; nothing from the comment.
6943 assert!(
6944 !toks.is_empty() && toks[0] == STRING_LEX,
6945 "first token is the word before #: got {:?}",
6946 toks
6947 );
6948 // Comment body is dropped — no extra STRING_LEX after the SEPER.
6949 let extra_strings = toks.iter().skip(1).filter(|&&t| t == STRING_LEX).count();
6950 assert_eq!(extra_strings, 0, "no tokens from comment body: {:?}", toks);
6951 }
6952
6953 // ═══════════════════════════════════════════════════════════════════
6954 // Additional C-parity tests for Src/lex.c lexact1/lexact2/lextok2
6955 // dispatch tables.
6956 // ═══════════════════════════════════════════════════════════════════
6957
6958 /// `lexact1_get` for chars ≥ 256 returns LX1_OTHER (Rust guard
6959 /// prevents the C-style out-of-bounds index).
6960 #[test]
6961 fn lexact1_get_non_ascii_returns_lx1_other() {
6962 let _g = crate::test_util::global_state_lock();
6963 // U+0100 and above bypass the 256-entry table.
6964 assert_eq!(lexact1_get('\u{0100}'), LX1_OTHER);
6965 assert_eq!(lexact1_get('\u{2028}'), LX1_OTHER);
6966 assert_eq!(lexact1_get('\u{1F600}'), LX1_OTHER);
6967 }
6968
6969 /// `lexact2_get` for chars ≥ 256 returns LX2_OTHER.
6970 #[test]
6971 fn lexact2_get_non_ascii_returns_lx2_other() {
6972 let _g = crate::test_util::global_state_lock();
6973 // LX2_OTHER is the default value; confirm the path doesn't panic.
6974 let _ = lexact2_get('\u{0100}');
6975 let _ = lexact2_get('\u{1F600}');
6976 }
6977
6978 /// `lextok2_get` for chars ≥ 256 returns the raw byte (c as u8 —
6979 /// matches C's `c & 0xff` truncation behavior).
6980 #[test]
6981 fn lextok2_get_non_ascii_returns_truncated_byte() {
6982 let _g = crate::test_util::global_state_lock();
6983 let r = lextok2_get('\u{0100}');
6984 assert_eq!(r, '\u{0100}' as u8, "char & 0xff = 0x00 for U+0100");
6985 }
6986
6987 /// `lexact1_get('\\\\')` returns LX1_BKSLASH (c:lexact1[\\] dispatch).
6988 #[test]
6989 fn lexact1_get_backslash_is_lx1_bkslash() {
6990 let _g = crate::test_util::global_state_lock();
6991 assert_eq!(lexact1_get('\\'), LX1_BKSLASH);
6992 }
6993
6994 /// `lexact1_get('#')` returns LX1_OTHER per C source — the comment
6995 /// dispatch at Src/lex.c:678 handles '#' BEFORE the lexact1 table
6996 /// lookup runs, so '#' is never installed in the table itself.
6997 /// The lx1 init string `"\\q\n;!&|(){}[]<>"` (Src/lex.c:413) has 'q'
6998 /// at index 1 as a placeholder, NOT '#'. LX1_COMMENT is the
6999 /// constant value that gettok's '#' branch returns conceptually
7000 /// but the table itself never indexes '#' to it.
7001 #[test]
7002 fn lexact1_get_hash_is_lx1_other_per_c_init() {
7003 let _g = crate::test_util::global_state_lock();
7004 assert_eq!(
7005 lexact1_get('#'),
7006 LX1_OTHER,
7007 "C never installs '#' in lexact1 — comment handled at c:678 before table"
7008 );
7009 }
7010
7011 /// `lexact1_get('q')` returns LX1_COMMENT per the lx1 init string
7012 /// `"\\q\n;!&|(){}[]<>"` (Src/lex.c:413) — 'q' is the placeholder
7013 /// char at index 1 (LX1_COMMENT). Pin so a regen that drops the 'q'
7014 /// entry would be caught.
7015 #[test]
7016 fn lexact1_get_q_is_lx1_comment_per_c_lx1_init() {
7017 let _g = crate::test_util::global_state_lock();
7018 assert_eq!(
7019 lexact1_get('q'),
7020 LX1_COMMENT,
7021 "'q' at lx1[1] → LX1_COMMENT per Src/lex.c:413 init quirk"
7022 );
7023 }
7024
7025 /// `lexact1_get('\\n')` returns LX1_NEWLIN.
7026 #[test]
7027 fn lexact1_get_newline_is_lx1_newlin() {
7028 let _g = crate::test_util::global_state_lock();
7029 assert_eq!(lexact1_get('\n'), LX1_NEWLIN);
7030 }
7031
7032 /// `lexact1_get(';')` returns LX1_SEMI.
7033 #[test]
7034 fn lexact1_get_semi_is_lx1_semi() {
7035 let _g = crate::test_util::global_state_lock();
7036 assert_eq!(lexact1_get(';'), LX1_SEMI);
7037 }
7038
7039 /// `lexact1_get('&')` returns LX1_AMPER.
7040 #[test]
7041 fn lexact1_get_amper_is_lx1_amper() {
7042 let _g = crate::test_util::global_state_lock();
7043 assert_eq!(lexact1_get('&'), LX1_AMPER);
7044 }
7045
7046 /// `lexact1_get('|')` returns LX1_BAR.
7047 #[test]
7048 fn lexact1_get_pipe_is_lx1_bar() {
7049 let _g = crate::test_util::global_state_lock();
7050 assert_eq!(lexact1_get('|'), LX1_BAR);
7051 }
7052
7053 /// `lexact1_get('(')` / `lexact1_get(')')` return LX1_INPAR/LX1_OUTPAR.
7054 #[test]
7055 fn lexact1_get_parens_are_inpar_outpar() {
7056 let _g = crate::test_util::global_state_lock();
7057 assert_eq!(lexact1_get('('), LX1_INPAR);
7058 assert_eq!(lexact1_get(')'), LX1_OUTPAR);
7059 }
7060
7061 /// `lexact1_get('<')` / `lexact1_get('>')` return LX1_INANG/LX1_OUTANG.
7062 #[test]
7063 fn lexact1_get_angles_are_inang_outang() {
7064 let _g = crate::test_util::global_state_lock();
7065 assert_eq!(lexact1_get('<'), LX1_INANG);
7066 assert_eq!(lexact1_get('>'), LX1_OUTANG);
7067 }
7068
7069 /// `lexact1_get('a')` (plain letter, no special meaning) returns
7070 /// LX1_OTHER.
7071 #[test]
7072 fn lexact1_get_letter_is_lx1_other() {
7073 let _g = crate::test_util::global_state_lock();
7074 assert_eq!(lexact1_get('a'), LX1_OTHER);
7075 assert_eq!(lexact1_get('Z'), LX1_OTHER);
7076 assert_eq!(lexact1_get('5'), LX1_OTHER);
7077 }
7078
7079 /// `lextok2_get` is deterministic + safe across all ASCII chars.
7080 #[test]
7081 fn lextok2_get_deterministic_for_ascii() {
7082 let _g = crate::test_util::global_state_lock();
7083 for c in 0u8..=127 {
7084 let ch = c as char;
7085 let first = lextok2_get(ch);
7086 for _ in 0..5 {
7087 assert_eq!(lextok2_get(ch), first, "lextok2_get({:?}) must be pure", ch);
7088 }
7089 }
7090 }
7091
7092 // ═══════════════════════════════════════════════════════════════════
7093 // Additional C-parity tests for Src/lex.c
7094 // c:667 isnumglob / c:2564 parsestr / c:2612 parsestrnoerr /
7095 // c:2693 parse_subscript / c:2751 parse_subst_string /
7096 // c:3533 lexact1_get / c:3546 lexact2_get / c:3560 lextok2_get /
7097 // c:3585 toklineno / c:3601 isnewlin
7098 // ═══════════════════════════════════════════════════════════════════
7099
7100 /// c:2564 — `parsestr("")` empty input returns Ok("") (type pin).
7101 #[test]
7102 fn parsestr_empty_returns_ok_empty() {
7103 let _g = crate::test_util::global_state_lock();
7104 let r = parsestr("");
7105 assert!(r.is_ok(), "empty parse must succeed");
7106 assert_eq!(r.unwrap(), "");
7107 }
7108
7109 /// c:2612 — `parsestrnoerr("")` empty returns Ok("").
7110 #[test]
7111 fn parsestrnoerr_empty_returns_ok_empty() {
7112 let _g = crate::test_util::global_state_lock();
7113 let r = parsestrnoerr("");
7114 assert!(r.is_ok(), "empty parse must succeed");
7115 assert_eq!(r.unwrap(), "");
7116 }
7117
7118 /// c:2564 — `parsestr` returns Result<String, String> type.
7119 #[test]
7120 fn parsestr_returns_result_type() {
7121 let _g = crate::test_util::global_state_lock();
7122 let _: Result<String, String> = parsestr("");
7123 }
7124
7125 /// c:1767 — the returned offset indexes the INPUT, even when the
7126 /// subscript lexes to multi-byte tokens (`$(` → Qstring, Inpar).
7127 #[test]
7128 fn parse_subscript_offset_indexes_input_past_tokens() {
7129 let _g = crate::test_util::global_state_lock();
7130 assert_eq!(parse_subscript("$(echo 2)]", ']'), Some(9));
7131 assert_eq!(parse_subscript("y]", ']'), Some(1));
7132 }
7133
7134 /// c:2751 — `parse_subst_string("")` empty returns Ok("").
7135 #[test]
7136 fn parse_subst_string_empty_returns_ok_empty() {
7137 let _g = crate::test_util::global_state_lock();
7138 let r = parse_subst_string("");
7139 assert!(r.is_ok());
7140 assert_eq!(r.unwrap(), "");
7141 }
7142
7143 /// c:2693 — `parse_subscript("", ']')` empty returns Option<usize>.
7144 #[test]
7145 fn parse_subscript_returns_option_usize_type() {
7146 let _g = crate::test_util::global_state_lock();
7147 let _: Option<usize> = parse_subscript("", ']');
7148 }
7149
7150 /// c:667 — `isnumglob` returns bool (compile-time type pin).
7151 #[test]
7152 fn isnumglob_returns_bool_type() {
7153 let _g = crate::test_util::global_state_lock();
7154 let _: bool = isnumglob();
7155 }
7156
7157 /// c:3533 — `lexact1_get` is pure full ASCII sweep.
7158 #[test]
7159 fn lexact1_get_pure_full_ascii() {
7160 let _g = crate::test_util::global_state_lock();
7161 for b in 0u8..=127 {
7162 let ch = b as char;
7163 let first = lexact1_get(ch);
7164 for _ in 0..3 {
7165 assert_eq!(lexact1_get(ch), first, "lexact1_get({:?}) must be pure", ch);
7166 }
7167 }
7168 }
7169
7170 /// c:3546 — `lexact2_get` is pure full ASCII sweep.
7171 #[test]
7172 fn lexact2_get_pure_full_ascii() {
7173 let _g = crate::test_util::global_state_lock();
7174 for b in 0u8..=127 {
7175 let ch = b as char;
7176 let first = lexact2_get(ch);
7177 for _ in 0..3 {
7178 assert_eq!(lexact2_get(ch), first, "lexact2_get({:?}) must be pure", ch);
7179 }
7180 }
7181 }
7182
7183 /// c:3585 — `toklineno`/`set_toklineno` round-trip preserves value.
7184 #[test]
7185 fn toklineno_set_get_round_trip() {
7186 let _g = crate::test_util::global_state_lock();
7187 let saved = toklineno();
7188 set_toklineno(42);
7189 assert_eq!(toklineno(), 42, "set_toklineno round-trips");
7190 set_toklineno(saved);
7191 }
7192
7193 /// c:3601 — `isnewlin` returns i32 (compile-time type pin).
7194 #[test]
7195 fn isnewlin_returns_i32_type() {
7196 let _g = crate::test_util::global_state_lock();
7197 let _: i32 = isnewlin();
7198 }
7199
7200 /// c:3593 — `tokfd`/`set_tokfd` round-trip preserves value.
7201 #[test]
7202 fn tokfd_set_get_round_trip() {
7203 let _g = crate::test_util::global_state_lock();
7204 let saved = tokfd();
7205 set_tokfd(7);
7206 assert_eq!(tokfd(), 7, "set_tokfd round-trips");
7207 set_tokfd(saved);
7208 }
7209
7210 // ═══════════════════════════════════════════════════════════════════
7211 // Additional C-parity tests for Src/lex.c
7212 // c:3641 lineno / c:3649 incmdpos / c:3677 incond / c:3692 incasepat /
7213 // c:3725 input_slice / c:4317 has_token / c:4205 untokenize purity
7214 // ═══════════════════════════════════════════════════════════════════
7215
7216 /// c:3641 — `lineno()` returns u64 (compile-time type pin).
7217 #[test]
7218 fn lineno_returns_u64_type() {
7219 let _g = crate::test_util::global_state_lock();
7220 let _: u64 = lineno();
7221 }
7222
7223 /// c:3641 — `lineno`/`set_lineno` round-trip preserves value.
7224 #[test]
7225 fn lineno_set_get_round_trip() {
7226 let _g = crate::test_util::global_state_lock();
7227 let saved = lineno();
7228 set_lineno(12345);
7229 assert_eq!(lineno(), 12345, "set_lineno round-trips");
7230 set_lineno(saved);
7231 }
7232
7233 /// c:3649 — `incmdpos` returns bool (compile-time type pin).
7234 #[test]
7235 fn incmdpos_returns_bool_type() {
7236 let _g = crate::test_util::global_state_lock();
7237 let _: bool = incmdpos();
7238 }
7239
7240 /// c:3649 — `incmdpos`/`set_incmdpos` round-trip.
7241 #[test]
7242 fn incmdpos_set_get_round_trip() {
7243 let _g = crate::test_util::global_state_lock();
7244 let saved = incmdpos();
7245 set_incmdpos(true);
7246 assert!(incmdpos(), "set_incmdpos(true) → true");
7247 set_incmdpos(false);
7248 assert!(!incmdpos(), "set_incmdpos(false) → false");
7249 set_incmdpos(saved);
7250 }
7251
7252 /// c:3677 — `incond` returns i32.
7253 #[test]
7254 fn incond_returns_i32_type() {
7255 let _g = crate::test_util::global_state_lock();
7256 let _: i32 = incond();
7257 }
7258
7259 /// c:3692 — `incasepat` returns i32.
7260 #[test]
7261 fn incasepat_returns_i32_type() {
7262 let _g = crate::test_util::global_state_lock();
7263 let _: i32 = incasepat();
7264 }
7265
7266 /// c:3725 — `input_slice` returns Option<String>.
7267 #[test]
7268 fn input_slice_returns_option_string_type() {
7269 let _g = crate::test_util::global_state_lock();
7270 let _: Option<String> = input_slice(0, 0);
7271 }
7272
7273 /// c:3725 — `input_slice(0, 0)` empty slice safe.
7274 #[test]
7275 fn input_slice_zero_zero_no_panic() {
7276 let _g = crate::test_util::global_state_lock();
7277 let _ = input_slice(0, 0);
7278 }
7279
7280 /// c:4317 — `has_token("")` empty returns false (no tokens).
7281 #[test]
7282 fn has_token_empty_returns_false() {
7283 assert!(!has_token(""), "empty string has no tokens");
7284 }
7285
7286 /// c:4317 — `has_token("plain")` ASCII without tokens returns false.
7287 #[test]
7288 fn has_token_plain_ascii_returns_false() {
7289 assert!(!has_token("hello world"), "plain ASCII has no tokens");
7290 assert!(!has_token("abc123"), "alphanumeric has no tokens");
7291 }
7292
7293 /// c:4317 — `has_token` returns bool (compile-time type pin).
7294 #[test]
7295 fn has_token_returns_bool_type() {
7296 let _: bool = has_token("anything");
7297 }
7298
7299 /// c:2282 — `has_token` scans CHARS, not bytes.
7300 ///
7301 /// C scans bytes, which is safe there only because zsh metafies: a
7302 /// raw byte >= 0x80 is stored escaped, so a bare 0x84..=0xA1 in a
7303 /// metafied string is always a real token. zshrs keeps UTF-8 `str`
7304 /// and stores tokens as PUA CHARS (crate::token_char) (see `untokenize`),
7305 /// so a byte-scan mistakes a UTF-8 continuation byte for a token:
7306 /// `\u{2192}` encodes as E2 86 92 and both 0x86 and 0x92 answer
7307 /// `itok`. The false positive is unrecoverable downstream, because
7308 /// the char-based `untokenize`/`haswilds` correctly leave the word
7309 /// alone -- execcmd_exec's `while has_token(&args[0])` glob sweep
7310 /// (c:Src/exec.c:3315-3318) then never clears its condition.
7311 #[test]
7312 fn has_token_ignores_utf8_continuation_bytes() {
7313 let _g = crate::test_util::global_state_lock();
7314 for s in ["\u{2192}", "\u{2026}", "\u{2570}\u{2500}", "caf\u{e9}", "\u{21e3}0 B/s"] {
7315 assert!(
7316 !has_token(s),
7317 "{:?} holds no token CHAR (bytes {:02x?})",
7318 s,
7319 s.as_bytes()
7320 );
7321 }
7322 }
7323
7324 /// c:2282 — the flip side: every char whose UTF-8 encoding carries a
7325 /// byte inside the ITOK range 0x84..=0xA1, but whose scalar value is
7326 /// outside it, must NOT register. Sweeps the BMP rather than trusting
7327 /// a hand-picked list.
7328 #[test]
7329 fn has_token_only_fires_on_chars_in_the_itok_range() {
7330 let _g = crate::test_util::global_state_lock();
7331 for cp in [0x2192u32, 0x2026, 0x2570, 0x00e9, 0x21e3, 0x1f600, 0x0086, 0x0092] {
7332 let c = char::from_u32(cp).expect("valid scalar");
7333 let s = c.to_string();
7334 let in_range = (0xe184..=0xe1a1).contains(&cp);
7335 assert_eq!(
7336 has_token(&s),
7337 in_range && crate::ported::ztype_h::itok(cp as u8),
7338 "U+{:04X} (bytes {:02x?}) token-ness must follow the CHAR, not its bytes",
7339 cp,
7340 s.as_bytes()
7341 );
7342 }
7343 }
7344
7345 /// c:2282 — real token chars still register, so the char-scan did not
7346 /// simply disable detection.
7347 #[test]
7348 fn has_token_still_detects_real_token_chars() {
7349 let _g = crate::test_util::global_state_lock();
7350 assert!(has_token("x\u{e184}y"), "U+e184 is a token char");
7351 assert!(has_token("\u{e19c}"), "U+e19C is a token char");
7352 assert!(
7353 has_token("plain\u{e184}"),
7354 "a token char anywhere in the word counts"
7355 );
7356 }
7357
7358 /// c:4317 — `has_token` is pure (deterministic across calls).
7359 #[test]
7360 fn has_token_is_pure() {
7361 for s in ["", "abc", "abc def", "x\u{e184}y", "\u{e19c}"] {
7362 let first = has_token(s);
7363 for _ in 0..3 {
7364 assert_eq!(has_token(s), first, "has_token({:?}) must be pure", s);
7365 }
7366 }
7367 }
7368
7369 /// c:4205 — `untokenize` is pure (same input → same output).
7370 #[test]
7371 fn untokenize_is_pure() {
7372 for s in ["", "abc", "hello world", "no tokens here"] {
7373 let first = untokenize(s);
7374 for _ in 0..3 {
7375 assert_eq!(untokenize(s), first, "untokenize({:?}) must be pure", s);
7376 }
7377 }
7378 }
7379
7380 /// c:4205 — `untokenize` returns String (compile-time type pin).
7381 #[test]
7382 fn untokenize_returns_string_type() {
7383 let _: String = untokenize("anything");
7384 }
7385
7386 /// c:4205 — `untokenize("")` empty returns empty.
7387 #[test]
7388 fn untokenize_empty_returns_empty() {
7389 assert_eq!(untokenize(""), "", "empty → empty");
7390 }
7391
7392 // ── Lex parity: case-pattern nested-paren absorption ────────────────
7393 //
7394 // Pinned against upstream `Src/zsh dumptokens` output (the
7395 // `Src/Modules/zshrs_dump.c::bin_dumptokens` builtin). Catches
7396 // regressions of two C-faithful fixes:
7397 //
7398 // 1. lex.rs cmd_or_math (CMD_OR_MATH_CMD path) restoring the
7399 // `hungetc(')')` matching C lex.c:511-518. Without it, the
7400 // inner `)` consumed by `dquote_parse` is permanently dropped
7401 // from the input stream, so `((a|)b)` lex'd as 6 tokens
7402 // (one missing OUTPAR) instead of the correct 7.
7403 //
7404 // 2. parse.rs par_case setting `incmdpos = 0` before the zshlex
7405 // that advances past `;;` to the next arm — C parse.c:1391.
7406 // That's exercised by integration tests in tests/; lex-only
7407 // tests below verify the LEXER side.
7408 //
7409 // Expected sequences captured from:
7410 // $ Src/zsh -fc 'module_path=(Src/Modules); zmodload zsh/zshrs_dump;
7411 // dumptokens FILE'
7412 //
7413 // Surfaced via zinit.zsh:2946 `((add-|)fpath)` which failed with
7414 // `expected ')' in case pattern` before the lex+parse fixes.
7415
7416 /// Helper — drive the lexer from `input` and collect the resulting
7417 /// token kinds up to ENDINPUT.
7418 fn collect_lex_kinds(input: &str) -> Vec<lextok> {
7419 let _ = lex_init(input);
7420 let mut out = Vec::new();
7421 loop {
7422 ctxtlex();
7423 let t = tok();
7424 out.push(t);
7425 if t == ENDINPUT || t == LEXERR {
7426 return out;
7427 }
7428 }
7429 }
7430
7431 /// Lex parity: `((a|)b)` — verifies the inner OUTPAR survives
7432 /// cmd_or_math's rewind path. Expected sequence (from upstream
7433 /// `Src/zsh dumptokens`):
7434 /// INPAR INPAR STRING BAR OUTPAR STRING OUTPAR SEPER ENDINPUT
7435 /// Before the lex.rs c:511-518 fix, the inner OUTPAR was dropped
7436 /// and we emitted only 8 tokens instead of 9.
7437 #[test]
7438 fn lex_parity_nested_paren_alt_empty_tail() {
7439 let _g = crate::test_util::global_state_lock();
7440 let kinds = collect_lex_kinds("((a|)b)");
7441 assert_eq!(
7442 kinds,
7443 vec![
7444 INPAR_TOK, INPAR_TOK, STRING_LEX, BAR_TOK, OUTPAR_TOK, STRING_LEX, OUTPAR_TOK,
7445 ENDINPUT
7446 ],
7447 "((a|)b) must emit two OUTPAR tokens — the inner one is \
7448 what cmd_or_math's `hungetc(')')` restore (c:lex.c:511-518) \
7449 puts back after dquote_parse consumes it"
7450 );
7451 }
7452
7453 /// Lex parity: trivial `(a|)b` (no outer wrap) — sanity check that
7454 /// the cmd_or_math path is NOT triggered for single `(`, so the
7455 /// token stream is the natural one. Expected (from `Src/zsh
7456 /// dumptokens`):
7457 /// INPAR STRING BAR OUTPAR STRING SEPER ENDINPUT
7458 #[test]
7459 fn lex_parity_single_paren_alt_empty_tail() {
7460 let _g = crate::test_util::global_state_lock();
7461 let kinds = collect_lex_kinds("(a|)b");
7462 assert_eq!(
7463 kinds,
7464 vec![INPAR_TOK, STRING_LEX, BAR_TOK, OUTPAR_TOK, STRING_LEX, ENDINPUT],
7465 "(a|)b single-paren alt — five tokens, no `((` math probe"
7466 );
7467 }
7468
7469 /// Lex parity: `((a)b)` nested with non-alternation inner group.
7470 /// Expected:
7471 /// INPAR INPAR STRING OUTPAR STRING OUTPAR SEPER ENDINPUT
7472 /// Another regression catcher for cmd_or_math's rewind.
7473 #[test]
7474 fn lex_parity_nested_paren_simple_inner() {
7475 let _g = crate::test_util::global_state_lock();
7476 let kinds = collect_lex_kinds("((a)b)");
7477 assert_eq!(
7478 kinds,
7479 vec![INPAR_TOK, INPAR_TOK, STRING_LEX, OUTPAR_TOK, STRING_LEX, OUTPAR_TOK, ENDINPUT],
7480 "((a)b) — two OUTPARs preserved through cmd_or_math rewind"
7481 );
7482 }
7483
7484 // Bug #1021 assignment leg: is_valid_assignment_target must accept a
7485 // non-ASCII alphanumeric identifier under MULTIBYTE (c:lex.c:1233
7486 // itype_end(t, INAMESPC, 0) → wcsitype IIDENT → iswalnum), so `日=x` /
7487 // `café=y` lex as ENVSTRING assignments, not command words. ASCII behavior
7488 // is unchanged (the added clause fires only for non-ASCII).
7489 // The function validates the NAME part only (the token before `=`), so
7490 // inputs here are bare identifiers, not `name=value`.
7491 #[test]
7492 fn assignment_target_accepts_multibyte_name() {
7493 let _g = crate::test_util::global_state_lock();
7494 // CLI sets MULTIBYTE on at init; the unit harness leaves it unwritten.
7495 crate::ported::options::opt_state_set("multibyte", true);
7496 // Non-ASCII names are valid assignment targets.
7497 assert!(is_valid_assignment_target("日"));
7498 assert!(is_valid_assignment_target("café"));
7499 assert!(is_valid_assignment_target("変数"));
7500 assert!(is_valid_assignment_target("v日")); // ASCII-prefixed multibyte
7501 assert!(is_valid_assignment_target("日+")); // augment `+=` form (c:1241)
7502 // ASCII targets still valid.
7503 assert!(is_valid_assignment_target("foo"));
7504 assert!(is_valid_assignment_target("foo_bar"));
7505 // With POSIXIDENTIFIERS a multibyte name is NOT a valid target
7506 // (c:utils.c:4348 wcsitype returns 0); ASCII stays valid.
7507 crate::ported::options::opt_state_set("posixidentifiers", true);
7508 assert!(!is_valid_assignment_target("日"));
7509 assert!(is_valid_assignment_target("foo"));
7510 crate::ported::options::opt_state_set("posixidentifiers", false);
7511 }
7512
7513 // NOTE: full-case-construct lex parity is integration-only.
7514 // When the parser drives the lex via par_case (incmdpos=0 set at
7515 // c:Src/parse.c:1391 between arms), the `((alt|empty)tail)`
7516 // tokens emerge as the c:1322 absorbed-pattern STRING. When the
7517 // lexer runs standalone (no incmdpos manipulation between
7518 // statements), the same source produces a different absorption
7519 // pattern. Use the integration tests in `tests/case_pattern_*.rs`
7520 // and the regression run on `~/.zinit/bin/zinit.zsh` for the
7521 // parser-driven case — the standalone tests here pin the pure
7522 // lexer behavior that the cmd_or_math fix targets.
7523}
7524
7525// !!! WARNING: RUST-ONLY HELPER — NO DIRECT C COUNTERPART AS A
7526// FREE FUNCTION !!! Adapts the cited C pattern to the Rust
7527// pipeline. EXACT C untokenize (Src/utils.c:4205 + ztokens table,
7528// Src/lex.c:38): pure ITOK→ztokens mapping, Nularg dropped —
7529// WITHOUT the $'...'-decode deviation the pipeline's
7530// untokenize() above carries. Callers cite the C sites that
7531// need the exact mapping (lex.c:1716, subst.c:1543).
7532/// Bridge helper — EXACT semantics of C `untokenize(char *s)` from
7533/// `Src/exec.c:2077`: maps EVERY itok char to its ASCII original via
7534/// the ztokens table (`Src/lex.c:38`), dropping only Nularg (c:2089
7535/// `if (c != Nularg)`). No `$'...'` inline decode, no quote-marker
7536/// stripping.
7537///
7538/// Distinct from the two ported variants in `src/ported/lex.rs`:
7539/// - `untokenize` — substitution-stream variant: strips Snull/Dnull
7540/// and inline-decodes `$'...'` regions (see its doc block).
7541/// - `untokenize_preserve_quotes` — ztokens mapping EXCEPT Qstring
7542/// stays a raw marker (its callers need stringsubst's qt
7543/// detection) and Nularg is retained.
7544///
7545/// Callers porting C sites that literally call C `untokenize` on text
7546/// that is then RE-LEXED or RE-PARSED need this exact variant:
7547/// - `parsestrnoerr` (c:Src/lex.c:1716) — the dquote_parse re-lex
7548/// must see ASCII `$`/`'`/`"` so nested `${k}` re-tokenizes and
7549/// assoc-subscript quote chars survive to getarg's key lookup.
7550/// - `untok_and_escape` (c:Src/subst.c:1543) — paramsubst flag args
7551/// like `(j.$'\n'.)` must render as the literal text `$'\n'`
7552/// (zsh 5.9 `print -r ${(j.$'\n'.)a}` → `x$'\n'y`), not decode
7553/// to a bare newline. Bug #626 in docs/BUGS.md.
7554/// - `$words` / `$compwords` (c:Src/Zle/compcore.c:642-643
7555/// `for (p = clwords + aadd; *p; p++, q++) untokenize(*q = ztrdup(*p));`)
7556/// — completion functions read the word text and branch on its QUOTING.
7557/// `_zstyle` (Completion/Zsh/Command/_zstyle:325-333) tests whether the
7558/// context argument still carries its literal quotes to pick the style
7559/// table: a bare `:completion:*` selects `ctop=c` (114 names) while the
7560/// quoted `':completion:*'` selects `ctop=a-z` (176 names). Stripping
7561/// Snull/Dnull here silently truncated that completion by 62 names.
7562pub fn untokenize_ztokens(s: &str) -> String {
7563 let mut result = String::with_capacity(s.len());
7564 let chars: Vec<char> = s.chars().collect();
7565 let mut i = 0;
7566 while i < chars.len() {
7567 let c = chars[i];
7568 // Meta-pair passthrough — identical guard to `untokenize`
7569 // (lex.rs:5003-5008), and needed for the same reason. C is safe
7570 // here without a guard because C metafies BYTES: every imeta byte
7571 // (0x83..=0xa2) is stored as `Meta, byte ^ 0x20`, and that xor maps
7572 // 0x83..=0x9f -> 0xa3..=0xbf and 0xa0..=0xa2 -> 0x80..=0x82, so a C
7573 // continuation byte can never land in ITOK (0x84..=0xa1). zshrs
7574 // metafies at the CHARACTER level, so a raw high char C would leave
7575 // alone (0x81 is not imeta) is stored as the pair (U+0083, U+00A1)
7576 // — U+00A1 was Nularg before tokens moved to the PUA (crate::token_char).
7577 // Copy the pair verbatim and never untokenize the continuation.
7578 if c as u32 == 0x83 && i + 1 < chars.len() {
7579 result.push(c);
7580 result.push(chars[i + 1]);
7581 i += 2;
7582 continue;
7583 }
7584 let cu = c as u32;
7585 // c:Src/ztype.h:52 ITOK — Pound (0x84) ..= Nularg (0xa1).
7586 if (0xe184..=0xe1a1).contains(&cu) {
7587 // c:2089 — `if (c != Nularg) *p++ = ztokens[c - Pound];`
7588 if c != crate::ported::zsh_h::Nularg {
7589 let idx = (cu - 0xe184) as usize;
7590 result.push(crate::ported::lex::ztokens.chars().nth(idx).unwrap_or(c));
7591 }
7592 } else {
7593 result.push(c);
7594 }
7595 i += 1;
7596 }
7597 result
7598}