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