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