zsh/ported/parse.rs
1//! Zsh parser — direct port from zsh/Src/parse.c.
2//!
3//! Pulls tokens via the lex.rs free ported (zshlex/tok/tokstr) and
4//! builds an AST tree (relocated to src/extensions/zsh_ast.rs as a
5//! Rust-only IR) plus emits wordcode into ECBUF via the P9b/P9c
6//! pipeline. Follows the zsh grammar closely; productions match
7//! `par_*` in Src/parse.c.
8
9use super::lex::{
10 lextok, set_tok, AMPER, AMPERBANG, AMPOUTANG, BANG_TOK, BARAMP, BAR_TOK, CASE, COPROC, DAMPER,
11 DBAR, DINANG, DINANGDASH, DINBRACK, DINPAR, DOLOOP, DONE, DOUTANG, DOUTANGAMP, DOUTANGAMPBANG,
12 DOUTANGBANG, DOUTBRACK, DOUTPAR, DSEMI, ELIF, ELSE, ENDINPUT, ENVARRAY, ENVSTRING, ESAC, FI,
13 FOR, FOREACH, FUNC, IF, INANGAMP, INANG_TOK, INBRACE_TOK, INOUTANG, INOUTPAR, INPAR_TOK,
14 IS_REDIROP, LEXERR, LEX_HEREDOCS, NEWLIN, NOCORRECT, OUTANGAMP, OUTANGAMPBANG, OUTANGBANG,
15 OUTANG_TOK, OUTBRACE_TOK, OUTPAR_TOK, REPEAT, SELECT, SEMI, SEMIAMP, SEMIBAR, SEPER,
16 STRING_LEX, THEN, TIME, TRINANG, TYPESET, UNTIL, WHILE, ZEND,
17};
18use super::zsh_h::{
19 eprog, estate, funcdump, isset, redir, unset, wc_code, wordcode, Bang, Dash, Equals, Inang,
20 Outang, Tilde, ALIASFUNCDEF, COND_AND, COND_MOD, COND_MODI, COND_NOT, COND_NT, COND_OR,
21 COND_REGEX, COND_STRDEQ, COND_STREQ, COND_STRGTR, COND_STRLT, COND_STRNEQ, CSHJUNKIELOOPS,
22 EC_DUP, EC_NODUP, EF_HEAP, EF_REAL, EXECOPT, IGNOREBRACES, IS_DASH, MULTIFUNCDEF, OPT_ISSET,
23 PM_UNDEFINED, POSIXBUILTINS, REDIRF_FROM_HEREDOC, REDIR_APP, REDIR_APPNOW, REDIR_ERRAPP,
24 REDIR_ERRAPPNOW, REDIR_ERRWRITE, REDIR_ERRWRITENOW, REDIR_FROM_HEREDOC_MASK, REDIR_HEREDOC,
25 REDIR_HEREDOCDASH, REDIR_HERESTR, REDIR_INPIPE, REDIR_MERGEIN, REDIR_MERGEOUT, REDIR_OUTPIPE,
26 REDIR_READ, REDIR_READWRITE, REDIR_VARID_MASK, REDIR_WRITE, REDIR_WRITENOW, SHORTLOOPS,
27 SHORTREPEAT, WCB_COND, WCB_SIMPLE, WC_REDIR, WC_REDIR_FROM_HEREDOC, WC_REDIR_TYPE,
28 WC_REDIR_VARID, WC_SUBLIST_COPROC, WC_SUBLIST_NOT,
29};
30pub use crate::heredoc_ast::HereDoc;
31use crate::ported::lex::{
32 incasepat, incmdpos, incond, infor, input_slice, inredir, inrepeat, intypeset, isnewlin,
33 lex_init, lineno, noaliases, nocorrect, pos, set_incasepat, set_incmdpos, set_incond,
34 set_infor, set_inredir, set_inrepeat, set_intypeset, set_isnewlin, set_lineno, set_noaliases,
35 set_nocorrect, tok, tokfd, toklineno, tokstr, zshlex,
36};
37use crate::ported::signals::unqueue_signals;
38use crate::ported::utils::{errflag, zerr, zwarnnam, ERRFLAG_ERROR};
39use crate::prompt::{cmdpop, cmdpush};
40pub use crate::zsh_ast::{
41 CaseArm, CaseTerm, CaseTerminator, CompoundCommand, ForList, HereDocInfo, ListFlags, ListOp,
42 Redirect, RedirectOp, ShellCommand, ShellWord, SimpleCommand, SublistFlags, SublistOp,
43 VarModifier, ZshAssign, ZshAssignValue, ZshCase, ZshCommand, ZshCond, ZshFor, ZshFuncDef,
44 ZshIf, ZshList, ZshParamFlag, ZshPipe, ZshProgram, ZshRedir, ZshRepeat, ZshSimple, ZshSublist,
45 ZshTry, ZshWhile,
46};
47use crate::zsh_h::{
48 wc_bdata, CS_ALWAYS, CS_ARRAY, CS_CASE, CS_CMDAND, CS_CMDOR, CS_COND, CS_CURSH, CS_ELIF,
49 CS_ELSE, CS_ERRPIPE, CS_FOR, CS_FOREACH, CS_FUNCDEF, CS_IF, CS_IFTHEN, CS_PIPE, CS_REPEAT,
50 CS_SELECT, CS_SUBSH, CS_UNTIL, CS_WHILE, EF_RUN, WCB_ARITH, WCB_ASSIGN, WCB_CASE, WCB_CURSH,
51 WCB_END, WCB_FOR, WCB_FUNCDEF, WCB_IF, WCB_LIST, WCB_PIPE, WCB_REDIR, WCB_REPEAT, WCB_SELECT,
52 WCB_SUBLIST, WCB_SUBSH, WCB_TIMED, WCB_TRY, WCB_TYPESET, WCB_WHILE, WC_ASSIGN_ARRAY,
53 WC_ASSIGN_INC, WC_ASSIGN_NEW, WC_ASSIGN_SCALAR, WC_CASE_AND, WC_CASE_HEAD, WC_CASE_OR,
54 WC_CASE_TESTAND, WC_FOR_COND, WC_FOR_LIST, WC_FOR_PPARAM, WC_IF_ELIF, WC_IF_ELSE, WC_IF_HEAD,
55 WC_IF_IF, WC_PIPE_END, WC_PIPE_LINENO, WC_PIPE_MID, WC_REDIR_WORDS, WC_SELECT_LIST,
56 WC_SELECT_PPARAM, WC_SUBLIST_AND, WC_SUBLIST_END, WC_SUBLIST_FLAGS, WC_SUBLIST_OR,
57 WC_SUBLIST_SIMPLE, WC_SUBLIST_TYPE, WC_TIMED_EMPTY, WC_TIMED_PIPE, WC_WHILE_UNTIL,
58 WC_WHILE_WHILE, Z_ASYNC, Z_DISOWN, Z_END, Z_SIMPLE, Z_SYNC,
59};
60use serde::{Deserialize, Serialize};
61use std::fs::{self, File};
62use std::io::{Read, Seek, SeekFrom, Write};
63use std::os::unix::fs::MetadataExt;
64use std::path::Path;
65use std::sync::atomic::{AtomicUsize, Ordering};
66use std::sync::mpsc;
67use std::thread;
68use std::time::Duration;
69
70// Names lifted out of inside-fn `use` statements (PORT.md
71// 'no imports inside FNs ever').
72
73// Direct port of `Src/parse.c:287-289` grow-policy constants.
74const EC_INIT_SIZE: i32 = 256;
75
76// Pending-here-document list — direct port of `Src/parse.c:84
77// struct heredocs *hdocs;`. Per-parser file-static (bucket-1 in
78// PORT_PLAN.md): each worker thread parsing a separate program needs
79// its own pending-heredoc list. Saved/restored across nested parses
80// by `parse_context_save`/`parse_context_restore` (parse.c:299/337).
81thread_local! {
82 /// Port of file-static `struct heredocs *hdocs;` from `Src/parse.c:84`.
83 pub static HDOCS: std::cell::RefCell<Option<Box<crate::ported::zsh_h::heredocs>>>
84 = const { std::cell::RefCell::new(None) };
85}
86
87// Wordcode-buffer thread-locals — direct port of `Src/parse.c:269-285`
88// file-statics. Per-evaluator (bucket-1 in PORT_PLAN.md): each worker
89// thread parsing a separate program needs its own wordcode buffer.
90//
91// ECBUF: the wordcode array being built. C `Wordcode ecbuf`
92// (parse.c:275).
93// ECLEN: allocated entries in ECBUF (parse.c:269).
94// ECUSED: entries actually used so far (parse.c:271).
95// ECNPATS: count of patterns referenced by ECBUF (parse.c:273).
96// ECSOFFS / ECSSUB: byte offsets into the string region
97// (parse.c:279). ECSSUB subtracts substring overlap.
98// ECNFUNC: count of functions defined so far (parse.c:285).
99// ECSTRS_INDEX: dedup index for long strings — C uses a binary tree
100// of `struct eccstr` (zsh.h:836); the canonical Eccstr port exists
101// at zsh_h::eccstr but stays unused at runtime here. The HashMap
102// preserves the API contract (lookup by (nfunc, str) → offs) with
103// simpler ownership semantics.
104thread_local! {
105 /// `ECBUF` static.
106 pub static ECBUF: std::cell::RefCell<Vec<u32>> = std::cell::RefCell::new(Vec::new());
107 static ECLEN: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
108 static ECUSED: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
109 static ECNPATS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
110 static ECSOFFS: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
111 static ECSSUB: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
112 static ECNFUNC: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
113 static ECSTRS_INDEX: std::cell::RefCell<std::collections::HashMap<(i32, String), u32>>
114 = std::cell::RefCell::new(std::collections::HashMap::new());
115 /// C zsh's `eccstr` BST (parse.c:447). Port of `Eccstr ecstrs` —
116 /// a hashval-ordered binary search tree of long-strings for
117 /// dedup. Same cmp logic as C: nfunc, then hashval, then strcmp.
118 /// HashMap above is a fast-path lookup; this tree is the
119 /// C-fidelity walker that mirrors C's exact dedup-hit pattern
120 /// (including its quirks for hash-colliding content).
121 static ECSTRS_TREE: std::cell::RefCell<Option<Box<EccstrNode>>>
122 = const { std::cell::RefCell::new(None) };
123 /// Reverse index for `ecgetstr`: offs → owned string. Populated
124 /// at ecstrcode time so the consumer can recover the string from
125 /// the wordcode offs without walking the encode-time HashMap.
126 /// Stores the METAFIED BYTE form of each long-string, exactly
127 /// matching what C's strs region holds. `String` would not work
128 /// here because Rust strings carry UTF-8-encoded chars (e.g.
129 /// the Dash marker `\u{e19b}` UTF-8-encodes to two bytes
130 /// `\xc2 \x9b`) while C stores zsh markers as single bytes
131 /// (raw `\x9b`). Storing Vec<u8> lets us write byte-for-byte
132 /// what C writes after metafy.
133 pub static ECSTRS_REVERSE: std::cell::RefCell<std::collections::HashMap<u32, Vec<u8>>>
134 = std::cell::RefCell::new(std::collections::HashMap::new());
135}
136const EC_DOUBLE_THRESHOLD: i32 = 32768;
137const EC_INCREMENT: i32 = 1024;
138
139/// Port of `#define YYERROR(O) { tok = LEXERR; ecused = (O); return 0; }`
140/// from `Src/parse.c:87`. Only flags the error: the one diagnostic,
141/// "parse error near `<zshlextext>'", is printed by `yyerror` once the
142/// parse unwinds to `parse_list` / `par_event`.
143macro_rules! YYERROR {
144 ($o:expr) => {{
145 set_tok(LEXERR);
146 ECUSED.set(($o) as i32);
147 return 0;
148 }};
149}
150
151/// Port of `#define YYERRORV(O) { tok = LEXERR; ecused = (O); return; }`
152/// from `Src/parse.c:88` — the `void` twin of `YYERROR`.
153macro_rules! YYERRORV {
154 ($o:expr) => {{
155 set_tok(LEXERR);
156 ECUSED.set(($o) as i32);
157 return;
158 }};
159}
160
161/// Port of `#define COND_ERROR(X,Y)` from `Src/parse.c:89-96`:
162/// `zwarn(X,Y); herrflush(); if (noerrs != 2) errflag |= ERRFLAG_ERROR;
163/// YYERROR(ecused)`. The `%s` conversion is `nicezputs` (c:Src/utils.c:311),
164/// which untokenizes through `ztokens`; Rust pre-formats the message, so the
165/// macro applies that step to the argument (`nicedupstring`).
166macro_rules! COND_ERROR {
167 ($fmt:literal, $arg:expr) => {{
168 crate::ported::utils::zwarn(&format!(
169 $fmt,
170 crate::ported::utils::nicedupstring($arg)
171 ));
172 crate::ported::hist::herrflush();
173 if *crate::ported::utils::noerrs_lock().lock().unwrap() != 2 {
174 crate::ported::utils::errflag
175 .fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::SeqCst);
176 }
177 YYERROR!(ECUSED.get());
178 }};
179}
180
181/// Port of `parse_context_save()` from `Src/parse.c:295` — C signature `parse_context_save(struct parse_stack *ps, int toplevel)`.
182/// Snapshots the lexer-side file-statics plus the pending heredoc list,
183/// plus the wordcode-buffer state (`ecbuf`, `ecstrs`, `ecused`, ...).
184/// Saves Rust-only recursion counters too so nested parses get fresh limits.
185/// WARNING: param names don't match C — Rust=(ps) vs C=(ps, toplevel)
186pub fn parse_context_save(ps: &mut parse_stack) {
187 // parse.c:299 — `ps->hdocs = hdocs; hdocs = NULL;` — save the
188 // canonical C linked-list and clear it for the nested parse.
189 ps.hdocs = HDOCS.with_borrow_mut(|h| h.take());
190 // zshrs-only: save the parallel AST-glue Vec the same way.
191 // LEX_HEREDOCS carries terminator/strip_tabs/quoted metadata
192 // that has no C analog (C stores it implicitly via tokstr).
193 ps.lex_heredocs = LEX_HEREDOCS.with_borrow_mut(|v| std::mem::take(v));
194 // parse.c:302-310 — save lexer-side state.
195 ps.incmdpos = incmdpos();
196 // parse.c:303 — `ps->aliasspaceflag = aliasspaceflag;`. Mirrors
197 // lex.c LEX_ALIAS_SPACE_FLAG so nested parses preserve the
198 // HISTIGNORESPACE-via-alias state across parser re-entry.
199 ps.aliasspaceflag = crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.get());
200 ps.incond = incond();
201 ps.inredir = inredir();
202 ps.incasepat = incasepat();
203 ps.isnewlin = isnewlin();
204 ps.infor = infor();
205 ps.inrepeat_ = inrepeat();
206 ps.intypeset = intypeset();
207 // c:310-318 — the wordcode buffer being built. A nested parse's
208 // `init_parse` (c:513-522) resets all of it, so without this an
209 // enclosing wordcode parse (`zcompile` reaching a `$(…)`, whose
210 // skipcomm runs `parse_event(OUTPAR)`) lost everything it had emitted.
211 //
212 // !!! WARNING: COPIED, NOT HANDED OVER !!! C moves the buffer into `ps`
213 // and sets `ecbuf = NULL` (c:318); every C nested parse then starts
214 // with `init_parse`. The copy leaves the thread-locals readable for a
215 // Rust nested parse that skips `init_parse`, where C would dereference
216 // NULL; `parse_context_restore` overwrites them either way.
217 ps.eclen = ECLEN.get();
218 ps.ecused = ECUSED.get();
219 ps.ecnpats = ECNPATS.get();
220 ps.ecbuf = Some(ECBUF.with_borrow(|b| b.clone()));
221 ps.ecstrs = ECSTRS_TREE.with_borrow(|t| t.clone());
222 ps.ecstrs_index = ECSTRS_INDEX.with_borrow(|m| m.clone());
223 ps.ecstrs_reverse = ECSTRS_REVERSE.with_borrow(|m| m.clone());
224 ps.ecsoffs = ECSOFFS.get();
225 ps.ecssub = ECSSUB.get();
226 ps.ecnfunc = ECNFUNC.get();
227 set_incmdpos(true);
228 set_incond(0);
229 set_inredir(false);
230 set_incasepat(0);
231 set_infor(0);
232 set_inrepeat(0);
233 set_intypeset(false);
234}
235
236/// Port of `parse_context_restore()` from `Src/parse.c:326` — C signature `parse_context_restore(const struct parse_stack *ps, int toplevel)`.
237/// Inverse of `parse_context_save`. Restores lexer-side state +
238/// pending heredocs + Rust-only counters from `ps`, then clears
239/// `errflag & ERRFLAG_ERROR` per parse.c:354.
240/// WARNING: param names don't match C — Rust=(ps) vs C=(ps, toplevel)
241pub fn parse_context_restore(ps: &parse_stack) {
242 // parse.c:330-331 — free any in-progress wordcode buffer: the nested
243 // parse's buffer is overwritten by the saved one below.
244
245 // parse.c:333-352 — restore saved state.
246 // parse.c:337 — `hdocs = ps->hdocs;`
247 HDOCS.with_borrow_mut(|h| *h = ps.hdocs.clone());
248 // zshrs-only: restore the parallel AST-glue Vec.
249 LEX_HEREDOCS.with_borrow_mut(|v| *v = ps.lex_heredocs.clone());
250 set_incmdpos(ps.incmdpos);
251 // parse.c:334 — `aliasspaceflag = ps->aliasspaceflag;`.
252 crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.set(ps.aliasspaceflag));
253 set_incond(ps.incond);
254 set_inredir(ps.inredir);
255 set_incasepat(ps.incasepat);
256 set_isnewlin(ps.isnewlin);
257 set_infor(ps.infor);
258 set_inrepeat(ps.inrepeat_);
259 set_intypeset(ps.intypeset);
260 // c:330-331, c:345-352 — drop the nested buffer, put the saved one back.
261 ECLEN.set(ps.eclen);
262 ECUSED.set(ps.ecused);
263 ECNPATS.set(ps.ecnpats);
264 ECBUF.with_borrow_mut(|b| *b = ps.ecbuf.clone().unwrap_or_default());
265 ECSTRS_TREE.with_borrow_mut(|t| *t = ps.ecstrs.clone());
266 ECSTRS_INDEX.with_borrow_mut(|m| *m = ps.ecstrs_index.clone());
267 ECSTRS_REVERSE.with_borrow_mut(|m| *m = ps.ecstrs_reverse.clone());
268 ECSOFFS.set(ps.ecsoffs);
269 ECSSUB.set(ps.ecssub);
270 ECNFUNC.set(ps.ecnfunc);
271
272 // parse.c:354 — `errflag &= ~ERRFLAG_ERROR;` — clear the
273 // error flag so the outer parse sees a clean state.
274 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
275}
276
277/// Port of `ecadjusthere()` from `Src/parse.c:360` — C signature `ecadjusthere(int p, int d)`. Walk
278/// the pending-heredocs list and bump each `pc` by `d` if it's
279/// at or after position `p`. Called by `ecispace` / `ecdel` when
280/// wordcodes shift.
281pub fn ecadjusthere(p: usize, d: i32) {
282 // c:362 `struct heredocs *h;`
283 // c:364-366 — `for (h = hdocs; h; h = h->next) if (h->pc >= p)
284 // h->pc += d;`. par_redir records each pending here-document's
285 // wordcode position in HDOCS (c:2290-2296); when par_pipe/par_sublist
286 // later open a slot in front of it (a `cat <<X | cat` stage gets its
287 // WC_PIPE header inserted), setheredoc must still patch the redirection
288 // it recorded, not the word that slid into its old position.
289 HDOCS.with_borrow_mut(|head| {
290 let mut h = head.as_deref_mut();
291 while let Some(node) = h {
292 if node.pc >= p as i32 {
293 node.pc += d; // c:366
294 }
295 h = node.next.as_deref_mut();
296 }
297 });
298}
299
300// === AST tree relocated to src/extensions/zsh_ast.rs ===
301//
302// zsh C does NOT have an AST tree — it emits wordcode directly via
303// par_event/par_list/par_sublist/par_pipe/par_cmd/par_simple/etc.
304// (Src/parse.c:485-3000) into a flat `Wordcode ecbuf[]`. The Zsh*/
305// Shell* AST node types lived in this file as a Rust-only IR that
306// stands in for that wordcode.
307//
308// P9e (PORT_PLAN.md): the types moved to src/extensions/zsh_ast.rs
309// to make their Rust-only-extension nature explicit. The full P9c +
310// P9d rewrite (par_* emitting wordcode + vm_helper reading wordcode)
311// retires them entirely — until then, callers reach them via this
312// re-export.
313
314/// Port of `ecispace()` from `Src/parse.c:372` — C signature `ecispace(int p, int n)`. Insert `n`
315/// empty wordcode slots at position `p`, shifting later entries
316/// right, growing the buffer as needed, adjusting heredoc pointers.
317pub fn ecispace(p: usize, n: usize) {
318 // parse.c:376-381 — grow if needed.
319 let need = n as i32;
320 if (ECLEN.get() - ECUSED.get()) < need {
321 let cur = ECLEN.get();
322 let mut a = if cur < EC_DOUBLE_THRESHOLD {
323 cur
324 } else {
325 EC_INCREMENT
326 };
327 if need > a {
328 a = need;
329 }
330 ECBUF.with_borrow_mut(|buf| {
331 buf.resize((cur + a) as usize, 0);
332 });
333 ECLEN.set(cur + a);
334 }
335 // parse.c:382-385 — memmove p → p+n, gap of n.
336 let m = ECUSED.get() as usize - p;
337 if m > 0 {
338 ECBUF.with_borrow_mut(|buf| {
339 let needed = (ECUSED.get() as usize) + n;
340 if buf.len() < needed {
341 buf.resize(needed, 0);
342 }
343 for i in (0..m).rev() {
344 buf[p + n + i] = buf[p + i];
345 }
346 for i in 0..n {
347 buf[p + i] = 0;
348 }
349 });
350 }
351 // parse.c:386 — bump ecused by n.
352 ECUSED.set(ECUSED.get() + need);
353 // parse.c:387 — `ecadjusthere(p, n)`.
354 ecadjusthere(p, need);
355}
356
357/// Port of `ecadd()` from `Src/parse.c:397` — C signature `ecadd(wordcode c)`. Append `c` to
358/// the wordcode buffer with grow-on-demand, return the new index.
359pub fn ecadd(c: u32) -> usize {
360 // parse.c:399-405 — `if ((eclen - ecused) < 1) grow`.
361 if (ECLEN.get() - ECUSED.get()) < 1 {
362 let cur = ECLEN.get();
363 let a = if cur < EC_DOUBLE_THRESHOLD {
364 cur
365 } else {
366 EC_INCREMENT
367 };
368 ECBUF.with_borrow_mut(|buf| {
369 buf.resize((cur + a) as usize, 0);
370 });
371 ECLEN.set(cur + a);
372 }
373 let idx = ECUSED.get();
374 ECBUF.with_borrow_mut(|buf| {
375 if (idx as usize) >= buf.len() {
376 buf.resize((idx + 1) as usize, 0);
377 }
378 buf[idx as usize] = c;
379 });
380 ECUSED.set(idx + 1);
381 idx as usize
382}
383
384/// Port of `ecdel()` from `Src/parse.c:413` — C signature `ecdel(int p)`. Remove the
385/// wordcode at position `p`, shift later entries left by one,
386/// decrement ecused, adjust pending heredoc pointers.
387pub fn ecdel(p: usize) {
388 // parse.c:415-418 — memmove + decrement ecused.
389 let n = ECUSED.get() as usize - p - 1;
390 if n > 0 {
391 ECBUF.with_borrow_mut(|buf| {
392 for i in 0..n {
393 buf[p + i] = buf[p + i + 1];
394 }
395 });
396 }
397 ECUSED.set(ECUSED.get() - 1);
398 // parse.c:420 — `ecadjusthere(p, -1)`.
399 ecadjusthere(p, -1);
400}
401
402/// Port of `ecstrcode()` from `Src/parse.c:426` — C signature `ecstrcode(char *s)`. Encode a
403/// string into a single wordcode (short strings ≤4 bytes packed
404/// inline; longer strings get an offset into the deduped registry).
405///
406/// The long-string path stores the METAFIED bytes (matches what C's
407/// strs region contains): collapse Rust UTF-8 chars in 0x80..=0xff
408/// to single bytes, then apply zsh metafy (high bytes ≥ 0x83 →
409/// `Meta=0x83 + byte^0x20`). Length tracking (ECSOFFS) uses the
410/// metafied byte count — same as C `strlen(s) + 1` where C's `s`
411/// is already metafied at this point.
412pub fn ecstrcode(s: &str) -> u32 {
413 // Convert Rust char-form → C-byte form. zsh's metafy() at
414 // Src/utils.c only converts bytes flagged IMETA: 0x00, 0x83
415 // (Meta itself), and 0x84..=0xa2 (Pound..Marker, the lex
416 // markers). Other bytes 0x01..=0x82 and 0xa3..=0xff pass
417 // through unchanged. See utils.c:4195-4204 typtab init.
418 //
419 // Rust receives chars. Classify each:
420 // - a lex token char (PUA, crate::token_char) → its single C
421 // token byte, as C's lexer adds it
422 // - codepoint < 0x80 → ASCII, 1 byte unchanged
423 // - codepoint in [0x80..=0x82] or [0xa3..=0xff] → single
424 // non-imeta byte (user-input range); 1 byte unchanged
425 // - codepoint > 0xff → multi-byte UTF-8 source char (e.g.
426 // '━' = U+2501 = 0xe2 0x94 0x81). Metafy ONLY the bytes
427 // that fall in 0x83..=0xa2; pass others through. For '━':
428 // 0xe2 stays, 0x94 → 0x83 0xb4, 0x81 stays.
429 let mut c_bytes: Vec<u8> = Vec::with_capacity(s.len());
430 let imeta = |b: u8| -> bool { b == 0 || (0x83..=0xa2).contains(&b) };
431 let mut it = s.chars().peekable();
432 while let Some(ch) = it.next() {
433 // A `Meta` (U+0083) followed by a scalar in U+0080..=U+00FF is
434 // zshrs's char-level encoding of ONE raw byte — what `$'\xNN'`
435 // emits (lex.rs `getkeystring_dollar_quote`), what
436 // `script_bytes::decode_script_bytes` produces for a non-UTF-8
437 // script byte, and what `utils::unmetafy_str` reverses at the
438 // write boundary. U+0083 is never a token: the token range starts
439 // at `Pound` U+e184 (zsh_h.rs:159), so this cannot swallow a lex
440 // marker. Recover the byte and let the imeta rule below decide,
441 // exactly as C does for a byte read off the file: `caf\xe9`
442 // stores `63 61 66 e9` like zsh, where re-encoding the pair as
443 // text stored `63 61 66 83 c3 83 a9` and read back as garbage.
444 if ch == '\u{83}' {
445 if let Some(&n) = it.peek() {
446 let nu = n as u32;
447 if (0x80..=0xff).contains(&nu) {
448 it.next();
449 let b = (nu as u8) ^ 32;
450 if imeta(b) {
451 c_bytes.push(0x83);
452 c_bytes.push(b ^ 0x20);
453 } else {
454 c_bytes.push(b);
455 }
456 continue;
457 }
458 }
459 }
460 let cu = ch as u32;
461 if cu < 0x80 {
462 // ASCII — single byte unchanged.
463 c_bytes.push(cu as u8);
464 } else if let Some(b) = crate::token_char::token_byte(ch) {
465 // Lex token char: stored as its single C byte, as C's lexer
466 // adds it. Real U+0083..=U+00A2 text is not a token and takes
467 // the metafy branch below like any other data.
468 c_bytes.push(b);
469 } else {
470 // User-input char: encode UTF-8 then metafy imeta bytes.
471 // For chars 0x80..=0xff (like 'º' U+00BA), UTF-8 gives
472 // 2 bytes (e.g. `0xc2 0xba`) — zsh's lex reads these as
473 // raw bytes from input and metafy passes 0xc2 / 0xba
474 // through (both NOT imeta).
475 let mut tmp = [0u8; 4];
476 for &b in ch.encode_utf8(&mut tmp).as_bytes() {
477 if imeta(b) {
478 c_bytes.push(0x83);
479 c_bytes.push(b ^ 0x20);
480 } else {
481 c_bytes.push(b);
482 }
483 }
484 }
485 }
486 // c:`has_token` (Src/utils.c:2282) → `itok(*s)` → `typtab[c] & ITOK`.
487 // ITOK is set for bytes `Pound..=Nularg` (0x84..=0xa1) per
488 // Src/utils.c:4198 (`for (t0=Pound; t0<=LAST_NORMAL_TOK; t0++)
489 // typtab[t0]|=ITOK`) plus :4200 (`for (t0=Snull; t0<=Nularg; t0++)
490 // typtab[t0]|=ITOK|IMETA|INULL`). Pound=0x84 Bang=0x9c (last normal),
491 // Snull=0x9d..Nularg=0xa1. Meta=0x83 has IMETA but NOT ITOK.
492 let t = c_bytes.iter().any(|&b| (0x84..=0xa1).contains(&b));
493 let l = c_bytes.len() + 1; // include NUL terminator
494 if l <= 4 {
495 // parse.c:436-445 — short-string inline pack. Uses raw C-bytes
496 // (NOT metafied — the inline packing stores 1 byte per slot).
497 let mut c: u32 = if t { 3 } else { 2 };
498 match l {
499 4 => {
500 c |= (c_bytes[2] as u32) << 19;
501 c |= (c_bytes[1] as u32) << 11;
502 c |= (c_bytes[0] as u32) << 3;
503 }
504 3 => {
505 c |= (c_bytes[1] as u32) << 11;
506 c |= (c_bytes[0] as u32) << 3;
507 }
508 2 => {
509 c |= (c_bytes[0] as u32) << 3;
510 }
511 1 => {
512 // parse.c:443 — empty string special case.
513 c = if t { 7 } else { 6 };
514 }
515 _ => {}
516 }
517 c
518 } else {
519 // parse.c:447-466 — long string. Port of C's eccstr BST walk
520 // exactly: walk the tree comparing nfunc, then hashval, then
521 // strcmp on bytes. Return offs on full match; insert new
522 // leaf otherwise. Matches C's exact dedup-hit pattern
523 // (which is content-dependent — hash collisions and the
524 // lazy short-circuit cmp chain make the tree shape determine
525 // whether matching nodes are reachable).
526 // hasher is byte-by-byte polynomial (hashtable.c:86); pass
527 // c_bytes via from_utf8_unchecked so non-UTF-8 zsh marker
528 // bytes feed straight in. SAFETY: hasher only iterates
529 // `.bytes()` — no UTF-8 validity assumed.
530 let val =
531 crate::ported::hashtable::hasher(unsafe { std::str::from_utf8_unchecked(&c_bytes) });
532 let nfunc = ECNFUNC.get();
533 let found_offs = ECSTRS_TREE.with_borrow_mut(|root| {
534 // Walk the tree. At each node, if all 3 cmps == 0,
535 // return the node's offs. Otherwise descend left/right
536 // by the first non-zero cmp's sign.
537 let mut cur: &mut Option<Box<EccstrNode>> = root;
538 loop {
539 let p = match cur.as_mut() {
540 Some(p) => p,
541 None => break None,
542 };
543 // c:448 — `cmp = p->nfunc - ecnfunc`
544 let mut cmp = (p.nfunc as i64) - (nfunc as i64);
545 if cmp == 0 {
546 // c:448 — `&& !(cmp = (long)p->hashval - (long)val)`
547 // C does `(int)(p->hashval - val)` — unsigned 32-bit
548 // subtraction wraps, then cast to int. Use
549 // wrapping_sub + as i32 to match the bit pattern.
550 cmp = (p.hashval.wrapping_sub(val) as i32) as i64;
551 if cmp == 0 {
552 // c:448 — `&& !(cmp = strcmp(p->str, s))`
553 cmp = match p.str.as_slice().cmp(c_bytes.as_slice()) {
554 std::cmp::Ordering::Less => -1,
555 std::cmp::Ordering::Equal => 0,
556 std::cmp::Ordering::Greater => 1,
557 };
558 if cmp == 0 {
559 // c:450 — `return p->offs;`
560 break Some(p.offs);
561 }
562 }
563 }
564 // c:452 — `pp = (cmp < 0 ? &p->left : &p->right);`
565 cur = if cmp < 0 { &mut p.left } else { &mut p.right };
566 }
567 });
568 if let Some(offs) = found_offs {
569 return offs;
570 }
571 // c:462 — `p->offs = ((ecsoffs - ecssub) << 2) | (t ? 1 : 0);`
572 let offs = (((ECSOFFS.get() - ECSSUB.get()) as u32) << 2) | if t { 1 } else { 0 };
573 // c:463 — `p->aoffs = ecsoffs;` (absolute write position).
574 let aoffs = ECSOFFS.get() as u32;
575 // c:457-465 — insert new node at the NULL slot the walk
576 // terminated at. Encode the walk path as a Vec<bool> of
577 // left/right turns (true = right), then re-descend to
578 // insert. Borrow-checker friendly: a single mutable walk
579 // that either finds an existing node (descend) or fills
580 // the empty slot (return).
581 let stored = c_bytes.clone();
582 let stored_len = stored.len();
583 let new_node = Box::new(EccstrNode {
584 left: None,
585 right: None,
586 str: stored.clone(),
587 offs,
588 aoffs,
589 nfunc,
590 hashval: val,
591 });
592 ECSTRS_TREE.with_borrow_mut(|root| {
593 // Build the path first (immutable-walk; safe because we
594 // only ever go further down).
595 let mut path: Vec<bool> = Vec::new();
596 {
597 let mut cur: &Option<Box<EccstrNode>> = root;
598 while let Some(p) = cur.as_ref() {
599 let mut cmp = (p.nfunc as i64) - (nfunc as i64);
600 if cmp == 0 {
601 // C does `(int)(p->hashval - val)` — unsigned 32-bit
602 // subtraction wraps, then cast to int. Use
603 // wrapping_sub + as i32 to match the bit pattern.
604 cmp = (p.hashval.wrapping_sub(val) as i32) as i64;
605 if cmp == 0 {
606 cmp = match p.str.as_slice().cmp(c_bytes.as_slice()) {
607 std::cmp::Ordering::Less => -1,
608 std::cmp::Ordering::Equal => 0,
609 std::cmp::Ordering::Greater => 1,
610 };
611 }
612 }
613 let go_right = cmp >= 0;
614 path.push(go_right);
615 cur = if go_right { &p.right } else { &p.left };
616 }
617 }
618 // Descend mutably along the recorded path and assign at
619 // the NULL leaf.
620 let mut cur: &mut Option<Box<EccstrNode>> = root;
621 for turn in path {
622 let p = cur.as_mut().expect("path matches walk");
623 cur = if turn { &mut p.right } else { &mut p.left };
624 }
625 *cur = Some(new_node);
626 });
627 // Also keep the existing reverse index (offs → bytes) for
628 // ecgetstr_wordcode and copy_ecstr — they read flat by offs.
629 ECSTRS_REVERSE.with_borrow_mut(|m| {
630 m.insert(offs, stored);
631 });
632 let _ = l;
633 ECSOFFS.set(ECSOFFS.get() + (stored_len + 1) as i32);
634 offs
635 }
636}
637
638/// Port of `init_parse_status()` from `Src/parse.c:491`.
639/// Initialize parser status. Clears the per-parse-call lexer flags
640/// so a fresh parse starts from cmd-position with no nesting
641/// state inherited from a prior parse.
642///
643/// Previously the Rust port omitted `inrepeat_ = 0` at c:501.
644/// `inrepeat_` is the `repeat N <body>` parse-state counter that
645/// the lexer toggles in 3 phases (1 → 2 → 3 → 0). Without the
646/// reset, a fresh parse called after an in-flight `repeat`
647/// command would inherit the stale counter and silently misread
648/// the next token as a body of an already-completed repeat.
649pub fn init_parse_status() {
650 // c:491
651 // parse.c:500-502 — `incasepat = incond = inredir = infor =
652 // intypeset = 0; inrepeat_ = 0; incmdpos = 1;`
653 set_incasepat(0); // c:500
654 set_incond(0); // c:500
655 set_inredir(false); // c:500
656 set_infor(0); // c:500
657 set_intypeset(false); // c:500
658 set_inrepeat(0); // c:501 inrepeat_ = 0
659 set_incmdpos(true); // c:502
660}
661
662/// Port of `init_parse()` from `Src/parse.c:509`.
663/// Initialize parser for a fresh parse. C source allocates a
664/// fresh wordcode buffer (ecbuf) sized EC_INIT_SIZE, resets the
665/// per-parse-call counters, and calls init_parse_status. zshrs
666/// has no flat wordcode buffer (AST is built inline) so this
667/// function reduces to init_parse_status + recursion_depth/
668/// global_iterations clear.
669pub fn init_parse() {
670 // parse.c:513-520 — `ecbuf = (Wordcode) zalloc(EC_INIT_SIZE *
671 // sizeof(wordcode)); eclen = EC_INIT_SIZE; ecused = 0;
672 // ecnpats = 0; ecstrs = NULL; ecsoffs = ecnfunc = 0;
673 // ecssub = 0;`. P9b — initialize the per-evaluator wordcode
674 // buffer for this parse call. zshrs uses thread-local
675 // statics declared at file scope (parse.rs:25-50).
676 ECBUF.with_borrow_mut(|buf| {
677 buf.clear();
678 buf.resize(EC_INIT_SIZE as usize, 0);
679 });
680 ECLEN.set(EC_INIT_SIZE);
681 ECUSED.set(0);
682 ECNPATS.set(0);
683 ECSOFFS.set(0);
684 ECSSUB.set(0);
685 ECNFUNC.set(0);
686 ECSTRS_INDEX.with_borrow_mut(|m| m.clear());
687 ECSTRS_REVERSE.with_borrow_mut(|m| m.clear());
688 ECSTRS_TREE.with_borrow_mut(|t| *t = None);
689
690 // parse.c:522 — `init_parse_status();`
691 init_parse_status();
692}
693
694/// Port of `copy_ecstr()` from `Src/parse.c:537` — C signature `copy_ecstr(Eccstr s, char *p)`.
695/// Walks the BST and writes each entry to `p[s->aoffs..]` matching
696/// C's recursive in-order traversal exactly. The old impl used the
697/// `ECSTRS_REVERSE` HashMap keyed by `offs` (= ecssub-relative
698/// wordcode-encoded offset), which collides across funcdef scopes:
699/// a string at relative offs=0 inside funcdef A and another at
700/// relative offs=0 inside funcdef B share the same key, so one
701/// overwrites the other.
702pub fn copy_ecstr(_table: &std::collections::HashMap<u32, Vec<u8>>, p: &mut [u8]) {
703 // c:537-544 — walk eccstr BST recursively, writing each node's
704 // str at p[node->aoffs..node->aoffs + strlen + 1] (NUL-terminated).
705 ECSTRS_TREE.with_borrow(|root| {
706 copy_ecstr_walk(root, p);
707 });
708}
709
710/// Port of `bld_eprog()` from `Src/parse.c:547` — C signature `bld_eprog(int heap)`. Finalizes
711/// the in-build `ECBUF`/`ECSTRS`/`ECNPATS` state into an `Eprog`.
712/// Resets the build state so a new parse can start.
713pub fn bld_eprog(heap: bool) -> eprog {
714 // c:547
715
716 // c:555 — emit WC_END opcode. `WCB_END` is `WC_END_DEFAULT` (0).
717 ecadd(0);
718
719 let ecused = ECUSED.with(|c| c.get()) as usize;
720 let ecnpats = ECNPATS.with(|c| c.get()) as usize;
721 let ecsoffs = ECSOFFS.with(|c| c.get()) as usize;
722
723 // c:557-559 — `ret->len = ((ecnpats * sizeof(Patprog)) +
724 // (ecused * sizeof(wordcode)) +
725 // ecsoffs);`
726 // sizeof(Patprog) = sizeof(struct patprog *) = pointer size.
727 // On 64-bit targets that's 8, on 32-bit that's 4. C's eprog
728 // ->len is the canonical value for parity tests, so we use
729 // the same arithmetic.
730 let prog_bytes = ecused * 4; // sizeof(wordcode) = 4
731 let len = (ecnpats * size_of::<*const u8>()) + prog_bytes + ecsoffs;
732
733 // Snapshot the wordcode buffer + string table.
734 let prog_words: Vec<u32> = ECBUF.with(|c| c.borrow()[..ecused].to_vec());
735 let mut strs_bytes = vec![0u8; ecsoffs];
736 ECSTRS_REVERSE.with(|c| copy_ecstr(&c.borrow(), &mut strs_bytes));
737
738 // c:566 — store strs as raw bytes via from_utf8_unchecked so
739 // single-byte zsh markers (e.g. Dash 0x9b) survive intact.
740 // `String::from_utf8_lossy` would replace them with U+FFFD
741 // (`\xef\xbf\xbd`), breaking byte-for-byte parity with C's
742 // strs region. SAFETY: downstream consumers of `eprog.strs`
743 // index by byte offset (per the wordcode `(offs >> 2)` offset
744 // encoding) and call `.as_bytes()` — they never iterate as
745 // chars or rely on UTF-8 validity, so storing non-UTF-8 bytes
746 // in a String is safe in practice. C zsh's strs is `char *`
747 // with the same byte-not-char semantics.
748 let strs_string = unsafe { String::from_utf8_unchecked(strs_bytes) };
749 let ret = eprog {
750 flags: if heap { EF_HEAP } else { EF_REAL }, // c:570
751 len: len as i32, // c:559
752 npats: ecnpats as i32, // c:561
753 nref: if heap { -1 } else { 1 }, // c:562
754 pats: Vec::new(), // c:563 dummy_patprog
755 prog: prog_words, // c:565
756 strs: Some(strs_string),
757 shf: None,
758 dump: None,
759 // The pool `ecstrcode` (c:426) just built is METAFIED — that
760 // function encodes every IMETA byte as `Meta` + `b ^ 0x20`
761 // (parse.rs:363-389) and the wordcode offsets it hands back
762 // count metafied bytes. Flagging the pool clean made
763 // `ecgetstr` skip `unmetafy`, so any multibyte glyph with a
764 // continuation byte in 0x83..=0xa2 came back mangled
765 // (`—` E2 80 94 → stored E2 80 83 B4 → decoded U+2003 + U+00B4).
766 strs_metafied: true,
767 };
768
769 // c:577 — free ecbuf so next parse starts fresh.
770 ECBUF.with(|c| c.borrow_mut().clear());
771 ECLEN.with(|c| c.set(0));
772 ECUSED.with(|c| c.set(0));
773 ECNPATS.with(|c| c.set(0));
774 ECSOFFS.with(|c| c.set(0));
775 ECSTRS_INDEX.with(|c| c.borrow_mut().clear());
776 ECSTRS_REVERSE.with(|c| c.borrow_mut().clear());
777 ECSTRS_TREE.with(|t| *t.borrow_mut() = None);
778
779 ret
780}
781
782/// Port of `empty_eprog()` from `Src/parse.c:584` — C signature `empty_eprog(Eprog p)`. C
783/// body: `return (!p || !p->prog || *p->prog == WCB_END());` —
784/// the eprog is empty when its prog buffer is missing or the
785/// first wordcode is the WC_END marker. Used by signal handlers
786/// (`Src/signals.c:712`) to short-circuit a trap that resolves to
787/// an empty program.
788pub fn empty_eprog(p: &eprog) -> bool {
789 p.prog.is_empty() || p.prog[0] == WCB_END()
790}
791
792/// Port of `clear_hdocs()` from `Src/parse.c:591` — C signature `clear_hdocs(void)`.
793/// Clear pending here-document list. The C version walks
794/// `hdocs` and frees each node; Rust drops the `Box<heredocs>`
795/// chain automatically when the head is replaced with None.
796pub fn clear_hdocs() {
797 // c:591
798 // c:593-598 — for (p = hdocs; p; p = n) { n = p->next; zfree(p); }
799 // c:599 — hdocs = NULL;
800 HDOCS.with_borrow_mut(|h| *h = None);
801 // zshrs-only: also drop the parallel AST-glue Vec. No C
802 // analog — LEX_HEREDOCS is Rust-only working-set state.
803 LEX_HEREDOCS.with_borrow_mut(|v| v.clear());
804}
805
806/// Port of `parse_event()` from `Src/parse.c:614` (body c:615-631).
807/// Top-level parse-event entry. Reads one event from the lexer (a
808/// sublist optionally followed by SEPER/AMPER/AMPERBANG) and
809/// returns the resulting ZshProgram.
810///
811/// `endtok` is the token that terminates the event — usually
812/// ENDINPUT, but for command-style substitutions the closing
813/// `)` (zsh's CMD_SUBST_CLOSE).
814///
815/// zshrs port note: zsh's parse_event returns an `Eprog` (heap-
816/// allocated wordcode program). zshrs returns a `ZshProgram`
817/// (AST root). Same role at the parse-output boundary.
818pub fn parse_event(endtok: lextok) -> Option<ZshProgram> {
819 // parse.c:616-619 — reset state and prime the lexer.
820 set_tok(ENDINPUT);
821 set_incmdpos(true);
822 // parse.c:618 — `aliasspaceflag = 0;`. Fresh event: discard any
823 // alias-space carry-over from a prior parse so HISTIGNORESPACE
824 // doesn't suppress the next entered command line.
825 crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.set(0));
826
827 // parse.c:626-628 — a sub-parse for a substitution (endtok !=
828 // ENDINPUT, e.g. `par_subsh` with OUTPAR) doesn't need its own
829 // eprog; par_event drives it and the caller discards the program.
830 if endtok != ENDINPUT {
831 zshlex();
832 init_parse();
833 if !par_event(endtok) {
834 clear_hdocs();
835 return None;
836 }
837 return Some(ZshProgram { lists: Vec::new() });
838 }
839
840 // parse.c:616-630 — top-level event parse. The lexer pulls
841 // characters straight from SHIN via hgetc → ingetc → shingetchar
842 // (input.c) and accumulates each into its token buffer with add();
843 // no LEX_INPUT seeding. `zshlex()` (already primed by the shared
844 // entry below) reads the first token; parse_program_until drives the
845 // grammar over the event, refilling input one line at a time as
846 // ingetc's inputline() arm fires.
847 zshlex();
848 init_parse();
849 if tok() == ENDINPUT {
850 return None; // EOF — loop() terminates.
851 }
852 // parse.c:639-643 — skip leading command separators; a SEPER on a fresh
853 // newline at the top level (endtok == ENDINPUT) is an EMPTY command — the
854 // bare-Enter case — so return None WITHOUT pulling another input line.
855 // Missing this, `parse_program_until` treated the trailing newline as an
856 // incomplete list and read a continuation line, so every empty Enter (and
857 // the line after it) rendered the PS2 `>` prompt instead of PS1.
858 while tok() == SEPER {
859 if isnewlin() > 0 {
860 clear_hdocs();
861 return None;
862 }
863 zshlex();
864 }
865 if tok() == ENDINPUT {
866 return None;
867 }
868 // single_event = TRUE: one logical command line per parse_event, the
869 // faithful port of C par_event's endtok==ENDINPUT top-level mode
870 // (parse.c:635). loop() reads+executes ONE event at a time so `-v`/`-x`
871 // echo and runtime `setopt verbose`/`xtrace` interleave with execution
872 // exactly as zsh does — whole-program parsing can't (it finishes
873 // parsing before any execution).
874 //
875 // Enabling this required isolating the runtime nested-parse machinery
876 // from the now-mid-stream outer reader (the parse/execute interleave
877 // exposed three split-global hazards that whole-program mode hid):
878 // * cmd-subst / proc-sub / eval / source bodies re-parse via
879 // vm_helper::parse_isolated (the AST-path bridge for C's
880 // parse_string, exec.c:283) — it sets `strin` so a drained nested
881 // buffer EOFs instead of STEALING the outer reader's next SHIN line.
882 // * strinbeg/strinend now bump BOTH `strin` copies (hist.rs + the
883 // input.rs one ingetc checks); lexinit zeroes BOTH `lexstop` copies
884 // — C has a single global for each, zshrs split them.
885 // * loop() saves/restores LEX_LINENO across execode (init.rs), the
886 // execlist `oldlineno` discipline (exec.c:28/292), so per-statement
887 // SET_LINENO doesn't freeze `$LINENO` for the next event.
888 let mut program = parse_program_until(None, true);
889 // c:622-625 `if (!par_event(endtok)) { clear_hdocs(); return NULL; }` —
890 // par_event sets `tok = LEXERR` on a syntax error (c:670-682) and returns
891 // 0 (c:686-689), so a failed event yields NULL even when lists before the
892 // error parsed. loop() then sees "no program" + LEXERR and, reading a
893 // shell's stdin at top level, keeps reading (c:Src/init.c:171).
894 if program.lists.is_empty() || tok() == LEXERR {
895 clear_hdocs();
896 return None;
897 }
898 // parse.c:630 bld_eprog post-pass — wire heredoc bodies collected
899 // by zshlex into the ZshRedir nodes (mirror of `parse()`).
900 let bodies: Vec<HereDocInfo> = LEX_HEREDOCS
901 .with_borrow(|v| v.clone())
902 .into_iter()
903 .map(|h| HereDocInfo {
904 content: h.content,
905 terminator: h.terminator,
906 quoted: h.quoted,
907 })
908 .collect();
909 if !bodies.is_empty() {
910 fill_heredoc_bodies(&mut program, &bodies);
911 }
912 Some(program)
913}
914
915/// Port of `par_event()` from `Src/parse.c:635`.
916///
917/// ```text
918/// event : ENDINPUT
919/// | SEPER
920/// | sublist [ SEPER | AMPER | AMPERBANG ]
921/// ```
922///
923/// Returns 1/true when an event (or the closing `endtok`) was parsed. A
924/// sublist followed by anything but a separator, `endtok` or ENDINPUT is
925/// a syntax error: `tok` becomes LEXERR and the error is reported here.
926///
927/// !!! WARNING: AST, NOT WORDCODE !!! C emits the list code with
928/// `ecadd`/`set_list_code` (c:649-668) and patches `Z_END` into the last
929/// one (c:688); this port drives the AST `par_sublist` and keeps only the
930/// token walk and the error handling. Its one caller, `parse_event` with a
931/// closing token (skipcomm's `parse_event(OUTPAR)`), throws the program
932/// away, as C does with `dummy_eprog` (c:626-629).
933pub fn par_event(endtok: lextok) -> bool {
934 // c:639-643
935 while tok() == SEPER {
936 if isnewlin() > 0 && endtok == ENDINPUT {
937 return false;
938 }
939 zshlex();
940 }
941 // c:644-647
942 if tok() == ENDINPUT {
943 return false;
944 }
945 if tok() == endtok {
946 return true;
947 }
948
949 // c:651-669
950 let mut r = false;
951 if par_sublist().is_some() {
952 let t = tok();
953 if t == ENDINPUT || t == endtok {
954 r = true; // c:652-654
955 } else if t == SEPER {
956 // c:655-659
957 if isnewlin() <= 0 || endtok != ENDINPUT {
958 zshlex();
959 }
960 r = true;
961 } else if t == AMPER || t == AMPERBANG {
962 zshlex(); // c:660-668
963 r = true;
964 }
965 }
966 if !r {
967 // c:670-682
968 set_tok(LEXERR);
969 if errflag.load(Ordering::Relaxed) != 0 {
970 yyerror(0);
971 return false;
972 }
973 yyerror(1);
974 crate::ported::hist::herrflush();
975 if *crate::ported::utils::noerrs_lock().lock().unwrap() != 2 {
976 errflag.fetch_or(ERRFLAG_ERROR, Ordering::Relaxed);
977 }
978 return false;
979 }
980 // c:684-691
981 if !par_event(endtok) {
982 return errflag.load(Ordering::Relaxed) == 0;
983 }
984 true
985}
986
987/// Port of `parse_list()` from `Src/parse.c:697` — C signature `parse_list(void)`. C-shape entry
988/// point: drives `par_list` and finalizes via `bld_eprog`. Returns
989/// `None` on syntax error.
990pub fn parse_list() -> Option<eprog> {
991 // c:697
992 set_tok(ENDINPUT);
993 init_parse();
994 zshlex();
995 // c:Src/parse.c:705 — `par_list(&c);` emits wordcode for the
996 // full multi-statement list (its goto-rec loop walks all
997 // SEPER-separated sublists). The Rust AST par_list() emits
998 // NOTHING to the wordcode buffer (only builds the AST), so
999 // bld_eprog returned an empty program AND tok stayed at
1000 // SEPER, tripping the syntax-error check below for any
1001 // \`cmd; cmd\` body.
1002 //
1003 // Route through par_event_wordcode (the wordcode emitter,
1004 // lines 4395+) which mirrors C's par_list loop semantics
1005 // and populates the wordcode buffer that bld_eprog reads.
1006 let _start = par_event_wordcode();
1007 if tok() != ENDINPUT {
1008 clear_hdocs();
1009 set_tok(LEXERR);
1010 // c:Src/parse.c:708 — `yyerror(0);`. C-faithful invocation;
1011 // the message format ("parse error near `X'") is built
1012 // inside yyerror from zshlextext/tokstr().
1013 yyerror(0);
1014 return None;
1015 }
1016 Some(bld_eprog(false))
1017}
1018
1019/// Port of `parse_cond()` from `Src/parse.c:722` — C signature `parse_cond(void)`. Only used by
1020/// `bin_test`/`bin_bracket` for `/bin/test`/`[` compat — the
1021/// `condlex` global must already point at `testlex` before entry.
1022pub fn parse_cond() -> Option<eprog> {
1023 // c:722
1024 init_parse();
1025 // `par_cond_top` is C's wordcode `par_cond` (c:2409); the bare name is
1026 // taken by the `[[ ]]` AST parser in this file.
1027 if par_cond_top() == 0 {
1028 clear_hdocs();
1029 return None;
1030 }
1031 Some(bld_eprog(true))
1032}
1033
1034// ============================================================
1035// Wordcode emission helpers (parse.c private helpers)
1036//
1037// Direct ports of zsh's wordcode-emission helpers in parse.c.
1038// These write u32 opcodes into a flat `ecbuf` array thread-local
1039// via ecadd / ecdel / ecispace / ecstrcode and friends. The
1040// par_*_wordcode family at parse.rs:1700-3500 walks the lex
1041// stream and emits a real wordcode buffer here.
1042//
1043// (The AST tree built by par_program / par_simple / etc. is a
1044// separate path used by fusevm; see compile_zsh.rs for the AST
1045// → fusevm-bytecode compiler.)
1046// ============================================================
1047
1048/// Patch a list-placeholder wordcode with its actual opcode +
1049/// jump distance. Direct port of zsh/Src/parse.c:738
1050/// `set_list_code`. zsh emits an `ecadd(0)` placeholder before
1051/// par_sublist runs, then comes back through set_list_code to
1052/// rewrite the slot with WCB_LIST(type, distance) once the
1053/// sublist's final length is known.
1054///
1055/// Port of `set_list_code()` from `Src/parse.c:738` — C signature `set_list_code(int p, int type, int cmplx)`.
1056/// Patches the WCB_LIST header at `p` based on
1057/// whether the sublist body is simple (single command, no
1058/// pipeline) and Z_SYNC/Z_END — emits the Z_SIMPLE-optimized
1059/// header when possible, otherwise the plain WCB_LIST(type, 0).
1060pub fn set_list_code(p: usize, type_code: i32, cmplx: bool) {
1061 let _ = wc_bdata;
1062 // c:740 — `if (!cmplx && (type == Z_SYNC || type == (Z_SYNC | Z_END))
1063 // && WC_SUBLIST_TYPE(ecbuf[p+1]) == WC_SUBLIST_END)`
1064 let sublist_code = ECBUF.with_borrow(|b| b.get(p + 1).copied().unwrap_or(0));
1065 let z = type_code;
1066 let qualifies = !cmplx
1067 && (z == Z_SYNC || z == (Z_SYNC | Z_END))
1068 && WC_SUBLIST_TYPE(sublist_code) == WC_SUBLIST_END;
1069 if qualifies {
1070 // c:742 — `int ispipe = !(WC_SUBLIST_FLAGS(ecbuf[p+1])
1071 // & WC_SUBLIST_SIMPLE);`
1072 let ispipe = (WC_SUBLIST_FLAGS(sublist_code) & WC_SUBLIST_SIMPLE) == 0;
1073 // c:743 — `ecbuf[p] = WCB_LIST((type|Z_SIMPLE), ecused-2-p);`
1074 let used = ECUSED.get() as usize;
1075 let off = used.saturating_sub(2 + p);
1076 ECBUF.with_borrow_mut(|b| {
1077 if p < b.len() {
1078 b[p] = WCB_LIST((z | Z_SIMPLE) as wordcode, off as wordcode);
1079 }
1080 });
1081 // c:744 — `ecdel(p+1);`
1082 ecdel(p + 1);
1083 // c:745-746 — `if (ispipe) ecbuf[p+1] = WC_PIPE_LINENO(ecbuf[p+1]);`
1084 if ispipe {
1085 ECBUF.with_borrow_mut(|b| {
1086 if p + 1 < b.len() {
1087 b[p + 1] = WC_PIPE_LINENO(b[p + 1]);
1088 }
1089 });
1090 }
1091 } else {
1092 // c:748 — `ecbuf[p] = WCB_LIST(type, 0);`
1093 ECBUF.with_borrow_mut(|b| {
1094 if p < b.len() {
1095 b[p] = WCB_LIST(z as wordcode, 0);
1096 }
1097 });
1098 }
1099}
1100
1101/// Port of `set_sublist_code()` from `Src/parse.c:755` — C signature `set_sublist_code(int p, int type, int flags, int skip, int cmplx)`.
1102/// Patches the WCB_SUBLIST header at `p`.
1103/// When the sublist is non-complex (single command, no pipeline),
1104/// sets WC_SUBLIST_SIMPLE and rewrites the following slot to
1105/// `WC_PIPE_LINENO`.
1106pub fn set_sublist_code(p: usize, type_code: i32, flags: i32, skip: i32, cmplx: bool) {
1107 if cmplx {
1108 // c:758 — `ecbuf[p] = WCB_SUBLIST(type, flags, skip);`
1109 ECBUF.with_borrow_mut(|b| {
1110 if p < b.len() {
1111 b[p] = WCB_SUBLIST(type_code as wordcode, flags as wordcode, skip as wordcode);
1112 }
1113 });
1114 } else {
1115 // c:760 — `ecbuf[p] = WCB_SUBLIST(type, flags|WC_SUBLIST_SIMPLE, skip);`
1116 ECBUF.with_borrow_mut(|b| {
1117 if p < b.len() {
1118 b[p] = WCB_SUBLIST(
1119 type_code as wordcode,
1120 (flags as wordcode) | WC_SUBLIST_SIMPLE,
1121 skip as wordcode,
1122 );
1123 }
1124 });
1125 // c:761 — `ecbuf[p+1] = WC_PIPE_LINENO(ecbuf[p+1]);`
1126 ECBUF.with_borrow_mut(|b| {
1127 if p + 1 < b.len() {
1128 b[p + 1] = WC_PIPE_LINENO(b[p + 1]);
1129 }
1130 });
1131 }
1132}
1133
1134/// Parse a list (sublist with optional & or ;).
1135///
1136/// Port of `par_list()` from `Src/parse.c:771` (body c:771-804; the
1137/// `par_list1` wrapper follows at c:808).
1138///
1139/// **Structural divergence**: zsh's parse.c emits flat wordcode
1140/// into the `ecbuf` u32 array via `ecadd(0)` (placeholder),
1141/// `set_list_code(p, code, complexity)`, `wc_bdata(Z_END)`. zshrs
1142/// builds an AST node `ZshList { sublist, flags }` instead. The
1143/// async/sync/disown discrimination at parse.c:785-790 maps to
1144/// zshrs's `ListFlags { async_, disown }` field — Z_SYNC is the
1145/// default (no flags), Z_ASYNC = `&` = `async_=true`, Z_DISOWN +
1146/// Z_ASYNC = `&!`/`&|` = both true. Same semantics, different
1147/// representation. This divergence is repository-wide: every
1148/// `par_*` function emits wordcode in C, every `parse_*` builds
1149/// AST in Rust. The compile_zsh module then traverses the AST to
1150/// emit fusevm bytecode, which serves the same role as zsh's
1151/// wordcode but with a different opcode set and execution model.
1152fn par_list(single_event: bool) -> Option<(ZshList, bool)> {
1153 let sublist = par_sublist()?;
1154
1155 // c:769-803 — `list : { SEPER } [ sublist [ { SEPER | AMPER |
1156 // AMPERBANG } list ] ]`. The second tuple element reports
1157 // whether the list ended with an EXPLICIT terminator. C's list
1158 // grammar only chains sublists across SEPER/AMPER/AMPERBANG; a
1159 // sublist followed directly by another command (`{a} {b}`) ends
1160 // the list and the dangling token is the CALLER's problem
1161 // (par_while takes a dangling INBRACE as the loop body, par_event
1162 // yyerrors at c:671-680). parse_program_until needs this bit to
1163 // reproduce that split.
1164 let (flags, terminated) = match tok() {
1165 AMPER => {
1166 zshlex();
1167 (
1168 ListFlags {
1169 async_: true,
1170 disown: false,
1171 },
1172 true,
1173 )
1174 }
1175 AMPERBANG => {
1176 zshlex();
1177 (
1178 ListFlags {
1179 async_: true,
1180 disown: true,
1181 },
1182 true,
1183 )
1184 }
1185 SEPER => {
1186 // c:Src/parse.c par_event SEPER branch — `if (isnewlin <= 0
1187 // || endtok != ENDINPUT) zshlex();`. The lexer folds both
1188 // `;` and `\n` into SEPER (lex.rs:265), distinguishing them
1189 // via LEX_ISNEWLIN (C `isnewlin`). At a TOP-LEVEL event
1190 // boundary (single_event ≈ endtok == ENDINPUT) a SEPER that
1191 // was a NEWLINE terminates the event and is NOT consumed —
1192 // it's left for the next parse_event, whose leading skip
1193 // reads the following line only THEN. That's what lets
1194 // loop() read+execute one event at a time so `-v`/`-x` echo
1195 // and runtime `setopt verbose` interleave with execution
1196 // like zsh. A `;` SEPER (isnewlin <= 0), or any SEPER inside
1197 // a body / whole-program parse (single_event = false), is
1198 // consumed to chain the next sublist.
1199 let is_newline = crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) > 0;
1200 if !(single_event && is_newline) {
1201 zshlex();
1202 }
1203 (ListFlags::default(), true)
1204 }
1205 SEMI | NEWLIN => {
1206 // Unfolded `;` / preserved newline (LEXFLAGS_NEWLINE) — not a
1207 // top-level event terminator; consume and continue.
1208 zshlex();
1209 (ListFlags::default(), true)
1210 }
1211 _ => (ListFlags::default(), false),
1212 };
1213
1214 Some((ZshList { sublist, flags }, terminated))
1215}
1216
1217/// Port of `par_list1()` from `Src/parse.c:808`.
1218/// Parse one list — non-recursing variant. Like par_list but
1219/// doesn't recurse on the trailing-separator path; used by
1220/// callers that only want one statement (e.g. each arm of a
1221/// case body).
1222pub fn par_list1() -> Option<ZshSublist> {
1223 // parse.c:810-816 — body is a single par_sublist call wrapped
1224 // in the eu/ecused tracking that zshrs doesn't need (no
1225 // wordcode buffer).
1226 par_sublist()
1227}
1228
1229/// Parse a sublist (pipelines connected by && or ||).
1230///
1231/// Port of `par_sublist()` from `Src/parse.c:825` (with `par_sublist2`
1232/// at c:869-892 folded in). par_sublist handles the
1233/// && / || conjunction and emits WC_SUBLIST opcodes; par_sublist2
1234/// handles the leading `!` negation and `coproc` keyword.
1235///
1236/// AST mapping: ZshSublist { pipe, conj_chain }, where `conj_chain`
1237/// is a Vec<(ConjOp, ZshSublist)> for chained && / ||. C uses
1238/// flat wordcode with WC_SUBLIST_AND / WC_SUBLIST_OR markers.
1239fn par_sublist() -> Option<ZshSublist> {
1240 let mut flags = SublistFlags::default();
1241
1242 // Handle coproc and !
1243 if tok() == COPROC {
1244 flags.coproc = true;
1245 zshlex();
1246 } else if tok() == BANG_TOK {
1247 flags.not = true;
1248 zshlex();
1249 }
1250
1251 // c:882-883 (par_sublist2) — `if (!par_pline(cmplx) && !f) return -1;`:
1252 // after `coproc` or `!` a missing pipeline is not an error, so `{ ! }`,
1253 // `( ! )` and `fn() { ! }` parse, and the empty pipeline runs as a null
1254 // command (status 0, negated to 1). An error par_pline itself raised
1255 // still fails the sublist.
1256 let errflag_before = crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed);
1257 let pipe = match par_pline() {
1258 Some(p) => p,
1259 // A `|` / `|&` with no command before it is still an error, reported
1260 // by the caller at that token (`{ ! | }` → "parse error near `|'").
1261 None if (flags.not || flags.coproc)
1262 && !matches!(tok(), BAR_TOK | BARAMP)
1263 && crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed)
1264 == errflag_before =>
1265 {
1266 ZshPipe {
1267 cmd: ZshCommand::Simple(ZshSimple {
1268 assigns: Vec::new(),
1269 words: Vec::new(),
1270 redirs: Vec::new(),
1271 typeset_reswd: false,
1272 via_alias: None,
1273 }),
1274 next: None,
1275 lineno: toklineno(),
1276 merge_stderr: false,
1277 }
1278 }
1279 None => return None,
1280 };
1281
1282 // Check for && or ||
1283 let next = match tok() {
1284 DAMPER => {
1285 zshlex();
1286 if !crate::dash_mode::dash_skip_operator_separators() {
1287 return None;
1288 }
1289 // c:Src/parse.c:par_sublist — and-or operators (`&&`,
1290 // `||`) require a sublist on each side. After consuming
1291 // `&&`/`||`, another and-or operator OR a pipe-operator
1292 // immediately after is a parse error in C zsh. zshrs's
1293 // recursion silently returned None and dropped the
1294 // operator. Bug #171 in docs/BUGS.md.
1295 if matches!(tok(), DAMPER | DBAR | BAR_TOK | BARAMP) {
1296 let name = match tok() {
1297 DAMPER => "&&",
1298 DBAR => "||",
1299 BAR_TOK => "|",
1300 BARAMP => "|&",
1301 _ => "operator",
1302 };
1303 zerr(&format!("parse error near `{}'", name));
1304 return None;
1305 }
1306 par_sublist().map(|s| (SublistOp::And, Box::new(s)))
1307 }
1308 DBAR => {
1309 zshlex();
1310 if !crate::dash_mode::dash_skip_operator_separators() {
1311 return None;
1312 }
1313 if matches!(tok(), DAMPER | DBAR | BAR_TOK | BARAMP) {
1314 let name = match tok() {
1315 DAMPER => "&&",
1316 DBAR => "||",
1317 BAR_TOK => "|",
1318 BARAMP => "|&",
1319 _ => "operator",
1320 };
1321 zerr(&format!("parse error near `{}'", name));
1322 return None;
1323 }
1324 par_sublist().map(|s| (SublistOp::Or, Box::new(s)))
1325 }
1326 _ => None,
1327 };
1328
1329 Some(ZshSublist { pipe, next, flags })
1330}
1331
1332/// Port of `par_sublist2()` from `Src/parse.c:869` — C signature `par_sublist2(int *cmplx)`.
1333/// Secondary-sublist arm: handles the `COPROC`/`Bang` prefix
1334/// in front of a pline. Returns the WC_SUBLIST flag word added.
1335pub fn par_sublist2(cmplx: &mut i32) -> Option<i32> {
1336 // c:870 — `int f = 0;`
1337 let mut f: i32 = 0;
1338 // c:873-880 — COPROC / BANG prefix flags.
1339 if tok() == COPROC {
1340 *cmplx = 1;
1341 f |= WC_SUBLIST_COPROC as i32;
1342 zshlex();
1343 } else if tok() == BANG_TOK {
1344 *cmplx = 1;
1345 f |= WC_SUBLIST_NOT as i32;
1346 zshlex();
1347 }
1348 // c:882-883 — `if (!par_pline(cmplx) && !f) return -1;`
1349 if !par_pipe_wordcode(cmplx) && f == 0 {
1350 return None;
1351 }
1352 // c:885 — `return f;`
1353 Some(f)
1354}
1355
1356/// Port of `par_pline()` from `Src/parse.c:894`.
1357/// Parse a pipeline (cmds joined by `|` / `|&`).
1358/// AST: ZshPipe { cmds: Vec<ZshCommand> }.
1359/// C emits WC_PIPE wordcodes per command; same flow.
1360fn par_pline() -> Option<ZshPipe> {
1361 let lineno = toklineno();
1362 let cmd = par_cmd(false)?;
1363
1364 // Check for | or |&
1365 let mut merge_stderr = false;
1366 let next = match tok() {
1367 BAR_TOK | BARAMP => {
1368 merge_stderr = tok() == BARAMP;
1369 zshlex();
1370 if !crate::dash_mode::dash_skip_operator_separators() {
1371 return None;
1372 }
1373 // c:Src/parse.c:par_pline — pipe-operators require a
1374 // command on each side. After consuming `|`/`|&`,
1375 // C zsh's recursive par_pline call returns -1 (parse
1376 // error) when the next token is another pipe-operator
1377 // — `a | | b` errors with `parse error near `|''`.
1378 // zshrs's `par_pline()?` silently returned None on
1379 // missing command, dropping the rest of the input
1380 // without diagnosing the empty-pipe-operand. Bug #171
1381 // in docs/BUGS.md.
1382 if matches!(tok(), BAR_TOK | BARAMP) {
1383 let name = if tok() == BARAMP { "|&" } else { "|" };
1384 zerr(&format!("parse error near `{}'", name));
1385 return None;
1386 }
1387 // c:914-916 — `if (!par_pline(cmplx)) { tok = LEXERR; }`: a
1388 // `|` with no command after it (`print a |` at end of input)
1389 // is a syntax error, not a one-stage pipeline. Returning the
1390 // bare `None` let the left side parse and RUN.
1391 let rest = par_pline();
1392 if rest.is_none() {
1393 set_tok(LEXERR);
1394 }
1395 rest.map(Box::new)
1396 }
1397 _ => None,
1398 };
1399
1400 Some(ZshPipe {
1401 cmd,
1402 next,
1403 lineno,
1404 merge_stderr,
1405 })
1406}
1407
1408/// Port of `par_cmd()` from `Src/parse.c:958` — C signature `par_cmd(int *cmplx, int zsh_construct)`.
1409/// Parse a command — dispatches by leading token (FOR / CASE /
1410/// IF / WHILE / UNTIL / REPEAT / FUNC / DINBRACK / DINPAR /
1411/// Inpar subshell / Inbrace current-shell / TIME / NOCORRECT,
1412/// else simple). Direct port of zsh/Src/parse.c:958 `par_cmd`.
1413fn par_cmd(zsh_construct: bool) -> Option<ZshCommand> {
1414 // Parse leading redirections
1415 let mut redirs = Vec::new();
1416 while IS_REDIROP(tok()) {
1417 if let Some(redir) = par_redir() {
1418 redirs.push(redir);
1419 }
1420 }
1421 // c:964-969 — the leading redirections belong to WHATEVER command
1422 // follows, compound or simple: `>| f { print x ;}` captures the group
1423 // (Src/subst.c:1993 builds exactly that for `${ … }`). par_simple takes
1424 // them itself; a compound form gets them ahead of its trailing ones.
1425 let mut leading = Some(redirs);
1426
1427 // c:Src/parse.c:970-1030 par_cmd — push the open construct onto the
1428 // cmdstack for the duration of its sub-parse, then pop. The cmdstack
1429 // drives `%_` prompt expansion, so this is what makes a multi-line
1430 // construct's PS2 render `for> ` / `while> ` / `case> ` across
1431 // continuation lines instead of a bare `> `. (The cmdstack does NOT
1432 // drive tokenization in zshrs — the lexer's only reads are the
1433 // debug-only DPUTS "cmdstack not empty" asserts — so wrapping the AST
1434 // dispatch is behaviorally inert outside the prompt.) IF is excluded
1435 // to match C, where par_if manages its own cmdstack (CS_IF/CS_IFTHEN);
1436 // DINPAR/TIME/INOUTPAR are not cmdstack constructs in C either.
1437 let cmd = match tok() {
1438 FOR => {
1439 cmdpush(CS_FOR as u8); // c:972
1440 let c = par_for();
1441 cmdpop(); // c:974
1442 c
1443 }
1444 FOREACH => {
1445 cmdpush(CS_FOREACH as u8); // c:977
1446 let c = par_for();
1447 cmdpop(); // c:979
1448 c
1449 }
1450 SELECT => {
1451 cmdpush(CS_SELECT as u8); // c:983
1452 let c = parse_select();
1453 cmdpop(); // c:985
1454 c
1455 }
1456 CASE => {
1457 cmdpush(CS_CASE as u8); // c:988
1458 let c = par_case();
1459 cmdpop(); // c:990
1460 c
1461 }
1462 IF => par_if(), // c:992-994 — par_if manages its own cmdstack
1463 WHILE => {
1464 cmdpush(CS_WHILE as u8); // c:996
1465 let c = par_while(false);
1466 cmdpop(); // c:998
1467 c
1468 }
1469 UNTIL => {
1470 cmdpush(CS_UNTIL as u8); // c:1001
1471 let c = par_while(true);
1472 cmdpop(); // c:1003
1473 c
1474 }
1475 REPEAT => {
1476 cmdpush(CS_REPEAT as u8); // c:1006
1477 let c = par_repeat();
1478 cmdpop(); // c:1008
1479 c
1480 }
1481 INPAR_TOK => {
1482 cmdpush(CS_SUBSH as u8); // c:1012
1483 let c = par_subsh(zsh_construct);
1484 cmdpop(); // c:1014
1485 c
1486 }
1487 INOUTPAR => parse_anon_funcdef(),
1488 INBRACE_TOK => {
1489 cmdpush(CS_CURSH as u8); // c:1017
1490 let c = parse_cursh(zsh_construct);
1491 cmdpop(); // c:1019
1492 c
1493 }
1494 FUNC => {
1495 cmdpush(CS_FUNCDEF as u8); // c:1022
1496 let c = par_funcdef();
1497 cmdpop(); // c:1024
1498 c
1499 }
1500 DINBRACK => {
1501 cmdpush(CS_COND as u8); // c:1027
1502 let c = par_cond();
1503 cmdpop(); // c:1029
1504 c
1505 }
1506 DINPAR => parse_arith(),
1507 TIME => par_time(),
1508 _ => par_simple(leading.take().unwrap_or_default()),
1509 };
1510 let leading = leading.unwrap_or_default();
1511
1512 // Parse trailing redirections. For Simple commands the redirs were
1513 // already captured inside par_simple; for compound forms (Cursh,
1514 // Subsh, If, While, etc.) we collect them here and wrap in
1515 // ZshCommand::Redirected so compile_zsh can scope-bracket them.
1516 if let Some(inner) = cmd {
1517 let mut trailing: Vec<ZshRedir> = leading;
1518 while IS_REDIROP(tok()) {
1519 if let Some(redir) = par_redir() {
1520 trailing.push(redir);
1521 }
1522 }
1523 // c:Src/parse.c:par_cmd — compound forms (Cursh `{...}`, Subsh
1524 // `(...)`, If/While/Until/For/Case/Select/Repeat/Funcdef) must
1525 // be followed by a valid sublist/list separator (`;`, `\n`,
1526 // `&`, `|`, `&&`, `||`, redirect-op) — STRING_LEX after a
1527 // compound is a parse error. zshrs's outer par_list loop
1528 // silently treated trailing words as a new command, masking
1529 // syntax errors like `{ echo a; } b c`. Mirror C's strict
1530 // post-compound terminator check. Bug #146 in docs/BUGS.md.
1531 // With zsh_construct set (an anonymous function's `( … )` / `{ … }`
1532 // body, c:2114) the words that follow are that function's arguments
1533 // (c:2148-2167), not a stray word after a compound command.
1534 if !zsh_construct
1535 && !matches!(inner, ZshCommand::Simple(_))
1536 && tok() == STRING_LEX
1537 && COND_LIST_DEPTH.with(|d| d.get()) == 0
1538 {
1539 // c:Src/parse.c:2738 — the error token is `zshlextext`, the
1540 // human-readable source text, so quotes/sigils are shown
1541 // verbatim (`"quoted"`, `'x'`, `$var`). zshrs's `tokstr()`
1542 // still carries the inline quote MARKERS (Dnull/Snull/Stringg
1543 // = `\u{e19e}`/`\u{e19d}`/`\u{e185}`); emitting it raw printed the
1544 // marker bytes and dropped the visible quotes. Untokenize to
1545 // the display form: Dnull→`"`, Snull→`'`, Stringg→`$`, Bnull→`\`.
1546 let bad = crate::ported::lex::untokenize_preserve_quotes(&tokstr().unwrap_or_default());
1547 zerr(&format!("parse error near `{}'", bad));
1548 // Reset state before returning so the outer loop's None
1549 // detection unwinds cleanly.
1550 set_incmdpos(true);
1551 set_incasepat(0);
1552 set_incond(0);
1553 set_intypeset(false);
1554 return None;
1555 }
1556 // c:1072-1075 — every par_cmd tail resets the lexer state
1557 // toggles so the NEXT command starts in cmd position with
1558 // case/cond/typeset off. par_simple/par_cond set `incmdpos=0`
1559 // during their bodies; without this reset the next iteration
1560 // of the outer par_list loop sees `if` / `done` / `select`
1561 // etc. as plain strings and the AST collapses.
1562 set_incmdpos(true);
1563 set_incasepat(0);
1564 set_incond(0);
1565 set_intypeset(false);
1566 if trailing.is_empty() {
1567 return Some(inner);
1568 }
1569 // Simple already absorbed its own redirs (compile path expects
1570 // them on ZshSimple), so don't double-wrap.
1571 if matches!(inner, ZshCommand::Simple(_)) {
1572 if let ZshCommand::Simple(mut s) = inner {
1573 s.redirs.extend(trailing);
1574 return Some(ZshCommand::Simple(s));
1575 }
1576 unreachable!()
1577 }
1578 return Some(ZshCommand::Redirected(Box::new(inner), trailing));
1579 }
1580 // Same reset on the empty-cmd branch (mirror c:1072 unconditional
1581 // path — the C function only returns 0 above when the dispatch
1582 // produced no command, and falls through to the reset block).
1583 set_incmdpos(true);
1584 set_incasepat(0);
1585 set_incond(0);
1586 set_intypeset(false);
1587
1588 None
1589}
1590
1591/// Port of `par_for()` from `Src/parse.c:1087`.
1592/// Parse `for NAME in WORDS; do BODY; done` (foreach style) AND
1593/// `for ((init; cond; incr)) do BODY done` (c-style).
1594/// parse_for_cstyle is the
1595/// inner branch for the `((...))` arithmetic-header variant
1596/// (parse.c:1100-1140 inside par_for).
1597fn par_for() -> Option<ZshCommand> {
1598 let is_foreach = tok() == FOREACH;
1599 // c:1094-1095 (Src/parse.c, par_for) — set `infor=2` (only when
1600 // tok==FOR) so the lexer's `(` peek at lex.c:784-789
1601 // (`if (infor) { ... return DINPAR; }`) routes the arith-for
1602 // body through dbparens semicolon-splitting instead of the
1603 // `cmd_or_math` whole-body capture path. Without this, `for ((
1604 // i=0; i<3; i++ ))` lexed as a single `((arith))` expression
1605 // and parse_for_cstyle's second zshlex got an empty/wrong tok.
1606 //
1607 // Guard the loop-variable read against alias expansion and
1608 // spell-correction. C uses `incmdpos = 0` for the first name
1609 // (c:1094) plus `noaliases = nocorrect = 1` for the rest
1610 // (c:1123), restoring them at c:1137. We guard the WHOLE name
1611 // read with `noaliases`/`nocorrect` (the actual anti-alias
1612 // mechanism) and leave `incmdpos` untouched: without this, a
1613 // user `alias i='if [[ … ]]; then … fi'` (real zpwr config,
1614 // set by zpwrBindAliasesZshLate) makes `for i in {0..$#}`
1615 // expand the loop variable `i` into `if`, so par_for's next
1616 // token is IF instead of the STRING name and it dies with
1617 // "expected variable name in for". Forcing incmdpos=0 here
1618 // (as C does) instead regresses the `(`/`((`/`do` detection
1619 // for for-paren, arith-for and the loop body, which this
1620 // parser recovers from the inherited command position.
1621 let saved_noaliases = noaliases();
1622 let saved_nocorrect = nocorrect();
1623 set_noaliases(true);
1624 set_nocorrect(1);
1625 set_infor(if tok() == FOR { 2 } else { 0 }); // c:1095
1626 zshlex(); // c:1096
1627
1628 // Check for C-style: for (( init; cond; step ))
1629 if tok() == DINPAR {
1630 // c:1110-1111 — close out infor / cmdpos after parse_for_cstyle
1631 // has consumed the init/cond/step triple. Done inside the
1632 // helper itself so we honour the C ordering.
1633 set_noaliases(saved_noaliases);
1634 set_nocorrect(saved_nocorrect);
1635 return parse_for_cstyle();
1636 }
1637
1638 // c:1116 — `infor = 0;` immediately on entering the foreach
1639 // branch. Without this, `infor` stays at 2 (set at c:1095 when
1640 // tok==FOR) for the rest of par_for, and the lexer's `((`
1641 // peek at lex.c:786 routes every subsequent `((...))` inside
1642 // the loop body through dbparens — so `for x in a; do (( 1
1643 // )); done` and `if (( 1 )) { … }` inside the do-body both
1644 // mis-lexed as a c-style for header.
1645 set_infor(0); // c:1116
1646
1647 // Get variable name(s). zsh parse.c par_for accepts multiple
1648 // identifier tokens before `in`/`(`/newline — `for k v in ...`
1649 // assigns each iteration's pair of values to k and v in turn.
1650 // We store the names space-joined since variable identifiers
1651 // can't contain whitespace.
1652 //
1653 // c:1123-1131 — C's shape is a `for(;;)` whose FIRST iteration takes
1654 // the current token unconditionally and only THEN tests the next one:
1655 // for (;;) { n++; ecstr(tokstr); zshlex();
1656 // if (tok != STRING || !strcmp(tokstr, "in") || sel) break;
1657 // if (!isident(tokstr) || errflag) YYERRORV(oecused); }
1658 // So the loop variable may itself be spelled `in` — `for in in in in
1659 // in stop` iterates $in over `in in in stop` (A01grammar.ztst:292).
1660 // The old Rust loop tested `v == "in"` BEFORE taking the first name,
1661 // so that form died with "expected variable name in for".
1662 let mut names: Vec<String> = Vec::new();
1663 // c:1117-1118 — `if (tok != STRING || !isident(tokstr)) YYERRORV(oecused);`
1664 // — the FIRST name must be an identifier (`for a-b in x` is an error).
1665 if tok() == STRING_LEX && crate::ported::params::isident(&tokstr().unwrap_or_default()) {
1666 loop {
1667 names.push(tokstr().unwrap_or_default()); // c:1124 ecstr(tokstr)
1668 zshlex(); // c:1125
1669 // c:1126 — `if (tok != STRING || !strcmp(tokstr, "in") || sel) break;`
1670 if tok() != STRING_LEX {
1671 break;
1672 }
1673 let v = tokstr().unwrap_or_default();
1674 if v == "in" {
1675 break;
1676 }
1677 // c:1127-1132 — C YYERRORs on a non-identifier here. The Rust
1678 // lexer can hand back the `(word1 word2)` list of the
1679 // `for x (…)` form as a single Inpar-wrapped STRING (see the
1680 // list reader below), so stop the name scan instead of
1681 // erroring and let that reader handle it.
1682 if !crate::ported::params::isident(&v) {
1683 break;
1684 }
1685 }
1686 }
1687 // c:1137-1138 — restore alias / spell-correct state now the
1688 // name list is fully read (the `in`/`(` list that follows must
1689 // lex with the caller's normal settings).
1690 set_noaliases(saved_noaliases);
1691 set_nocorrect(saved_nocorrect);
1692 if names.is_empty() {
1693 // c:1117-1118 — `YYERRORV(oecused)` only flags LEXERR; the single
1694 // "parse error near `<zshlextext>'" comes from par_event's yyerror.
1695 set_tok(LEXERR); // c:87 `tok = LEXERR`
1696 return None;
1697 }
1698 let var = names.join(" ");
1699
1700 // Skip newlines
1701 skip_separators();
1702
1703 // Get list. The lexer-port quirk: `for x (a b c)` arrives as a
1704 // single String token with the parens lexed-as-content
1705 // (`<Inpar>a b c<Outpar>`) instead of as separate Inpar/String/
1706 // Outpar tokens. Detect that shape and split it manually.
1707 let list = if tok() == STRING_LEX
1708 && tokstr()
1709 .map(|s| s.starts_with('\u{e188}') && s.ends_with('\u{e18a}'))
1710 .unwrap_or(false)
1711 {
1712 let raw = tokstr().unwrap_or_default();
1713 // Strip leading Inpar + trailing Outpar. KEEP the inner
1714 // content tokenized — `for x ({1..3}) …` has `{1..3}` as
1715 // Inbrace+content+Outbrace markers, which compile_word_str
1716 // needs to detect and brace-expand. Untokenizing here would
1717 // collapse the markers to plain `{` `}` chars and the brace-
1718 // expansion pass (which strictly requires Inbrace TOKEN per
1719 // Src/glob.c:hasbraces) would skip the word entirely.
1720 // Split only on UNTOKENIZED whitespace at the top level —
1721 // tokenized characters (TOKEN range \u{e184}..\u{e1a1}) are part
1722 // of one word; bare ASCII spaces / tabs separate words.
1723 let inner = &raw[raw.char_indices().nth(1).map(|(i, _)| i).unwrap_or(0)
1724 ..raw
1725 .char_indices()
1726 .last()
1727 .map(|(i, _)| i)
1728 .unwrap_or(raw.len())];
1729 let mut words: Vec<String> = Vec::new();
1730 let mut cur = String::new();
1731 for c in inner.chars() {
1732 if c == ' ' || c == '\t' || c == '\n' {
1733 if !cur.is_empty() {
1734 words.push(std::mem::take(&mut cur));
1735 }
1736 } else {
1737 cur.push(c);
1738 }
1739 }
1740 if !cur.is_empty() {
1741 words.push(cur);
1742 }
1743 zshlex();
1744 ForList::Words(words)
1745 } else if tok() == STRING_LEX {
1746 let s = tokstr();
1747 if s.map(|s| s == "in").unwrap_or(false) {
1748 // c:Src/parse.c:1147-1154 — after consuming `in`, the
1749 // for-list reads in WORD position, not command position.
1750 // Reset incmdpos=false so the lexer's LX2_INBRACE arm
1751 // (lex.rs:1791) treats a leading `{` as the brace-
1752 // expansion marker (`bct++; add(Inbrace)`) instead of
1753 // returning STRING("{") + promoting to INBRACE_TOK.
1754 // Without this, `for i in {1..3}` saw `{` as the body-
1755 // opener brace, so the word-collection loop got an
1756 // empty word list and the loop body silently ran 0
1757 // iterations.
1758 set_incmdpos(false);
1759 zshlex();
1760 let mut words = Vec::new();
1761 while tok() == STRING_LEX {
1762 let _ts_s = tokstr();
1763 if let Some(s) = _ts_s.as_deref() {
1764 words.push(s.to_string());
1765 }
1766 zshlex();
1767 }
1768 // c:1149-1150 — `if (tok != SEPER) YYERRORV(oecused);` — the
1769 // word list must end at a separator (`for x in a b` at end of
1770 // input is an error).
1771 if tok() != SEPER {
1772 set_tok(LEXERR); // c:87 `tok = LEXERR`
1773 return None;
1774 }
1775 // c:Src/parse.c:1162 — `incmdpos = 1;` after the
1776 // wordlist + SEPER are consumed, so the next token
1777 // (`do` / `{` body opener) lexes at command position.
1778 set_incmdpos(true);
1779 ForList::Words(words)
1780 } else {
1781 ForList::Positional
1782 }
1783 } else if tok() == INPAR_TOK {
1784 // for var (...) — `for x ({1..3})`: inside the parens, the
1785 // list is in WORD position so `{` must lex as the brace-
1786 // expansion Inbrace marker, NOT as a body-opener INBRACE_TOK.
1787 // Without resetting incmdpos before the next zshlex, the
1788 // lexer's LX2_INBRACE arm promotes `{` to INBRACE_TOK and
1789 // the word-collection loop exits empty, giving
1790 // `for x ({1..3})` an empty iteration.
1791 set_incmdpos(false);
1792 zshlex();
1793 let mut words = Vec::new();
1794 while tok() == STRING_LEX || tok() == SEPER {
1795 if tok() == STRING_LEX {
1796 let _ts_s = tokstr();
1797 if let Some(s) = _ts_s.as_deref() {
1798 words.push(s.to_string());
1799 }
1800 }
1801 zshlex();
1802 }
1803 // c:1158-1159 — `if (tok != OUTPAR) YYERRORV(oecused);`
1804 if tok() != OUTPAR_TOK {
1805 set_tok(LEXERR); // c:87 `tok = LEXERR`
1806 return None;
1807 }
1808 // After the `)` of a for-list, the next token is the
1809 // body opener — `do`/`{`. zsh's lexer needs incmdpos
1810 // set so `{` lexes as Inbrace (not as a literal). C
1811 // analogue: parse.c::par_for sets `incmdpos = 1`
1812 // after consuming the Outpar before the body parse.
1813 set_incmdpos(true);
1814 zshlex();
1815 ForList::Words(words)
1816 } else {
1817 ForList::Positional
1818 };
1819
1820 // Skip to body
1821 skip_separators();
1822
1823 // Parse body
1824 let body = parse_loop_body(is_foreach, false)?;
1825
1826 Some(ZshCommand::For(ZshFor {
1827 var,
1828 list,
1829 body: Box::new(body),
1830 is_select: false,
1831 }))
1832}
1833
1834/// Port of `par_case()` from `Src/parse.c:1209` — C decl `par_case(int *cmplx)`. AST variant: builds a `ZshCommand::Case` instead of emitting WC_CASE wordcode.
1835/// Parse case statement
1836/// Parse `case WORD in PATTERN) BODY ;; ... esac`. Direct port
1837/// of zsh/Src/parse.c:1209 `par_case`. Each case arm is a
1838/// (pattern_list, body, terminator) tuple where terminator is
1839/// `;;` (default), `;&` (fallthrough), or `;|` (continue testing).
1840fn par_case() -> Option<ZshCommand> {
1841 // C par_case (parse.c:1209-1241). Order of state toggles
1842 // matters — the lexer reads the case word in `incmdpos=0`
1843 // (so it's not promoted to a reswd), then the `in`/`{` in
1844 // `incmdpos=1, noaliases=1, nocorrect=1` (so the `in` literal
1845 // isn't alias-expanded or spell-corrected), then sets
1846 // `incasepat=1, incmdpos=0` before the first pattern.
1847 set_incmdpos(false);
1848 zshlex(); // skip 'case'
1849
1850 let word = match tok() {
1851 STRING_LEX => {
1852 let w = tokstr().unwrap_or_default();
1853 // c:1222 — `incmdpos = 1;` before the next zshlex so the
1854 // `in` keyword is recognised. c:1223-1225 — save+force
1855 // noaliases / nocorrect.
1856 set_incmdpos(true);
1857 let ona = noaliases();
1858 let onc = nocorrect();
1859 set_noaliases(true);
1860 set_nocorrect(1);
1861 zshlex();
1862 // Restore noaliases/nocorrect after the `in`-or-`{` token
1863 // is in hand; both are unconditionally restored at c:1238-1239.
1864 let restore = |ona: bool, onc: i32| {
1865 set_noaliases(ona);
1866 set_nocorrect(onc);
1867 };
1868 (w, ona, onc, restore)
1869 }
1870 _ => {
1871 // c:1218-1219 — `if (tok != STRING) YYERRORV(oecused);`
1872 set_tok(LEXERR); // c:87 `tok = LEXERR`
1873 return None;
1874 }
1875 };
1876 let (word, ona, onc, restore) = word;
1877
1878 skip_separators();
1879
1880 // Expect 'in' or {
1881 let use_brace = tok() == INBRACE_TOK;
1882 if tok() == STRING_LEX {
1883 let s = tokstr();
1884 if s.map(|s| s != "in").unwrap_or(true) {
1885 // c:1228-1232 — restore noaliases/nocorrect on error path.
1886 restore(ona, onc);
1887 set_tok(LEXERR); // c:1232 YYERRORV(oecused)
1888 return None;
1889 }
1890 } else if !use_brace {
1891 restore(ona, onc);
1892 set_tok(LEXERR); // c:1232 YYERRORV(oecused)
1893 return None;
1894 }
1895 // c:1236-1239 — `incasepat = 1; incmdpos = 0; noaliases = ona;
1896 // nocorrect = onc;` — set the case-pattern context AND restore
1897 // alias/correct state BEFORE the zshlex that consumes `in`/`{`.
1898 set_incasepat(1);
1899 set_incmdpos(false);
1900 restore(ona, onc);
1901 zshlex();
1902
1903 let mut arms = Vec::new();
1904
1905 loop {
1906 // No arm-count cap: zsh `case` statements have no limit, and compsys
1907 // dispatchers (`_describe`, `_git`, generated `_arguments` bodies) emit
1908 // huge case bodies. The loop terminates on `esac` / EOF below; a former
1909 // 10_000-arm cap silently truncated (and errored on) larger ones.
1910
1911 // Set incasepat BEFORE skipping separators so lexer knows we're in case pattern context
1912 // This affects how [ and | are lexed
1913 set_incasepat(1);
1914
1915 skip_separators();
1916
1917 // Check for end
1918 // Note: 'esac' might be String "esac" if incasepat > 0 prevents reserved word recognition
1919 let is_esac = tok() == ESAC
1920 || (tok() == STRING_LEX && tokstr().map(|s| s == "esac").unwrap_or(false));
1921 if (use_brace && tok() == OUTBRACE_TOK) || (!use_brace && is_esac) {
1922 // c:1395-1397 — `incmdpos = 1; incasepat = 0; zshlex();`: the word
1923 // after `esac` is in command position, so a closing `fi` / `done`
1924 // / `}` right after it is still a reserved word. Without it
1925 // `if true; then case a in a) print x;; esac fi` was a parse error.
1926 set_incmdpos(true);
1927 set_incasepat(0);
1928 zshlex();
1929 break;
1930 }
1931
1932 // Also break on EOF. c:Src/parse.c:1209 par_case requires
1933 // ESAC (or `}` in brace form) to close the block — reaching
1934 // ENDINPUT without either is a parse error (`case ... esack`
1935 // typo absorbs `esack` as part of the body and silently
1936 // terminates rc=0 otherwise). Bug #400.
1937 // c:1256-1257 — `if (tok != STRING) YYERRORV(oecused);`: the
1938 // diagnostic is par_event's "parse error near `<zshlextext>'", which
1939 // names the last token read (`case x in a) echo;;` → near `;;').
1940 if tok() == ENDINPUT || tok() == LEXERR {
1941 set_incasepat(0);
1942 set_tok(LEXERR); // c:87 `tok = LEXERR`
1943 return None;
1944 }
1945
1946 // c:1250 — `if (tok == INPAR) zshlex();` — leading-paren
1947 // skip path. Used when the lexer DID return INPAR_TOK (e.g.
1948 // SHGLOB or incmdpos forced it). In the normal case-pattern
1949 // path the lexer absorbs `(...)` into one Stringg and the
1950 // hack at c:1322 strips the surrounding parens later. Both
1951 // paths land here.
1952 let leading_inpar_consumed = tok() == INPAR_TOK;
1953 if leading_inpar_consumed {
1954 zshlex();
1955 }
1956
1957 // c:1255-1262 — read pattern STRING. zsh's parser falls
1958 // straight into the STRING reader after the optional INPAR.
1959 // BAR before any pattern means empty string.
1960 let mut patterns = Vec::new();
1961 // Tracks whether the c:1322-1354 hack has fired (paren-
1962 // wrapped Stringg absorbed by the lexer). When it has, the
1963 // closing `)` was already absorbed — no separate OUTPAR
1964 // arm-close to consume.
1965 let mut absorbed_outpar = false;
1966
1967 // Nested-paren pattern: the user wrote `((alt|alt)tail)` —
1968 // the leading arm-INPAR was consumed; the NEXT token is also
1969 // INPAR. zinit.zsh:2946 hits this with `((add-|)fpath)`
1970 // meaning "fpath or add-fpath".
1971 //
1972 // The current lexer (cmd_or_math rewind path interacting
1973 // with gettokstr) drops ONE of the two `)` chars on the
1974 // way out, so the token stream is INPAR INPAR STRING BAR
1975 // STRING OUTPAR — only ONE OUTPAR for two source `)`. We
1976 // can't reconstruct the exact pattern from these tokens.
1977 // Proper fix needs to land in lex.rs (cmd_or_math rewind +
1978 // gettokstr LX2_INPAR/OUTPAR interplay) — see docs/BUGS.md
1979 // entry "case-pattern nested-paren lexer gap".
1980 //
1981 // Workaround: consume every token up to and INCLUDING the
1982 // arm-closing OUTPAR, build a best-effort pattern string,
1983 // set `absorbed_outpar = true` so the post-pattern OUTPAR
1984 // check below skips. The resulting glob may match a slightly
1985 // wider set than the original (`(add-|fpath)` vs
1986 // `(add-|)fpath`) but the rest of the script parses + runs.
1987 // Unblocking sourcing zinit-style configs is the priority.
1988 if leading_inpar_consumed && tok() == INPAR_TOK {
1989 let mut buf = String::new();
1990 let mut depth = 0i32;
1991 loop {
1992 match tok() {
1993 INPAR_TOK => {
1994 buf.push('(');
1995 depth += 1;
1996 zshlex();
1997 }
1998 OUTPAR_TOK => {
1999 // The single OUTPAR token in the stream is
2000 // simultaneously the inner glob-group close
2001 // AND the arm close (lexer collapses both).
2002 // Consume it, balance the inner depth, exit.
2003 // `absorbed_outpar = true` tells the shared
2004 // post-pattern check below to skip its own
2005 // consume.
2006 if depth > 0 {
2007 buf.push(')');
2008 depth -= 1;
2009 }
2010 zshlex();
2011 break;
2012 }
2013 STRING_LEX => {
2014 if let Some(s) = tokstr() {
2015 if s == "esac" {
2016 break;
2017 }
2018 buf.push_str(&s);
2019 }
2020 set_incasepat(2);
2021 zshlex();
2022 }
2023 BAR_TOK => {
2024 buf.push('|');
2025 set_incasepat(1);
2026 zshlex();
2027 }
2028 _ => break,
2029 }
2030 }
2031 patterns.push(buf);
2032 set_incasepat(0);
2033 set_incmdpos(true);
2034 absorbed_outpar = true;
2035 }
2036
2037 // Skip the legacy STRING/BAR pattern-read loop when the
2038 // nested-paren branch above already populated `patterns` and
2039 // consumed the arm-close. Otherwise the legacy loop would
2040 // greedily absorb the body's first command token (`echo`) as
2041 // another alt-pattern.
2042 if !patterns.is_empty() && absorbed_outpar {
2043 // skip to body parse
2044 } else {
2045 // c:1253-1256 — `if (tok == BAR) { str = dupstring("");
2046 // skip_zshlex = 1; }` — an arm that OPENS with `|` has the
2047 // empty string as its first alternative (`case '' in |burble)`).
2048 if tok() == BAR_TOK {
2049 patterns.push(String::new());
2050 }
2051 loop {
2052 if tok() == STRING_LEX {
2053 let s = tokstr();
2054 if s.as_deref().map(|s| s == "esac").unwrap_or(false) {
2055 break;
2056 }
2057 let mut str_val = s.unwrap_or_default();
2058
2059 // c:1322-1354 hack: when this is the first alt AND
2060 // the string starts with the Inpar marker, the lexer
2061 // absorbed the whole `(...)` as one token. Chuck the
2062 // blanks around `|`/parens at depth 1 (c:1332-1338 —
2063 // `( d | e )` must become `(d|e)`), then strip the
2064 // surrounding parens — the remainder IS the pattern.
2065 // The closing arm-paren was absorbed too, so we don't
2066 // expect a separate OUTPAR token afterward.
2067 if patterns.is_empty() && str_val.starts_with(crate::ported::zsh_h::Inpar) {
2068 use crate::ported::zsh_h::{Bar, Inpar, Outpar};
2069 let meta = crate::ported::zsh_h::Meta as char;
2070 let blank =
2071 |c: char| c.is_ascii() && crate::ported::ztype_h::iblank(c as u8);
2072 let mut chars: Vec<char> = str_val.chars().collect();
2073 let mut pct = 0i32;
2074 let mut i = 0usize;
2075 let mut end_idx: Option<usize> = None;
2076 while i < chars.len() {
2077 if chars[i] == Inpar {
2078 pct += 1;
2079 }
2080 if pct == 1 {
2081 // c:1332-1334 — chuck blanks AFTER `|`/`(`.
2082 if chars[i] == Bar || chars[i] == Inpar {
2083 while i + 1 < chars.len() && blank(chars[i + 1]) {
2084 chars.remove(i + 1);
2085 }
2086 }
2087 // c:1335-1338 — chuck blanks BEFORE `|`/`)`
2088 // (not Meta-escaped blanks).
2089 if chars[i] == Bar || chars[i] == Outpar {
2090 while i >= 1
2091 && blank(chars[i - 1])
2092 && (i < 2 || chars[i - 2] != meta)
2093 {
2094 chars.remove(i - 1);
2095 i -= 1;
2096 }
2097 }
2098 }
2099 if chars[i] == Outpar {
2100 pct -= 1;
2101 if pct == 0 {
2102 end_idx = Some(i);
2103 break;
2104 }
2105 }
2106 i += 1;
2107 }
2108 if let Some(idx) = end_idx {
2109 // c:1346-1352 — the surrounding-paren strip is only
2110 // valid when the WHOLE string is `(...)` (close-paren
2111 // is the last char). C asserts this with
2112 // `DPUTS(*str != Inpar || str[sl-1] != Outpar)` and
2113 // `YYERRORV` on `if (*s || pct ...)` when anything
2114 // follows the matched close-paren. When trailing
2115 // content exists (`(a|b)*`, `(a|b)c`), the `(...)` is
2116 // a glob GROUP, not the optional case-opener — keep
2117 // the whole string verbatim as one pattern and let
2118 // the separate arm-closing `)` (OUTPAR_TOK) be
2119 // consumed below. Stripping here turned `(a|b)*` into
2120 // `a|b*` ("a" or "b*"), breaking OS-dispatch case arms.
2121 if idx == chars.len() - 1 {
2122 chars.remove(idx);
2123 chars.remove(0);
2124 str_val = chars.into_iter().collect();
2125 absorbed_outpar = true;
2126 }
2127 }
2128 }
2129 patterns.push(str_val);
2130 if absorbed_outpar {
2131 // c:Src/parse.c:1300-1302 — after a whole-`(...)`
2132 // pattern the next token may be the body's first
2133 // command word; C lexes it with `incasepat = -1;
2134 // incmdpos = 1;` so assignments (`out+=hit`)
2135 // become ENVSTRING instead of a plain STRING
2136 // (lex.c:1229-1230 gates ENVSTRING on incmdpos).
2137 set_incasepat(-1);
2138 set_incmdpos(true);
2139 } else {
2140 set_incasepat(2);
2141 }
2142 zshlex();
2143 // When the hack fired the closing `)` is already
2144 // consumed; don't read alt-`|` continuations either.
2145 if absorbed_outpar {
2146 break;
2147 }
2148 } else if tok() != BAR_TOK {
2149 break;
2150 }
2151
2152 if tok() == BAR_TOK {
2153 set_incasepat(1);
2154 zshlex();
2155 // c:1360-1367 — after consuming a `|`, C re-lexes and
2156 // switches on the token:
2157 // case OUTPAR: case BAR: /* Empty string */
2158 // str = dupstring(""); break;
2159 // so `spurble|)`, `a||b` and `durgle|)` all carry an
2160 // empty alternative. The old loop silently dropped it.
2161 if tok() == BAR_TOK || tok() == OUTPAR_TOK {
2162 patterns.push(String::new());
2163 }
2164 } else {
2165 break;
2166 }
2167 }
2168 } // end else of "skip legacy loop when nested-paren branch fired"
2169 set_incasepat(0);
2170
2171 // c:1305 — expect OUTPAR (arm-close) when the hack didn't
2172 // already swallow it.
2173 //
2174 // Bug #34 in docs/BUGS.md: the absorbed-pattern hack assumed
2175 // the leading `(` and the case-arm closing `)` were both
2176 // absorbed into the single STRING token. That's true for
2177 // `(x))` (the inner `)` closes the absorbed group; the second
2178 // `)` is the arm closer) only when the lexer slurps BOTH.
2179 // The Rust lexer slurps just `(x|y)` (one balanced pair); the
2180 // second `)` arrives as a separate OUTPAR_TOK that must still
2181 // be consumed as the case-arm closer. Detect and consume it.
2182 if !absorbed_outpar {
2183 if tok() != OUTPAR_TOK {
2184 // c:1356 — neither `)` nor `|` after the pattern:
2185 // `YYERRORV(oecused);`.
2186 set_tok(LEXERR); // c:87 `tok = LEXERR`
2187 return None;
2188 }
2189 // c:Src/parse.c:1257-1258 — `if (tok != STRING)
2190 // YYERRORV(oecused);` C requires at least one pattern
2191 // STRING before `)`. zshrs accepted empty `case x in)`
2192 // and silently fell through to the next iteration with
2193 // an empty pattern arm, swallowing the rest of the
2194 // script. Reject the empty-pattern shape unless a
2195 // leading INPAR was consumed (the `(pat)` form has
2196 // already validated the pattern inside). Bug #161 in
2197 // docs/BUGS.md.
2198 if patterns.is_empty() && !leading_inpar_consumed {
2199 set_tok(LEXERR); // c:1257 YYERRORV(oecused)
2200 return None;
2201 }
2202 set_incmdpos(true);
2203 zshlex();
2204 // When the lexer emitted a separate INPAR_TOK at the
2205 // arm start (consumed via `leading_inpar_consumed`
2206 // above), the OUTPAR_TOK we just consumed closed the
2207 // alternation GROUP. If the next token is ALSO
2208 // OUTPAR_TOK, the user wrote `(pat))` and that second
2209 // `)` is the case-arm closer that still needs to be
2210 // consumed before body parsing. Bug #34 in
2211 // docs/BUGS.md.
2212 if leading_inpar_consumed && tok() == OUTPAR_TOK {
2213 zshlex();
2214 }
2215 } else if tok() == OUTPAR_TOK {
2216 // The lexer absorbed `(pat)` as the pattern but left the
2217 // case-arm closing `)` as a separate OUTPAR_TOK. Consume
2218 // it now so body parsing starts at the body, not at `)`.
2219 set_incmdpos(true);
2220 zshlex();
2221 } else {
2222 set_incmdpos(true);
2223 }
2224
2225 // Parse body. Pass end_tokens explicitly so the body's
2226 // parser stops at DSEMI/SEMIAMP/SEMIBAR/ESAC without
2227 // tripping parse_program_until's orphan-terminator check
2228 // (line 7131) which only fires when end_tokens is None.
2229 // Without this, a case arm whose body has no trailing
2230 // `;;` before `esac` (last arm — zsh accepts the dangling
2231 // form) produced "parse error near orphan terminator" on
2232 // the closing `esac`. zsh's par_case at parse.c:1318 sets
2233 // up the case-arm reader to recognize the same terminator
2234 // set; the Rust port was passing the implicit-None and
2235 // hitting the top-level orphan check.
2236 let body = parse_program_until(Some(&[DSEMI, SEMIAMP, SEMIBAR, ESAC]), false);
2237
2238 // Get terminator. Set incasepat=1 BEFORE the zshlex
2239 // advance so the next token (the next arm's pattern, like
2240 // `[a-z]`) gets tokenized in pattern context. Without
2241 // this, a `[`-prefixed pattern after the FIRST arm became
2242 // Inbrack instead of String and the pattern-loop bailed
2243 // out with "expected ')' in case pattern".
2244 // c:Src/parse.c:1391 — `incasepat = 1; incmdpos = 0;` BEFORE
2245 // the zshlex that advances past `;;` to the next arm's first
2246 // token. The incmdpos=0 setting is what makes the lexer
2247 // absorb the next arm's `((add-|)fpath)` into a single STRING
2248 // (gettokstr's LX2_INPAR path at lex.c:1080+ runs only when
2249 // incmdpos is FALSE — at incmdpos=1 the lexer emits raw
2250 // INPAR_TOK for the inner `(` and par_case's pattern-read
2251 // loop has no path to recover). Our Rust port previously set
2252 // only incasepat=1 and not incmdpos=0, which forced
2253 // multi-arm patterns like `((add-|)fpath)` to fail with
2254 // "expected ')' in case pattern" — surfaced on
2255 // zinit.zsh:2946.
2256 let terminator = match tok() {
2257 DSEMI => {
2258 set_incasepat(1);
2259 set_incmdpos(false);
2260 zshlex();
2261 CaseTerm::Break
2262 }
2263 SEMIAMP => {
2264 set_incasepat(1);
2265 set_incmdpos(false);
2266 zshlex();
2267 CaseTerm::Continue
2268 }
2269 SEMIBAR => {
2270 set_incasepat(1);
2271 set_incmdpos(false);
2272 zshlex();
2273 CaseTerm::TestNext
2274 }
2275 // c:1386-1387 — `if ((tok == ESAC && !brflag) || (tok ==
2276 // OUTBRACE && brflag)) break;` — left for the loop top, which
2277 // consumes the closer.
2278 ESAC if !use_brace => CaseTerm::Break,
2279 STRING_LEX if !use_brace && tokstr().as_deref() == Some("esac") => CaseTerm::Break,
2280 OUTBRACE_TOK if use_brace => CaseTerm::Break,
2281 // c:1388-1389 — `if (tok != DSEMI && tok != SEMIAMP && tok !=
2282 // SEMIBAR) YYERRORV(oecused);`
2283 _ => {
2284 set_incasepat(0);
2285 set_tok(LEXERR); // c:87 `tok = LEXERR`
2286 return None;
2287 }
2288 };
2289
2290 if !patterns.is_empty() {
2291 arms.push(CaseArm {
2292 patterns,
2293 body,
2294 terminator,
2295 });
2296 }
2297 }
2298
2299 Some(ZshCommand::Case(ZshCase { word, arms }))
2300}
2301
2302/// Port of `par_if()` from `Src/parse.c:1411` — C decl `par_if(int *cmplx)`. AST variant: builds a `ZshIf` instead of emitting WC_IF wordcode.
2303/// Parse if statement
2304/// Parse `if COND; then BODY; [elif COND; then BODY;]* [else BODY;] fi`, plus
2305/// the brace forms `if COND { BODY } [elif COND { BODY }]* [else { BODY }]`.
2306///
2307/// Faithful port of zsh/Src/parse.c:1411 `par_if` — a SINGLE `for(;;)` loop that
2308/// processes the `if` arm and every `elif` arm through the same body code, so the
2309/// C's one `if (tok == SEPER) break;` after a brace body (c:1471-1472) covers
2310/// if- and elif-bodies uniformly. (The previous port split this into a pre-loop
2311/// if-body + a separate elif/else loop, which forced that single break to be
2312/// duplicated per arm and needed a special `fi`-gate; the single loop retires
2313/// all of that.) The C emits WC_IF wordcodes per arm; zshrs builds a ZshIf AST —
2314/// arm 0 -> (cond, then), later arms -> elif, plus an optional else_.
2315fn par_if() -> Option<ZshCommand> {
2316 // tok() == IF on entry (the dispatcher matched `if`); the loop consumes it.
2317 let mut if_cond: Option<Box<ZshProgram>> = None;
2318 let mut if_then: Option<Box<ZshProgram>> = None;
2319 let mut elif: Vec<(ZshProgram, ZshProgram)> = Vec::new();
2320 let mut else_: Option<Box<ZshProgram>> = None;
2321 // `usebrace` mirrors c:1449/1458 — the body form of the LAST arm parsed; it
2322 // gates whether a trailing `else {` is a brace-else (c:1492 `tok == INBRACE
2323 // && usebrace`) or, for a then-form arm, a brace-group statement inside the
2324 // else body that still ends at `fi` (p10k.zsh:5575).
2325 let mut usebrace = false;
2326 // Mirrors C's `xtok` where the loop exits, for the post-loop else check
2327 // (c:1487 `if (xtok == ELSE || tok == ELSE)`).
2328 let mut xtok_at_exit = IF;
2329 // c:1476-1482 — set when the SHORTLOOPS arm took the body; C `break`s
2330 // straight out of the arm loop there, so no `fi`/`else` may follow.
2331 let mut short_form = false;
2332
2333 loop {
2334 let xtok = tok(); // c:1420
2335 // c:1422-1425 — `fi` at the top closes a then-form if/elif chain.
2336 if xtok == FI {
2337 set_incmdpos(false);
2338 zshlex();
2339 xtok_at_exit = FI;
2340 break;
2341 }
2342 zshlex(); // c:1426 — consume IF / ELIF / ELSE
2343 // c:1428-1429 — `else` ends the arm loop (handled after it).
2344 if xtok == ELSE {
2345 xtok_at_exit = ELSE;
2346 break;
2347 }
2348 skip_separators(); // c:1430
2349 // c:1432-1435 — only IF / ELIF may open an arm.
2350 if xtok != IF && xtok != ELIF {
2351 set_tok(LEXERR); // c:1434 YYERRORV(oecused)
2352 return None;
2353 }
2354 // c:1438 — the cond is an ordinary list; a `{ … }` body opener ends it
2355 // via the no-separator chaining rule, so only THEN is a hard stop.
2356 // fast-syntax-highlighting.plugin.zsh:365-375:
2357 // if [[ … ]] { … } elif { type curl … } { … }
2358 // COND_LIST_DEPTH: suppress the Rust-only post-compound STRING guard
2359 // (see the thread_local's comment) for the condition list — a word
2360 // straight after `{ … }` here is the SHORTLOOPS body, not an error.
2361 COND_LIST_DEPTH.with(|d| d.set(d.get() + 1));
2362 let cond = parse_program_until(Some(&[THEN]), false);
2363 COND_LIST_DEPTH.with(|d| d.set(d.get() - 1));
2364 set_incmdpos(true); // c:1438
2365 // c:1440-1443 — `if (tok == ENDINPUT) { cmdpop();
2366 // YYERRORV(oecused); }`. YYERRORV only sets LEXERR; the message
2367 // ("parse error near `if'") comes from par_list's yyerror, so don't
2368 // invent one here — `eval "if"` must print zsh's wording
2369 // (A01grammar.ztst:505).
2370 if tok() == ENDINPUT {
2371 set_tok(LEXERR); // c:87 `tok = LEXERR`
2372 return None;
2373 }
2374 skip_separators(); // c:1444
2375 xtok_at_exit = FI; // c:1446 — `xtok = FI` sentinel for the else check
2376
2377 let mut this_arm_brace = false;
2378 let body = if tok() == THEN {
2379 // c:1448-1456 — classic `then … (fi|elif|else)`.
2380 zshlex(); // consume then
2381 let b = parse_program_until(Some(&[ELSE, ELIF, FI]), false);
2382 set_incmdpos(true);
2383 b
2384 } else if tok() == INBRACE_TOK {
2385 // c:1457-1473 — brace body `{ … }`.
2386 this_arm_brace = true;
2387 zshlex(); // consume {
2388 let b = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2389 if tok() != OUTBRACE_TOK {
2390 set_tok(LEXERR); // c:87 YYERRORV(oecused) — no own message
2391 return None;
2392 }
2393 zshlex(); // c:1469 — consume }
2394 set_incmdpos(true); // c:1470
2395 b
2396 } else if unset(SHORTLOOPS) {
2397 // c:1473-1475 — `else if (unset(SHORTLOOPS)) { cmdpop();
2398 // YYERRORV(oecused); }`. YYERRORV only flags LEXERR; the single
2399 // diagnostic ("parse error near `<zshlextext>'") comes from
2400 // par_list's yyerror. zshrs used to invent its own wording, so
2401 // `if true;\n:\nfi` said "expected `then' or `{' after if
2402 // condition" where zsh says "parse error near `fi'"
2403 // (A01grammar.ztst:241).
2404 set_tok(LEXERR); // c:87 `tok = LEXERR`
2405 return None;
2406 } else {
2407 // c:1476-1482 — SHORTLOOPS short form:
2408 // cmdpop(); cmdpush(nc); par_save_list1(cmplx);
2409 // ecbuf[pp] = WCB_IF(type, …); incmdpos = 1; break;
2410 // One sublist is the whole body and the `if` ENDS here — no
2411 // `fi`, no `else`. `if { true } print true`
2412 // (A01grammar.ztst:500) needs this; without the arm the parse
2413 // died on the body's first word.
2414 short_form = true;
2415 par_list1().map(|sublist| ZshProgram {
2416 lists: vec![ZshList {
2417 sublist,
2418 flags: ListFlags::default(),
2419 }],
2420 })?
2421 };
2422 usebrace = this_arm_brace;
2423
2424 // Record the arm: the first is the `if`, the rest are `elif`.
2425 if if_cond.is_none() {
2426 if_cond = Some(Box::new(cond));
2427 if_then = Some(Box::new(body));
2428 } else {
2429 elif.push((cond, body));
2430 }
2431
2432 // c:1471-1472 — after a brace body, a following separator ends the
2433 // construct; break WITHOUT consuming it so the outer par_list keeps the
2434 // separator between the if and the next command (otherwise that command
2435 // becomes a "STRING after compound" parse error). A then-form body
2436 // instead falls through to the loop top to read the next ELSE/ELIF/FI;
2437 // a brace body with no separator also falls through (e.g. `} elif`,
2438 // `} else` on the same line). Because a brace arm never reaches the
2439 // loop-top `fi`-consume, it cannot steal an enclosing then-form's `fi`.
2440 // ENDINPUT is NOT a separator: C breaks only on `tok == SEPER`, so
2441 // `if [[ x ]] { y }` at end of input loops back and YYERRORs
2442 // ("parse error near `}'"), exactly as zsh does.
2443 if this_arm_brace && matches!(tok(), SEPER | NEWLIN | SEMI) {
2444 break;
2445 }
2446 // c:1481 — the SHORTLOOPS arm ends with `break;` (with xtok still FI
2447 // from c:1445), so the construct is complete: no `fi`, no `else`.
2448 if short_form {
2449 break;
2450 }
2451 }
2452
2453 // c:1487-1505 — the optional else clause. When the loop exited via
2454 // `xtok == ELSE`, the `else` keyword was already consumed by the loop's
2455 // zshlex; the inner guard re-consumes only if we are somehow still on it.
2456 if xtok_at_exit == ELSE || tok() == ELSE {
2457 if tok() == ELSE {
2458 zshlex();
2459 }
2460 skip_separators(); // c:1490
2461 let ebody = if usebrace && tok() == INBRACE_TOK {
2462 // c:1492-1497 — brace else (only when the preceding arm was brace).
2463 zshlex(); // consume {
2464 let b = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2465 if tok() != OUTBRACE_TOK {
2466 set_tok(LEXERR); // c:87 YYERRORV(oecused) — no own message
2467 return None;
2468 }
2469 zshlex(); // consume }
2470 b
2471 } else {
2472 // c:1498-1502 — then-form else, stops at and consumes `fi`.
2473 let b = parse_program_until(Some(&[FI]), false);
2474 if tok() != FI {
2475 set_tok(LEXERR); // c:1503 YYERRORV(oecused)
2476 return None;
2477 }
2478 zshlex(); // consume fi
2479 b
2480 };
2481 set_incmdpos(false); // c:1502 — incmdpos = 0 after the else body
2482 else_ = Some(Box::new(ebody));
2483 }
2484
2485 let cond = match if_cond {
2486 Some(c) => c,
2487 None => {
2488 set_tok(LEXERR); // c:87 YYERRORV(oecused)
2489 return None;
2490 }
2491 };
2492 let then = if_then.expect("if_then set with if_cond");
2493 Some(ZshCommand::If(ZshIf {
2494 cond,
2495 then,
2496 elif,
2497 else_,
2498 }))
2499}
2500
2501/// Port of `par_while()` from `Src/parse.c:1521` — C decl `par_while(int *cmplx)`. AST variant; the `until` flag selects C's `tok == UNTIL` branch (c:1524).
2502/// Parse while/until loop
2503/// Parse `while COND; do BODY; done` and `until COND; do BODY; done`.
2504/// Direct port of zsh/Src/parse.c:1521 `par_while`. The
2505/// `until` variant is the same loop with the condition negated.
2506fn par_while(until: bool) -> Option<ZshCommand> {
2507 zshlex(); // skip while/until
2508
2509 // c:1521-1551 par_while — the condition's parser must stop at
2510 // `do` or `{`. Without an explicit end-token set, parse_program
2511 // consumes the brace-form body as additional condition lists,
2512 // leaving parse_loop_body with nothing — `while (( i++ < 3 )) {
2513 // echo $i }` silently parsed but executed nothing.
2514 // c:1528 — `par_save_list(cmplx);` — the cond is an ORDINARY
2515 // list: a leading `{...}` block is the cond's first command
2516 // (`while {false} {body}`), and the body INBRACE is reachable
2517 // because a list followed by `{` without a separator ends (the
2518 // chaining rule in parse_program_until). DOLOOP stays in the
2519 // stop set because `do` at command position can't start a
2520 // command. INBRACE_TOK was previously (wrongly) in the set too,
2521 // which made a brace-form COND parse as an empty cond + body.
2522 // COND_LIST_DEPTH: as for par_if's condition — a word straight after a
2523 // compound cond (`while (( i-- > 0 )) print $i`) is the SHORTLOOPS body
2524 // (c:1548-1549 `par_save_list1`), not a stray word after a compound.
2525 COND_LIST_DEPTH.with(|d| d.set(d.get() + 1));
2526 let cond = Box::new(parse_program_until(Some(&[DOLOOP]), false));
2527 COND_LIST_DEPTH.with(|d| d.set(d.get() - 1));
2528
2529 skip_separators();
2530 let body = parse_loop_body(false, false)?;
2531
2532 // c:Src/parse.c:1521-1551 par_while — WC_WHILE wordcode is tagged
2533 // with WC_WHILE_TYPE differentiating WHILE vs UNTIL at the wordcode
2534 // layer. The AST mirror in zsh_ast.rs has separate Until(ZshWhile)
2535 // and While(ZshWhile) variants; route by the `until` flag here so
2536 // downstream pattern-matchers can distinguish without poking
2537 // inside the payload's bool.
2538 let w = ZshWhile {
2539 cond,
2540 body: Box::new(body),
2541 until,
2542 };
2543 Some(if until {
2544 ZshCommand::Until(w) // c:1521 (WC_WHILE_TYPE = WC_WHILE_UNTIL)
2545 } else {
2546 ZshCommand::While(w) // c:1521 (WC_WHILE_TYPE = WC_WHILE_WHILE)
2547 })
2548}
2549
2550/// Port of `par_repeat()` from `Src/parse.c:1565` — C decl `par_repeat(int *cmplx)`. AST variant.
2551/// Parse repeat loop
2552/// Parse `repeat N; do BODY; done`. Direct port of
2553/// zsh/Src/parse.c:1565 `par_repeat`. The C source supports
2554/// the SHORTLOOPS short-form `repeat N CMD` (no do/done) — zshrs's
2555/// parser doesn't yet special-case that variant.
2556fn par_repeat() -> Option<ZshCommand> {
2557 // c:1572 — `incmdpos = 0;` BEFORE lexing the count word, so
2558 // checkalias's `(incmdpos && tok == STRING)` gate stays closed and
2559 // the count is NEVER alias-expanded. OMZL::directories.zsh defines
2560 // `alias 1='cd -1'`; without this, `repeat 1; do …; done` (hsmw
2561 // file:114) expanded `1` mid-header and errored "parse error near
2562 // `do'" (#665, sibling of the funcdef-name bug #662).
2563 set_incmdpos(false);
2564 zshlex(); // skip 'repeat'
2565
2566 let count = match tok() {
2567 STRING_LEX => {
2568 let c = tokstr().unwrap_or_default();
2569 // c:1577-1578 — `incmdpos = 1; zshlex();` — restore command
2570 // position before the separator/`do` lex so the body's
2571 // reserved words promote again.
2572 set_incmdpos(true);
2573 zshlex();
2574 c
2575 }
2576 _ => {
2577 set_tok(LEXERR); // c:1575 YYERRORV(oecused)
2578 return None;
2579 }
2580 };
2581
2582 skip_separators();
2583 // c:1600 — par_repeat's short-form gate is wider: it unlocks
2584 // when SHORTLOOPS OR SHORTREPEAT is set (vs SHORTLOOPS alone for
2585 // for/while). Pass `is_repeat=true` so parse_loop_body
2586 // applies that widened gate.
2587 let body = parse_loop_body(false, true)?;
2588
2589 Some(ZshCommand::Repeat(ZshRepeat {
2590 count,
2591 body: Box::new(body),
2592 }))
2593}
2594
2595/// Port of `par_subsh()` from `Src/parse.c:1619` — C decl `par_subsh(int *cmplx, int zsh_construct)`. AST variant, `(...)` arm only.
2596/// Parse (...) subshell
2597/// Parse a subshell `( ... )`. Direct port of zsh/Src/parse.c:1619
2598/// `par_subsh`. Body parses as a normal list; the subshell wrapper
2599/// fork-isolates execution in the executor.
2600fn par_subsh(zsh_construct: bool) -> Option<ZshCommand> {
2601 zshlex(); // skip (
2602 // c:Src/parse.c:par_subsh — `parse_event(OUTPAR)` parses until
2603 // the matching `)`. zshrs's previous port called bare
2604 // `parse_program()` (parse_program_until(None, false)) which has no
2605 // way to know it should stop at OUTPAR_TOK — at top-level
2606 // that's fine (the outer loop just sees an extra OUTPAR after
2607 // the inner body), but the parse_event-equivalent's new
2608 // yyerror-on-unconsumed-token behavior at parse_program_until's
2609 // None arm now reports a spurious "parse error near `)'" when
2610 // the construct ends. Pass OUTPAR_TOK so parse_program_until
2611 // stops cleanly at the closing paren.
2612 let prog = parse_program_until(Some(&[OUTPAR_TOK]), false);
2613 // c:1630-1631 — `if (tok != ((otok == INPAR) ? OUTPAR : OUTBRACE))
2614 // YYERRORV(oecused);`. This arm is only reached from par_cmd's
2615 // INPAR_TOK case (c:1010-1014), so the expected closer is OUTPAR.
2616 // Silently accepting a missing `)` let an unterminated subshell parse
2617 // clean: `zsh -fc '('` is a parse error with status 1, while zshrs
2618 // returned 0 and ran nothing. `((` inherits the same path (the lexer
2619 // emits two INPARs), which is why `((1` reached the command layer and
2620 // reported `command not found: 1` instead of a parse error.
2621 if tok() != OUTPAR_TOK {
2622 set_tok(LEXERR); // c:1631 YYERRORV(oecused)
2623 return None;
2624 }
2625 set_incmdpos(!zsh_construct); // c:1632
2626 zshlex(); // c:1633
2627 Some(ZshCommand::Subsh(Box::new(prog)))
2628}
2629
2630/// Port of `par_funcdef()` from `Src/parse.c:1672` — C decl `par_funcdef(int *cmplx)`. AST variant.
2631/// Parse function definition
2632/// Parse `function NAME { BODY }` or `NAME () { BODY }`. Direct
2633/// port of zsh/Src/parse.c:1672 `par_funcdef`. zsh handles
2634/// the multiple keyword shapes (function FOO, FOO (), function FOO ()),
2635/// the optional `[fname1 fname2 ...]` for multi-name function defs,
2636/// and the `function FOO () { ... }` traditional/POSIX hybrid form.
2637fn par_funcdef() -> Option<ZshCommand> {
2638 // c:1680-1681 — `nocorrect = 1; incmdpos = 0;` BEFORE lexing the
2639 // name. incmdpos=0 keeps checkalias's `(incmdpos && tok == STRING)`
2640 // gate closed, so the word after `function` is NEVER alias-expanded
2641 // (`alias ts='cd x'; function ts { }` must define `ts`, not `cd`).
2642 // nocorrect=1 likewise suppresses spell-correction on the name.
2643 set_nocorrect(1);
2644 set_incmdpos(false);
2645 zshlex(); // skip 'function'
2646
2647 let mut names = Vec::new();
2648 let mut tracing = false;
2649
2650 // Handle options like -T and function names. Two subtleties:
2651 //
2652 // 1. Flags: zsh's lexer encodes a leading `-` as
2653 // `zsh_h::Dash` (`\u{e19b}`, `Src/zsh.h:182`) inside the String tokstr.
2654 // The previous `s.starts_with('-')` check failed for
2655 // `\u{e19b}T`, so `function -T NAME { body }` slipped the
2656 // `-T` token into `names` and the function got registered
2657 // as `T` plus the intended `NAME`.
2658 //
2659 // 2. Body opener: zsh's lexer emits the opening `{` as a
2660 // String (not INBRACE_TOK) when it follows the String
2661 // NAME — the preceding name token resets incmdpos to
2662 // false, and only `{` immediately followed by `}` (the
2663 // empty-body case) gets promoted to Inbrace. The funcdef
2664 // parser must recognise the bare-`{` String as the body
2665 // opener; otherwise `function NAME { body }` falls through
2666 // to `_ => break`, no body parses, and the FuncDef never
2667 // lands in the AST. This is consistent with C zsh's
2668 // par_funcdef which knows it's in funcdef-header context
2669 // and accepts the brace either way.
2670 // c:Src/parse.c:1687-1698 —
2671 // /* Consume an initial (-T), (--), or (-T --).
2672 // * Anything else is a literal function name.
2673 // */
2674 // if (tok == STRING && tokstr[0] == Dash) {
2675 // if (tokstr[1] == 'T' && !tokstr[2]) { ++do_tracing; zshlex(); }
2676 // if (tok == STRING && tokstr[0] == Dash &&
2677 // tokstr[1] == Dash && !tokstr[2]) { zshlex(); }
2678 // }
2679 // The option prologue runs EXACTLY ONCE, before the name wordlist, and
2680 // `--` ends it: `function -- -T { … }` defines a function literally
2681 // named `-T`, and `function -- { … }` is a plain anonymous function.
2682 // The old code tested for `-T` inside the name loop instead, so `--`
2683 // was taken as a NAME and a post-`--` `-T` was eaten as the option.
2684 {
2685 let lead: Option<Vec<char>> = if tok() == STRING_LEX {
2686 tokstr().map(|t| t.chars().collect())
2687 } else {
2688 None
2689 };
2690 let is_dash = |c: char| c == '-' || c == Dash;
2691 if let Some(sc) = lead {
2692 if sc.first().copied().is_some_and(is_dash) {
2693 // c:1689-1692 — the `-T` tracing option.
2694 if sc.len() == 2 && sc[1] == 'T' {
2695 tracing = true;
2696 zshlex();
2697 }
2698 // c:1693-1697 — an immediately-following `--` ends options.
2699 let after: Option<Vec<char>> = if tok() == STRING_LEX {
2700 tokstr().map(|t| t.chars().collect())
2701 } else {
2702 None
2703 };
2704 if let Some(a) = after {
2705 if a.len() == 2 && is_dash(a[0]) && is_dash(a[1]) {
2706 zshlex();
2707 }
2708 }
2709 }
2710 }
2711 }
2712
2713 loop {
2714 match tok() {
2715 STRING_LEX => {
2716 let _ts_s = tokstr()?;
2717 let s = _ts_s.as_str();
2718 // c:1702 — `if ((*tokstr == Inbrace || *tokstr == '{') && !tokstr[1])`.
2719 // Body opener can be either the literal `{` (early-return
2720 // path at lex.c:1141-1144 / lex.rs LX2_INBRACE cmdpos
2721 // branch) or the Inbrace marker `\u{e18f}` (lex.c:1420
2722 // post-switch add(c) where c was rewritten via lextok2).
2723 if s == "{" || s == "\u{e18f}" {
2724 break;
2725 }
2726 // c:Src/parse.c par_funcdef — `if (tokstr[0] == Dash &&
2727 // tokstr[1] == 'T' && !tokstr[2])`: ONLY the exact word
2728 // `-T` (Dash + 'T' + nothing else) is the tracing option.
2729 // Every other word — INCLUDING names that merely start
2730 // with `-` or `+` such as `-zui_std_button_ext` or `+foo`
2731 // — is a function NAME. The previous check skipped any
2732 // `-`/`+`-prefixed word as an option, so all 47 of ZUI's
2733 // `function -zui_std_*()` names were dropped, leaving an
2734 // anonymous `function { body }` that executed its body at
2735 // parse time (the cascade of "command not found: -zui_std_*"
2736 // and "bad substitution" while sourcing stdlib.lzui).
2737 // c:1700-1706 — past the option prologue above, EVERY
2738 // remaining STRING is a literal function name (including
2739 // `-T`, `--`, `-zui_std_*`, `+foo`, …).
2740 names.push(s.to_string());
2741 zshlex();
2742 }
2743 INBRACE_TOK | INOUTPAR | SEPER | NEWLIN => break,
2744 _ => break,
2745 }
2746 }
2747
2748 // c:1715-1716 — `nocorrect = 0; incmdpos = 1;` after the name
2749 // wordlist, BEFORE lexing past `()`/separators into the body, so
2750 // body keywords (`if`, `while`, …) get reserved-word promotion
2751 // again (the header ran with incmdpos=0 to keep names alias-free).
2752 set_nocorrect(0);
2753 set_incmdpos(true);
2754
2755 // Optional ()
2756 let saw_paren = tok() == INOUTPAR;
2757 // Track the byte position just past `()` / the most-recent separator, so an
2758 // UNBRACED short body (`function f () cmd`) can be sliced for `functions`
2759 // rendering. pos() AFTER a zshlex is already past the next token (one-token
2760 // lookahead), so record it BEFORE each advance (mirrors parse.rs:2484).
2761 let mut body_mark = crate::funcdef_capture::body_mark_begin();
2762 if saw_paren {
2763 zshlex();
2764 }
2765
2766 while tok() == SEPER || tok() == NEWLIN {
2767 crate::funcdef_capture::body_mark_restart(&mut body_mark);
2768 zshlex();
2769 }
2770
2771 // Body opener: real Inbrace OR a String containing the literal `{`
2772 // (early-return path) OR a String containing the Inbrace marker
2773 // `\u{e18f}` (bct++ path post-switch add). C parse.c:1702 handles
2774 // both string forms via `*tokstr == Inbrace || *tokstr == '{'`.
2775 let body_opener_is_string_brace =
2776 tok() == STRING_LEX && tokstr().map(|s| s == "{" || s == "\u{e18f}").unwrap_or(false);
2777 if tok() == INBRACE_TOK || body_opener_is_string_brace {
2778 // Capture body_start BEFORE the lexer advances past the
2779 // first body token. After the previous zshlex consumed
2780 // `{`, lexer.pos points just past `{` (which is where the
2781 // body source starts). The next `zshlex()` would advance
2782 // past the first token (`echo`), making body_start land
2783 // mid-body and lose the first word — `typeset -f f` would
2784 // print `a; echo b` for `{ echo a; echo b }`.
2785 // c:Src/parse.c:1690-1706 — par_funcdef requires a clean
2786 // body-opener brace when the anonymous form `function {body}`
2787 // is used (no names AND no `()`). zsh's lexer keeps the `{`
2788 // as its own STRING token via the lex.c:1141-1144 early-
2789 // return at command position, but the body brace must be
2790 // followed by whitespace for the inner par_list to find a
2791 // matching OUTBRACE — without a separator, the closing `}`
2792 // gets merged into the last word (`X}`) and par_list ends
2793 // without OUTBRACE, which C zsh reports as `parse error near
2794 // \`}'`. zshrs's lexer has the same `bct` semantics; reject
2795 // here at the parse step so the funcdef doesn't silently run
2796 // with the stray `}` attached. With names or `()` present,
2797 // the body brace is allowed even without a separator
2798 // (`function name {body}` and `function () {body}` both work
2799 // in zsh). Bug #60 in docs/BUGS.md.
2800 if names.is_empty() && !saw_paren {
2801 // Check the source byte IMMEDIATELY after the `{`. Tokenizing the
2802 // brace leaves `pos()` at `{`+2 — the lexer consumes the brace AND
2803 // the one separator char behind it — so that byte lives at
2804 // `pos() - 1`, NOT at `pos()`. Reading `pos()` looked one byte too
2805 // far and hit the first BODY character whenever exactly ONE space
2806 // followed the brace, so `function { echo x }` was rejected as
2807 // malformed while `function { echo x }` (two spaces, leaving a
2808 // second space under the peek) parsed. Both are valid in zsh.
2809 //
2810 // The genuinely malformed `function {body}` never reaches here:
2811 // with no separator the lexer folds `{body` into a single STRING
2812 // word instead of emitting a brace token, so this branch is not
2813 // taken and the parse fails downstream — which is what zsh does
2814 // too (`parse error near `}'`). Bug #60 in docs/BUGS.md.
2815 let next_byte = pos()
2816 .checked_sub(1)
2817 .and_then(|p| input_slice(p, pos()))
2818 .and_then(|s| s.bytes().next())
2819 .unwrap_or(b' ');
2820 if !matches!(next_byte, b' ' | b'\t' | b'\n' | b';') {
2821 // c:1735 YYERRORV(oecused) — par_event's yyerror names
2822 // zshlextext (`function {` at end of input → near `{').
2823 set_tok(LEXERR); // c:87 `tok = LEXERR`
2824 return None;
2825 }
2826 }
2827 crate::funcdef_capture::body_mark_restart(&mut body_mark);
2828 zshlex();
2829 // c:Src/parse.c — func body terminates at OUTBRACE_TOK.
2830 // Explicit end-token keeps the inner parse from hitting the
2831 // top-level stray-`}` arm (#168). Bug #167 family.
2832 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2833 // c:Src/parse.c:1733-1737 — `if (tok != OUTBRACE) { cmdpop();
2834 // ... YYERRORV(oecused); }`. Hard-error on missing close brace
2835 // so `function f { echo hi` doesn't silently register a half-
2836 // parsed body. Bug #405.
2837 if tok() != OUTBRACE_TOK {
2838 set_tok(LEXERR); // c:87 YYERRORV(oecused) — no own message
2839 return None;
2840 }
2841 // See the matching `NAME ()` arm below and Bug #642: slice
2842 // through pos() so the funcdef `}` is always inside the slice
2843 // (at EOF `pos()-1` excluded it), then strip that single trailing
2844 // brace — so a body ending in a balanced `} always { ... }` keeps
2845 // its close on the `.zwc` round-trip.
2846 let body_source = crate::funcdef_capture::body_text(body_mark)
2847 .map(|s| {
2848 let t = s.trim();
2849 // Strip the statement SEPARATOR that followed the funcdef
2850 // `}` FIRST — for `name() { body }; next` the slice ends in
2851 // `};`, so stripping the brace before the `;` left the `}`
2852 // stranded (`${functions[name]}` → `body }`, then re-parsed
2853 // to a `parse error near '}'`). Order: trailing separators →
2854 // closing brace → the body's own last-statement separator.
2855 let t = t.trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace());
2856 let t = t.strip_suffix('}').unwrap_or(t).trim_end();
2857 let t = t
2858 .trim_end_matches(|c: char| c == ';' || c == '\n')
2859 .trim_end();
2860 t.to_string()
2861 })
2862 .filter(|s| !s.is_empty());
2863 // c:Src/parse.c par_funcdef bakes alias expansion into the wordcode; see
2864 // vm_helper::funcdef_note_resolved_body.
2865 crate::vm_helper::funcdef_note_resolved_body(body_source.as_deref());
2866 zshlex();
2867
2868 // Anonymous form `function () { body } a b c` (with `()`) or
2869 // `function { body } a b c` (zsh-only shorthand, no `()`). No
2870 // name was collected. Mirror parse_anon_funcdef: synthesize
2871 // `_zshrs_anon_N`, collect trailing args, set auto_call_args
2872 // so compile_funcdef registers + immediately calls the
2873 // function with the args as positional params.
2874 if names.is_empty() {
2875 let mut args = Vec::new();
2876 while tok() == STRING_LEX {
2877 if let Some(s) = tokstr() {
2878 args.push(s);
2879 }
2880 zshlex();
2881 }
2882 static ANON_COUNTER: AtomicUsize = AtomicUsize::new(0);
2883 let n = ANON_COUNTER.fetch_add(1, Ordering::Relaxed);
2884 let name = format!("_zshrs_anon_kw_{}", n);
2885 return Some(ZshCommand::FuncDef(ZshFuncDef {
2886 names: vec![name],
2887 body: Box::new(body),
2888 tracing,
2889 keyword_form: true,
2890 auto_call_args: Some(args),
2891 body_source,
2892 }));
2893 }
2894
2895 Some(ZshCommand::FuncDef(ZshFuncDef {
2896 names,
2897 body: Box::new(body),
2898 tracing,
2899 keyword_form: true,
2900 auto_call_args: None,
2901 body_source,
2902 }))
2903 } else if unset(SHORTLOOPS) {
2904 // c:Src/parse.c:1742 — `else if (unset(SHORTLOOPS)) YYERRORV`.
2905 // Braceless short body (`function f () CMD`) requires SHORTLOOPS.
2906 set_tok(LEXERR); // c:1746 YYERRORV(oecused)
2907 None
2908 } else {
2909 // c:Src/parse.c:1747-1748 — `else par_list1(&c)`. The short body
2910 // is ONE sublist (a single command), NOT a full list. The prior
2911 // `par_list(false)` greedily consumed past the `;` (so `function
2912 // foo () print bar; foo` swallowed the trailing `foo` call and
2913 // errored "parse error near `foo'"). Use par_cmd for the single
2914 // command, matching the brace-less `foo() CMD` path
2915 // (parse_inline_funcdef). Bug surface: C04funcdef `function foo ()
2916 // print bar`.
2917 // Slice the raw short-body text (unbraced_body_start was captured
2918 // before the body token was lexed, above) so `functions`/`typeset -f`
2919 // can list it (fusevm shfuncs render from this raw `body`, not
2920 // getpermtext — hashtable.rs:1397). Without it `function f () print x`
2921 // listed as `f () { }` (empty).
2922 par_cmd(false).map(|cmd| {
2923 let body_source = crate::funcdef_capture::body_text(body_mark)
2924 .map(|s| {
2925 s.trim()
2926 .trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace())
2927 .to_string()
2928 })
2929 .filter(|s| !s.is_empty());
2930 // c:Src/parse.c par_funcdef bakes alias expansion into the wordcode; see
2931 // vm_helper::funcdef_note_resolved_body.
2932 crate::vm_helper::funcdef_note_resolved_body(body_source.as_deref());
2933 let list = ZshList {
2934 sublist: ZshSublist {
2935 pipe: ZshPipe {
2936 cmd,
2937 next: None,
2938 lineno: lineno(),
2939 merge_stderr: false,
2940 },
2941 next: None,
2942 flags: SublistFlags::default(),
2943 },
2944 flags: ListFlags::default(),
2945 };
2946 ZshCommand::FuncDef(ZshFuncDef {
2947 names,
2948 body: Box::new(ZshProgram { lists: vec![list] }),
2949 tracing,
2950 keyword_form: true,
2951 auto_call_args: None,
2952 body_source,
2953 })
2954 })
2955 }
2956}
2957
2958/// Port of `par_time()` from `Src/parse.c:1787` — C decl `par_time(void)`. AST variant: builds `ZshCommand::Time` instead of WCB_TIMED.
2959/// Parse time command
2960/// Parse `time CMD` (POSIX time keyword). Direct port of
2961/// zsh/Src/parse.c:1787 `par_time`. The `time` keyword
2962/// times the execution of the following pipeline / cmd.
2963fn par_time() -> Option<ZshCommand> {
2964 zshlex(); // skip 'time'
2965
2966 // Check if there's a pipeline to time
2967 if tok() == SEPER || tok() == NEWLIN || tok() == ENDINPUT {
2968 Some(ZshCommand::Time(None))
2969 } else {
2970 let sublist = par_sublist();
2971 Some(ZshCommand::Time(sublist.map(Box::new)))
2972 }
2973}
2974
2975/// Port of `par_dinbrack()` from `Src/parse.c:1810` — C signature `par_dinbrack(void)`. Body
2976/// parser inside `[[ ... ]]` — calls `par_cond` to emit the
2977/// condition wordcode then advances past `]]`.
2978pub fn par_dinbrack() -> Option<()> {
2979 // c:1810
2980 let oecused = ECUSED.get(); // c:1812 `int oecused = ecused;`
2981 set_incond(1); // c:1814
2982 set_incmdpos(false); // c:1815
2983 zshlex(); // c:1816
2984 let _ = par_cond(); // c:1817
2985 if tok() != DOUTBRACK {
2986 // c:1818
2987 // c:1819 — `YYERROR(oecused);`
2988 set_tok(LEXERR);
2989 ECUSED.set(oecused);
2990 return None;
2991 }
2992 set_incond(0); // c:1820
2993 set_incmdpos(true); // c:1821
2994 zshlex(); // c:1822
2995 Some(())
2996}
2997
2998/// Port of `par_simple()` from `Src/parse.c:1836` — C decl `par_simple(int *cmplx, int nr)`. AST variant.
2999/// Parse a simple command
3000/// Parse a simple command (assignments + words + redirections).
3001/// Direct port of zsh/Src/parse.c:1836 `par_simple` —
3002/// the largest single function in parse.c. Handles ENVSTRING/
3003/// ENVARRAY assignments at command head, intermixed redirs,
3004/// typeset-style multi-assignment commands, and the trailing
3005/// inout-par `()` that converts a simple command into an inline
3006/// function definition.
3007fn par_simple(mut redirs: Vec<ZshRedir>) -> Option<ZshCommand> {
3008 let mut typeset_reswd = false;
3009 // zshrs-only: the alias the command word (not an argument) came out of.
3010 let mut first_word_alias: Option<(String, usize)> = None;
3011 let mut assigns = Vec::new();
3012 let mut words = Vec::new();
3013 // c:1840 — `char *hasalias = input_hasalias();` — the alias (if any)
3014 // whose expansion the FIRST word of this command came out of. Refreshed
3015 // (only while still NULL) after every word-lexing zshlex at c:1916-1917 /
3016 // 1950-1951 / 1995-1996 / 2025-2026.
3017 let mut hasalias: Option<String> = crate::ported::input::input_hasalias();
3018
3019 // c:1934-1974 — `{var}>file` brace-FD detection is wired
3020 // INSIDE the words loop below (parse.rs:4940-4956) rather than
3021 // here at the head. The words-loop site sees the tok=STRING
3022 // `{varname}` followed by a REDIROP and routes into par_redir
3023 // with redir.varid populated. C does it inline at the start of
3024 // each STRING/TYPESET arm iteration; functionally equivalent.
3025
3026 // c:1843-1846 — leading-NOCORRECT prefix: `nocorrect echo hello`
3027 // emits a NOCORRECT token at the start of par_simple. C sets
3028 // `nocorrect = 1` and skips past via the `zshlex();` at the
3029 // for-loop tail (c:1907). zshrs's par_simple (AST) had no
3030 // NOCORRECT arm so the token was silently dropped and the
3031 // following command line evaporated — `nocorrect echo hello`
3032 // produced empty output.
3033 //
3034 // c:1844-1907 — ONE loop takes NOCORRECT and the leading assignments in
3035 // any order, so `z=1 nocorrect cmd` is as valid as `nocorrect z=1 cmd`.
3036 // Two consecutive loops (NOCORRECT first) rejected the former with a
3037 // parse error.
3038 loop {
3039 if tok() == NOCORRECT {
3040 set_nocorrect(1); // c:1846
3041 zshlex(); // c:1907 (loop-tail zshlex)
3042 continue;
3043 }
3044 if tok() != ENVSTRING && tok() != ENVARRAY {
3045 break; // c:1905-1906
3046 }
3047 if let Some(assign) = parse_assign() {
3048 // c:1856-1865 — the name is NUL-terminated inside tokstr.
3049 if tok() == ENVSTRING {
3050 // !!! RUST-ONLY !!! — C NUL-terminates the name INSIDE tokstr, and
3051 // `zshlextext` is that same buffer (lex.c:1980/2018). zshlex does not
3052 // refresh it at ENDINPUT (c:276 skips exalias), so a parse error at end
3053 // of input right after an assignment names only the name (`f() { x=1}`
3054 // → near `x'). LEX_ZSHLEXTEXT is an owned copy; write the cut through.
3055 crate::ported::lex::LEX_ZSHLEXTEXT.with_borrow_mut(|t| {
3056 if let Some(text) = t.as_mut().filter(|text| text.starts_with(assign.name.as_str())) {
3057 text.truncate(assign.name.len());
3058 }
3059 });
3060 }
3061 assigns.push(assign);
3062 }
3063 zshlex();
3064 }
3065
3066 // Parse words and redirections
3067 loop {
3068 // c:1916-1917 — `zshlex(); if (!hasalias) hasalias =
3069 // input_hasalias();` — C refreshes after every word-lexing advance.
3070 // The previous advance happened at the tail of the last iteration
3071 // (or in the caller for the first token), so the top of the loop is
3072 // the equivalent observation point.
3073 if hasalias.is_none() {
3074 hasalias = crate::ported::input::input_hasalias();
3075 }
3076 match tok() {
3077 ENVSTRING | ENVARRAY if words.is_empty() => {
3078 // c:1843-1907 — still in command position (no command
3079 // word collected yet): a `name=val` token here is a
3080 // PRE-COMMAND assignment, even when it follows a
3081 // redirection (`a=b 2>/dev/null c=d`). C's single
3082 // par_simple loop collects assignments and redirs in any
3083 // order until the command word; zshrs split that into a
3084 // leading-assignments while-loop (above) plus this main
3085 // loop, and the leading loop stopped at the first redir.
3086 // Without this arm the trailing `c=d` fell into the
3087 // typeset-word path below and was silently dropped, so
3088 // `a=b 2>/dev/null c=d` set neither parameter.
3089 if let Some(assign) = parse_assign() {
3090 // c:1856-1865 — the name is NUL-terminated inside tokstr.
3091 if tok() == ENVSTRING {
3092 // !!! RUST-ONLY !!! — write the cut through to zshlextext (see the
3093 // leading-assignment loop in par_simple).
3094 crate::ported::lex::LEX_ZSHLEXTEXT.with_borrow_mut(|t| {
3095 if let Some(text) = t.as_mut().filter(|text| text.starts_with(assign.name.as_str())) {
3096 text.truncate(assign.name.len());
3097 }
3098 });
3099 }
3100 assigns.push(assign);
3101 }
3102 zshlex();
3103 }
3104 ENVSTRING | ENVARRAY => {
3105 // Mid-command assignment-shape arg under typeset
3106 // / declare / local / etc. (intypeset gates the
3107 // lexer to emit Envstring/Envarray for `name=val`
3108 // and `name=()` past the command name). Parse the
3109 // assignment, then emit a synthetic word
3110 // `NAME=value` (scalar) or `NAME=( … )` (array)
3111 // string so typeset's builtin arg list sees the
3112 // assignment-shape arg. Avoids the inline-env
3113 // scope path that mistakenly treats it like a
3114 // pre-cmd `X=Y cmd` assignment.
3115 if let Some(assign) = parse_assign() {
3116 // c:2010-2019 — the mid-command scan has no `+=` arm, so
3117 // `equalsplit` NUL-terminates at the `=` and the `+`
3118 // stays part of the name left in tokstr. c:2032 likewise
3119 // `ecstr(tokstr)`s an ENVARRAY name whole. typeset_single
3120 // then rejects `x+` ("not valid in this context"); the
3121 // synthetic word dropped the `+` and assigned instead.
3122 let nul_at = if assign.append {
3123 format!("{}+", assign.name)
3124 } else {
3125 assign.name.clone()
3126 };
3127 if tok() == ENVSTRING {
3128 // !!! RUST-ONLY !!! — write the cut through to zshlextext (see the
3129 // leading-assignment loop in par_simple).
3130 crate::ported::lex::LEX_ZSHLEXTEXT.with_borrow_mut(|t| {
3131 if let Some(text) = t.as_mut().filter(|text| text.starts_with(nul_at.as_str())) {
3132 text.truncate(nul_at.len());
3133 }
3134 });
3135 }
3136 let synthetic = match &assign.value {
3137 ZshAssignValue::Scalar(v) => format!("{}={}", nul_at, v),
3138 ZshAssignValue::Array(elems) => {
3139 // c:Src/builtin.c — assoc paren-init `h=( "" v
3140 // k2 v2 )` must preserve empty-string
3141 // elements (zsh stores key="" + value="v").
3142 // The bin_typeset paren-init splitter at
3143 // `builtin.rs:4358` recognizes the
3144 // REJOIN_SEP (`\u{1f}`) sentinel between
3145 // array elements and skips the leading/
3146 // trailing parens trim; using it here
3147 // round-trips empties end-to-end through
3148 // the synthetic-arg rebuild. Space-join
3149 // collapses adjacent empties (`(` + `""` +
3150 // `empty-val` becomes `( empty-val`) so
3151 // bin_typeset never sees the empty key.
3152 // Bug #93 in docs/BUGS.md.
3153 let mut buf = String::with_capacity(
3154 nul_at.len()
3155 + 4
3156 + elems.iter().map(|e| e.len() + 1).sum::<usize>(),
3157 );
3158 buf.push_str(&nul_at);
3159 buf.push_str("=(");
3160 for elem in elems {
3161 buf.push('\u{1f}');
3162 buf.push_str(elem);
3163 }
3164 buf.push('\u{1f}');
3165 buf.push(')');
3166 buf
3167 }
3168 };
3169 words.push(synthetic);
3170 }
3171 zshlex();
3172 }
3173 STRING_LEX | TYPESET => {
3174 if words.is_empty() {
3175 first_word_alias =
3176 crate::intercepts::alias_origin(crate::ported::input::input_hasalias());
3177 }
3178 // c:1931-1932 — the reserved-word head decides WC_TYPESET.
3179 if words.is_empty() && tok() == TYPESET {
3180 typeset_reswd = true;
3181 }
3182 let s = tokstr();
3183 if let Some(s) = s {
3184 words.push(s);
3185 }
3186 // c:1929 — `incmdpos = 0;` so the next zshlex() does
3187 // not re-promote `{`/`[[`/reserved words at the
3188 // continuation position. Without this, `echo {a,b}`
3189 // re-lexes `{` as INBRACE_TOK (current-shell block)
3190 // and the brace expansion never reaches par_simple.
3191 set_incmdpos(false);
3192 // c:1931-1932 — `if (tok == TYPESET) intypeset = is_typeset = 1;`
3193 // Multi-assign `typeset a=1 b=2` relies on the lexer
3194 // re-emitting `b=2` as ENVSTRING; that path is gated
3195 // on `intypeset`. Without this, follow-on assignment
3196 // words arrive as STRING and the typeset builtin's
3197 // multi-assign form silently degrades.
3198 if tok() == TYPESET {
3199 set_intypeset(true);
3200 }
3201 zshlex();
3202 // c:1950-1951 — `if (!hasalias) hasalias = input_hasalias();`
3203 if hasalias.is_none() {
3204 hasalias = crate::ported::input::input_hasalias();
3205 }
3206 // Check for function definition foo() { ... }
3207 if words.len() == 1 && tok() == INOUTPAR {
3208 // c:2058-2060 — `/* Error if preceding assignments */
3209 // if (assignments || postassigns) YYERROR(oecused);` —
3210 // `x=1 f() { … }` is a parse error, not a definition.
3211 if !assigns.is_empty() {
3212 set_tok(LEXERR); // c:2060 YYERROR(oecused)
3213 return None;
3214 }
3215 // The ALIAS_FUNC_DEF gate applies to EVERY route into the
3216 // funcdef body, not just the match arm below; this early
3217 // return is the single-word shape.
3218 // c:2061-2068 — `if
3219 // (isset(EXECOPT) && hasalias && !isset(ALIASFUNCDEF) &&
3220 // argc && hasalias != input_hasalias()) {
3221 // zwarn("defining function based on alias `%s'",
3222 // hasalias); YYERROR(oecused); }`
3223 //
3224 // 5.9.2 parse.c:2057-2061 is just `zwarn(...); YYERROR(oecused);` — no herrflush/errflag
3225 // (the dev tree adds them), so yyerror still reports `parse error near `()'` after the warning.
3226 if isset(EXECOPT)
3227 && hasalias.is_some()
3228 && !isset(ALIASFUNCDEF)
3229 && words.len() != 0
3230 && hasalias != crate::ported::input::input_hasalias()
3231 {
3232 crate::ported::utils::zwarn(&format!(
3233 // c:2058
3234 "defining function based on alias `{}'",
3235 hasalias.as_deref().unwrap_or("")
3236 ));
3237 set_tok(LEXERR); // c:2067 YYERROR
3238 return None;
3239 }
3240 return parse_inline_funcdef(std::mem::take(&mut words));
3241 }
3242 // `{name}>file` named-fd redirect: the lexer doesn't
3243 // recognize this shape, so the bare word `{name}`
3244 // arrives as a String. If it matches `{IDENT}` and
3245 // the NEXT token is a redirop, pop it off as the
3246 // varid for that redir.
3247 if !words.is_empty() && IS_REDIROP(tok()) {
3248 let last = words.last().unwrap();
3249 let untoked = super::lex::untokenize(last);
3250 if untoked.starts_with('{') && untoked.ends_with('}') && untoked.len() > 2 {
3251 let name = &untoked[1..untoked.len() - 1];
3252 // c:1944 — `if (itype_end(tokstr+1, IIDENT, 0) >= ptr)`:
3253 // every char between the braces must be an IIDENT char.
3254 // IIDENT (utils.c:4173/4190) covers digits, letters and
3255 // `_` — with NO first-char restriction, so an all-digit
3256 // name like `{1}` (a positional param, used by p10k's
3257 // `exec {1}>&-`) is a valid fd var. The prior check also
3258 // required the first char to be `_`/alphabetic, which
3259 // rejected `{1}`/`{2}`/`{12}` — zsh accepts them.
3260 if !name.is_empty() && name.bytes().all(crate::ported::ztype_h::iident) {
3261 let varid = name.to_string();
3262 words.pop();
3263 if let Some(mut redir) = par_redir() {
3264 redir.varid = Some(varid);
3265 redirs.push(redir);
3266 }
3267 continue;
3268 }
3269 }
3270 }
3271 }
3272 _ if IS_REDIROP(tok()) => {
3273 match par_redir() {
3274 Some(redir) => redirs.push(redir),
3275 None => break, // Error in redir parsing, stop
3276 }
3277 }
3278 INOUTPAR if !words.is_empty() => {
3279 // c:2055-2057 — `if (!isset(MULTIFUNCDEF) && argc > 1)
3280 // YYERROR(oecused);` — multi-name funcdef gate:
3281 // `f1 f2() { ... }` defines f1 AND f2 to the same
3282 // body, but only when MULTIFUNCDEF is set.
3283 if !isset(MULTIFUNCDEF) && words.len() > 1 {
3284 set_tok(LEXERR); // c:2057 YYERROR(oecused)
3285 return None;
3286 }
3287 // c:2058-2060 — `/* Error if preceding assignments */ if
3288 // (assignments || postassigns) YYERROR(oecused);` —
3289 // `x=1 f() { … }` is a parse error, not a definition.
3290 if !assigns.is_empty() {
3291 set_tok(LEXERR); // c:2060 YYERROR(oecused)
3292 return None;
3293 }
3294 // c:2061-2068 — `if
3295 // (isset(EXECOPT) && hasalias && !isset(ALIASFUNCDEF) &&
3296 // argc && hasalias != input_hasalias()) {
3297 // zwarn("defining function based on alias `%s'", hasalias);
3298 // YYERROR(oecused); }`
3299 //
3300 // 5.9.2 parse.c:2057-2061 is just `zwarn(...); YYERROR(oecused);` — no herrflush/errflag
3301 // (the dev tree adds them), so yyerror still reports `parse error near `()'` after the warning.
3302 if isset(EXECOPT)
3303 && hasalias.is_some()
3304 && !isset(ALIASFUNCDEF)
3305 && words.len() != 0
3306 && hasalias != crate::ported::input::input_hasalias()
3307 {
3308 crate::ported::utils::zwarn(&format!(
3309 // c:2058
3310 "defining function based on alias `{}'",
3311 hasalias.as_deref().unwrap_or("")
3312 ));
3313 set_tok(LEXERR); // c:2067 YYERROR
3314 return None;
3315 }
3316 // foo() { ... } / multi-name `f1 f2 f3() { ... }` style
3317 // function. c:Src/parse.c:2055-2068 — under MULTIFUNCDEF
3318 // every preceding word names the SAME body; pass them all
3319 // so each is registered (the gate above already rejected
3320 // multi-name without MULTIFUNCDEF). Previously only the
3321 // last word (`words.pop()`) was defined, so `f1 f2 f3()
3322 // {...}` left f1/f2 as "command not found".
3323 return parse_inline_funcdef(std::mem::take(&mut words));
3324 }
3325 _ => break,
3326 }
3327 }
3328
3329 if assigns.is_empty() && words.is_empty() && redirs.is_empty() {
3330 return None;
3331 }
3332
3333 Some(ZshCommand::Simple(ZshSimple {
3334 assigns,
3335 words,
3336 redirs,
3337 typeset_reswd,
3338 via_alias: first_word_alias,
3339 }))
3340}
3341
3342/// Port of `par_redir()` from `Src/parse.c:2229` — C decl `par_redir(int *rp, char *idstring)`. AST variant; the body lives in `par_redir_with_id` (C's `idstring` parameter).
3343/// Parse a redirection
3344/// Parse a redirection (>file, <file, >>file, <<HEREDOC, etc.).
3345/// Direct port of zsh/Src/parse.c:2229 `par_redir`. Returns
3346/// a ZshRedir node carrying the operator type, fd, target word
3347/// (or here-doc body / pipe-redir command), and any `{var}` style
3348/// fd-binding parameter.
3349fn par_redir() -> Option<ZshRedir> {
3350 par_redir_with_id(None)
3351}
3352
3353/// Wire a here-document body onto the redirection token that
3354/// requested it. Direct port of zsh/Src/parse.c:2347
3355/// `setheredoc`. Called when a heredoc terminator has been
3356/// matched and the body is ready to be attached to the redir.
3357///
3358/// zshrs port note: zsh's setheredoc patches the wordcode
3359/// in-place via `pc[1] = ecstrcode(doc); pc[2] = ecstrcode(term);`.
3360/// zshrs threads heredoc bodies through `HereDocInfo` structs
3361/// attached inline during the post-parse `fill_heredoc_bodies` walk.
3362/// This method is the AST-side equivalent: writes back to the
3363/// matching redir node by index.
3364/// Port of `setheredoc()` from `Src/parse.c:2347` — C decl `setheredoc(int pc, int type, char *str, char *termstr, char *munged_termstr)`. AST variant: writes back into the `ZshRedir` node instead of patching `ecbuf`.
3365/// Patches the
3366/// pending heredoc redir at `pc` with its body string + raw and
3367/// munged terminator forms.
3368pub fn setheredoc(pc: usize, redir_type: i32, doc: &str, term: &str, munged_term: &str) {
3369 // zshrs-only guard: AST-path heredocs use `pc = -1 as usize`
3370 // (i.e. `usize::MAX`) as a sentinel meaning "no wordcode slot to
3371 // patch". C never passes a negative pc since the wordcode emitter
3372 // is always active. Skip silently for the AST-only case.
3373 if pc == usize::MAX {
3374 return;
3375 }
3376 // c:2350 — `int varid = WC_REDIR_VARID(ecbuf[pc]) ? REDIR_VARID_MASK : 0;`
3377 let cur = ECBUF.with_borrow(|b| b.get(pc).copied().unwrap_or(0));
3378 let varid = if WC_REDIR_VARID(cur) != 0 {
3379 REDIR_VARID_MASK
3380 } else {
3381 0
3382 };
3383 // c:2351 — `ecbuf[pc] = WCB_REDIR(type | REDIR_FROM_HEREDOC_MASK | varid);`
3384 let new_header = WCB_REDIR((redir_type | REDIR_FROM_HEREDOC_MASK | varid) as wordcode);
3385 // c:2352 — `ecbuf[pc + 2] = ecstrcode(str);`
3386 let coded_str = ecstrcode(doc);
3387 // c:2353 — `ecbuf[pc + 3] = ecstrcode(termstr);`
3388 let coded_term = ecstrcode(term);
3389 // c:2354 — `ecbuf[pc + 4] = ecstrcode(munged_termstr);`
3390 let coded_munged = ecstrcode(munged_term);
3391 ECBUF.with_borrow_mut(|b| {
3392 b[pc] = new_header;
3393 b[pc + 2] = coded_str;
3394 b[pc + 3] = coded_term;
3395 b[pc + 4] = coded_munged;
3396 });
3397}
3398
3399/// Port of `par_wordlist()` from `Src/parse.c:2362` — C decl `par_wordlist(void)`. AST variant: returns the words instead of `ecstr`-ing them.
3400/// Parse a wordlist for `for ... in WORDS;`. Direct port of
3401/// zsh/Src/parse.c:2362 `par_wordlist`. Reads STRING tokens
3402/// until the next SEPER / SEMI / NEWLIN.
3403pub fn par_wordlist() -> Vec<String> {
3404 let mut out = Vec::new();
3405 // parse.c:2362-2378 — collect STRINGs into the wordlist.
3406 while tok() == STRING_LEX {
3407 if let Some(text) = tokstr() {
3408 out.push(text);
3409 }
3410 zshlex();
3411 }
3412 out
3413}
3414
3415/// Port of `par_nl_wordlist()` from `Src/parse.c:2379` — C decl `par_nl_wordlist(void)`. AST variant.
3416/// Parse a newline-separated wordlist. Direct port of
3417/// zsh/Src/parse.c:2379 `par_nl_wordlist`. Like
3418/// par_wordlist but tolerates leading/trailing newlines.
3419pub fn par_nl_wordlist() -> Vec<String> {
3420 // parse.c:2380-2381 — skip leading newlines.
3421 while tok() == NEWLIN {
3422 zshlex();
3423 }
3424 let out = par_wordlist();
3425 // parse.c:2395-2397 — skip trailing newlines.
3426 while tok() == NEWLIN {
3427 zshlex();
3428 }
3429 out
3430}
3431
3432/// Port of `COND_SEP()` from `Src/parse.c:2405` — C decl `#define COND_SEP() (tok == SEPER && condlex != testlex && *zshlextext != ';')`. C macro.
3433/// `COND_SEP()` macro from `Src/parse.c:2405`:
3434/// `(tok == SEPER && condlex != testlex && *zshlextext != ';')`
3435/// A newline (but NOT a `;`) between cond terms inside `[[ … ]]` is
3436/// skippable whitespace; a `;` is a parse error (`[[ a ; ]]`).
3437///
3438/// zshrs has two lexer modes feeding the two cond parsers:
3439/// - wordcode path (autoload / parse_string): LEXFLAGS_NEWLINE off,
3440/// so a newline is FOLDED to SEPER (lex.rs:265). C's `[[ a &&\n b ]]`
3441/// lands here; matching only NEWLIN missed it → "condition
3442/// expected" (broke every multi-line `&&`/`||` cond in compinit and
3443/// the compsys completers).
3444/// - AST path (`-c`, source): LEXFLAGS_NEWLINE on, newline stays
3445/// NEWLIN (unfolded).
3446/// `;` ALWAYS folds to SEPER (lex.rs:265, unconditional for SEMI) with
3447/// LEX_ISNEWLIN==0, so gating the SEPER arm on LEX_ISNEWLIN!=0
3448/// reproduces C's `*zshlextext != ';'` exclusion in both modes.
3449#[inline]
3450pub fn COND_SEP() -> bool {
3451 // c:2403 — `condlex != testlex`: the test-mode lexer never yields SEPER.
3452 if CONDLEX_TESTLEX.get() {
3453 return false;
3454 }
3455 match tok() {
3456 NEWLIN => true,
3457 SEPER => crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) != 0,
3458 _ => false,
3459 }
3460}
3461
3462/// Port of `par_cond()` from `Src/parse.c:2409` — C decl `par_cond(void)`. AST variant. It also inlines `par_dinbrack`'s wrapper (c:1810-1822): `incond`/`incmdpos` set, `[[` skipped, `]]` required.
3463/// Parse [[ ... ]] conditional
3464/// Parse `[[ EXPR ]]` conditional expression. Direct port of
3465/// zsh/Src/parse.c:2409 `par_cond` (and helpers par_cond_1,
3466/// par_cond_2, par_cond_double, par_cond_triple, par_cond_multi
3467/// at parse.c:2434-2731). Expression operators: `||` `&&` `!`
3468/// + unary tests (-f, -d, -n, -z, etc.) + binary tests (=, !=,
3469/// <, >, ==, =~, -eq, -ne, -lt, -le, -gt, -ge, -nt, -ot, -ef).
3470fn par_cond() -> Option<ZshCommand> {
3471 // C par_dinbrack (parse.c:1810-1822) wraps the body parse with
3472 // `incond = 1; incmdpos = 0;` BEFORE the first zshlex past `[[`,
3473 // and resets to `incond = 0; incmdpos = 1;` after `]]`. Without
3474 // `incond = 1`, lex.c does not promote `]]` to DOUTBRACK and the
3475 // cond body bleeds past the close bracket — the parser then
3476 // sees `]]` as a separate STRING command. Every `if [[ ... ]]; then`
3477 // failed with `command not found: ]]` before this fix.
3478 set_incond(1);
3479 set_incmdpos(false);
3480 zshlex(); // skip [[
3481 // c:1817 — no empty-cond special case here. `]]` reaches
3482 // par_cond_2 as a DOUTBRACK that still carries its tokstr
3483 // (lex.c:2010-2012 retypes the STRING in place), so
3484 // c:2553-2560 re-reads it as `[[ -n "]]" ]]` and the
3485 // MISSING `]]` is what fails, one token later — which is
3486 // why zsh reports `[[ ]];` near `;' and `[[ ]] print`
3487 // near `print', not near `]]'.
3488 let cond = parse_cond_expr();
3489
3490 if tok() == DOUTBRACK {
3491 set_incond(0);
3492 set_incmdpos(true);
3493 zshlex();
3494 } else {
3495 // c:Src/parse.c:1818-1819 — `if (tok != DOUTBRACK)
3496 // YYERRORV(oecused);`. par_dinbrack hard-requires DOUTBRACK
3497 // after par_cond; anything else is a parse error and the
3498 // outer parser's yyerror at c:2747 emits `parse error near
3499 // \`%s'` using zshlextext. Bug #473: BAR (`|`) inside
3500 // `[[ ab == a|b ]]` slipped past par_cond_or (which only
3501 // checks DBAR), the cond returned cleanly, and then the
3502 // top-level parser interpreted BAR as a pipe — running `b`
3503 // as a command (security-relevant if pattern RHS is user
3504 // input). Mirror C: emit parse error and abort.
3505 // c:1819 YYERRORV → tok = LEXERR, and the top-level parse
3506 // (c:673/708) calls yyerror once, which derives the token text
3507 // from `zshlextext` — tokstr when the token captured one, else
3508 // `tokstrings[tok]` (lex.c:1965). The port's yyerror already
3509 // does both, so hand-rolling a token-name table here only
3510 // produced a second, differently-worded message.
3511 yyerror(0);
3512 set_incond(0);
3513 set_incmdpos(true);
3514 return None;
3515 }
3516
3517 cond.map(ZshCommand::Cond)
3518}
3519
3520/// Port of `par_cond_1()` from `Src/parse.c:2434` — C signature `par_cond_1(void)`. Parses one
3521/// `||`-separated cond expression. Emits `WCB_COND(COND_AND, …)`
3522/// when an `&&` is found and recurses.
3523pub fn par_cond_1() -> i32 {
3524 // c:2434
3525
3526 let p = ECUSED.with(|c| c.get()) as usize;
3527 let r = par_cond_2();
3528 while COND_SEP() {
3529 condlex();
3530 }
3531 if tok() == DAMPER {
3532 condlex();
3533 while COND_SEP() {
3534 condlex();
3535 }
3536 ecispace(p, 1);
3537 par_cond_1();
3538 let ecused = ECUSED.with(|c| c.get()) as usize;
3539 ECBUF.with(|c| {
3540 c.borrow_mut()[p] = WCB_COND(COND_AND as u32, (ecused - 1 - p) as u32);
3541 });
3542 return 1;
3543 }
3544 r
3545}
3546
3547/// Port of `par_cond_2()` from `Src/parse.c:2476` — C signature `par_cond_2(void)`. The heavy
3548/// cond-term parser: handles `! cond`, `(cond)`, unary `[ -X arg ]`,
3549/// binary `[ A op B ]`, and `[ A op1 B op2 C … ]` n-ary chains.
3550pub fn par_cond_2() -> i32 {
3551 // c:2476
3552 // c:2480 — `(condlex == testlex) ? arrlen(testargs) + 1 : 0`. C's
3553 // `testargs` is a pointer into the argv; here it is an index into
3554 // `TESTARGS`, so `arrlen(testargs)` is the length less that index.
3555 let n_testargs: i32 = if CONDLEX_TESTLEX.get() {
3556 crate::ported::builtin::TESTARGS
3557 .with_borrow(|a| a.len() - crate::ported::builtin::TESTARGS_IDX.get())
3558 as i32
3559 + 1
3560 } else {
3561 0
3562 };
3563 // C's `*testargs`: the argument after the current token, `None` where C
3564 // sees the argv's NULL terminator.
3565 let next_testarg = || -> Option<String> {
3566 crate::ported::builtin::TESTARGS
3567 .with_borrow(|a| a.get(crate::ported::builtin::TESTARGS_IDX.get()).cloned())
3568 };
3569
3570 if n_testargs != 0 {
3571 // c:2482 — See the description of test in POSIX 1003.2.
3572 if tok() == crate::ported::zsh_h::NULLTOK {
3573 // c:2485 — no arguments: false.
3574 return par_cond_double("-n", "");
3575 }
3576 if n_testargs == 1 {
3577 // c:2487 — one argument: `[ foo ]` is equivalent to `[ -n foo ]`.
3578 let s1 = tokstr().unwrap_or_default();
3579 condlex();
3580 // c:2492 — ksh behavior: `[ -t ]` means `[ -t 1 ]`; bash disagrees.
3581 if unset(POSIXBUILTINS) && check_cond(&s1, "t") {
3582 return par_cond_double(&s1, "1");
3583 }
3584 return par_cond_double("-n", &s1);
3585 }
3586 if n_testargs > 2 {
3587 // c:2496 — three arguments: if the second argument is a binary
3588 // operator, perform that binary test on the first and third.
3589 let nx = next_testarg().unwrap_or_default();
3590 let mut nxc = nx.chars();
3591 if nx == "="
3592 || nx == "<"
3593 || nx == ">"
3594 || nx == "=="
3595 || nx == "!="
3596 || (nxc.next().map_or(false, IS_DASH) && get_cond_num(nxc.as_str()) >= 0)
3597 {
3598 let s1 = tokstr().unwrap_or_default(); // c:2505
3599 condlex();
3600 let s2 = tokstr().unwrap_or_default(); // c:2507
3601 condlex();
3602 let s3 = tokstr().unwrap_or_default(); // c:2509
3603 condlex();
3604 return par_cond_triple(&s1, &s2, &s3); // c:2511
3605 }
3606 }
3607 // c:2514-2516 — we fall through here on any non-numeric infix
3608 // operator or any other time there are at least two arguments.
3609 } else {
3610 while COND_SEP() {
3611 condlex();
3612 }
3613 }
3614 if tok() == BANG_TOK {
3615 // c:2521-2524 — in "test" compatibility mode, `! -a ...` and
3616 // `! -o ...` are treated as `[string] [and] ...` / `[string] [or] ...`.
3617 let nx = next_testarg();
3618 if !(n_testargs > 2
3619 && nx
3620 .as_deref()
3621 .map_or(false, |t| check_cond(t, "a") || check_cond(t, "o")))
3622 {
3623 // c:2528 — `[[ ! cond ]]`
3624 condlex();
3625 ecadd(WCB_COND(COND_NOT as u32, 0));
3626 return par_cond_2();
3627 }
3628 }
3629 if tok() == INPAR_TOK {
3630 // c:2533 — `[[ (cond) ]]`. Recurse into the WORDCODE cond OR-chain
3631 // `par_cond_top` (C's `par_cond`, parse.c:2409). The bare `par_cond`
3632 // in this crate is the AST `[[ ]]` parser — it opens with
3633 // `zshlex(); // skip [[`, so calling it here consumed the FIRST term
3634 // of the group (e.g. the `-z` in `[[ ( -z x ) ]]`) as if it were the
3635 // `[[`, then failed. Every parenthesised sub-condition parsed via
3636 // parse_string (autoload's function-body parse, `.zcompdump`, etc.)
3637 // hit `parse error`, which broke compinit/compaudit and thus all of
3638 // compsys.
3639 condlex();
3640 while COND_SEP() {
3641 condlex();
3642 }
3643 let r = par_cond_top();
3644 while COND_SEP() {
3645 condlex();
3646 }
3647 if tok() != OUTPAR_TOK {
3648 YYERROR!(ECUSED.get()); // c:2544 `YYERROR(ecused);`
3649 }
3650 condlex();
3651 return r;
3652 }
3653 let s1 = tokstr().unwrap_or_default();
3654 // c:2549 — `dble = (s1 && IS_DASH(*s1) && (!n_testargs ||
3655 // strspn(s1+1, "abcd...") == 1) && !s1[2]);` — IS_DASH covers
3656 // BOTH `-` and Dash (`\u{e19b}`). The raw tokstr inside `[[ ... ]]`
3657 // carries Dash as a marker byte, so `starts_with('-')` alone
3658 // matches only ASCII dashes and misses every `-z`, `-d`, `-r`
3659 // etc. — every such cond emitted the AST-only `condition
3660 // expected` error from par_cond_double. Use IS_DASH and count
3661 // chars (Dash is a single code point) instead of bytes.
3662 let s1_chars: Vec<char> = s1.chars().collect();
3663 let dble = !s1_chars.is_empty()
3664 && IS_DASH(s1_chars[0])
3665 && s1_chars.len() == 2
3666 && "abcdefghknoprstuvwxzLONGS".contains(s1_chars[1]);
3667 if tok() != STRING_LEX {
3668 if !s1.is_empty() && tok() != LEXERR && (!dble || n_testargs != 0) {
3669 // c:2557 — `[[ STRING ]]` re-interpreted as `[[ -n STRING ]]`.
3670 condlex();
3671 while COND_SEP() {
3672 condlex();
3673 }
3674 return par_cond_double("-n", &s1);
3675 }
3676 YYERROR!(ECUSED.get()); // c:2560 `YYERROR(ecused);`
3677 }
3678 condlex();
3679 if n_testargs == 2 && tok() != STRING_LEX && tokstr().is_some() && s1_chars.first().map_or(false, |c| IS_DASH(*c)) {
3680 // c:2563-2569 — something like "test -z" followed by a token. We'll
3681 // turn the token into a string (we've also checked it does have a
3682 // string representation).
3683 set_tok(STRING_LEX);
3684 } else {
3685 while COND_SEP() {
3686 condlex();
3687 }
3688 }
3689 if tok() == INANG_TOK || tok() == OUTANG_TOK {
3690 // c:2576 — `<` / `>` string compare.
3691 let xtok = tok();
3692 condlex();
3693 while COND_SEP() {
3694 condlex();
3695 }
3696 if tok() != STRING_LEX {
3697 YYERROR!(ECUSED.get()); // c:2577 `YYERROR(ecused);`
3698 }
3699 let s3 = tokstr().unwrap_or_default();
3700 condlex();
3701 while COND_SEP() {
3702 condlex();
3703 }
3704 let op = if xtok == INANG_TOK {
3705 COND_STRLT
3706 } else {
3707 COND_STRGTR
3708 };
3709 ecadd(WCB_COND(op as u32, 0));
3710 ecstr(&s1);
3711 ecstr(&s3);
3712 return 1;
3713 }
3714 if tok() != STRING_LEX {
3715 // c:2592 — only one operand seen → `[ -n s1 ]`.
3716 if tok() != LEXERR {
3717 if !dble || n_testargs != 0 {
3718 return par_cond_double("-n", &s1);
3719 }
3720 return par_cond_multi(&s1, &[]);
3721 }
3722 YYERROR!(ECUSED.get()); // c:2596 `YYERROR(ecused);`
3723 }
3724 let s2 = tokstr().unwrap_or_default();
3725 set_incond(incond() + 1);
3726 condlex();
3727 while COND_SEP() {
3728 condlex();
3729 }
3730 set_incond(incond() - 1);
3731 // c:Src/parse.c:2598-2600 — `if (!n_testargs) dble = (s2 &&
3732 // IS_DASH(*s2) && !s2[2]);` — RECOMPUTE dble based on s2 once
3733 // it's been read, so `[[ A -X B ]]` is treated as a 2-arg cond
3734 // `[ -X B ]` (par_cond_double) rather than a 3-arg triple. This
3735 // is what routes `[[ "" -a "x" ]]` to par_cond_double("", "-a")
3736 // → COND_ERROR "parse error: condition expected: ". Without
3737 // this, the original `dble` from s1 stayed false, the parser
3738 // grabbed s3 and built COND_MODI silently. parity bug #25.
3739 let s2_chars: Vec<char> = s2.chars().collect();
3740 let dble = if n_testargs == 0 {
3741 !s2_chars.is_empty() && IS_DASH(s2_chars[0]) && s2_chars.len() == 2
3742 } else {
3743 dble
3744 };
3745 if tok() == STRING_LEX && !dble {
3746 let s3 = tokstr().unwrap_or_default();
3747 condlex();
3748 while COND_SEP() {
3749 condlex();
3750 }
3751 if tok() == STRING_LEX {
3752 // c:2615 — n-ary `[ A op B C D ... ]`.
3753 let mut l: Vec<String> = vec![s2, s3];
3754 while tok() == STRING_LEX {
3755 l.push(tokstr().unwrap_or_default());
3756 condlex();
3757 while COND_SEP() {
3758 condlex();
3759 }
3760 }
3761 return par_cond_multi(&s1, &l);
3762 }
3763 return par_cond_triple(&s1, &s2, &s3);
3764 }
3765 par_cond_double(&s1, &s2)
3766}
3767
3768/// Port of `par_cond_double()` from `Src/parse.c:2626` — C signature `par_cond_double(char *a, char *b)`.
3769/// Emits wordcode for unary cond `[ -X b ]` or modular `[ -mod b ]`.
3770pub fn par_cond_double(a: &str, b: &str) -> i32 {
3771 // c:2628 — `if (!IS_DASH(a[0]) || !a[1])` — char-based, since
3772 // Dash is a single code point (`\u{e19b}`) and `a.len() < 2` on
3773 // BYTES would still pass for "-z" but fail for the marker form
3774 // `\u{e19b}z` (2 bytes). Walk by chars.
3775 let ac: Vec<char> = a.chars().collect();
3776 if ac.is_empty() || !IS_DASH(ac[0]) || ac.len() < 2 {
3777 // c:2629 `COND_ERROR("parse error: condition expected: %s", a)`.
3778 COND_ERROR!("parse error: condition expected: {}", a);
3779 }
3780 // c:2630 — `else if (!a[2] && strspn(a+1, "abcd...zhLONGS") == 1)`
3781 let unary_set = "abcdefgknoprstuvwxzhLONGS";
3782 if ac.len() == 2 && unary_set.contains(ac[1]) {
3783 // c:2631 — `ecadd(WCB_COND(a[1], 0));` uses the raw cond-op
3784 // letter byte as the opcode payload. Use the ASCII char's
3785 // code-point value directly — every letter in `unary_set`
3786 // fits in 7 bits.
3787 ecadd(WCB_COND(ac[1] as u32, 0));
3788 ecstr(b);
3789 } else {
3790 ecadd(WCB_COND(COND_MOD as u32, 1));
3791 ecstr(a);
3792 ecstr(b);
3793 }
3794 1
3795}
3796
3797/// Port of `get_cond_num()` from `Src/parse.c:2643` — C signature `get_cond_num(char *tst)`. Returns
3798/// the index of `tst` in `{"nt","ot","ef","eq","ne","lt","gt","le","ge"}`
3799/// or `-1` if not a recognized binary cond operator.
3800pub fn get_cond_num(tst: &str) -> i32 {
3801 // c:2643
3802 const CONDSTRS: [&str; 9] = [
3803 "nt", "ot", "ef", "eq", "ne", "lt", "gt", "le", "ge", // c:2647
3804 ];
3805 for (i, &c) in CONDSTRS.iter().enumerate() {
3806 if c == tst {
3807 return i as i32; // c:2654
3808 }
3809 }
3810 -1 // c:2656
3811}
3812
3813/// par_time's `static int inpartime` guard at C parse.c:1038
3814/// preventing infinite recursion on `time time foo`. The wordcode
3815/// path keeps this as a thread_local since C uses a function-level
3816/// `static int` (per-process; per-evaluator semantically matches).
3817thread_local! {
3818 static PARSER_INPARTIME: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3819}
3820
3821thread_local! {
3822 /// !!! WARNING: RUST-ONLY HELPER !!!
3823 ///
3824 /// C's `par_cmd` has NO post-compound terminator check — a STRING after
3825 /// a compound simply ends the list (`par_list` c:670 sees a non-separator
3826 /// and stops), and the diagnostic, if any, comes from the `if (!r)` tail.
3827 /// zshrs added a strict check inside `par_cmd` (Bug #146) which is wrong
3828 /// inside an `if` CONDITION, where a word right after `{ … }` is the
3829 /// legal SHORTLOOPS body: `if { true } print true` (c:1476-1482,
3830 /// A01grammar.ztst:500). This counter suppresses the Rust-only check
3831 /// while a condition list is being parsed.
3832 static COND_LIST_DEPTH: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
3833}
3834
3835/// Port of `par_cond_triple()` from `Src/parse.c:2659` — C decl `par_cond_triple(char *a, char *b, char *c)`.
3836/// Emits wordcode for the binary forms
3837/// `[ A op B ]` — `=` / `==` / `!=` / `<` / `>` / `=~` / `-X`.
3838///
3839/// C does `(b[0] == Equals || b[0] == '=')` etc., matching BOTH the
3840/// raw ASCII operator char AND its tokenized marker form per
3841/// `Src/zsh.h:159-194`:
3842/// Equals = `\u{e18d}`, Outang = `\u{e195}`, Inang = `\u{e194}`,
3843/// Tilde = `\u{e198}`, Bang = `\u{e19c}`, Dash = `\u{e19b}`.
3844/// Inside `[[ ... ]]` the lexer emits the marker bytes — comparing
3845/// against literal-only `b"=="` misses every cond op.
3846/// (The previous Rust port had the doc comment values wrong:
3847/// Outang=0x8e was actually Bar; Inang=0x91 was Inbrack;
3848/// Tilde=0x96 was OutangProc; Bang=0x8b was Outparmath. The code
3849/// itself uses the correct const names, so this was a docs-only fix.)
3850pub fn par_cond_triple(a: &str, b: &str, c: &str) -> i32 {
3851 // c:2659
3852 let bc: Vec<char> = b.chars().collect();
3853 let is_eq = |ch: char| ch == '=' || ch == Equals;
3854 let is_gt = |ch: char| ch == '>' || ch == Outang;
3855 let is_lt = |ch: char| ch == '<' || ch == Inang;
3856 let is_tilde = |ch: char| ch == '~' || ch == Tilde;
3857 let is_bang = |ch: char| ch == '!' || ch == Bang;
3858
3859 // c:2663 — `(b[0] == Equals || b[0] == '=') && !b[1]` → `=` (single).
3860 if bc.len() == 1 && is_eq(bc[0]) {
3861 ecadd(WCB_COND(COND_STREQ as u32, 0));
3862 ecstr(a);
3863 ecstr(c);
3864 let np = ECNPATS.with(|cc| {
3865 let v = cc.get();
3866 cc.set(v + 1);
3867 v
3868 }) as u32;
3869 ecadd(np);
3870 return 1;
3871 }
3872 // c:2668-2673 — `(t0 = (b[0] == '>' || b[0] == Outang) || b[0] == '<' || b[0] == Inang) && !b[1]`
3873 // then `t0 ? COND_STRGTR : COND_STRLT`. The assignment binds only the first
3874 // parenthesised term, but the `||` chain over the whole expression makes it true for
3875 // '<' and Inang as well, so C always emits COND_STRGTR here: in `test`/`[`, `a < b` is
3876 // evaluated as `a > b` (the reference build does the same).
3877 if bc.len() == 1 && (is_gt(bc[0]) || is_lt(bc[0])) {
3878 let op = COND_STRGTR;
3879 ecadd(WCB_COND(op as u32, 0));
3880 ecstr(a);
3881 ecstr(c);
3882 let np = ECNPATS.with(|cc| {
3883 let v = cc.get();
3884 cc.set(v + 1);
3885 v
3886 }) as u32;
3887 ecadd(np);
3888 return 1;
3889 }
3890 // c:2674-2679 — `==` STRDEQ.
3891 if bc.len() == 2 && is_eq(bc[0]) && is_eq(bc[1]) {
3892 ecadd(WCB_COND(COND_STRDEQ as u32, 0));
3893 ecstr(a);
3894 ecstr(c);
3895 let np = ECNPATS.with(|cc| {
3896 let v = cc.get();
3897 cc.set(v + 1);
3898 v
3899 }) as u32;
3900 ecadd(np);
3901 return 1;
3902 }
3903 // c:2680-2684 — `!=` STRNEQ.
3904 if bc.len() == 2 && is_bang(bc[0]) && is_eq(bc[1]) {
3905 ecadd(WCB_COND(COND_STRNEQ as u32, 0));
3906 ecstr(a);
3907 ecstr(c);
3908 let np = ECNPATS.with(|cc| {
3909 let v = cc.get();
3910 cc.set(v + 1);
3911 v
3912 }) as u32;
3913 ecadd(np);
3914 return 1;
3915 }
3916 // c:2685-2691 — `=~` REGEX (no pattern slot — implicit COND_MODI).
3917 if bc.len() == 2 && is_eq(bc[0]) && is_tilde(bc[1]) {
3918 ecadd(WCB_COND(COND_REGEX as u32, 0));
3919 ecstr(a);
3920 ecstr(c);
3921 return 1;
3922 }
3923 // c:2692-2702 — `-OP` numeric-or-modular cond (e.g. `-eq`, `-nt`).
3924 if !bc.is_empty() && IS_DASH(bc[0]) {
3925 let rest: String = bc[1..].iter().collect();
3926 let t = get_cond_num(&rest);
3927 if t > -1 {
3928 ecadd(WCB_COND((t + COND_NT) as u32, 0));
3929 ecstr(a);
3930 ecstr(c);
3931 return 1;
3932 }
3933 ecadd(WCB_COND(COND_MODI as u32, 0));
3934 ecstr(b);
3935 ecstr(a);
3936 ecstr(c);
3937 return 1;
3938 }
3939 // c:2703-2707 — `-mod A B C` modular cond on `a`.
3940 let ac: Vec<char> = a.chars().collect();
3941 if !ac.is_empty() && IS_DASH(ac[0]) && ac.len() > 1 {
3942 ecadd(WCB_COND(COND_MOD as u32, 2));
3943 ecstr(a);
3944 ecstr(b);
3945 ecstr(c);
3946 return 1;
3947 }
3948 // c:2709
3949 COND_ERROR!("condition expected: {}", b);
3950}
3951
3952/// Port of `par_cond_multi()` from `Src/parse.c:2716` — C signature `par_cond_multi(char *a, LinkList l)`.
3953/// Emits wordcode for `[ -OP A B C … ]` n-ary cond (alternation).
3954pub fn par_cond_multi(a: &str, l: &[String]) -> i32 {
3955 // c:2716 — `if (!IS_DASH(a[0]) || !a[1])`; same Dash/`-` dual
3956 // matching as par_cond_double, char-walked because Dash is a
3957 // single code point.
3958 let ac: Vec<char> = a.chars().collect();
3959 if ac.is_empty() || !IS_DASH(ac[0]) || ac.len() < 2 {
3960 // c:2719
3961 COND_ERROR!("condition expected: {}", a);
3962 }
3963 ecadd(WCB_COND(COND_MOD as u32, l.len() as u32));
3964 ecstr(a);
3965 for item in l {
3966 ecstr(item);
3967 }
3968 1
3969}
3970
3971/// Emit a parser-level error. Direct port of zsh/Src/parse.c
3972/// 2733-2766 `yyerror`. C version fills a per-event error buffer
3973/// and sets errflag. zshrs pushes onto errors which the
3974/// caller drains via parse()'s Result return.
3975/// Port of `yyerror()` from `Src/parse.c:2733` — C decl `yyerror(int noerr)`.
3976///
3977/// Faithful C body (verbatim):
3978/// ```c
3979/// int t0; char *t;
3980/// if ((t = dupstring(zshlextext))) untokenize(t);
3981/// for (t0 = 0; t0 != 20; t0++)
3982/// if (!t || !t[t0] || t[t0] == '\n') break;
3983/// if (!(histdone & HISTFLAG_NOEXEC) && !(errflag & ERRFLAG_INT)) {
3984/// if (t0) {
3985/// t = metafy(t, t0, META_STATIC);
3986/// zwarn("parse error near `%s%s'", t, t0 == 20 ? "..." : "");
3987/// } else
3988/// zwarn("parse error");
3989/// }
3990/// if (!noerr && noerrs != 2)
3991/// errflag |= ERRFLAG_ERROR;
3992/// ```
3993///
3994/// `zshlextext` is the C lexer's current-token text (`Src/lex.c:170`
3995/// `char *tokstr`); zshrs's equivalent is `lex::tokstr()`. The "20"
3996/// is C's tail-truncation length for the error message.
3997pub fn yyerror(noerr: i32) {
3998 // c:2733
3999 // c:2738 — `if ((t = dupstring(zshlextext))) untokenize(t);`.
4000 // In C, `zshlextext` falls back to `tokstrings[tok]` (lex.c:1965)
4001 // for punctuation tokens that didn't capture a tokstr — that's
4002 // how "parse error near `)'" gets the `)` for OUTPAR. Mirror by
4003 // consulting `lex::tokstring(tok())` when the captured tokstr is
4004 // None.
4005 // `zshlextext` is published by exalias (lex.c:1965-2018) — tokstr
4006 // for word tokens, `tokstrings[tok]` for punctuation — and is NOT
4007 // refreshed at ENDINPUT, since zshlex stops calling exalias there
4008 // (c:276). Reading `tokstr` instead lost that last-token memory,
4009 // so a parse error at EOF printed a bare "parse error" where zsh
4010 // names the token it died on.
4011 let t_opt: Option<String> = crate::ported::lex::LEX_ZSHLEXTEXT
4012 .with_borrow(|t| t.clone())
4013 // C's `untokenize` maps EVERY token through `ztokens`, quote markers
4014 // included (Snull → `'`, Dnull → `"`); zshrs's `lex::untokenize` is
4015 // the value-stream variant that drops them, so `[[ x = "a\\b"( ]]`
4016 // reported `a\\b( ]]` where zsh reports `"a\\b"( ]]`.
4017 .map(|raw| crate::ported::lex::untokenize_ztokens(&raw));
4018 let t_bytes: Vec<u8> = t_opt
4019 .as_ref()
4020 .map(|s| s.as_bytes().to_vec())
4021 .unwrap_or_default();
4022
4023 // c:2741-2743 — `for (t0 = 0; t0 != 20; t0++) if (!t || !t[t0]
4024 // || t[t0] == '\n') break;`
4025 let mut t0: usize = 0;
4026 while t0 != 20 {
4027 // c:2741
4028 let stop =
4029 t_opt.is_none() || t0 >= t_bytes.len() || t_bytes[t0] == 0 || t_bytes[t0] == b'\n';
4030 if stop {
4031 break;
4032 }
4033 t0 += 1;
4034 }
4035
4036 // c:2744 — `if (!(histdone & HISTFLAG_NOEXEC) && !(errflag &
4037 // ERRFLAG_INT))`. The HISTFLAG_NOEXEC gate suppresses warnings
4038 // from history-recall paths that aren't actually executing.
4039 let histdone_v = crate::ported::hist::histdone.load(Ordering::SeqCst);
4040 let hist_noexec = (histdone_v & crate::ported::zsh_h::HISTFLAG_NOEXEC as i32) != 0;
4041 let int_flagged = (errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_INT) != 0;
4042 if !hist_noexec && !int_flagged {
4043 // c:2744
4044 if t0 != 0 {
4045 // c:2745
4046 // c:2746 — `t = metafy(t, t0, META_STATIC);` — re-metafy
4047 // the truncated head so embedded Meta bytes display
4048 // correctly. The Rust port already holds an untokenized
4049 // string; use the byte slice [0..t0] directly.
4050 let head = std::str::from_utf8(&t_bytes[..t0]).unwrap_or_default();
4051 let suffix = if t0 == 20 { "..." } else { "" };
4052 crate::ported::utils::zwarn(&format!("parse error near `{}{}'", head, suffix));
4053 // c:2747
4054 } else {
4055 // c:2748
4056 crate::ported::utils::zwarn("parse error"); // c:2749
4057 }
4058 }
4059 // c:2751 — `if (!noerr && noerrs != 2) errflag |= ERRFLAG_ERROR;`.
4060 // The `noerrs != 2` gate (suppress-only-fatal-errors) is preserved
4061 // for parity with zerr/zwarn's matching check.
4062 let noerrs_v = *crate::ported::utils::noerrs_lock().lock().unwrap();
4063 if noerr == 0 && noerrs_v != 2 {
4064 // c:2751
4065 errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::SeqCst);
4066 // c:2752
4067 }
4068}
4069
4070// ============================================================
4071// Eprog runtime ops (parse.c:2767-2853)
4072//
4073// dupeprog / useeprog / freeeprog are zsh's reference-counting
4074// helpers for executable programs. zshrs's AST is owned by
4075// value (Rust ownership); cloning is a tree-deep copy via
4076// Clone, "use" is a no-op (the executor borrows the AST), and
4077// "free" is automatic on drop.
4078// ============================================================
4079
4080/// Duplicate an Eprog. Direct port of zsh/Src/parse.c:2767
4081/// Port of `dupeprog()` from `Src/parse.c:2767` — C decl `dupeprog(Eprog p, int heap)`.
4082/// Deep-copies the wordcode array, string
4083/// table, and pattern-prog slots. `dummy_eprog` is returned
4084/// unchanged. `heap`-allocated copies get `nref = -1` (never
4085/// freed); real ones get `nref = 1`.
4086pub fn dupeprog(p: &eprog, heap: bool) -> eprog {
4087 // c:2774-2775 — `if (p == &dummy_eprog) return p;` — caller-
4088 // observable identity in C uses a pointer compare; Rust's
4089 // equivalent is "if it has the dummy's shape (single WCB_END
4090 // word and no strs), return a copy of the same shape".
4091 // c:2796-2797 — `for (i = r->npats; i--; pp++) *pp = dummy_patprog1;`
4092 // C uses `dummy_patprog1` as a placeholder; the Rust port has
4093 // `Vec<Patprog>` (Box<patprog>) — synthesize an equivalent zero-
4094 // initialized patprog for each slot (resolved later by
4095 // pattern.c::patcompile-on-first-use).
4096 let dummy_pat = || crate::ported::zsh_h::patprog {
4097 startoff: 0,
4098 size: 0,
4099 mustoff: 0,
4100 patmlen: 0,
4101 globflags: 0,
4102 globend: 0,
4103 flags: 0,
4104 patnpar: 0,
4105 patstartch: 0,
4106 };
4107 let r = eprog {
4108 // c:2778 — `flags = (heap ? EF_HEAP : EF_REAL) | (p->flags & EF_RUN);`
4109 flags: (if heap { EF_HEAP } else { EF_REAL }) | (p.flags & EF_RUN),
4110 len: p.len,
4111 npats: p.npats,
4112 // c:2787 — `nref = heap ? -1 : 1;`
4113 nref: if heap { -1 } else { 1 },
4114 prog: p.prog.clone(),
4115 strs: p.strs.clone(), // pool copied verbatim — provenance below
4116 pats: (0..p.npats).map(|_| Box::new(dummy_pat())).collect(),
4117 shf: None,
4118 dump: None,
4119 strs_metafied: p.strs_metafied, // pool copied verbatim — carry provenance
4120 };
4121 r
4122}
4123
4124/// Port of `useeprog()` from `Src/parse.c:2813` — C signature `useeprog(Eprog p)`.
4125/// `if (p && p != &dummy_eprog && p->nref >= 0) p->nref++;` —
4126/// pin a real (non-heap, non-dummy) Eprog so it survives the
4127/// next `freeeprog`.
4128pub fn useeprog(p: &mut eprog) {
4129 // c:2815 — `if (p && p != &dummy_eprog && p->nref >= 0)`
4130 if p.nref >= 0 {
4131 p.nref += 1; // c:2816
4132 }
4133}
4134
4135/// Port of `freeeprog()` from `Src/parse.c:2823` — C signature `freeeprog(Eprog p)`.
4136/// Refcount-decrement; when it hits zero, drops the pattern progs,
4137/// decrements the dump refcount if any, and releases the eprog.
4138/// `dummy_eprog` is never freed. Heap-eprogs (`nref < 0`) are
4139/// never freed either — they live as long as the heap arena.
4140pub fn freeeprog(p: &mut eprog) {
4141 // c:2829 — `if (p && p != &dummy_eprog) { ... }`
4142 if p.nref > 0 {
4143 p.nref -= 1; // c:2832
4144 if p.nref == 0 {
4145 // c:2833-2840 — drop pats, dump refcount, then the eprog.
4146 // Rust's Drop handles the per-field cleanup; we just
4147 // need to decrement the dump count first.
4148 if let Some(dump) = p.dump.take() {
4149 let dumped = (*dump).clone();
4150 decrdumpcount(&dumped); // c:2837
4151 }
4152 p.prog.clear();
4153 p.strs = None;
4154 p.pats.clear();
4155 }
4156 }
4157}
4158
4159// =============================================================================
4160// Wordcode read helpers — used by text.rs's `gettext2` and exec dispatch
4161// to walk a compiled Eprog without re-running the parser. These are the
4162// only `Src/parse.c` functions ported so far in this file; the recursive-
4163// descent parser (par_event / par_list / par_cmd / par_*) follows
4164// below as free ported at module scope.
4165// =============================================================================
4166
4167/// Port of `ecgetstr()` from `Src/parse.c:2855` — C signature `ecgetstr(Estate s, int dup, int *tokflag)`.
4168/// `s->pc` advances through the wordcode buffer; `s->strs` indexes the
4169/// string pool. Returns the interned string (or a 1-3-char literal
4170/// inlined directly into the wordcode word).
4171pub fn ecgetstr(s: &mut estate, dup: i32, tokflag: Option<&mut i32>) -> String {
4172 let prog = &s.prog.prog;
4173 if s.pc >= prog.len() {
4174 return String::new();
4175 }
4176 let c = prog[s.pc]; // c:2858 `wordcode c = *s->pc++;`
4177 s.pc += 1;
4178 if let Some(tf) = tokflag {
4179 *tf = i32::from((c & 1) != 0); // c:2880 `*tokflag = (c & 1);`
4180 }
4181 if c == 6 || c == 7 {
4182 // c:2861 `if (c == 6 || c == 7) r = "";`
4183 return String::new();
4184 }
4185 let r: String = if (c & 2) != 0 {
4186 // c:2862 — `else if (c & 2)`
4187 // c:2863-2868 — 3-byte inline string packed into the wordcode
4188 // word; followed by `buf[3] = '\0'; r = dupstring(buf);`.
4189 // C's `dupstring` uses `strlen(buf)` which TRUNCATES at the
4190 // first NUL byte — short strings of 1 or 2 chars get padded
4191 // with NULs and truncated cleanly. The previous Rust port
4192 // used `retain(|&x| x != 0)` which would silently SPLICE OUT
4193 // an interior NUL (e.g. `[a, 0, b]` → "ab"), diverging from
4194 // C's strlen-truncate (`[a, 0, b]` → "a"). Fix: truncate at
4195 // first NUL to match C exactly.
4196 let b0 = ((c >> 3) & 0xff) as u8;
4197 let b1 = ((c >> 11) & 0xff) as u8;
4198 let b2 = ((c >> 19) & 0xff) as u8;
4199 let v = [b0, b1, b2];
4200 let end = v.iter().position(|&x| x == 0).unwrap_or(v.len()); // c:2869 strlen(buf)
4201 // C reads raw bytes (token codes included) — widen via the
4202 // wordcode-pool bridge, never from_utf8_lossy (which mangles
4203 // raw token bytes from C-zsh-written .zwc dumps to U+FFFD).
4204 crate::zwc::wordcode_pool_str_unmeta(&v[..end], s.prog.strs_metafied)
4205 } else {
4206 // c:2877 `else r = s->strs + (c >> 2);`
4207 let off = (c >> 2) as usize + s.strs_offset;
4208 let strs_bytes = s.strs.as_deref().unwrap_or("").as_bytes();
4209 if off >= strs_bytes.len() {
4210 String::new()
4211 } else {
4212 let tail = &strs_bytes[off..];
4213 let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
4214 crate::zwc::wordcode_pool_str_unmeta(&tail[..end], s.prog.strs_metafied)
4215 }
4216 };
4217 // c:2891 `return ((dup == EC_DUP || (dup && (c & 1))) ? dupstring(r) : r);`
4218 // Rust owns the String already; `dup` flag has no observable effect.
4219 let _ = (dup, EC_DUP, EC_NODUP);
4220 r
4221}
4222
4223// ============================================================
4224// Wordcode runtime getters (parse.c:2853-3060)
4225//
4226// Direct ports of the wordcode-read helpers (ecrawstr,
4227// ecgetstr, ecgetarr, ecgetredirs, ecgetlist, eccopyredirs).
4228// Read packed wordcode out of an Eprog at execution time.
4229// Used by exec_wordcode and the wordcode-walking dispatch in
4230// src/vm_helper.
4231// ============================================================
4232
4233/// Port of `ecrawstr()` from `Src/parse.c:2891` — C decl `ecrawstr(Eprog p, Wordcode pc, int *tokflag)`.
4234/// Like `ecgetstr` but reads at the given pc
4235/// without advancing — caller steps `pc` separately.
4236pub fn ecrawstr(p: &eprog, pc: usize, tokflag: Option<&mut i32>) -> String {
4237 if pc >= p.prog.len() {
4238 return String::new();
4239 }
4240 let c = p.prog[pc]; // c:2894
4241 if let Some(tf) = tokflag {
4242 *tf = i32::from((c & 1) != 0); // c:2898/2906/2912
4243 }
4244 if c == 6 || c == 7 {
4245 // c:2897
4246 return String::new();
4247 }
4248 if (c & 2) != 0 {
4249 // c:2902-2906 — same 3-byte inline string as ecgetstr, then
4250 // `buf[3] = '\0'; return dupstring(buf);` — truncate at first
4251 // NUL via strlen (NOT splice out interior NULs).
4252 let b0 = ((c >> 3) & 0xff) as u8;
4253 let b1 = ((c >> 11) & 0xff) as u8;
4254 let b2 = ((c >> 19) & 0xff) as u8;
4255 let v = [b0, b1, b2];
4256 let end = v.iter().position(|&x| x == 0).unwrap_or(v.len()); // c:2906 strlen(buf)
4257 // Raw-byte widening — see ecgetstr (same C-convention bridge).
4258 crate::zwc::wordcode_pool_str_unmeta(&v[..end], p.strs_metafied)
4259 } else {
4260 // c:2911
4261 let off = (c >> 2) as usize;
4262 let strs_bytes = p.strs.as_deref().unwrap_or("").as_bytes();
4263 if off >= strs_bytes.len() {
4264 return String::new();
4265 }
4266 let tail = &strs_bytes[off..];
4267 let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
4268 crate::zwc::wordcode_pool_str_unmeta(&tail[..end], p.strs_metafied)
4269 }
4270}
4271
4272/// Port of `ecgetarr()` from `Src/parse.c:2917` — C decl `ecgetarr(Estate s, int num, int dup, int *tokflag)`.
4273/// Reads `num` strings from wordcode at `s->pc`
4274/// and OR-folds each entry's token flag into `*tokflag`.
4275pub fn ecgetarr(s: &mut estate, num: usize, dup: i32, tokflag: Option<&mut i32>) -> Vec<String> {
4276 let mut ret: Vec<String> = Vec::with_capacity(num); // c:2922
4277 let mut tf: i32 = 0;
4278 for _ in 0..num {
4279 // c:2924 `while (num--)`
4280 let mut tmp = 0;
4281 ret.push(ecgetstr(s, dup, Some(&mut tmp))); // c:2925
4282 tf |= tmp; // c:2926
4283 }
4284 if let Some(out) = tokflag {
4285 // c:2929
4286 *out = tf;
4287 }
4288 ret
4289}
4290
4291/// Port of `ecgetlist()` from `Src/parse.c:2937` — C decl `ecgetlist(Estate s, int num, int dup, int *tokflag)`.
4292/// Same shape as `ecgetarr` but C returns
4293/// `LinkList`; zshrs uses `Vec<String>` for both.
4294pub fn ecgetlist(s: &mut estate, num: usize, dup: i32, tokflag: Option<&mut i32>) -> Vec<String> {
4295 if num == 0 {
4296 // c:2949-2952
4297 if let Some(tf) = tokflag {
4298 *tf = 0;
4299 }
4300 return Vec::new();
4301 }
4302 ecgetarr(s, num, dup, tokflag)
4303}
4304
4305/// Port of `ecgetredirs()` from `Src/parse.c:2959` — C signature `ecgetredirs(Estate s)`.
4306///
4307/// `strs` must be the same tail `ecgetstr` uses (`s->strs` / `estate.strs` from offset).
4308/// WARNING: param names don't match C — Rust=(prog, strs, pc) vs C=(s)
4309pub fn ecgetredirs(s: &mut estate) -> Vec<redir> {
4310 let mut ret: Vec<redir> = Vec::new(); // c:2959 `LinkList ret = newlinklist();`
4311 let prog_len = s.prog.prog.len();
4312 if s.pc >= prog_len {
4313 return ret;
4314 }
4315 let mut code = s.prog.prog[s.pc]; // c:2962 `wordcode code = *s->pc++;`
4316 s.pc += 1;
4317
4318 loop {
4319 if wc_code(code) != WC_REDIR {
4320 // c:2988-2989 `s->pc--` then break from while
4321 s.pc = s.pc.saturating_sub(1);
4322 break;
4323 }
4324
4325 let typ = WC_REDIR_TYPE(code); // c:2967 `r->type = WC_REDIR_TYPE(code);`
4326 if s.pc >= prog_len {
4327 break;
4328 }
4329 let fd1_w = s.prog.prog[s.pc]; // c:2968 `r->fd1 = *s->pc++;`
4330 s.pc += 1;
4331
4332 let name = ecgetstr(s, EC_DUP, None); // c:2969 `r->name = ecgetstr(...)`
4333
4334 let (flags, here_terminator, munged_here_terminator) = if WC_REDIR_FROM_HEREDOC(code) != 0 {
4335 // c:2970-2973
4336 let term = ecgetstr(s, EC_DUP, None);
4337 let munged = ecgetstr(s, EC_DUP, None);
4338 (REDIRF_FROM_HEREDOC, Some(term), Some(munged))
4339 } else {
4340 // c:2974-2977
4341 (0, None, None)
4342 };
4343
4344 let varid = if WC_REDIR_VARID(code) != 0 {
4345 // c:2979-2980
4346 Some(ecgetstr(s, EC_DUP, None))
4347 } else {
4348 None // c:2981-2982
4349 };
4350
4351 ret.push(redir {
4352 // c:2965-2982 fields + c:2984 `addlinknode`
4353 typ,
4354 flags,
4355 fd1: fd1_w as i32,
4356 fd2: 0,
4357 name: Some(name),
4358 varid,
4359 here_terminator,
4360 munged_here_terminator,
4361 });
4362
4363 if s.pc >= prog_len {
4364 break;
4365 }
4366 code = s.prog.prog[s.pc]; // c:2986 `code = *s->pc++;`
4367 s.pc += 1;
4368 }
4369
4370 ret // c:2990 `return ret`
4371}
4372
4373/// Port of `eccopyredirs()` from `Src/parse.c:3003` — C signature `eccopyredirs(Estate s)`. Reads
4374/// the WC_REDIR run at `s->pc`, counts the wordcodes needed,
4375/// reserves space in `ecbuf` via `ecispace`, then re-walks `s->pc`
4376/// re-emitting each redir's wordcodes into the reserved slot —
4377/// finally calls `bld_eprog(0)` to package the result as an Eprog.
4378pub fn eccopyredirs(s: &mut estate) -> Option<eprog> {
4379 let prog_len = s.prog.prog.len();
4380 if s.pc >= prog_len {
4381 return None;
4382 }
4383 // c:3007-3009 — `if (wc_code(*pc) != WC_REDIR) return NULL;`
4384 let first_code = s.prog.prog[s.pc];
4385 if wc_code(first_code) != WC_REDIR {
4386 return None;
4387 }
4388 // c:3011 — `init_parse();`
4389 init_parse();
4390
4391 // c:3013-3027 — count wordcodes the redir run will need.
4392 // Each WC_REDIR contributes `code + fd1 + name` = 3, plus
4393 // `+2` if WC_REDIR_FROM_HEREDOC (terminator + munged), plus
4394 // `+1` if WC_REDIR_VARID.
4395 let mut probe = s.pc;
4396 let mut ncodes = 0usize;
4397 loop {
4398 if probe >= prog_len {
4399 break;
4400 }
4401 let code = s.prog.prog[probe];
4402 if wc_code(code) != WC_REDIR {
4403 break;
4404 }
4405 let mut ncode = if WC_REDIR_FROM_HEREDOC(code) != 0 {
4406 5
4407 } else {
4408 3
4409 };
4410 if WC_REDIR_VARID(code) != 0 {
4411 ncode += 1;
4412 }
4413 probe += ncode;
4414 ncodes += ncode;
4415 }
4416
4417 // c:3028-3029 — `r = ecused; ecispace(r, ncodes);`
4418 let r0 = ECUSED.get() as usize;
4419 ecispace(r0, ncodes);
4420
4421 // c:3031-3053 — re-walk `s->pc` and write into ecbuf[r..].
4422 let mut r = r0;
4423 loop {
4424 if s.pc >= prog_len {
4425 break;
4426 }
4427 let code = s.prog.prog[s.pc];
4428 if wc_code(code) != WC_REDIR {
4429 break;
4430 }
4431 s.pc += 1;
4432 // c:3036 — `ecbuf[r++] = code;`
4433 ECBUF.with_borrow_mut(|buf| {
4434 if r >= buf.len() {
4435 buf.resize(r + 1, 0);
4436 }
4437 buf[r] = code;
4438 });
4439 r += 1;
4440 // c:3038 — `ecbuf[r++] = *s->pc++;` (the fd1 word)
4441 let fd1 = s.prog.prog[s.pc];
4442 s.pc += 1;
4443 ECBUF.with_borrow_mut(|buf| {
4444 if r >= buf.len() {
4445 buf.resize(r + 1, 0);
4446 }
4447 buf[r] = fd1;
4448 });
4449 r += 1;
4450 // c:3041 — `ecbuf[r++] = ecstrcode(ecgetstr(s, EC_NODUP, NULL));`
4451 let name = ecgetstr(s, EC_NODUP, None);
4452 let nc = ecstrcode(&name);
4453 ECBUF.with_borrow_mut(|buf| {
4454 if r >= buf.len() {
4455 buf.resize(r + 1, 0);
4456 }
4457 buf[r] = nc;
4458 });
4459 r += 1;
4460 // c:3042-3047 — heredoc terminators.
4461 if WC_REDIR_FROM_HEREDOC(code) != 0 {
4462 let term = ecgetstr(s, EC_NODUP, None);
4463 let tc = ecstrcode(&term);
4464 ECBUF.with_borrow_mut(|buf| {
4465 if r >= buf.len() {
4466 buf.resize(r + 1, 0);
4467 }
4468 buf[r] = tc;
4469 });
4470 r += 1;
4471 let munged = ecgetstr(s, EC_NODUP, None);
4472 let mc = ecstrcode(&munged);
4473 ECBUF.with_borrow_mut(|buf| {
4474 if r >= buf.len() {
4475 buf.resize(r + 1, 0);
4476 }
4477 buf[r] = mc;
4478 });
4479 r += 1;
4480 }
4481 // c:3048-3049 — varid.
4482 if WC_REDIR_VARID(code) != 0 {
4483 let varid = ecgetstr(s, EC_NODUP, None);
4484 let vc = ecstrcode(&varid);
4485 ECBUF.with_borrow_mut(|buf| {
4486 if r >= buf.len() {
4487 buf.resize(r + 1, 0);
4488 }
4489 buf[r] = vc;
4490 });
4491 r += 1;
4492 }
4493 }
4494
4495 // c:3056 — `return bld_eprog(0);` — `bld_eprog` appends the
4496 // WC_END marker and packages ECBUF/ECSTRS into an Eprog.
4497 Some(bld_eprog(false))
4498}
4499
4500/// Port of `init_eprog()` from `Src/parse.c:3069` — C signature `init_eprog(void)`. Sets up
4501/// `dummy_eprog_code = WCB_END(); dummy_eprog.len = sizeof(wordcode);
4502/// dummy_eprog.prog = &dummy_eprog_code; dummy_eprog.strs = NULL;`.
4503/// Called once at shell startup (init_main → init_misc → init_eprog).
4504pub fn init_eprog() {
4505 let mut d = DUMMY_EPROG.lock().unwrap();
4506 d.prog = vec![WCB_END()]; // c:3071/3073
4507 d.len = size_of::<wordcode>() as i32; // c:3072
4508 d.strs = None; // c:3074
4509 d.flags = 0;
4510 d.npats = 0;
4511 d.nref = 0;
4512}
4513
4514// =====================================================================
4515// `bin_zcompile` and wordcode-dump helpers — port of `Src/parse.c:3104+`.
4516//
4517// The wordcode dump format (`.zwc`) is a serialized parse tree zsh can
4518// `mmap()` and dispatch from without re-parsing on every shell start.
4519// File layout (one struct = `FD_PRELEN` `u32`s):
4520// - `pre[0]` = magic word (FD_MAGIC native byte-order, FD_OMAGIC
4521// opposite byte-order).
4522// - `pre[1]` = packed `{flags(8) | other_offset(24)}` byte field.
4523// - `pre[2..12]` = `ZSH_VERSION` C-string padded to 40 bytes.
4524// - `pre[12]` = `fdheaderlen` (total prelude+header word count).
4525// - Then a sequence of `struct fdhead` records, one per function,
4526// each followed by its NUL-terminated name (padded to 4-byte).
4527// - Then the wordcode bytes for every function back-to-back.
4528//
4529// On a little-endian host writing a dump twice: first `FD_MAGIC` for
4530// native readers, then re-walks the body byte-swapped and emits a
4531// second `FD_OMAGIC` copy so big-endian readers can mmap it too.
4532// =====================================================================
4533
4534// File-format constants — port of `Src/parse.c:3104-3150`.
4535
4536/// `#define FD_EXT ".zwc"` from `Src/parse.c:3104`.
4537pub const FD_EXT: &str = ".zwc";
4538
4539/// `#define FD_MINMAP 4096` from `Src/parse.c:3105`. mmap threshold
4540/// — `-M` mode only kicks in when the wordcode body is at least
4541/// this many bytes (otherwise read(2) is preferred).
4542pub const FD_MINMAP: usize = 4096;
4543
4544/// `#define FD_PRELEN 12` from `Src/parse.c:3107`. File-header
4545/// length in u32 words: magic + packed-flags-byte + 10 version words.
4546pub const FD_PRELEN: usize = 12;
4547
4548/// `#define FD_MAGIC 0x04050607` from `Src/parse.c:3108`. Sentinel
4549/// for native-byte-order dumps.
4550pub const FD_MAGIC: u32 = 0x04050607;
4551
4552/// `#define FD_OMAGIC 0x07060504` from `Src/parse.c:3109`. Sentinel
4553/// for opposite-byte-order dumps (byte-swapped FD_MAGIC).
4554pub const FD_OMAGIC: u32 = 0x07060504;
4555
4556/// `#define FDF_MAP 1` from `Src/parse.c:3111`. Bit set when the
4557/// dump should be `mmap()`-ed (`-M` flag) vs read normally (`-R`).
4558pub const FDF_MAP: u32 = 1;
4559
4560/// `#define FDF_OTHER 2` from `Src/parse.c:3112`. Bit indicating
4561/// this dump has an opposite-byte-order copy at `fdother(f)`.
4562pub const FDF_OTHER: u32 = 2;
4563
4564/// Port of `struct fdhead` from `Src/parse.c:3116`. One per function
4565/// inside a wordcode dump. All fields are `wordcode` (u32).
4566#[allow(non_camel_case_types)]
4567#[derive(Debug, Clone, Copy)]
4568pub struct fdhead {
4569 /// Offset (in u32 words) to the start of this function's
4570 /// wordcode body inside the dump.
4571 pub start: u32, // c:3117
4572 /// Wordcode-byte length of the body (excludes pattern-prog slots).
4573 pub len: u32, // c:3118
4574 /// Number of compiled patterns the body references.
4575 pub npats: u32, // c:3119
4576 /// Offset of the string table inside `prog->prog`.
4577 pub strs: u32, // c:3120
4578 /// Header-record length in u32 words (record + name).
4579 pub hlen: u32, // c:3121
4580 /// Packed `{ kshload_bits(2) | name_tail_offset(30) }` field.
4581 pub flags: u32, // c:3122
4582}
4583
4584/// `#define FDHF_KSHLOAD 1` from `Src/parse.c:3149`. Function-header
4585/// flag word — `-k` ksh-style autoload marker.
4586pub const FDHF_KSHLOAD: u32 = 1;
4587
4588/// `#define FDHF_ZSHLOAD 2` from `Src/parse.c:3150`. `-z` zsh-style
4589/// autoload marker.
4590pub const FDHF_ZSHLOAD: u32 = 2;
4591
4592/// Port of `struct wcfunc` from `Src/parse.c:3158`. Build-time
4593/// per-function aggregate before write_dump emits it: the function
4594/// (or source-file) name, the compiled `Eprog` from `parse_string`,
4595/// and the FDHF_* autoload-style flag word.
4596#[allow(non_camel_case_types)]
4597#[derive(Debug, Clone)]
4598pub struct wcfunc {
4599 pub name: String, // c:3159
4600 /// Compiled program (`Eprog prog` c:3160) — wordcode + strs +
4601 /// npats as built by `bld_eprog`.
4602 pub prog: eprog, // c:3160
4603 pub flags: u32, // c:3161
4604}
4605
4606/// Port of `dump_find_func()` from `Src/parse.c:3167` — C decl `dump_find_func(Wordcode h, char *name)`.
4607/// Walks the header table inside a loaded
4608/// dump for a function with the given basename; returns the
4609/// matching `fdhead` record (C returns the `FDHead` pointer).
4610pub fn dump_find_func(h: &[u32], name: &str) -> Option<fdhead> {
4611 // c:3167
4612 let header_words = fdheaderlen(h) as usize;
4613 let end = header_words; // walking u32 offsets, end-exclusive
4614 // c:3152 — `fdname(n)` is a `char *` INTO the header words, so C compares
4615 // in place. The same view here: the words were read in host byte order
4616 // (load_dump_header, c:3268), exactly the bytes C's pointer walks.
4617 // SAFETY: a byte view of the u32 header words; u32 has no padding and
4618 // every bit pattern is a valid u8.
4619 let header_bytes = unsafe { std::slice::from_raw_parts(h.as_ptr() as *const u8, h.len() * 4) };
4620 let want = name.as_bytes();
4621 let mut cur = firstfdhead_offset();
4622 while cur < end {
4623 if let Some(fh) = read_fdhead(h, cur) {
4624 // c:3172 — `if (!strcmp(name, fdname(n) + fdhtail(n)))`: compinit's
4625 // `autoload -rUz` walks every entry of every `$fpath` digest, so
4626 // this is one slice prefix test plus the NUL strcmp stops at, not a
4627 // per-byte lookup through each u32 word. A name running to the end
4628 // of the header counts as terminated, as the per-byte reader did.
4629 let name_start = (cur + FDHEAD_WORDS) * 4 + fdhtail(&fh) as usize;
4630 let entry = header_bytes.get(name_start..).unwrap_or(&[]);
4631 let same = entry.starts_with(want) && entry.get(want.len()).map_or(true, |&b| b == 0);
4632 if same {
4633 return Some(fh); // c:3173 `return n;`
4634 }
4635 cur = nextfdhead_offset(h, cur);
4636 } else {
4637 break;
4638 }
4639 }
4640 None // c:3175
4641}
4642
4643/// Port of `bin_zcompile()` from `Src/parse.c:3180` — C decl `bin_zcompile(char *nam, char **args, Options ops, UNUSED(int func))`.
4644/// Validates the option set, then dispatches
4645/// to one of: `-t` (test/list), `-c`/`-a` (dump current functions),
4646/// or the default (compile source files to `.zwc`).
4647pub fn bin_zcompile(
4648 nam: &str, // c:3180
4649 args: &[String],
4650 ops: &crate::ported::zsh_h::options,
4651 _func: i32,
4652) -> i32 {
4653 // c:3185-3192 — illegal-combination guard.
4654 if (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z'))
4655 || (OPT_ISSET(ops, b'R') && OPT_ISSET(ops, b'M'))
4656 || (OPT_ISSET(ops, b'c')
4657 && (OPT_ISSET(ops, b'U') || OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'z')))
4658 || (!(OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) && OPT_ISSET(ops, b'm'))
4659 {
4660 zwarnnam(nam, "invalid combination of options"); // c:3192
4661 return 1;
4662 }
4663
4664 // c:3194 — `-c`/`-a` + KSHAUTOLOAD warning.
4665 if (OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) && isset(crate::ported::zsh_h::KSHAUTOLOAD) {
4666 zwarnnam(nam, "functions will use zsh style autoloading"); // c:3195
4667 }
4668
4669 // c:3196-3197 — flag word from `-k` / `-z`.
4670 let flags: u32 = if OPT_ISSET(ops, b'k') {
4671 FDHF_KSHLOAD
4672 } else if OPT_ISSET(ops, b'z') {
4673 FDHF_ZSHLOAD
4674 } else {
4675 0
4676 };
4677
4678 // c:3199 — `-t` test/list mode.
4679 if OPT_ISSET(ops, b't') {
4680 // c:3199
4681 if args.is_empty() {
4682 zwarnnam(nam, "too few arguments"); // c:3202
4683 return 1;
4684 }
4685 let dump_name = if args[0].ends_with(FD_EXT) {
4686 args[0].clone()
4687 } else {
4688 format!("{}{}", args[0], FD_EXT)
4689 };
4690 let f = match load_dump_header(nam, &dump_name, 1) {
4691 // c:3206
4692 Some(buf) => buf,
4693 None => return 1,
4694 };
4695 // c:3209 — per-function check.
4696 if args.len() > 1 {
4697 for name in &args[1..] {
4698 // c:3210
4699 if dump_find_func(&f, name).is_none() {
4700 // c:3212
4701 return 1;
4702 }
4703 }
4704 return 0;
4705 }
4706 // c:3215-3221 — listing arm. Walk every fdhead, print
4707 // each function's full name. C uses `fdname(h)` which
4708 // includes the path prefix; matches our `fdname()` impl.
4709 let mapped = if (fdflags(&f) & FDF_MAP) != 0 {
4710 "mapped"
4711 } else {
4712 "read"
4713 };
4714 println!("zwc file ({}) for zsh-{}", mapped, fdversion(&f));
4715 let header_words = fdheaderlen(&f) as usize;
4716 let mut cur = firstfdhead_offset();
4717 while cur < header_words {
4718 if read_fdhead(&f, cur).is_none() {
4719 break;
4720 }
4721 println!("{}", fdname(&f, cur));
4722 cur = nextfdhead_offset(&f, cur);
4723 }
4724 return 0;
4725 }
4726
4727 if args.is_empty() {
4728 zwarnnam(nam, "too few arguments"); // c:3226
4729 return 1;
4730 }
4731
4732 // c:3228 — map mode discriminant.
4733 let map: i32 = if OPT_ISSET(ops, b'M') {
4734 2
4735 } else if OPT_ISSET(ops, b'R') {
4736 0
4737 } else {
4738 1
4739 };
4740
4741 // c:3230-3236 — single-file default-mode short path.
4742 if args.len() == 1 && !(OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) {
4743 let dump = format!("{}{}", args[0], FD_EXT);
4744 return build_dump(nam, &dump, args, OPT_ISSET(ops, b'U') as i32, map, flags);
4745 }
4746
4747 // c:3239-3247 — multi-file or `-c`/`-a` mode.
4748 let dump = if args[0].ends_with(FD_EXT) {
4749 args[0].clone()
4750 } else {
4751 format!("{}{}", args[0], FD_EXT)
4752 };
4753 let rest = &args[1..];
4754 if OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a') {
4755 let what =
4756 (if OPT_ISSET(ops, b'c') { 1 } else { 0 }) | (if OPT_ISSET(ops, b'a') { 2 } else { 0 });
4757 build_cur_dump(nam, &dump, rest, OPT_ISSET(ops, b'm') as i32, map, what)
4758 } else {
4759 build_dump(nam, &dump, rest, OPT_ISSET(ops, b'U') as i32, map, flags)
4760 }
4761}
4762
4763/// Port of `load_dump_header()` from `Src/parse.c:3258` — C decl `load_dump_header(char *nam, char *name, int err)`.
4764/// Opens the file, reads + validates the magic
4765/// and version, then slurps the full header table into memory.
4766/// Returns the header u32-array on success or None on any failure
4767/// (emitting C-shaped warnings when `err != 0`).
4768pub fn load_dump_header(nam: &str, name: &str, err: i32) -> Option<Vec<u32>> {
4769 // !!! WARNING: RUST-ONLY HELPER !!!
4770 //
4771 // C's `nam` is a `char *`, and `check_dump_file` passes it as NULL:
4772 // c:Src/parse.c:3870 — `load_dump_header(NULL, file, 0)`. In C
4773 // `zwarnnam(NULL, …)` is NOT silent: it reaches `zwarning(NULL, …)` and
4774 // takes the else branch there, printing the bare `scriptname:` prefix —
4775 // exactly what `zwarn` does. The Rust signature is `&str`, so NULL is
4776 // encoded as `""`, and `zwarnnam("")` instead takes `zwarning(Some(""))`'s
4777 // CMD branch (utils.rs) — prefix `:` then the empty cmd then another `:` —
4778 // emitting a SECOND colon:
4779 // zsh zsh:1: invalid zwc file: <path> / _zz01:2: …
4780 // zshrs zshrs::1: invalid zwc file: <path> / _zz01::2: …
4781 //
4782 // Route the empty name to `zwarn`, which is the same output C's NULL
4783 // branch produces. Same trap, same C citations, as the note in
4784 // `src/ported/jobs.rs`.
4785 let warn_nam = |msg: &str| {
4786 if nam.is_empty() {
4787 crate::ported::utils::zwarn(msg)
4788 } else {
4789 zwarnnam(nam, msg)
4790 }
4791 };
4792 // c:3258
4793
4794 let mut f = match File::open(name) {
4795 // c:3263
4796 Ok(h) => h,
4797 Err(_) => {
4798 if err != 0 {
4799 warn_nam(&format!("can't open zwc file: {}", name)); // c:3265
4800 }
4801 return None;
4802 }
4803 };
4804
4805 // c:3261 `wordcode buf[FD_PRELEN + 1];` + c:3268 `read(fd, buf, (FD_PRELEN + 1) *
4806 // sizeof(wordcode))` — the words are read straight into the wordcode
4807 // buffer, in host byte order; `fdmagic` below is what detects a file
4808 // written with the other order (c:3270, c:3285-3301).
4809 let mut buf: Vec<u32> = vec![0u32; FD_PRELEN + 1];
4810 // SAFETY: a byte view of the u32 buffer, as C's `read(fd, buf, n *
4811 // sizeof(wordcode))`; u32 has no padding and every bit pattern is valid.
4812 let buf_bytes =
4813 unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len() * 4) };
4814 if f.read_exact(buf_bytes).is_err() {
4815 if err != 0 {
4816 warn_nam(&format!("invalid zwc file: {}", name)); // c:3277
4817 }
4818 return None;
4819 }
4820
4821 // c:3270 — magic + version check against `ZSH_VERSION` (C global;
4822 // zshrs mirrors it in `patchlevel::ZSH_VERSION`).
4823 let magic_ok = fdmagic(&buf) == FD_MAGIC || fdmagic(&buf) == FD_OMAGIC;
4824 let v_ok = fdversion(&buf) == crate::ported::patchlevel::ZSH_VERSION;
4825 if !magic_ok {
4826 if err != 0 {
4827 warn_nam(&format!("invalid zwc file: {}", name)); // c:3277
4828 }
4829 return None;
4830 }
4831 if !v_ok {
4832 if err != 0 {
4833 warn_nam(
4834 &format!(
4835 "zwc file has wrong version (zsh-{}): {}", // c:3274
4836 fdversion(&buf),
4837 name
4838 ),
4839 );
4840 }
4841 return None;
4842 }
4843
4844 // c:3285 — if magic matches host byte order, head len is `pre[FD_PRELEN]`.
4845 // Else seek to `fdother(buf)` and re-read.
4846 if fdmagic(&buf) != FD_MAGIC {
4847 let other = fdother(&buf) as u64; // c:3290
4848 // c:3292-3294 — re-read the other copy's prefix into the same buffer.
4849 // SAFETY: byte view of the u32 buffer, as above.
4850 let buf_bytes =
4851 unsafe { std::slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u8, buf.len() * 4) };
4852 if f.seek(SeekFrom::Start(other)).is_err() || f.read_exact(buf_bytes).is_err() {
4853 warn_nam(&format!("invalid zwc file: {}", name)); // c:3295
4854 return None;
4855 }
4856 }
4857
4858 let total_words = fdheaderlen(&buf) as usize; // c:3286/3299
4859 if total_words < FD_PRELEN + 1 {
4860 warn_nam(&format!("invalid zwc file: {}", name));
4861 return None;
4862 }
4863
4864 // c:3287/3300 `head = zhalloc(len)`, c:3302 `memcpy(head, buf, (FD_PRELEN +
4865 // 1) * sizeof(wordcode))`, c:3304-3305 `read(fd, head + (FD_PRELEN + 1),
4866 // len)` — the rest of the header is read straight into the words after
4867 // the prefix, one read, no per-word conversion. Converting byte chunks
4868 // one word at a time was most of `compinit`'s cost here: every
4869 // `autoload -r` lookup re-reads each digest header on $fpath, which C
4870 // does too (check_dump_file maps a digest only for a non-test lookup).
4871 let mut head: Vec<u32> = vec![0u32; total_words];
4872 head[..FD_PRELEN + 1].copy_from_slice(&buf); // c:3302
4873 let rest = &mut head[FD_PRELEN + 1..];
4874 if !rest.is_empty() {
4875 // SAFETY: byte view of the u32 words, as above.
4876 let rest_bytes =
4877 unsafe { std::slice::from_raw_parts_mut(rest.as_mut_ptr() as *mut u8, rest.len() * 4) };
4878 if f.read_exact(rest_bytes).is_err() {
4879 warn_nam(&format!("invalid zwc file: {}", name)); // c:3307
4880 return None;
4881 }
4882 }
4883 Some(head) // c:3311
4884}
4885
4886/// Port of `fdswap()` from `Src/parse.c:3318` — C signature `fdswap(Wordcode p, int n)`.
4887/// Byte-swap each u32 in `p[..n]` in place. Used when writing the
4888/// opposite-byte-order copy of a wordcode dump.
4889pub fn fdswap(p: &mut [u32]) {
4890 // c:3318
4891 for w in p.iter_mut() {
4892 *w = w.swap_bytes();
4893 }
4894}
4895
4896/// Port of `write_dump()` from `Src/parse.c:3334` — C decl `write_dump(int dfd, LinkList progs, int map, int hlen, int tlen)`.
4897/// Writes the prelude + header records +
4898/// body wordcode bytes to the dump file descriptor.
4899///
4900/// Two passes: first native-byte-order (`FD_MAGIC`), then opposite-
4901/// byte-order (`FD_OMAGIC`) so big-endian readers can mmap the
4902/// same file. Bodies are byte-swapped via `fdswap` on the second pass.
4903pub fn write_dump(
4904 dfd: &mut File, // c:3334
4905 progs: &[wcfunc],
4906 mut map: i32,
4907 hlen: i32,
4908 tlen: i32,
4909) -> std::io::Result<()> {
4910 // c:3345-3346 — `if (map == 1) map = (tlen >= FD_MINMAP);`
4911 if map == 1 {
4912 map = ((tlen as usize) >= FD_MINMAP) as i32;
4913 }
4914
4915 // C `sizeof(Patprog)` — pointer size (see bld_eprog len arithmetic).
4916 let patprog_size = size_of::<*const u8>() as i32;
4917
4918 let mut other = 0u32; // c:3338
4919 let ohlen = hlen;
4920
4921 loop {
4922 // c:3349 — `for (ohlen = hlen; ; hlen = ohlen)`.
4923 let mut cur_hlen = ohlen;
4924 // c:3348 — `memset(pre, 0, sizeof(wordcode) * FD_PRELEN);`
4925 let mut pre = vec![0u32; FD_PRELEN];
4926 pre[0] = if other != 0 { FD_OMAGIC } else { FD_MAGIC }; // c:3350
4927 let flags = (if map != 0 { FDF_MAP } else { 0 }) | other;
4928 fdsetflags(&mut pre, flags as u8); // c:3351
4929 fdsetother(&mut pre, tlen as u32); // c:3352
4930 // c:3353 — copy ZSH_VERSION C-string into pre[2..].
4931 let ver = crate::ported::patchlevel::ZSH_VERSION.as_bytes();
4932 for (i, &b) in ver.iter().enumerate() {
4933 let word = 2 + i / 4;
4934 let shift = (i % 4) * 8;
4935 pre[word] |= (b as u32) << shift;
4936 }
4937 // c:3354 — write prelude.
4938 for w in &pre {
4939 dfd.write_all(&w.to_le_bytes())?;
4940 }
4941 // c:3356 — per-fn header records.
4942 for wcf in progs {
4943 let n = &wcf.name;
4944 let prog = &wcf.prog;
4945 // c:3362-3363 — body length in bytes excluding the
4946 // pattern-prog slots: `prog->len - (prog->npats *
4947 // sizeof(Patprog))`.
4948 let len_bytes = prog.len - prog.npats * patprog_size;
4949 let mut head = fdhead {
4950 start: cur_hlen as u32, // c:3360
4951 len: len_bytes as u32, // c:3363
4952 npats: prog.npats as u32, // c:3364
4953 // c:3365 — `head.strs = prog->strs - ((char *) prog->prog);`
4954 // In bld_eprog's layout strs sits right after the code
4955 // words, so the byte offset is ecused * 4.
4956 strs: (prog.prog.len() * 4) as u32, // c:3365
4957 hlen: ((FDHEAD_WORDS as u32) + ((n.len() as u32 + 4) / 4)), // c:3366
4958 flags: 0,
4959 };
4960 // c:3361 — `hlen += (prog->len - npats*sizeof(Patprog) +
4961 // sizeof(wordcode) - 1) / sizeof(wordcode);`
4962 cur_hlen += (len_bytes + 3) / 4;
4963 // c:3368-3371 — name tail offset from path basename.
4964 let tail = n.rfind('/').map(|p| p + 1).unwrap_or(0);
4965 head.flags = fdhbldflags(wcf.flags, tail as u32); // c:3372
4966 // c:3373 — opposite-byte-order swap on second pass.
4967 let mut head_words: Vec<u32> = vec![
4968 head.start, head.len, head.npats, head.strs, head.hlen, head.flags,
4969 ];
4970 if other != 0 {
4971 fdswap(&mut head_words);
4972 }
4973 for w in &head_words {
4974 dfd.write_all(&w.to_le_bytes())?;
4975 }
4976 // c:3376-3379 — write the name + NUL, then pad to a word
4977 // boundary with the leading bytes of `head` (C: `write_loop
4978 // (dfd, (char *)&head, sizeof(wordcode) - tmp);`).
4979 dfd.write_all(n.as_bytes())?;
4980 dfd.write_all(&[0u8])?;
4981 let tmp = (n.len() + 1) & 3;
4982 if tmp != 0 {
4983 let head_bytes = head_words[0].to_le_bytes();
4984 dfd.write_all(&head_bytes[..4 - tmp])?;
4985 }
4986 }
4987 // c:3381-3388 — per-fn bodies: code words then the strs region,
4988 // padded to a word boundary. `tmp = (prog->len - npats*
4989 // sizeof(Patprog) + sizeof(wordcode) - 1) / sizeof(wordcode);
4990 // write_loop(dfd, (char *)prog->prog, tmp * sizeof(wordcode));`
4991 for wcf in progs {
4992 let prog = &wcf.prog;
4993 let len_bytes = (prog.len - prog.npats * patprog_size) as usize;
4994 let tmp = (len_bytes + 3) / 4;
4995 // c:3386-3387 — on the other pass only the code words are
4996 // swapped (`fdswap(prog->prog, ((Wordcode) prog->strs) -
4997 // prog->prog)`); the strs chars are byte-order neutral.
4998 let mut body_bytes: Vec<u8> = Vec::with_capacity(tmp * 4);
4999 for &w in &prog.prog {
5000 let w = if other != 0 { w.swap_bytes() } else { w };
5001 body_bytes.extend_from_slice(&w.to_le_bytes());
5002 }
5003 if let Some(s) = &prog.strs {
5004 body_bytes.extend_from_slice(s.as_bytes());
5005 }
5006 // C reads up to 3 bytes of heap slop past strs; emit NULs
5007 // (readers never look past head.len so the value is free).
5008 body_bytes.resize(tmp * 4, 0);
5009 dfd.write_all(&body_bytes)?;
5010 }
5011 if other != 0 {
5012 // c:3389
5013 break;
5014 }
5015 other = FDF_OTHER; // c:3391
5016 }
5017 Ok(())
5018}
5019
5020/// Port of `build_dump()` from `Src/parse.c:3397` — C signature
5021/// `build_dump(char *nam, char *dump, char **files, int ali, int map, int flags)`.
5022/// Source-file → wordcode dump compiler:
5023/// parses each source file via `parse_string` and serializes the
5024/// resulting `Eprog`s through `write_dump` into `<dump>.zwc`.
5025pub fn build_dump(
5026 nam: &str, // c:3397
5027 dump: &str,
5028 files: &[String],
5029 ali: i32,
5030 map: i32,
5031 flags: u32,
5032) -> i32 {
5033 use crate::ported::utils::{errflag, ERRFLAG_ERROR};
5034 use std::os::unix::fs::OpenOptionsExt;
5035 use std::sync::atomic::Ordering;
5036
5037 // c:3403-3404 — append FD_EXT unless already suffixed.
5038 let dump: String = if dump.ends_with(FD_EXT) {
5039 dump.to_string()
5040 } else {
5041 format!("{}{}", dump, FD_EXT)
5042 };
5043
5044 // c:3406 — `unlink(dump);`
5045 let _ = fs::remove_file(&dump);
5046 // c:3407-3410 — `open(dump, O_WRONLY|O_CREAT, 0444)`.
5047 let mut dfd = match fs::OpenOptions::new()
5048 .write(true)
5049 .create(true)
5050 .mode(0o444)
5051 .open(&dump)
5052 {
5053 Ok(f) => f,
5054 Err(_) => {
5055 zwarnnam(nam, &format!("can't write zwc file: {}", dump)); // c:3408
5056 return 1;
5057 }
5058 };
5059
5060 let patprog_size = size_of::<*const u8>() as i32; // C sizeof(Patprog)
5061 let ona = crate::ported::lex::noaliases(); // c:3398 `ona = noaliases`
5062 crate::ported::lex::set_noaliases(ali != 0); // c:3412 `noaliases = ali;`
5063
5064 let mut progs: Vec<wcfunc> = Vec::new(); // c:3411
5065 let mut flags = flags;
5066 let mut hlen = FD_PRELEN as i32; // c:3414
5067 let mut tlen: i32 = 0;
5068
5069 for fname in files {
5070 // c:3418-3425 — `-k` / `-z` pseudo-args flip the autoload style.
5071 if check_cond(fname, "k") {
5072 flags = (flags & !(FDHF_KSHLOAD | FDHF_ZSHLOAD)) | FDHF_KSHLOAD; // c:3419
5073 continue;
5074 } else if check_cond(fname, "z") {
5075 flags = (flags & !(FDHF_KSHLOAD | FDHF_ZSHLOAD)) | FDHF_ZSHLOAD; // c:3422
5076 continue;
5077 }
5078 // c:3426-3437 — open + fstat + S_ISREG + read the whole file.
5079 let fnam = crate::ported::utils::unmeta(fname); // c:3426
5080 let is_reg = fs::metadata(&fnam).map(|m| m.is_file()).unwrap_or(false);
5081 let bytes = if is_reg { fs::read(&fnam).ok() } else { None };
5082 let bytes = match bytes {
5083 Some(b) => b,
5084 None => {
5085 zwarnnam(nam, &format!("can't open file: {}", fname)); // c:3432
5086 crate::ported::lex::set_noaliases(ona); // c:3433
5087 let _ = fs::remove_file(&dump); // c:3434
5088 return 1;
5089 }
5090 };
5091 // c:3450 — `file = metafy(file, flen, META_REALLOC);` — C hands
5092 // the metafied bytes straight to `parse_string`, because C's lexer
5093 // eats metafied bytes. The Rust lexer eats char-form text, and
5094 // `utils::metafy` builds a BYTE-form buffer that no `String` can
5095 // hold, so it fell back to `from_utf8_lossy` and stamped U+FFFD
5096 // into the dump: `zcompile` of `print -r -- a\u{2014}b` wrote the
5097 // pool bytes `61 e2 80 83 a3 ef bf bd 62` where zsh writes
5098 // `61 e2 80 83 b4 62`, and sourcing that dump printed the
5099 // replacement character in BOTH shells. C's byte-level metafy is
5100 // applied where it belongs — `ecstrcode` (c:436, parse.rs:346)
5101 // converts char-form to C-byte form as the pool is written — so
5102 // what the lexer needs here is char-form text.
5103 let file = crate::script_bytes::decode_script_bytes(&bytes);
5104
5105 // c:3452-3460 — parse; any error aborts the whole dump.
5106 let prog = crate::ported::exec::parse_string(&file, 1);
5107 let errored = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
5108 let prog = match prog {
5109 Some(p) if !errored => p,
5110 _ => {
5111 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:3453
5112 zwarnnam(nam, &format!("can't read file: {}", fname)); // c:3456
5113 crate::ported::lex::set_noaliases(ona); // c:3457
5114 let _ = fs::remove_file(&dump); // c:3458
5115 return 1;
5116 }
5117 };
5118
5119 // c:3467-3472 — accumulate header + body length budgets.
5120 let flen = (fname.len() as i32 + 4) / 4; // c:3469
5121 hlen += (FDHEAD_WORDS as i32) + flen; // c:3470
5122 tlen += (prog.len - prog.npats * patprog_size + 3) / 4; // c:3471
5123
5124 // c:3463-3466 — wcfunc node.
5125 let wcf_flags = if (prog.flags & EF_RUN) != 0 {
5126 FDHF_KSHLOAD // c:3465
5127 } else {
5128 flags
5129 };
5130 progs.push(wcfunc {
5131 name: fname.clone(),
5132 prog,
5133 flags: wcf_flags,
5134 });
5135 }
5136 crate::ported::lex::set_noaliases(ona); // c:3474
5137
5138 let tlen = (tlen + hlen) * 4; // c:3476
5139
5140 // c:3478 — `write_dump(dfd, progs, map, hlen, tlen);` (void in C).
5141 let _ = write_dump(&mut dfd, &progs, map, hlen, tlen);
5142
5143 0 // c:3482
5144}
5145
5146/// Port of `cur_add_func()` from `Src/parse.c:3489` — C decl `cur_add_func(char *nam, Shfunc shf, LinkList names, LinkList progs, int *hlen, int *tlen, int what)`.
5147/// Adds a shfunc to the in-build dump progs+names lists. Returns 1 after
5148/// a `zwarnnam` when the function cannot be dumped; the caller removes the
5149/// half-written dump.
5150///
5151/// zshrs divergence: C parses every function into `shf->funcdef`
5152/// (wordcode) at definition time, so `dupeprog(shf->funcdef, 1)`
5153/// always has a program to copy. zshrs defers the compile — a
5154/// loaded user function stores its source in `shf.body` with
5155/// `funcdef == None` (`Src/exec.c:5540-5545`). The loaded-function arm
5156/// therefore falls back to `parse_string(body, 1)` to obtain the same
5157/// wordcode `Eprog` C would have had eagerly, using the exact substrate
5158/// `build_dump` feeds to `write_dump`.
5159pub fn cur_add_func(
5160 nam: &str, // c:3489
5161 shf: &crate::ported::zsh_h::shfunc,
5162 names: &mut Vec<String>,
5163 progs: &mut Vec<wcfunc>,
5164 hlen: &mut i32,
5165 tlen: &mut i32,
5166 what: i32,
5167) -> i32 {
5168 let fname = shf.node.nam.clone();
5169 let flags = shf.node.flags;
5170 let patprog_size = size_of::<*const u8>() as i32; // C `sizeof(Patprog)`
5171
5172 let prog: eprog = if (flags & PM_UNDEFINED as i32) != 0 {
5173 // c:3495-3512 — autoload stub: only dumpable with `-a`.
5174 if (what & 2) == 0 {
5175 zwarnnam(nam, &format!("function is not loaded: {}", fname)); // c:3499
5176 return 1;
5177 }
5178 // c:3502 — `noaliases = (shf->node.flags & PM_UNALIASED);`
5179 let ona = crate::ported::lex::noaliases();
5180 crate::ported::lex::set_noaliases(
5181 (flags & crate::ported::zsh_h::PM_UNALIASED as i32) != 0,
5182 );
5183 // c:3503 — `getfpfunc(shf->node.nam, NULL, NULL, NULL, 0)`.
5184 let mut dir_out: Option<String> = None;
5185 let mut dump_out: Option<(eprog, i32)> = None;
5186 let found = crate::ported::exec::getfpfunc(&fname, &mut dir_out, None, 0, &mut dump_out);
5187 crate::ported::lex::set_noaliases(ona); // c:3506 / c:3511
5188 let loaded: Option<eprog> = match dump_out {
5189 // c:3509-3510 — `if (prog->dump) prog = dupeprog(prog, 1);`
5190 Some((p, _)) => Some(if p.dump.is_some() {
5191 dupeprog(&p, true)
5192 } else {
5193 p
5194 }),
5195 None => match found {
5196 // zshrs's `getfpfunc` only materializes an `Eprog`
5197 // for `.zwc` digest hits; a plain autoload source
5198 // comes back as a path. C's `getfpfunc` parses the
5199 // source itself — reproduce that with the same
5200 // read + `parse_string` path `build_dump` uses.
5201 Some(path) => {
5202 let fnam = crate::ported::utils::unmeta(&path);
5203 match fs::read(&fnam) {
5204 Ok(bytes) => {
5205 // Same char-form read as `build_dump`
5206 // above (c:3450): `utils::metafy` cannot
5207 // represent its own byte-form output in a
5208 // `String` and lossily replaced any
5209 // non-ASCII source char.
5210 let file = crate::script_bytes::decode_script_bytes(&bytes);
5211 crate::ported::exec::parse_string(&file, 1)
5212 }
5213 Err(_) => None,
5214 }
5215 }
5216 None => None,
5217 },
5218 };
5219 match loaded {
5220 Some(p) => p,
5221 None => {
5222 zwarnnam(nam, &format!("can't load function: {}", fname)); // c:3505
5223 return 1;
5224 }
5225 }
5226 } else {
5227 // c:3513-3518 — loaded function: dump only with `-c`.
5228 if (what & 1) == 0 {
5229 zwarnnam(nam, &format!("function is already loaded: {}", fname)); // c:3515
5230 return 1;
5231 }
5232 // c:3517 — `prog = dupeprog(shf->funcdef, 1);`. In zshrs a
5233 // loaded function usually carries its source in `body` with
5234 // `funcdef == None` (deferred compile); compile it now to
5235 // recover the wordcode C stored eagerly.
5236 match &shf.funcdef {
5237 Some(fd) => dupeprog(fd, true),
5238 None => match &shf.body {
5239 Some(b) => match {
5240 // The stored body is text the definition's parse
5241 // already alias-expanded (C stores wordcode); compile
5242 // it under the same lexer pins `functions` uses so an
5243 // alias is not expanded a second time.
5244 let _pin = crate::vm_helper::funcdef_lex_pin(&fname, b);
5245 crate::ported::exec::parse_string(b, 1)
5246 } {
5247 Some(p) => p,
5248 None => {
5249 zwarnnam(nam, &format!("can't load function: {}", fname));
5250 return 1;
5251 }
5252 },
5253 // Empty-bodied function (no funcdef, no source):
5254 // emit a zero-length program, matching the
5255 // degenerate `Eprog` C would carry for `f() { }`.
5256 None => eprog::default(),
5257 },
5258 }
5259 };
5260
5261 // c:3521-3527 — build the wcfunc node.
5262 let wcf_flags = if (prog.flags & EF_RUN) != 0 {
5263 FDHF_KSHLOAD // c:3526
5264 } else {
5265 FDHF_ZSHLOAD // c:3526
5266 };
5267 // c:3531-3534 — accumulate header + body word budgets.
5268 *hlen += (FDHEAD_WORDS as i32) + ((fname.len() as i32 + 4) / 4); // c:3531-3532
5269 *tlen += (prog.len - prog.npats * patprog_size + 3) / 4; // c:3533-3534
5270 names.push(fname.clone()); // c:3529
5271 progs.push(wcfunc {
5272 name: fname,
5273 prog,
5274 flags: wcf_flags,
5275 });
5276
5277 0 // c:3536
5278}
5279
5280/// Port of `build_cur_dump()` from `Src/parse.c:3536` — C decl `build_cur_dump(char *nam, char *dump, char **names, int match, int map, int what)`.
5281/// Serializes the currently-loaded shell
5282/// functions (`-c` → `what & 1`) and/or autoloadable ones (`-a` →
5283/// `what & 2`) into a `.zwc` dump. Shares `write_dump` with the
5284/// source-file variant `build_dump`.
5285///
5286/// The per-function collection is `cur_add_func` (`Src/parse.c:3489`).
5287/// The shfunctab entries are cloned out first so its read lock is released
5288/// before `getfpfunc` (which re-locks the table on the PM_UNDEFINED
5289/// autoload path) runs inside `cur_add_func`.
5290pub fn build_cur_dump(
5291 nam: &str, // c:3536
5292 dump: &str,
5293 names: &[String],
5294 match_: i32,
5295 map: i32,
5296 what: i32,
5297) -> i32 {
5298 use crate::ported::utils::ERRFLAG_ERROR;
5299 use std::os::unix::fs::OpenOptionsExt;
5300 use std::sync::atomic::Ordering;
5301
5302 // c:3542-3543 — `if (!strsfx(FD_EXT, dump)) dump = dyncat(dump, FD_EXT);`
5303 let dump: String = if dump.ends_with(FD_EXT) {
5304 dump.to_string()
5305 } else {
5306 format!("{}{}", dump, FD_EXT)
5307 };
5308
5309 // c:3545 — `unlink(dump);`
5310 let _ = fs::remove_file(&dump);
5311 // c:3546-3549 — `open(dump, O_WRONLY|O_CREAT, 0444)`.
5312 let mut dfd = match fs::OpenOptions::new()
5313 .write(true)
5314 .create(true)
5315 .mode(0o444)
5316 .open(&dump)
5317 {
5318 Ok(f) => f,
5319 Err(_) => {
5320 zwarnnam(nam, &format!("can't write zwc file: {}", dump)); // c:3547
5321 return 1;
5322 }
5323 };
5324
5325 // c:3551-3555 — `progs`/`lnames` lists, `hlen = FD_PRELEN`, `tlen = 0`.
5326 let mut progs: Vec<wcfunc> = Vec::new();
5327 let mut lnames: Vec<String> = Vec::new();
5328 let mut hlen = FD_PRELEN as i32;
5329 let mut tlen: i32 = 0;
5330
5331 // Per-function candidates gathered from `shfunctab` before the
5332 // program serialization pass. Held as owned data so the table's
5333 // read lock is released before `getfpfunc` (which re-locks the
5334 // table on the PM_UNDEFINED autoload path) runs.
5335 let mut candidates: Vec<crate::ported::zsh_h::shfunc> = Vec::new();
5336
5337 if names.is_empty() {
5338 // c:3557-3567 — no names: dump every function in the table.
5339 let tab = crate::ported::hashtable::shfunctab_lock()
5340 .read()
5341 .expect("shfunctab poisoned");
5342 for (_, shf) in tab.iter() {
5343 candidates.push(shf.clone());
5344 }
5345 } else if match_ != 0 {
5346 // c:3568-3597 — pattern match against the whole table per arg.
5347 for pat_name in names {
5348 // c:3577 — `tokenize(pat = dupstring(*names));`
5349 let mut tok = pat_name.clone();
5350 crate::ported::glob::tokenize(&mut tok);
5351 // c:3579 — `patcompile(pat, PAT_STATIC, NULL)`.
5352 let pprog = match crate::ported::pattern::patcompile(
5353 &tok,
5354 crate::ported::zsh_h::PAT_STATIC,
5355 None::<&mut String>,
5356 ) {
5357 Some(p) => p,
5358 None => {
5359 zwarnnam(nam, &format!("bad pattern: {}", pat_name)); // c:3581
5360 let _ = fs::remove_file(&dump); // c:3583
5361 return 1;
5362 }
5363 };
5364 let tab = crate::ported::hashtable::shfunctab_lock()
5365 .read()
5366 .expect("shfunctab poisoned");
5367 // c:3586-3595 — for each function not already collected that
5368 // matches the pattern, add it. `lnames` is the dedup list
5369 // (C: `!linknodebydatum(lnames, hn->nam)`).
5370 for (fname, shf) in tab.iter() {
5371 if !candidates.iter().any(|c| c.node.nam == *fname)
5372 && crate::ported::pattern::pattry(&pprog, fname)
5373 {
5374 candidates.push(shf.clone());
5375 }
5376 }
5377 drop(tab);
5378 }
5379 } else {
5380 // c:3598-3617 — explicit names: each must resolve to a function.
5381 for fname in names {
5382 let errored = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
5383 let tab = crate::ported::hashtable::shfunctab_lock()
5384 .read()
5385 .expect("shfunctab poisoned");
5386 match (errored, tab.get(fname)) {
5387 // c:3600-3601 — `if (errflag || !(shf = getnode(*names)))`.
5388 (false, Some(shf)) => {
5389 candidates.push(shf.clone());
5390 }
5391 _ => {
5392 drop(tab);
5393 zwarnnam(nam, &format!("unknown function: {}", fname)); // c:3603
5394 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:3604
5395 let _ = fs::remove_file(&dump); // c:3606
5396 return 1;
5397 }
5398 }
5399 }
5400 }
5401
5402 for shf in &candidates {
5403 // c:3541-3617 — `cur_add_func(nam, shf, lnames, progs, &hlen, &tlen, what)`.
5404 if cur_add_func(nam, shf, &mut lnames, &mut progs, &mut hlen, &mut tlen, what) != 0 {
5405 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
5406 let _ = fs::remove_file(&dump);
5407 return 1;
5408 }
5409 }
5410
5411 // c:3619-3625 — `if (empty(progs)) { zwarnnam(nam, "no functions"); ... }`
5412 if progs.is_empty() {
5413 zwarnnam(nam, "no functions"); // c:3620
5414 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:3621
5415 let _ = fs::remove_file(&dump); // c:3623
5416 return 1;
5417 }
5418
5419 // c:3626 — `tlen = (tlen + hlen) * sizeof(wordcode);`
5420 let tlen = (tlen + hlen) * 4;
5421
5422 // c:3628 — `write_dump(dfd, progs, map, hlen, tlen);`
5423 let _ = write_dump(&mut dfd, &progs, map, hlen, tlen);
5424
5425 // c:3630 — `close(dfd);` (Rust: `dfd` drops here). Keep `lnames`
5426 // referenced — it mirrors C's parallel names list.
5427 let _ = lnames;
5428
5429 0 // c:3632
5430}
5431
5432/// Port of `zwcstat()` from `Src/parse.c:3656` — C decl `zwcstat(char *filename, struct stat *buf)`.
5433/// Stats a `.zwc` file, falling back to
5434/// `.zwc.old` if the primary doesn't exist (zsh uses the `.old`
5435/// suffix to keep a previous dump readable while a rewrite is in
5436/// progress).
5437pub fn zwcstat(filename: &str) -> Option<fs::Metadata> {
5438 // c:3656
5439 if let Ok(m) = fs::metadata(filename) {
5440 return Some(m);
5441 }
5442 let old = format!("{}.old", filename);
5443 fs::metadata(&old).ok()
5444}
5445
5446/// Port of `load_dump_file()` from `Src/parse.c:3675` — C decl `load_dump_file(char *dump, struct stat *sbuf, int other, int len)`.
5447/// Reads (or mmap()'s) a complete `.zwc`
5448/// file into memory. Returns the u32 buffer or None on I/O error.
5449pub fn load_dump_file(
5450 dump: &str, // c:3675
5451 _sbuf: &fs::Metadata,
5452 other: i32,
5453 _len: usize,
5454) -> Option<Vec<u32>> {
5455 let mut f = File::open(dump).ok()?;
5456 if other != 0 {
5457 f.seek(SeekFrom::Start(other as u64)).ok()?;
5458 }
5459 let mut bytes = Vec::new();
5460 f.read_to_end(&mut bytes).ok()?;
5461 Some(
5462 bytes
5463 .chunks_exact(4)
5464 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
5465 .collect(),
5466 )
5467}
5468
5469/// Port of `try_dump_file()` from `Src/parse.c:3746` — C decl `try_dump_file(char *path, char *name, char *file, int *ksh, int test_only)`.
5470/// Tries to load function `name` from a
5471/// `.zwc` digest (`<path>.zwc`) or per-function compiled file
5472/// (`<file>.zwc`) when each is newer than its uncompiled source.
5473pub fn try_dump_file(
5474 path: &str,
5475 name: &str,
5476 file: &str, // c:3746
5477 test_only: bool,
5478) -> Option<(eprog, i32)> {
5479 use std::fs;
5480
5481 // c:3753-3758 — if path ends in .zwc, treat as direct digest.
5482 if path.ends_with(FD_EXT) {
5483 crate::ported::signals::queue_signals();
5484 let result = fs::metadata(path)
5485 .ok()
5486 .and_then(|m| check_dump_file(path, &m, name, test_only));
5487 unqueue_signals();
5488 return result;
5489 }
5490
5491 // c:3759-3760 — dig = "<path>.zwc", wc = "<file>.zwc".
5492 let dig = format!("{}{}", path, FD_EXT);
5493 let wc = format!("{}{}", file, FD_EXT);
5494
5495 // c:3762-3764 — zwcstat(dig, &std); stat(wc, &stc); stat(file, &stn);
5496 let std_meta = fs::metadata(&dig);
5497 let stc_meta = fs::metadata(&wc);
5498 let stn_meta = fs::metadata(file);
5499
5500 crate::ported::signals::queue_signals();
5501
5502 // zsh compares `st_mtime` (time_t SECONDS) with `>=` (c:3772-3780). Use
5503 // second-granularity mtime (MetadataExt::mtime, imported at top) — NOT
5504 // SystemTime (`.modified()`), whose nanosecond precision disagrees with zsh
5505 // whenever the dump and source share a second but differ in nsec, making
5506 // autoload .zwc preference flaky.
5507
5508 // c:3771-3777 — use the digest when it is >= both the per-fn .zwc and the
5509 // source (or those are absent).
5510 if let Ok(std_m) = &std_meta {
5511 let dig_s = std_m.mtime();
5512 let dig_ge_wc = stc_meta.as_ref().map_or(true, |c| dig_s >= c.mtime()); // c:3772
5513 let dig_ge_src = stn_meta.as_ref().map_or(true, |n| dig_s >= n.mtime()); // c:3773
5514 if dig_ge_wc && dig_ge_src {
5515 if let Some(prog) = check_dump_file(&dig, std_m, name, test_only) {
5516 unqueue_signals();
5517 return Some(prog);
5518 }
5519 }
5520 }
5521
5522 // c:3779-3784 — try per-function .zwc when it is >= the source (or absent).
5523 if let Ok(stc_m) = &stc_meta {
5524 let wc_s = stc_m.mtime();
5525 let src_newer_or_missing = stn_meta.as_ref().map_or(true, |n| wc_s >= n.mtime()); // c:3780
5526 if src_newer_or_missing {
5527 if let Some(prog) = check_dump_file(&wc, stc_m, name, test_only) {
5528 unqueue_signals();
5529 return Some(prog);
5530 }
5531 }
5532 }
5533
5534 unqueue_signals(); // c:3787
5535 None // c:3788
5536}
5537
5538/// Port of `try_source_file()` from `Src/parse.c:3795` — C signature `try_source_file(char *file)`.
5539/// Returns an Eprog (the wordcode dump body) if `<file>.zwc` exists
5540/// and is newer than `<file>`, else None. The dump entry searched is
5541/// the file's basename (`tail`), matching how `zcompile` names
5542/// source-file entries.
5543pub fn try_source_file(file: &str) -> Option<eprog> {
5544 // c:3795
5545
5546 // c:3802-3805 — if ((tail = strrchr(file, '/'))) tail++; else tail = file;
5547 let tail = match file.rfind('/') {
5548 Some(i) => &file[i + 1..],
5549 None => file,
5550 };
5551
5552 // c:3807-3812 — if (strsfx(FD_EXT, file)) { ... return check_dump_file(file, NULL, tail, NULL, 0); }
5553 if file.ends_with(FD_EXT) {
5554 crate::ported::signals::queue_signals(); // c:3808
5555 let meta = fs::metadata(file);
5556 let prog = match meta {
5557 Ok(m) => check_dump_file(file, &m, tail, false).map(|(p, _)| p), // c:3809
5558 Err(_) => None,
5559 };
5560 unqueue_signals(); // c:3810
5561 return prog;
5562 }
5563
5564 // c:3813 — wc = dyncat(file, FD_EXT);
5565 let wc = format!("{}{}", file, FD_EXT);
5566
5567 // c:3815-3816 — rc = stat(wc, &stc); rn = stat(file, &stn);
5568 let stc = fs::metadata(&wc);
5569 let stn = fs::metadata(file);
5570
5571 crate::ported::signals::queue_signals(); // c:3818
5572 // c:3819-3823 — if (!rc && (rn || stc.st_mtime >= stn.st_mtime) && (prog = check_dump_file(...))) return prog;
5573 if let Ok(meta_c) = &stc {
5574 // c:3819 — `stc.st_mtime >= stn.st_mtime`: second-granularity (time_t,
5575 // via MetadataExt::mtime imported at top), not SystemTime nsec, to agree
5576 // with zsh on equal-second boundaries.
5577 let newer_than_src = match (&stc, &stn) {
5578 (Ok(c), Ok(n)) => c.mtime() >= n.mtime(),
5579 (Ok(_), Err(_)) => true, // c:3819 — `rn` (src missing) ⇒ accept .zwc
5580 _ => false,
5581 };
5582 if newer_than_src {
5583 let prog = check_dump_file(&wc, meta_c, tail, false); // c:3820
5584 if let Some((p, _)) = prog {
5585 unqueue_signals(); // c:3821
5586 return Some(p); // c:3822
5587 }
5588 }
5589 }
5590 unqueue_signals(); // c:3824
5591 None // c:3825
5592}
5593
5594/// Port of `check_dump_file()` from `Src/parse.c:3833` — C decl `check_dump_file(char *file, struct stat *sbuf, char *name, int *ksh, int test_only)`.
5595/// Walks the `dumps` mmap list looking for `(dev, ino)` matching
5596/// `sbuf`; on miss, calls `load_dump_header` to read the .zwc
5597/// header. Then `dump_find_func(d, name)` locates the function
5598/// table entry. Returns the wordcode slice + ksh-load flag.
5599///
5600/// ```c
5601/// Eprog
5602/// check_dump_file(char *file, struct stat *sbuf, char *name,
5603/// int *ksh, int test_only)
5604/// {
5605/// int isrec = 0;
5606/// Wordcode d;
5607/// FDHead h;
5608/// FuncDump f;
5609/// struct stat lsbuf;
5610/// if (!sbuf) {
5611/// if (zwcstat(file, &lsbuf)) return NULL;
5612/// sbuf = &lsbuf;
5613/// }
5614/// rec:
5615/// d = NULL;
5616/// for (f = dumps; f; f = f->next)
5617/// if (f->dev == sbuf->st_dev && f->ino == sbuf->st_ino)
5618/// { d = f->map; break; }
5619/// if (!f && (isrec || !(d = load_dump_header(NULL, file, 0))))
5620/// return NULL;
5621/// if ((h = dump_find_func(d, name))) {
5622/// if (test_only) return &dummy_eprog;
5623/// /* allocate Eprog from f->map at h offset, incrdumpcount,
5624/// return prog */
5625/// }
5626/// return NULL;
5627/// }
5628/// ```
5629/// Rust port returns `Option<(eprog, i32)>` instead of the C
5630/// `Eprog` pointer + `*ksh` out-param: tuple element 0 is the
5631/// loaded program (wordcode + string table per the fdhead record),
5632/// element 1 is the ksh-load mode exactly as C writes `*ksh`:
5633/// `FDHF_KSHLOAD → 2`, `FDHF_ZSHLOAD → 0`, neither → `1`
5634/// (c:3954-3956).
5635pub fn check_dump_file(
5636 // c:3833
5637 file: &str,
5638 sbuf: &fs::Metadata,
5639 name: &str,
5640 test_only: bool,
5641) -> Option<(eprog, i32)> {
5642 use std::os::unix::fs::MetadataExt;
5643
5644 // c:3842-3846 — `if (!sbuf) { zwcstat(file, &lsbuf); sbuf = &lsbuf; }`
5645 // Rust takes sbuf by &Metadata — never null.
5646 let dev = sbuf.dev(); // c:3859
5647 let ino = sbuf.ino(); // c:3859
5648
5649 // c:3854 — `d = NULL;`
5650 let mut d: Option<Vec<u32>> = None;
5651 let mut found_mmap = false; // c:3858 `for (f = dumps; f; ...)`
5652
5653 // c:3858-3862 — walk DUMPS for matching dev/ino.
5654 {
5655 let dumps_guard = DUMPS.lock().expect("dumps poisoned");
5656 for f in dumps_guard.iter() {
5657 // c:3858
5658 if f.dev == dev && f.ino == ino {
5659 // c:3859
5660 d = Some(f.map.clone()); // c:3860
5661 found_mmap = true;
5662 break; // c:3861
5663 }
5664 }
5665 }
5666
5667 // c:3870-3871 — `if (!f && (isrec || !(d = load_dump_header(NULL, file, 0)))) return NULL;`
5668 if !found_mmap {
5669 // c:3870
5670 match load_dump_header("", file, 0) {
5671 // c:3870 load_dump_header
5672 Some(loaded) => d = Some(loaded),
5673 None => return None, // c:3871
5674 }
5675 }
5676
5677 // c:3873 — `if ((h = dump_find_func(d, name)))`
5678 let dump = d?;
5679 let h = dump_find_func(&dump, name)?; // c:3873
5680
5681 // c:3876-3879 — `if (test_only) return &dummy_eprog;`
5682 if test_only {
5683 // c:3876
5684 return Some((eprog::default(), 0)); // c:3879 dummy
5685 }
5686
5687 // c:3889-3892 / c:3929-3932 — `if (h->npats > h->len / sizeof(wordcode)) {
5688 // zwarn("%s: invalid description: %s", file, name); return NULL; }`: a
5689 // header claiming more patterns than its body has words is corrupt, and
5690 // the function is refused rather than run.
5691 if h.npats > h.len / 4 {
5692 crate::ported::utils::zwarn(&format!("{}: invalid description: {}", file, name));
5693 return None;
5694 }
5695
5696 // c:3954-3956 — `*ksh = ((fdhflags(h) & FDHF_KSHLOAD) ? 2 :
5697 // ((fdhflags(h) & FDHF_ZSHLOAD) ? 0 : 1));`
5698 let ksh = if (fdhflags(&h) & FDHF_KSHLOAD) != 0 {
5699 2
5700 } else if (fdhflags(&h) & FDHF_ZSHLOAD) != 0 {
5701 0
5702 } else {
5703 1
5704 };
5705
5706 // c:3919-3958 — the read (non-mmap) branch: open the file, seek
5707 // to the function's wordcode, read `h->len` bytes (wordcode +
5708 // string pool), and build an EF_REAL Eprog around it. zshrs has
5709 // no mmap'd EF_MAP variant — DUMPS entries store the file words
5710 // in `map`, so both branches funnel into the same read-and-copy.
5711 //
5712 // if ((fd = open(file, O_RDONLY)) < 0 ||
5713 // lseek(fd, ((h->start * sizeof(wordcode)) +
5714 // ((fdflags(d) & FDF_OTHER) ? fdother(d) : 0)), 0) < 0)
5715 // return NULL;
5716 // d = (Wordcode) zalloc(h->len + po);
5717 // if (read(fd, ((char *) d) + po, h->len) != (int)h->len) return NULL;
5718 // prog->flags = EF_REAL;
5719 // prog->len = h->len + po;
5720 // prog->npats = np = h->npats;
5721 // prog->prog = (Wordcode) (((char *) d) + po);
5722 // prog->strs = ((char *) prog->prog) + h->strs;
5723 let body_off = (h.start as u64) * 4
5724 + if (fdflags(&dump) & FDF_OTHER) != 0 {
5725 fdother(&dump) as u64 // c:3924
5726 } else {
5727 0
5728 };
5729 let mut f = File::open(file).ok()?; // c:3922
5730 f.seek(SeekFrom::Start(body_off)).ok()?; // c:3923
5731 let mut bytes = vec![0u8; h.len as usize];
5732 if f.read_exact(&mut bytes).is_err() {
5733 // c:3931 `read(...) != h->len`
5734 return None;
5735 }
5736 // `h->strs` is the byte offset of the string pool inside the read
5737 // region; everything before it is wordcode.
5738 let strs_off = (h.strs as usize).min(bytes.len());
5739 let prog_words: Vec<u32> = bytes[..strs_off]
5740 .chunks_exact(4)
5741 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
5742 .collect();
5743 // SAFETY: same byte-not-char convention as `bld_eprog` (c:566) —
5744 // consumers index `strs` by byte offset and never require UTF-8.
5745 let strs_string = unsafe { String::from_utf8_unchecked(bytes[strs_off..].to_vec()) };
5746 let po = h.npats as usize * size_of::<*const u8>(); // c:3920
5747 let prog = eprog {
5748 flags: EF_REAL, // c:3941
5749 len: (h.len as usize + po) as i32, // c:3942
5750 npats: h.npats as i32, // c:3943
5751 nref: 1, // c:3944
5752 pats: Vec::new(), // c:3945/3952 dummy_patprog1 fill
5753 prog: prog_words, // c:3946
5754 strs: Some(strs_string), // c:3947
5755 shf: None, // c:3948
5756 dump: None, // c:3949
5757 strs_metafied: true, // .zwc pool read verbatim — still METAFIED (see eprog field doc)
5758 };
5759
5760 // c:3899 — incrdumpcount(f) on the mmap-cache hit path.
5761 if found_mmap {
5762 let dumps_guard = DUMPS.lock().expect("dumps poisoned");
5763 if let Some(f) = dumps_guard.iter().find(|f| f.dev == dev && f.ino == ino) {
5764 incrdumpcount(f); // c:3899
5765 }
5766 }
5767
5768 Some((prog, ksh)) // c:3958
5769}
5770
5771/// Port of `incrdumpcount()` from `Src/parse.c:3970` — C decl `incrdumpcount(FuncDump f)`. The `#else` (non-USE_MMAP) twin is at c:4021.
5772/// `f->count++;` — refcount-up a loaded dump entry. The Rust port
5773/// keys lookup by `filename` because Rust can't raw-pointer-compare
5774/// funcdump values inside a `Mutex<Vec<...>>`; same observable
5775/// effect (the count of the matching entry increments).
5776pub fn incrdumpcount(f: &funcdump) {
5777 // c:3970 — `f->count++;`
5778 if let Some(d) = DUMPS
5779 .lock()
5780 .unwrap()
5781 .iter_mut()
5782 .find(|d| d.filename.as_deref() == f.filename.as_deref())
5783 {
5784 d.count += 1; // c:3973
5785 }
5786}
5787
5788/// Port of `freedump()` from `Src/parse.c:3976` — C signature `freedump(FuncDump f)`. Public
5789/// helper for the rare external caller; locks the dumps mutex and
5790/// drops the entry with the given filename.
5791pub fn freedump(f: &funcdump) {
5792 // c:3976
5793 let mut g = DUMPS.lock().unwrap();
5794 if let Some(name) = f.filename.as_deref() {
5795 freedump_locked(&mut g, name);
5796 }
5797}
5798
5799/// Port of `decrdumpcount()` from `Src/parse.c:3988` — C decl `decrdumpcount(FuncDump f)`. The `#else` (non-USE_MMAP) twin is at c:4026.
5800/// `f->count--; if (!f->count) { unlink from dumps; freedump(f); }`.
5801pub fn decrdumpcount(f: &funcdump) {
5802 // c:3988
5803 let key = f.filename.clone();
5804 let mut g = DUMPS.lock().unwrap();
5805 let mut hit_zero: Option<String> = None;
5806 for d in g.iter_mut() {
5807 if d.filename == key {
5808 d.count -= 1; // c:3991
5809 if d.count == 0 {
5810 // c:3992
5811 hit_zero = d.filename.clone();
5812 }
5813 break;
5814 }
5815 }
5816 if let Some(name) = hit_zero {
5817 // c:3994-4001
5818 freedump_locked(&mut g, &name);
5819 }
5820}
5821
5822/// Port of `closedumps()` from `Src/parse.c:4008` — C decl `closedumps(void)`. The `#else` (non-USE_MMAP) twin is at c:4033.
5823/// Walks
5824/// `dumps` freeing every entry. Called on shell exit (exec.c:522).
5825pub fn closedumps() {
5826 // c:4008
5827 let mut g = DUMPS.lock().unwrap();
5828 g.clear(); // c:4011-4014 `while (dumps) { ... freedump(...); ... }`
5829}
5830
5831/// Port of `dump_autoload()` from `Src/parse.c:4042` — C decl `dump_autoload(char *nam, char *file, int on, Options ops, int func)`.
5832/// Registers every function in a `.zwc`
5833/// for autoload via `shfunctab`.
5834pub fn dump_autoload(
5835 nam: &str,
5836 file: &str, // c:4042
5837 on: i32,
5838 ops: &crate::ported::zsh_h::options,
5839 func: i32,
5840) -> i32 {
5841 use crate::ported::zsh_h::shfunc;
5842 let mut ret = 0; // c:4047
5843
5844 // c:4049-4050 — if (!strsfx(FD_EXT, file)) file = dyncat(file, FD_EXT);
5845 let file_owned;
5846 let file = if !file.ends_with(FD_EXT) {
5847 file_owned = format!("{}{}", file, FD_EXT);
5848 file_owned.as_str()
5849 } else {
5850 file
5851 };
5852
5853 // c:4052-4053 — if (!(h = load_dump_header(nam, file, 1))) return 1;
5854 let h = match load_dump_header(nam, file, 1) {
5855 Some(buf) => buf,
5856 None => return 1,
5857 };
5858
5859 // c:4055-4056 — for (n = firstfdhead(h); n < e; n = nextfdhead(n))
5860 let hlen = fdheaderlen(&h) as usize; // c:4055
5861 let mut n_off = firstfdhead_offset();
5862 while n_off < hlen {
5863 let head = match read_fdhead(&h, n_off) {
5864 Some(hd) => hd,
5865 None => break,
5866 };
5867 // c:4057-4061 — shf = zshcalloc; shf->node.flags = on; ...addnode(fdname + fdhtail)
5868 let name_full = fdname(&h, n_off);
5869 let tail = fdhtail(&head) as usize;
5870 let basename: String = name_full.chars().skip(tail).collect();
5871 let mut shf = shfunc {
5872 node: crate::ported::zsh_h::hashnode {
5873 next: None,
5874 nam: basename.clone(),
5875 flags: on, // c:4058
5876 },
5877 filename: None,
5878 lineno: 0,
5879 funcdef: None,
5880 redir: None,
5881 sticky: None, // c:4060 NULL
5882 body: None,
5883 redir_text: None,
5884 };
5885 // c:4059 — shf->funcdef = mkautofn(shf); (placeholder Eprog ptr)
5886 let _ = crate::ported::builtin::mkautofn(&mut shf as *mut _);
5887 // c:4061 — shfunctab->addnode(...)
5888 let snapshot = shf.clone();
5889 {
5890 let mut tab = crate::ported::hashtable::shfunctab_lock()
5891 .write()
5892 .expect("shfunctab poisoned");
5893 tab.add(shf);
5894 }
5895 // c:4062-4063 — if (OPT_ISSET(ops,'X') && eval_autoload(...)) ret = 1;
5896 if OPT_ISSET(ops, b'X') {
5897 let mut shf_ref = snapshot;
5898 if crate::ported::builtin::eval_autoload(&mut shf_ref as *mut _, &basename, ops, func)
5899 != 0
5900 {
5901 ret = 1;
5902 }
5903 }
5904 n_off = nextfdhead_offset(&h, n_off);
5905 }
5906 let _ = nam;
5907 ret // c:4065
5908}
5909
5910/// Port of C `struct eccstr` (zsh.h:836) — the long-string dedup BST
5911/// node. The dedup-walk and cmp logic in `ecstrcode` is faithful to
5912/// parse.c:447-453 including the conditional cmp chain
5913/// (nfunc → hashval → strcmp), so corpus inputs where C's eccstr BST walk
5914/// finds-or-misses match get the same outcome on the Rust side.
5915#[derive(Debug, Clone)]
5916struct EccstrNode {
5917 left: Option<Box<EccstrNode>>,
5918 right: Option<Box<EccstrNode>>,
5919 /// C-byte form of the string (single byte per char ≤ 0xff).
5920 /// Owned because Rust doesn't have C zsh's "stable pointers into
5921 /// the lexer's tokstr arena" — every tokstr lives as a fresh
5922 /// Rust String allocation.
5923 str: Vec<u8>,
5924 /// Wordcode-encoded offset: `(byte_offset << 2) | token_bit`.
5925 /// Same shape as `Eccstr::offs` (parse.c:459).
5926 offs: u32,
5927 /// Absolute byte offset in the final strs region (= `ecsoffs` at
5928 /// insert time). C `Eccstr::aoffs` (parse.c:464). copy_ecstr uses
5929 /// THIS for the write position — distinct from `offs` which is
5930 /// ecssub-relative and collides across funcdef scopes.
5931 aoffs: u32,
5932 /// `nfunc` snapshot at insert time. Per-function namespace key
5933 /// — top-level scripts use 0; each funcdef bumps it.
5934 nfunc: i32,
5935 /// Hash of `str` computed via zsh's `hasher` (hashtable.c:86).
5936 hashval: u32,
5937}
5938// === end AST relocation ===
5939
5940// Parser state lives in file-scope thread_locals:
5941// - LEX_* (lexer side, matching Src/lex.c file-statics)
5942// - ECBUF / ECLEN / ECUSED / ECNPATS / ECSOFFS / ECSSUB / ECNFUNC /
5943// ECSTRS_INDEX / ECSTRS_REVERSE (wordcode-emission state, matching
5944// Src/parse.c file-statics)
5945//
5946// Callers use the free-fn entry points directly:
5947// crate::ported::parse::parse_init(input);
5948// let prog = crate::ported::parse::parse();
5949
5950const MAX_RECURSION_DEPTH: usize = 500;
5951
5952/// Direct port of `struct parse_stack` at `Src/zsh.h:3099-3109`.
5953/// Used by `parse_context_save` / `parse_context_restore`
5954/// (parse.c:295-355) to snapshot per-parse-call state so a nested
5955/// parse (e.g. inside command substitution) doesn't clobber the
5956/// outer parse.
5957///
5958/// A second port of `struct parse_stack` exists at
5959/// `crate::ported::zsh_h::parse_stack` (zsh.h:1066) using canonical
5960/// Wordcode / Eccstr / `struct heredocs` types. This local version is the
5961/// one `parse_context_save` / `parse_context_restore` use; it holds the
5962/// wordcode-buffer state as the thread-local shapes (`Vec`, maps)
5963/// the emitter keeps.
5964#[allow(non_camel_case_types)]
5965#[derive(Debug, Default, Clone)]
5966pub struct parse_stack {
5967 // ── Direct port of struct parse_stack at zsh.h:3099-3109 ──
5968 /// Pending heredocs awaiting body collection (canonical C
5969 /// linked-list shape). C: `struct heredocs *hdocs` (zsh.h:3100).
5970 /// Mirrors `parse::HDOCS` thread_local across nested parses.
5971 pub hdocs: Option<Box<crate::ported::zsh_h::heredocs>>,
5972 /// !!! WARNING: NOT IN PARSE_STACK — Rust-only AST-glue !!!
5973 /// Snapshot of `lex::LEX_HEREDOCS` (the parallel Rust-only Vec
5974 /// carrying terminator / strip_tabs / quoted metadata).
5975 /// Saved/restored alongside the canonical `hdocs` so nested
5976 /// parses get a clean AST view. C's parse_stack has no analog
5977 /// because C tracks terminator metadata implicitly via tokstr.
5978 pub lex_heredocs: Vec<HereDoc>,
5979 /// C: `int incmdpos` (zsh.h:3102).
5980 pub incmdpos: bool,
5981 /// C: `int aliasspaceflag` (zsh.h:3103).
5982 pub aliasspaceflag: i32,
5983 /// C: `int incond` (zsh.h:3104).
5984 pub incond: i32,
5985 /// C: `int inredir` (zsh.h:3105).
5986 pub inredir: bool,
5987 /// C: `int incasepat` (zsh.h:3106).
5988 pub incasepat: i32,
5989 /// C: `int isnewlin` (zsh.h:3107).
5990 pub isnewlin: i32,
5991 /// C: `int infor` (zsh.h:3108).
5992 pub infor: i32,
5993 /// C: `int inrepeat_` (zsh.h:3109).
5994 pub inrepeat_: i32,
5995 /// C: `int intypeset` (zsh.h:3110).
5996 pub intypeset: bool,
5997 // ── Wordcode-buffer state (zsh.h:3112-3115) ──
5998 // C `int eclen, ecused, ecnpats; Wordcode ecbuf; Eccstr ecstrs;
5999 // int ecsoffs, ecssub, ecnfunc;` — the ECBUF family of thread-locals
6000 // above, saved so a nested parse (skipcomm's `parse_event(OUTPAR)`,
6001 // whose `init_parse` resets them) cannot clobber an enclosing wordcode
6002 // parse such as `zcompile`'s.
6003 pub eclen: i32,
6004 pub ecused: i32,
6005 pub ecnpats: i32,
6006 pub ecbuf: Option<Vec<u32>>,
6007 /// C `Eccstr ecstrs` — the long-string BST (`ECSTRS_TREE`).
6008 ecstrs: Option<Box<EccstrNode>>,
6009 /// !!! WARNING: NOT IN PARSE_STACK — Rust-only !!! The lookup index
6010 /// (`ECSTRS_INDEX`) and reverse map (`ECSTRS_REVERSE`) this port keeps
6011 /// beside the BST; C reaches both through `ecstrs` alone.
6012 ecstrs_index: std::collections::HashMap<(i32, String), u32>,
6013 ecstrs_reverse: std::collections::HashMap<u32, Vec<u8>>,
6014 pub ecsoffs: i32,
6015 pub ecssub: i32,
6016 pub ecnfunc: i32,
6017}
6018
6019// Old uppercase Rust-only `ParseStack` is gone. Compat alias so
6020// existing call sites (context.rs) keep resolving until the
6021// rename ripples through.
6022/// `ParseStack` type alias.
6023#[allow(non_camel_case_types)]
6024pub type ParseStack = parse_stack;
6025
6026/// `mod_export struct eprog dummy_eprog;` from `Src/parse.c:3066`.
6027/// Placeholder Eprog used by `shf->funcdef = &dummy_eprog;` in
6028/// builtin.c when clearing a stale autoload stub. Held in a Mutex
6029/// so `init_eprog` can set it once at shell startup.
6030pub static DUMMY_EPROG: std::sync::Mutex<eprog> = std::sync::Mutex::new(eprog {
6031 flags: 0,
6032 len: 0,
6033 npats: 0,
6034 nref: 0,
6035 prog: Vec::new(),
6036 strs: None,
6037 pats: Vec::new(),
6038 shf: None,
6039 dump: None,
6040 strs_metafied: false, // no pool (strs: None) — flag unused
6041});
6042
6043/// !!! WARNING: RUST-ONLY HELPER !!!
6044///
6045/// No function of this name exists in `Src/parse.c`. This helper stands in
6046/// for the post-parse here-document back-fill. C never needs it: `setheredoc`
6047/// (c:2347) patches the heredoc body straight into `ecbuf` at parse time,
6048/// while the zshrs AST records a `heredoc_idx` that this walk resolves
6049/// afterwards.
6050/// Walk every ZshRedir in the program and, for any with a `heredoc_idx`,
6051/// pull the body+terminator out of `bodies` and stuff into `heredoc`.
6052/// `bodies[i]` corresponds to the i-th heredoc registered by the lexer
6053/// during scanning (in source order).
6054fn fill_heredoc_bodies(prog: &mut ZshProgram, bodies: &[HereDocInfo]) {
6055 for list in &mut prog.lists {
6056 fill_in_sublist(&mut list.sublist, bodies);
6057 }
6058}
6059
6060/// !!! WARNING: RUST-ONLY HELPER !!!
6061///
6062/// No function of this name exists in `Src/parse.c`. This helper stands in
6063/// for one recursion level of the `fill_heredoc_bodies` walk described above
6064/// (sublist level). No C counterpart: C patches `ecbuf` in `setheredoc`,
6065/// c:2347.
6066fn fill_in_sublist(sub: &mut ZshSublist, bodies: &[HereDocInfo]) {
6067 fill_in_pipe(&mut sub.pipe, bodies);
6068 if let Some(next) = &mut sub.next {
6069 fill_in_sublist(&mut next.1, bodies);
6070 }
6071}
6072
6073/// !!! WARNING: RUST-ONLY HELPER !!!
6074///
6075/// No function of this name exists in `Src/parse.c`. This helper stands in
6076/// for one recursion level of the `fill_heredoc_bodies` walk described above
6077/// (pipeline level). No C counterpart: C patches `ecbuf` in `setheredoc`,
6078/// c:2347.
6079fn fill_in_pipe(pipe: &mut ZshPipe, bodies: &[HereDocInfo]) {
6080 fill_in_command(&mut pipe.cmd, bodies);
6081 if let Some(next) = &mut pipe.next {
6082 fill_in_pipe(next, bodies);
6083 }
6084}
6085
6086/// !!! WARNING: RUST-ONLY HELPER !!!
6087///
6088/// No function of this name exists in `Src/parse.c`. This helper stands in
6089/// for one recursion level of the `fill_heredoc_bodies` walk described above
6090/// (command level). No C counterpart: C patches `ecbuf` in `setheredoc`,
6091/// c:2347.
6092fn fill_in_command(cmd: &mut ZshCommand, bodies: &[HereDocInfo]) {
6093 match cmd {
6094 ZshCommand::Simple(s) => {
6095 for r in &mut s.redirs {
6096 if let Some(idx) = r.heredoc_idx {
6097 if let Some(info) = bodies.get(idx) {
6098 r.heredoc = Some(info.clone());
6099 }
6100 }
6101 }
6102 }
6103 ZshCommand::Subsh(p) | ZshCommand::Cursh(p) => fill_heredoc_bodies(p, bodies),
6104 ZshCommand::FuncDef(f) => fill_heredoc_bodies(&mut f.body, bodies),
6105 ZshCommand::If(i) => {
6106 fill_heredoc_bodies(&mut i.cond, bodies);
6107 fill_heredoc_bodies(&mut i.then, bodies);
6108 for (c, b) in &mut i.elif {
6109 fill_heredoc_bodies(c, bodies);
6110 fill_heredoc_bodies(b, bodies);
6111 }
6112 if let Some(e) = &mut i.else_ {
6113 fill_heredoc_bodies(e, bodies);
6114 }
6115 }
6116 ZshCommand::While(w) | ZshCommand::Until(w) => {
6117 fill_heredoc_bodies(&mut w.cond, bodies);
6118 fill_heredoc_bodies(&mut w.body, bodies);
6119 }
6120 ZshCommand::For(f) => fill_heredoc_bodies(&mut f.body, bodies),
6121 ZshCommand::Case(c) => {
6122 for arm in &mut c.arms {
6123 fill_heredoc_bodies(&mut arm.body, bodies);
6124 }
6125 }
6126 ZshCommand::Repeat(r) => fill_heredoc_bodies(&mut r.body, bodies),
6127 ZshCommand::Time(Some(sublist)) => fill_in_sublist(sublist, bodies),
6128 ZshCommand::Try(t) => {
6129 fill_heredoc_bodies(&mut t.try_block, bodies);
6130 fill_heredoc_bodies(&mut t.always, bodies);
6131 }
6132 ZshCommand::Redirected(inner, redirs) => {
6133 for r in redirs {
6134 if let Some(idx) = r.heredoc_idx {
6135 if let Some(info) = bodies.get(idx) {
6136 r.heredoc = Some(info.clone());
6137 }
6138 }
6139 }
6140 fill_in_command(inner, bodies);
6141 }
6142 ZshCommand::Time(None) | ZshCommand::Cond(_) | ZshCommand::Arith(_) => {}
6143 }
6144}
6145
6146/// !!! WARNING: RUST-ONLY HELPER !!!
6147///
6148/// No function of this name exists in `Src/parse.c`. This helper stands in
6149/// for the `name()` shape test that C performs inline in `par_simple` at
6150/// c:2016-2028 (`if (tok == INOUTPAR)`), where the funcdef decision is made
6151/// against the already-emitted wordcode instead of an AST node.
6152/// If `list` is a Simple containing one word that ends in the
6153/// `<Inpar><Outpar>` token pair (the lexer-port encoding of `()`),
6154/// return the bare name. Used by `parse_program_until` to detect
6155/// `name() {body}` style function definitions where the lexer
6156/// hasn't split the `()` from the name.
6157/// Detect the `name() …` shape inside a Simple. Returns the function
6158/// name and (when the body was already inlined into the same Simple,
6159/// e.g. `foo() echo hi`) the rest of the words as the body's argv.
6160/// Returns None for non-funcdef shapes.
6161fn simple_name_with_inoutpar(list: &ZshList) -> Option<(Vec<String>, Vec<String>)> {
6162 if list.flags.async_ || list.sublist.next.is_some() {
6163 return None;
6164 }
6165 let pipe = &list.sublist.pipe;
6166 if pipe.next.is_some() {
6167 return None;
6168 }
6169 let simple = match &pipe.cmd {
6170 ZshCommand::Simple(s) => s,
6171 _ => return None,
6172 };
6173 if simple.words.is_empty() || !simple.assigns.is_empty() {
6174 return None;
6175 }
6176 let suffix = "\u{e188}\u{e18a}"; // Inpar + Outpar
6177 // Find the FIRST word ending in `()`. zsh accepts the
6178 // multi-name shorthand `fna fnb fnc() { body }` (parse.c:
6179 // par_funcdef wordlist) — words[0..i-1] are extra names,
6180 // words[i] is `lastname()`. Words after are the body argv
6181 // (one-line shorthand, `name() cmd args`).
6182 let par_idx = simple.words.iter().position(|w| w.ends_with(suffix))?;
6183 let mut names: Vec<String> = Vec::with_capacity(par_idx + 1);
6184 for w in &simple.words[..par_idx] {
6185 // Earlier names must be bare identifiers, NOT contain
6186 // tokens that imply they're not function names (no `()`,
6187 // no quotes, no expansions). zsh's lexer enforces this
6188 // at the wordlist level; we approximate by requiring the
6189 // word be an identifier-shaped token after untokenize.
6190 let bare = super::lex::untokenize(w);
6191 let valid = !bare.is_empty()
6192 && bare
6193 .chars()
6194 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '$');
6195 if !valid {
6196 return None;
6197 }
6198 names.push(bare);
6199 }
6200 let last = &simple.words[par_idx];
6201 let bare = &last[..last.len() - suffix.len()];
6202 if bare.is_empty() {
6203 return None;
6204 }
6205 // c:Src/lex.c:2164 `skipcomm` — an EMPTY command substitution `$()`
6206 // reaches the parser as `String`/`Qstring` immediately followed by the
6207 // `Inpar`…`Outpar` pair the lexer stamped around the (empty) body, so
6208 // the word's tail is byte-identical to the funcdef `name()` shape. C
6209 // never confuses them because a funcdef arrives as a separate
6210 // `INOUTPAR` token (c:Src/parse.c:2051 par_simple) while `$()`'s
6211 // parens are consumed inside the word. Here the two shapes are told
6212 // apart by what precedes the parens: a `$` marker means command
6213 // substitution, so `print a $() b` is a plain command, not a
6214 // definition of a function named `$`.
6215 if bare.ends_with(crate::ported::zsh_h::Stringg)
6216 || bare.ends_with(crate::ported::zsh_h::Qstring)
6217 || bare.ends_with('$')
6218 {
6219 return None;
6220 }
6221 names.push(super::lex::untokenize(bare));
6222 let rest = simple.words[par_idx + 1..].to_vec();
6223 Some((names, rest))
6224}
6225
6226/// !!! WARNING: RUST-ONLY HELPER !!!
6227///
6228/// No function of this name exists in `Src/parse.c`. This helper stands in
6229/// for a parse-only entry point that seeds the option defaults the lexer
6230/// reads. C gets those from `install_emulation_defaults` (Src/options.c) at
6231/// startup and its parser only READS options; `init_parse` (c:509) and
6232/// `init_parse_status` (c:491) are the real C initialisers and are ported
6233/// separately.
6234/// Initialize parser state for a fresh parse of `input`.
6235/// Free-fn entry point — resets parser thread_locals and loads input.
6236pub fn parse_init(input: &str) {
6237 // Seed the option defaults the parser/lexer inspect. Real zsh
6238 // installs these via `install_emulation_defaults` (options.c:172)
6239 // at shell startup; zshrs's parse-only test entry path bypasses
6240 // init_main, so we mirror the `zsh` emulation defaults here.
6241 //
6242 // Under `cfg(test)` (lib unit tests share one process) we
6243 // unconditionally OVERWRITE these on every parse_init so cross-
6244 // test option pollution (a prior test that flipped one of these
6245 // and panicked before its restore ran) doesn't leak into the
6246 // parser's reserved-word recognition / one-liner detection.
6247 //
6248 // In the REAL shell this overwrite is WRONG: parse_init runs for
6249 // every -c string, eval, and cmd-subst body, so resetting
6250 // `posixbuiltins` here silently wiped `zshrs -o POSIX_BUILTINS`
6251 // and made `setopt posix_builtins` evaporate at the next parse
6252 // (A04redirect.ztst POSIX_BUILTINS chunks). C zsh's parser READS
6253 // options; it never writes them. Production seeds only entries
6254 // that are missing entirely (parse-only test paths that bypass
6255 // init_main still get the zsh-emulation defaults).
6256 let overwrite = cfg!(test);
6257 for (name, default) in [
6258 ("shortloops", true),
6259 ("shortrepeat", false),
6260 ("multifuncdef", true),
6261 ("aliasfuncdef", false),
6262 ("ignorebraces", false),
6263 ("cshjunkieloops", false),
6264 ("posixbuiltins", false),
6265 ("execopt", true),
6266 ("kshautoload", false),
6267 ("aliases", true),
6268 ] {
6269 if overwrite || crate::ported::options::opt_state_get(name).is_none() {
6270 crate::ported::options::opt_state_set(name, default);
6271 }
6272 }
6273 lex_init(input);
6274}
6275
6276/// Port of `ecgetstr` from `Src/parse.c:2855` — C decl `ecgetstr(Estate s, int dup, int *tokflag)`. Rust fn `ecgetstr_wordcode` is the wordcode-pipeline decoder variant; the AST-side `ecgetstr` is at parse.rs:3869.
6277/// P9b decoder (wordcode-pipeline variant): direct port of
6278/// `ecgetstr(Estate s, int dup, int *tokflag)` from
6279/// `Src/parse.c:2855-2890`. Reads a wordcode at `pc`, decodes the
6280/// encoded string back to owned String. Returns (string,
6281/// pc_after_consumed). Distinct from the existing `ecgetstr` (which
6282/// takes a separate strs buffer for text.rs) — this variant uses
6283/// the live ECSTRS_REVERSE HashMap populated at ecstrcode time.
6284pub fn ecgetstr_wordcode(buf: &[u32], pc: usize) -> (String, usize) {
6285 if pc >= buf.len() {
6286 return (String::new(), pc);
6287 }
6288 let c = buf[pc];
6289 let next = pc + 1;
6290 // parse.c:2862-2863 — empty-string sentinels.
6291 if c == 6 || c == 7 {
6292 return (String::new(), next);
6293 }
6294 // parse.c:2864-2871 — inline-packed short string.
6295 if (c & 2) != 0 {
6296 let b0 = ((c >> 3) & 0xff) as u8;
6297 let b1 = ((c >> 11) & 0xff) as u8;
6298 let b2 = ((c >> 19) & 0xff) as u8;
6299 let mut bytes: Vec<u8> = Vec::new();
6300 for b in [b0, b1, b2] {
6301 if b == 0 {
6302 break;
6303 }
6304 bytes.push(b);
6305 }
6306 return (String::from_utf8_lossy(&bytes).into_owned(), next);
6307 }
6308 // parse.c:2872-2873 — long string via offs lookup. Map value is
6309 // metafied Vec<u8>; convert back to display String. Unmetafy is
6310 // the caller's job (the wordcode-parity dumper does it; other
6311 // callers may want raw bytes).
6312 let s = ECSTRS_REVERSE
6313 .with_borrow(|m| m.get(&c).cloned())
6314 .map(|v| String::from_utf8_lossy(&v).into_owned())
6315 .unwrap_or_default();
6316 (s, next)
6317}
6318
6319/// !!! WARNING: RUST-ONLY HELPER !!!
6320///
6321/// No function of this name exists in `Src/parse.c`. This helper stands in
6322/// for the AST-pipeline top-level driver. C's equivalents are `parse_event`
6323/// (c:614) and `par_event` (c:635), both ported separately; this entry loops
6324/// `parse_program_until` instead of emitting wordcode.
6325/// Parse the complete input. Direct port of `parse_event` /
6326/// `par_list` from `Src/parse.c:614-720`. On syntax error,
6327/// sets `errflag |= ERRFLAG_ERROR` (via `zerr`) and returns the
6328/// partial program — callers check `errflag` to detect failure,
6329/// matching C's `Eprog parse_event(...)` + `if (errflag) {...}`.
6330pub fn parse() -> ZshProgram {
6331 zshlex();
6332
6333 let mut program = parse_program_until(None, false);
6334
6335 // Post-pass: wire heredoc bodies (collected by the inline NEWLIN
6336 // walk in zshlex into LEX_HEREDOCS) back into ZshRedir.heredoc
6337 // fields via heredoc_idx. No C analog — LEX_HEREDOCS is the
6338 // Rust-only AST-glue Vec.
6339 let bodies: Vec<HereDocInfo> = LEX_HEREDOCS
6340 .with_borrow(|v| v.clone())
6341 .into_iter()
6342 .map(|h| HereDocInfo {
6343 content: h.content,
6344 terminator: h.terminator,
6345 quoted: h.quoted,
6346 })
6347 .collect();
6348 if !bodies.is_empty() {
6349 fill_heredoc_bodies(&mut program, &bodies);
6350 }
6351
6352 program
6353}
6354
6355/// !!! WARNING: RUST-ONLY HELPER !!!
6356///
6357/// No function of this name exists in `Src/parse.c`. This helper stands in
6358/// for the wordcode-pipeline top-level driver. C's closest analogue is
6359/// `parse_list` (c:697), but that calls `init_parse` + `par_list` +
6360/// `bld_eprog`; this entry only drives `par_list_wordcode`, leaving
6361/// init/build to the caller.
6362/// Wordcode-emission top-level driver. Closest C analog is
6363/// `parse_list(void)` at `Src/parse.c:697-712`: init_parse +
6364/// zshlex + par_list(&c) + bld_eprog. This entry omits init_parse
6365/// and bld_eprog (caller responsibilities) and inlines a guard
6366/// loop around par_list_wordcode for cases where the lexer leaves
6367/// a non-ENDINPUT terminator (LEXERR, missing close-token, etc.).
6368pub fn par_event_wordcode() -> usize {
6369 let start = ECUSED.get() as usize;
6370 // C `parse_list` (parse.c:697-712) calls par_list ONCE — par_list's
6371 // own goto-rec loop handles all SEPER-separated sublists. The
6372 // outer loop here exists for safety against early-return cases
6373 // (LEXERR, missing terminator) but normally par_list_wordcode
6374 // consumes everything in one call.
6375 let mut cmplx: i32 = 0;
6376 while tok() != ENDINPUT && tok() != LEXERR {
6377 par_list_wordcode(&mut cmplx);
6378 match tok() {
6379 SEMI | NEWLIN | AMPER | AMPERBANG | SEPER => {
6380 zshlex();
6381 }
6382 _ => break,
6383 }
6384 }
6385 // No trailing `ecadd(WCB_END())` here: C's `par_list` (c:769)
6386 // emits none — the single terminating `WCB_END` comes from
6387 // `bld_eprog` (c:555). Emitting one here too made every program
6388 // one word longer than C's (double END), breaking .zwc dump
6389 // byte-parity with `zcompile`.
6390 start
6391}
6392
6393/// Port of `par_list` from `Src/parse.c:771` — C decl `par_list(int *cmplx)`. Rust fn `par_list_wordcode` is the wordcode-emitting variant (the AST twin is `par_list` at parse.rs:1008).
6394/// `list : { SEPER } [ sublist [ { SEPER | AMPER | AMPERBANG } list ] ]`.
6395/// True line-by-line port: takes `cmplx: &mut i32` matching C's
6396/// `int *cmplx` out-parameter, uses stack-local `c` per iteration
6397/// like C (so inner sublist cmplx is independent of outer).
6398pub fn par_list_wordcode(cmplx: &mut i32) {
6399 // c:773 — `int p, lp = -1, c;`
6400 let mut p: usize;
6401 let mut lp: i32 = -1;
6402 let mut c: i32;
6403 loop {
6404 // c:775 `rec:` — c:777-778 `while (tok == SEPER) zshlex();`
6405 while tok() == SEPER {
6406 zshlex();
6407 }
6408 // c:780 — `p = ecadd(0);`
6409 p = ecadd(0);
6410 // c:781 — `c = 0;`
6411 c = 0;
6412 // c:783 — `if (par_sublist(&c)) { ... }`
6413 if par_sublist_wordcode(&mut c) {
6414 // c:784 — `*cmplx |= c;`
6415 *cmplx |= c;
6416 // c:785 — `if (tok == SEPER || tok == AMPER || tok == AMPERBANG)`
6417 let t = tok();
6418 if t == SEPER || t == AMPER || t == AMPERBANG {
6419 // c:786-787 — `if (tok != SEPER) *cmplx = 1;`
6420 if t != SEPER {
6421 *cmplx = 1;
6422 }
6423 // c:788-790 — `set_list_code(p, ..., c);`
6424 let z = if t == SEPER {
6425 Z_SYNC
6426 } else if t == AMPER {
6427 Z_ASYNC
6428 } else {
6429 Z_ASYNC | Z_DISOWN
6430 };
6431 set_list_code(p, z, c != 0);
6432 // c:791 — `incmdpos = 1;`
6433 set_incmdpos(true);
6434 // c:792-794 — `do { zshlex(); } while (tok == SEPER);`
6435 loop {
6436 zshlex();
6437 if tok() != SEPER {
6438 break;
6439 }
6440 }
6441 // c:795 — `lp = p;` c:796 — `goto rec;`
6442 lp = p as i32;
6443 continue;
6444 } else {
6445 // c:798 — `set_list_code(p, (Z_SYNC | Z_END), c);`
6446 set_list_code(p, Z_SYNC | Z_END, c != 0);
6447 }
6448 } else {
6449 // c:800-802 — `ecused--; if (lp >= 0) ecbuf[lp] |= wc_bdata(Z_END);`
6450 ECUSED.set((ECUSED.get() - 1).max(0));
6451 if lp >= 0 {
6452 ECBUF.with_borrow_mut(|b| {
6453 if (lp as usize) < b.len() {
6454 b[lp as usize] |= wc_bdata(Z_END as wordcode);
6455 }
6456 });
6457 }
6458 }
6459 break;
6460 }
6461}
6462
6463/// Port of `par_list1` from `Src/parse.c:808` — C decl `par_list1(int *cmplx)`. Rust fn `par_list1_wordcode` is the wordcode-emitting variant.
6464/// Single-sublist variant used by funcdef bodies and the short
6465/// `for`/`while`/`repeat` forms — exactly one sublist with
6466/// `Z_SYNC|Z_END`, no chain.
6467pub fn par_list1_wordcode(cmplx: &mut i32) {
6468 // c:810 — `int p = ecadd(0), c = 0;`
6469 let p = ecadd(0);
6470 let mut c: i32 = 0;
6471 // c:812 — `if (par_sublist(&c)) { ... }`
6472 if par_sublist_wordcode(&mut c) {
6473 // c:813 — `set_list_code(p, (Z_SYNC | Z_END), c);`
6474 set_list_code(p, Z_SYNC | Z_END, c != 0);
6475 // c:814 — `*cmplx |= c;`
6476 *cmplx |= c;
6477 } else {
6478 // c:816 — `ecused--;`
6479 ECUSED.set((ECUSED.get() - 1).max(0));
6480 }
6481}
6482
6483/// Port of `par_save_list` from `Src/parse.c:475` — C decl `#define par_save_list(C) do { int eu = ecused; par_list(C); if (eu == ecused) ecadd(WCB_END()); } while (0)`. Rust fn `par_save_list_wordcode` is the wordcode-emitting variant of that C macro.
6484/// do { int eu = ecused; par_list(C); if (eu == ecused) ecadd(WCB_END()); } while (0)
6485pub fn par_save_list_wordcode(cmplx: &mut i32) {
6486 let eu = ECUSED.get();
6487 par_list_wordcode(cmplx);
6488 if ECUSED.get() == eu {
6489 ecadd(WCB_END());
6490 }
6491}
6492
6493/// Port of `par_save_list1` from `Src/parse.c:481` — C decl `#define par_save_list1(C) do { int eu = ecused; par_list1(C); if (eu == ecused) ecadd(WCB_END()); } while (0)`. Rust fn `par_save_list1_wordcode` is the wordcode-emitting variant of that C macro.
6494pub fn par_save_list1_wordcode(cmplx: &mut i32) {
6495 let eu = ECUSED.get();
6496 par_list1_wordcode(cmplx);
6497 if ECUSED.get() == eu {
6498 ecadd(WCB_END());
6499 }
6500}
6501
6502/// Port of `par_sublist` from `Src/parse.c:825` — C decl `par_sublist(int *cmplx)`. Rust fn `par_sublist_wordcode` is the wordcode-emitting variant.
6503/// `sublist : sublist2 [ ( DBAR | DAMPER ) { SEPER } sublist ]`.
6504/// Emits a WCB_SUBLIST header, recurses into par_sublist2 for
6505/// the !/coproc prefix + pipeline, then chains via DBAR (`||`)
6506/// or DAMPER (`&&`) recursively. Returns true if at least one
6507/// pipeline was emitted.
6508pub fn par_sublist_wordcode(cmplx: &mut i32) -> bool {
6509 // c:827 — `int f, p, c = 0;`
6510 let mut c: i32 = 0;
6511 // c:829 — `p = ecadd(0);`
6512 let p = ecadd(0);
6513 // c:831 — `if ((f = par_sublist2(&c)) != -1) { ... }`
6514 match par_sublist2(&mut c) {
6515 Some(f) => {
6516 // c:832 — `int e = ecused;`
6517 let e = ECUSED.get() as usize;
6518 // c:834 — `*cmplx |= c;`
6519 *cmplx |= c;
6520 if tok() == DBAR || tok() == DAMPER {
6521 // c:836 — `enum lextok qtok = tok;`
6522 let qtok = tok();
6523 // c:839 — `cmdpush(tok == DBAR ? CS_CMDOR : CS_CMDAND);`
6524 cmdpush(if qtok == DBAR {
6525 CS_CMDOR as u8
6526 } else {
6527 CS_CMDAND as u8
6528 });
6529 // c:840 — `zshlex();`
6530 zshlex();
6531 // c:841-842 — `while (tok == SEPER) zshlex();`
6532 while tok() == SEPER {
6533 zshlex();
6534 }
6535 // c:843 — `sl = par_sublist(cmplx);`
6536 let sl = par_sublist_wordcode(cmplx);
6537 // c:844-847 — `set_sublist_code(p, (sl ? ... : WC_SUBLIST_END),
6538 // f, (e - 1 - p), c);`
6539 let st = if sl {
6540 if qtok == DBAR {
6541 WC_SUBLIST_OR
6542 } else {
6543 WC_SUBLIST_AND
6544 }
6545 } else {
6546 WC_SUBLIST_END
6547 };
6548 set_sublist_code(p, st as i32, f, (e - 1 - p) as i32, c != 0);
6549 // c:848 — `cmdpop();`
6550 cmdpop();
6551 } else {
6552 // c:850-853 — `if (tok == AMPER || tok == AMPERBANG)
6553 // { c = 1; *cmplx |= c; }`
6554 if tok() == AMPER || tok() == AMPERBANG {
6555 c = 1;
6556 *cmplx |= c;
6557 }
6558 // c:854 — `set_sublist_code(p, WC_SUBLIST_END, f,
6559 // (e - 1 - p), c);`
6560 set_sublist_code(p, WC_SUBLIST_END as i32, f, (e - 1 - p) as i32, c != 0);
6561 }
6562 // c:856 — `return 1;`
6563 true
6564 }
6565 None => {
6566 // c:858-859 — `ecused--; return 0;`
6567 ECUSED.set((ECUSED.get() - 1).max(0));
6568 false
6569 }
6570 }
6571}
6572
6573/// Port of `par_pline` from `Src/parse.c:894` — C decl `par_pline(int *cmplx)`. Rust fn `par_pipe_wordcode` is the wordcode-emitting variant; named `par_pipe_wordcode` because the AST `par_pline` (parse.rs:1183) already owns the C name.
6574/// `pline : cmd [ ( BAR | BARAMP ) { SEPER } pline ]`. Emits a
6575/// WCB_PIPE header (mid for chain links, end for the last cmd)
6576/// plus the optional BARAMP `2>&1` synthetic redir.
6577/// (Named `par_pipe_wordcode` to disambiguate from the AST
6578/// `par_pline` at parse.rs:1183 — semantically the same `pline`
6579/// production.)
6580pub fn par_pipe_wordcode(cmplx: &mut i32) -> bool {
6581 // c:897 — `zlong line = toklineno;`
6582 let line = toklineno() as i64;
6583 // c:899 — `p = ecadd(0);`
6584 let p = ecadd(0);
6585 // c:901-904 — `if (!par_cmd(cmplx, 0)) { ecused--; return 0; }`
6586 if !par_cmd_wordcode(cmplx, 0) {
6587 ECUSED.set((ECUSED.get() - 1).max(0));
6588 return false;
6589 }
6590 if tok() == BAR_TOK {
6591 // c:906 — `*cmplx = 1;`
6592 *cmplx = 1;
6593 // c:907 — `cmdpush(CS_PIPE);`
6594 cmdpush(CS_PIPE as u8);
6595 // c:908 — `zshlex();`
6596 zshlex();
6597 // c:909-910 — `while (tok == SEPER) zshlex();`
6598 while tok() == SEPER {
6599 zshlex();
6600 }
6601 // c:911 — `ecbuf[p] = WCB_PIPE(WC_PIPE_MID, line>=0 ? line+1 : 0);`
6602 ECBUF.with_borrow_mut(|b| {
6603 if p < b.len() {
6604 b[p] = WCB_PIPE(
6605 WC_PIPE_MID,
6606 if line >= 0 { (line + 1) as wordcode } else { 0 },
6607 );
6608 }
6609 });
6610 // c:912 — `ecispace(p+1, 1);`
6611 ecispace(p + 1, 1);
6612 // c:913 — `ecbuf[p+1] = ecused - 1 - p;`
6613 let used = ECUSED.get() as usize;
6614 ECBUF.with_borrow_mut(|b| {
6615 if p + 1 < b.len() {
6616 b[p + 1] = (used.saturating_sub(1 + p)) as wordcode;
6617 }
6618 });
6619 // c:914-916 — `if (!par_pline(cmplx)) { tok = LEXERR; }`
6620 if !par_pipe_wordcode(cmplx) {
6621 set_tok(LEXERR);
6622 }
6623 // c:917 — `cmdpop();`
6624 cmdpop();
6625 true
6626 } else if tok() == BARAMP {
6627 // c:920-923 — walk past inline WC_REDIR to find r.
6628 let mut r = p + 1;
6629 loop {
6630 let code = ECBUF.with_borrow(|b| b.get(r).copied().unwrap_or(0));
6631 if wc_code(code) != WC_REDIR {
6632 break;
6633 }
6634 r += WC_REDIR_WORDS(code) as usize;
6635 }
6636 // c:925-928 — `ecispace(r, 3);` + synthetic `2>&1` redir
6637 ecispace(r, 3);
6638 ECBUF.with_borrow_mut(|b| {
6639 if r + 2 < b.len() {
6640 b[r] = WCB_REDIR(REDIR_MERGEOUT as wordcode);
6641 b[r + 1] = 2;
6642 b[r + 2] = ecstrcode("1");
6643 }
6644 });
6645 // c:930 — `*cmplx = 1;`
6646 *cmplx = 1;
6647 cmdpush(CS_ERRPIPE as u8);
6648 zshlex();
6649 while tok() == SEPER {
6650 zshlex();
6651 }
6652 ECBUF.with_borrow_mut(|b| {
6653 if p < b.len() {
6654 b[p] = WCB_PIPE(
6655 WC_PIPE_MID,
6656 if line >= 0 { (line + 1) as wordcode } else { 0 },
6657 );
6658 }
6659 });
6660 ecispace(p + 1, 1);
6661 let used = ECUSED.get() as usize;
6662 ECBUF.with_borrow_mut(|b| {
6663 if p + 1 < b.len() {
6664 b[p + 1] = (used.saturating_sub(1 + p)) as wordcode;
6665 }
6666 });
6667 if !par_pipe_wordcode(cmplx) {
6668 set_tok(LEXERR);
6669 }
6670 cmdpop();
6671 true
6672 } else {
6673 // c:944 — `ecbuf[p] = WCB_PIPE(WC_PIPE_END, line>=0 ? line+1 : 0);`
6674 ECBUF.with_borrow_mut(|b| {
6675 if p < b.len() {
6676 b[p] = WCB_PIPE(
6677 WC_PIPE_END,
6678 if line >= 0 { (line + 1) as wordcode } else { 0 },
6679 );
6680 }
6681 });
6682 true
6683 }
6684}
6685
6686/// Port of `par_cmd` from `Src/parse.c:958` — C decl `par_cmd(int *cmplx, int zsh_construct)`. Rust fn `par_cmd_wordcode` is the wordcode-emitting variant.
6687/// Parses leading + trailing redirs and
6688/// dispatches on the current token to the right par_* builder.
6689/// Returns false only when no command was emitted (no redirs +
6690/// par_simple returned 0).
6691pub fn par_cmd_wordcode(cmplx: &mut i32, zsh_construct: i32) -> bool {
6692 // c:960 — `int r, nr = 0;`
6693 let mut nr: i32 = 0;
6694 // c:962 — `r = ecused;`
6695 let mut r: usize = ECUSED.get() as usize;
6696 // c:964-968 — leading redirs.
6697 if IS_REDIROP(tok()) {
6698 // c:965 — `*cmplx = 1;`
6699 *cmplx = 1;
6700 // c:966-968 — `while (IS_REDIROP(tok)) { nr += par_redir(&r, NULL); }`
6701 while IS_REDIROP(tok()) {
6702 nr += par_redir_wordcode(&mut r, None);
6703 }
6704 }
6705 // c:970-1066 — token-dispatch switch.
6706 match tok() {
6707 FOR => {
6708 cmdpush(CS_FOR as u8);
6709 par_for_wordcode(cmplx);
6710 cmdpop();
6711 }
6712 FOREACH => {
6713 cmdpush(CS_FOREACH as u8);
6714 par_for_wordcode(cmplx);
6715 cmdpop();
6716 }
6717 SELECT => {
6718 // c:982 — `*cmplx = 1;`
6719 *cmplx = 1;
6720 cmdpush(CS_SELECT as u8);
6721 par_for_wordcode(cmplx);
6722 cmdpop();
6723 }
6724 CASE => {
6725 cmdpush(CS_CASE as u8);
6726 par_case_wordcode(cmplx);
6727 cmdpop();
6728 }
6729 IF => {
6730 par_if_wordcode(cmplx);
6731 }
6732 WHILE => {
6733 cmdpush(CS_WHILE as u8);
6734 par_while_wordcode(cmplx);
6735 cmdpop();
6736 }
6737 UNTIL => {
6738 cmdpush(CS_UNTIL as u8);
6739 par_while_wordcode(cmplx);
6740 cmdpop();
6741 }
6742 REPEAT => {
6743 cmdpush(CS_REPEAT as u8);
6744 par_repeat_wordcode(cmplx);
6745 cmdpop();
6746 }
6747 INPAR_TOK => {
6748 // c:1011 — `*cmplx = 1;`
6749 *cmplx = 1;
6750 cmdpush(CS_SUBSH as u8);
6751 par_subsh_wordcode(cmplx, zsh_construct);
6752 cmdpop();
6753 }
6754 INBRACE_TOK => {
6755 cmdpush(CS_CURSH as u8);
6756 par_subsh_wordcode(cmplx, zsh_construct);
6757 cmdpop();
6758 }
6759 FUNC => {
6760 cmdpush(CS_FUNCDEF as u8);
6761 par_funcdef_wordcode(cmplx);
6762 cmdpop();
6763 }
6764 DINBRACK => {
6765 cmdpush(CS_COND as u8);
6766 par_cond_wordcode();
6767 cmdpop();
6768 }
6769 DINPAR => {
6770 par_arith_wordcode();
6771 }
6772 TIME => {
6773 // c:1037-1050 — `static int inpartime` guard so
6774 // `time time foo` doesn't recurse infinitely.
6775 if !PARSER_INPARTIME.with(|c| c.get()) {
6776 // c:1041 — `*cmplx = 1;`
6777 *cmplx = 1;
6778 PARSER_INPARTIME.with(|c| c.set(true));
6779 par_time_wordcode();
6780 PARSER_INPARTIME.with(|c| c.set(false));
6781 } else {
6782 set_tok(STRING_LEX);
6783 let sr = par_simple_wordcode(cmplx, nr);
6784 if sr == 0 && nr == 0 {
6785 return false;
6786 }
6787 if sr > 1 {
6788 *cmplx = 1;
6789 r += (sr - 1) as usize;
6790 }
6791 }
6792 }
6793 _ => {
6794 // c:1054 — `if (!(sr = par_simple(cmplx, nr)))`
6795 let sr = par_simple_wordcode(cmplx, nr);
6796 if sr == 0 {
6797 if nr == 0 {
6798 return false;
6799 }
6800 } else if sr > 1 {
6801 // c:1060-1061 — `*cmplx = 1; r += sr - 1;`
6802 *cmplx = 1;
6803 r += (sr - 1) as usize;
6804 }
6805 }
6806 }
6807 // c:1067-1071 — trailing redirs.
6808 // c:1067 — `if (IS_REDIROP(tok)) { *cmplx = 1; while (...) (void)par_redir(&r, NULL); }`
6809 if IS_REDIROP(tok()) {
6810 *cmplx = 1;
6811 while IS_REDIROP(tok()) {
6812 let _ = par_redir_wordcode(&mut r, None);
6813 }
6814 }
6815 // c:1072-1075 — `incmdpos=1; incasepat=0; incond=0; intypeset=0;`
6816 set_incmdpos(true);
6817 set_incasepat(0);
6818 set_incond(0);
6819 set_intypeset(false);
6820 let _ = r;
6821 // c:1076 — `return 1;`
6822 true
6823}
6824
6825/// Port of `par_for` from `Src/parse.c:1087` — C decl `par_for(int *cmplx)`. Rust fn `par_for_wordcode` is the wordcode-emitting variant.
6826pub fn par_for_wordcode(cmplx: &mut i32) {
6827 // c:1089 — `int oecused = ecused, csh = (tok == FOREACH), p, sel = (tok == SELECT);`
6828 let oecused = ECUSED.get() as usize;
6829 let csh = tok() == FOREACH;
6830 let sel = tok() == SELECT;
6831 let p: usize;
6832 // c:1090 — `int type;`
6833 let r#type: wordcode;
6834
6835 // c:1092 — `p = ecadd(0);`
6836 p = ecadd(0);
6837
6838 // c:1094 — `incmdpos = 0;`
6839 set_incmdpos(false);
6840 // c:1095 — `infor = tok == FOR ? 2 : 0;`
6841 set_infor(if tok() == FOR { 2 } else { 0 });
6842 // c:1096 — `zshlex();`
6843 zshlex();
6844 // c:1097 — `if (tok == DINPAR) {`
6845 if tok() == DINPAR {
6846 // c:1098 — `zshlex();`
6847 zshlex();
6848 // c:1099-1100 — `if (tok != DINPAR) YYERRORV(oecused);`
6849 if tok() != DINPAR {
6850 YYERRORV!(oecused);
6851 }
6852 // c:1101 — `ecstr(tokstr);`
6853 ecstr(&tokstr().unwrap_or_default());
6854 // c:1102 — `zshlex();`
6855 zshlex();
6856 // c:1103-1104
6857 if tok() != DINPAR {
6858 YYERRORV!(oecused);
6859 }
6860 // c:1105
6861 ecstr(&tokstr().unwrap_or_default());
6862 // c:1106
6863 zshlex();
6864 // c:1107-1108
6865 if tok() != DOUTPAR {
6866 YYERRORV!(oecused);
6867 }
6868 // c:1109
6869 ecstr(&tokstr().unwrap_or_default());
6870 // c:1110 — `infor = 0;`
6871 set_infor(0);
6872 // c:1111 — `incmdpos = 1;`
6873 set_incmdpos(true);
6874 // c:1112 — `zshlex();`
6875 zshlex();
6876 // c:1113 — `type = WC_FOR_COND;`
6877 r#type = WC_FOR_COND;
6878 } else {
6879 // c:1115 — `int np = 0, n, posix_in, ona = noaliases, onc = nocorrect;`
6880 let mut np: usize = 0;
6881 let mut n: u32;
6882 let posix_in: bool;
6883 let ona = noaliases();
6884 let onc = nocorrect();
6885 // c:1116 — `infor = 0;`
6886 set_infor(0);
6887 // c:1117-1118 — `if (tok != STRING || !isident(tokstr)) YYERRORV(oecused);`
6888 if tok() != STRING_LEX || !crate::ported::params::isident(&tokstr().unwrap_or_default()) {
6889 YYERRORV!(oecused);
6890 }
6891 // c:1119-1120 — `if (!sel) np = ecadd(0);`
6892 if !sel {
6893 np = ecadd(0);
6894 }
6895 // c:1121 — `n = 0;`
6896 n = 0;
6897 // c:1122 — `incmdpos = 1;`
6898 set_incmdpos(true);
6899 // c:1123 — `noaliases = nocorrect = 1;`
6900 set_noaliases(true);
6901 set_nocorrect(1);
6902 // c:1124 — `for (;;) {`
6903 loop {
6904 // c:1125 — `n++;`
6905 n += 1;
6906 // c:1126 — `ecstr(tokstr);`
6907 ecstr(&tokstr().unwrap_or_default());
6908 // c:1127 — `zshlex();`
6909 zshlex();
6910 // c:1128-1129 — `if (tok != STRING || !strcmp(tokstr, "in") || sel) break;`
6911 if tok() != STRING_LEX || tokstr().as_deref() == Some("in") || sel {
6912 break;
6913 }
6914 // c:1130-1135 — `if (!isident(tokstr) || errflag) { ... YYERRORV; }`
6915 if !crate::ported::params::isident(&tokstr().unwrap_or_default())
6916 || (errflag.load(Ordering::Relaxed) & 1) != 0
6917 {
6918 set_noaliases(ona);
6919 set_nocorrect(onc);
6920 YYERRORV!(oecused);
6921 }
6922 }
6923 // c:1137-1138 — `noaliases = ona; nocorrect = onc;`
6924 set_noaliases(ona);
6925 set_nocorrect(onc);
6926 // c:1139-1140 — `if (!sel) ecbuf[np] = n;`
6927 if !sel {
6928 ECBUF.with_borrow_mut(|b| {
6929 b[np] = n;
6930 });
6931 }
6932 // c:1141 — `posix_in = isnewlin;`
6933 posix_in = isnewlin() != 0;
6934 // c:1142-1143 — `while (isnewlin) zshlex();`
6935 while isnewlin() != 0 {
6936 zshlex();
6937 }
6938 // c:1144 — `if (tok == STRING && !strcmp(tokstr, "in")) {`
6939 if tok() == STRING_LEX && tokstr().as_deref() == Some("in") {
6940 // c:1145 — `incmdpos = 0;`
6941 set_incmdpos(false);
6942 // c:1146 — `zshlex();`
6943 zshlex();
6944 // c:1147 — `np = ecadd(0);`
6945 np = ecadd(0);
6946 // c:1148 — `n = par_wordlist();`
6947 let n2 = par_wordlist_wordcode();
6948 // c:1149-1150 — `if (tok != SEPER) YYERRORV(oecused);`
6949 if tok() != SEPER {
6950 YYERRORV!(oecused);
6951 }
6952 // c:1151 — `ecbuf[np] = n;`
6953 ECBUF.with_borrow_mut(|b| {
6954 b[np] = n2 as wordcode;
6955 });
6956 // c:1152 — `type = (sel ? WC_SELECT_LIST : WC_FOR_LIST);`
6957 r#type = if sel { WC_SELECT_LIST } else { WC_FOR_LIST };
6958 } else if !posix_in && tok() == INPAR_TOK {
6959 // c:1153-1154 — `else if (!posix_in && tok == INPAR)`
6960 // c:1154 — `incmdpos = 0;`
6961 set_incmdpos(false);
6962 // c:1155 — `zshlex();`
6963 zshlex();
6964 // c:1156 — `np = ecadd(0);`
6965 np = ecadd(0);
6966 // c:1157 — `n = par_nl_wordlist();`
6967 let n2 = par_nl_wordlist_wordcode();
6968 // c:1158-1159 — `if (tok != OUTPAR) YYERRORV(oecused);`
6969 if tok() != OUTPAR_TOK {
6970 YYERRORV!(oecused);
6971 }
6972 // c:1160 — `ecbuf[np] = n;`
6973 ECBUF.with_borrow_mut(|b| {
6974 b[np] = n2 as wordcode;
6975 });
6976 // c:1161 — `incmdpos = 1;`
6977 set_incmdpos(true);
6978 // c:1162 — `zshlex();`
6979 zshlex();
6980 // c:1163 — `type = (sel ? WC_SELECT_LIST : WC_FOR_LIST);`
6981 r#type = if sel { WC_SELECT_LIST } else { WC_FOR_LIST };
6982 } else {
6983 // c:1165 — `type = (sel ? WC_SELECT_PPARAM : WC_FOR_PPARAM);`
6984 r#type = if sel { WC_SELECT_PPARAM } else { WC_FOR_PPARAM };
6985 }
6986 let _ = np;
6987 }
6988 // c:1167 — `incmdpos = 1;`
6989 set_incmdpos(true);
6990 // c:1168-1169 — `while (tok == SEPER) zshlex();`
6991 while tok() == SEPER {
6992 zshlex();
6993 }
6994 // c:1170-1193 — body dispatch (inline in C, factored here for
6995 // reuse by par_while/par_repeat — same control flow, same calls).
6996 par_loop_body_wordcode(cmplx, csh, oecused);
6997 // c:1195-1197 — `ecbuf[p] = (sel ? WCB_SELECT(...) : WCB_FOR(...));`
6998 let used = ECUSED.get() as usize;
6999 let off = used.saturating_sub(1 + p) as wordcode;
7000 ECBUF.with_borrow_mut(|b| {
7001 b[p] = if sel {
7002 WCB_SELECT(r#type, off)
7003 } else {
7004 WCB_FOR(r#type, off)
7005 };
7006 });
7007}
7008
7009/// Port of `par_wordlist` from `Src/parse.c:2362` — C decl `par_wordlist(void)`. Rust fn `par_wordlist_wordcode` is the wordcode-emitting variant.
7010/// emits wordcode form. Returns the number of strings emitted.
7011fn par_wordlist_wordcode() -> u32 {
7012 // c:2364 — `int num = 0;`
7013 let mut num: u32 = 0;
7014 // c:2365 — `while (tok == STRING) {`
7015 while tok() == STRING_LEX {
7016 // c:2366 — `ecstr(tokstr);`
7017 ecstr(&tokstr().unwrap_or_default());
7018 // c:2367 — `num++;`
7019 num += 1;
7020 // c:2368 — `zshlex();`
7021 zshlex();
7022 }
7023 // c:2370 — `return num;`
7024 num
7025}
7026
7027/// Port of `par_nl_wordlist` from `Src/parse.c:2379` — C decl `par_nl_wordlist(void)`. Rust fn `par_nl_wordlist_wordcode` is the wordcode-emitting variant.
7028/// emits wordcode form. Like par_wordlist but tolerates SEPER
7029/// between words.
7030fn par_nl_wordlist_wordcode() -> u32 {
7031 // c:2381 — `int num = 0;`
7032 let mut num: u32 = 0;
7033 // c:2383 — `while (tok == STRING || tok == SEPER) {`
7034 while tok() == STRING_LEX || tok() == SEPER || tok() == NEWLIN {
7035 // c:2384-2387 — `if (tok != SEPER) { ecstr(tokstr); num++; }`
7036 if tok() == STRING_LEX {
7037 ecstr(&tokstr().unwrap_or_default());
7038 num += 1;
7039 }
7040 // c:2388 — `zshlex();`
7041 zshlex();
7042 }
7043 // c:2390 — `return num;`
7044 num
7045}
7046
7047/// !!! WARNING: RUST-ONLY HELPER !!!
7048///
7049/// No function of this name exists in `Src/parse.c`. This helper stands in
7050/// for the loop-body tail shared by C's `par_for` / `par_while` /
7051/// `par_repeat` (the DOLOOP / INBRACE / short-form ladder at c:1170-1194),
7052/// factored out because all three Rust wordcode emitters need it.
7053/// Body dispatch shared by par_for / par_while / par_repeat.
7054/// Direct port of `Src/parse.c:1170-1194`.
7055fn par_loop_body_wordcode(cmplx: &mut i32, csh: bool, oecused: usize) {
7056 if tok() == DOLOOP {
7057 zshlex();
7058 // c:1172 — `par_save_list(cmplx);`
7059 par_save_list_wordcode(cmplx);
7060 if tok() != DONE {
7061 YYERRORV!(oecused);
7062 }
7063 set_incmdpos(false);
7064 zshlex();
7065 } else if tok() == INBRACE_TOK {
7066 zshlex();
7067 // c:1179 — `par_save_list(cmplx);`
7068 par_save_list_wordcode(cmplx);
7069 if tok() != OUTBRACE_TOK {
7070 YYERRORV!(oecused);
7071 }
7072 set_incmdpos(false);
7073 zshlex();
7074 } else if csh || isset(CSHJUNKIELOOPS) {
7075 // c:1185 — `par_save_list(cmplx);`
7076 par_save_list_wordcode(cmplx);
7077 if tok() != ZEND {
7078 YYERRORV!(oecused);
7079 }
7080 set_incmdpos(false);
7081 zshlex();
7082 } else if unset(SHORTLOOPS) {
7083 YYERRORV!(oecused);
7084 } else {
7085 // c:1193 — `par_save_list1(cmplx);`
7086 par_save_list1_wordcode(cmplx);
7087 }
7088}
7089
7090/// !!! WARNING: RUST-ONLY HELPER !!!
7091///
7092/// No function of this name exists in `Src/parse.c`. This helper stands in
7093/// for C's SELECT routing: `par_cmd` sends both FOR and SELECT to `par_for`
7094/// (c:983-985), so this is a one-line alias onto `par_for_wordcode`.
7095/// `select` shares par_for body (c:983-985 routes SELECT to par_for).
7096pub fn par_select_wordcode(cmplx: &mut i32) {
7097 par_for_wordcode(cmplx);
7098}
7099
7100/// Port of `par_case` from `Src/parse.c:1209` — C decl `par_case(int *cmplx)`. Rust fn `par_case_wordcode` is the wordcode-emitting variant.
7101pub fn par_case_wordcode(_cmplx: &mut i32) {
7102 // c:1211 — `int oecused = ecused, brflag, p, pp, palts, type, nalts;`
7103 let oecused = ECUSED.get() as usize;
7104 let brflag: bool;
7105 let p: usize;
7106 let mut pp: usize;
7107 let mut palts: usize;
7108 let mut r#type: wordcode;
7109 let mut nalts: u32;
7110 // c:1212 — `int ona, onc;`
7111 let ona: bool;
7112 let onc: i32;
7113
7114 // c:1214 — `p = ecadd(0);`
7115 p = ecadd(0);
7116
7117 // c:1216 — `incmdpos = 0;`
7118 set_incmdpos(false);
7119 // c:1217 — `zshlex();`
7120 zshlex();
7121 // c:1218-1219 — `if (tok != STRING) YYERRORV(oecused);`
7122 if tok() != STRING_LEX {
7123 YYERRORV!(oecused);
7124 }
7125 // c:1220 — `ecstr(tokstr);`
7126 ecstr(&tokstr().unwrap_or_default());
7127
7128 // c:1222 — `incmdpos = 1;`
7129 set_incmdpos(true);
7130 // c:1223-1224 — `ona = noaliases; onc = nocorrect;`
7131 ona = noaliases();
7132 onc = nocorrect();
7133 // c:1225 — `noaliases = nocorrect = 1;`
7134 set_noaliases(true);
7135 set_nocorrect(1);
7136 // c:1226 — `zshlex();`
7137 zshlex();
7138 // c:1227-1228 — `while (tok == SEPER) zshlex();`
7139 while tok() == SEPER {
7140 zshlex();
7141 }
7142 // c:1229 — `if (!(tok == STRING && !strcmp(tokstr, "in")) && tok != INBRACE)`
7143 if !(tok() == STRING_LEX && tokstr().as_deref() == Some("in")) && tok() != INBRACE_TOK {
7144 // c:1231-1233 — restore noaliases/nocorrect + ERROR
7145 set_noaliases(ona);
7146 set_nocorrect(onc);
7147 YYERRORV!(oecused);
7148 }
7149 // c:1235 — `brflag = (tok == INBRACE);`
7150 brflag = tok() == INBRACE_TOK;
7151 // c:1236 — `incasepat = 1;`
7152 set_incasepat(1);
7153 // c:1237 — `incmdpos = 0;`
7154 set_incmdpos(false);
7155 // c:1238-1239 — `noaliases = ona; nocorrect = onc;`
7156 set_noaliases(ona);
7157 set_nocorrect(onc);
7158 // c:1240 — `zshlex();`
7159 zshlex();
7160
7161 // c:1242 — `for (;;) {`
7162 'arms: loop {
7163 // c:1243 — `char *str;`
7164 let mut str: String;
7165 // c:1244 — `int skip_zshlex;`
7166 let skip_zshlex: bool;
7167
7168 // c:1246-1247 — `while (tok == SEPER) zshlex();`
7169 while tok() == SEPER {
7170 zshlex();
7171 }
7172 // c:1248-1249 — `if (tok == OUTBRACE) break;`
7173 if tok() == OUTBRACE_TOK {
7174 break 'arms;
7175 }
7176 // c:1250-1251 — `if (tok == INPAR) zshlex();`
7177 if tok() == INPAR_TOK {
7178 zshlex();
7179 }
7180 // c:1252-1254 — `if (tok == BAR) { str = ""; skip_zshlex = 1; }`
7181 if tok() == BAR_TOK {
7182 str = String::new();
7183 skip_zshlex = true;
7184 } else {
7185 // c:1256-1257 — `if (tok != STRING) YYERRORV(oecused);`
7186 if tok() != STRING_LEX {
7187 YYERRORV!(oecused);
7188 }
7189 // c:1258-1259 — `if (!strcmp(tokstr, "esac")) break;`
7190 if tokstr().as_deref() == Some("esac") {
7191 break 'arms;
7192 }
7193 // c:1260 — `str = dupstring(tokstr);`
7194 str = tokstr().unwrap_or_default();
7195 // c:1261 — `skip_zshlex = 0;`
7196 skip_zshlex = false;
7197 }
7198 // c:1263 — `type = WC_CASE_OR;`
7199 r#type = WC_CASE_OR;
7200 // c:1264-1266 — `pp = ecadd(0); palts = ecadd(0); nalts = 0;`
7201 pp = ecadd(0);
7202 palts = ecadd(0);
7203 nalts = 0;
7204 // c:1300 — `incasepat = -1;`
7205 set_incasepat(-1);
7206 // c:1301 — `incmdpos = 1;`
7207 set_incmdpos(true);
7208 // c:1302-1303 — `if (!skip_zshlex) zshlex();`
7209 if !skip_zshlex {
7210 zshlex();
7211 }
7212 // c:1304 — `for (;;) {`
7213 loop {
7214 // c:1305-1313 — `if (tok == OUTPAR) { ecstr(str);
7215 // ecadd(ecnpats++); nalts++; incasepat = 0;
7216 // incmdpos = 1; zshlex(); break; }`
7217 if tok() == OUTPAR_TOK {
7218 ecstr(&str);
7219 let np = ECNPATS.with(|cc| {
7220 let v = cc.get();
7221 cc.set(v + 1);
7222 v
7223 }) as u32;
7224 ecadd(np);
7225 nalts += 1;
7226 set_incasepat(0);
7227 set_incmdpos(true);
7228 zshlex();
7229 break;
7230 }
7231 // c:1314-1320 — `else if (tok == BAR) { ecstr(str);
7232 // ecadd(ecnpats++); nalts++; incasepat = 1;
7233 // incmdpos = 0; }`
7234 else if tok() == BAR_TOK {
7235 ecstr(&str);
7236 let np = ECNPATS.with(|cc| {
7237 let v = cc.get();
7238 cc.set(v + 1);
7239 v
7240 }) as u32;
7241 ecadd(np);
7242 nalts += 1;
7243 set_incasepat(1);
7244 set_incmdpos(false);
7245 }
7246 // c:1321-1357 — else { ... `(...)` whole-pattern hack:
7247 // the lexer absorbed a complete `(...)` as one STRING
7248 // (str[0] == Inpar) and the current tok is already the
7249 // body's first token. Massage blanks around `|`/parens
7250 // at depth 1, validate balance, strip the surrounding
7251 // parens; the remainder IS the pattern. }
7252 else {
7253 use crate::ported::zsh_h::{Bar, Inpar, Outpar};
7254 if nalts == 0 && str.starts_with(Inpar) {
7255 let meta = crate::ported::zsh_h::Meta as char;
7256 let blank = |c: char| c.is_ascii() && crate::ported::ztype_h::iblank(c as u8);
7257 let mut chars: Vec<char> = str.chars().collect();
7258 // c:1323 — `int pct = 0, sl;`
7259 let mut pct = 0i32;
7260 let mut i = 0usize;
7261 // c:1326-1344 — scan/massage loop. `s` ↔ `i`;
7262 // chuck(p) ↔ chars.remove(idx).
7263 let mut early_break = false;
7264 while i < chars.len() {
7265 if chars[i] == Inpar {
7266 pct += 1;
7267 }
7268 // c:1329-1330 — `if (!pct) break;` (char past
7269 // the balanced close → trailing garbage).
7270 if pct == 0 {
7271 early_break = true;
7272 break;
7273 }
7274 if pct == 1 {
7275 // c:1332-1334 — chuck blanks AFTER `|`/`(`.
7276 if chars[i] == Bar || chars[i] == Inpar {
7277 while i + 1 < chars.len() && blank(chars[i + 1]) {
7278 chars.remove(i + 1);
7279 }
7280 }
7281 // c:1335-1338 — chuck blanks BEFORE `|`/`)`
7282 // (not Meta-escaped blanks).
7283 if chars[i] == Bar || chars[i] == Outpar {
7284 while i >= 1
7285 && blank(chars[i - 1])
7286 && (i < 2 || chars[i - 2] != meta)
7287 {
7288 chars.remove(i - 1);
7289 i -= 1;
7290 }
7291 }
7292 }
7293 if chars[i] == Outpar {
7294 pct -= 1;
7295 }
7296 i += 1;
7297 }
7298 // c:1345-1346 — `if (*s || pct || s == str)
7299 // YYERRORV(oecused);`
7300 if early_break || pct != 0 || chars.is_empty() {
7301 YYERRORV!(oecused);
7302 }
7303 // c:1347-1352 — strip surrounding `(...)`.
7304 chars.pop();
7305 chars.remove(0);
7306 let stripped: String = chars.into_iter().collect();
7307 // c:1353-1355 — `ecstr(str); ecadd(ecnpats++); nalts++;`
7308 ecstr(&stripped);
7309 let np = ECNPATS.with(|cc| {
7310 let v = cc.get();
7311 cc.set(v + 1);
7312 v
7313 }) as u32;
7314 ecadd(np);
7315 nalts += 1;
7316 // c:1356 — `break;` — tok is already the body's
7317 // first token; fall through to par_save_list.
7318 break;
7319 }
7320 // c:1358 — `YYERRORV(oecused);`
7321 YYERRORV!(oecused);
7322 }
7323
7324 // c:1359 — `zshlex();`
7325 zshlex();
7326 // c:1360-1377 — switch on next tok.
7327 match tok() {
7328 STRING_LEX => {
7329 // c:1361-1365
7330 str = tokstr().unwrap_or_default();
7331 zshlex();
7332 }
7333 OUTPAR_TOK | BAR_TOK => {
7334 // c:1367-1371 — empty string
7335 str = String::new();
7336 }
7337 _ => {
7338 // c:1374-1376 — `YYERRORV(oecused);`
7339 YYERRORV!(oecused);
7340 }
7341 }
7342 }
7343 // c:1379 — `incasepat = 0;`
7344 set_incasepat(0);
7345 // c:1380 — `par_save_list(cmplx);`
7346 par_save_list_wordcode(_cmplx);
7347 // c:1381-1384 — terminator → arm type
7348 if tok() == SEMIAMP {
7349 r#type = WC_CASE_AND;
7350 } else if tok() == SEMIBAR {
7351 r#type = WC_CASE_TESTAND;
7352 }
7353 // c:1385 — `ecbuf[pp] = WCB_CASE(type, ecused - 1 - pp);`
7354 let used = ECUSED.get() as usize;
7355 ECBUF.with_borrow_mut(|b| {
7356 b[pp] = WCB_CASE(r#type, (used.saturating_sub(1 + pp)) as wordcode);
7357 });
7358 // c:1386 — `ecbuf[palts] = nalts;`
7359 ECBUF.with_borrow_mut(|b| {
7360 b[palts] = nalts;
7361 });
7362 // c:1387-1388 — terminator (ESAC w/o brace OR OUTBRACE w/ brace) → break
7363 if (tok() == ESAC && !brflag) || (tok() == OUTBRACE_TOK && brflag) {
7364 break 'arms;
7365 }
7366 // c:1389-1390 — `if (tok != DSEMI && tok != SEMIAMP && tok != SEMIBAR) YYERRORV;`
7367 if tok() != DSEMI && tok() != SEMIAMP && tok() != SEMIBAR {
7368 YYERRORV!(oecused);
7369 }
7370 // c:1391 — `incasepat = 1;`
7371 set_incasepat(1);
7372 // c:1392 — `incmdpos = 0;`
7373 set_incmdpos(false);
7374 // c:1393 — `zshlex();`
7375 zshlex();
7376 }
7377 // c:1395 — `incmdpos = 1;`
7378 set_incmdpos(true);
7379 // c:1396 — `incasepat = 0;`
7380 set_incasepat(0);
7381 // c:1397 — `zshlex();`
7382 zshlex();
7383
7384 // c:1399 — `ecbuf[p] = WCB_CASE(WC_CASE_HEAD, ecused - 1 - p);`
7385 let used = ECUSED.get() as usize;
7386 ECBUF.with_borrow_mut(|b| {
7387 b[p] = WCB_CASE(WC_CASE_HEAD, (used.saturating_sub(1 + p)) as wordcode);
7388 });
7389}
7390
7391/// Port of `par_if` from `Src/parse.c:1411` — C decl `par_if(int *cmplx)`. Rust fn `par_if_wordcode` is the wordcode-emitting variant.
7392pub fn par_if_wordcode(cmplx: &mut i32) {
7393 // c:1413 — `int oecused = ecused, p, pp, type, usebrace = 0;`
7394 let oecused = ECUSED.get() as usize;
7395 let p: usize;
7396 let mut pp: usize = 0;
7397 let mut r#type: wordcode = WC_IF_IF;
7398 let mut usebrace: i32 = 0;
7399 // c:1414 — `enum lextok xtok;`
7400 let mut xtok: lextok;
7401 // c:1415 — `unsigned char nc;`
7402 let nc: u8;
7403 let _ = nc;
7404
7405 // c:1417 — `p = ecadd(0);`
7406 p = ecadd(0);
7407
7408 // c:1419 — `for (;;) {`
7409 loop {
7410 // c:1420 — `xtok = tok;`
7411 xtok = tok();
7412 // c:1421 — `cmdpush(xtok == IF ? CS_IF : CS_ELIF);`
7413 cmdpush(if xtok == IF {
7414 CS_IF as u8
7415 } else {
7416 CS_ELIF as u8
7417 });
7418 // c:1422-1426 — `if (xtok == FI) { incmdpos = 0; zshlex(); break; }`
7419 if xtok == FI {
7420 set_incmdpos(false);
7421 zshlex();
7422 break;
7423 }
7424 // c:1427 — `zshlex();`
7425 zshlex();
7426 // c:1428-1429 — `if (xtok == ELSE) break;`
7427 if xtok == ELSE {
7428 break;
7429 }
7430 // c:1430-1431 — `while (tok == SEPER) zshlex();`
7431 while tok() == SEPER {
7432 zshlex();
7433 }
7434 // c:1432-1435 — `if (!(xtok == IF || xtok == ELIF)) { cmdpop(); YYERRORV; }`
7435 if !(xtok == IF || xtok == ELIF) {
7436 cmdpop();
7437 YYERRORV!(oecused);
7438 }
7439 // c:1436 — `pp = ecadd(0);`
7440 pp = ecadd(0);
7441 // c:1437 — `type = (xtok == IF ? WC_IF_IF : WC_IF_ELIF);`
7442 r#type = if xtok == IF { WC_IF_IF } else { WC_IF_ELIF };
7443 // c:1438 — `par_save_list(cmplx);` — condition body
7444 par_save_list_wordcode(cmplx);
7445 // c:1439 — `incmdpos = 1;`
7446 set_incmdpos(true);
7447 // c:1440-1443 — `if (tok == ENDINPUT) { cmdpop(); YYERRORV; }`
7448 if tok() == ENDINPUT {
7449 cmdpop();
7450 YYERRORV!(oecused);
7451 }
7452 // c:1444-1445 — `while (tok == SEPER) zshlex();`
7453 while tok() == SEPER {
7454 zshlex();
7455 }
7456 // c:1446 — `xtok = FI;` — pre-set so the post-loop check works
7457 xtok = FI;
7458 // c:1447 — `nc = cmdstack[cmdsp - 1] == CS_IF ? CS_IFTHEN : CS_ELIFTHEN;`
7459 // (Not tracked separately in zshrs cmdstack — derive from cur top
7460 // by reading CMDSTACK; for safety use CS_IFTHEN as default.)
7461 // We don't have a way to read top easily — match by tracking
7462 // whether we just pushed CS_IF or CS_ELIF.
7463 // For wordcode emission this only affects cmdstack debug output;
7464 // not the emitted wordcode. Use CS_IFTHEN.
7465 let nc_local: u8 = CS_IFTHEN as u8;
7466 if tok() == THEN {
7467 // c:1448-1456 — THEN branch
7468 // c:1449 — `usebrace = 0;`
7469 usebrace = 0;
7470 // c:1450 — `cmdpop();`
7471 cmdpop();
7472 // c:1451 — `cmdpush(nc);`
7473 cmdpush(nc_local);
7474 // c:1452 — `zshlex();`
7475 zshlex();
7476 // c:1453 — `par_save_list(cmplx);` — then body
7477 par_save_list_wordcode(cmplx);
7478 // c:1454 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
7479 let used = ECUSED.get() as usize;
7480 ECBUF.with_borrow_mut(|b| {
7481 b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
7482 });
7483 // c:1455 — `incmdpos = 1;`
7484 set_incmdpos(true);
7485 // c:1456 — `cmdpop();`
7486 cmdpop();
7487 } else if tok() == INBRACE_TOK {
7488 // c:1457-1473 — INBRACE branch
7489 // c:1458 — `usebrace = 1;`
7490 usebrace = 1;
7491 // c:1459 — `cmdpop();`
7492 cmdpop();
7493 // c:1460 — `cmdpush(nc);`
7494 cmdpush(nc_local);
7495 // c:1461 — `zshlex();`
7496 zshlex();
7497 // c:1462 — `par_save_list(cmplx);`
7498 par_save_list_wordcode(cmplx);
7499 // c:1463-1466 — `if (tok != OUTBRACE) { cmdpop(); YYERRORV; }`
7500 if tok() != OUTBRACE_TOK {
7501 cmdpop();
7502 YYERRORV!(oecused);
7503 }
7504 // c:1467 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
7505 let used = ECUSED.get() as usize;
7506 ECBUF.with_borrow_mut(|b| {
7507 b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
7508 });
7509 // c:1469 — `zshlex();`
7510 zshlex();
7511 // c:1470 — `incmdpos = 1;`
7512 set_incmdpos(true);
7513 // c:1471-1472 — `if (tok == SEPER) break;`
7514 if tok() == SEPER {
7515 break;
7516 }
7517 // c:1473 — `cmdpop();`
7518 cmdpop();
7519 } else if unset(SHORTLOOPS) {
7520 // c:1474-1476 — `cmdpop(); YYERRORV;`
7521 cmdpop();
7522 YYERRORV!(oecused);
7523 } else {
7524 // c:1477-1484 — short loop form
7525 // c:1478 — `cmdpop();`
7526 cmdpop();
7527 // c:1479 — `cmdpush(nc);`
7528 cmdpush(nc_local);
7529 // c:1480 — `par_save_list1(cmplx);`
7530 par_save_list1_wordcode(cmplx);
7531 // c:1481 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
7532 let used = ECUSED.get() as usize;
7533 ECBUF.with_borrow_mut(|b| {
7534 b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
7535 });
7536 // c:1482 — `incmdpos = 1;`
7537 set_incmdpos(true);
7538 // c:1483 — `break;`
7539 break;
7540 }
7541 }
7542 // c:1486 — `cmdpop();`
7543 cmdpop();
7544 // c:1487 — `if (xtok == ELSE || tok == ELSE) {`
7545 if xtok == ELSE || tok() == ELSE {
7546 // c:1488 — `pp = ecadd(0);`
7547 pp = ecadd(0);
7548 // c:1489 — `cmdpush(CS_ELSE);`
7549 cmdpush(CS_ELSE as u8);
7550 // c:1490-1491 — `while (tok == SEPER) zshlex();`
7551 while tok() == SEPER {
7552 zshlex();
7553 }
7554 // c:1492-1498 — `if (tok == INBRACE && usebrace) { ... } else { ... }`
7555 if tok() == INBRACE_TOK && usebrace != 0 {
7556 // c:1493 — `zshlex();`
7557 zshlex();
7558 // c:1494 — `par_save_list(cmplx);`
7559 par_save_list_wordcode(cmplx);
7560 // c:1495-1498 — `if (tok != OUTBRACE) { cmdpop(); YYERRORV; }`
7561 if tok() != OUTBRACE_TOK {
7562 cmdpop();
7563 YYERRORV!(oecused);
7564 }
7565 } else {
7566 // c:1500 — `par_save_list(cmplx);`
7567 par_save_list_wordcode(cmplx);
7568 // c:1501-1504 — `if (tok != FI) { cmdpop(); YYERRORV; }`
7569 if tok() != FI {
7570 cmdpop();
7571 YYERRORV!(oecused);
7572 }
7573 }
7574 // c:1506 — `incmdpos = 0;`
7575 set_incmdpos(false);
7576 // c:1507 — `ecbuf[pp] = WCB_IF(WC_IF_ELSE, ecused - 1 - pp);`
7577 let used = ECUSED.get() as usize;
7578 ECBUF.with_borrow_mut(|b| {
7579 b[pp] = WCB_IF(WC_IF_ELSE, (used.saturating_sub(1 + pp)) as wordcode);
7580 });
7581 // c:1508 — `zshlex();`
7582 zshlex();
7583 // c:1509 — `cmdpop();`
7584 cmdpop();
7585 }
7586 // c:1511 — `ecbuf[p] = WCB_IF(WC_IF_HEAD, ecused - 1 - p);`
7587 let used = ECUSED.get() as usize;
7588 ECBUF.with_borrow_mut(|b| {
7589 b[p] = WCB_IF(WC_IF_HEAD, (used.saturating_sub(1 + p)) as wordcode);
7590 });
7591}
7592
7593/// Port of `par_while` from `Src/parse.c:1521` — C decl `par_while(int *cmplx)`. Rust fn `par_while_wordcode` is the wordcode-emitting variant.
7594pub fn par_while_wordcode(cmplx: &mut i32) {
7595 // c:1523 — `int oecused = ecused, p;`
7596 let oecused = ECUSED.get() as usize;
7597 let p: usize;
7598 // c:1524 — `int type = (tok == UNTIL ? WC_WHILE_UNTIL : WC_WHILE_WHILE);`
7599 let r#type: wordcode = if tok() == UNTIL {
7600 WC_WHILE_UNTIL
7601 } else {
7602 WC_WHILE_WHILE
7603 };
7604
7605 // c:1526 — `p = ecadd(0);`
7606 p = ecadd(0);
7607 // c:1527 — `zshlex();`
7608 zshlex();
7609 // c:1528 — `par_save_list(cmplx);` — condition.
7610 par_save_list_wordcode(cmplx);
7611 // c:1529 — `incmdpos = 1;`
7612 set_incmdpos(true);
7613 // c:1530-1531 — `while (tok == SEPER) zshlex();`
7614 while tok() == SEPER {
7615 zshlex();
7616 }
7617 // c:1532-1545 — body dispatch (inlined in C; we factor via
7618 // par_loop_body_wordcode since for/while/repeat share this
7619 // identical block).
7620 if tok() == DOLOOP {
7621 // c:1533 — `zshlex();`
7622 zshlex();
7623 // c:1534 — `par_save_list(cmplx);`
7624 par_save_list_wordcode(cmplx);
7625 // c:1535-1536 — `if (tok != DONE) YYERRORV(oecused);`
7626 if tok() != DONE {
7627 YYERRORV!(oecused);
7628 }
7629 // c:1537 — `incmdpos = 0;`
7630 set_incmdpos(false);
7631 // c:1538 — `zshlex();`
7632 zshlex();
7633 } else if tok() == INBRACE_TOK {
7634 // c:1540 — `zshlex();`
7635 zshlex();
7636 // c:1541 — `par_save_list(cmplx);`
7637 par_save_list_wordcode(cmplx);
7638 // c:1542-1543 — `if (tok != OUTBRACE) YYERRORV(oecused);`
7639 if tok() != OUTBRACE_TOK {
7640 YYERRORV!(oecused);
7641 }
7642 // c:1544 — `incmdpos = 0;`
7643 set_incmdpos(false);
7644 // c:1545 — `zshlex();`
7645 zshlex();
7646 } else if isset(CSHJUNKIELOOPS) {
7647 // c:1546-1550
7648 par_save_list_wordcode(cmplx);
7649 if tok() != ZEND {
7650 YYERRORV!(oecused);
7651 }
7652 zshlex();
7653 } else if unset(SHORTLOOPS) {
7654 // c:1551-1552 — `YYERRORV(oecused);`
7655 YYERRORV!(oecused);
7656 } else {
7657 // c:1554 — `par_save_list1(cmplx);`
7658 par_save_list1_wordcode(cmplx);
7659 }
7660
7661 // c:1556 — `ecbuf[p] = WCB_WHILE(type, ecused - 1 - p);`
7662 let used = ECUSED.get() as usize;
7663 ECBUF.with_borrow_mut(|b| {
7664 b[p] = WCB_WHILE(r#type, (used.saturating_sub(1 + p)) as wordcode);
7665 });
7666}
7667
7668/// !!! WARNING: RUST-ONLY HELPER !!!
7669///
7670/// No function of this name exists in `Src/parse.c`. This helper stands in
7671/// for C's UNTIL routing: `par_cmd` sends both WHILE and UNTIL to `par_while`
7672/// (c:990-992), so this is a one-line alias onto `par_while_wordcode`.
7673/// `until` shares par_while body — tok==UNTIL flips the type.
7674pub fn par_until_wordcode(cmplx: &mut i32) {
7675 par_while_wordcode(cmplx);
7676}
7677
7678/// Port of `par_repeat` from `Src/parse.c:1565` — C decl `par_repeat(int *cmplx)`. Rust fn `par_repeat_wordcode` is the wordcode-emitting variant.
7679pub fn par_repeat_wordcode(cmplx: &mut i32) {
7680 // c:1567 — `/* ### what to do about inrepeat_ here? */`
7681 // c:1568 — `int oecused = ecused, p;`
7682 let oecused = ECUSED.get() as usize;
7683 let p: usize;
7684
7685 // c:1570 — `p = ecadd(0);`
7686 p = ecadd(0);
7687
7688 // c:1572 — `incmdpos = 0;`
7689 set_incmdpos(false);
7690 // c:1573 — `zshlex();`
7691 zshlex();
7692 // c:1574-1575 — `if (tok != STRING) YYERRORV(oecused);`
7693 if tok() != STRING_LEX {
7694 YYERRORV!(oecused);
7695 }
7696 // c:1576 — `ecstr(tokstr);`
7697 ecstr(&tokstr().unwrap_or_default());
7698 // c:1577 — `incmdpos = 1;`
7699 set_incmdpos(true);
7700 // c:1578 — `zshlex();`
7701 zshlex();
7702 // c:1579-1580 — `while (tok == SEPER) zshlex();`
7703 while tok() == SEPER {
7704 zshlex();
7705 }
7706 // c:1581-1604 — body dispatch (inlined here matching C exactly).
7707 if tok() == DOLOOP {
7708 // c:1582-1587
7709 zshlex();
7710 par_save_list_wordcode(cmplx);
7711 if tok() != DONE {
7712 YYERRORV!(oecused);
7713 }
7714 set_incmdpos(false);
7715 zshlex();
7716 } else if tok() == INBRACE_TOK {
7717 // c:1589-1594
7718 zshlex();
7719 par_save_list_wordcode(cmplx);
7720 if tok() != OUTBRACE_TOK {
7721 YYERRORV!(oecused);
7722 }
7723 set_incmdpos(false);
7724 zshlex();
7725 } else if isset(CSHJUNKIELOOPS) {
7726 // c:1596-1599
7727 par_save_list_wordcode(cmplx);
7728 if tok() != ZEND {
7729 YYERRORV!(oecused);
7730 }
7731 zshlex();
7732 } else if unset(SHORTLOOPS) && unset(SHORTREPEAT) {
7733 // c:1601-1602 — par_repeat needs BOTH SHORTLOOPS and SHORTREPEAT
7734 // unset to refuse short form (more permissive than par_while).
7735 YYERRORV!(oecused);
7736 } else {
7737 // c:1604 — `par_save_list1(cmplx);`
7738 par_save_list1_wordcode(cmplx);
7739 }
7740
7741 // c:1606 — `ecbuf[p] = WCB_REPEAT(ecused - 1 - p);`
7742 let used = ECUSED.get() as usize;
7743 ECBUF.with_borrow_mut(|b| {
7744 b[p] = WCB_REPEAT((used.saturating_sub(1 + p)) as wordcode);
7745 });
7746}
7747
7748/// Port of `par_funcdef` from `Src/parse.c:1672` — C decl `par_funcdef(int *cmplx)`. Rust fn `par_funcdef_wordcode` is the wordcode-emitting variant.
7749///
7750/// The `function NAME { ... }` form. Emits a WCB_FUNCDEF header
7751/// followed by a names-count slot, the names themselves, four
7752/// metadata slots (string-area start, string-area length, npats,
7753/// do_tracing), then the body wordcode, then WCB_END.
7754///
7755/// Critical: saves/resets `ecnpats` + `ecssub` + `ecsoffs` around
7756/// the body parse so per-function pattern counts don't leak into
7757/// the enclosing scope's `ecnpats` accumulator (parse.c:1723-1758).
7758pub fn par_funcdef_wordcode(cmplx: &mut i32) {
7759 // c:1674 — `int oecused = ecused, num = 0, onp, p, c = 0;`
7760 let oecused = ECUSED.get() as usize;
7761 let mut num: i32 = 0;
7762 let onp: i32;
7763 let p: usize;
7764 let mut c: i32 = 0;
7765 // c:1675 — `int so, oecssub = ecssub;`
7766 let so: i32;
7767 let oecssub = ECSSUB.get();
7768 // c:1676 — `zlong oldlineno = lineno;`
7769 let oldlineno = lineno();
7770 // c:1677 — `int do_tracing = 0;`
7771 let mut do_tracing: i32 = 0;
7772
7773 // c:1679 — `lineno = 0;`
7774 set_lineno(0);
7775 // c:1680 — `nocorrect = 1;`
7776 set_nocorrect(1);
7777 // c:1681 — `incmdpos = 0;`
7778 set_incmdpos(false);
7779 // c:1682 — `zshlex();`
7780 zshlex();
7781
7782 // c:1684 — `p = ecadd(0);`
7783 p = ecadd(0);
7784 // c:1685 — `ecadd(0); /* p + 1 */`
7785 let p1 = ecadd(0);
7786
7787 // c:1687-1699 — `Consume an initial (-T), (--), or (-T --).`
7788 // c:1690 — `if (tok == STRING && tokstr[0] == Dash) {`
7789 if tok() == STRING_LEX {
7790 let s = tokstr().unwrap_or_default();
7791 let bytes = s.as_bytes();
7792 // C: `tokstr[0] == Dash`. The Dash token is U+e19B (crate::token_char),
7793 // `0xee 0x82 0x9b` in UTF-8; the plain char `'-'` is 0x2d.
7794 // Match either form.
7795 let first_is_dash = bytes.starts_with(&[0xee, 0x86, 0x9b])
7796 || (bytes.len() >= 1 && bytes[0] == b'-');
7797 if first_is_dash {
7798 // c:1691-1694 — `if (tokstr[1] == 'T' && !tokstr[2]) { ++do_tracing; zshlex(); }`
7799 // After the leading dash byte(s), check remaining bytes.
7800 let after_dash = if bytes.starts_with(&[0xee, 0x86, 0x9b]) {
7801 &bytes[3..]
7802 } else {
7803 &bytes[1..]
7804 };
7805 if after_dash.len() == 1 && after_dash[0] == b'T' {
7806 do_tracing += 1;
7807 zshlex();
7808 }
7809 // c:1695-1698 — `if (tok == STRING && tokstr[0] == Dash &&
7810 // tokstr[1] == Dash && !tokstr[2]) zshlex();`
7811 if tok() == STRING_LEX {
7812 let s2 = tokstr().unwrap_or_default();
7813 let b2 = s2.as_bytes();
7814 let mut idx = 0;
7815 let mut dashes = 0;
7816 while idx < b2.len() && dashes < 2 {
7817 if b2[idx..].starts_with(&[0xee, 0x86, 0x9b]) {
7818 idx += 3;
7819 dashes += 1;
7820 } else if b2[idx] == b'-' {
7821 idx += 1;
7822 dashes += 1;
7823 } else {
7824 break;
7825 }
7826 }
7827 if dashes == 2 && idx == b2.len() {
7828 zshlex();
7829 }
7830 }
7831 }
7832 }
7833
7834 // c:1701-1709 — names loop.
7835 // `while (tok == STRING) { if ((*tokstr == Inbrace || *tokstr == '{')
7836 // && !tokstr[1]) { tok = INBRACE; break; } ecstr(tokstr); num++; zshlex(); }`
7837 while tok() == STRING_LEX {
7838 let s = tokstr().unwrap_or_default();
7839 let bytes = s.as_bytes();
7840 // First char tests for the Inbrace token (U+e18F, UTF-8 `0xee 0x82 0x8f`) or `{`,
7841 // and length-1 check (`!tokstr[1]`).
7842 let is_inbrace_only = (bytes.len() == 1 && bytes[0] == b'{')
7843 || (bytes == [0xee, 0x86, 0x8f]);
7844 if is_inbrace_only {
7845 set_tok(INBRACE_TOK);
7846 break;
7847 }
7848 ecstr(&s);
7849 num += 1;
7850 zshlex();
7851 }
7852
7853 // c:1711-1714 — four metadata placeholder slots.
7854 let m2 = ecadd(0);
7855 let m3 = ecadd(0);
7856 let m4 = ecadd(0);
7857 let m5 = ecadd(0);
7858
7859 // c:1716 — `nocorrect = 0;`
7860 set_nocorrect(0);
7861 // c:1717 — `incmdpos = 1;`
7862 set_incmdpos(true);
7863 // c:1718-1719 — `if (tok == INOUTPAR) zshlex();`
7864 if tok() == INOUTPAR {
7865 zshlex();
7866 }
7867 // c:1720-1721 — `while (tok == SEPER) zshlex();`
7868 while tok() == SEPER {
7869 zshlex();
7870 }
7871
7872 // c:1723 — `ecnfunc++;`
7873 ECNFUNC.set(ECNFUNC.get() + 1);
7874 // c:1724 — `ecssub = so = ecsoffs;`
7875 so = ECSOFFS.get();
7876 ECSSUB.set(so);
7877 // c:1725 — `onp = ecnpats;`
7878 onp = ECNPATS.with(|cc| cc.get());
7879 // c:1726 — `ecnpats = 0;`
7880 ECNPATS.with(|cc| cc.set(0));
7881
7882 // c:1728 — `if (tok == INBRACE) {`
7883 if tok() == INBRACE_TOK {
7884 // c:1729 — `zshlex();`
7885 zshlex();
7886 // c:1730 — `par_list(&c);`
7887 par_list_wordcode(&mut c);
7888 // c:1731-1736 — `if (tok != OUTBRACE) { lineno += oldlineno; ... }`
7889 if tok() != OUTBRACE_TOK {
7890 set_lineno(lineno() + oldlineno);
7891 ECNPATS.with(|cc| cc.set(onp));
7892 ECSSUB.set(oecssub);
7893 YYERRORV!(oecused);
7894 }
7895 // c:1737-1740 — `if (num == 0) { incmdpos = 0; }`
7896 if num == 0 {
7897 set_incmdpos(false);
7898 }
7899 // c:1741 — `zshlex();`
7900 zshlex();
7901 } else if unset(SHORTLOOPS) {
7902 // c:1742-1746 — `lineno += oldlineno; ecnpats = onp; ecssub = oecssub; YYERRORV`
7903 set_lineno(lineno() + oldlineno);
7904 ECNPATS.with(|cc| cc.set(onp));
7905 ECSSUB.set(oecssub);
7906 YYERRORV!(oecused);
7907 } else {
7908 // c:1748 — `par_list1(&c);`
7909 par_list1_wordcode(&mut c);
7910 }
7911
7912 // c:1750 — `ecadd(WCB_END());`
7913 ecadd(WCB_END());
7914 // c:1751-1754 — fill the 4 metadata slots
7915 let cur_sofs = ECSOFFS.get();
7916 let body_npats = ECNPATS.with(|cc| cc.get());
7917 ECBUF.with_borrow_mut(|b| {
7918 b[m2] = (so - oecssub) as wordcode;
7919 b[m3] = (cur_sofs - so) as wordcode;
7920 b[m4] = body_npats as wordcode;
7921 b[m5] = do_tracing as wordcode;
7922 });
7923 // c:1755 — `ecbuf[p + 1] = num;`
7924 ECBUF.with_borrow_mut(|b| {
7925 b[p1] = num as wordcode;
7926 });
7927
7928 // c:1757 — `ecnpats = onp;`
7929 ECNPATS.with(|cc| cc.set(onp));
7930 // c:1758 — `ecssub = oecssub;`
7931 ECSSUB.set(oecssub);
7932 // c:1759 — `ecnfunc++;`
7933 ECNFUNC.set(ECNFUNC.get() + 1);
7934
7935 // c:1761 — `ecbuf[p] = WCB_FUNCDEF(ecused - 1 - p);`
7936 let used = ECUSED.get() as usize;
7937 ECBUF.with_borrow_mut(|b| {
7938 b[p] = WCB_FUNCDEF((used.saturating_sub(1 + p)) as wordcode);
7939 });
7940
7941 // c:1763-1777 — anonymous-function trailing args (num == 0 case).
7942 if num == 0 {
7943 // c:1766 — `int parg = ecadd(0);`
7944 let parg = ecadd(0);
7945 // c:1767 — `ecadd(0);`
7946 ecadd(0);
7947 // c:1768-1772 — `while (tok == STRING) { ecstr(tokstr); num++; zshlex(); }`
7948 while tok() == STRING_LEX {
7949 ecstr(&tokstr().unwrap_or_default());
7950 num += 1;
7951 zshlex();
7952 }
7953 // c:1773-1774 — `if (num > 0) *cmplx = 1;`
7954 if num > 0 {
7955 *cmplx = 1;
7956 }
7957 // c:1775 — `ecbuf[parg] = ecused - parg;`
7958 // c:1776 — `ecbuf[parg+1] = num;`
7959 let used2 = ECUSED.get() as usize;
7960 ECBUF.with_borrow_mut(|b| {
7961 b[parg] = (used2 - parg) as wordcode;
7962 b[parg + 1] = num as wordcode;
7963 });
7964 }
7965 // c:1778 — `lineno += oldlineno;`
7966 set_lineno(lineno() + oldlineno);
7967}
7968
7969/// Size of `struct fdhead` in `wordcode` (u32) units. Used by all
7970/// the header-walk macros below.
7971pub const FDHEAD_WORDS: usize = size_of::<fdhead>() / 4;
7972
7973/// Port of `par_subsh` from `Src/parse.c:1619` — C decl `par_subsh(int *cmplx, int zsh_construct)`. Rust fn `par_subsh_wordcode` is the wordcode-emitting variant; `zsh_construct` selects the `{...}` (cursh) shape as in C.
7974/// `Src/parse.c:1619-1665`. Handles both `(...)` subshell and
7975/// `{...}` brace group (cursh) plus optional `always { ... }`
7976/// trailing block. C uses a single function with `zsh_construct=1`
7977/// for `{...}` and 0 for `(...)`.
7978pub fn par_subsh_wordcode(cmplx: &mut i32, zsh_construct: i32) {
7979 // c:1621 — `enum lextok otok = tok;`
7980 let otok = tok();
7981 // c:1622 — `int oecused = ecused, p, pp;`
7982 let oecused = ECUSED.get() as usize;
7983 let p: usize;
7984 let pp: usize;
7985
7986 // c:1624 — `p = ecadd(0);`
7987 p = ecadd(0);
7988 // c:1625 — `/* Extra word only needed for always block */`
7989 // c:1626 — `pp = ecadd(0);`
7990 pp = ecadd(0);
7991 // c:1627 — `zshlex();`
7992 zshlex();
7993 // c:1628 — `par_list(cmplx);`
7994 par_list_wordcode(cmplx);
7995 // c:1629 — `ecadd(WCB_END());`
7996 ecadd(WCB_END());
7997 // c:1630-1631 — `if (tok != ((otok == INPAR) ? OUTPAR : OUTBRACE))
7998 // YYERRORV(oecused);`
7999 if tok()
8000 != (if otok == INPAR_TOK {
8001 OUTPAR_TOK
8002 } else {
8003 OUTBRACE_TOK
8004 })
8005 {
8006 YYERRORV!(oecused);
8007 }
8008 // c:1632 — `incmdpos = !zsh_construct;`
8009 set_incmdpos(zsh_construct == 0);
8010 // c:1633 — `zshlex();`
8011 zshlex();
8012
8013 // c:1635 — `/* Optional always block. No intervening SEPERs allowed. */`
8014 // c:1636 — `if (otok == INBRACE && tok == STRING && !strcmp(tokstr, "always")) {`
8015 if otok == INBRACE_TOK && tok() == STRING_LEX && tokstr().as_deref() == Some("always") {
8016 // c:1637 — `ecbuf[pp] = WCB_TRY(ecused - 1 - pp);`
8017 let used = ECUSED.get() as usize;
8018 ECBUF.with_borrow_mut(|b| {
8019 b[pp] = WCB_TRY((used.saturating_sub(1 + pp)) as wordcode);
8020 });
8021 // c:1638 — `incmdpos = 1;`
8022 set_incmdpos(true);
8023 // c:1639-1641 — `do { zshlex(); } while (tok == SEPER);`
8024 loop {
8025 zshlex();
8026 if tok() != SEPER {
8027 break;
8028 }
8029 }
8030
8031 // c:1643-1644 — `if (tok != INBRACE) YYERRORV(oecused);`
8032 if tok() != INBRACE_TOK {
8033 YYERRORV!(oecused);
8034 }
8035 // c:1645 — `cmdpop();`
8036 cmdpop();
8037 // c:1646 — `cmdpush(CS_ALWAYS);`
8038 cmdpush(CS_ALWAYS as u8);
8039
8040 // c:1648 — `zshlex();`
8041 zshlex();
8042 // c:1649 — `par_save_list(cmplx);`
8043 par_save_list_wordcode(cmplx);
8044 // c:1650-1651 — `while (tok == SEPER) zshlex();`
8045 while tok() == SEPER {
8046 zshlex();
8047 }
8048
8049 // c:1653 — `incmdpos = 1;`
8050 set_incmdpos(true);
8051
8052 // c:1655-1656 — `if (tok != OUTBRACE) YYERRORV(oecused);`
8053 if tok() != OUTBRACE_TOK {
8054 YYERRORV!(oecused);
8055 }
8056 // c:1657 — `zshlex();`
8057 zshlex();
8058 // c:1658 — `ecbuf[p] = WCB_TRY(ecused - 1 - p);`
8059 let used = ECUSED.get() as usize;
8060 ECBUF.with_borrow_mut(|b| {
8061 b[p] = WCB_TRY((used.saturating_sub(1 + p)) as wordcode);
8062 });
8063 } else {
8064 // c:1660-1661 — `ecbuf[p] = (otok == INPAR ? WCB_SUBSH(...) : WCB_CURSH(...));`
8065 let used = ECUSED.get() as usize;
8066 let off = used.saturating_sub(1 + p);
8067 ECBUF.with_borrow_mut(|b| {
8068 b[p] = if otok == INPAR_TOK {
8069 WCB_SUBSH(off as wordcode)
8070 } else {
8071 WCB_CURSH(off as wordcode)
8072 };
8073 });
8074 }
8075}
8076
8077/// Port of `par_time()` from `Src/parse.c:1787` — C signature `par_time(void)`. `time PIPE`
8078/// emits WCB_TIMED(WC_TIMED_PIPE) + the sublist code; bare `time`
8079/// with no pipeline emits WCB_TIMED(WC_TIMED_EMPTY).
8080pub fn par_time_wordcode() {
8081 // c:1791 — `zshlex();`
8082 zshlex();
8083 // c:1793-1794 — `p = ecadd(0); ecadd(0);`
8084 let p = ecadd(0);
8085 ecadd(0);
8086 // c:1795 — `if ((f = par_sublist2(&c)) < 0)`
8087 let mut c = 0i32;
8088 let f = par_sublist2(&mut c);
8089 match f {
8090 Some(flags) => {
8091 // c:1799 — `ecbuf[p] = WCB_TIMED(WC_TIMED_PIPE);`
8092 ECBUF.with_borrow_mut(|b| {
8093 if p < b.len() {
8094 b[p] = WCB_TIMED(WC_TIMED_PIPE);
8095 }
8096 });
8097 // c:1800 — `set_sublist_code(p+1, WC_SUBLIST_END, f,
8098 // ecused-2-p, c);`
8099 let used = ECUSED.get() as usize;
8100 let skip = used.saturating_sub(2 + p) as i32;
8101 set_sublist_code(p + 1, WC_SUBLIST_END as i32, flags, skip, c != 0);
8102 }
8103 None => {
8104 // c:1796-1798 — `ecused--; ecbuf[p] = WCB_TIMED(WC_TIMED_EMPTY);`
8105 ECUSED.set((ECUSED.get() - 1).max(0));
8106 ECBUF.with_borrow_mut(|b| {
8107 if p < b.len() {
8108 b[p] = WCB_TIMED(WC_TIMED_EMPTY);
8109 }
8110 });
8111 }
8112 }
8113}
8114
8115/// Port of `par_dinbrack()` from `Src/parse.c:1810` — C signature `par_dinbrack(void)`. Wraps
8116/// `par_cond` (the cond-expression emitter at parse.c:2409) with
8117/// the `[[ ... ]]` framing: incond/incmdpos toggles + DOUTBRACK
8118/// expectation.
8119pub fn par_cond_wordcode() {
8120 let oecused = ECUSED.get();
8121 // c:1814 — `incond = 1;`
8122 set_incond(1);
8123 // c:1815 — `incmdpos = 0;`
8124 set_incmdpos(false);
8125 // c:1816 — `zshlex();` past `[[`.
8126 zshlex();
8127 // c:1817 — `par_cond();` — call the no-skip cond-expression
8128 // entry that EMITS WORDCODE (par_cond_top → par_cond_1 →
8129 // par_cond_2 → par_cond_double/triple/multi). NOT the AST
8130 // `par_cond` at parse.rs:4644 which is a misnamed `par_dinbrack`
8131 // that skips `[[` AND `]]` and returns a ZshCommand AST node
8132 // instead of pushing WC_COND opcodes. NOT `parse_cond_expr`
8133 // either — that's also AST-only, returning ZshCond. With
8134 // `parse_cond_expr` here, every `[[ ... ]]` test produced ZERO
8135 // wordcode payload and parity dropped ~148 words on /etc/zshrc.
8136 let _ = par_cond_top();
8137 // c:1818-1819 — `if (tok != DOUTBRACK) YYERRORV(oecused);`
8138 if tok() != DOUTBRACK {
8139 let _ = oecused;
8140 YYERRORV!(oecused);
8141 }
8142 // c:1820 — `incond = 0;`
8143 set_incond(0);
8144 // c:1821 — `incmdpos = 1;`
8145 set_incmdpos(true);
8146 // c:1822 — `zshlex();` past `]]`.
8147 zshlex();
8148}
8149
8150/// !!! WARNING: RUST-ONLY HELPER !!!
8151///
8152/// No function of this name exists in `Src/parse.c`. This helper stands in
8153/// for the `case DINPAR:` arm of `par_cmd` (c:1031-1035: `ecadd(WCB_ARITH());
8154/// ecstr(tokstr); zshlex();`), which C inlines rather than giving it a
8155/// function.
8156/// Port of the `case DINPAR:` arm of `par_cmd` from
8157/// `Src/parse.c:1031-1034`:
8158/// ```c
8159/// ecadd(WCB_ARITH());
8160/// ecstr(tokstr);
8161/// zshlex();
8162/// ```
8163/// `(( EXPR ))` arithmetic at command position — emits the ARITH
8164/// opcode followed by the interned EXPR string, then advances past
8165/// the DINPAR token (which already carries the body text).
8166pub fn par_arith_wordcode() {
8167 // c:1032 — `ecadd(WCB_ARITH());`
8168 ecadd(WCB_ARITH());
8169 // c:1033 — `ecstr(tokstr);` — interns the expression string and
8170 // appends its strcode index to the wordcode buffer.
8171 let expr = tokstr().unwrap_or_default();
8172 ecstr(&expr);
8173 // c:1034 — `zshlex();`
8174 zshlex();
8175}
8176
8177/// Port of `par_simple` from `Src/parse.c:1836` — C decl `par_simple(int *cmplx, int nr)`. Rust fn `par_simple_wordcode` is the wordcode-emitting variant.
8178/// Emits WC_SIMPLE + word count +
8179/// interned string offsets. Returns `0` when nothing was emitted,
8180/// otherwise `1 + (number of code words consumed by redirections)`.
8181/// The full C body handles assignments (ENVSTRING/ENVARRAY),
8182/// inline `{var}>file` brace-FDs, prefix modifiers (NOCORRECT etc),
8183/// and `name() { body }` funcdef detection — those paths are
8184/// progressively wired into the AST parser; this wordcode-emitter
8185/// covers the simple `cmd args...` case + interleaved redirs.
8186pub fn par_simple_wordcode(cmplx: &mut i32, mut nr: i32) -> i32 {
8187 // c:1838-1841 — `int oecused = ecused, isnull = 1, r, argc = 0,
8188 // p, isfunc = 0, sr = 0;`
8189 // `int c = *cmplx, nrediradd, assignments = 0, ppost = 0,
8190 // is_typeset = 0;`
8191 // c is the SAVED initial cmplx so INOUTPAR can restore via
8192 // `*cmplx = c;` at c:2070.
8193 let oecused = ECUSED.get() as usize;
8194 let c_saved = *cmplx;
8195 let mut isnull = true;
8196 let mut argc: u32 = 0;
8197 let mut sr: i32 = 0;
8198 let mut assignments = false;
8199 let mut isfunc = false;
8200
8201 // c:1843 — `r = ecused;` — saves the offset where redirs get
8202 // INSERTED (via ecispace). Each redir shifts later words DOWN
8203 // by ncodes, so the SIMPLE placeholder at `p` (set later) must
8204 // also bump by ncodes when a redir lands. C uses `&r` to pass
8205 // the cursor by reference; Rust uses a mutable local + manual
8206 // bumps after each par_redir_wordcode call.
8207 let mut r: usize = ECUSED.get() as usize;
8208
8209 // c:1844-1919 — pre-cmd loop: NOCORRECT, ENVSTRING (scalar
8210 // assigns), ENVARRAY (array assigns), IS_REDIROP. Loops until
8211 // a non-assignment token is seen.
8212 loop {
8213 match tok() {
8214 NOCORRECT => {
8215 // c:1846-1849
8216 *cmplx = 1;
8217 set_nocorrect(1);
8218 }
8219 ENVSTRING => {
8220 // c:1848-1898 — scalar assignment `name=value` or
8221 // `name+=value`. Emits WCB_ASSIGN(SCALAR, NEW|INC, 0)
8222 // followed by ecstr(name), ecstr(value).
8223 let raw = tokstr().unwrap_or_default();
8224 // Find first of Inbrack / '=' / '+' (the C scan at
8225 // c:1851-1853). Inside Inbrack we skipparens — i.e.
8226 // skip `name[...]` index, then continue.
8227 // c:1851-1853 — `for (ptr = tokstr; *ptr && *ptr != Inbrack
8228 // && *ptr != '=' && *ptr != '+'; ptr++); if (*ptr == Inbrack)
8229 // skipparens(Inbrack, Outbrack, &ptr);`. Walk to the first
8230 // `[`/`=`/`+`/Equals-token, then if we landed on `[`, skip
8231 // the balanced `name[index]` pair via skipparens.
8232 let bytes: Vec<char> = raw.chars().collect();
8233 let raw_str: String = bytes.iter().collect();
8234 let mut idx = 0usize;
8235 while idx < bytes.len() {
8236 let ch = bytes[idx];
8237 if ch == '\u{e191}' /* Inbrack */
8238 || ch == '=' || ch == '+' || ch == '\u{e18d}'
8239 /* Equals */
8240 {
8241 break;
8242 }
8243 idx += 1;
8244 }
8245 if idx < bytes.len() && bytes[idx] == '\u{e191}'
8246 /* Inbrack */
8247 {
8248 // c:1855 — `skipparens(Inbrack, Outbrack, &ptr);`.
8249 let byte_off: usize = bytes[..idx].iter().map(|c| c.len_utf8()).sum();
8250 let mut cursor: &str = &raw_str[byte_off..];
8251 let _ = crate::ported::utils::skipparens('\u{e191}', '\u{e192}', &mut cursor);
8252 let consumed = raw_str.len() - byte_off - cursor.len();
8253 let advance_chars = raw_str[byte_off..byte_off + consumed].chars().count();
8254 idx += advance_chars;
8255 // Continue scanning for `=` / `+` after the `]`.
8256 while idx < bytes.len() {
8257 let ch = bytes[idx];
8258 if ch == '=' || ch == '+' || ch == '\u{e18d}' {
8259 break;
8260 }
8261 idx += 1;
8262 }
8263 }
8264 let is_inc = idx < bytes.len() && bytes[idx] == '+';
8265 // c:1856-1858 — `if (*ptr == '+') { *ptr++ = '\0';
8266 // ecadd(WCB_ASSIGN(SCALAR, INC, 0)); } else WCB_NEW`
8267 // C nulls the `+` AT THAT POSITION then advances ptr.
8268 // `name` is bytes BEFORE the `+`, NOT including it.
8269 let name_end = idx;
8270 if is_inc {
8271 idx += 1;
8272 }
8273 let flag = if is_inc { WC_ASSIGN_INC } else { WC_ASSIGN_NEW };
8274 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, flag, 0));
8275 // c:1860 — `if (*ptr == '=') { *ptr = '\0'; str = ptr + 1; }
8276 // else equalsplit(tokstr, &str);`
8277 let name: String = bytes[..name_end].iter().collect();
8278 let str_off = if idx < bytes.len() && (bytes[idx] == '=' || bytes[idx] == '\u{e18d}')
8279 {
8280 idx + 1
8281 } else {
8282 idx
8283 };
8284 let value: String = bytes[str_off..].iter().collect();
8285 // c:1866-1877 — scan value for `=(`/`<(`/`>(` (proc
8286 // subst); if found, bump cmplx (suppresses Z_SIMPLE).
8287 let vbytes: Vec<char> = value.chars().collect();
8288 for (i, ch) in vbytes.iter().enumerate() {
8289 if i + 1 < vbytes.len() && vbytes[i + 1] == '\u{e188}'
8290 /* Inpar */
8291 {
8292 if *ch == '\u{e18d}' /* Equals */
8293 || *ch == '\u{e194}' /* Inang */
8294 || *ch == '\u{e196}'
8295 /* OutangProc */
8296 {
8297 *cmplx = 1;
8298 break;
8299 }
8300 }
8301 }
8302 // c:1856-1865 — `name` is NUL-terminated inside tokstr.
8303 // !!! RUST-ONLY !!! — write the cut through to zshlextext (see the
8304 // leading-assignment loop in par_simple).
8305 crate::ported::lex::LEX_ZSHLEXTEXT.with_borrow_mut(|t| {
8306 if let Some(text) = t.as_mut().filter(|text| text.starts_with(name.as_str())) {
8307 text.truncate(name.len());
8308 }
8309 });
8310 ecstr(&name);
8311 ecstr(&value);
8312 isnull = false;
8313 assignments = true;
8314 }
8315 ENVARRAY => {
8316 // c:1883-1908 — array assignment `name=( ... )` in the
8317 // pre-cmd loop (no `typeset`-style typeset_force flag).
8318 // c:1884 — `int oldcmdpos = incmdpos, n, type2;`
8319 let oldcmdpos = incmdpos();
8320 let n: u32;
8321 let type2: wordcode;
8322 let p: usize;
8323
8324 // c:1886-1889 — `array setting is cmplx because it can
8325 // contain process substitutions`
8326 // c:1890 — `*cmplx = c = 1;`
8327 *cmplx = 1;
8328 // c:1891 — `p = ecadd(0);`
8329 p = ecadd(0);
8330 // c:1892 — `incmdpos = 0;`
8331 set_incmdpos(false);
8332 // c:1893-1897 — `+=` detection: if tokstr ends in `+`,
8333 // strip the `+` and use WC_ASSIGN_INC; else WC_ASSIGN_NEW.
8334 let raw = tokstr().unwrap_or_default();
8335 let (name, t2) = if raw.ends_with('+') {
8336 (raw[..raw.len() - 1].to_string(), WC_ASSIGN_INC)
8337 } else {
8338 (raw.clone(), WC_ASSIGN_NEW)
8339 };
8340 type2 = t2;
8341 // c:1898 — `ecstr(tokstr);` (tokstr now NUL-trimmed)
8342 ecstr(&name);
8343 // c:1899 — `cmdpush(CS_ARRAY);`
8344 cmdpush(CS_ARRAY as u8);
8345 // c:1900 — `zshlex();`
8346 zshlex();
8347 // c:1901 — `n = par_nl_wordlist();`
8348 n = par_nl_wordlist_wordcode();
8349 // c:1902 — `ecbuf[p] = WCB_ASSIGN(WC_ASSIGN_ARRAY, type2, n);`
8350 ECBUF.with_borrow_mut(|b| {
8351 b[p] = WCB_ASSIGN(WC_ASSIGN_ARRAY, type2, n);
8352 });
8353 // c:1903 — `cmdpop();`
8354 cmdpop();
8355 // c:1904-1905 — `if (tok != OUTPAR) YYERROR(oecused);`
8356 if tok() != OUTPAR_TOK {
8357 YYERROR!(oecused);
8358 }
8359 // c:1906 — `incmdpos = oldcmdpos;`
8360 set_incmdpos(oldcmdpos);
8361 // c:1907 — `isnull = 0;`
8362 isnull = false;
8363 // c:1908 — `assignments = 1;`
8364 assignments = true;
8365 }
8366 t if IS_REDIROP(t) => {
8367 // c:1900-1904 — `*cmplx = c = 1; nr += par_redir(&r,
8368 // NULL); continue;`. The wordcode-emitting redir is
8369 // distinct from the AST par_redir — it INSERTS
8370 // WCB_REDIR + fd + ecstrcode(name) at offset `r`
8371 // via ecispace, shifting any later words down.
8372 *cmplx = 1;
8373 let added = par_redir_wordcode(&mut r, None);
8374 if added == 0 {
8375 break;
8376 }
8377 nr += added;
8378 continue;
8379 }
8380 _ => break,
8381 }
8382 zshlex(); // c:1907 `zshlex();`
8383 }
8384
8385 // c:1920-1921 — `if (tok == AMPER || tok == AMPERBANG) YYERROR;`
8386 if tok() == AMPER || tok() == AMPERBANG {
8387 YYERROR!(oecused);
8388 }
8389
8390 // c:1923 — `p = ecadd(WCB_SIMPLE(0));`
8391 let mut p = ecadd(WCB_SIMPLE(0));
8392
8393 // c:1924-2105 — main words loop. is_typeset tracks whether the
8394 // outer command was `typeset`/`export`/etc. so the final
8395 // placeholder gets WCB_TYPESET instead of WCB_SIMPLE.
8396 let mut is_typeset = false;
8397 let mut postassigns: u32 = 0;
8398 let mut ppost: usize = 0;
8399 loop {
8400 match tok() {
8401 STRING_LEX | TYPESET => {
8402 // c:1926 — `int redir_var = 0;`
8403 let mut redir_var = false;
8404 // c:1928-1929 — `*cmplx = 1; incmdpos = 0;`
8405 *cmplx = 1;
8406 set_incmdpos(false);
8407 // c:1931-1932 — TYPESET → intypeset = is_typeset = 1.
8408 if tok() == TYPESET {
8409 set_intypeset(true);
8410 is_typeset = true;
8411 }
8412 let s = tokstr().unwrap_or_default();
8413 // c:1934-1974 — `{var}>file` brace-FD detection.
8414 // `if (!isset(IGNOREBRACES) && *tokstr == Inbrace)`
8415 let bytes = s.as_bytes();
8416 let first_is_inbrace = bytes.starts_with(&[0xee, 0x86, 0x8f])
8417 || (bytes.len() >= 1 && bytes[0] == b'{');
8418 if !isset(IGNOREBRACES) && first_is_inbrace {
8419 // c:1937-1938 — `char *eptr = tokstr + strlen(tokstr) - 1;`
8420 // `char *ptr = eptr;`
8421 // C tests `*eptr == Outbrace` (0x90 marker or `}`) AND
8422 // there's content between `{` and `}` (`ptr > tokstr + 1`).
8423 let last_two_outbrace = bytes.len() >= 2
8424 && (bytes.ends_with(&[0xee, 0x86, 0x90]) || bytes.last() == Some(&b'}'));
8425 let opener_len = if bytes.starts_with(&[0xee, 0x86, 0x8f]) {
8426 3
8427 } else {
8428 1
8429 };
8430 let closer_len = if bytes.ends_with(&[0xee, 0x86, 0x90]) {
8431 3
8432 } else if bytes.last() == Some(&b'}') {
8433 1
8434 } else {
8435 0
8436 };
8437 if last_two_outbrace && bytes.len() > opener_len + closer_len {
8438 // c:1944 — `if (itype_end(tokstr+1, IIDENT, 0) >= ptr)`
8439 // Inner content is the identifier between `{` and `}`.
8440 let inner_start = opener_len;
8441 let inner_end = bytes.len() - closer_len;
8442 let inner = &s[inner_start..inner_end];
8443 if !inner.is_empty() && crate::ported::params::isident(inner) {
8444 // c:1946-1948 — `char *idstring = dupstrpfx(...);`
8445 // `redir_var = 1; zshlex();`
8446 let idstring = inner.to_string();
8447 redir_var = true;
8448 zshlex();
8449 // c:1953-1958 — `if (IS_REDIROP(tok) && tokfd == -1)
8450 // { *cmplx = c = 1; nrediradd = par_redir(&r, id);
8451 // p += nrediradd; sr += nrediradd; }`
8452 if IS_REDIROP(tok()) && tokfd() == -1 {
8453 *cmplx = 1;
8454 let nrediradd = par_redir_wordcode(&mut r, Some(&idstring));
8455 p += nrediradd as usize;
8456 sr += nrediradd;
8457 } else if postassigns > 0 {
8458 // c:1959-1966 — postassigns path: emit
8459 // WCB_ASSIGN(SCALAR, INC, 0) + name + ""
8460 postassigns += 1;
8461 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_INC, 0));
8462 ecstr(&s);
8463 ecstr("");
8464 } else {
8465 // c:1968-1972 — `else { ecstr(toksave); argc++; }`
8466 ecstr(&s);
8467 argc += 1;
8468 }
8469 }
8470 }
8471 }
8472 if !redir_var {
8473 // c:1977-1996 — normal (non-redir-var) STRING/TYPESET.
8474 if postassigns > 0 {
8475 // c:1979-1989 — typeset with bare-name arg → INC
8476 postassigns += 1;
8477 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_INC, 0));
8478 ecstr(&s);
8479 ecstr("");
8480 } else {
8481 ecstr(&s);
8482 argc += 1;
8483 }
8484 zshlex();
8485 }
8486 isnull = false;
8487 }
8488 ENVSTRING => {
8489 // c:2005-2026 — mid-cmd ENVSTRING (under intypeset
8490 // context). Emits WCB_ASSIGN(SCALAR, NEW, 0) then
8491 // ecstr(name) + ecstr(value), tracking the first
8492 // postassign offset in `ppost` (which the trailing
8493 // WCB_TYPESET header points to).
8494 if postassigns == 0 {
8495 ppost = ecadd(0);
8496 }
8497 postassigns += 1;
8498 // c:2010-2014 — `for (ptr = tokstr; *ptr && *ptr != Inbrack
8499 // && *ptr != '=' && *ptr != '+'; ptr++); if (*ptr == Inbrack)
8500 // skipparens(Inbrack, Outbrack, &ptr);`.
8501 let raw = tokstr().unwrap_or_default();
8502 let bytes: Vec<char> = raw.chars().collect();
8503 let mut idx = 0usize;
8504 while idx < bytes.len() {
8505 let ch = bytes[idx];
8506 if ch == '\u{e191}' /* Inbrack */
8507 || ch == '=' || ch == '+' || ch == '\u{e18d}'
8508 /* Equals */
8509 {
8510 break;
8511 }
8512 idx += 1;
8513 }
8514 if idx < bytes.len() && bytes[idx] == '\u{e191}'
8515 /* Inbrack */
8516 {
8517 // c:2014 — `skipparens(Inbrack, Outbrack, &ptr);`.
8518 let byte_off: usize = bytes[..idx].iter().map(|c| c.len_utf8()).sum();
8519 let mut cursor: &str = &raw[byte_off..];
8520 let _ = crate::ported::utils::skipparens('\u{e191}', '\u{e192}', &mut cursor);
8521 let consumed = raw.len() - byte_off - cursor.len();
8522 let advance_chars = raw[byte_off..byte_off + consumed].chars().count();
8523 idx += advance_chars;
8524 while idx < bytes.len() {
8525 let ch = bytes[idx];
8526 if ch == '=' || ch == '+' || ch == '\u{e18d}' {
8527 break;
8528 }
8529 idx += 1;
8530 }
8531 }
8532 let name: String = bytes[..idx].iter().collect();
8533 let str_off = if idx < bytes.len() && (bytes[idx] == '=' || bytes[idx] == '\u{e18d}')
8534 {
8535 idx + 1
8536 } else {
8537 idx
8538 };
8539 let value: String = bytes[str_off..].iter().collect();
8540 // c:2016-2019 — `name` is NUL-terminated inside tokstr.
8541 // !!! RUST-ONLY !!! — write the cut through to zshlextext (see the
8542 // leading-assignment loop in par_simple).
8543 crate::ported::lex::LEX_ZSHLEXTEXT.with_borrow_mut(|t| {
8544 if let Some(text) = t.as_mut().filter(|text| text.starts_with(name.as_str())) {
8545 text.truncate(name.len());
8546 }
8547 });
8548 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_NEW, 0));
8549 ecstr(&name);
8550 ecstr(&value);
8551 isnull = false;
8552 zshlex();
8553 }
8554 ENVARRAY => {
8555 // c:2027-2050 — mid-cmd ENVARRAY (typeset N=(…) form).
8556 // C tracks postassigns + ppost the same as ENVSTRING,
8557 // but the inner emit is WCB_ASSIGN(ARRAY, NEW, n)
8558 // with `n` patched in after par_nl_wordlist consumes
8559 // the elements. C also toggles intypeset=0 around the
8560 // wordlist so the lexer doesn't try to re-emit
8561 // assignments inside the array.
8562 *cmplx = 1;
8563 if postassigns == 0 {
8564 ppost = ecadd(0);
8565 }
8566 postassigns += 1;
8567 let parr = ecadd(0);
8568 let raw = tokstr().unwrap_or_default();
8569 let is_inc = raw.ends_with('+');
8570 let name = if is_inc {
8571 &raw[..raw.len() - 1]
8572 } else {
8573 raw.as_str()
8574 };
8575 let flag = if is_inc { WC_ASSIGN_INC } else { WC_ASSIGN_NEW };
8576 ecstr(name);
8577 cmdpush(CS_ARRAY as u8);
8578 set_intypeset(false);
8579 zshlex();
8580 // c:2044 — `n = par_nl_wordlist();` (parse.c:2379-2391).
8581 // SEPER + NEWLIN both allowed between elements.
8582 let mut nelem = 0u32;
8583 loop {
8584 let t = tok();
8585 if t != STRING_LEX && t != SEPER && t != NEWLIN {
8586 break;
8587 }
8588 if t == STRING_LEX {
8589 ecstr(&tokstr().unwrap_or_default());
8590 nelem += 1;
8591 }
8592 zshlex();
8593 }
8594 ECBUF.with_borrow_mut(|b| {
8595 if parr < b.len() {
8596 b[parr] = WCB_ASSIGN(WC_ASSIGN_ARRAY, flag, nelem);
8597 }
8598 });
8599 cmdpop();
8600 set_intypeset(true);
8601 if tok() != OUTPAR_TOK {
8602 YYERROR!(oecused);
8603 }
8604 isnull = false;
8605 zshlex();
8606 }
8607 t if IS_REDIROP(t) => {
8608 // c:1999-2010 — `nrediradd = par_redir(&r, NULL);
8609 // p += nrediradd; if (ppost) ppost += nrediradd;
8610 // sr += nrediradd;`
8611 *cmplx = 1;
8612 let added = par_redir_wordcode(&mut r, None);
8613 if added == 0 {
8614 break;
8615 }
8616 p += added as usize;
8617 if ppost != 0 {
8618 ppost += added as usize;
8619 }
8620 sr += added;
8621 }
8622 INOUTPAR => {
8623 // c:2051 — `} else if (tok == INOUTPAR) {`
8624 // c:2052 — `zlong oldlineno = lineno;`
8625 let oldlineno = lineno();
8626 // c:2053 — `int onp, so, oecssub = ecssub;`
8627 let oecssub = ECSSUB.get();
8628 // c:2055-2057 — `if (!isset(MULTIFUNCDEF) && argc > 1) YYERROR;`
8629 if !isset(MULTIFUNCDEF) && argc > 1 {
8630 YYERROR!(oecused);
8631 }
8632 // c:2058-2060 — `if (assignments || postassigns) YYERROR;`
8633 if assignments || postassigns > 0 {
8634 YYERROR!(oecused);
8635 }
8636 // c:2061-2068 — hasalias check + zwarn — skipped (no
8637 // alias tracking on the wordcode path).
8638
8639 // c:2070 — `*cmplx = c;`
8640 *cmplx = c_saved;
8641 // c:2071 — `lineno = 0;`
8642 set_lineno(0);
8643 // c:2072 — `incmdpos = 1;`
8644 set_incmdpos(true);
8645 // c:2073 — `cmdpush(CS_FUNCDEF);`
8646 cmdpush(CS_FUNCDEF as u8);
8647 // c:2074 — `zshlex();`
8648 zshlex();
8649 // c:2075-2076 — `while (tok == SEPER) zshlex();`
8650 while tok() == SEPER {
8651 zshlex();
8652 }
8653 // c:2079 — `ecispace(p + 1, 1); ecbuf[p+1] = argc;
8654 // ecadd(0)*4`. Insert the argc word at p+1, then
8655 // append 4 placeholder words.
8656 ecispace(p + 1, 1);
8657 ECBUF.with_borrow_mut(|b| {
8658 if p + 1 < b.len() {
8659 b[p + 1] = argc;
8660 }
8661 });
8662 // c:2080-2083 — four metadata placeholder slots.
8663 ecadd(0);
8664 ecadd(0);
8665 ecadd(0);
8666 ecadd(0);
8667
8668 // c:2085 — `ecnfunc++;`
8669 ECNFUNC.set(ECNFUNC.get() + 1);
8670 // c:2086 — `ecssub = so = ecsoffs;`
8671 let so = ECSOFFS.get();
8672 ECSSUB.set(so);
8673 // c:2087 — `onp = ecnpats;`
8674 let onp = ECNPATS.with(|cc| cc.get());
8675 // c:2088 — `ecnpats = 0;`
8676 ECNPATS.with(|cc| cc.set(0));
8677
8678 // c:2091 — `int c = 0;` — INNER cmplx for the body
8679 // parse. Local to each branch; C's enclosing *cmplx
8680 // is NOT modified by the body.
8681 let mut body_c: i32 = 0;
8682 // c:2090 — `if (tok == INBRACE) {`
8683 if tok() == INBRACE_TOK {
8684 // c:2093 — `zshlex();`
8685 zshlex();
8686 // c:2094 — `par_list(&c);`
8687 par_list_wordcode(&mut body_c);
8688 // c:2095-2101 — `if (tok != OUTBRACE) { cmdpop();
8689 // lineno += oldlineno; ecnpats = onp;
8690 // ecssub = oecssub; YYERROR; }`
8691 if tok() != OUTBRACE_TOK {
8692 cmdpop();
8693 set_lineno(lineno() + oldlineno);
8694 ECNPATS.with(|cc| cc.set(onp));
8695 ECSSUB.set(oecssub);
8696 YYERROR!(oecused);
8697 }
8698 // c:2102-2105 — `if (argc == 0) incmdpos = 0;`
8699 if argc == 0 {
8700 set_incmdpos(false);
8701 }
8702 // c:2106 — `zshlex();`
8703 zshlex();
8704 } else {
8705 // c:2107-2132 — short-body funcdef form: `f() cmd`
8706 // or `() cmd`. Wraps single par_cmd result in a
8707 // synthetic WC_LIST / WC_SUBLIST /
8708 // WC_PIPE(WC_PIPE_END, 0) header trio.
8709 let ll = ecadd(0);
8710 let sl = ecadd(0);
8711 ecadd(WCB_PIPE(WC_PIPE_END, 0));
8712 let ok = par_cmd_wordcode(&mut body_c, if argc == 0 { 1 } else { 0 });
8713 if !ok {
8714 cmdpop();
8715 YYERROR!(oecused);
8716 }
8717 if argc == 0 {
8718 // c:2118-2127 — anonymous funcdef may take args
8719 // after the body; first one already read.
8720 set_incmdpos(false);
8721 }
8722 // c:2130-2131 — inner sublist/list use inner cmplx.
8723 let used = ECUSED.get() as usize;
8724 set_sublist_code(
8725 sl,
8726 WC_SUBLIST_END as i32,
8727 0,
8728 (used.saturating_sub(1 + sl)) as i32,
8729 body_c != 0,
8730 );
8731 set_list_code(ll, Z_SYNC | Z_END, body_c != 0);
8732 }
8733 let _ = body_c;
8734 // c:2133 — `cmdpop();`
8735 cmdpop();
8736
8737 // c:2135 — `ecadd(WCB_END());`
8738 ecadd(WCB_END());
8739 // c:2136-2139 — fill 4 metadata slots at p+argc+2..5
8740 let p_argc = (p + (argc as usize) + 2) as usize;
8741 let cur_so = ECSOFFS.get();
8742 let np_now = ECNPATS.with(|cc| cc.get());
8743 ECBUF.with_borrow_mut(|b| {
8744 b[p_argc] = (so - oecssub) as wordcode;
8745 b[p_argc + 1] = (cur_so - so) as wordcode;
8746 b[p_argc + 2] = np_now as wordcode;
8747 b[p_argc + 3] = 0;
8748 });
8749
8750 // c:2141-2143 — `ecnpats = onp; ecssub = oecssub; ecnfunc++;`
8751 ECNPATS.with(|cc| cc.set(onp));
8752 ECSSUB.set(oecssub);
8753 ECNFUNC.set(ECNFUNC.get() + 1);
8754
8755 // c:2145 — `ecbuf[p] = WCB_FUNCDEF(ecused - 1 - p);`
8756 let used = ECUSED.get() as usize;
8757 let header_off = used.saturating_sub(1 + p) as wordcode;
8758 ECBUF.with_borrow_mut(|b| {
8759 b[p] = WCB_FUNCDEF(header_off);
8760 });
8761
8762 // c:2147-2172 — `if (argc == 0) { /* anonymous fn args */ }`
8763 if argc == 0 {
8764 // c:2150 — `int parg = ecadd(0);`
8765 let mut parg = ecadd(0);
8766 // c:2151 — `ecadd(0);`
8767 ecadd(0);
8768 // c:2152 — `while (tok == STRING || IS_REDIROP(tok)) {`
8769 while tok() == STRING_LEX || IS_REDIROP(tok()) {
8770 if tok() == STRING_LEX {
8771 // c:2155-2157
8772 ecstr(&tokstr().unwrap_or_default());
8773 argc += 1;
8774 zshlex();
8775 } else {
8776 // c:2159-2165 — *cmplx=c=1; nrediradd=par_redir;
8777 // p += nrediradd; ppost += nrediradd if ppost;
8778 // sr += nrediradd; parg += nrediradd;
8779 *cmplx = 1;
8780 let added = par_redir_wordcode(&mut r, None);
8781 if added == 0 {
8782 break;
8783 }
8784 p += added as usize;
8785 if ppost != 0 {
8786 ppost += added as usize;
8787 }
8788 sr += added;
8789 parg += added as usize;
8790 }
8791 }
8792 // c:2168-2169 — `if (argc > 0) *cmplx = 1;`
8793 if argc > 0 {
8794 *cmplx = 1;
8795 }
8796 // c:2170 — `ecbuf[parg] = ecused - parg;`
8797 // c:2171 — `ecbuf[parg+1] = argc;`
8798 let used2 = ECUSED.get() as usize;
8799 ECBUF.with_borrow_mut(|b| {
8800 b[parg] = (used2 - parg) as wordcode;
8801 b[parg + 1] = argc;
8802 });
8803 }
8804 // c:2173 — `lineno += oldlineno;`
8805 set_lineno(lineno() + oldlineno);
8806
8807 // c:2175-2177 — `isfunc = 1; isnull = 0; break;`
8808 isfunc = true;
8809 isnull = false;
8810 break;
8811 }
8812 _ => break,
8813 }
8814 }
8815
8816 // c:2173-2176 — `if (isnull && !(sr + nr)) { ecused = oecused;
8817 // return 0; }` — undo everything including pre-cmd assignments
8818 // if no actual command word emerged.
8819 if isnull && sr + nr == 0 && !assignments {
8820 ECUSED.set(p as i32);
8821 return 0;
8822 }
8823 // c:2186-2187 — `incmdpos = 1; intypeset = 0;` — reset before
8824 // the placeholder patch so the next-token lex doesn't carry
8825 // typeset/incond state.
8826 set_incmdpos(true);
8827 set_intypeset(false);
8828 // c:2189-2199 — `if (!isfunc) { if (is_typeset) ecbuf[p] =
8829 // WCB_TYPESET(argc); else ecbuf[p] = WCB_SIMPLE(argc); }`.
8830 // When isfunc=true the INOUTPAR branch already wrote WCB_FUNCDEF
8831 // at p; do NOT clobber it.
8832 if !isfunc {
8833 let header = if is_typeset {
8834 if postassigns > 0 {
8835 ECBUF.with_borrow_mut(|b| {
8836 if ppost < b.len() {
8837 b[ppost] = postassigns;
8838 }
8839 });
8840 } else {
8841 ecadd(0);
8842 }
8843 WCB_TYPESET(argc)
8844 } else {
8845 WCB_SIMPLE(argc)
8846 };
8847 ECBUF.with_borrow_mut(|b| {
8848 if p < b.len() {
8849 b[p] = header;
8850 }
8851 });
8852 }
8853 1 + sr
8854}
8855
8856/// Port of `par_redir` from `Src/parse.c:2229` — C decl `par_redir(int *rp, char *idstring)`. Rust fn `par_redir_wordcode` is the wordcode-emitting variant (the AST twin is `par_redir` at parse.rs:3096).
8857/// the wordcode-emitting variant that
8858/// pushes WCB_REDIR + fd + ecstrcode(name) into ECBUF. Distinct
8859/// from the AST `par_redir` (parse.rs:3771) which builds a
8860/// ZshRedir struct for the AST executor pipeline.
8861///
8862/// Returns the number of wordcodes added (3 for the basic shape,
8863/// 4 with idstring, 5 for HEREDOC[DASH] which carries the
8864/// terminator strings inline). Returns 0 on parse error.
8865///
8866/// `idstring` mirrors C's `char *idstring` parameter — `None` =
8867/// NULL (no `{var}>file` brace-FD shape), `Some(id)` = the captured
8868/// `{var}` name. C callers without a var pass NULL inline; Rust
8869/// callers do the same with `None`.
8870fn par_redir_wordcode(rp: &mut usize, idstring: Option<&str>) -> i32 {
8871 // c:2231 — `int r = *rp, type, fd1, oldcmdpos, oldnc, ncodes;`
8872 let r: usize = *rp;
8873 let mut r#type: i32;
8874 let fd1: i32;
8875 let oldcmdpos: bool;
8876 let oldnc: i32;
8877 let mut ncodes: usize;
8878 // c:2232 — `char *name;`
8879 let name: String;
8880
8881 // c:2234 — `oldcmdpos = incmdpos;`
8882 oldcmdpos = incmdpos();
8883 // c:2235 — `incmdpos = 0;`
8884 set_incmdpos(false);
8885 // c:2236 — `oldnc = nocorrect;`
8886 oldnc = nocorrect();
8887 // c:2237-2238 — `if (tok != INANG && tok != INOUTANG) nocorrect = 1;`
8888 if tok() != INANG_TOK && tok() != INOUTANG {
8889 set_nocorrect(1);
8890 }
8891 // c:2239 — `type = redirtab[tok - OUTANG];`
8892 // Map current redirop token to redirtab index — matches order of
8893 // C `enum { OUTANG, OUTANGBANG, DOUTANG, DOUTANGBANG, INANG,
8894 // INOUTANG, DINANG, DINANGDASH, INANGAMP, OUTANGAMP, AMPOUTANG,
8895 // OUTANGAMPBANG, DOUTANGAMP, DOUTANGAMPBANG, TRINANG }`.
8896 r#type = match tok() {
8897 OUTANG_TOK => REDIR_WRITE,
8898 OUTANGBANG => REDIR_WRITENOW,
8899 DOUTANG => REDIR_APP,
8900 DOUTANGBANG => REDIR_APPNOW,
8901 INANG_TOK => REDIR_READ,
8902 INOUTANG => REDIR_READWRITE,
8903 DINANG => REDIR_HEREDOC,
8904 DINANGDASH => REDIR_HEREDOCDASH,
8905 INANGAMP => REDIR_MERGEIN,
8906 OUTANGAMP => REDIR_MERGEOUT,
8907 AMPOUTANG => REDIR_ERRWRITE,
8908 OUTANGAMPBANG => REDIR_ERRWRITENOW,
8909 DOUTANGAMP => REDIR_ERRAPP,
8910 DOUTANGAMPBANG => REDIR_ERRAPPNOW,
8911 TRINANG => REDIR_HERESTR,
8912 _ => {
8913 set_incmdpos(oldcmdpos);
8914 set_nocorrect(oldnc);
8915 return 0;
8916 }
8917 };
8918 // c:2240 — `fd1 = tokfd;`
8919 fd1 = tokfd();
8920 // The operator's own text, kept for the YYERROR path below. C does not
8921 // need this: `zshlextext` (c:225) is a pointer alias that still points at
8922 // the redirection operator's lexeme after the `zshlex()` below moves on to
8923 // a token that carries no text (EOF/NEWLIN), which is why zsh reports
8924 // "parse error near `>'" and not a bare "parse error". This port derives
8925 // yyerror's token text from `tokstr`/`tokstrings[tok]`, both of which are
8926 // empty for those tokens, so the lexeme has to be carried by hand.
8927 let redir_op_text: Option<String> = {
8928 let i = tok() as usize;
8929 if i < crate::ported::lex::tokstrings.len() {
8930 crate::ported::lex::tokstrings[i].map(|t| t.to_string())
8931 } else {
8932 None
8933 }
8934 };
8935 // c:2241 — `zshlex();`
8936 zshlex();
8937 // c:2242-2243 — `if (tok != STRING && tok != ENVSTRING) YYERROR(ecused);`
8938 if tok() != STRING_LEX && tok() != ENVSTRING {
8939 set_incmdpos(oldcmdpos);
8940 set_nocorrect(oldnc);
8941 // c:2243 — `YYERROR(ecused)`, which is
8942 // `{ tok = LEXERR; ecused = (O); return 0; }` (c:87). It prints
8943 // NOTHING: the diagnostic comes from `yyerror` (c:2733) once the
8944 // parse unwinds, as "parse error near `X'" built from the offending
8945 // token. The port raised its own `zerr("expected word after
8946 // redirection")` instead — a message with no counterpart in zsh — so
8947 // `echo >`, `echo <>`, `cat <<` and friends reported
8948 // zshrs: zsh:1: expected word after redirection
8949 // where zsh reports
8950 // zsh:1: parse error near `>'
8951 if crate::ported::lex::tokstr().is_none() {
8952 crate::ported::lex::set_tokstr(redir_op_text);
8953 }
8954 set_tok(LEXERR);
8955 return 0;
8956 }
8957 // c:2244 — `incmdpos = oldcmdpos;`
8958 set_incmdpos(oldcmdpos);
8959 // c:2245 — `nocorrect = oldnc;`
8960 set_nocorrect(oldnc);
8961
8962 // c:2248-2249 — `if (fd1 == -1) fd1 = IS_READFD(type) ? 0 : 1;`
8963 let fd1 = if fd1 == -1 {
8964 if is_readfd(r#type) {
8965 0
8966 } else {
8967 1
8968 }
8969 } else {
8970 fd1
8971 };
8972
8973 // c:2251 — `name = tokstr;`
8974 name = tokstr().unwrap_or_default();
8975
8976 // c:2253-2321 — switch on type:
8977 match r#type {
8978 // c:2254-2300 — REDIR_HEREDOC / REDIR_HEREDOCDASH
8979 x if x == REDIR_HEREDOC || x == REDIR_HEREDOCDASH => {
8980 // c:2257 — `struct heredocs **hd;`
8981 // c:2258 — `int htype = type;`
8982 let htype = r#type;
8983 // c:2260-2261 — `if (strchr(tokstr, '\n')) YYERROR(ecused);`
8984 if name.contains('\n') {
8985 YYERROR!(ECUSED.get());
8986 }
8987 // c:2263-2273 — `ncodes = 5; if (idstring) { type |= MASK; ncodes = 6; }`
8988 if idstring.is_some() {
8989 r#type |= REDIR_VARID_MASK;
8990 ncodes = 6;
8991 } else {
8992 ncodes = 5;
8993 }
8994 // c:2277 — `ecispace(r, ncodes);`
8995 ecispace(r, ncodes);
8996 // c:2278 — `*rp = r + ncodes;`
8997 *rp = r + ncodes;
8998 // c:2279 — `ecbuf[r] = WCB_REDIR(type | REDIR_FROM_HEREDOC_MASK);`
8999 ECBUF.with_borrow_mut(|b| {
9000 b[r] = WCB_REDIR((r#type | REDIR_FROM_HEREDOC_MASK) as wordcode);
9001 // c:2280 — `ecbuf[r + 1] = fd1;`
9002 b[r + 1] = fd1 as wordcode;
9003 });
9004 // c:2282-2286 — r+2..4 are filled later by setheredoc.
9005 // c:2287-2288 — `if (idstring) ecbuf[r + 5] = ecstrcode(idstring);`
9006 if let Some(id) = idstring {
9007 let coded = ecstrcode(id);
9008 ECBUF.with_borrow_mut(|b| {
9009 b[r + 5] = coded;
9010 });
9011 }
9012 // c:2290-2296 — `for (hd = &hdocs; *hd; hd = &(*hd)->next);
9013 // *hd = zalloc(sizeof(struct heredocs));
9014 // (*hd)->next = NULL;
9015 // (*hd)->type = htype;
9016 // (*hd)->pc = r;
9017 // (*hd)->str = tokstr;`
9018 HDOCS.with_borrow_mut(|head| {
9019 let mut cur = head;
9020 while cur.is_some() {
9021 cur = &mut cur.as_mut().unwrap().next; // c:2290
9022 }
9023 *cur = Some(Box::new(crate::ported::zsh_h::heredocs {
9024 // c:2292-2296
9025 next: None,
9026 typ: htype,
9027 pc: r as i32,
9028 str: Some(name.clone()),
9029 }));
9030 });
9031 // c:2298 — `zshlex();`
9032 zshlex();
9033 // c:2299 — `return ncodes;`
9034 return ncodes as i32;
9035 }
9036 // c:2301-2308 — REDIR_WRITE / REDIR_WRITENOW
9037 x if x == REDIR_WRITE || x == REDIR_WRITENOW => {
9038 // c:2303-2305 — `if (tokstr[0] == OutangProc && tokstr[1] == Inpar)
9039 // type = REDIR_OUTPIPE;`
9040 let nb: Vec<char> = name.chars().collect();
9041 if nb.len() >= 2 && nb[0] == '\u{e196}' && nb[1] == '\u{e188}' {
9042 r#type = REDIR_OUTPIPE;
9043 } else if nb.len() >= 2 && nb[0] == '\u{e194}' && nb[1] == '\u{e188}' {
9044 // c:2306-2307 — `else if (tokstr[0] == Inang && tokstr[1] == Inpar) YYERROR;`
9045 YYERROR!(ECUSED.get());
9046 }
9047 }
9048 // c:2309-2315 — REDIR_READ
9049 x if x == REDIR_READ => {
9050 let nb: Vec<char> = name.chars().collect();
9051 if nb.len() >= 2 && nb[0] == '\u{e194}' && nb[1] == '\u{e188}' {
9052 r#type = REDIR_INPIPE;
9053 } else if nb.len() >= 2 && nb[0] == '\u{e196}' && nb[1] == '\u{e188}' {
9054 YYERROR!(ECUSED.get());
9055 }
9056 }
9057 // c:2316-2320 — REDIR_READWRITE
9058 x if x == REDIR_READWRITE => {
9059 let nb: Vec<char> = name.chars().collect();
9060 if nb.len() >= 2 && (nb[0] == '\u{e194}' || nb[0] == '\u{e196}') && nb[1] == '\u{e188}' {
9061 r#type = if nb[0] == '\u{e194}' {
9062 REDIR_INPIPE
9063 } else {
9064 REDIR_OUTPIPE
9065 };
9066 }
9067 }
9068 _ => {}
9069 }
9070 // c:2322 — `zshlex();`
9071 zshlex();
9072
9073 // c:2326-2333 — `if (idstring) { type |= MASK; ncodes = 4; } else ncodes = 3;`
9074 if idstring.is_some() {
9075 r#type |= REDIR_VARID_MASK;
9076 ncodes = 4;
9077 } else {
9078 ncodes = 3;
9079 }
9080
9081 // c:2334 — `ecispace(r, ncodes);`
9082 ecispace(r, ncodes);
9083 // c:2335 — `*rp = r + ncodes;`
9084 *rp = r + ncodes;
9085 // c:2336 — `ecbuf[r] = WCB_REDIR(type);`
9086 let coded_name = ecstrcode(&name);
9087 ECBUF.with_borrow_mut(|b| {
9088 b[r] = WCB_REDIR(r#type as wordcode);
9089 // c:2337 — `ecbuf[r + 1] = fd1;`
9090 b[r + 1] = fd1 as wordcode;
9091 // c:2338 — `ecbuf[r + 2] = ecstrcode(name);`
9092 b[r + 2] = coded_name;
9093 });
9094 // c:2339-2340 — `if (idstring) ecbuf[r + 3] = ecstrcode(idstring);`
9095 if let Some(id) = idstring {
9096 let coded_id = ecstrcode(id);
9097 ECBUF.with_borrow_mut(|b| {
9098 b[r + 3] = coded_id;
9099 });
9100 }
9101 // c:2342 — `return ncodes;`
9102 ncodes as i32
9103}
9104
9105/// Port of `IS_READFD` from `Src/zsh.h:407` — C decl `#define IS_READFD(X) (((X)>=REDIR_READWRITE && (X)<=REDIR_MERGEIN) || (X)==REDIR_INPIPE)`. Rust fn `is_readfd`. DIVERGENCE: the Rust port covers the `REDIR_READWRITE..REDIR_MERGEIN` range but omits C's `|| (X)==REDIR_INPIPE` arm.
9106/// default fd (0 for read-ish, 1 for write-ish) when none specified.
9107fn is_readfd(t: i32) -> bool {
9108 matches!(
9109 t,
9110 x if x == REDIR_READ
9111 || x == REDIR_READWRITE
9112 || x == REDIR_MERGEIN
9113 || x == REDIR_HEREDOC
9114 || x == REDIR_HEREDOCDASH
9115 || x == REDIR_HERESTR
9116 )
9117}
9118
9119/// !!! WARNING: RUST-ONLY HELPER !!!
9120///
9121/// No function of this name exists in `Src/parse.c`. This helper stands in
9122/// for the AST-pipeline whole-program entry. C's counterparts `parse_event`
9123/// (c:614) and `par_event` (c:635) are ported separately; this one loops
9124/// `parse_program_until` with no end-token sentinel.
9125/// Parse a program (list of lists)
9126/// Parse a complete program (top-level entry). Calls
9127/// parse_program_until with no end-token sentinel. Direct port of
9128/// zsh/Src/parse.c:614-720 `parse_event` / `par_list` /
9129/// `par_event` flow. C distinguishes COND_EVENT (single command
9130/// for here-string) from full event parse; zshrs's parse_program
9131/// is the full-event entry.
9132fn parse_program() -> ZshProgram {
9133 parse_program_until(None, false)
9134}
9135
9136/// !!! WARNING: RUST-ONLY HELPER !!!
9137///
9138/// No function of this name exists in `Src/parse.c`. This helper stands in
9139/// for the AST-pipeline event loop modelled on the body of `par_event`
9140/// (c:635-695); `single_event` reproduces C's `endtok == ENDINPUT` top-level
9141/// mode.
9142/// Parse a program until we hit an end token
9143/// Parse a program until one of `end_tokens` is seen (or EOF).
9144/// Drives par_list in a loop. C equivalent: the body of par_event
9145/// (parse.c:635-695) iterating par_list against the lexer.
9146///
9147/// `single_event` mirrors C par_event's `endtok == ENDINPUT` top-level
9148/// mode: when true, parse ONE event (the sublists `;`/`&`-chained on one
9149/// logical line, including any multi-line compound) and STOP at the
9150/// terminating top-level newline WITHOUT reading the next line — so
9151/// loop() executes the event then re-reads for the next, interleaving
9152/// input with execution exactly as zsh does. Whole-program and
9153/// nested-body parses pass false (multi-list to EOF / end token).
9154fn parse_program_until(end_tokens: Option<&[lextok]>, single_event: bool) -> ZshProgram {
9155 let mut lists = Vec::new();
9156
9157 loop {
9158 // Skip separators
9159 while tok() == SEPER || tok() == NEWLIN {
9160 // !!! DASH-FAITHFUL GATE (no C counterpart) !!! dash's grammar
9161 // takes one `;` after a command and none before it: a leading or
9162 // doubled `;` is `Syntax error: ";" unexpected`. Newlines may repeat.
9163 if tok() == SEPER
9164 && crate::dash_mode::dash_faithful()
9165 && crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) == 0
9166 {
9167 zerr("Syntax error: \";\" unexpected");
9168 set_tok(LEXERR);
9169 break;
9170 }
9171 zshlex();
9172 }
9173
9174 if tok() == ENDINPUT {
9175 break;
9176 }
9177 if tok() == LEXERR {
9178 // c:Src/parse.c:671-680 par_event — when the lexer
9179 // returned LEXERR (e.g. unbalanced `$((1+(2))` math
9180 // sub, unterminated string, etc.), C emits `yyerror(1)`
9181 // and sets errflag so the script aborts with a parse
9182 // error diagnostic + non-zero exit. zshrs's
9183 // parse_program_until previously just `break`'d on
9184 // LEXERR, silently swallowing the malformed input and
9185 // exiting rc=0 — so `$((1+(2))` ran as if it were
9186 // empty. Bug #529 in docs/BUGS.md. Emit yyerror
9187 // mirroring the C behaviour; the broken script then
9188 // surfaces the parse error to the caller.
9189 // c:Src/parse.c — empty-msg yyerror call mapped to the
9190 // C-faithful `yyerror(0)` (zshrs's previous shape).
9191 yyerror(0);
9192 break;
9193 }
9194
9195 // Check for end tokens
9196 if let Some(end_toks) = end_tokens {
9197 if end_toks.contains(&tok()) {
9198 break;
9199 }
9200 }
9201
9202 // Also stop at these tokens when not explicitly looking for them
9203 // Note: Else/Elif/Then are NOT here - they're handled by par_if
9204 // to allow nested if statements inside case arms, loops, etc.
9205 //
9206 // c:Src/parse.c:par_event — when an orphan terminator (DONE
9207 // outside a loop, FI outside an if, ESAC outside a case)
9208 // appears at the top level (end_tokens=None), C errors via
9209 // YYERROR. zshrs's `break` silently accepted `done`/`fi`/
9210 // `esac` as no-op input. Error at the outermost call so
9211 // unscoped terminators don't sneak through; nested calls
9212 // still break cleanly via the end_tokens contains-check
9213 // above.
9214 match tok() {
9215 DONE | FI | ESAC | DOLOOP | ZEND if end_tokens.is_none() => {
9216 // c:Src/parse.c:par_event — emit the specific token
9217 // name (`done`, `fi`, `esac`, `do`) so error-parsing
9218 // tools can identify the unmatched terminator. C zsh
9219 // writes `parse error near \`<tok>'`; the Rust port
9220 // was emitting a generic "orphan terminator" string.
9221 // Bug #142, #413.
9222 let name = match tok() {
9223 DONE => "done",
9224 FI => "fi",
9225 ESAC => "esac",
9226 DOLOOP => "do",
9227 // c:Src/parse.c:1184-1190 — `end` closes a loop
9228 // body ONLY under `csh || isset(CSHJUNKIELOOPS)`.
9229 // With the option off the short-loop form
9230 // (par_save_list1, c:1193) consumes one command
9231 // and leaves ZEND in command position, where
9232 // par_event errors. zshrs broke out silently, so
9233 // `unsetopt cshjunkieloops; for f in a b; print
9234 // $f; end` ran and exited 0 instead of the
9235 // expected "parse error near `end'" + status 1
9236 // (E01options.ztst:13).
9237 ZEND => "end",
9238 _ => "orphan terminator",
9239 };
9240 zerr(&format!("parse error near `{}'", name));
9241 break;
9242 }
9243 DSEMI | SEMIAMP | SEMIBAR if end_tokens.is_none() => {
9244 // c:Src/parse.c:par_event — case-arm terminators
9245 // (`;;`, `;&`, `;|`) outside a case construct are a
9246 // parse error. zshrs's `break` silently accepted them
9247 // at top level, truncating the rest of the script.
9248 // Bug #141 in docs/BUGS.md.
9249 let name = match tok() {
9250 DSEMI => ";;",
9251 SEMIAMP => ";&",
9252 SEMIBAR => ";|",
9253 _ => "case terminator",
9254 };
9255 zerr(&format!("parse error near `{}'", name));
9256 break;
9257 }
9258 OUTBRACE_TOK if end_tokens.is_none() => {
9259 // c:Src/parse.c:par_event — orphan `}` (no matching
9260 // `{` opener) at top level is a parse error. zshrs's
9261 // generic break swallowed it silently, leaving the
9262 // `echo a` in `echo a }` running and ignoring the
9263 // stray brace. Bug #168 in docs/BUGS.md.
9264 zerr("parse error near `}'");
9265 break;
9266 }
9267 OUTBRACE_TOK | DSEMI | SEMIAMP | SEMIBAR | DONE | FI | ESAC | ZEND => break,
9268 _ => {}
9269 }
9270
9271 match par_list(single_event) {
9272 Some((list, terminated)) => {
9273 let detected = simple_name_with_inoutpar(&list);
9274 let was_detected = detected.is_some();
9275 lists.push(list);
9276 // Synthesize a FuncDef for the `name() { body }` shape
9277 // at parse time so body_source is captured while the
9278 // lexer still has the input. The lexer port emits
9279 // `name(` as a single Word ending in `<Inpar><Outpar>`,
9280 // so the Simple list is followed by an Inbrace once
9281 // separators are skipped. For `name() cmd args` the
9282 // body has already been swallowed into the same
9283 // Simple's words tail — synthesize directly from there.
9284 if let Some((names, body_argv)) = detected {
9285 if !body_argv.is_empty() {
9286 // One-line body already in the Simple. Build
9287 // a Simple from body_argv as the function body.
9288 lists.pop();
9289 let body_simple = ZshCommand::Simple(ZshSimple {
9290 assigns: Vec::new(),
9291 words: body_argv,
9292 redirs: Vec::new(),
9293 typeset_reswd: false,
9294 via_alias: None,
9295 });
9296 let body_list = ZshList {
9297 sublist: ZshSublist {
9298 pipe: ZshPipe {
9299 cmd: body_simple,
9300 next: None,
9301 lineno: lineno(),
9302 merge_stderr: false,
9303 },
9304 next: None,
9305 flags: SublistFlags::default(),
9306 },
9307 flags: ListFlags::default(),
9308 };
9309 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
9310 names,
9311 body: Box::new(ZshProgram {
9312 lists: vec![body_list],
9313 }),
9314 tracing: false,
9315 keyword_form: false,
9316 auto_call_args: None,
9317 body_source: None,
9318 });
9319 let synthetic = ZshList {
9320 sublist: ZshSublist {
9321 pipe: ZshPipe {
9322 cmd: funcdef,
9323 next: None,
9324 lineno: lineno(),
9325 merge_stderr: false,
9326 },
9327 next: None,
9328 flags: SublistFlags::default(),
9329 },
9330 flags: ListFlags::default(),
9331 };
9332 lists.push(synthetic);
9333 continue;
9334 }
9335 // Else: words.len() == 1 (only the trailing `name()`
9336 // word), brace body follows. `names` may carry
9337 // multiple identifiers from the `fna fnb fnc()`
9338 // shorthand — all share the same brace body per
9339 // src/zsh/Src/parse.c:1666 par_funcdef wordlist.
9340 // Skip separators on the real lexer; safe because
9341 // parse_program's next iteration would also skip them.
9342 while tok() == SEPER || tok() == NEWLIN {
9343 zshlex();
9344 }
9345 if tok() == INBRACE_TOK {
9346 // Capture body_start BEFORE the lexer
9347 // advances past the first body token. The
9348 // outer zshlex() consumed `{`; lexer.pos
9349 // is now right after `{`. The next
9350 // `zshlex()` would advance past `echo`,
9351 // making body_start land mid-body and
9352 // lose the first word — `typeset -f f`
9353 // printed `a; echo b` instead of
9354 // `echo a; echo b` for `f() { echo a;
9355 // echo b }`.
9356 let body_mark = crate::funcdef_capture::body_mark_begin();
9357 zshlex();
9358 // c:Src/parse.c — synth funcdef body terminates
9359 // at OUTBRACE_TOK. Explicit end-token avoids
9360 // the top-level stray-`}` arm. Bug #167/#168.
9361 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9362 // Bug #642 family: slice THROUGH pos() so the funcdef
9363 // `}` is always inside the slice, then strip that
9364 // single trailing brace (+ the `;`/`\n` separator
9365 // before it). The prior `pos()-1` form left the `}`
9366 // in `body_source` for the inline `name() { body }`
9367 // shape (this arm) — so `${functions[name]}` returned
9368 // `body }` and `.zinit-diff-functions`'
9369 // `${(qk)functions[@]}` re-parsed it into a
9370 // `parse error near '}'` on every function. Match the
9371 // canonical strip used by the other two funcdef arms
9372 // (parse.rs:2457 / 9528).
9373 let body_source = crate::funcdef_capture::body_text(body_mark)
9374 .map(|s| {
9375 let t = s.trim();
9376 let t = t.strip_suffix('}').unwrap_or(t).trim_end();
9377 let t = t
9378 .trim_end_matches(|c: char| c == ';' || c == '\n')
9379 .trim_end();
9380 t.to_string()
9381 })
9382 .filter(|s| !s.is_empty());
9383 // c:Src/parse.c par_funcdef bakes alias expansion into the wordcode; see
9384 // vm_helper::funcdef_note_resolved_body.
9385 crate::vm_helper::funcdef_note_resolved_body(body_source.as_deref());
9386 if tok() == OUTBRACE_TOK {
9387 zshlex();
9388 }
9389 // Replace the Simple list with a FuncDef list.
9390 lists.pop();
9391 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
9392 names,
9393 body: Box::new(body),
9394 tracing: false,
9395 keyword_form: false,
9396 auto_call_args: None,
9397 body_source,
9398 });
9399 let synthetic = ZshList {
9400 sublist: ZshSublist {
9401 pipe: ZshPipe {
9402 cmd: funcdef,
9403 next: None,
9404 lineno: lineno(),
9405 merge_stderr: false,
9406 },
9407 next: None,
9408 flags: SublistFlags::default(),
9409 },
9410 flags: ListFlags::default(),
9411 };
9412 lists.push(synthetic);
9413 } else if !matches!(tok(), ENDINPUT | OUTBRACE_TOK | SEPER | NEWLIN) {
9414 // No-brace one-line body: `foo() echo hello`.
9415 // Parse a single command for the body.
9416 let body_cmd = par_cmd(false);
9417 if let Some(cmd) = body_cmd {
9418 let body_list = ZshList {
9419 sublist: ZshSublist {
9420 pipe: ZshPipe {
9421 cmd,
9422 next: None,
9423 lineno: lineno(),
9424 merge_stderr: false,
9425 },
9426 next: None,
9427 flags: SublistFlags::default(),
9428 },
9429 flags: ListFlags::default(),
9430 };
9431 lists.pop();
9432 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
9433 names: names.clone(),
9434 body: Box::new(ZshProgram {
9435 lists: vec![body_list],
9436 }),
9437 tracing: false,
9438 keyword_form: false,
9439 auto_call_args: None,
9440 body_source: None,
9441 });
9442 let synthetic = ZshList {
9443 sublist: ZshSublist {
9444 pipe: ZshPipe {
9445 cmd: funcdef,
9446 next: None,
9447 lineno: lineno(),
9448 merge_stderr: false,
9449 },
9450 next: None,
9451 flags: SublistFlags::default(),
9452 },
9453 flags: ListFlags::default(),
9454 };
9455 lists.push(synthetic);
9456 }
9457 }
9458 }
9459 // c:769-803 + c:644-682 — lists only chain across
9460 // explicit SEPER/AMPER/AMPERBANG. When par_list ended
9461 // WITHOUT a terminator, C's grammar says the list is
9462 // over: nested contexts (while-cond, brace bodies)
9463 // return to the caller with the dangling token (so
9464 // `while {false} {print no}` hands the second `{` to
9465 // par_while as the loop body), and the TOP level
9466 // (par_event, c:671-680) yyerrors — `{false} {print
9467 // no}` is `parse error near \`{'` in zsh. The funcdef
9468 // synthesis branch above manages its own following
9469 // tokens (`f() { body }` legitimately has INBRACE
9470 // right after the Simple), so it is exempt.
9471 if !was_detected && !terminated {
9472 match tok() {
9473 // End-of-input / lex error: loop top handles.
9474 ENDINPUT | LEXERR => {}
9475 // Construct closers and orphan terminators:
9476 // the loop-top match already errors/breaks
9477 // for these with the right diagnostics.
9478 OUTBRACE_TOK | DSEMI | SEMIAMP | SEMIBAR | DONE | FI | ESAC | ZEND
9479 | DOLOOP | ELSE | ELIF | THEN => {}
9480 t if end_tokens.map_or(false, |e| e.contains(&t)) => {}
9481 _ if end_tokens.is_some() => {
9482 // Nested list context: C par_list just
9483 // ends; the construct parser (par_while
9484 // body dispatch, par_subsh closer check)
9485 // deals with the token.
9486 break;
9487 }
9488 offending => {
9489 // Top level: C par_event yyerror(1) +
9490 // errflag, c:671-682. Same tokstr
9491 // injection as the None arm below so the
9492 // "near `X'" tail survives punctuation
9493 // tokens with no tokstr.
9494 if crate::ported::lex::tokstr().is_none() {
9495 let i = offending as usize;
9496 if i < crate::ported::lex::tokstrings.len() {
9497 if let Some(s) = crate::ported::lex::tokstrings[i] {
9498 crate::ported::lex::set_tokstr(Some(s.to_string()));
9499 }
9500 }
9501 }
9502 set_tok(LEXERR); // c:672
9503 yyerror(1);
9504 let noerrs_v = *crate::ported::utils::noerrs_lock().lock().unwrap();
9505 if noerrs_v != 2 {
9506 errflag.fetch_or(
9507 crate::ported::zsh_h::ERRFLAG_ERROR,
9508 Ordering::SeqCst,
9509 );
9510 }
9511 break;
9512 }
9513 }
9514 }
9515 }
9516 None => {
9517 // c:Src/parse.c:644-645 par_event — `if (tok ==
9518 // ENDINPUT) return 0;`. End-of-input is NORMAL — no
9519 // diagnostic. The yyerror path at c:670-682 fires only
9520 // when par_sublist actually failed (the C `!r` branch).
9521 //
9522 // zshrs's previous fix here unconditionally called
9523 // yyerror on every None return, breaking legitimate
9524 // end-of-input scenarios like `(echo sub)` (par_list
9525 // returns None after the subshell parser consumes the
9526 // whole construct, leaving tok at ENDINPUT).
9527 if tok() == ENDINPUT {
9528 break;
9529 }
9530 // c:Src/parse.c:671-680 par_event — par_sublist failed:
9531 // emit the canonical yyerror with the "near `X'" tail
9532 // derived from zshlextext/tokstr().
9533 //
9534 // c:Src/lex.c:1965 — `zshlextext = tokstrings[tok]`
9535 // is set DURING zshlex, before set_tok(LEXERR) here.
9536 // zshrs's zshlex doesn't update LEX_TOKSTR for
9537 // single-char punctuation tokens (OUTPAR/INPAR/etc.),
9538 // so by the time yyerror runs, tokstr() is None and
9539 // the tail "near `)'" is lost. Inject the current
9540 // tok's canonical text into LEX_TOKSTR here so the
9541 // C-faithful yyerror lookup finds it. Mirrors the
9542 // visible effect of C's zshlextext fallback.
9543 let already_flagged =
9544 (errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR) != 0;
9545 let offending_tok = tok();
9546 if crate::ported::lex::tokstr().is_none() {
9547 let i = offending_tok as usize;
9548 if i < crate::ported::lex::tokstrings.len() {
9549 if let Some(s) = crate::ported::lex::tokstrings[i] {
9550 crate::ported::lex::set_tokstr(Some(s.to_string()));
9551 }
9552 }
9553 }
9554 set_tok(LEXERR); // c:672
9555 yyerror(if already_flagged { 0 } else { 1 });
9556 // c:Src/parse.c:679-680 — `if (noerrs != 2) errflag |=
9557 // ERRFLAG_ERROR;`. C sets errflag explicitly after the
9558 // yyerror(1) print-only branch. Without this, the
9559 // caller (`execute_script_zsh_pipeline` here, `bin_eval`
9560 // for the parse_string path) can't distinguish "no
9561 // parse error" from "parse error already printed", so
9562 // $? stays at 0 after `eval ')foo'`.
9563 let noerrs_v = *crate::ported::utils::noerrs_lock().lock().unwrap();
9564 if noerrs_v != 2 {
9565 errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::SeqCst);
9566 }
9567 break;
9568 }
9569 }
9570
9571 // c:Src/parse.c par_event — single-event (top-level, endtok ==
9572 // ENDINPUT) mode stops at the terminating top-level newline. The
9573 // SEPER par_list arm above left `tok` AT the newline-SEPER (not
9574 // consumed, LEX_ISNEWLIN > 0), so break before the loop-top
9575 // separator skip would `zshlex()` past it and read the next
9576 // line. A `;`-SEPER (LEX_ISNEWLIN <= 0) or `&` was consumed by
9577 // par_list and leaves a different token here, so chaining keeps
9578 // looping (same logical line). Multi-line compounds were already
9579 // fully consumed inside par_list, so their internal newlines
9580 // never reach this check.
9581 if single_event && tok() == SEPER && crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) > 0
9582 {
9583 break;
9584 }
9585 }
9586
9587 ZshProgram { lists }
9588}
9589
9590/// !!! WARNING: RUST-ONLY HELPER !!!
9591///
9592/// No function of this name exists in `Src/parse.c`. This helper stands in
9593/// for the ENVSTRING / ENVARRAY assignment arms that C handles inline inside
9594/// `par_simple` (c:1900-1998).
9595/// Parse an assignment
9596/// Parse an assignment word `NAME=value` or `NAME=(arr items)`.
9597/// Sub-routine of par_simple. The C source handles assignments
9598/// inline in par_simple via the ENVSTRING/ENVARRAY token paths
9599/// (parse.c:1842-2000ish); zshrs splits it out to a dedicated
9600/// helper for clarity.
9601fn parse_assign() -> Option<ZshAssign> {
9602 // Helper: locate the `=` that delimits NAME from VALUE — the scan
9603 // par_simple's ENVSTRING arm runs (c:Src/parse.c:1851-1865):
9604 //
9605 // for (ptr = tokstr;
9606 // *ptr && *ptr != Inbrack && *ptr != '=' && *ptr != '+';
9607 // ptr++);
9608 // if (*ptr == Inbrack) skipparens(Inbrack, Outbrack, &ptr);
9609 // if (*ptr == '+') *ptr++ = '\0';
9610 // if (*ptr == '=') { *ptr = '\0'; str = ptr + 1; }
9611 // else equalsplit(tokstr, &str);
9612 //
9613 // Only the lexer's Inbrack/Outbrack TOKENS delimit the subscript, so a
9614 // quoted or backslashed bracket inside it is content. The previous
9615 // depth walk counted raw `[`/`(`/`{` as well, so a quoted one in the
9616 // key (`pc['\[']=…`, `pc['(']=…` — Functions/Prompts/prompt_clint_setup)
9617 // never returned to depth 0, the assignment was dropped and the next
9618 // token was a parse error. zshrs's lexer spells the assignment `=` as
9619 // the Equals token where C keeps it raw, so both spellings count.
9620 fn find_assign_equals(s: &str) -> Option<usize> {
9621 use crate::ported::zsh_h::{Inbrack, Outbrack};
9622 let is_eq = |c: char| c == '=' || c == Equals;
9623 // c:1851-1853
9624 let mut ptr = s
9625 .char_indices()
9626 .find(|&(_, c)| c == Inbrack || c == '+' || is_eq(c))
9627 .map_or(s.len(), |(i, _)| i);
9628 // c:1854 — `if (*ptr == Inbrack) skipparens(Inbrack, Outbrack, &ptr);`
9629 if s[ptr..].starts_with(Inbrack) {
9630 let mut cursor = &s[ptr..];
9631 crate::ported::utils::skipparens(Inbrack, Outbrack, &mut cursor);
9632 ptr = s.len() - cursor.len();
9633 }
9634 // c:1855-1857 — `+=`
9635 if s[ptr..].starts_with('+') {
9636 ptr += 1;
9637 }
9638 // c:1861-1863
9639 if s[ptr..].chars().next().is_some_and(is_eq) {
9640 return Some(ptr);
9641 }
9642 // c:1865 — `equalsplit(tokstr, &str)` (c:Src/utils.c:4133): the
9643 // first `=` anywhere.
9644 s.char_indices().find(|&(_, c)| is_eq(c)).map(|(i, _)| i)
9645 }
9646
9647 let _ts_tokstr = tokstr()?;
9648 let tokstr = _ts_tokstr.as_str();
9649
9650 // Parse name=value or name+=value.
9651 let (name, value_str, append) = if tok() == ENVARRAY {
9652 let (name, append) = if let Some(stripped) = tokstr.strip_suffix('+') {
9653 (stripped, true)
9654 } else {
9655 (tokstr, false)
9656 };
9657 (name.to_string(), String::new(), append)
9658 } else if let Some(pos) = find_assign_equals(tokstr) {
9659 let name_part = &tokstr[..pos];
9660 let (name, append) = if let Some(stripped) = name_part.strip_suffix('+') {
9661 (stripped, true)
9662 } else {
9663 (name_part, false)
9664 };
9665 (
9666 name.to_string(),
9667 tokstr[pos + Equals.len_utf8()..].to_string(),
9668 append,
9669 )
9670 } else if let Some(pos) = tokstr.find('=') {
9671 // Fallback to literal '=' for compatibility
9672 let name_part = &tokstr[..pos];
9673 let (name, append) = if let Some(stripped) = name_part.strip_suffix('+') {
9674 (stripped, true)
9675 } else {
9676 (name_part, false)
9677 };
9678 (name.to_string(), tokstr[pos + 1..].to_string(), append)
9679 } else {
9680 return None;
9681 };
9682
9683 let value = if tok() == ENVARRAY {
9684 // Array assignment: name=(...)
9685 // c:Src/parse.c:1895 par_simple ENVARRAY arm:
9686 // `int oldcmdpos = incmdpos; ... incmdpos = 0; ... zshlex();`
9687 // Reset incmdpos to false BEFORE the array body's first lex so
9688 // a leading `{...}` (brace expansion) doesn't trip the
9689 // empty-buf+incmdpos rule at lex.c:1141 that returns `{` as
9690 // STRING and lets the reswd_lookup promote it to INBRACE_TOK.
9691 let oldcmdpos = crate::ported::lex::incmdpos();
9692 crate::ported::lex::set_incmdpos(false);
9693 // c:Src/parse.c:2041-2046 — `intypeset = 0;` BEFORE reading
9694 // the array body, restored to the saved value after. Without
9695 // this, an element containing `=` (`typeset -a w=( VAR=1
9696 // sudo )`) re-triggers the lexer's intypeset assignment
9697 // detection and lexes `VAR=1` as ENVSTRING instead of a plain
9698 // STRING element — the body loop ends early on the non-STRING
9699 // token and the parser errors "near `)'".
9700 let old_intypeset = crate::ported::lex::intypeset();
9701 crate::ported::lex::set_intypeset(false);
9702 let mut elements = Vec::new();
9703 zshlex(); // skip past token
9704
9705 // c:Src/parse.c:2043 par_nl_wordlist — read array elements until the
9706 // token is no longer a word/separator (the closing `)` ends it). zsh
9707 // imposes NO element-count limit; arrays grow dynamically. A former
9708 // `MAX_ARRAY_ELEMENTS = 10_000` cap here silently truncated (and
9709 // errored on) any larger literal — which broke loading `.zcompdump`
9710 // (`_comps`/`_lastcomp` hold tens of thousands of entries with
9711 // zsh-more-completions) and any big generated array. The loop
9712 // terminates on the closing paren via zshlex advancing each pass.
9713 while matches!(tok(), STRING_LEX | SEPER | NEWLIN) {
9714 if tok() == STRING_LEX {
9715 let _ts_s = crate::ported::lex::tokstr();
9716 if let Some(s) = _ts_s.as_deref() {
9717 elements.push(s.to_string());
9718 }
9719 }
9720 zshlex();
9721 }
9722 // c:Src/parse.c — `incmdpos = oldcmdpos;` (restore at end of arm)
9723 crate::ported::lex::set_incmdpos(oldcmdpos);
9724 // c:Src/parse.c:2046 — `intypeset = 1;` (restore for any
9725 // follow-on `b=( … )` in `typeset a=(…) b=(…)`).
9726 crate::ported::lex::set_intypeset(old_intypeset);
9727
9728 // The closing Outpar is consumed here. The outer par_simple
9729 // loop will then `zshlex()` past whatever follows (typically
9730 // a separator or the next word) — calling zshlex twice in
9731 // tandem (here AND in par_simple) over-advances and merges
9732 // a following `name() { … }` funcdef into the same Simple.
9733 // We only consume Outpar; let the caller handle the rest.
9734 // Without this guard `g=(o1); f() { :; }` parsed as one
9735 // Simple with assigns=[g] and words=["f()"] (one token).
9736 // c:Src/parse.c:1904-1905 / c:2048-2049 — `if (tok != OUTPAR)
9737 // YYERROR(oecused);`. The element loop stops at any other token, so
9738 // without this `x=(q()` (INOUTPAR) assigned `q` and ran on.
9739 if tok() != OUTPAR_TOK {
9740 yyerror(0);
9741 return None;
9742 }
9743 if tok() == OUTPAR_TOK {
9744 // Note: do NOT zshlex() here. par_simple's `lexer
9745 // .zshlex()` after `parse_assign` returns advances past
9746 // the Outpar onto the next significant token.
9747 //
9748 // Force `incmdpos=true` so the next zshlex() recognizes
9749 // a follow-up `b=(...)` / `b=val` as Envarray/Envstring.
9750 // The lexer flips incmdpos to false on bare Outpar (which
9751 // is correct for subshell-close context), but for an
9752 // array-assignment close more assigns/words may follow.
9753 set_incmdpos(true);
9754 }
9755
9756 ZshAssignValue::Array(elements)
9757 } else {
9758 ZshAssignValue::Scalar(value_str)
9759 };
9760
9761 Some(ZshAssign {
9762 name,
9763 value,
9764 append,
9765 })
9766}
9767
9768/// Port of `par_redir` from `Src/parse.c:2229` — C decl `par_redir(int *rp, char *idstring)`. Rust fn `par_redir_with_id` is the AST variant carrying C's `char *idstring` parameter; `par_redir` at parse.rs:3096 is the NULL-idstring wrapper.
9769/// AST `par_redir` variant accepting an idstring for the
9770/// `{var}>file` brace-FD shape. C signature
9771/// `par_redir(int *rp, char *idstring)` (parse.c:2229). The
9772/// idstring is stored in the resulting ZshRedir.varid for the
9773/// executor to bind the named variable to the chosen fd.
9774fn par_redir_with_id(idstring: Option<&str>) -> Option<ZshRedir> {
9775 let varid: Option<String> = idstring.map(|s| s.to_string());
9776 let rtype = match tok() {
9777 OUTANG_TOK => REDIR_WRITE,
9778 OUTANGBANG => REDIR_WRITENOW,
9779 DOUTANG => REDIR_APP,
9780 DOUTANGBANG => REDIR_APPNOW,
9781 INANG_TOK => REDIR_READ,
9782 INOUTANG => REDIR_READWRITE,
9783 DINANG => REDIR_HEREDOC,
9784 DINANGDASH => REDIR_HEREDOCDASH,
9785 TRINANG => REDIR_HERESTR,
9786 INANGAMP => REDIR_MERGEIN,
9787 OUTANGAMP => REDIR_MERGEOUT,
9788 AMPOUTANG => REDIR_ERRWRITE,
9789 OUTANGAMPBANG => REDIR_ERRWRITENOW,
9790 DOUTANGAMP => REDIR_ERRAPP,
9791 DOUTANGAMPBANG => REDIR_ERRAPPNOW,
9792 _ => return None,
9793 };
9794
9795 let fd = if tokfd() >= 0 {
9796 tokfd()
9797 } else if matches!(
9798 rtype,
9799 REDIR_READ
9800 | REDIR_READWRITE
9801 | REDIR_MERGEIN
9802 | REDIR_HEREDOC
9803 | REDIR_HEREDOCDASH
9804 | REDIR_HERESTR
9805 ) {
9806 0
9807 } else {
9808 1
9809 };
9810
9811 // c:2234-2245 — save/restore incmdpos and nocorrect around the
9812 // zshlex that consumes the redir target word:
9813 // oldcmdpos = incmdpos; incmdpos = 0;
9814 // oldnc = nocorrect;
9815 // if (tok != INANG && tok != INOUTANG) nocorrect = 1;
9816 // ... zshlex; check tok; ...
9817 // incmdpos = oldcmdpos; nocorrect = oldnc;
9818 // Without this, a redir target lexes in the parent's incmdpos
9819 // (re-promoting `{` / reswords) AND with parent nocorrect (so
9820 // spelling-correction wrongly runs inside `> $(cmd)` etc.).
9821 let oldcmdpos = incmdpos();
9822 set_incmdpos(false);
9823 let oldnc = nocorrect();
9824 let cur = tok();
9825 if cur != INANG_TOK && cur != INOUTANG {
9826 set_nocorrect(1);
9827 }
9828 // The operator's own lexeme, kept for the YYERROR path below — see the
9829 // note in the wordcode `par_redir` above for why C does not need it.
9830 let redir_op_text: Option<String> = {
9831 let i = cur as usize;
9832 if i < crate::ported::lex::tokstrings.len() {
9833 crate::ported::lex::tokstrings[i].map(|t| t.to_string())
9834 } else {
9835 None
9836 }
9837 };
9838 zshlex();
9839
9840 let name = match tok() {
9841 STRING_LEX | ENVSTRING => {
9842 let n = tokstr().unwrap_or_default();
9843 // c:2244-2245 — restore incmdpos / nocorrect right after
9844 // the redir target word is confirmed, BEFORE the trailing
9845 // zshlex advances past it. The advance itself happens
9846 // below so REDIR_HEREDOC[DASH] can push onto HDOCS first
9847 // (matching the wordcode variant at parse.rs:6894-6908) —
9848 // otherwise the NEWLIN drained by that zshlex sees an
9849 // empty HDOCS list and gethere never collects the body.
9850 set_incmdpos(oldcmdpos);
9851 set_nocorrect(oldnc);
9852 n
9853 }
9854 _ => {
9855 set_incmdpos(oldcmdpos);
9856 set_nocorrect(oldnc);
9857 // c:2243 `YYERROR(ecused)` — see the note on the wordcode arm
9858 // above; the message is yyerror's "parse error near `X'", not a
9859 // custom one.
9860 if crate::ported::lex::tokstr().is_none() {
9861 crate::ported::lex::set_tokstr(redir_op_text);
9862 }
9863 set_tok(LEXERR);
9864 return None;
9865 }
9866 };
9867
9868 // Heredoc terminator capture. C parse.c:2254-2317 par_redir builds
9869 // a `struct heredocs` entry here for REDIR_HEREDOC[DASH]. zshrs
9870 // pushes onto HDOCS (canonical C linked list, c:2290-2296) AND
9871 // onto LEX_HEREDOCS (Rust-only AST-glue Vec carrying parsed-out
9872 // terminator/strip_tabs/quoted metadata for downstream AST
9873 // consumers). Quoted terminators (`<<'EOF'` / `<<"EOF"` / `<<\EOF`)
9874 // disable expansion in the body — Snull `\u{e19d}` marks single-quote,
9875 // Dnull `\u{e19e}` marks double-quote, Bnull `\u{e19f}` marks
9876 // backslash-escaped chars.
9877 let heredoc_idx = if matches!(rtype, REDIR_HEREDOC | REDIR_HEREDOCDASH) {
9878 let strip_tabs = rtype == REDIR_HEREDOCDASH;
9879 let quoted = name.contains('\u{e19d}')
9880 || name.contains('\u{e19e}')
9881 || name.contains('\u{e19f}')
9882 || name.starts_with('\'')
9883 || name.starts_with('"');
9884 let term = name
9885 .chars()
9886 .filter(|c| {
9887 *c != '\'' && *c != '"' && *c != '\u{e19d}' && *c != '\u{e19e}' && *c != '\u{e19f}'
9888 })
9889 .collect::<String>();
9890 // c:2290-2296 — `for (hd = &hdocs; *hd; hd = &(*hd)->next);
9891 // *hd = zalloc(sizeof(struct heredocs));
9892 // (*hd)->next = NULL;
9893 // (*hd)->type = htype;
9894 // (*hd)->pc = r;
9895 // (*hd)->str = tokstr;`
9896 // AST path has no wordcode pc to patch; use -1 sentinel so the
9897 // inline NEWLIN walk in `zshlex()` skips the setheredoc call.
9898 HDOCS.with_borrow_mut(|head| {
9899 let mut cur = head;
9900 while cur.is_some() {
9901 cur = &mut cur.as_mut().unwrap().next; // c:2290
9902 }
9903 *cur = Some(Box::new(crate::ported::zsh_h::heredocs {
9904 // c:2292-2296
9905 next: None,
9906 typ: rtype,
9907 pc: -1,
9908 str: Some(name.clone()),
9909 }));
9910 });
9911 // zshrs-only: push parallel AST-glue entry onto LEX_HEREDOCS.
9912 let idx = LEX_HEREDOCS.with_borrow_mut(|v| {
9913 v.push(HereDoc {
9914 terminator: term,
9915 strip_tabs,
9916 content: String::new(),
9917 quoted,
9918 processed: false,
9919 });
9920 v.len() - 1
9921 });
9922 Some(idx)
9923 } else {
9924 None
9925 };
9926
9927 // c:2298 (heredoc) / c:2322 (other redirs) — final zshlex() advance
9928 // past the redir target word. MUST run after the HDOCS push above
9929 // so the heredoc-drain inside this zshlex sees the new entry. For
9930 // non-heredoc forms the order is irrelevant; consolidating to a
9931 // single tail-call here matches the wordcode variant.
9932 zshlex();
9933
9934 Some(ZshRedir {
9935 rtype,
9936 fd,
9937 name,
9938 heredoc: None,
9939 varid,
9940 heredoc_idx,
9941 })
9942}
9943
9944/// !!! WARNING: RUST-ONLY HELPER !!!
9945///
9946/// No function of this name exists in `Src/parse.c`. This helper stands in
9947/// for the `if (tok == DINPAR)` c-style-for branch that C handles inline
9948/// inside `par_for` (c:1096-1140).
9949/// Parse C-style for loop: for (( init; cond; step ))
9950/// Parse the c-style `for ((init; cond; incr)) do BODY done`.
9951/// Inner branch of zsh/Src/parse.c:1100-1140 inside par_for.
9952/// Recognized when the token after FOR is DINPAR (the `((`
9953/// detected by gettok via dbparens setup).
9954fn parse_for_cstyle() -> Option<ZshCommand> {
9955 // We're at (( (Dinpar None) - the opening ((
9956 // Lexer returns:
9957 // Dinpar None - opening ((
9958 // Dinpar "init" - init expression, semicolon consumed
9959 // Dinpar "cond" - cond expression, semicolon consumed
9960 // Doutpar "step" - step expression, closing )) consumed
9961 zshlex(); // Get init: Dinpar "i=0"
9962
9963 if tok() != DINPAR {
9964 set_tok(LEXERR); // c:1100 YYERRORV(oecused)
9965 return None;
9966 }
9967 let init = tokstr().unwrap_or_default();
9968
9969 zshlex(); // Get cond: Dinpar "i<10"
9970
9971 if tok() != DINPAR {
9972 set_tok(LEXERR); // c:1104 YYERRORV(oecused)
9973 return None;
9974 }
9975 let cond = tokstr().unwrap_or_default();
9976
9977 zshlex(); // Get step: Doutpar "i++"
9978
9979 if tok() != DOUTPAR {
9980 set_tok(LEXERR); // c:1108 YYERRORV(oecused)
9981 return None;
9982 }
9983 let step = tokstr().unwrap_or_default();
9984
9985 // c:1110 — `infor = 0;` before the body opener. The companion
9986 // `incmdpos = 1;` at c:1111 is intentionally skipped here for
9987 // the same reason c:1094's `incmdpos = 0;` is skipped in
9988 // par_for above — zshrs doesn't mirror the full
9989 // incmdpos state-machine inline.
9990 set_infor(0); // c:1110
9991 zshlex(); // Move past ))
9992
9993 skip_separators();
9994 let body = parse_loop_body(false, false)?;
9995
9996 Some(ZshCommand::For(ZshFor {
9997 var: String::new(),
9998 list: ForList::CStyle { init, cond, step },
9999 body: Box::new(body),
10000 is_select: false,
10001 }))
10002}
10003
10004/// !!! WARNING: RUST-ONLY HELPER !!!
10005///
10006/// No function of this name exists in `Src/parse.c`. This helper stands in
10007/// for the SELECT arm of `par_for` (c:1087; `par_cmd` routes SELECT there at
10008/// c:983-985), which C does not split into its own function.
10009/// Parse select loop (same syntax as for)
10010/// Parse `select NAME in WORDS; do BODY; done`. Same shape as
10011/// `for NAME in WORDS; do ...` but with menu-prompt semantics in
10012/// the executor. C equivalent: the SELECT case in par_for at
10013/// parse.c:1087-1207 (selects share parser flow with foreach).
10014fn parse_select() -> Option<ZshCommand> {
10015 // `select` shares par_for's grammar (var, words, body) but the
10016 // compile path is different (interactive prompt loop).
10017 match par_for()? {
10018 ZshCommand::For(mut f) => {
10019 f.is_select = true;
10020 Some(ZshCommand::For(f))
10021 }
10022 other => Some(other),
10023 }
10024}
10025
10026/// !!! WARNING: RUST-ONLY HELPER !!!
10027///
10028/// No function of this name exists in `Src/parse.c`. This helper stands in
10029/// for the DOLOOP / INBRACE / short-form loop-body ladder that C repeats
10030/// inline in `par_for` (c:1170-1194), `par_while` (c:1536-1553) and
10031/// `par_repeat` (c:1586-1602).
10032/// Parse loop body (do...done, {...}, or shortloop)
10033/// Parse the `do BODY done` body of a for/while/until/select/
10034/// repeat loop. Direct equivalent of zsh's parse.c handling
10035/// inside the loop builders — they all consume DOLOOP, parse a
10036/// list until DONE, and return the list. The `foreach_style`
10037/// flag signals foreach (where short-form `for NAME in WORDS;
10038/// CMD` may skip do/done) vs c-style (which always requires
10039/// do/done).
10040///
10041/// `is_repeat` widens the SHORTLOOPS gate so `SHORTREPEAT` also
10042/// unlocks the short form for `repeat N CMD` (per c:1600
10043/// `unset(SHORTLOOPS) && unset(SHORTREPEAT)`).
10044fn parse_loop_body(foreach_style: bool, is_repeat: bool) -> Option<ZshProgram> {
10045 // c:1180-1194 — body dispatch order per par_for:
10046 // `do ... done` (DOLOOP) — primary form.
10047 // `{ ... }` (INBRACE) — alternate.
10048 // csh/CSHJUNKIELOOPS — terminator is `end`.
10049 // else if (unset(SHORTLOOPS)) — YYERROR.
10050 // else — short form (single command).
10051 if tok() == DOLOOP {
10052 zshlex();
10053 // Body parse must declare DONE as an end-token so the
10054 // parse_program_until top-level orphan-DONE guard doesn't
10055 // mis-fire on the legitimate loop terminator.
10056 let body = parse_program_until(Some(&[DONE]), false);
10057 // c:Src/parse.c:1182-1183 / :1535-1536 / :1597-1598 —
10058 // `if (tok != DONE) YYERRORV(oecused);`. zshrs previously
10059 // silently accepted EOF as a substitute for `done`, so
10060 // `for i in a; do echo hi; don` ran the loop with `don` as
10061 // a command (which then failed "command not found") instead
10062 // of erroring at parse time. Bug #403, #404.
10063 if tok() != DONE {
10064 set_tok(LEXERR); // c:1174 YYERRORV(oecused)
10065 return None;
10066 }
10067 // c:1175 / c:1537 / c:1586 — `incmdpos = 0; zshlex();`: the word after
10068 // the loop closer is not in command position, so under IGNORE_BRACES a
10069 // `}` there is an ordinary word and `{ for i in 1; do :; done }` is a
10070 // parse error, as in zsh.
10071 set_incmdpos(false);
10072 zshlex();
10073 Some(body)
10074 } else if tok() == INBRACE_TOK {
10075 zshlex();
10076 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
10077 // c:Src/parse.c:1186 / :1539 — `if (tok != OUTBRACE) YYERRORV`.
10078 if tok() != OUTBRACE_TOK {
10079 set_tok(LEXERR); // c:87 YYERRORV(oecused) — no own message
10080 return None;
10081 }
10082 // c:1182 / c:1544 — the brace form clears incmdpos the same way.
10083 set_incmdpos(false);
10084 zshlex();
10085 Some(body)
10086 } else if foreach_style || isset(CSHJUNKIELOOPS) {
10087 // c:1184 / 1546 / 1595 — `else if (csh || isset(CSHJUNKIELOOPS))`.
10088 let body = parse_program_until(Some(&[ZEND]), false);
10089 // c:1190 / 1548 — `if (tok != ZEND) YYERRORV`.
10090 if tok() != ZEND {
10091 set_tok(LEXERR); // c:1187 YYERRORV(oecused)
10092 return None;
10093 }
10094 zshlex();
10095 Some(body)
10096 } else {
10097 // c:1190 / 1474 / 1551 / 1600 — short-form gate. C bails
10098 // with YYERROR when `unset(SHORTLOOPS) && (!is_repeat ||
10099 // unset(SHORTREPEAT))`. zshrs's option machinery isn't
10100 // initialised at parse-test time (no `init_main` →
10101 // `install_emulation_defaults`), so a strict port here
10102 // body. parse_init seeds SHORTLOOPS=on mirroring C
10103 // `install_emulation_defaults`, so this fires only when a
10104 // script explicitly disabled the option.
10105 if unset(SHORTLOOPS) && (!is_repeat || unset(SHORTREPEAT)) {
10106 // c:Src/parse.c:1190 / :1474 / :1551 / :1600 —
10107 // `YYERRORV(oecused)` only sets `tok = LEXERR`; the
10108 // DIAGNOSTIC is emitted upstream by par_list's
10109 // `if (!r) { ... yyerror(1); ... }` (c:670-677), which
10110 // prints `parse error near \`<zshlextext>'`. zshrs
10111 // invented its own wording, so `unsetopt shortloops;
10112 // eval 'for f in a; print $f'` said "short loop form
10113 // requires SHORTLOOPS option" where zsh says
10114 // "parse error near \`print'" (E01options.ztst:74).
10115 // c:87-88 — `#define YYERRORV(O) { tok = LEXERR; ecused =
10116 // (O); return; }`. The macro sets the token and RETURNS;
10117 // the single diagnostic comes from the caller
10118 // (par_list's `if (!r) { ... yyerror(1); ... }`, c:670-677
10119 // — mirrored by parse_program_until's LEXERR arm). Calling
10120 // yyerror here as well printed the message twice.
10121 set_tok(LEXERR); // c:87 `tok = LEXERR`
10122 return None;
10123 }
10124 // c:Src/parse.c:1604 / :1474 / :1551 — short form calls
10125 // par_save_list1 → par_list1 → par_sublist, which parses
10126 // ONE sublist and leaves the trailing SEPER untouched for
10127 // the outer par_list to consume. zshrs previously routed
10128 // through par_list() which consumes the trailing `;`/`\n`
10129 // separator — that swallowed the separator between the
10130 // loop's body command and the next outer command, so
10131 // `repeat 2 print x; print y` parsed as repeat-then-eof
10132 // and par_cmd's post-compound STRING_LEX guard at parse.rs
10133 // line 1170 fired "parse error near `print'". Bug #593.
10134 par_list1().map(|sublist| ZshProgram {
10135 lists: vec![ZshList {
10136 sublist,
10137 flags: ListFlags::default(),
10138 }],
10139 })
10140 }
10141}
10142
10143/// !!! WARNING: RUST-ONLY HELPER !!!
10144///
10145/// No function of this name exists in `Src/parse.c`. This helper stands in
10146/// for the anonymous-function shape that C handles inline in `par_simple`
10147/// (c:2016-2028) via the INOUTPAR test with no preceding name.
10148/// `() { body } arg1 arg2 …` — anonymous function. Defines a fresh
10149/// function named `_zshrs_anon_N`, invokes it with the args, and the
10150/// body runs with positional params set. Implemented as the desugared
10151/// pair (FuncDef + Simple call) so the compile path doesn't need new
10152/// machinery.
10153/// Parse an anonymous function definition `() { BODY }` followed
10154/// by call args. zsh treats `() { echo hi; } a b c` as defining
10155/// and immediately calling an anon fn with args a/b/c. C
10156/// equivalent: the INOUTPAR shape in par_simple at parse.c:1836+
10157/// triggers an anon-funcdef path.
10158fn parse_anon_funcdef() -> Option<ZshCommand> {
10159 // Track the byte position just past the `()` / most-recent separator so an
10160 // UNBRACED body's raw text can be sliced for `functions` rendering. pos()
10161 // AFTER a zshlex is already past the next token (one-token lookahead), so
10162 // body_start must be recorded BEFORE each advance. Mirrors the braced
10163 // path's body_start-before-zshlex capture (parse.rs:2484).
10164 let mut body_mark = crate::funcdef_capture::body_mark_begin();
10165 zshlex(); // skip ()
10166 while tok() == SEPER || tok() == NEWLIN {
10167 crate::funcdef_capture::body_mark_restart(&mut body_mark);
10168 zshlex();
10169 }
10170 // c:Src/parse.c:1728-1748 — after `()` (and any separators, skipped at
10171 // c:1720), the body is either a braced `{ … }` (par_list) OR, with
10172 // SHORTLOOPS set (the default), a single UNBRACED command (par_list1):
10173 // `() print hi` runs an anon fn whose body is `print hi`, and
10174 // `f() () print hi` nests one as another function's body. A bodyless `()`
10175 // is a parse error in zsh ("parse error near `()'"), which par_cmd()
10176 // produces naturally when the next token can't start a command (`}`, EOF).
10177 let (body, body_source) = if tok() != INBRACE_TOK {
10178 // c:2110 — no SHORTLOOPS gate on the `name ()` / `()` body (see
10179 // parse_inline_funcdef).
10180 // c:2114 — `par_cmd(&c, argc == 0)`: for an anonymous function
10181 // zsh_construct is set, so a `( … )` / `{ … }` body leaves the NEXT
10182 // word out of command position (c:1632, c:2118-2128) and
10183 // `() (cat $1 $2) <(print a) =(print b)` reads its arguments as words.
10184 // Slice the raw body text (body_start was captured before the body
10185 // token was lexed, above) so `functions`/`typeset -f` can render it:
10186 // fusevm shfuncs carry no C-shaped Eprog, so hashtable.rs:1397-1414
10187 // renders from this raw `body` string, not getpermtext. Without it the
10188 // body listed as `f () { }` (empty).
10189 let cmd = par_cmd(true)?;
10190 let body_source = crate::funcdef_capture::body_text(body_mark)
10191 .map(|s| {
10192 s.trim()
10193 .trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace())
10194 .to_string()
10195 })
10196 .filter(|s| !s.is_empty());
10197 // c:Src/parse.c par_funcdef bakes alias expansion into the wordcode; see
10198 // vm_helper::funcdef_note_resolved_body.
10199 crate::vm_helper::funcdef_note_resolved_body(body_source.as_deref());
10200 let list = ZshList {
10201 sublist: ZshSublist {
10202 pipe: ZshPipe {
10203 cmd,
10204 next: None,
10205 lineno: lineno(),
10206 merge_stderr: false,
10207 },
10208 next: None,
10209 flags: SublistFlags::default(),
10210 },
10211 flags: ListFlags::default(),
10212 };
10213 (ZshProgram { lists: vec![list] }, body_source)
10214 } else {
10215 zshlex(); // skip {
10216 // c:Src/parse.c:par_subsh — anon `() { … }` body must terminate at
10217 // OUTBRACE_TOK. Pass it as the explicit end-token so the inner
10218 // parse stops cleanly at `}` rather than hitting the top-level
10219 // stray-`}` arm (#168). Bug #167 family.
10220 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
10221 // c:Src/parse.c:1733-1737 — same `if (tok != OUTBRACE) YYERRORV`
10222 // gate as the named-funcdef path. Bug #405 sibling.
10223 if tok() != OUTBRACE_TOK {
10224 set_tok(LEXERR); // c:87 YYERRORV(oecused) — no own message
10225 return None;
10226 }
10227 // c:2102-2105 — `if (argc == 0) { /* Anonymous function, possibly
10228 // with arguments */ incmdpos = 0; }`: the words after `}` are the
10229 // function's arguments, so `() { echo $1 } (y|z)*` lexes `(y|z)*` as
10230 // a glob word rather than a subshell.
10231 set_incmdpos(false);
10232 zshlex();
10233 (body, None)
10234 };
10235 // c:2148-2167 — collect trailing args AND redirections, interleaved in any
10236 // order, until a separator. zsh's anon-fn form `() { body } a b c` runs
10237 // body with $1=a, $2=b, $3=c; redirs apply to that invocation:
10238 // `() { read v; print $1 $v } <input1 Shirley >output1 dude`
10239 // (c:Src/parse.c par_simple — the anon function is a simple command
10240 // whose words and redirs follow the body in any order). The previous
10241 // port collected ONLY leading STRING args here and left redirs to
10242 // par_cmd's trailing loop, so an arg AFTER a redir (`<in arg`) was
10243 // never reached and hit par_cmd's "parse error near `<arg>'" gate.
10244 let mut args = Vec::new();
10245 let mut redirs: Vec<ZshRedir> = Vec::new();
10246 loop {
10247 let t = tok();
10248 if t == STRING_LEX {
10249 if let Some(s) = tokstr() {
10250 args.push(s);
10251 }
10252 zshlex();
10253 } else if IS_REDIROP(t) {
10254 match par_redir() {
10255 Some(r) => redirs.push(r),
10256 None => break,
10257 }
10258 } else {
10259 break;
10260 }
10261 }
10262
10263 // Generate a unique name. Module-level static would be cleaner but
10264 // a thread-local atomic is enough — anonymous functions are
10265 // ephemeral and the name isn't user-visible.
10266 static ANON_COUNTER: AtomicUsize = AtomicUsize::new(0);
10267 let n = ANON_COUNTER.fetch_add(1, Ordering::Relaxed);
10268 let name = format!("_zshrs_anon_{}", n);
10269 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
10270 names: vec![name],
10271 body: Box::new(body),
10272 tracing: false,
10273 keyword_form: false,
10274 auto_call_args: Some(args),
10275 body_source,
10276 });
10277 // Wrap in Redirected so the redirs bracket the single invocation
10278 // (compile_zsh.rs:929 wraps the body in a WithRedirectsBegin/End
10279 // scope for Redirected(FuncDef, …)). par_cmd's trailing-redir loop
10280 // then finds none and returns this node unwrapped.
10281 if redirs.is_empty() {
10282 Some(funcdef)
10283 } else {
10284 Some(ZshCommand::Redirected(Box::new(funcdef), redirs))
10285 }
10286}
10287
10288/// !!! WARNING: RUST-ONLY HELPER !!!
10289///
10290/// No function of this name exists in `Src/parse.c`. This helper stands in
10291/// for the `case INBRACE:` arm of `par_cmd` (c:1013-1019), which C inlines as
10292/// `cmdpush(CS_CURSH); par_subsh(cmplx, 1); cmdpop();`.
10293/// Parse {...} cursh
10294/// Parse a current-shell brace block `{ BODY }`. C source
10295/// par_cmd at parse.c:958-1085 handles Inbrace → emit WC_CURSH
10296/// and recurses into the list. zshrs's parse_cursh extracts that
10297/// arm into a dedicated method.
10298fn parse_cursh(zsh_construct: bool) -> Option<ZshCommand> {
10299 zshlex(); // skip {
10300 // c:Src/parse.c:par_subsh — pass OUTBRACE_TOK as the explicit
10301 // body terminator so the inner parse stops cleanly at `}` rather
10302 // than falling through the top-level `OUTBRACE_TOK if
10303 // end_tokens.is_none()` arm (which errors on stray `}` per bug
10304 // #168). Bug #167 in docs/BUGS.md.
10305 let prog = parse_program_until(Some(&[OUTBRACE_TOK]), false);
10306
10307 // c:Src/parse.c:par_subsh — `{ … }` requires a matching `}`.
10308 // C errors via YYERRORV when the body parse returns without
10309 // seeing OUTBRACE_TOK (parse.c:1623 inbrack check). zshrs's
10310 // previous behavior silently returned `Cursh(prog)` and ran the
10311 // body as if the braces were absent. Bug #167 in docs/BUGS.md.
10312 if tok() != OUTBRACE_TOK {
10313 // c:1630-1631 — `YYERRORV(oecused)`: flag LEXERR and let
10314 // par_event's yyerror name `zshlextext`, the last token lexed
10315 // (`{ print` → near `print', `{ print;` → near `;').
10316 set_tok(LEXERR); // c:87 `tok = LEXERR`
10317 return None;
10318 }
10319 // Check for { ... } always { ... }. Direct port of zsh's
10320 // par_subsh at parse.c:1612-1660 — note the two `incmdpos = 1`
10321 // forces (parse.c:1632, 1637): after consuming the closing
10322 // Outbrace AND after matching the `always` keyword, the parser
10323 // explicitly resets command position so the next `{` lexes as
10324 // Inbrace. Without these resets the lexer's String-clears-cmdpos
10325 // rule (lex.rs:976-983) leaves the second `{` in word position,
10326 // turning `always { ... }` into a Simple `{` `echo` … and the
10327 // try/always pairing is silently lost.
10328 {
10329 set_incmdpos(!zsh_construct); // parse.c:1632 incmdpos = !zsh_construct
10330 zshlex();
10331
10332 // Check for 'always'
10333 if tok() == STRING_LEX {
10334 let s = tokstr();
10335 if s.map(|s| s == "always").unwrap_or(false) {
10336 set_incmdpos(true); // parse.c:1637 incmdpos = 1
10337 zshlex();
10338 skip_separators();
10339
10340 // c:1643-1644 — `if (tok != INBRACE) YYERRORV(oecused);`
10341 if tok() != INBRACE_TOK {
10342 set_tok(LEXERR); // c:87 `tok = LEXERR`
10343 return None;
10344 }
10345 zshlex();
10346 // c:Src/parse.c — always-clause body terminates at
10347 // OUTBRACE_TOK. Bug #167/#168 family.
10348 let always = parse_program_until(Some(&[OUTBRACE_TOK]), false);
10349 set_incmdpos(true); // c:1653 `incmdpos = 1;`
10350 // c:1655-1656 — `if (tok != OUTBRACE) YYERRORV(oecused);`
10351 if tok() != OUTBRACE_TOK {
10352 set_tok(LEXERR); // c:87 `tok = LEXERR`
10353 return None;
10354 }
10355 zshlex();
10356 return Some(ZshCommand::Try(ZshTry {
10357 try_block: Box::new(prog),
10358 always: Box::new(always),
10359 }));
10360 }
10361 }
10362 }
10363
10364 Some(ZshCommand::Cursh(Box::new(prog)))
10365}
10366
10367/// !!! WARNING: RUST-ONLY HELPER !!!
10368///
10369/// No function of this name exists in `Src/parse.c`. This helper stands in
10370/// for the `NAME () { ... }` funcdef shape that C handles inline in
10371/// `par_simple` (c:2016-2028) after the INOUTPAR token.
10372/// Parse inline function definition: name() { ... }
10373/// Parse the inline form `NAME () { BODY }` (POSIX-style funcdef
10374/// without the `function` keyword). The name has already been
10375/// consumed and pushed by par_simple before this method fires.
10376/// C source: handled inline in par_simple's INOUTPAR-after-name
10377/// arm (parse.c:1836-2228).
10378fn parse_inline_funcdef(names: Vec<String>) -> Option<ZshCommand> {
10379 // !!! DASH-STRICT GATE (no C counterpart) !!! dash's parser refuses a
10380 // function named after a special builtin or a non-identifier
10381 // ("Syntax error: Bad function name"); zsh accepts both.
10382 if crate::dash_mode::dash_strict()
10383 && names.iter().any(|n| crate::dash_mode::dash_bad_function_name(n))
10384 {
10385 zerr("Syntax error: Bad function name");
10386 set_tok(LEXERR);
10387 return None;
10388 }
10389 // par_simple's STRING loop left `incmdpos = 0`; the funcdef body
10390 // `{ ... }` requires `incmdpos = 1` so the lexer recognises `{`
10391 // as INBRACE_TOK (current-shell block opener) instead of a
10392 // literal `{` STRING. Without this, `myfunc() { echo body }`
10393 // parsed the body as the single STRING `"{"`, then `echo body`
10394 // fell out at top level. Mirrors the C path where par_cmd's
10395 // dispatcher (parse.c:958) is called with `incmdpos = 1` for
10396 // the funcdef body.
10397 set_incmdpos(true);
10398 // Track the byte position just past `()` / the most-recent separator, so an
10399 // UNBRACED short body (`f() cmd`) can be sliced for `functions` rendering.
10400 // pos() AFTER a zshlex is already past the next token (one-token
10401 // lookahead), so record it BEFORE each advance (mirrors parse.rs:2484).
10402 let mut body_mark = crate::funcdef_capture::body_mark_begin();
10403 // Skip ()
10404 if tok() == INOUTPAR {
10405 zshlex();
10406 }
10407
10408 while tok() == SEPER || tok() == NEWLIN {
10409 crate::funcdef_capture::body_mark_restart(&mut body_mark);
10410 zshlex();
10411 }
10412
10413 // Parse body
10414 if tok() == INBRACE_TOK {
10415 // Same body_start-before-zshlex fix as par_funcdef.
10416 crate::funcdef_capture::body_mark_restart(&mut body_mark);
10417 zshlex();
10418 // c:Src/parse.c — inline funcdef body terminates at OUTBRACE_TOK.
10419 // Explicit end-token keeps the inner parse from hitting the
10420 // top-level stray-`}` arm (#168). Bug #167 family.
10421 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
10422 // c:Src/parse.c:1733-1737 — `if (tok != OUTBRACE) { cmdpop();
10423 // lineno += oldlineno; ecnpats = onp; ecssub = oecssub;
10424 // YYERRORV(oecused); }`. Without this gate, `f() { echo hi`
10425 // silently registered as a complete fn with body `echo hi`.
10426 // Bug #405.
10427 if tok() != OUTBRACE_TOK {
10428 set_tok(LEXERR); // c:87 YYERRORV(oecused) — no own message
10429 return None;
10430 }
10431 // Slice THROUGH pos() (not `pos()-1`). pos() sits just past the
10432 // funcdef `}` and any trailing whitespace/newline the lexer
10433 // consumed, so `[body_start, pos())` always contains the funcdef
10434 // `}`. Using `pos()-1` landed ON the `}` at end-of-input (no
10435 // trailing separator) and thus EXCLUDED it, so the subsequent
10436 // `strip_suffix('}')` chopped a legitimate inner `}` instead —
10437 // corrupting any body ending in a balanced `} always { ... }`
10438 // into `par_subsh: 'always' block missing }` on the `.zwc`
10439 // source path (getpermtext re-emits the file with no trailing
10440 // newline, so the last funcdef `}` sits at EOF). Bug #642.
10441 let body_source = crate::funcdef_capture::body_text(body_mark)
10442 .map(|s| {
10443 // The slice now always ends with the funcdef `}` (plus
10444 // optional trailing whitespace). Trim, strip that single
10445 // closing brace, then drop the structural separator
10446 // (`;`/`\n`) that preceded it — C zsh's getpermtext emits
10447 // each command without the trailing `;`/`\n`.
10448 let t = s.trim();
10449 // Strip the statement SEPARATOR that followed the funcdef
10450 // `}` FIRST — for `name() { body }; next` the slice ends in
10451 // `};`, so stripping the brace before the `;` left the `}`
10452 // stranded (`${functions[name]}` → `body }`, then re-parsed
10453 // to a `parse error near '}'`). Order: trailing separators →
10454 // closing brace → the body's own last-statement separator.
10455 let t = t.trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace());
10456 let t = t.strip_suffix('}').unwrap_or(t).trim_end();
10457 let t = t
10458 .trim_end_matches(|c: char| c == ';' || c == '\n')
10459 .trim_end();
10460 t.to_string()
10461 })
10462 .filter(|s| !s.is_empty());
10463 // c:Src/parse.c par_funcdef bakes alias expansion into the wordcode; see
10464 // vm_helper::funcdef_note_resolved_body.
10465 crate::vm_helper::funcdef_note_resolved_body(body_source.as_deref());
10466 zshlex();
10467 Some(ZshCommand::FuncDef(ZshFuncDef {
10468 names,
10469 body: Box::new(body),
10470 tracing: false,
10471 keyword_form: false,
10472 auto_call_args: None,
10473 body_source,
10474 }))
10475 } else {
10476 // c:Src/parse.c:2110 — the `name ()` form (par_simple) takes ANY
10477 // command as body via `par_cmd(&c, argc == 0)` with NO SHORTLOOPS
10478 // gate (the gate at c:1742 belongs to the `function` keyword path,
10479 // par_funcdef). `f() ( echo x )` is valid under `emulate sh`/`ksh`.
10480 // verified: zsh -fc 'emulate sh; f() ( echo sub ); f' -> sub
10481 //
10482 // !!! BASH-MODE (no C counterpart) !!! bash's grammar requires a
10483 // COMPOUND command body: `f() echo hi` is a syntax error there.
10484 if crate::dash_mode::bash_mode() && tok() == STRING_LEX {
10485 set_tok(LEXERR);
10486 return None;
10487 }
10488 // Slice the raw short-body text (unbraced_body_start was captured
10489 // before the body token was lexed, above) so `functions`/`typeset -f`
10490 // can list it (fusevm shfuncs render from this raw `body`, not
10491 // getpermtext — hashtable.rs:1397). Without it `f() print x` listed as
10492 // `f () { }` (empty).
10493 match par_cmd(false) {
10494 Some(cmd) => {
10495 let body_source = crate::funcdef_capture::body_text(body_mark)
10496 .map(|s| {
10497 s.trim()
10498 .trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace())
10499 .to_string()
10500 })
10501 .filter(|s| !s.is_empty());
10502 // c:Src/parse.c par_funcdef bakes alias expansion into the wordcode; see
10503 // vm_helper::funcdef_note_resolved_body.
10504 crate::vm_helper::funcdef_note_resolved_body(body_source.as_deref());
10505 let list = ZshList {
10506 sublist: ZshSublist {
10507 pipe: ZshPipe {
10508 cmd,
10509 next: None,
10510 // c:Src/parse.c:2112 — `(void)ecadd(WCB_PIPE(
10511 // WC_PIPE_END, 0));`. The BRACELESS short-body
10512 // form (`f() cmd`) hand-builds its pipe wordcode
10513 // with a line number of literal 0, unlike
10514 // `par_pline` (c:944) which stores `toklineno+1`.
10515 // `execpline2`'s guard (c:2056 `… &&
10516 // WC_PIPE_LINENO(pcode)`) then never fires, so the
10517 // body runs with the CALLER's `lineno` still in
10518 // place — which is why `f() g; g() :; functions -t
10519 // f; f` traces `+f:4> g` (the call site's line) and
10520 // not the body's own. E02xtrace.ztst pins this
10521 // (its own comment calls the 4 "incorrect", but it
10522 // is what zsh does).
10523 lineno: 0,
10524 merge_stderr: false,
10525 },
10526 next: None,
10527 flags: SublistFlags::default(),
10528 },
10529 flags: ListFlags::default(),
10530 };
10531 Some(ZshCommand::FuncDef(ZshFuncDef {
10532 names,
10533 body: Box::new(ZshProgram { lists: vec![list] }),
10534 tracing: false,
10535 keyword_form: false,
10536 auto_call_args: None,
10537 body_source,
10538 }))
10539 }
10540 // c:2114-2117 — `if (!par_cmd(&c, argc == 0)) { cmdpop();
10541 // YYERROR(oecused); }`: `f()` with no body is a syntax error.
10542 // The bare `None` never flagged LEXERR, so nothing reported it
10543 // and the script ran on with status 0. c:2071 `lineno = 0;` is
10544 // never undone on this path (only the brace arm's c:2097 adds
10545 // `oldlineno` back), so the diagnostic carries no line number.
10546 //
10547 // That holds only when par_cmd itself returns 0 (nothing
10548 // parsed: `f()` at end of input). A compound body that fails
10549 // INSIDE (`f() ( :`, `f() if`) YYERRORs in its own parser while
10550 // par_cmd still returns 1 (c:958-1060 `break`s after every
10551 // compound), so C runs on to c:2172 `lineno += oldlineno` and
10552 // the message keeps its line. A sub-construct failure is the
10553 // one that already left `tok == LEXERR`.
10554 None => {
10555 if tok() != LEXERR {
10556 set_lineno(0);
10557 }
10558 set_tok(LEXERR);
10559 None
10560 }
10561 }
10562 }
10563}
10564
10565/// !!! WARNING: RUST-ONLY HELPER !!!
10566///
10567/// No function of this name exists in `Src/parse.c`. This helper stands in
10568/// for the entry alias onto the AST cond ladder; C enters at `par_cond`
10569/// (c:2409) directly.
10570/// Parse conditional expression
10571/// Top of `[[ ]]` cond-expression parsing — entry to recursive
10572/// descent (or → and → not → primary). Direct port of zsh's
10573/// par_cond_1 at parse.c:2434-2475.
10574fn parse_cond_expr() -> Option<ZshCond> {
10575 parse_cond_or()
10576}
10577
10578/// Port of `par_cond` from `Src/parse.c:2409` — C decl `par_cond(void)`. Rust fn `parse_cond_or` is the AST variant: C's `cond : cond_1 [ DBAR { SEPER } cond ]` production, building `ZshCond::Or`.
10579/// Cond-expression `||` level. C: inside par_cond_1 at
10580/// parse.c:2434-2475 (the `cond_or` ladder).
10581fn parse_cond_or() -> Option<ZshCond> {
10582 let left = parse_cond_and()?;
10583 skip_cond_separators();
10584
10585 if tok() == DBAR {
10586 zshlex();
10587 skip_cond_separators();
10588 parse_cond_or().map(|right| ZshCond::Or(Box::new(left), Box::new(right)))
10589 } else {
10590 Some(left)
10591 }
10592}
10593
10594/// Port of `par_cond_1` from `Src/parse.c:2434` — C decl `par_cond_1(void)`. Rust fn `parse_cond_and` is the AST variant: C's `cond_1 : cond_2 [ DAMPER { SEPER } cond_1 ]` production, building `ZshCond::And`.
10595/// Cond-expression `&&` level. C: par_cond_2 at parse.c:2476-2625.
10596fn parse_cond_and() -> Option<ZshCond> {
10597 let left = parse_cond_not()?;
10598 skip_cond_separators();
10599
10600 if tok() == DAMPER {
10601 zshlex();
10602 skip_cond_separators();
10603 parse_cond_and().map(|right| ZshCond::And(Box::new(left), Box::new(right)))
10604 } else {
10605 Some(left)
10606 }
10607}
10608
10609/// `static FuncDump dumps;` from `Src/parse.c:3652` — head of the
10610/// loaded-`.zwc` linked list. C walks `dumps`/`p->next` directly;
10611/// the Rust port uses a `Mutex<Vec<funcdump>>` indexed by filename
10612/// so refcount ops can find an entry without raw-pointer compare.
10613pub static DUMPS: std::sync::Mutex<Vec<funcdump>> = std::sync::Mutex::new(Vec::new());
10614
10615/// Port of `par_cond_2` from `Src/parse.c:2476` — C decl `par_cond_2(void)`. Rust fn `parse_cond_not` is the AST variant, BANG and INPAR arms (c:2528-2547); the STRING arms are split out into `parse_cond_primary`.
10616/// Cond-expression `!` negation level. C: handled inside
10617/// par_cond_2 at parse.c:2476-2625 via the Bang token check.
10618fn parse_cond_not() -> Option<ZshCond> {
10619 skip_cond_separators();
10620
10621 // ! can be either BANG_TOK or String "!"
10622 let is_not =
10623 tok() == BANG_TOK || (tok() == STRING_LEX && tokstr().map(|s| s == "!").unwrap_or(false));
10624 if is_not {
10625 zshlex();
10626 let inner = parse_cond_not()?;
10627 return Some(ZshCond::Not(Box::new(inner)));
10628 }
10629
10630 if tok() == INPAR_TOK {
10631 zshlex();
10632 skip_cond_separators();
10633 // c:2534-2547 par_cond_2 INPAR branch — an empty body
10634 // `[[ ( ) ]]` reaches par_cond_2 with tok=OUTPAR, which the
10635 // lexer produced from gettok with no tokstr, so c:2560 YYERRORs
10636 // and yyerror names the token from `tokstrings[OUTPAR]`: zsh
10637 // says "parse error near `)'". parse_cond_primary's `_` arm
10638 // returns None for exactly that case, and par_cond's
10639 // `tok != DOUTBRACK` arm runs yyerror while OUTPAR is still
10640 // current — no separate diagnostic needed here. Bug #538.
10641 let inner = parse_cond_expr()?;
10642 skip_cond_separators();
10643 // c:2543-2544 — `if (tok != OUTPAR) YYERROR(ecused);`. A group
10644 // opened with `(` MUST close with `)`; `[[ ( a ]]` is a parse
10645 // error in zsh ("parse error near `]]'", status 1), not a
10646 // silently-accepted group. The port dropped the YYERROR and
10647 // accepted the unterminated group, so `[[ ( a ]]` evaluated as
10648 // `[[ a ]]` and exited 0.
10649 if tok() != OUTPAR_TOK {
10650 // c:2544 `YYERROR(ecused)` — the macro sets `tok = LEXERR`
10651 // before returning, and that is what par_dinbrack's
10652 // `if (tok != DOUTBRACK) YYERRORV(oecused)` (c:1818) tests.
10653 // Returning None alone is not enough here: the offending
10654 // token IS `]]`, so without the retype par_dinbrack took
10655 // its success arm and `[[ ( a ]]` silently exited 0.
10656 set_tok(LEXERR); // c:2544
10657 return None;
10658 }
10659 zshlex(); // c:2545 condlex()
10660 return Some(inner);
10661 }
10662
10663 parse_cond_primary()
10664}
10665
10666/// !!! WARNING: RUST-ONLY HELPER !!!
10667///
10668/// No function of this name exists in `Src/parse.c`. This helper stands in
10669/// for the STRING arms of `par_cond_2` (c:2549-2624) together with the
10670/// `par_cond_double` / `par_cond_triple` / `par_cond_multi` arity dispatch
10671/// (c:2626/2659/2716), which C keeps inside `par_cond_2`.
10672/// Cond-expression primary: unary tests (-f, -d, ...), binary
10673/// tests (=, !=, <, >, ==, =~, -eq, -ne, ...), and parenthesized
10674/// sub-expressions. Direct port of par_cond_double / par_cond_triple
10675/// / par_cond_multi at parse.c:2626-2731 (chosen by arg count).
10676fn parse_cond_primary() -> Option<ZshCond> {
10677 let s1 = match tok() {
10678 STRING_LEX => {
10679 let s = tokstr().unwrap_or_default();
10680 zshlex();
10681 s
10682 }
10683 _ => {
10684 // c:2553-2560 — "Check first argument for [[ STRING ]]
10685 // re-interpretation": a non-STRING token that still carries
10686 // a tokstr (the lexer retypes `]]` and `!` in place, keeping
10687 // it) is read as its own operand, `[[ -n "$tokstr" ]]`.
10688 // c:2549 `dble` blocks that for a bare two-char `-X`, which
10689 // stays a unary operator awaiting its argument. Everything
10690 // else — LEXERR, and punctuation like `)` or `;` that never
10691 // captured a tokstr — is the YYERROR case: return None and
10692 // let par_cond's `tok != DOUTBRACK` arm run yyerror.
10693 let s1 = tokstr().unwrap_or_default();
10694 let s1_chars: Vec<char> = s1.chars().collect();
10695 // c:2549 with n_testargs == 0: `IS_DASH(*s1) && !s1[2]`.
10696 let dble = s1_chars.len() == 2 && IS_DASH(s1_chars[0]);
10697 if s1.is_empty() || tok() == LEXERR || dble {
10698 return None;
10699 }
10700 zshlex(); // c:2557 `do condlex(); while (COND_SEP());`
10701 skip_cond_separators();
10702 return Some(ZshCond::Unary("-n".to_string(), s1)); // c:2558
10703 }
10704 };
10705 skip_cond_separators();
10706
10707 // !!! WARNING: RUST-ONLY ADAPTER — the AST form of C's `COND_ERROR(X,Y)`
10708 // macro (c:Src/parse.c:89-96):
10709 // zwarn(X,Y); herrflush(); if (noerrs != 2) errflag |= ERRFLAG_ERROR;
10710 // YYERROR(ecused)
10711 // Rust pre-formats the message, so the caller applies the `%s`
10712 // (nicezputs → `nicedupstring`) step itself. `YYERROR` is `tok = LEXERR`
10713 // plus the early `None` the cond parsers propagate.
10714 macro_rules! cond_error {
10715 ($fmt:literal, $arg:expr) => {{
10716 crate::ported::utils::zerr(&format!(
10717 $fmt,
10718 crate::ported::utils::nicedupstring(&$arg)
10719 ));
10720 crate::ported::utils::errflag.fetch_or(
10721 crate::ported::zsh_h::ERRFLAG_ERROR,
10722 std::sync::atomic::Ordering::Relaxed,
10723 );
10724 set_tok(LEXERR);
10725 return None;
10726 }};
10727 }
10728
10729 let s1_chars: Vec<char> = s1.chars().collect();
10730 // c:2549 — `dble = (s1 && IS_DASH(*s1) && (!n_testargs || …) && !s1[2]);`
10731 // With n_testargs == 0 (the `[[ ]]` entry point) that is "a bare
10732 // two-char `-X`". A negative number (`-5`) is NOT exempt in C: it is
10733 // just another two-char dash word, and `[[ -5 -lt -3 ]]` still parses
10734 // as a triple below because the SECOND word decides (c:2599-2600).
10735 let dble = s1_chars.len() == 2 && IS_DASH(s1_chars[0]);
10736
10737 // c:2576-2587 — `<` / `>` string comparison, checked before the
10738 // operand count for every s1 (a dash word included: `[[ -n < b ]]`
10739 // is `STRLT("-n", "b")`).
10740 if tok() == INANG_TOK || tok() == OUTANG_TOK {
10741 let op = if tok() == INANG_TOK { "<" } else { ">" };
10742 set_incond(incond() + 1);
10743 zshlex();
10744 set_incond(incond() - 1);
10745 skip_cond_separators();
10746 if tok() != STRING_LEX {
10747 // c:2580-2581 `if (tok != STRING) YYERROR(ecused);` — no message
10748 // of its own; par_event's yyerror reports "parse error near `]]'".
10749 set_tok(LEXERR);
10750 return None;
10751 }
10752 let s3 = tokstr().unwrap_or_default();
10753 zshlex(); // c:2584 `do condlex(); while (COND_SEP());`
10754 skip_cond_separators();
10755 return Some(ZshCond::Binary(s1, op.to_string(), s3)); // c:2585-2588
10756 }
10757
10758 // c:2588-2597 — `if (tok != STRING)`: only one operand. `!dble` →
10759 // `par_cond_double(dupstring("-n"), s1)`; a bare `-X` →
10760 // `par_cond_multi(s1, newlinklist())`, a COND_MOD with no operands that
10761 // the evaluator rejects with "unknown condition" (c:Src/cond.c:186-193).
10762 // A multi-char `-prefix` is not dble, so `[[ -prefix ]]` is
10763 // `[[ -n -prefix ]]` → true.
10764 if tok() != STRING_LEX {
10765 if !dble {
10766 return Some(ZshCond::Unary("-n".to_string(), s1)); // c:2591
10767 }
10768 return Some(ZshCond::ModCond(s1, Vec::new())); // c:2593
10769 }
10770
10771 // c:2598-2603
10772 // s2 = tokstr;
10773 // if (!n_testargs)
10774 // dble = (s2 && IS_DASH(*s2) && !s2[2]);
10775 // incond++; -- parentheses do globbing
10776 // do condlex(); while (COND_SEP());
10777 // incond--; -- parentheses do grouping
10778 // `dble` is RECOMPUTED from the second word: `[[ A -X B ]]` reads as the
10779 // two-word `par_cond_double(A, "-X")` (`[[ "" -a "x" ]]` → "parse error:
10780 // condition expected: "), and the bump makes the lexer keep `(` literal
10781 // in the RHS word (`[[ x =~ (foo) ]]`).
10782 let s2 = tokstr().unwrap_or_default();
10783 let s2_chars: Vec<char> = s2.chars().collect();
10784 let dble = s2_chars.len() == 2 && IS_DASH(s2_chars[0]);
10785 set_incond(incond() + 1);
10786 zshlex();
10787 skip_cond_separators();
10788 set_incond(incond() - 1);
10789
10790 // c:2604-2622
10791 if tok() == STRING_LEX && !dble {
10792 let s3 = tokstr().unwrap_or_default();
10793 zshlex(); // c:2606 `do condlex(); while (COND_SEP());`
10794 skip_cond_separators();
10795 if tok() == STRING_LEX {
10796 // c:2607-2616 — a fourth word: every remaining STRING joins the
10797 // operand list and the whole term is `par_cond_multi(s1, l)`.
10798 let mut l: Vec<String> = vec![s2, s3];
10799 while tok() == STRING_LEX {
10800 l.push(tokstr().unwrap_or_default());
10801 zshlex();
10802 skip_cond_separators();
10803 }
10804 // c:2716-2719 par_cond_multi —
10805 // if (!IS_DASH(a[0]) || !a[1])
10806 // COND_ERROR("condition expected: %s", a);
10807 if !(s1_chars.len() >= 2 && IS_DASH(s1_chars[0])) {
10808 cond_error!("condition expected: {}", s1);
10809 }
10810 return Some(ZshCond::ModCond(s1, l)); // c:2722-2726 COND_MOD
10811 }
10812
10813 // c:2618 `par_cond_triple(s1, s2, s3)` (c:2659-2712). The operator
10814 // checks run in C's order: `=`, `<`/`>`, `==`, `!=`, `=~`, a dash
10815 // operator, then a dash FIRST word.
10816 let bc = &s2_chars;
10817 let is_eq = |ch: char| ch == '=' || ch == Equals;
10818 let is_bang = |ch: char| ch == '!' || ch == Bang;
10819 // c:2663 `(b[0] == Equals || b[0] == '=') && !b[1]` → COND_STREQ
10820 // c:2668 `(b[0] == '>' || Outang || '<' || Inang) && !b[1]`
10821 // c:2674 `==` → COND_STRDEQ, c:2680 `!=` → COND_STRNEQ
10822 let string_op = (bc.len() == 1
10823 && (is_eq(bc[0]) || matches!(bc[0], '<' | '>') || bc[0] == Inang || bc[0] == Outang))
10824 || (bc.len() == 2 && is_eq(bc[0]) && is_eq(bc[1]))
10825 || (bc.len() == 2 && is_bang(bc[0]) && is_eq(bc[1]));
10826 // c:2685-2686 `(b[0] == Equals || '=') && (b[1] == '~' || Tilde) &&
10827 // !b[2]` → COND_REGEX. The lexer hands the TOKEN forms (`=` at word
10828 // start → Equals, `~` → Tilde) inside cond bodies.
10829 if bc.len() == 2 && is_eq(bc[0]) && (bc[1] == '~' || bc[1] == Tilde) {
10830 return Some(ZshCond::Regex(s1, s3));
10831 }
10832 // c:2692-2702 — `IS_DASH(b[0])`: a numeric/file operator
10833 // (`get_cond_num`) or an infix module condition (COND_MODI). Both
10834 // stay a Binary node; the evaluator reports an unknown `-X`.
10835 if string_op || bc.first().is_some_and(|&ch| IS_DASH(ch)) {
10836 return Some(ZshCond::Binary(s1, s2, s3));
10837 }
10838 // c:2703-2707 — `IS_DASH(a[0]) && a[1]`: `WCB_COND(COND_MOD, 2)`
10839 // named by the first word, so `[[ -n a b ]]` evaluates to "unknown
10840 // condition: -n" (status 2) instead of failing to parse.
10841 if s1_chars.len() >= 2 && IS_DASH(s1_chars[0]) {
10842 return Some(ZshCond::ModCond(s1, vec![s2, s3]));
10843 }
10844 // c:2709 `COND_ERROR("condition expected: %s", b)`.
10845 cond_error!("condition expected: {}", s2);
10846 }
10847
10848 // c:2620 `par_cond_double(s1, s2)` (c:2626-2640) —
10849 // if (!IS_DASH(a[0]) || !a[1])
10850 // COND_ERROR("parse error: condition expected: %s", a);
10851 // else if (!a[2] && strspn(a+1, "abcdefgknoprstuvwxzhLONGS") == 1)
10852 // WCB_COND(a[1], 0) -- the builtin unary test
10853 // else
10854 // WCB_COND(COND_MOD, 1) -- a one-operand module condition
10855 if !(s1_chars.len() >= 2 && IS_DASH(s1_chars[0])) {
10856 cond_error!("parse error: condition expected: {}", s1);
10857 }
10858 if s1_chars.len() == 2 && "abcdefgknoprstuvwxzhLONGS".contains(s1_chars[1]) {
10859 return Some(ZshCond::Unary(s1, s2)); // c:2631-2632
10860 }
10861 Some(ZshCond::ModCond(s1, vec![s2])) // c:2634-2637
10862}
10863
10864/// !!! WARNING: RUST-ONLY HELPER !!!
10865///
10866/// No function of this name exists in `Src/parse.c`. This helper stands in
10867/// for C's open-coded `while (COND_SEP()) condlex();` separator skip,
10868/// repeated at c:2413, c:2418, c:2437 and c:2442.
10869fn skip_cond_separators() {
10870 // c:2405 `COND_SEP()` — a `;` is NOT a cond separator, so it ends
10871 // the condition and becomes the token the parse error names
10872 // (`[[ ]];` → "parse error near `;'"). Testing `tokstr` for a `;`
10873 // never saw one: SEPER carries no tokstr, so the old `unwrap_or`
10874 // swallowed every `;` and the error pointed one token too far.
10875 while COND_SEP() {
10876 zshlex();
10877 }
10878}
10879
10880/// !!! WARNING: RUST-ONLY HELPER !!!
10881///
10882/// No function of this name exists in `Src/parse.c`. This helper stands in
10883/// for the `case DINPAR:` arm of `par_cmd` (c:1031-1035) in AST form,
10884/// building a `ZshCommand::Arith`.
10885/// Parse (( ... )) arithmetic command
10886/// Parse `(( EXPR ))` arithmetic command. C source: parse.c:1810-1834
10887/// `par_dinbrack` (despite the name; the function actually handles
10888/// DINPAR `(( ))` blocks too).
10889fn parse_arith() -> Option<ZshCommand> {
10890 let expr = tokstr().unwrap_or_default();
10891 zshlex();
10892 Some(ZshCommand::Arith(expr))
10893}
10894
10895/// !!! WARNING: RUST-ONLY HELPER !!!
10896///
10897/// No function of this name exists in `Src/parse.c`. This helper stands in
10898/// for C's open-coded `while (tok == SEPER) zshlex();` skips at the head of
10899/// `par_list` (c:774-775) and friends.
10900/// Skip separator tokens
10901fn skip_separators() {
10902 while tok() == SEPER || tok() == NEWLIN {
10903 zshlex();
10904 }
10905}
10906
10907// `fdheaderlen` / `fdmagic` / `fdflags` / etc. macros from
10908// `Src/parse.c:3125-3152`. C uses raw pointer arithmetic on a
10909// `Wordcode` (= `u32 *`); the Rust port takes a slice and indexes.
10910
10911/// Port of `fdheaderlen()` from `Src/parse.c:3125` — C decl `#define fdheaderlen(f) (((Wordcode) (f))[FD_PRELEN])`. C macro; the Rust port indexes a `&[u32]` where C does `Wordcode` pointer arithmetic.
10912/// ) — header
10913/// length in u32 words (read from prelude word `FD_PRELEN`).
10914#[inline]
10915pub fn fdheaderlen(f: &[u32]) -> u32 {
10916 f[FD_PRELEN]
10917}
10918
10919/// Port of `fdmagic()` from `Src/parse.c:3127` — C decl `#define fdmagic(f) (((Wordcode) (f))[0])`. C macro.
10920/// ) — first prelude
10921/// word, either `FD_MAGIC` or `FD_OMAGIC`.
10922#[inline]
10923pub fn fdmagic(f: &[u32]) -> u32 {
10924 f[0]
10925}
10926
10927/// Port of `fdflags()` from `Src/parse.c:3131` — C decl `#define fdflags(f) fdbyte(f, 0)`. C macro (via `fdbyte`, c:3129).
10928/// ) — low byte of
10929/// the packed `pre[1]` word.
10930#[inline]
10931pub fn fdflags(f: &[u32]) -> u32 {
10932 // `pre[1]` is a u32 viewed as 4 bytes; flags = byte 0.
10933 f[1] & 0xff
10934}
10935
10936/// Port of `fdsetflags()` from `Src/parse.c:3132` — C decl `#define fdsetflags(f,v) fdsetbyte(f, 0, v)`. C macro (via `fdsetbyte`, c:3128).
10937/// ) — write
10938/// the low byte of `pre[1]`.
10939#[inline]
10940pub fn fdsetflags(f: &mut [u32], v: u8) {
10941 f[1] = (f[1] & !0xff) | (v as u32);
10942}
10943
10944/// Port of `fdother()` from `Src/parse.c:3133` — C decl `#define fdother(f) (fdbyte(f, 1) + (fdbyte(f, 2) << 8) + (fdbyte(f, 3) << 16))`. C macro.
10945/// ) — high 24 bits
10946/// of `pre[1]`, holds the byte-offset to the opposite-byte-order
10947/// dump copy.
10948#[inline]
10949pub fn fdother(f: &[u32]) -> u32 {
10950 (f[1] >> 8) & 0x00ff_ffff
10951}
10952
10953/// Port of `fdsetother()` from `Src/parse.c:3134` — C decl `#define fdsetother(f, o) do { fdsetbyte(f, 1, ((o) & 0xff)); fdsetbyte(f, 2, (((o) >> 8) & 0xff)); fdsetbyte(f, 3, (((o) >> 16) & 0xff)); } while (0)`. C macro.
10954/// ).
10955#[inline]
10956pub fn fdsetother(f: &mut [u32], o: u32) {
10957 f[1] = (f[1] & 0xff) | ((o & 0x00ff_ffff) << 8);
10958}
10959
10960/// Port of `fdversion()` from `Src/parse.c:3140` — C decl `#define fdversion(f) ((char *) ((f) + 2))`. C macro; the Rust port copies the NUL-terminated string out instead of returning a pointer into the dump.
10961/// ) — read the
10962/// `ZSH_VERSION` C-string from `pre[2..]`.
10963pub fn fdversion(f: &[u32]) -> String {
10964 let bytes: Vec<u8> = f[2..]
10965 .iter()
10966 .take(10)
10967 .flat_map(|w| w.to_le_bytes().into_iter())
10968 .collect();
10969 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
10970 String::from_utf8_lossy(&bytes[..end]).into_owned()
10971}
10972
10973/// Port of `firstfdhead` from `Src/parse.c:3142` — C decl `#define firstfdhead(f) ((FDHead) (((Wordcode) (f)) + FD_PRELEN))`. Rust fn `firstfdhead_offset` returns the u32-word OFFSET of the first `fdhead` rather than a `FDHead` pointer.
10974/// ) — pointer
10975/// to the first `struct fdhead` past the prelude.
10976#[inline]
10977pub fn firstfdhead_offset() -> usize {
10978 FD_PRELEN
10979}
10980
10981/// Port of `nextfdhead` from `Src/parse.c:3143` — C decl `#define nextfdhead(f) ((FDHead) (((Wordcode) (f)) + (f)->hlen))`. Rust fn `nextfdhead_offset` returns the u32-word OFFSET of the next `fdhead` rather than a `FDHead` pointer.
10982/// ) — advance to
10983/// the next header by reading the current `hlen` slot.
10984#[inline]
10985pub fn nextfdhead_offset(f: &[u32], cur: usize) -> usize {
10986 cur + (f[cur + 4] as usize) // .hlen is field 4 of fdhead
10987}
10988
10989/// Port of `fdhflags()` from `Src/parse.c:3145` — C decl `#define fdhflags(f) (((FDHead) (f))->flags)`. C macro.
10990/// ) — low 2 bits
10991/// of the header's `flags` field (the kshload/zshload marker).
10992#[inline]
10993pub fn fdhflags(h: &fdhead) -> u32 {
10994 h.flags & 0x3
10995}
10996
10997/// Port of `fdhtail()` from `Src/parse.c:3146` — C decl `#define fdhtail(f) (((FDHead) (f))->flags >> 2)`. C macro.
10998/// ) — high 30 bits
10999/// of `flags`, byte offset from the name start to its basename.
11000#[inline]
11001pub fn fdhtail(h: &fdhead) -> u32 {
11002 h.flags >> 2
11003}
11004
11005/// Port of `fdhbldflags()` from `Src/parse.c:3147` — C decl `#define fdhbldflags(f,t) ((f) | ((t) << 2))`. C macro.
11006/// ) — pack
11007/// `(flags, tail)` into one u32 (low 2 bits = flags, high 30 = tail).
11008#[inline]
11009pub fn fdhbldflags(flags: u32, tail: u32) -> u32 {
11010 flags | (tail << 2)
11011}
11012
11013/// Port of `fdname()` from `Src/parse.c:3152` — C decl `#define fdname(f) ((char *) (((FDHead) (f)) + 1))`. C macro; the Rust port copies the NUL-terminated name out of the dump buffer.
11014/// ) — name string
11015/// follows the fdhead record immediately. Reads bytes from the
11016/// dump buffer until NUL.
11017pub fn fdname(buf: &[u32], header_offset: usize) -> String {
11018 let name_word_off = header_offset + FDHEAD_WORDS;
11019 let bytes: Vec<u8> = buf[name_word_off..]
11020 .iter()
11021 .flat_map(|w| w.to_le_bytes().into_iter())
11022 .take_while(|&b| b != 0)
11023 .collect();
11024 String::from_utf8_lossy(&bytes).into_owned()
11025}
11026
11027/// !!! WARNING: RUST-ONLY HELPER !!!
11028///
11029/// No function of this name exists in `Src/parse.c`. This helper stands in
11030/// for decoding one `struct fdhead` record (c:3116-3123) out of a `&[u32]`
11031/// dump buffer. C needs no decoder: it casts the buffer to `FDHead` and reads
11032/// the struct in place.
11033/// Decode a `fdhead` record at the given u32-word offset in the
11034/// dump buffer. Used by the header-walk loops in `bin_zcompile -t`.
11035pub fn read_fdhead(buf: &[u32], offset: usize) -> Option<fdhead> {
11036 if offset + FDHEAD_WORDS > buf.len() {
11037 return None;
11038 }
11039 Some(fdhead {
11040 start: buf[offset],
11041 len: buf[offset + 1],
11042 npats: buf[offset + 2],
11043 strs: buf[offset + 3],
11044 hlen: buf[offset + 4],
11045 flags: buf[offset + 5],
11046 })
11047}
11048
11049/// Port of `freedump()` from `Src/parse.c:3976` — C signature `freedump(FuncDump f)`. C
11050/// `munmap`s, `zclose`s the fd, and frees the struct. The Rust
11051/// port relies on Drop for the `funcdump` (no mmap held in this
11052/// port — `addr`/`map` are byte-offset placeholders), so the
11053/// equivalent is removing the entry from the dumps list. Called
11054/// by `decrdumpcount` when the refcount hits zero (c:3988) and
11055/// by `closedumps` when shutting down (c:4008).
11056fn freedump_locked(g: &mut std::sync::MutexGuard<'_, Vec<funcdump>>, filename: &str) {
11057 // c:3976
11058 g.retain(|d| d.filename.as_deref() != Some(filename));
11059}
11060
11061// =====================================================================
11062// Remaining `Src/parse.c` ports (this section finishes the file).
11063//
11064// Several of these emit into the C-wordcode buffer (`ECBUF`/etc.) and
11065// are kept for completeness — the live zshrs runtime uses the
11066// `ZshProgram` AST path instead, but `bin_zcompile` (`-c`/`-a` modes)
11067// and any future `.zwc`-emit pipeline both call into these.
11068// =====================================================================
11069
11070/// Port of `ecstr()` from `Src/parse.c:473` — C decl `#define ecstr(S) ecadd(ecstrcode(S))`. C macro.
11071/// `ecstr(s)` helper — `ecadd(ecstrcode(s))`. Mirrors the C macro at
11072/// `Src/parse.c:473` used everywhere by the par_* emitters.
11073#[inline]
11074pub fn ecstr(s: &str) {
11075 let code = ecstrcode(s);
11076 ecadd(code);
11077}
11078
11079/// Port of the `condlex` function-pointer global from `Src/parse.c:2399` —
11080/// C decl `void (*condlex) (void) = zshlex;`. C repoints it between `zshlex`
11081/// and `testlex` (builtin.c:7200); `CONDLEX_TESTLEX` is that pointer, true
11082/// while it addresses `testlex`. `bin_test` sets it around `parse_cond`
11083/// (builtin.c:7278, 7280) and `par_cond_2` compares it (`condlex == testlex`,
11084/// c:2480) exactly as C does.
11085thread_local! {
11086 pub static CONDLEX_TESTLEX: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
11087}
11088
11089/// Port of calling through `condlex` from `Src/parse.c:2399`: dispatches to
11090/// `testlex` while `CONDLEX_TESTLEX` is set, otherwise to `zshlex`.
11091#[inline]
11092pub fn condlex() {
11093 if CONDLEX_TESTLEX.get() {
11094 crate::ported::builtin::testlex();
11095 } else {
11096 zshlex();
11097 }
11098}
11099
11100/// !!! WARNING: RUST-ONLY HELPER !!!
11101///
11102/// No function of this name exists in `Src/parse.c`. This helper stands in
11103/// for the recursive tree walk that C writes as a self-recursive call inside
11104/// `copy_ecstr` itself (c:537-545).
11105fn copy_ecstr_walk(node: &Option<Box<EccstrNode>>, p: &mut [u8]) {
11106 let mut cur = node.as_ref();
11107 while let Some(n) = cur {
11108 // c:540 — `memcpy(p + s->aoffs, s->str, strlen(s->str) + 1);`
11109 let off = n.aoffs as usize;
11110 let need = off + n.str.len() + 1;
11111 if need <= p.len() {
11112 p[off..off + n.str.len()].copy_from_slice(&n.str);
11113 p[off + n.str.len()] = 0;
11114 }
11115 // c:541 — `copy_ecstr(s->left, p);`
11116 copy_ecstr_walk(&n.left, p);
11117 // c:542 — `s = s->right;`
11118 cur = n.right.as_ref();
11119 }
11120}
11121
11122/// Port of `par_cond()` from `Src/parse.c:2409` — C signature `par_cond(void)`. Top-level cond
11123/// OR-chain — drives `par_cond_1` and stitches `||`-separated terms
11124/// with `WCB_COND(COND_OR, …)`. This is the missing top of the
11125/// wordcode cond chain: `par_cond_wordcode` (the par_dinbrack port)
11126/// must call into HERE so that `[[ a || b ]]` and friends land
11127/// real WC_COND opcodes in `ecbuf`. Without this, the wordcode
11128/// emitter for `[[ ... ]]` produced zero words and parity dropped
11129/// 148 words on `/etc/zshrc` alone.
11130pub fn par_cond_top() -> i32 {
11131 // c:2411 — `int p = ecused, r;`
11132 let p = ECUSED.with(|c| c.get()) as usize;
11133 let r = par_cond_1();
11134 while COND_SEP() {
11135 condlex();
11136 }
11137 if tok() == DBAR {
11138 // c:2417 — `condlex(); while (COND_SEP()) condlex();`
11139 condlex();
11140 while COND_SEP() {
11141 condlex();
11142 }
11143 // c:2420-2422 — `ecispace(p, 1); par_cond(); ecbuf[p] =
11144 // WCB_COND(COND_OR, ecused-1-p);`
11145 ecispace(p, 1);
11146 par_cond_top();
11147 let ecused = ECUSED.with(|c| c.get()) as usize;
11148 ECBUF.with(|c| {
11149 c.borrow_mut()[p] = WCB_COND(COND_OR as u32, (ecused - 1 - p) as u32);
11150 });
11151 return 1;
11152 }
11153 r
11154}
11155
11156/// Port of `check_cond()` from `Src/parse.c:2459` — C decl `static int check_cond(const char *input, const char *cond)`.
11157/// True iff `input` is the two-char `-X`
11158/// form whose `X` matches `cond` — used by par_cond_2 to detect
11159/// `-a` / `-o` n-ary chain operators and by build_dump for `-k` /
11160/// `-z`. C: `return !IS_DASH(input[0]) ? 0 : !strcmp(input+1, cond);`.
11161pub fn check_cond(input: &str, cond: &str) -> bool {
11162 let mut chars = input.chars();
11163 match chars.next() {
11164 Some(c) if IS_DASH(c) => chars.as_str() == cond,
11165 _ => false,
11166 }
11167}
11168
11169#[cfg(test)]
11170mod tests {
11171 use super::*;
11172 use crate::utils::{errflag, ERRFLAG_ERROR};
11173 use std::fs;
11174 use std::path::Path;
11175 use std::sync::atomic::Ordering;
11176 use std::sync::mpsc;
11177 use std::thread;
11178 use std::time::Duration;
11179
11180 /// `try_source_file` MUST refuse a stale `.zwc` cache when the
11181 /// uncompiled source has been modified more recently. The C body
11182 /// at c:3819 reads `stc.st_mtime >= stn.st_mtime` — explicitly
11183 /// `>=`, meaning only an equal-or-newer zwc is acceptable.
11184 ///
11185 /// A regression that ignored the mtime check (or used the wrong
11186 /// direction) would silently keep loading the OLD compiled body
11187 /// after the user edited the source file — every `source foo.zsh`
11188 /// would replay yesterday's code, the worst-class shell bug.
11189 ///
11190 /// Pin: create source + .zwc, then touch source to make it
11191 /// newer. try_source_file must return None.
11192 #[test]
11193 fn try_source_file_skips_stale_zwc() {
11194 let _g = crate::test_util::global_state_lock();
11195 let dir = tempfile::tempdir().expect("tempdir");
11196 let src = dir.path().join("script.zsh");
11197 let zwc = dir.path().join("script.zsh.zwc");
11198 // Create zwc FIRST (older), then source (newer).
11199 fs::write(&zwc, b"placeholder zwc").unwrap();
11200 thread::sleep(Duration::from_millis(20));
11201 fs::write(&src, b"echo hi").unwrap();
11202
11203 let result = try_source_file(src.to_str().unwrap());
11204 assert!(
11205 result.is_none(),
11206 "c:3819 — stale .zwc (older than source) MUST be rejected; \
11207 got {:?}",
11208 result
11209 );
11210 }
11211
11212 /// `try_source_file` returns None when no `.zwc` exists for the
11213 /// requested file (c:3819 `if let Ok(meta_c) = &stc` gate fails).
11214 /// This is the common case — most user scripts don't ship with
11215 /// a pre-compiled `.zwc`. The fn returning None lets the caller
11216 /// fall through to the source-read path. A regression that
11217 /// returned `Some(file)` on missing `.zwc` would route every
11218 /// `source foo.zsh` through `check_dump_file` against a
11219 /// non-existent file and crash.
11220 #[test]
11221 fn try_source_file_returns_none_when_no_zwc() {
11222 let _g = crate::test_util::global_state_lock();
11223 let dir = tempfile::tempdir().expect("tempdir");
11224 let src = dir.path().join("plain.zsh");
11225 fs::write(&src, b"echo hi").unwrap();
11226 // No .zwc sibling.
11227
11228 let result = try_source_file(src.to_str().unwrap());
11229 assert!(
11230 result.is_none(),
11231 "c:3819 gate fails when stat(wc) returns Err → None"
11232 );
11233 }
11234
11235 /// Test helper. Mirrors zsh's `errflag` save/clear/check pattern
11236 /// around a parse — see `Src/init.c:loop` which clears errflag
11237 /// before parse_event() and tests it after. Returns `Err` if the
11238 /// parse set `ERRFLAG_ERROR`; otherwise `Ok(program)`.
11239 fn parse(input: &str) -> Result<ZshProgram, String> {
11240 let saved = errflag.load(Ordering::Relaxed);
11241 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
11242 parse_init(input);
11243 let prog = crate::ported::parse::parse();
11244 let had_err = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
11245 // Restore prior error bits; don't carry our new error into the
11246 // outer test runner.
11247 errflag.store(saved, Ordering::Relaxed);
11248 if had_err {
11249 Err("parse error".to_string())
11250 } else {
11251 Ok(prog)
11252 }
11253 }
11254
11255 #[test]
11256 fn test_simple_command() {
11257 let _g = crate::test_util::global_state_lock();
11258 let prog = parse("echo hello world").unwrap();
11259 assert_eq!(prog.lists.len(), 1);
11260 match &prog.lists[0].sublist.pipe.cmd {
11261 ZshCommand::Simple(s) => {
11262 assert_eq!(s.words, vec!["echo", "hello", "world"]);
11263 }
11264 _ => panic!("expected simple command"),
11265 }
11266 }
11267
11268 #[test]
11269 fn test_pipeline() {
11270 let _g = crate::test_util::global_state_lock();
11271 let prog = parse("ls | grep foo | wc -l").unwrap();
11272 assert_eq!(prog.lists.len(), 1);
11273
11274 let pipe = &prog.lists[0].sublist.pipe;
11275 assert!(pipe.next.is_some());
11276
11277 let pipe2 = pipe.next.as_ref().unwrap();
11278 assert!(pipe2.next.is_some());
11279 }
11280
11281 #[test]
11282 fn test_and_or() {
11283 let _g = crate::test_util::global_state_lock();
11284 let prog = parse("cmd1 && cmd2 || cmd3").unwrap();
11285 let sublist = &prog.lists[0].sublist;
11286
11287 assert!(sublist.next.is_some());
11288 let (op, _) = sublist.next.as_ref().unwrap();
11289 assert_eq!(*op, SublistOp::And);
11290 }
11291
11292 #[test]
11293 fn test_if_then() {
11294 let _g = crate::test_util::global_state_lock();
11295 let prog = parse("if test -f foo; then echo yes; fi").unwrap();
11296 match &prog.lists[0].sublist.pipe.cmd {
11297 ZshCommand::If(_) => {}
11298 _ => panic!("expected if command"),
11299 }
11300 }
11301
11302 #[test]
11303 fn test_for_loop() {
11304 let _g = crate::test_util::global_state_lock();
11305 let prog = parse("for i in a b c; do echo $i; done").unwrap();
11306 match &prog.lists[0].sublist.pipe.cmd {
11307 ZshCommand::For(f) => {
11308 assert_eq!(f.var, "i");
11309 match &f.list {
11310 ForList::Words(w) => assert_eq!(w, &vec!["a", "b", "c"]),
11311 _ => panic!("expected word list"),
11312 }
11313 }
11314 _ => panic!("expected for command"),
11315 }
11316 }
11317
11318 #[test]
11319 fn test_case() {
11320 let _g = crate::test_util::global_state_lock();
11321 let prog = parse("case $x in a) echo a;; b) echo b;; esac").unwrap();
11322 match &prog.lists[0].sublist.pipe.cmd {
11323 ZshCommand::Case(c) => {
11324 assert_eq!(c.arms.len(), 2);
11325 }
11326 _ => panic!("expected case command"),
11327 }
11328 }
11329
11330 #[test]
11331 fn test_function() {
11332 let _g = crate::test_util::global_state_lock();
11333 // First test just parsing "function foo" to see what happens
11334 let prog = parse("function foo { }").unwrap();
11335 match &prog.lists[0].sublist.pipe.cmd {
11336 ZshCommand::FuncDef(f) => {
11337 assert_eq!(f.names, vec!["foo"]);
11338 }
11339 _ => panic!(
11340 "expected function, got {:?}",
11341 prog.lists[0].sublist.pipe.cmd
11342 ),
11343 }
11344 }
11345
11346 #[test]
11347 fn test_redirection() {
11348 let _g = crate::test_util::global_state_lock();
11349 let prog = parse("echo hello > file.txt").unwrap();
11350 match &prog.lists[0].sublist.pipe.cmd {
11351 ZshCommand::Simple(s) => {
11352 assert_eq!(s.redirs.len(), 1);
11353 assert_eq!(s.redirs[0].rtype, REDIR_WRITE);
11354 }
11355 _ => panic!("expected simple command"),
11356 }
11357 }
11358
11359 /// Regression: `{var}>file` named-fd redirects accept any IIDENT name,
11360 /// including all-digit positional-parameter names like `{1}` (used by
11361 /// p10k's `_p9k_restore_prompt`: `exec {1}>&-`). The prior check required
11362 /// the first char to be alphabetic/`_`, which rejected `{1}`/`{2}`/`{12}`
11363 /// and made zshrs treat `{1}` as a command word — `command not found: {1}`.
11364 /// zsh's rule is `itype_end(tokstr+1, IIDENT, 0) >= ptr` (per-char IIDENT,
11365 /// no first-char restriction).
11366 #[test]
11367 fn test_fd_var_redir_digit_name() {
11368 let _g = crate::test_util::global_state_lock();
11369 for (src, want) in [
11370 ("echo hi {1}>file", "1"),
11371 ("echo hi {2}>file", "2"),
11372 ("echo hi {12}>file", "12"),
11373 ("echo hi {myfd}>file", "myfd"),
11374 ("echo hi {fd1}>file", "fd1"),
11375 ] {
11376 let prog = parse(src).unwrap_or_else(|_| panic!("failed to parse {src:?}"));
11377 match &prog.lists[0].sublist.pipe.cmd {
11378 ZshCommand::Simple(s) => {
11379 assert_eq!(s.redirs.len(), 1, "{src:?}");
11380 assert_eq!(
11381 s.redirs[0].varid.as_deref(),
11382 Some(want),
11383 "{src:?} should carry fd-var {want:?}"
11384 );
11385 }
11386 _ => panic!("expected simple command for {src:?}"),
11387 }
11388 }
11389 }
11390
11391 #[test]
11392 fn test_assignment() {
11393 let _g = crate::test_util::global_state_lock();
11394 let prog = parse("FOO=bar echo $FOO").unwrap();
11395 match &prog.lists[0].sublist.pipe.cmd {
11396 ZshCommand::Simple(s) => {
11397 assert_eq!(s.assigns.len(), 1);
11398 assert_eq!(s.assigns[0].name, "FOO");
11399 }
11400 _ => panic!("expected simple command"),
11401 }
11402 }
11403
11404 #[test]
11405 fn test_parse_completion_function() {
11406 let _g = crate::test_util::global_state_lock();
11407 let input = r#"_2to3_fixes() {
11408 local -a fixes
11409 fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
11410 (( ${#fixes} )) && _describe -t fixes 'fix' fixes
11411}"#;
11412 let result = parse(input);
11413 assert!(
11414 result.is_ok(),
11415 "Failed to parse completion function: {:?}",
11416 result.err()
11417 );
11418 let prog = result.unwrap();
11419 assert!(
11420 !prog.lists.is_empty(),
11421 "Expected at least one list in program"
11422 );
11423 }
11424
11425 #[test]
11426 fn test_parse_array_with_complex_elements() {
11427 let _g = crate::test_util::global_state_lock();
11428 let input = r#"arguments=(
11429 '(- * :)'{-h,--help}'[show this help message and exit]'
11430 {-d,--doctests_only}'[fix up doctests only]'
11431 '*:filename:_files'
11432)"#;
11433 let result = parse(input);
11434 assert!(
11435 result.is_ok(),
11436 "Failed to parse array assignment: {:?}",
11437 result.err()
11438 );
11439 }
11440
11441 #[test]
11442 fn test_parse_full_completion_file() {
11443 let _g = crate::test_util::global_state_lock();
11444 let input = r##"#compdef 2to3
11445
11446# zsh completions for '2to3'
11447
11448_2to3_fixes() {
11449 local -a fixes
11450 fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
11451 (( ${#fixes} )) && _describe -t fixes 'fix' fixes
11452}
11453
11454local -a arguments
11455
11456arguments=(
11457 '(- * :)'{-h,--help}'[show this help message and exit]'
11458 {-d,--doctests_only}'[fix up doctests only]'
11459 {-f,--fix}'[each FIX specifies a transformation; default: all]:fix name:_2to3_fixes'
11460 {-j,--processes}'[run 2to3 concurrently]:number: '
11461 {-x,--nofix}'[prevent a transformation from being run]:fix name:_2to3_fixes'
11462 {-l,--list-fixes}'[list available transformations]'
11463 {-p,--print-function}'[modify the grammar so that print() is a function]'
11464 {-v,--verbose}'[more verbose logging]'
11465 '--no-diffs[do not show diffs of the refactoring]'
11466 {-w,--write}'[write back modified files]'
11467 {-n,--nobackups}'[do not write backups for modified files]'
11468 {-o,--output-dir}'[put output files in this directory instead of overwriting]:directory:_directories'
11469 {-W,--write-unchanged-files}'[also write files even if no changes were required]'
11470 '--add-suffix[append this string to all output filenames]:suffix: '
11471 '*:filename:_files'
11472)
11473
11474_arguments -s -S $arguments
11475"##;
11476 let result = parse(input);
11477 assert!(
11478 result.is_ok(),
11479 "Failed to parse full completion file: {:?}",
11480 result.err()
11481 );
11482 let prog = result.unwrap();
11483 // Should have parsed successfully with at least one statement
11484 assert!(!prog.lists.is_empty(), "Expected at least one list");
11485 }
11486
11487 #[test]
11488 fn test_parse_logs_sh() {
11489 let _g = crate::test_util::global_state_lock();
11490 let input = r#"#!/usr/bin/env bash
11491shopt -s globstar
11492
11493if [[ $(uname) == Darwin ]]; then
11494 tail -f /var/log/**/*.log /var/log/**/*.out | lolcat
11495else
11496 if [[ $ZPWR_DISTRO_NAME == raspbian ]]; then
11497 tail -f /var/log/**/*.log | lolcat
11498 else
11499 printf "Unsupported...\n" >&2
11500 fi
11501fi
11502"#;
11503 let result = parse(input);
11504 assert!(
11505 result.is_ok(),
11506 "Failed to parse logs.sh: {:?}",
11507 result.err()
11508 );
11509 }
11510
11511 #[test]
11512 fn test_parse_case_with_glob() {
11513 let _g = crate::test_util::global_state_lock();
11514 let input = r#"case "$ZPWR_OS_TYPE" in
11515 darwin*) open_cmd='open'
11516 ;;
11517 cygwin*) open_cmd='cygstart'
11518 ;;
11519 linux*)
11520 open_cmd='xdg-open'
11521 ;;
11522esac"#;
11523 let result = parse(input);
11524 assert!(
11525 result.is_ok(),
11526 "Failed to parse case with glob: {:?}",
11527 result.err()
11528 );
11529 }
11530
11531 #[test]
11532 fn test_parse_case_with_nested_if() {
11533 let _g = crate::test_util::global_state_lock();
11534 // Test case with nested if and glob patterns
11535 let input = r##"function zpwrGetOpenCommand(){
11536 local open_cmd
11537 case "$ZPWR_OS_TYPE" in
11538 darwin*) open_cmd='open' ;;
11539 cygwin*) open_cmd='cygstart' ;;
11540 linux*)
11541 if [[ "$_zpwr_uname_r" != *icrosoft* ]];then
11542 open_cmd='nohup xdg-open'
11543 fi
11544 ;;
11545 esac
11546}"##;
11547 let result = parse(input);
11548 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
11549 }
11550
11551 #[test]
11552 fn test_parse_zpwr_scripts() {
11553 let _g = crate::test_util::global_state_lock();
11554 let scripts_dir = Path::new("/Users/wizard/.zpwr/scripts");
11555 if !scripts_dir.exists() {
11556 eprintln!("Skipping test: scripts directory not found");
11557 return;
11558 }
11559
11560 let mut total = 0;
11561 let mut passed = 0;
11562 let mut failed_files = Vec::new();
11563 let mut timeout_files = Vec::new();
11564
11565 for ext in &["sh", "zsh"] {
11566 let pattern = scripts_dir.join(format!("*.{}", ext));
11567 if let Ok(entries) = glob::glob(pattern.to_str().unwrap()) {
11568 for entry in entries.flatten() {
11569 total += 1;
11570 let file_path = entry.display().to_string();
11571 let content = match fs::read_to_string(&entry) {
11572 Ok(c) => c,
11573 Err(e) => {
11574 failed_files.push((file_path, format!("read error: {}", e)));
11575 continue;
11576 }
11577 };
11578
11579 // Parse with timeout
11580 let content_clone = content.clone();
11581 let (tx, rx) = mpsc::channel();
11582 let handle = thread::spawn(move || {
11583 let result = parse(&content_clone);
11584 let _ = tx.send(result);
11585 });
11586
11587 match rx.recv_timeout(Duration::from_secs(2)) {
11588 Ok(Ok(_)) => passed += 1,
11589 Ok(Err(err)) => {
11590 failed_files.push((file_path, err));
11591 }
11592 Err(_) => {
11593 timeout_files.push(file_path);
11594 // Thread will be abandoned
11595 }
11596 }
11597 }
11598 }
11599 }
11600
11601 eprintln!("\n=== ZPWR Scripts Parse Results ===");
11602 eprintln!("Passed: {}/{}", passed, total);
11603
11604 if !timeout_files.is_empty() {
11605 eprintln!("\nTimeout files (>2s):");
11606 for file in &timeout_files {
11607 eprintln!(" {}", file);
11608 }
11609 }
11610
11611 if !failed_files.is_empty() {
11612 eprintln!("\nFailed files:");
11613 for (file, err) in &failed_files {
11614 eprintln!(" {} - {}", file, err);
11615 }
11616 }
11617
11618 // Allow some failures initially, but track progress
11619 let pass_rate = if total > 0 {
11620 (passed as f64 / total as f64) * 100.0
11621 } else {
11622 0.0
11623 };
11624 eprintln!("Pass rate: {:.1}%", pass_rate);
11625
11626 // Require at least 50% pass rate for now
11627 assert!(pass_rate >= 50.0, "Pass rate too low: {:.1}%", pass_rate);
11628 }
11629
11630 /// c:2643 — `get_cond_num` returns 0..=8 for the canonical binary
11631 /// test operators in order `nt ot ef eq ne lt gt le ge`. The
11632 /// index IS the wordcode opcode dispatch key; flipping any entry
11633 /// would silently mis-dispatch `[[ a -eq b ]]` to a different op.
11634 #[test]
11635 fn get_cond_num_canonical_order_matches_dispatch_table() {
11636 let _g = crate::test_util::global_state_lock();
11637 assert_eq!(get_cond_num("nt"), 0);
11638 assert_eq!(get_cond_num("ot"), 1);
11639 assert_eq!(get_cond_num("ef"), 2);
11640 assert_eq!(get_cond_num("eq"), 3);
11641 assert_eq!(get_cond_num("ne"), 4);
11642 assert_eq!(get_cond_num("lt"), 5);
11643 assert_eq!(get_cond_num("gt"), 6);
11644 assert_eq!(get_cond_num("le"), 7);
11645 assert_eq!(get_cond_num("ge"), 8);
11646 }
11647
11648 /// c:2643 — unknown operator returns -1 (sentinel for "not in the
11649 /// binary set"). Regression returning 0 silently would alias
11650 /// every unknown op to `-nt`, dispatching to the wrong handler.
11651 #[test]
11652 fn get_cond_num_unknown_operator_returns_minus_one() {
11653 let _g = crate::test_util::global_state_lock();
11654 assert_eq!(get_cond_num("xx"), -1);
11655 assert_eq!(get_cond_num(""), -1);
11656 assert_eq!(get_cond_num("eqnt"), -1, "exact-match required");
11657 assert_eq!(
11658 get_cond_num("NT"),
11659 -1,
11660 "case-sensitive — uppercase rejected"
11661 );
11662 }
11663
11664 /// c:2628 — `par_cond_double` requires arg `a` to start with `-`
11665 /// AND have at least one more char. Empty string OR single `-`
11666 /// must error (return 1 via zerr). Regression accepting empty
11667 /// would dispatch `[[ "" string ]]` as a unary test.
11668 #[test]
11669 fn par_cond_double_rejects_short_or_non_dash_first_arg() {
11670 let _g = crate::test_util::global_state_lock();
11671 // empty
11672 let _ = par_cond_double("", "b");
11673 // not-dash
11674 let _ = par_cond_double("foo", "b");
11675 // bare dash
11676 let _ = par_cond_double("-", "b");
11677 // All three must NOT crash + return 1 (error path).
11678 }
11679
11680 /// c:89-96 `COND_ERROR` — `YYERROR(ecused)` is `tok = LEXERR; return 0`,
11681 /// so every cond-term error path returns 0 and leaves the lexer in
11682 /// LEXERR, with `ERRFLAG_ERROR` raised unless `noerrs == 2`.
11683 #[test]
11684 fn par_cond_errors_return_zero_and_set_lexerr() {
11685 let _g = crate::test_util::global_state_lock();
11686 let reset = || {
11687 crate::ported::utils::errflag.store(0, Ordering::SeqCst);
11688 set_tok(crate::ported::zsh_h::NULLTOK);
11689 };
11690 let errored = || {
11691 tok() == LEXERR
11692 && crate::ported::utils::errflag.load(Ordering::SeqCst) & ERRFLAG_ERROR != 0
11693 };
11694
11695 reset();
11696 assert_eq!(par_cond_double("foo", "b"), 0); // c:2629
11697 assert!(errored());
11698
11699 reset();
11700 assert_eq!(par_cond_triple("a", "foo", "c"), 0); // c:2709
11701 assert!(errored());
11702
11703 reset();
11704 assert_eq!(par_cond_multi("a", &["b".to_string()]), 0); // c:2719
11705 assert!(errored());
11706
11707 // c:92 — `if (noerrs != 2) errflag |= ERRFLAG_ERROR;`
11708 reset();
11709 *crate::ported::utils::noerrs_lock().lock().unwrap() = 2;
11710 let r = par_cond_triple("a", "foo", "c");
11711 *crate::ported::utils::noerrs_lock().lock().unwrap() = 0;
11712 assert_eq!(r, 0);
11713 assert_eq!(tok(), LEXERR);
11714 assert_eq!(crate::ported::utils::errflag.load(Ordering::SeqCst), 0);
11715 reset();
11716 }
11717
11718 /// c:2647 CONDSTRS table — exhaustive iteration: every entry's
11719 /// index round-trips through get_cond_num. A regression that
11720 /// drops an entry would let `[[ a -ef b ]]` silently mis-dispatch.
11721 #[test]
11722 fn get_cond_num_round_trips_for_every_table_entry() {
11723 let _g = crate::test_util::global_state_lock();
11724 for (i, op) in ["nt", "ot", "ef", "eq", "ne", "lt", "gt", "le", "ge"]
11725 .iter()
11726 .enumerate()
11727 {
11728 assert_eq!(get_cond_num(op) as usize, i, "{op} must map to index {i}");
11729 }
11730 }
11731
11732 /// c:2643 — `get_cond_num` is byte-exact: a partial-prefix string
11733 /// must NOT match. `e` (one char) is not `eq`. Catches a
11734 /// regression using `starts_with` instead of equality.
11735 #[test]
11736 fn get_cond_num_partial_prefix_does_not_match() {
11737 let _g = crate::test_util::global_state_lock();
11738 assert_eq!(get_cond_num("e"), -1);
11739 assert_eq!(get_cond_num("eq2"), -1);
11740 assert_eq!(get_cond_num("n"), -1);
11741 }
11742
11743 /// c:2628 — `par_cond_double` checks `IS_DASH(ac[0])` so any
11744 /// non-dash first char fails. The lexed Dash sentinel `\u{e19b}`
11745 /// MUST be accepted alongside ASCII `-` (the lexer emits it
11746 /// inside `[[ ... ]]`). Regression dropping the sentinel form
11747 /// would break every cond expression after lexing.
11748 #[test]
11749 fn par_cond_double_accepts_lexed_dash_sentinel() {
11750 let _g = crate::test_util::global_state_lock();
11751 // First char being the Dash sentinel + valid unary letter
11752 // must NOT trigger the "condition expected" error path.
11753 // We can't easily probe the wordcode emission here, but
11754 // the function MUST return without panic for both forms.
11755 let _ = par_cond_double("-z", "foo");
11756 let _ = par_cond_double("\u{e19b}z", "foo");
11757 }
11758
11759 /// c:2643 — case sensitivity: uppercase `EQ` MUST NOT match `eq`.
11760 /// zsh's `[[ a -EQ b ]]` is documented as a parse error (only
11761 /// lowercase variants are recognised). Regression doing
11762 /// case-insensitive lookup would silently accept it.
11763 #[test]
11764 fn get_cond_num_is_case_sensitive() {
11765 let _g = crate::test_util::global_state_lock();
11766 assert_eq!(get_cond_num("EQ"), -1);
11767 assert_eq!(get_cond_num("Eq"), -1);
11768 assert_eq!(get_cond_num("eQ"), -1);
11769 // Lowercase still works.
11770 assert_eq!(get_cond_num("eq"), 3);
11771 }
11772
11773 /// `Src/parse.c:2862-2868` — `ecgetstr` inline-3-byte case packs
11774 /// up to 3 chars into bits 3-26 of the wordcode word, then C emits
11775 /// `buf[3] = '\0'; r = dupstring(buf);`. `dupstring` uses `strlen`
11776 /// so the resulting string TRUNCATES at the first NUL byte —
11777 /// short strings of 1 or 2 chars get their tail NUL-padded and
11778 /// silently dropped by strlen.
11779 ///
11780 /// The previous Rust port used `retain(|&x| x != 0)` which SPLICES
11781 /// OUT interior NULs (so `[a, 0, b]` would yield "ab" instead of
11782 /// C's "a"). Verify both endpoints work correctly:
11783 /// * 1-char string ("a", 0, 0) → "a" (strlen-truncate)
11784 /// * 2-char string ("ab", 0) → "ab" (strlen-truncate)
11785 /// * 3-char string ("abc") → "abc" (full)
11786 /// * pathological ("a", 0, "b") → "a" (NOT "ab")
11787 #[test]
11788 fn ecgetstr_inline_string_truncates_at_first_nul_like_c_strlen() {
11789 let _g = crate::test_util::global_state_lock();
11790 // Build a wordcode word with `c & 2 != 0` (inline-string flag)
11791 // and the 3 bytes packed at offsets 3, 11, 19. `c & 1` is the
11792 // tokflag; clear it for this test.
11793 fn pack_inline(b0: u8, b1: u8, b2: u8) -> u32 {
11794 // c:2862 layout — bit0 = tokflag (0 here), bit1 = inline (1),
11795 // bits 3-10 = b0, bits 11-18 = b1, bits 19-26 = b2.
11796 (2u32) | ((b0 as u32) << 3) | ((b1 as u32) << 11) | ((b2 as u32) << 19)
11797 }
11798 let mk_state = |word: u32| -> estate {
11799 let p = eprog {
11800 flags: 0,
11801 len: 1,
11802 npats: 0,
11803 nref: 0,
11804 pats: Vec::new(),
11805 prog: vec![word],
11806 strs: None,
11807 shf: None,
11808 dump: None,
11809 strs_metafied: false, // no pool (strs: None) — flag unused
11810 };
11811 estate {
11812 prog: Box::new(p),
11813 pc: 0,
11814 strs: None,
11815 strs_offset: 0,
11816 }
11817 };
11818
11819 // 1-char: ('a', 0, 0) → "a"
11820 let mut st = mk_state(pack_inline(b'a', 0, 0));
11821 assert_eq!(
11822 ecgetstr(&mut st, 0, None),
11823 "a",
11824 "c:2869 strlen truncates 1-char inline at the NUL tail"
11825 );
11826
11827 // 2-char: ('a', 'b', 0) → "ab"
11828 let mut st = mk_state(pack_inline(b'a', b'b', 0));
11829 assert_eq!(
11830 ecgetstr(&mut st, 0, None),
11831 "ab",
11832 "c:2869 strlen truncates 2-char inline at the NUL tail"
11833 );
11834
11835 // 3-char: ('a', 'b', 'c') → "abc"
11836 let mut st = mk_state(pack_inline(b'a', b'b', b'c'));
11837 assert_eq!(
11838 ecgetstr(&mut st, 0, None),
11839 "abc",
11840 "c:2869 full 3-byte inline preserved"
11841 );
11842
11843 // Pathological: ('a', 0, 'b') → "a" (NOT "ab" from retain-splice)
11844 let mut st = mk_state(pack_inline(b'a', 0, b'b'));
11845 assert_eq!(
11846 ecgetstr(&mut st, 0, None),
11847 "a",
11848 "c:2869 strlen STOPS at first NUL; must not splice 'b' through"
11849 );
11850 }
11851
11852 /// Pin: `init_parse_status` resets ALL six lexer-parser flags
11853 /// per `Src/parse.c:500-502`. Specifically `inrepeat_ = 0` at
11854 /// c:501 was previously missing in the Rust port. Pin every
11855 /// reset so a future regression that drops one is caught.
11856 #[test]
11857 fn init_parse_status_resets_all_lexer_parser_flags() {
11858 let _g = crate::test_util::global_state_lock();
11859 // Dirty every flag to a non-default value.
11860 set_incasepat(5);
11861 set_incond(7);
11862 set_inredir(true);
11863 set_infor(3);
11864 set_intypeset(true);
11865 set_inrepeat(2);
11866 set_incmdpos(false);
11867 // Reset.
11868 init_parse_status();
11869 // c:500-502 — every flag back to its default.
11870 assert_eq!(incasepat(), 0, "c:500 — incasepat = 0");
11871 assert_eq!(incond(), 0, "c:500 — incond = 0");
11872 assert!(!inredir(), "c:500 — inredir = 0");
11873 assert_eq!(infor(), 0, "c:500 — infor = 0");
11874 assert!(!intypeset(), "c:500 — intypeset = 0");
11875 assert_eq!(
11876 inrepeat(),
11877 0,
11878 "c:501 — inrepeat_ = 0 (was previously missing)"
11879 );
11880 assert!(incmdpos(), "c:502 — incmdpos = 1");
11881 }
11882
11883 // ═══════════════════════════════════════════════════════════════════
11884 // AST shape tests — feed source through parse(), walk the resulting
11885 // ZshProgram, assert structural properties. Each test uses the local
11886 // `parse(input)` helper that errors cleanly on parse failure.
11887 // Anchor: where applicable, behavior matches `zsh -n -c '...'`
11888 // (parse-only, no execution — which would error on syntax issues).
11889 // ═══════════════════════════════════════════════════════════════════
11890
11891 /// Empty input → ZshProgram with no lists.
11892 #[test]
11893 fn parse_empty_source_yields_zero_lists() {
11894 let _g = crate::test_util::global_state_lock();
11895 let prog = parse("").unwrap();
11896 assert_eq!(prog.lists.len(), 0);
11897 }
11898
11899 /// Comment-only input → no lists (comments are skipped at lex level).
11900 #[test]
11901 fn parse_only_comment_yields_zero_lists() {
11902 let _g = crate::test_util::global_state_lock();
11903 let prog = parse("# this is just a comment").unwrap();
11904 assert_eq!(prog.lists.len(), 0, "comments alone produce no cmds");
11905 }
11906
11907 /// Three commands separated by `;` → three lists.
11908 #[test]
11909 fn parse_three_semicolon_separated_commands_yield_three_lists() {
11910 let _g = crate::test_util::global_state_lock();
11911 let prog = parse("a; b; c").unwrap();
11912 assert_eq!(prog.lists.len(), 3);
11913 }
11914
11915 /// Background command — async flag set on the list.
11916 #[test]
11917 fn parse_background_command_sets_async_flag() {
11918 let _g = crate::test_util::global_state_lock();
11919 let prog = parse("sleep 1 &").unwrap();
11920 assert_eq!(prog.lists.len(), 1);
11921 assert!(
11922 prog.lists[0].flags.async_,
11923 "trailing `&` must set async_ flag"
11924 );
11925 }
11926
11927 /// Pipe count: `a | b | c | d` → 4 stages.
11928 #[test]
11929 fn parse_four_stage_pipeline_has_three_next_links() {
11930 let _g = crate::test_util::global_state_lock();
11931 let prog = parse("a | b | c | d").unwrap();
11932 let mut pipe = &prog.lists[0].sublist.pipe;
11933 let mut count = 1;
11934 while let Some(next) = &pipe.next {
11935 pipe = next;
11936 count += 1;
11937 }
11938 assert_eq!(count, 4, "4 commands should produce 4 pipe stages");
11939 }
11940
11941 /// `|&` between pipeline stages sets merge_stderr.
11942 #[test]
11943 fn parse_pipe_amp_sets_merge_stderr() {
11944 let _g = crate::test_util::global_state_lock();
11945 let prog = parse("a |& b").unwrap();
11946 let pipe = &prog.lists[0].sublist.pipe;
11947 assert!(pipe.next.is_some());
11948 assert!(pipe.merge_stderr, "|& must set merge_stderr");
11949 }
11950
11951 /// `cmd1 || cmd2`: sublist.next is Some with `Or`.
11952 #[test]
11953 fn parse_or_operator_sets_sublist_op_or() {
11954 let _g = crate::test_util::global_state_lock();
11955 let prog = parse("cmd1 || cmd2").unwrap();
11956 let sublist = &prog.lists[0].sublist;
11957 let (op, _) = sublist.next.as_ref().expect("must have next");
11958 assert_eq!(*op, SublistOp::Or);
11959 }
11960
11961 /// `! cmd` sets the not flag on the sublist.
11962 #[test]
11963 fn parse_bang_negation_sets_sublist_not_flag() {
11964 let _g = crate::test_util::global_state_lock();
11965 let prog = parse("! false").unwrap();
11966 let sublist = &prog.lists[0].sublist;
11967 assert!(sublist.flags.not, "`!` prefix must set sublist.flags.not");
11968 }
11969
11970 // ── Compound commands ────────────────────────────────────────────
11971 /// `while cond; do body; done` → ZshCommand::While.
11972 #[test]
11973 fn parse_while_loop_yields_while_command() {
11974 let _g = crate::test_util::global_state_lock();
11975 let prog = parse("while true; do echo x; done").unwrap();
11976 assert!(matches!(
11977 prog.lists[0].sublist.pipe.cmd,
11978 ZshCommand::While(_)
11979 ));
11980 }
11981
11982 /// `until cond; do body; done` → ZshCommand::Until.
11983 /// Anchor: `zsh -n -c 'until false; do echo; done'` accepts and parses
11984 /// as an until-loop. zshrs accepts but emits a DIFFERENT AST variant
11985 /// (not Until). Bug — until loop is mis-classified.
11986 #[test]
11987 fn parse_until_loop_yields_until_command_anchored_to_zsh() {
11988 let _g = crate::test_util::global_state_lock();
11989 let prog = parse("until false; do echo x; done").unwrap();
11990 assert!(
11991 matches!(prog.lists[0].sublist.pipe.cmd, ZshCommand::Until(_)),
11992 "zsh parses `until` as Until variant; zshrs uses different variant: {:?}",
11993 prog.lists[0].sublist.pipe.cmd
11994 );
11995 }
11996
11997 /// `(cmd)` → Subsh variant.
11998 #[test]
11999 fn parse_parens_yield_subsh_command() {
12000 let _g = crate::test_util::global_state_lock();
12001 let prog = parse("(echo hi)").unwrap();
12002 assert!(matches!(
12003 prog.lists[0].sublist.pipe.cmd,
12004 ZshCommand::Subsh(_)
12005 ));
12006 }
12007
12008 /// `{ cmd; }` → Cursh (current-shell) command.
12009 #[test]
12010 fn parse_braces_yield_cursh_command() {
12011 let _g = crate::test_util::global_state_lock();
12012 let prog = parse("{ echo hi; }").unwrap();
12013 assert!(matches!(
12014 prog.lists[0].sublist.pipe.cmd,
12015 ZshCommand::Cursh(_)
12016 ));
12017 }
12018
12019 /// `[[ a == b ]]` → ZshCommand::Cond.
12020 #[test]
12021 fn parse_double_brackets_yield_cond_command() {
12022 let _g = crate::test_util::global_state_lock();
12023 let prog = parse("[[ a == b ]]").unwrap();
12024 assert!(matches!(
12025 prog.lists[0].sublist.pipe.cmd,
12026 ZshCommand::Cond(_)
12027 ));
12028 }
12029
12030 /// `(( 1 + 2 ))` → ZshCommand::Arith.
12031 #[test]
12032 fn parse_double_parens_yield_arith_command() {
12033 let _g = crate::test_util::global_state_lock();
12034 let prog = parse("(( 1 + 2 ))").unwrap();
12035 assert!(matches!(
12036 prog.lists[0].sublist.pipe.cmd,
12037 ZshCommand::Arith(_)
12038 ));
12039 }
12040
12041 /// `repeat 3 do echo x; done` → ZshCommand::Repeat.
12042 #[test]
12043 fn parse_repeat_loop_yields_repeat_command() {
12044 let _g = crate::test_util::global_state_lock();
12045 let prog = parse("repeat 3 do echo x; done").unwrap();
12046 assert!(matches!(
12047 prog.lists[0].sublist.pipe.cmd,
12048 ZshCommand::Repeat(_)
12049 ));
12050 }
12051
12052 // ── Function definitions ─────────────────────────────────────────
12053 /// `name() { body; }` → FuncDef variant.
12054 #[test]
12055 fn parse_paren_funcdef_yields_funcdef_command() {
12056 let _g = crate::test_util::global_state_lock();
12057 let prog = parse("greet() { echo hi; }").unwrap();
12058 assert!(matches!(
12059 prog.lists[0].sublist.pipe.cmd,
12060 ZshCommand::FuncDef(_)
12061 ));
12062 }
12063
12064 /// `function name { body; }` → FuncDef variant (zsh keyword form).
12065 #[test]
12066 fn parse_function_keyword_funcdef_yields_funcdef_command() {
12067 let _g = crate::test_util::global_state_lock();
12068 let prog = parse("function greet { echo hi; }").unwrap();
12069 assert!(matches!(
12070 prog.lists[0].sublist.pipe.cmd,
12071 ZshCommand::FuncDef(_)
12072 ));
12073 }
12074
12075 /// Syntax error — `if` without `fi` → parse returns Err.
12076 /// Anchor: `echo 'if true; then echo' | zsh -n` → "parse error".
12077 #[test]
12078 fn parse_unterminated_if_returns_error_anchored_to_zsh() {
12079 let _g = crate::test_util::global_state_lock();
12080 let r = parse("if true; then echo yes");
12081 assert!(r.is_err(), "zsh -n: parse error near `\\n`");
12082 }
12083
12084 /// Syntax error — bare `done` without `for/while/until` → error.
12085 /// Anchor: `echo done | zsh -n` → "parse error near `done`".
12086 #[test]
12087 fn parse_orphan_done_returns_error_anchored_to_zsh() {
12088 let _g = crate::test_util::global_state_lock();
12089 let r = parse("done");
12090 assert!(r.is_err(), "zsh -n: parse error near `done`");
12091 }
12092
12093 /// Simple command's words are metafied at the AST layer (matches
12094 /// zsh's internal representation: `-` lexes to `Dash` = 0x9b, `*`
12095 /// to `Star`, etc.). zsh untokenizes via `untokenize()` BEFORE
12096 /// surfacing words at execution time (Src/exec.c:execcmd_args).
12097 /// This test pins the round-trip: `untokenize(word)` recovers the
12098 /// user-visible form. If parse-time unmetafy ever lands the
12099 /// untokenize call becomes a no-op; the test stays green either
12100 /// way. Companion test below pins the metafied internal form.
12101 #[test]
12102 fn parse_simple_command_words_unmetafied_like_zsh_anchored() {
12103 let _g = crate::test_util::global_state_lock();
12104 let prog = parse("ls -la /tmp").unwrap();
12105 match &prog.lists[0].sublist.pipe.cmd {
12106 ZshCommand::Simple(s) => {
12107 let untok: Vec<String> = s
12108 .words
12109 .iter()
12110 .map(|w| crate::ported::lex::untokenize(w))
12111 .collect();
12112 assert_eq!(
12113 untok,
12114 vec!["ls", "-la", "/tmp"],
12115 "untokenize(word) must yield the user-visible form"
12116 );
12117 }
12118 other => panic!("expected Simple, got {other:?}"),
12119 }
12120 }
12121
12122 /// Pin the OBSERVED zshrs contract: simple-command word array
12123 /// contains metafied bytes. This is the active (passing) version
12124 /// of the anchor above — it documents zshrs's current internal
12125 /// representation. If zshrs starts unmetafying at parse time, this
12126 /// test will FAIL and the anchor-style test above will start passing.
12127 #[test]
12128 fn parse_simple_command_words_metafied_internal_form() {
12129 let _g = crate::test_util::global_state_lock();
12130 let prog = parse("ls -la /tmp").unwrap();
12131 match &prog.lists[0].sublist.pipe.cmd {
12132 ZshCommand::Simple(s) => {
12133 assert_eq!(s.words.len(), 3);
12134 assert_eq!(s.words[0], "ls");
12135 assert_eq!(s.words[2], "/tmp");
12136 // s.words[1] contains the metafied `-` (`\u{e19b}` Dash byte)
12137 // followed by "la". Don't pin the exact byte form (it
12138 // may change); pin that the length is right.
12139 assert_eq!(s.words[1].chars().count(), 3, "`-la` is 3 chars");
12140 assert!(s.words[1].ends_with("la"));
12141 }
12142 other => panic!("expected Simple, got {other:?}"),
12143 }
12144 }
12145
12146 // ─── zsh-corpus pins for parser: structural shapes ────────────────
12147
12148 /// Empty input — parse succeeds, lists may be empty.
12149 #[test]
12150 fn parse_corpus_empty_input_no_error() {
12151 let _g = crate::test_util::global_state_lock();
12152 let prog = parse("").unwrap();
12153 assert!(
12154 prog.lists.is_empty() || prog.lists.len() <= 1,
12155 "empty input → 0 or 1 list, got {}",
12156 prog.lists.len()
12157 );
12158 }
12159
12160 /// Comment-only input parses as empty.
12161 #[test]
12162 fn parse_corpus_comment_only_no_error() {
12163 let _g = crate::test_util::global_state_lock();
12164 let r = parse("# just a comment");
12165 assert!(r.is_ok(), "comment-only parse should succeed");
12166 }
12167
12168 /// `cmd1; cmd2` — two top-level lists or two sublists.
12169 #[test]
12170 fn parse_corpus_semicolon_separates_commands() {
12171 let _g = crate::test_util::global_state_lock();
12172 let prog = parse("echo a; echo b").unwrap();
12173 // We pin: parse produces > 0 lists/sublists; details vary.
12174 assert!(!prog.lists.is_empty(), "non-empty parse");
12175 }
12176
12177 /// `a && b` — DAMPER joins into a sublist chain.
12178 #[test]
12179 fn parse_corpus_logical_and_parses() {
12180 let _g = crate::test_util::global_state_lock();
12181 let r = parse("true && false");
12182 assert!(r.is_ok(), "`a && b` parses cleanly");
12183 }
12184
12185 /// `a || b` — DBAR.
12186 #[test]
12187 fn parse_corpus_logical_or_parses() {
12188 let _g = crate::test_util::global_state_lock();
12189 let r = parse("false || true");
12190 assert!(r.is_ok(), "`a || b` parses cleanly");
12191 }
12192
12193 /// `a | b` pipeline.
12194 #[test]
12195 fn parse_corpus_pipeline_parses() {
12196 let _g = crate::test_util::global_state_lock();
12197 let r = parse("echo hi | cat");
12198 assert!(r.is_ok(), "`a | b` parses");
12199 }
12200
12201 /// `if true; then echo x; fi` — basic if-then-fi block.
12202 #[test]
12203 fn parse_corpus_if_then_fi_parses() {
12204 let _g = crate::test_util::global_state_lock();
12205 let r = parse("if true; then echo x; fi");
12206 assert!(r.is_ok(), "if/then/fi parses cleanly");
12207 }
12208
12209 /// `for i in 1 2 3; do echo $i; done`.
12210 #[test]
12211 fn parse_corpus_for_do_done_parses() {
12212 let _g = crate::test_util::global_state_lock();
12213 let r = parse("for i in 1 2 3; do echo $i; done");
12214 assert!(r.is_ok(), "for/do/done parses cleanly");
12215 }
12216
12217 /// `while true; do break; done`.
12218 #[test]
12219 fn parse_corpus_while_do_done_parses() {
12220 let _g = crate::test_util::global_state_lock();
12221 let r = parse("while true; do break; done");
12222 assert!(r.is_ok(), "while/do/done parses cleanly");
12223 }
12224
12225 /// `case x in (a) echo A;; esac` — case statement.
12226 #[test]
12227 fn parse_corpus_case_esac_parses() {
12228 let _g = crate::test_util::global_state_lock();
12229 let r = parse("case x in (a) echo A;; esac");
12230 assert!(r.is_ok(), "case/esac parses cleanly");
12231 }
12232
12233 /// Function definition `f() { echo x }`.
12234 #[test]
12235 fn parse_corpus_function_def_parses() {
12236 let _g = crate::test_util::global_state_lock();
12237 let r = parse("f() { echo x }");
12238 assert!(r.is_ok(), "f() {{ ... }} parses cleanly");
12239 }
12240
12241 /// `(subshell echo a)` — subshell.
12242 #[test]
12243 fn parse_corpus_subshell_parens_parses() {
12244 let _g = crate::test_util::global_state_lock();
12245 let r = parse("( echo a )");
12246 assert!(r.is_ok(), "subshell parses cleanly");
12247 }
12248
12249 // ═══════════════════════════════════════════════════════════════════
12250 // C-parity tests pinning Src/parse.c. Tests that capture KNOWN
12251 // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
12252 // ═══════════════════════════════════════════════════════════════════
12253
12254 /// `empty_eprog(p)` returns true on an eprog with empty `prog`.
12255 /// C `Src/parse.c:584`:
12256 /// `return (!p || !p->prog || *p->prog == WCB_END());`
12257 /// Rust port at parse.rs:685 — `p.prog.is_empty() || p.prog[0] == WCB_END()`.
12258 #[test]
12259 fn empty_eprog_empty_prog_returns_true() {
12260 let _g = crate::test_util::global_state_lock();
12261 let p = crate::ported::zsh_h::eprog::default();
12262 assert!(empty_eprog(&p), "empty prog vec → empty_eprog true");
12263 }
12264
12265 /// `empty_eprog(p)` returns true when first wordcode is WCB_END.
12266 /// C: `*p->prog == WCB_END()`.
12267 #[test]
12268 fn empty_eprog_first_wcb_end_returns_true() {
12269 let _g = crate::test_util::global_state_lock();
12270 let mut p = crate::ported::zsh_h::eprog::default();
12271 p.prog.push(WCB_END());
12272 assert!(empty_eprog(&p), "prog[0]==WCB_END → empty_eprog true");
12273 }
12274
12275 /// `empty_eprog(p)` returns false for non-empty non-END prog.
12276 #[test]
12277 fn empty_eprog_non_empty_non_end_returns_false() {
12278 let _g = crate::test_util::global_state_lock();
12279 let mut p = crate::ported::zsh_h::eprog::default();
12280 // Push some non-END wordcode (1 is arbitrary non-zero, not WCB_END).
12281 p.prog.push(1);
12282 assert!(!empty_eprog(&p), "non-END first opcode → false");
12283 }
12284
12285 /// `ecstrcode("")` returns a wordcode for the empty string. C
12286 /// `Src/parse.c:346-ish` ecstrcode interns strings in `ecbuf`.
12287 /// Pin: same call returns same wordcode (deterministic intern).
12288 #[test]
12289 fn ecstrcode_empty_string_returns_deterministic_code() {
12290 let _g = crate::test_util::global_state_lock();
12291 init_parse();
12292 let a = ecstrcode("");
12293 let b = ecstrcode("");
12294 assert_eq!(a, b, "intern of '' must be deterministic");
12295 }
12296
12297 /// `ecstrcode` of two different strings returns different codes.
12298 #[test]
12299 fn ecstrcode_distinct_strings_get_distinct_codes() {
12300 let _g = crate::test_util::global_state_lock();
12301 init_parse();
12302 let a = ecstrcode("foo");
12303 let b = ecstrcode("bar");
12304 // Should differ — if equal, intern table collapsed two different
12305 // strings to the same key (bug).
12306 assert_ne!(a, b, "different strings must intern to different codes");
12307 }
12308
12309 /// `parse_event(ENDINPUT)` on empty input returns None.
12310 /// C `Src/parse.c:715-ish` — empty token stream → no program.
12311 #[test]
12312 fn parse_event_empty_returns_none() {
12313 let _g = crate::test_util::global_state_lock();
12314 init_parse();
12315 // Empty input typically yields no program; needs lex state.
12316 let r = parse_event(crate::ported::lex::ENDINPUT);
12317 assert!(r.is_none(), "no tokens → no event");
12318 }
12319
12320 // ═══════════════════════════════════════════════════════════════════
12321 // Additional C-parity tests for Src/parse.c.
12322 // ═══════════════════════════════════════════════════════════════════
12323
12324 /// c:399 — `ecadd(c)` returns the index where `c` was placed,
12325 /// not the post-increment value. Sequential ecadd calls return
12326 /// strictly increasing indices.
12327 #[test]
12328 fn ecadd_returns_strictly_increasing_indices() {
12329 let _g = crate::test_util::global_state_lock();
12330 init_parse();
12331 let i0 = ecadd(0xDEAD);
12332 let i1 = ecadd(0xBEEF);
12333 let i2 = ecadd(0xC0DE);
12334 assert!(
12335 i1 > i0,
12336 "ecadd indices must strictly increase, got {i0} then {i1}"
12337 );
12338 assert!(
12339 i2 > i1,
12340 "ecadd indices must strictly increase, got {i1} then {i2}"
12341 );
12342 assert_eq!(i1, i0 + 1, "consecutive ecadds advance by 1");
12343 assert_eq!(i2, i1 + 1, "consecutive ecadds advance by 1");
12344 }
12345
12346 /// c:413 — `ecdel(p)` removes one wordcode, shrinks ecused by 1.
12347 /// Pin: subsequent ecadd reuses freed slot (ecused decreased).
12348 #[test]
12349 fn ecdel_shrinks_ecused_by_one() {
12350 let _g = crate::test_util::global_state_lock();
12351 init_parse();
12352 let _i0 = ecadd(0xA);
12353 let i1 = ecadd(0xB);
12354 let _i2 = ecadd(0xC);
12355 let next_before = ECUSED.get();
12356 ecdel(i1);
12357 let next_after = ECUSED.get();
12358 assert_eq!(
12359 next_after,
12360 next_before - 1,
12361 "ecdel must decrement ecused by exactly 1"
12362 );
12363 }
12364
12365 /// c:399-405 — `ecadd` after exhausting buffer must grow it (no
12366 /// panic on push past current eclen). Pin: 1000 adds don't crash.
12367 #[test]
12368 fn ecadd_grows_buffer_on_demand() {
12369 let _g = crate::test_util::global_state_lock();
12370 init_parse();
12371 for i in 0..1000 {
12372 ecadd(i as u32);
12373 }
12374 // No panic = grow path works.
12375 assert!(ECUSED.get() >= 1000, "1000 adds → ecused ≥ 1000");
12376 }
12377
12378 /// c:426 — `ecstrcode` of short strings (≤4 bytes) returns a
12379 /// packed inline wordcode (not an offset into the string region).
12380 /// Pin: identical short strings get identical codes.
12381 #[test]
12382 fn ecstrcode_short_strings_are_deterministic() {
12383 let _g = crate::test_util::global_state_lock();
12384 init_parse();
12385 let a = ecstrcode("ab");
12386 let b = ecstrcode("ab");
12387 assert_eq!(a, b, "same short string must intern to same code");
12388 }
12389
12390 /// c:426 — long strings (>4 bytes) hit the deduped string region.
12391 /// Pin: same long string returns same code on repeat (registry
12392 /// dedupes).
12393 #[test]
12394 fn ecstrcode_long_strings_dedupe_in_registry() {
12395 let _g = crate::test_util::global_state_lock();
12396 init_parse();
12397 let a = ecstrcode("a-much-longer-test-string");
12398 let b = ecstrcode("a-much-longer-test-string");
12399 assert_eq!(a, b, "registry must dedupe identical long strings");
12400 }
12401
12402 /// `clear_hdocs()` is idempotent — calling twice in a row leaves
12403 /// HDOCS = None and LEX_HEREDOCS empty.
12404 #[test]
12405 fn clear_hdocs_is_idempotent() {
12406 let _g = crate::test_util::global_state_lock();
12407 clear_hdocs();
12408 clear_hdocs();
12409 HDOCS.with_borrow(|h| assert!(h.is_none(), "HDOCS must be None"));
12410 LEX_HEREDOCS.with_borrow(|v| assert!(v.is_empty(), "LEX_HEREDOCS must be empty"));
12411 }
12412
12413 /// `init_parse()` resets parse state to known empty defaults.
12414 /// Multiple init_parse calls are safe (idempotent).
12415 #[test]
12416 fn init_parse_is_idempotent() {
12417 let _g = crate::test_util::global_state_lock();
12418 init_parse();
12419 init_parse();
12420 // No panic = pass.
12421 }
12422
12423 /// `empty_eprog` returns true for a default-constructed eprog
12424 /// (empty prog vec).
12425 #[test]
12426 fn empty_eprog_true_for_empty_prog() {
12427 let _g = crate::test_util::global_state_lock();
12428 let p = eprog {
12429 prog: Vec::new(),
12430 ..Default::default()
12431 };
12432 assert!(empty_eprog(&p), "empty prog vec → empty eprog");
12433 }
12434
12435 /// `empty_eprog` returns true when prog[0] == WCB_END().
12436 #[test]
12437 fn empty_eprog_true_for_end_only_prog() {
12438 let _g = crate::test_util::global_state_lock();
12439 let p = eprog {
12440 prog: vec![WCB_END()],
12441 ..Default::default()
12442 };
12443 assert!(empty_eprog(&p), "WCB_END as first opcode → empty");
12444 }
12445
12446 /// `ecadjusthere(p, d)` is safe to call when HDOCS is None.
12447 #[test]
12448 fn ecadjusthere_safe_when_hdocs_none() {
12449 let _g = crate::test_util::global_state_lock();
12450 clear_hdocs();
12451 // No panic = pass.
12452 ecadjusthere(0, 0);
12453 ecadjusthere(100, -5);
12454 ecadjusthere(0, 10);
12455 }
12456
12457 /// `ecispace(p, n)` with n=0 is a no-op.
12458 #[test]
12459 fn ecispace_zero_n_is_noop() {
12460 let _g = crate::test_util::global_state_lock();
12461 init_parse();
12462 let before = ECUSED.get();
12463 ecispace(0, 0);
12464 let after = ECUSED.get();
12465 assert_eq!(before, after, "ecispace(_, 0) must not advance ecused");
12466 }
12467
12468 // ═══════════════════════════════════════════════════════════════════
12469 // Additional C-parity tests for Src/parse.c
12470 // c:146 parse_context_save / c:191 parse_context_restore /
12471 // c:225 ecadjusthere / c:293 ecadd / c:346 ecstrcode / c:574 init_parse /
12472 // c:685 empty_eprog / c:693 clear_hdocs / c:786 parse_list / c:815 parse_cond
12473 // c:2234 par_wordlist / c:2249 par_nl_wordlist
12474 // ═══════════════════════════════════════════════════════════════════
12475
12476 /// c:293 — `ecadd` returns usize (compile-time type pin).
12477 #[test]
12478 fn ecadd_returns_usize_type() {
12479 let _g = crate::test_util::global_state_lock();
12480 init_parse();
12481 let _: usize = ecadd(0);
12482 }
12483
12484 /// c:346 — `ecstrcode` returns u32 (compile-time type pin).
12485 #[test]
12486 fn ecstrcode_returns_u32_type() {
12487 let _g = crate::test_util::global_state_lock();
12488 init_parse();
12489 let _: u32 = ecstrcode("");
12490 }
12491
12492 /// c:346 — `ecstrcode("")` empty string is safe.
12493 #[test]
12494 fn ecstrcode_empty_string_no_panic() {
12495 let _g = crate::test_util::global_state_lock();
12496 init_parse();
12497 let _ = ecstrcode("");
12498 }
12499
12500 /// c:346 — `ecstrcode` is deterministic for same input.
12501 #[test]
12502 fn ecstrcode_is_deterministic() {
12503 let _g = crate::test_util::global_state_lock();
12504 init_parse();
12505 for s in ["", "a", "abc", "hello world"] {
12506 let first = ecstrcode(s);
12507 for _ in 0..3 {
12508 assert_eq!(
12509 ecstrcode(s),
12510 first,
12511 "ecstrcode({:?}) must be deterministic",
12512 s
12513 );
12514 }
12515 }
12516 }
12517
12518 /// c:786 — `parse_list` returns Option<eprog>.
12519 #[test]
12520 fn parse_list_returns_option_eprog_type() {
12521 let _g = crate::test_util::global_state_lock();
12522 init_parse();
12523 let _: Option<eprog> = parse_list();
12524 }
12525
12526 /// c:815 — `parse_cond` returns Option<eprog>.
12527 #[test]
12528 fn parse_cond_returns_option_eprog_type() {
12529 let _g = crate::test_util::global_state_lock();
12530 init_parse();
12531 let _: Option<eprog> = parse_cond();
12532 }
12533
12534 /// c:2234 — `par_wordlist` returns Vec<String>.
12535 #[test]
12536 fn par_wordlist_returns_vec_string_type() {
12537 let _g = crate::test_util::global_state_lock();
12538 init_parse();
12539 let _: Vec<String> = par_wordlist();
12540 }
12541
12542 /// c:2249 — `par_nl_wordlist` returns Vec<String>.
12543 #[test]
12544 fn par_nl_wordlist_returns_vec_string_type() {
12545 let _g = crate::test_util::global_state_lock();
12546 init_parse();
12547 let _: Vec<String> = par_nl_wordlist();
12548 }
12549
12550 /// c:693 — `clear_hdocs` deterministic state after call (no-panic).
12551 #[test]
12552 fn clear_hdocs_deterministic_after_call() {
12553 let _g = crate::test_util::global_state_lock();
12554 clear_hdocs();
12555 clear_hdocs();
12556 }
12557
12558 /// c:225 — `ecadjusthere(0, 0)` is a no-op (no delta).
12559 #[test]
12560 fn ecadjusthere_zero_delta_no_panic() {
12561 let _g = crate::test_util::global_state_lock();
12562 ecadjusthere(0, 0);
12563 }
12564
12565 /// c:225 — `ecadjusthere` is safe for arbitrary positions.
12566 #[test]
12567 fn ecadjusthere_arbitrary_pos_no_panic() {
12568 let _g = crate::test_util::global_state_lock();
12569 for p in [0usize, 1, 100, 9999] {
12570 ecadjusthere(p, 0);
12571 ecadjusthere(p, 1);
12572 ecadjusthere(p, -1);
12573 }
12574 }
12575
12576 // ═══════════════════════════════════════════════════════════════════
12577 // Additional C-parity tests for Src/parse.c FD_* accessors
12578 // c:3127 fdmagic / c:3131 fdflags / c:3133 fdother / c:3140 fdversion /
12579 // c:3145 fdhflags / c:3146 fdhtail / c:3147 fdhbldflags
12580 // ═══════════════════════════════════════════════════════════════════
12581
12582 fn build_fd_header() -> Vec<u32> {
12583 let mut buf = vec![0u32; FD_PRELEN + 32];
12584 buf[0] = FD_MAGIC; // pre[0] magic
12585 buf[1] = (0x12u32) | (0x00ABCDEFu32 << 8); // flags=0x12, other=0xABCDEF
12586 // Embed version string starting at pre[2].
12587 let ver = b"5.9\0";
12588 for (i, chunk) in ver.chunks(4).enumerate() {
12589 let mut word = [0u8; 4];
12590 word[..chunk.len()].copy_from_slice(chunk);
12591 buf[2 + i] = u32::from_le_bytes(word);
12592 }
12593 buf[FD_PRELEN - 1] = (FD_PRELEN as u32) + 8; // header-len slot
12594 buf
12595 }
12596
12597 /// c:3127 — `fdmagic(f)` returns pre[0] verbatim.
12598 #[test]
12599 fn fdmagic_returns_pre_zero_word() {
12600 let buf = build_fd_header();
12601 assert_eq!(fdmagic(&buf), FD_MAGIC, "fdmagic = pre[0]");
12602 }
12603
12604 /// c:3131 — `fdflags` extracts low byte of pre[1].
12605 #[test]
12606 fn fdflags_low_byte_extraction() {
12607 let buf = build_fd_header();
12608 assert_eq!(fdflags(&buf), 0x12, "flags = pre[1] & 0xff");
12609 }
12610
12611 /// c:3133 — `fdother` extracts high 24 bits of pre[1].
12612 #[test]
12613 fn fdother_high_24_bits_extraction() {
12614 let buf = build_fd_header();
12615 assert_eq!(
12616 fdother(&buf),
12617 0x00ABCDEF,
12618 "other = pre[1] >> 8 & 0x00ffffff"
12619 );
12620 }
12621
12622 /// c:3132 — `fdsetflags` writes low byte, preserves high 24 bits.
12623 #[test]
12624 fn fdsetflags_preserves_high_24_bits() {
12625 let mut buf = build_fd_header();
12626 let other_before = fdother(&buf);
12627 fdsetflags(&mut buf, 0x42);
12628 assert_eq!(fdflags(&buf), 0x42, "new flags written");
12629 assert_eq!(fdother(&buf), other_before, "high 24 bits preserved");
12630 }
12631
12632 /// c:3134 — `fdsetother` writes high 24 bits, preserves low byte.
12633 #[test]
12634 fn fdsetother_preserves_low_byte() {
12635 let mut buf = build_fd_header();
12636 let flags_before = fdflags(&buf);
12637 fdsetother(&mut buf, 0x00DEADBE);
12638 assert_eq!(fdother(&buf), 0x00DEADBE, "new other written");
12639 assert_eq!(fdflags(&buf), flags_before, "low byte preserved");
12640 }
12641
12642 /// c:3134 — `fdsetother` clamps to 24 bits (caller-passed high bits dropped).
12643 #[test]
12644 fn fdsetother_clamps_to_24_bits() {
12645 let mut buf = build_fd_header();
12646 fdsetother(&mut buf, 0xFF_FFFF_FF);
12647 // Only the low 24 bits land in `other`.
12648 assert_eq!(fdother(&buf), 0x00FF_FFFF, "high bits dropped");
12649 }
12650
12651 /// c:3140 — `fdversion(buf)` returns String (compile-time type pin).
12652 #[test]
12653 fn fdversion_returns_string_type() {
12654 let buf = build_fd_header();
12655 let _: String = fdversion(&buf);
12656 }
12657
12658 /// c:3140 — `fdversion` reads the NUL-terminated string from pre[2..].
12659 #[test]
12660 fn fdversion_reads_until_nul() {
12661 let buf = build_fd_header();
12662 assert_eq!(fdversion(&buf), "5.9", "version read until NUL");
12663 }
12664
12665 /// c:3145 — `fdhflags(h)` returns low 2 bits of flags.
12666 #[test]
12667 fn fdhflags_low_two_bits() {
12668 let h = fdhead {
12669 start: 0,
12670 len: 0,
12671 npats: 0,
12672 strs: 0,
12673 hlen: 0,
12674 flags: 0b1011, // tail=2, kshload bits = 0b11
12675 };
12676 assert_eq!(fdhflags(&h), 0b11, "flags = h.flags & 0x3");
12677 }
12678
12679 /// c:3146 — `fdhtail(h)` returns high 30 bits (shifted right by 2).
12680 #[test]
12681 fn fdhtail_shift_right_two() {
12682 let h = fdhead {
12683 start: 0,
12684 len: 0,
12685 npats: 0,
12686 strs: 0,
12687 hlen: 0,
12688 flags: (0x12_3456 << 2) | 0x3,
12689 };
12690 assert_eq!(fdhtail(&h), 0x12_3456, "tail = h.flags >> 2");
12691 }
12692
12693 /// c:3147 — `fdhbldflags(flags, tail)` packs into single u32.
12694 #[test]
12695 fn fdhbldflags_packs_flags_low_tail_high() {
12696 let packed = fdhbldflags(0x3, 0x42);
12697 assert_eq!(packed & 0x3, 0x3, "low 2 bits = flags");
12698 assert_eq!(packed >> 2, 0x42, "high 30 bits = tail");
12699 }
12700
12701 /// c:3145-3147 — `fdhflags(h)`+`fdhtail(h)` round-trip via fdhbldflags.
12702 #[test]
12703 fn fdh_round_trip_via_bldflags() {
12704 for (flags, tail) in [(0u32, 0u32), (1, 100), (2, 0xABC), (3, 0xFFFF)] {
12705 let packed = fdhbldflags(flags, tail);
12706 let h = fdhead {
12707 start: 0,
12708 len: 0,
12709 npats: 0,
12710 strs: 0,
12711 hlen: 0,
12712 flags: packed,
12713 };
12714 assert_eq!(fdhflags(&h), flags, "flags round-trips");
12715 assert_eq!(fdhtail(&h), tail, "tail round-trips");
12716 }
12717 }
12718
12719 /// c:8271 — `firstfdhead_offset()` returns FD_PRELEN constant.
12720 #[test]
12721 fn firstfdhead_offset_returns_prelen() {
12722 assert_eq!(
12723 firstfdhead_offset(),
12724 FD_PRELEN,
12725 "first header starts after prelude"
12726 );
12727 }
12728
12729 /// c:3127 — `fdmagic` differentiates FD_MAGIC from FD_OMAGIC.
12730 #[test]
12731 fn fdmagic_differentiates_magic_omagic() {
12732 let mut buf = vec![FD_MAGIC; FD_PRELEN];
12733 assert_eq!(fdmagic(&buf), FD_MAGIC);
12734 buf[0] = FD_OMAGIC;
12735 assert_eq!(fdmagic(&buf), FD_OMAGIC, "swapped magic readable");
12736 assert_ne!(FD_MAGIC, FD_OMAGIC, "the two magics differ");
12737 }
12738}