Skip to main content

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}