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{9b}` 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/// Direct port of `parse_context_save(struct parse_stack *ps, int toplevel)` at `Src/parse.c:295`.
140/// Snapshots the lexer-side file-statics (which currently live on
141/// `lexer` until Phase 7 dissolution makes them file-scope
142/// thread_local!s) plus the pending heredoc list, plus the
143/// wordcode-buffer state (STUB until Phase 9b). Saves Rust-only
144/// recursion counters too so nested parses get fresh limits.
145/// WARNING: param names don't match C — Rust=(ps) vs C=(ps, toplevel)
146pub fn parse_context_save(ps: &mut parse_stack) {
147    // parse.c:299 — `ps->hdocs = hdocs; hdocs = NULL;` — save the
148    // canonical C linked-list and clear it for the nested parse.
149    ps.hdocs = HDOCS.with_borrow_mut(|h| h.take());
150    // zshrs-only: save the parallel AST-glue Vec the same way.
151    // LEX_HEREDOCS carries terminator/strip_tabs/quoted metadata
152    // that has no C analog (C stores it implicitly via tokstr).
153    ps.lex_heredocs = LEX_HEREDOCS.with_borrow_mut(|v| std::mem::take(v));
154    // parse.c:302-310 — save lexer-side state.
155    ps.incmdpos = incmdpos();
156    // parse.c:303 — `ps->aliasspaceflag = aliasspaceflag;`. Mirrors
157    // lex.c LEX_ALIAS_SPACE_FLAG so nested parses preserve the
158    // HISTIGNORESPACE-via-alias state across parser re-entry.
159    ps.aliasspaceflag = crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.get());
160    ps.incond = incond();
161    ps.inredir = inredir();
162    ps.incasepat = incasepat();
163    ps.isnewlin = isnewlin();
164    ps.infor = infor();
165    ps.inrepeat_ = inrepeat();
166    ps.intypeset = intypeset();
167    // parse.c:312-317 — wordcode buffer state. STUB until Phase 9b
168    // (zshrs has no ecbuf yet).
169    ps.eclen = 0;
170    ps.ecused = 0;
171    ps.ecnpats = 0;
172    ps.ecbuf = None;
173    ps.ecstrs = None;
174    ps.ecsoffs = 0;
175    ps.ecssub = 0;
176    ps.ecnfunc = 0;
177    set_incmdpos(true);
178    set_incond(0);
179    set_inredir(false);
180    set_incasepat(0);
181    set_infor(0);
182    set_inrepeat(0);
183    set_intypeset(false);
184}
185
186/// Direct port of `parse_context_restore(const struct parse_stack *ps, int toplevel)` at `Src/parse.c:326`.
187/// Inverse of `parse_context_save`. Restores lexer-side state +
188/// pending heredocs + Rust-only counters from `ps`, then clears
189/// `errflag & ERRFLAG_ERROR` per parse.c:354.
190/// WARNING: param names don't match C — Rust=(ps) vs C=(ps, toplevel)
191pub fn parse_context_restore(ps: &parse_stack) {
192    // parse.c:330-331 — free any in-progress wordcode buffer.
193    // zshrs has no wordcode yet (STUB until Phase 9b); the AST
194    // nodes are owned by their parent so dropping the parser
195    // frees them.
196
197    // parse.c:333-352 — restore saved state.
198    // parse.c:337 — `hdocs = ps->hdocs;`
199    HDOCS.with_borrow_mut(|h| *h = ps.hdocs.clone());
200    // zshrs-only: restore the parallel AST-glue Vec.
201    LEX_HEREDOCS.with_borrow_mut(|v| *v = ps.lex_heredocs.clone());
202    set_incmdpos(ps.incmdpos);
203    // parse.c:334 — `aliasspaceflag = ps->aliasspaceflag;`.
204    crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.set(ps.aliasspaceflag));
205    set_incond(ps.incond);
206    set_inredir(ps.inredir);
207    set_incasepat(ps.incasepat);
208    set_isnewlin(ps.isnewlin);
209    set_infor(ps.infor);
210    set_inrepeat(ps.inrepeat_);
211    set_intypeset(ps.intypeset);
212    // ecbuf/eclen/ecused/ecnpats/ecstrs/ecsoffs/ecssub/ecnfunc
213    // STUB until Phase 9b.
214
215    // parse.c:354 — `errflag &= ~ERRFLAG_ERROR;` — clear the
216    // error flag so the outer parse sees a clean state.
217    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
218}
219
220/// Direct port of `ecadjusthere(int p, int d)` at `Src/parse.c:360`. Walk
221/// the pending-heredocs list and bump each `pc` by `d` if it's
222/// at or after position `p`. Called by `ecispace` / `ecdel` when
223/// wordcodes shift.
224#[allow(unused_variables)]
225pub fn ecadjusthere(p: usize, d: i32) {
226    // parse.c:362-366 — `for (p2 = hdocs; p2; p2 = p2->next) if
227    // (p2->pc >= p) p2->pc += d;`. zshrs's hdocs are still
228    // Vec<HereDoc> on the lexer (pre-P9c migration); since none
229    // of them carry a wordcode pc today (the AST tree has no pc
230    // slots), this is a no-op until Phase 9c wires
231    // `hdocs.pc` into wordcode emission.
232}
233
234// === AST tree relocated to src/extensions/zsh_ast.rs ===
235//
236// zsh C does NOT have an AST tree — it emits wordcode directly via
237// par_event/par_list/par_sublist/par_pipe/par_cmd/par_simple/etc.
238// (Src/parse.c:485-3000) into a flat `Wordcode ecbuf[]`. The Zsh*/
239// Shell* AST node types lived in this file as a Rust-only IR that
240// stands in for that wordcode.
241//
242// P9e (PORT_PLAN.md): the types moved to src/extensions/zsh_ast.rs
243// to make their Rust-only-extension nature explicit. The full P9c +
244// P9d rewrite (par_* emitting wordcode + vm_helper reading wordcode)
245// retires them entirely — until then, callers reach them via this
246// re-export.
247
248/// Direct port of `ecispace(int p, int n)` at `Src/parse.c:372`. Insert `n`
249/// empty wordcode slots at position `p`, shifting later entries
250/// right, growing the buffer as needed, adjusting heredoc pointers.
251pub fn ecispace(p: usize, n: usize) {
252    // parse.c:376-381 — grow if needed.
253    let need = n as i32;
254    if (ECLEN.get() - ECUSED.get()) < need {
255        let cur = ECLEN.get();
256        let mut a = if cur < EC_DOUBLE_THRESHOLD {
257            cur
258        } else {
259            EC_INCREMENT
260        };
261        if need > a {
262            a = need;
263        }
264        ECBUF.with_borrow_mut(|buf| {
265            buf.resize((cur + a) as usize, 0);
266        });
267        ECLEN.set(cur + a);
268    }
269    // parse.c:382-385 — memmove p → p+n, gap of n.
270    let m = ECUSED.get() as usize - p;
271    if m > 0 {
272        ECBUF.with_borrow_mut(|buf| {
273            let needed = (ECUSED.get() as usize) + n;
274            if buf.len() < needed {
275                buf.resize(needed, 0);
276            }
277            for i in (0..m).rev() {
278                buf[p + n + i] = buf[p + i];
279            }
280            for i in 0..n {
281                buf[p + i] = 0;
282            }
283        });
284    }
285    // parse.c:386 — bump ecused by n.
286    ECUSED.set(ECUSED.get() + need);
287    // parse.c:387 — `ecadjusthere(p, n)`.
288    ecadjusthere(p, need);
289}
290
291/// Direct port of `ecadd(wordcode c)` at `Src/parse.c:397`. Append `c` to
292/// the wordcode buffer with grow-on-demand, return the new index.
293pub fn ecadd(c: u32) -> usize {
294    // parse.c:399-405 — `if ((eclen - ecused) < 1) grow`.
295    if (ECLEN.get() - ECUSED.get()) < 1 {
296        let cur = ECLEN.get();
297        let a = if cur < EC_DOUBLE_THRESHOLD {
298            cur
299        } else {
300            EC_INCREMENT
301        };
302        ECBUF.with_borrow_mut(|buf| {
303            buf.resize((cur + a) as usize, 0);
304        });
305        ECLEN.set(cur + a);
306    }
307    let idx = ECUSED.get();
308    ECBUF.with_borrow_mut(|buf| {
309        if (idx as usize) >= buf.len() {
310            buf.resize((idx + 1) as usize, 0);
311        }
312        buf[idx as usize] = c;
313    });
314    ECUSED.set(idx + 1);
315    idx as usize
316}
317
318/// Direct port of `ecdel(int p)` at `Src/parse.c:413`. Remove the
319/// wordcode at position `p`, shift later entries left by one,
320/// decrement ecused, adjust pending heredoc pointers.
321pub fn ecdel(p: usize) {
322    // parse.c:415-418 — memmove + decrement ecused.
323    let n = ECUSED.get() as usize - p - 1;
324    if n > 0 {
325        ECBUF.with_borrow_mut(|buf| {
326            for i in 0..n {
327                buf[p + i] = buf[p + i + 1];
328            }
329        });
330    }
331    ECUSED.set(ECUSED.get() - 1);
332    // parse.c:420 — `ecadjusthere(p, -1)`.
333    ecadjusthere(p, -1);
334}
335
336/// Direct port of `ecstrcode(char *s)` at `Src/parse.c:426`. Encode a
337/// string into a single wordcode (short strings ≤4 bytes packed
338/// inline; longer strings get an offset into the deduped registry).
339///
340/// The long-string path stores the METAFIED bytes (matches what C's
341/// strs region contains): collapse Rust UTF-8 chars in 0x80..=0xff
342/// to single bytes, then apply zsh metafy (high bytes ≥ 0x83 →
343/// `Meta=0x83 + byte^0x20`). Length tracking (ECSOFFS) uses the
344/// metafied byte count — same as C `strlen(s) + 1` where C's `s`
345/// is already metafied at this point.
346pub fn ecstrcode(s: &str) -> u32 {
347    // Convert Rust char-form → C-byte form. zsh's metafy() at
348    // Src/utils.c only converts bytes flagged IMETA: 0x00, 0x83
349    // (Meta itself), and 0x84..=0xa2 (Pound..Marker, the lex
350    // markers). Other bytes 0x01..=0x82 and 0xa3..=0xff pass
351    // through unchanged. See utils.c:4195-4204 typtab init.
352    //
353    // Rust receives chars. Classify each:
354    //   - codepoint in [0x83..=0xa2] → marker char (emitted by lex
355    //     post-metafy in C); 1 byte unchanged
356    //   - codepoint < 0x80 → ASCII, 1 byte unchanged
357    //   - codepoint in [0x80..=0x82] or [0xa3..=0xff] → single
358    //     non-imeta byte (user-input range); 1 byte unchanged
359    //   - codepoint > 0xff → multi-byte UTF-8 source char (e.g.
360    //     '━' = U+2501 = 0xe2 0x94 0x81). Metafy ONLY the bytes
361    //     that fall in 0x83..=0xa2; pass others through. For '━':
362    //     0xe2 stays, 0x94 → 0x83 0xb4, 0x81 stays.
363    let mut c_bytes: Vec<u8> = Vec::with_capacity(s.len());
364    let imeta = |b: u8| -> bool { b == 0 || (0x83..=0xa2).contains(&b) };
365    for ch in s.chars() {
366        let cu = ch as u32;
367        if cu < 0x80 {
368            // ASCII — single byte unchanged.
369            c_bytes.push(cu as u8);
370        } else if (0x83..=0xa2).contains(&cu) {
371            // Lex marker char (emitted by lex.add(Marker) post-metafy
372            // in C). Stored as single byte.
373            c_bytes.push(cu as u8);
374        } else {
375            // User-input char: encode UTF-8 then metafy imeta bytes.
376            // For chars 0x80..=0xff (like 'º' U+00BA), UTF-8 gives
377            // 2 bytes (e.g. `0xc2 0xba`) — zsh's lex reads these as
378            // raw bytes from input and metafy passes 0xc2 / 0xba
379            // through (both NOT imeta).
380            let mut tmp = [0u8; 4];
381            for &b in ch.encode_utf8(&mut tmp).as_bytes() {
382                if imeta(b) {
383                    c_bytes.push(0x83);
384                    c_bytes.push(b ^ 0x20);
385                } else {
386                    c_bytes.push(b);
387                }
388            }
389        }
390    }
391    // c:`has_token` (Src/utils.c:2282) → `itok(*s)` → `typtab[c] & ITOK`.
392    // ITOK is set for bytes `Pound..=Nularg` (0x84..=0xa1) per
393    // Src/utils.c:4198 (`for (t0=Pound; t0<=LAST_NORMAL_TOK; t0++)
394    // typtab[t0]|=ITOK`) plus :4200 (`for (t0=Snull; t0<=Nularg; t0++)
395    // typtab[t0]|=ITOK|IMETA|INULL`). Pound=0x84 Bang=0x9c (last normal),
396    // Snull=0x9d..Nularg=0xa1. Meta=0x83 has IMETA but NOT ITOK.
397    let t = c_bytes.iter().any(|&b| (0x84..=0xa1).contains(&b));
398    let l = c_bytes.len() + 1; // include NUL terminator
399    if l <= 4 {
400        // parse.c:436-445 — short-string inline pack. Uses raw C-bytes
401        // (NOT metafied — the inline packing stores 1 byte per slot).
402        let mut c: u32 = if t { 3 } else { 2 };
403        match l {
404            4 => {
405                c |= (c_bytes[2] as u32) << 19;
406                c |= (c_bytes[1] as u32) << 11;
407                c |= (c_bytes[0] as u32) << 3;
408            }
409            3 => {
410                c |= (c_bytes[1] as u32) << 11;
411                c |= (c_bytes[0] as u32) << 3;
412            }
413            2 => {
414                c |= (c_bytes[0] as u32) << 3;
415            }
416            1 => {
417                // parse.c:443 — empty string special case.
418                c = if t { 7 } else { 6 };
419            }
420            _ => {}
421        }
422        c
423    } else {
424        // parse.c:447-466 — long string. Port of C's eccstr BST walk
425        // exactly: walk the tree comparing nfunc, then hashval, then
426        // strcmp on bytes. Return offs on full match; insert new
427        // leaf otherwise. Matches C's exact dedup-hit pattern
428        // (which is content-dependent — hash collisions and the
429        // lazy short-circuit cmp chain make the tree shape determine
430        // whether matching nodes are reachable).
431        // hasher is byte-by-byte polynomial (hashtable.c:86); pass
432        // c_bytes via from_utf8_unchecked so non-UTF-8 zsh marker
433        // bytes feed straight in. SAFETY: hasher only iterates
434        // `.bytes()` — no UTF-8 validity assumed.
435        let val =
436            crate::ported::hashtable::hasher(unsafe { std::str::from_utf8_unchecked(&c_bytes) });
437        let nfunc = ECNFUNC.get();
438        let found_offs = ECSTRS_TREE.with_borrow_mut(|root| {
439            // Walk the tree. At each node, if all 3 cmps == 0,
440            // return the node's offs. Otherwise descend left/right
441            // by the first non-zero cmp's sign.
442            let mut cur: &mut Option<Box<EccstrNode>> = root;
443            loop {
444                let p = match cur.as_mut() {
445                    Some(p) => p,
446                    None => break None,
447                };
448                // c:448 — `cmp = p->nfunc - ecnfunc`
449                let mut cmp = (p.nfunc as i64) - (nfunc as i64);
450                if cmp == 0 {
451                    // c:448 — `&& !(cmp = (long)p->hashval - (long)val)`
452                    // C does `(int)(p->hashval - val)` — unsigned 32-bit
453                    // subtraction wraps, then cast to int. Use
454                    // wrapping_sub + as i32 to match the bit pattern.
455                    cmp = (p.hashval.wrapping_sub(val) as i32) as i64;
456                    if cmp == 0 {
457                        // c:448 — `&& !(cmp = strcmp(p->str, s))`
458                        cmp = match p.str.as_slice().cmp(c_bytes.as_slice()) {
459                            std::cmp::Ordering::Less => -1,
460                            std::cmp::Ordering::Equal => 0,
461                            std::cmp::Ordering::Greater => 1,
462                        };
463                        if cmp == 0 {
464                            // c:450 — `return p->offs;`
465                            break Some(p.offs);
466                        }
467                    }
468                }
469                // c:452 — `pp = (cmp < 0 ? &p->left : &p->right);`
470                cur = if cmp < 0 { &mut p.left } else { &mut p.right };
471            }
472        });
473        if let Some(offs) = found_offs {
474            return offs;
475        }
476        // c:462 — `p->offs = ((ecsoffs - ecssub) << 2) | (t ? 1 : 0);`
477        let offs = (((ECSOFFS.get() - ECSSUB.get()) as u32) << 2) | if t { 1 } else { 0 };
478        // c:463 — `p->aoffs = ecsoffs;` (absolute write position).
479        let aoffs = ECSOFFS.get() as u32;
480        // c:457-465 — insert new node at the NULL slot the walk
481        // terminated at. Encode the walk path as a Vec<bool> of
482        // left/right turns (true = right), then re-descend to
483        // insert. Borrow-checker friendly: a single mutable walk
484        // that either finds an existing node (descend) or fills
485        // the empty slot (return).
486        let stored = c_bytes.clone();
487        let stored_len = stored.len();
488        let new_node = Box::new(EccstrNode {
489            left: None,
490            right: None,
491            str: stored.clone(),
492            offs,
493            aoffs,
494            nfunc,
495            hashval: val,
496        });
497        ECSTRS_TREE.with_borrow_mut(|root| {
498            // Build the path first (immutable-walk; safe because we
499            // only ever go further down).
500            let mut path: Vec<bool> = Vec::new();
501            {
502                let mut cur: &Option<Box<EccstrNode>> = root;
503                while let Some(p) = cur.as_ref() {
504                    let mut cmp = (p.nfunc as i64) - (nfunc as i64);
505                    if cmp == 0 {
506                        // C does `(int)(p->hashval - val)` — unsigned 32-bit
507                        // subtraction wraps, then cast to int. Use
508                        // wrapping_sub + as i32 to match the bit pattern.
509                        cmp = (p.hashval.wrapping_sub(val) as i32) as i64;
510                        if cmp == 0 {
511                            cmp = match p.str.as_slice().cmp(c_bytes.as_slice()) {
512                                std::cmp::Ordering::Less => -1,
513                                std::cmp::Ordering::Equal => 0,
514                                std::cmp::Ordering::Greater => 1,
515                            };
516                        }
517                    }
518                    let go_right = cmp >= 0;
519                    path.push(go_right);
520                    cur = if go_right { &p.right } else { &p.left };
521                }
522            }
523            // Descend mutably along the recorded path and assign at
524            // the NULL leaf.
525            let mut cur: &mut Option<Box<EccstrNode>> = root;
526            for turn in path {
527                let p = cur.as_mut().expect("path matches walk");
528                cur = if turn { &mut p.right } else { &mut p.left };
529            }
530            *cur = Some(new_node);
531        });
532        // Also keep the existing reverse index (offs → bytes) for
533        // ecgetstr_wordcode and copy_ecstr — they read flat by offs.
534        ECSTRS_REVERSE.with_borrow_mut(|m| {
535            m.insert(offs, stored);
536        });
537        let _ = l;
538        ECSOFFS.set(ECSOFFS.get() + (stored_len + 1) as i32);
539        offs
540    }
541}
542
543/// Initialize parser status. Direct port of zsh/Src/parse.c:491
544/// `init_parse_status`. Clears the per-parse-call lexer flags
545/// so a fresh parse starts from cmd-position with no nesting
546/// state inherited from a prior parse.
547///
548/// Previously the Rust port omitted `inrepeat_ = 0` at c:501.
549/// `inrepeat_` is the `repeat N <body>` parse-state counter that
550/// the lexer toggles in 3 phases (1 → 2 → 3 → 0). Without the
551/// reset, a fresh parse called after an in-flight `repeat`
552/// command would inherit the stale counter and silently misread
553/// the next token as a body of an already-completed repeat.
554pub fn init_parse_status() {
555    // c:491
556    // parse.c:500-502 — `incasepat = incond = inredir = infor =
557    // intypeset = 0; inrepeat_ = 0; incmdpos = 1;`
558    set_incasepat(0); // c:500
559    set_incond(0); // c:500
560    set_inredir(false); // c:500
561    set_infor(0); // c:500
562    set_intypeset(false); // c:500
563    set_inrepeat(0); // c:501 inrepeat_ = 0
564    set_incmdpos(true); // c:502
565}
566
567/// Initialize parser for a fresh parse. Direct port of
568/// zsh/Src/parse.c:509 `init_parse`. C source allocates a
569/// fresh wordcode buffer (ecbuf) sized EC_INIT_SIZE, resets the
570/// per-parse-call counters, and calls init_parse_status. zshrs
571/// has no flat wordcode buffer (AST is built inline) so this
572/// function reduces to init_parse_status + recursion_depth/
573/// global_iterations clear.
574pub fn init_parse() {
575    // parse.c:513-520 — `ecbuf = (Wordcode) zalloc(EC_INIT_SIZE *
576    // sizeof(wordcode)); eclen = EC_INIT_SIZE; ecused = 0;
577    // ecnpats = 0; ecstrs = NULL; ecsoffs = ecnfunc = 0;
578    // ecssub = 0;`. P9b — initialize the per-evaluator wordcode
579    // buffer for this parse call. zshrs uses thread-local
580    // statics declared at file scope (parse.rs:25-50).
581    ECBUF.with_borrow_mut(|buf| {
582        buf.clear();
583        buf.resize(EC_INIT_SIZE as usize, 0);
584    });
585    ECLEN.set(EC_INIT_SIZE);
586    ECUSED.set(0);
587    ECNPATS.set(0);
588    ECSOFFS.set(0);
589    ECSSUB.set(0);
590    ECNFUNC.set(0);
591    ECSTRS_INDEX.with_borrow_mut(|m| m.clear());
592    ECSTRS_REVERSE.with_borrow_mut(|m| m.clear());
593    ECSTRS_TREE.with_borrow_mut(|t| *t = None);
594
595    // parse.c:522 — `init_parse_status();`
596    init_parse_status();
597}
598
599/// Port of `copy_ecstr(Eccstr s, char *p)` from `Src/parse.c:537`.
600/// Walks the BST and writes each entry to `p[s->aoffs..]` matching
601/// C's recursive in-order traversal exactly. The old impl used the
602/// `ECSTRS_REVERSE` HashMap keyed by `offs` (= ecssub-relative
603/// wordcode-encoded offset), which collides across funcdef scopes:
604/// a string at relative offs=0 inside funcdef A and another at
605/// relative offs=0 inside funcdef B share the same key, so one
606/// overwrites the other.
607pub fn copy_ecstr(_table: &std::collections::HashMap<u32, Vec<u8>>, p: &mut [u8]) {
608    // c:537-544 — walk eccstr BST recursively, writing each node's
609    // str at p[node->aoffs..node->aoffs + strlen + 1] (NUL-terminated).
610    ECSTRS_TREE.with_borrow(|root| {
611        copy_ecstr_walk(root, p);
612    });
613}
614
615/// Port of `bld_eprog(int heap)` from `Src/parse.c:547`. Finalizes
616/// the in-build `ECBUF`/`ECSTRS`/`ECNPATS` state into an `Eprog`.
617/// Resets the build state so a new parse can start.
618pub fn bld_eprog(heap: bool) -> eprog {
619    // c:547
620
621    // c:555 — emit WC_END opcode. `WCB_END` is `WC_END_DEFAULT` (0).
622    ecadd(0);
623
624    let ecused = ECUSED.with(|c| c.get()) as usize;
625    let ecnpats = ECNPATS.with(|c| c.get()) as usize;
626    let ecsoffs = ECSOFFS.with(|c| c.get()) as usize;
627
628    // c:557-559 — `ret->len = ((ecnpats * sizeof(Patprog)) +
629    //                            (ecused * sizeof(wordcode)) +
630    //                            ecsoffs);`
631    // sizeof(Patprog) = sizeof(struct patprog *) = pointer size.
632    // On 64-bit targets that's 8, on 32-bit that's 4. C's eprog
633    // ->len is the canonical value for parity tests, so we use
634    // the same arithmetic.
635    let prog_bytes = ecused * 4; // sizeof(wordcode) = 4
636    let len = (ecnpats * size_of::<*const u8>()) + prog_bytes + ecsoffs;
637
638    // Snapshot the wordcode buffer + string table.
639    let prog_words: Vec<u32> = ECBUF.with(|c| c.borrow()[..ecused].to_vec());
640    let mut strs_bytes = vec![0u8; ecsoffs];
641    ECSTRS_REVERSE.with(|c| copy_ecstr(&c.borrow(), &mut strs_bytes));
642
643    // c:566 — store strs as raw bytes via from_utf8_unchecked so
644    // single-byte zsh markers (e.g. Dash 0x9b) survive intact.
645    // `String::from_utf8_lossy` would replace them with U+FFFD
646    // (`\xef\xbf\xbd`), breaking byte-for-byte parity with C's
647    // strs region. SAFETY: downstream consumers of `eprog.strs`
648    // index by byte offset (per the wordcode `(offs >> 2)` offset
649    // encoding) and call `.as_bytes()` — they never iterate as
650    // chars or rely on UTF-8 validity, so storing non-UTF-8 bytes
651    // in a String is safe in practice. C zsh's strs is `char *`
652    // with the same byte-not-char semantics.
653    let strs_string = unsafe { String::from_utf8_unchecked(strs_bytes) };
654    let ret = eprog {
655        flags: if heap { EF_HEAP } else { EF_REAL }, // c:570
656        len: len as i32,                             // c:559
657        npats: ecnpats as i32,                       // c:561
658        nref: if heap { -1 } else { 1 },             // c:562
659        pats: Vec::new(),                            // c:563 dummy_patprog
660        prog: prog_words,                            // c:565
661        strs: Some(strs_string),
662        shf: None,
663        dump: None,
664        strs_metafied: false, // native pool — clean UTF-8
665    };
666
667    // c:577 — free ecbuf so next parse starts fresh.
668    ECBUF.with(|c| c.borrow_mut().clear());
669    ECLEN.with(|c| c.set(0));
670    ECUSED.with(|c| c.set(0));
671    ECNPATS.with(|c| c.set(0));
672    ECSOFFS.with(|c| c.set(0));
673    ECSTRS_INDEX.with(|c| c.borrow_mut().clear());
674    ECSTRS_REVERSE.with(|c| c.borrow_mut().clear());
675    ECSTRS_TREE.with(|t| *t.borrow_mut() = None);
676
677    ret
678}
679
680/// Port of `int empty_eprog(Eprog p)` from `Src/parse.c:584`. C
681/// body: `return (!p || !p->prog || *p->prog == WCB_END());` —
682/// the eprog is empty when its prog buffer is missing or the
683/// first wordcode is the WC_END marker. Used by signal handlers
684/// (`Src/signals.c:712`) to short-circuit a trap that resolves to
685/// an empty program.
686pub fn empty_eprog(p: &eprog) -> bool {
687    p.prog.is_empty() || p.prog[0] == WCB_END()
688}
689
690/// Clear pending here-document list. Direct port of
691/// `clear_hdocs(void)` from `Src/parse.c:591`. The C version walks
692/// `hdocs` and frees each node; Rust drops the `Box<heredocs>`
693/// chain automatically when the head is replaced with None.
694pub fn clear_hdocs() {
695    // c:591
696    // c:593-598 — for (p = hdocs; p; p = n) { n = p->next; zfree(p); }
697    // c:599 — hdocs = NULL;
698    HDOCS.with_borrow_mut(|h| *h = None);
699    // zshrs-only: also drop the parallel AST-glue Vec. No C
700    // analog — LEX_HEREDOCS is Rust-only working-set state.
701    LEX_HEREDOCS.with_borrow_mut(|v| v.clear());
702}
703
704/// Top-level parse-event entry. Direct port of zsh/Src/parse.c:
705/// 612-631 `parse_event`. Reads one event from the lexer (a
706/// sublist optionally followed by SEPER/AMPER/AMPERBANG) and
707/// returns the resulting ZshProgram.
708///
709/// `endtok` is the token that terminates the event — usually
710/// ENDINPUT, but for command-style substitutions the closing
711/// `)` (zsh's CMD_SUBST_CLOSE).
712///
713/// zshrs port note: zsh's parse_event returns an `Eprog` (heap-
714/// allocated wordcode program). zshrs returns a `ZshProgram`
715/// (AST root). Same role at the parse-output boundary.
716pub fn parse_event(endtok: lextok) -> Option<ZshProgram> {
717    // parse.c:616-619 — reset state and prime the lexer.
718    set_tok(ENDINPUT);
719    set_incmdpos(true);
720    // parse.c:618 — `aliasspaceflag = 0;`. Fresh event: discard any
721    // alias-space carry-over from a prior parse so HISTIGNORESPACE
722    // doesn't suppress the next entered command line.
723    crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.set(0));
724
725    // parse.c:626-628 — a sub-parse for a substitution (endtok !=
726    // ENDINPUT, e.g. `par_subsh` with OUTPAR) doesn't need its own
727    // eprog; par_event drives it and the caller discards the program.
728    if endtok != ENDINPUT {
729        zshlex();
730        init_parse();
731        if !par_event(endtok) {
732            clear_hdocs();
733            return None;
734        }
735        return Some(ZshProgram { lists: Vec::new() });
736    }
737
738    // parse.c:616-630 — top-level event parse. The lexer pulls
739    // characters straight from SHIN via hgetc → ingetc → shingetchar
740    // (input.c) and accumulates each into its token buffer with add();
741    // no LEX_INPUT seeding. `zshlex()` (already primed by the shared
742    // entry below) reads the first token; parse_program_until drives the
743    // grammar over the event, refilling input one line at a time as
744    // ingetc's inputline() arm fires.
745    zshlex();
746    init_parse();
747    if tok() == ENDINPUT {
748        return None; // EOF — loop() terminates.
749    }
750    // parse.c:639-643 — skip leading command separators; a SEPER on a fresh
751    // newline at the top level (endtok == ENDINPUT) is an EMPTY command — the
752    // bare-Enter case — so return None WITHOUT pulling another input line.
753    // Missing this, `parse_program_until` treated the trailing newline as an
754    // incomplete list and read a continuation line, so every empty Enter (and
755    // the line after it) rendered the PS2 `>` prompt instead of PS1.
756    while tok() == SEPER {
757        if isnewlin() > 0 {
758            clear_hdocs();
759            return None;
760        }
761        zshlex();
762    }
763    if tok() == ENDINPUT {
764        return None;
765    }
766    // single_event = TRUE: one logical command line per parse_event, the
767    // faithful port of C par_event's endtok==ENDINPUT top-level mode
768    // (parse.c:635). loop() reads+executes ONE event at a time so `-v`/`-x`
769    // echo and runtime `setopt verbose`/`xtrace` interleave with execution
770    // exactly as zsh does — whole-program parsing can't (it finishes
771    // parsing before any execution).
772    //
773    // Enabling this required isolating the runtime nested-parse machinery
774    // from the now-mid-stream outer reader (the parse/execute interleave
775    // exposed three split-global hazards that whole-program mode hid):
776    //   * cmd-subst / proc-sub / eval / source bodies re-parse via
777    //     vm_helper::parse_isolated (the AST-path bridge for C's
778    //     parse_string, exec.c:283) — it sets `strin` so a drained nested
779    //     buffer EOFs instead of STEALING the outer reader's next SHIN line.
780    //   * strinbeg/strinend now bump BOTH `strin` copies (hist.rs + the
781    //     input.rs one ingetc checks); lexinit zeroes BOTH `lexstop` copies
782    //     — C has a single global for each, zshrs split them.
783    //   * loop() saves/restores LEX_LINENO across execode (init.rs), the
784    //     execlist `oldlineno` discipline (exec.c:28/292), so per-statement
785    //     SET_LINENO doesn't freeze `$LINENO` for the next event.
786    let mut program = parse_program_until(None, true);
787    if program.lists.is_empty() {
788        clear_hdocs();
789        return None;
790    }
791    // parse.c:630 bld_eprog post-pass — wire heredoc bodies collected
792    // by zshlex into the ZshRedir nodes (mirror of `parse()`).
793    let bodies: Vec<HereDocInfo> = LEX_HEREDOCS
794        .with_borrow(|v| v.clone())
795        .into_iter()
796        .map(|h| HereDocInfo {
797            content: h.content,
798            terminator: h.terminator,
799            quoted: h.quoted,
800        })
801        .collect();
802    if !bodies.is_empty() {
803        fill_heredoc_bodies(&mut program, &bodies);
804    }
805    Some(program)
806}
807
808/// Parse one event (sublist with optional separator). Direct
809/// port of zsh/Src/parse.c:635 `par_event`. Returns true if
810/// an event was successfully parsed, false on EOF / endtok.
811///
812/// zshrs port note: the C version emits wordcodes via ecadd/
813/// set_list_code; zshrs's parser builds AST nodes via
814/// par_sublist + par_list. Same flow, different output.
815pub fn par_event(endtok: lextok) -> bool {
816    // parse.c:639-643 — skip leading SEPERs.
817    while tok() == SEPER {
818        // parse.c:640-641 — at top-level (endtok == ENDINPUT),
819        // a SEPER on a fresh line ends the event.
820        if isnewlin() > 0 && endtok == ENDINPUT {
821            return false;
822        }
823        zshlex();
824    }
825    // parse.c:644-647 — terminate on EOF or matching close-token.
826    if tok() == ENDINPUT {
827        return false;
828    }
829    if tok() == endtok {
830        return true;
831    }
832    // parse.c:649-... — drive par_sublist + handle terminator.
833    // zshrs's par_sublist already builds the AST node directly.
834    match par_sublist() {
835        Some(_) => {
836            // parse.c:651-693 — terminator handling. zshrs's
837            // par_list wraps this; for parse_event we just
838            // confirm the sublist parsed.
839            true
840        }
841        None => false,
842    }
843}
844
845/// Port of `parse_list(void)` from `Src/parse.c:697`. C-shape entry
846/// point: drives `par_list` and finalizes via `bld_eprog`. Returns
847/// `None` on syntax error.
848pub fn parse_list() -> Option<eprog> {
849    // c:697
850    set_tok(ENDINPUT);
851    init_parse();
852    zshlex();
853    // c:Src/parse.c:705 — `par_list(&c);` emits wordcode for the
854    // full multi-statement list (its goto-rec loop walks all
855    // SEPER-separated sublists). The Rust AST par_list() emits
856    // NOTHING to the wordcode buffer (only builds the AST), so
857    // bld_eprog returned an empty program AND tok stayed at
858    // SEPER, tripping the syntax-error check below for any
859    // \`cmd; cmd\` body.
860    //
861    // Route through par_event_wordcode (the wordcode emitter,
862    // lines 4395+) which mirrors C's par_list loop semantics
863    // and populates the wordcode buffer that bld_eprog reads.
864    let _start = par_event_wordcode();
865    if tok() != ENDINPUT {
866        clear_hdocs();
867        set_tok(LEXERR);
868        // c:Src/parse.c:708 — `yyerror(0);`. C-faithful invocation;
869        // the message format ("parse error near `X'") is built
870        // inside yyerror from zshlextext/tokstr().
871        yyerror(0);
872        return None;
873    }
874    Some(bld_eprog(false))
875}
876
877/// Port of `parse_cond(void)` from `Src/parse.c:722`. Only used by
878/// `bin_test`/`bin_bracket` for `/bin/test`/`[` compat — the
879/// `condlex` global must already point at `testlex` before entry.
880pub fn parse_cond() -> Option<eprog> {
881    // c:722
882    init_parse();
883    if par_cond().is_none() {
884        clear_hdocs();
885        return None;
886    }
887    Some(bld_eprog(true))
888}
889
890// ============================================================
891// Wordcode emission helpers (parse.c private helpers)
892//
893// Direct ports of zsh's wordcode-emission helpers in parse.c.
894// These write u32 opcodes into a flat `ecbuf` array thread-local
895// via ecadd / ecdel / ecispace / ecstrcode and friends. The
896// par_*_wordcode family at parse.rs:1700-3500 walks the lex
897// stream and emits a real wordcode buffer here.
898//
899// (The AST tree built by par_program / par_simple / etc. is a
900// separate path used by fusevm; see compile_zsh.rs for the AST
901// → fusevm-bytecode compiler.)
902// ============================================================
903
904/// Patch a list-placeholder wordcode with its actual opcode +
905/// jump distance. Direct port of zsh/Src/parse.c:738
906/// `set_list_code`. zsh emits an `ecadd(0)` placeholder before
907/// par_sublist runs, then comes back through set_list_code to
908/// rewrite the slot with WCB_LIST(type, distance) once the
909/// sublist's final length is known.
910///
911/// Port of `set_list_code(int p, int type, int cmplx)` from
912/// `Src/parse.c:738`. Patches the WCB_LIST header at `p` based on
913/// whether the sublist body is simple (single command, no
914/// pipeline) and Z_SYNC/Z_END — emits the Z_SIMPLE-optimized
915/// header when possible, otherwise the plain WCB_LIST(type, 0).
916pub fn set_list_code(p: usize, type_code: i32, cmplx: bool) {
917    let _ = wc_bdata;
918    // c:740 — `if (!cmplx && (type == Z_SYNC || type == (Z_SYNC | Z_END))
919    // && WC_SUBLIST_TYPE(ecbuf[p+1]) == WC_SUBLIST_END)`
920    let sublist_code = ECBUF.with_borrow(|b| b.get(p + 1).copied().unwrap_or(0));
921    let z = type_code;
922    let qualifies = !cmplx
923        && (z == Z_SYNC || z == (Z_SYNC | Z_END))
924        && WC_SUBLIST_TYPE(sublist_code) == WC_SUBLIST_END;
925    if qualifies {
926        // c:742 — `int ispipe = !(WC_SUBLIST_FLAGS(ecbuf[p+1])
927        // & WC_SUBLIST_SIMPLE);`
928        let ispipe = (WC_SUBLIST_FLAGS(sublist_code) & WC_SUBLIST_SIMPLE) == 0;
929        // c:743 — `ecbuf[p] = WCB_LIST((type|Z_SIMPLE), ecused-2-p);`
930        let used = ECUSED.get() as usize;
931        let off = used.saturating_sub(2 + p);
932        ECBUF.with_borrow_mut(|b| {
933            if p < b.len() {
934                b[p] = WCB_LIST((z | Z_SIMPLE) as wordcode, off as wordcode);
935            }
936        });
937        // c:744 — `ecdel(p+1);`
938        ecdel(p + 1);
939        // c:745-746 — `if (ispipe) ecbuf[p+1] = WC_PIPE_LINENO(ecbuf[p+1]);`
940        if ispipe {
941            ECBUF.with_borrow_mut(|b| {
942                if p + 1 < b.len() {
943                    b[p + 1] = WC_PIPE_LINENO(b[p + 1]);
944                }
945            });
946        }
947    } else {
948        // c:748 — `ecbuf[p] = WCB_LIST(type, 0);`
949        ECBUF.with_borrow_mut(|b| {
950            if p < b.len() {
951                b[p] = WCB_LIST(z as wordcode, 0);
952            }
953        });
954    }
955}
956
957/// Port of `set_sublist_code(int p, int type, int flags, int skip, int cmplx)`
958/// from `Src/parse.c:755`. Patches the WCB_SUBLIST header at `p`.
959/// When the sublist is non-complex (single command, no pipeline),
960/// sets WC_SUBLIST_SIMPLE and rewrites the following slot to
961/// `WC_PIPE_LINENO`.
962pub fn set_sublist_code(p: usize, type_code: i32, flags: i32, skip: i32, cmplx: bool) {
963    if cmplx {
964        // c:758 — `ecbuf[p] = WCB_SUBLIST(type, flags, skip);`
965        ECBUF.with_borrow_mut(|b| {
966            if p < b.len() {
967                b[p] = WCB_SUBLIST(type_code as wordcode, flags as wordcode, skip as wordcode);
968            }
969        });
970    } else {
971        // c:760 — `ecbuf[p] = WCB_SUBLIST(type, flags|WC_SUBLIST_SIMPLE, skip);`
972        ECBUF.with_borrow_mut(|b| {
973            if p < b.len() {
974                b[p] = WCB_SUBLIST(
975                    type_code as wordcode,
976                    (flags as wordcode) | WC_SUBLIST_SIMPLE,
977                    skip as wordcode,
978                );
979            }
980        });
981        // c:761 — `ecbuf[p+1] = WC_PIPE_LINENO(ecbuf[p+1]);`
982        ECBUF.with_borrow_mut(|b| {
983            if p + 1 < b.len() {
984                b[p + 1] = WC_PIPE_LINENO(b[p + 1]);
985            }
986        });
987    }
988}
989
990/// Parse a list (sublist with optional & or ;).
991///
992/// Direct port of zsh/Src/parse.c:771-804 `par_list` (and the
993/// par_list1 wrapper at parse.c:807-817).
994///
995/// **Structural divergence**: zsh's parse.c emits flat wordcode
996/// into the `ecbuf` u32 array via `ecadd(0)` (placeholder),
997/// `set_list_code(p, code, complexity)`, `wc_bdata(Z_END)`. zshrs
998/// builds an AST node `ZshList { sublist, flags }` instead. The
999/// async/sync/disown discrimination at parse.c:785-790 maps to
1000/// zshrs's `ListFlags { async_, disown }` field — Z_SYNC is the
1001/// default (no flags), Z_ASYNC = `&` = `async_=true`, Z_DISOWN +
1002/// Z_ASYNC = `&!`/`&|` = both true. Same semantics, different
1003/// representation. This divergence is repository-wide: every
1004/// `par_*` function emits wordcode in C, every `parse_*` builds
1005/// AST in Rust. The compile_zsh module then traverses the AST to
1006/// emit fusevm bytecode, which serves the same role as zsh's
1007/// wordcode but with a different opcode set and execution model.
1008fn par_list(single_event: bool) -> Option<(ZshList, bool)> {
1009    let sublist = par_sublist()?;
1010
1011    // c:769-803 — `list : { SEPER } [ sublist [ { SEPER | AMPER |
1012    // AMPERBANG } list ] ]`. The second tuple element reports
1013    // whether the list ended with an EXPLICIT terminator. C's list
1014    // grammar only chains sublists across SEPER/AMPER/AMPERBANG; a
1015    // sublist followed directly by another command (`{a} {b}`) ends
1016    // the list and the dangling token is the CALLER's problem
1017    // (par_while takes a dangling INBRACE as the loop body, par_event
1018    // yyerrors at c:671-680). parse_program_until needs this bit to
1019    // reproduce that split.
1020    let (flags, terminated) = match tok() {
1021        AMPER => {
1022            zshlex();
1023            (
1024                ListFlags {
1025                    async_: true,
1026                    disown: false,
1027                },
1028                true,
1029            )
1030        }
1031        AMPERBANG => {
1032            zshlex();
1033            (
1034                ListFlags {
1035                    async_: true,
1036                    disown: true,
1037                },
1038                true,
1039            )
1040        }
1041        SEPER => {
1042            // c:Src/parse.c par_event SEPER branch — `if (isnewlin <= 0
1043            // || endtok != ENDINPUT) zshlex();`. The lexer folds both
1044            // `;` and `\n` into SEPER (lex.rs:265), distinguishing them
1045            // via LEX_ISNEWLIN (C `isnewlin`). At a TOP-LEVEL event
1046            // boundary (single_event ≈ endtok == ENDINPUT) a SEPER that
1047            // was a NEWLINE terminates the event and is NOT consumed —
1048            // it's left for the next parse_event, whose leading skip
1049            // reads the following line only THEN. That's what lets
1050            // loop() read+execute one event at a time so `-v`/`-x` echo
1051            // and runtime `setopt verbose` interleave with execution
1052            // like zsh. A `;` SEPER (isnewlin <= 0), or any SEPER inside
1053            // a body / whole-program parse (single_event = false), is
1054            // consumed to chain the next sublist.
1055            let is_newline = crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) > 0;
1056            if !(single_event && is_newline) {
1057                zshlex();
1058            }
1059            (ListFlags::default(), true)
1060        }
1061        SEMI | NEWLIN => {
1062            // Unfolded `;` / preserved newline (LEXFLAGS_NEWLINE) — not a
1063            // top-level event terminator; consume and continue.
1064            zshlex();
1065            (ListFlags::default(), true)
1066        }
1067        _ => (ListFlags::default(), false),
1068    };
1069
1070    Some((ZshList { sublist, flags }, terminated))
1071}
1072
1073/// Parse one list — non-recursing variant. Direct port of
1074/// zsh/Src/parse.c:808 `par_list1`. Like par_list but
1075/// doesn't recurse on the trailing-separator path; used by
1076/// callers that only want one statement (e.g. each arm of a
1077/// case body).
1078pub fn par_list1() -> Option<ZshSublist> {
1079    // parse.c:810-816 — body is a single par_sublist call wrapped
1080    // in the eu/ecused tracking that zshrs doesn't need (no
1081    // wordcode buffer).
1082    par_sublist()
1083}
1084
1085/// Parse a sublist (pipelines connected by && or ||).
1086///
1087/// Direct port of zsh/Src/parse.c:825 `par_sublist` and
1088/// par_sublist2 at parse.c:869-892. par_sublist handles the
1089/// && / || conjunction and emits WC_SUBLIST opcodes; par_sublist2
1090/// handles the leading `!` negation and `coproc` keyword.
1091///
1092/// AST mapping: ZshSublist { pipe, conj_chain }, where `conj_chain`
1093/// is a Vec<(ConjOp, ZshSublist)> for chained && / ||. C uses
1094/// flat wordcode with WC_SUBLIST_AND / WC_SUBLIST_OR markers.
1095fn par_sublist() -> Option<ZshSublist> {
1096    let mut flags = SublistFlags::default();
1097
1098    // Handle coproc and !
1099    if tok() == COPROC {
1100        flags.coproc = true;
1101        zshlex();
1102    } else if tok() == BANG_TOK {
1103        flags.not = true;
1104        zshlex();
1105    }
1106
1107    let pipe = par_pline()?;
1108
1109    // Check for && or ||
1110    let next = match tok() {
1111        DAMPER => {
1112            zshlex();
1113            skip_separators();
1114            // c:Src/parse.c:par_sublist — and-or operators (`&&`,
1115            // `||`) require a sublist on each side. After consuming
1116            // `&&`/`||`, another and-or operator OR a pipe-operator
1117            // immediately after is a parse error in C zsh. zshrs's
1118            // recursion silently returned None and dropped the
1119            // operator. Bug #171 in docs/BUGS.md.
1120            if matches!(tok(), DAMPER | DBAR | BAR_TOK | BARAMP) {
1121                let name = match tok() {
1122                    DAMPER => "&&",
1123                    DBAR => "||",
1124                    BAR_TOK => "|",
1125                    BARAMP => "|&",
1126                    _ => "operator",
1127                };
1128                zerr(&format!("parse error near `{}'", name));
1129                return None;
1130            }
1131            par_sublist().map(|s| (SublistOp::And, Box::new(s)))
1132        }
1133        DBAR => {
1134            zshlex();
1135            skip_separators();
1136            if matches!(tok(), DAMPER | DBAR | BAR_TOK | BARAMP) {
1137                let name = match tok() {
1138                    DAMPER => "&&",
1139                    DBAR => "||",
1140                    BAR_TOK => "|",
1141                    BARAMP => "|&",
1142                    _ => "operator",
1143                };
1144                zerr(&format!("parse error near `{}'", name));
1145                return None;
1146            }
1147            par_sublist().map(|s| (SublistOp::Or, Box::new(s)))
1148        }
1149        _ => None,
1150    };
1151
1152    Some(ZshSublist { pipe, next, flags })
1153}
1154
1155/// Port of `par_sublist2(int *cmplx)` from `Src/parse.c:869`.
1156/// Secondary-sublist arm: handles the `COPROC`/`Bang` prefix
1157/// in front of a pline. Returns the WC_SUBLIST flag word added.
1158pub fn par_sublist2(cmplx: &mut i32) -> Option<i32> {
1159    // c:870 — `int f = 0;`
1160    let mut f: i32 = 0;
1161    // c:873-880 — COPROC / BANG prefix flags.
1162    if tok() == COPROC {
1163        *cmplx = 1;
1164        f |= WC_SUBLIST_COPROC as i32;
1165        zshlex();
1166    } else if tok() == BANG_TOK {
1167        *cmplx = 1;
1168        f |= WC_SUBLIST_NOT as i32;
1169        zshlex();
1170    }
1171    // c:882-883 — `if (!par_pline(cmplx) && !f) return -1;`
1172    if !par_pipe_wordcode(cmplx) && f == 0 {
1173        return None;
1174    }
1175    // c:885 — `return f;`
1176    Some(f)
1177}
1178
1179/// Parse a pipeline
1180/// Parse a pipeline (cmds joined by `|` / `|&`). Direct port of
1181/// zsh/Src/parse.c:894 `par_pline`. AST: ZshPipe { cmds: Vec<ZshCommand> }.
1182/// C emits WC_PIPE wordcodes per command; same flow.
1183fn par_pline() -> Option<ZshPipe> {
1184    let lineno = toklineno();
1185    let cmd = par_cmd()?;
1186
1187    // Check for | or |&
1188    let mut merge_stderr = false;
1189    let next = match tok() {
1190        BAR_TOK | BARAMP => {
1191            merge_stderr = tok() == BARAMP;
1192            zshlex();
1193            skip_separators();
1194            // c:Src/parse.c:par_pline — pipe-operators require a
1195            // command on each side. After consuming `|`/`|&`,
1196            // C zsh's recursive par_pline call returns -1 (parse
1197            // error) when the next token is another pipe-operator
1198            // — `a | | b` errors with `parse error near `|''`.
1199            // zshrs's `par_pline()?` silently returned None on
1200            // missing command, dropping the rest of the input
1201            // without diagnosing the empty-pipe-operand. Bug #171
1202            // in docs/BUGS.md.
1203            if matches!(tok(), BAR_TOK | BARAMP) {
1204                let name = if tok() == BARAMP { "|&" } else { "|" };
1205                zerr(&format!("parse error near `{}'", name));
1206                return None;
1207            }
1208            par_pline().map(Box::new)
1209        }
1210        _ => None,
1211    };
1212
1213    Some(ZshPipe {
1214        cmd,
1215        next,
1216        lineno,
1217        merge_stderr,
1218    })
1219}
1220
1221/// Parse a command
1222/// Parse a command — dispatches by leading token (FOR / CASE /
1223/// IF / WHILE / UNTIL / REPEAT / FUNC / DINBRACK / DINPAR /
1224/// Inpar subshell / Inbrace current-shell / TIME / NOCORRECT,
1225/// else simple). Direct port of zsh/Src/parse.c:958 `par_cmd`.
1226fn par_cmd() -> Option<ZshCommand> {
1227    // Parse leading redirections
1228    let mut redirs = Vec::new();
1229    while IS_REDIROP(tok()) {
1230        if let Some(redir) = par_redir() {
1231            redirs.push(redir);
1232        }
1233    }
1234
1235    // c:Src/parse.c:970-1030 par_cmd — push the open construct onto the
1236    // cmdstack for the duration of its sub-parse, then pop. The cmdstack
1237    // drives `%_` prompt expansion, so this is what makes a multi-line
1238    // construct's PS2 render `for> ` / `while> ` / `case> ` across
1239    // continuation lines instead of a bare `> `. (The cmdstack does NOT
1240    // drive tokenization in zshrs — the lexer's only reads are the
1241    // debug-only DPUTS "cmdstack not empty" asserts — so wrapping the AST
1242    // dispatch is behaviorally inert outside the prompt.) IF is excluded
1243    // to match C, where par_if manages its own cmdstack (CS_IF/CS_IFTHEN);
1244    // DINPAR/TIME/INOUTPAR are not cmdstack constructs in C either.
1245    let cmd = match tok() {
1246        FOR => {
1247            cmdpush(CS_FOR as u8); // c:972
1248            let c = par_for();
1249            cmdpop(); // c:974
1250            c
1251        }
1252        FOREACH => {
1253            cmdpush(CS_FOREACH as u8); // c:977
1254            let c = par_for();
1255            cmdpop(); // c:979
1256            c
1257        }
1258        SELECT => {
1259            cmdpush(CS_SELECT as u8); // c:983
1260            let c = parse_select();
1261            cmdpop(); // c:985
1262            c
1263        }
1264        CASE => {
1265            cmdpush(CS_CASE as u8); // c:988
1266            let c = par_case();
1267            cmdpop(); // c:990
1268            c
1269        }
1270        IF => par_if(), // c:992-994 — par_if manages its own cmdstack
1271        WHILE => {
1272            cmdpush(CS_WHILE as u8); // c:996
1273            let c = par_while(false);
1274            cmdpop(); // c:998
1275            c
1276        }
1277        UNTIL => {
1278            cmdpush(CS_UNTIL as u8); // c:1001
1279            let c = par_while(true);
1280            cmdpop(); // c:1003
1281            c
1282        }
1283        REPEAT => {
1284            cmdpush(CS_REPEAT as u8); // c:1006
1285            let c = par_repeat();
1286            cmdpop(); // c:1008
1287            c
1288        }
1289        INPAR_TOK => {
1290            cmdpush(CS_SUBSH as u8); // c:1012
1291            let c = par_subsh();
1292            cmdpop(); // c:1014
1293            c
1294        }
1295        INOUTPAR => parse_anon_funcdef(),
1296        INBRACE_TOK => {
1297            cmdpush(CS_CURSH as u8); // c:1017
1298            let c = parse_cursh();
1299            cmdpop(); // c:1019
1300            c
1301        }
1302        FUNC => {
1303            cmdpush(CS_FUNCDEF as u8); // c:1022
1304            let c = par_funcdef();
1305            cmdpop(); // c:1024
1306            c
1307        }
1308        DINBRACK => {
1309            cmdpush(CS_COND as u8); // c:1027
1310            let c = par_cond();
1311            cmdpop(); // c:1029
1312            c
1313        }
1314        DINPAR => parse_arith(),
1315        TIME => par_time(),
1316        _ => par_simple(redirs),
1317    };
1318
1319    // Parse trailing redirections. For Simple commands the redirs were
1320    // already captured inside par_simple; for compound forms (Cursh,
1321    // Subsh, If, While, etc.) we collect them here and wrap in
1322    // ZshCommand::Redirected so compile_zsh can scope-bracket them.
1323    if let Some(inner) = cmd {
1324        let mut trailing: Vec<ZshRedir> = Vec::new();
1325        while IS_REDIROP(tok()) {
1326            if let Some(redir) = par_redir() {
1327                trailing.push(redir);
1328            }
1329        }
1330        // c:Src/parse.c:par_cmd — compound forms (Cursh `{...}`, Subsh
1331        // `(...)`, If/While/Until/For/Case/Select/Repeat/Funcdef) must
1332        // be followed by a valid sublist/list separator (`;`, `\n`,
1333        // `&`, `|`, `&&`, `||`, redirect-op) — STRING_LEX after a
1334        // compound is a parse error. zshrs's outer par_list loop
1335        // silently treated trailing words as a new command, masking
1336        // syntax errors like `{ echo a; } b c`. Mirror C's strict
1337        // post-compound terminator check. Bug #146 in docs/BUGS.md.
1338        if !matches!(inner, ZshCommand::Simple(_)) && tok() == STRING_LEX {
1339            // c:Src/parse.c:2738 — the error token is `zshlextext`, the
1340            // human-readable source text, so quotes/sigils are shown
1341            // verbatim (`"quoted"`, `'x'`, `$var`). zshrs's `tokstr()`
1342            // still carries the inline quote MARKERS (Dnull/Snull/Stringg
1343            // = `\u{9e}`/`\u{9d}`/`\u{85}`); emitting it raw printed the
1344            // marker bytes and dropped the visible quotes. Untokenize to
1345            // the display form: Dnull→`"`, Snull→`'`, Stringg→`$`, Bnull→`\`.
1346            let bad = crate::ported::lex::untokenize_preserve_quotes(
1347                &tokstr().unwrap_or_default(),
1348            );
1349            zerr(&format!("parse error near `{}'", bad));
1350            // Reset state before returning so the outer loop's None
1351            // detection unwinds cleanly.
1352            set_incmdpos(true);
1353            set_incasepat(0);
1354            set_incond(0);
1355            set_intypeset(false);
1356            return None;
1357        }
1358        // c:1072-1075 — every par_cmd tail resets the lexer state
1359        // toggles so the NEXT command starts in cmd position with
1360        // case/cond/typeset off. par_simple/par_cond set `incmdpos=0`
1361        // during their bodies; without this reset the next iteration
1362        // of the outer par_list loop sees `if` / `done` / `select`
1363        // etc. as plain strings and the AST collapses.
1364        set_incmdpos(true);
1365        set_incasepat(0);
1366        set_incond(0);
1367        set_intypeset(false);
1368        if trailing.is_empty() {
1369            return Some(inner);
1370        }
1371        // Simple already absorbed its own redirs (compile path expects
1372        // them on ZshSimple), so don't double-wrap.
1373        if matches!(inner, ZshCommand::Simple(_)) {
1374            if let ZshCommand::Simple(mut s) = inner {
1375                s.redirs.extend(trailing);
1376                return Some(ZshCommand::Simple(s));
1377            }
1378            unreachable!()
1379        }
1380        return Some(ZshCommand::Redirected(Box::new(inner), trailing));
1381    }
1382    // Same reset on the empty-cmd branch (mirror c:1072 unconditional
1383    // path — the C function only returns 0 above when the dispatch
1384    // produced no command, and falls through to the reset block).
1385    set_incmdpos(true);
1386    set_incasepat(0);
1387    set_incond(0);
1388    set_intypeset(false);
1389
1390    None
1391}
1392
1393/// Parse for/foreach loop
1394/// Parse `for NAME in WORDS; do BODY; done` (foreach style) AND
1395/// `for ((init; cond; incr)) do BODY done` (c-style). Direct port
1396/// of zsh/Src/parse.c:1087 `par_for`. parse_for_cstyle is the
1397/// inner branch for the `((...))` arithmetic-header variant
1398/// (parse.c:1100-1140 inside par_for).
1399fn par_for() -> Option<ZshCommand> {
1400    let is_foreach = tok() == FOREACH;
1401    // c:1094-1095 (Src/parse.c, par_for) — set `infor=2` (only when
1402    // tok==FOR) so the lexer's `(` peek at lex.c:784-789
1403    // (`if (infor) { ... return DINPAR; }`) routes the arith-for
1404    // body through dbparens semicolon-splitting instead of the
1405    // `cmd_or_math` whole-body capture path. Without this, `for ((
1406    // i=0; i<3; i++ ))` lexed as a single `((arith))` expression
1407    // and parse_for_cstyle's second zshlex got an empty/wrong tok.
1408    //
1409    // Guard the loop-variable read against alias expansion and
1410    // spell-correction. C uses `incmdpos = 0` for the first name
1411    // (c:1094) plus `noaliases = nocorrect = 1` for the rest
1412    // (c:1123), restoring them at c:1137. We guard the WHOLE name
1413    // read with `noaliases`/`nocorrect` (the actual anti-alias
1414    // mechanism) and leave `incmdpos` untouched: without this, a
1415    // user `alias i='if [[ … ]]; then … fi'` (real zpwr config,
1416    // set by zpwrBindAliasesZshLate) makes `for i in {0..$#}`
1417    // expand the loop variable `i` into `if`, so par_for's next
1418    // token is IF instead of the STRING name and it dies with
1419    // "expected variable name in for". Forcing incmdpos=0 here
1420    // (as C does) instead regresses the `(`/`((`/`do` detection
1421    // for for-paren, arith-for and the loop body, which this
1422    // parser recovers from the inherited command position.
1423    let saved_noaliases = noaliases();
1424    let saved_nocorrect = nocorrect();
1425    set_noaliases(true);
1426    set_nocorrect(1);
1427    set_infor(if tok() == FOR { 2 } else { 0 }); // c:1095
1428    zshlex(); // c:1096
1429
1430    // Check for C-style: for (( init; cond; step ))
1431    if tok() == DINPAR {
1432        // c:1110-1111 — close out infor / cmdpos after parse_for_cstyle
1433        // has consumed the init/cond/step triple. Done inside the
1434        // helper itself so we honour the C ordering.
1435        set_noaliases(saved_noaliases);
1436        set_nocorrect(saved_nocorrect);
1437        return parse_for_cstyle();
1438    }
1439
1440    // c:1116 — `infor = 0;` immediately on entering the foreach
1441    // branch. Without this, `infor` stays at 2 (set at c:1095 when
1442    // tok==FOR) for the rest of par_for, and the lexer's `((`
1443    // peek at lex.c:786 routes every subsequent `((...))` inside
1444    // the loop body through dbparens — so `for x in a; do (( 1
1445    // )); done` and `if (( 1 )) { … }` inside the do-body both
1446    // mis-lexed as a c-style for header.
1447    set_infor(0); // c:1116
1448
1449    // Get variable name(s). zsh parse.c par_for accepts multiple
1450    // identifier tokens before `in`/`(`/newline — `for k v in ...`
1451    // assigns each iteration's pair of values to k and v in turn.
1452    // We store the names space-joined since variable identifiers
1453    // can't contain whitespace.
1454    let mut names: Vec<String> = Vec::new();
1455    while tok() == STRING_LEX {
1456        let v = tokstr().unwrap_or_default();
1457        if v == "in" {
1458            break;
1459        }
1460        names.push(v);
1461        zshlex();
1462    }
1463    // c:1137-1138 — restore alias / spell-correct state now the
1464    // name list is fully read (the `in`/`(` list that follows must
1465    // lex with the caller's normal settings).
1466    set_noaliases(saved_noaliases);
1467    set_nocorrect(saved_nocorrect);
1468    if names.is_empty() {
1469        zerr("expected variable name in for");
1470        return None;
1471    }
1472    let var = names.join(" ");
1473
1474    // Skip newlines
1475    skip_separators();
1476
1477    // Get list. The lexer-port quirk: `for x (a b c)` arrives as a
1478    // single String token with the parens lexed-as-content
1479    // (`<Inpar>a b c<Outpar>`) instead of as separate Inpar/String/
1480    // Outpar tokens. Detect that shape and split it manually.
1481    let list = if tok() == STRING_LEX
1482        && tokstr()
1483            .map(|s| s.starts_with('\u{88}') && s.ends_with('\u{8a}'))
1484            .unwrap_or(false)
1485    {
1486        let raw = tokstr().unwrap_or_default();
1487        // Strip leading Inpar + trailing Outpar. KEEP the inner
1488        // content tokenized — `for x ({1..3}) …` has `{1..3}` as
1489        // Inbrace+content+Outbrace markers, which compile_word_str
1490        // needs to detect and brace-expand. Untokenizing here would
1491        // collapse the markers to plain `{` `}` chars and the brace-
1492        // expansion pass (which strictly requires Inbrace TOKEN per
1493        // Src/glob.c:hasbraces) would skip the word entirely.
1494        // Split only on UNTOKENIZED whitespace at the top level —
1495        // tokenized characters (TOKEN range \u{84}..\u{a1}) are part
1496        // of one word; bare ASCII spaces / tabs separate words.
1497        let inner = &raw[raw.char_indices().nth(1).map(|(i, _)| i).unwrap_or(0)
1498            ..raw
1499                .char_indices()
1500                .last()
1501                .map(|(i, _)| i)
1502                .unwrap_or(raw.len())];
1503        let mut words: Vec<String> = Vec::new();
1504        let mut cur = String::new();
1505        for c in inner.chars() {
1506            if c == ' ' || c == '\t' || c == '\n' {
1507                if !cur.is_empty() {
1508                    words.push(std::mem::take(&mut cur));
1509                }
1510            } else {
1511                cur.push(c);
1512            }
1513        }
1514        if !cur.is_empty() {
1515            words.push(cur);
1516        }
1517        zshlex();
1518        ForList::Words(words)
1519    } else if tok() == STRING_LEX {
1520        let s = tokstr();
1521        if s.map(|s| s == "in").unwrap_or(false) {
1522            // c:Src/parse.c:1147-1154 — after consuming `in`, the
1523            // for-list reads in WORD position, not command position.
1524            // Reset incmdpos=false so the lexer's LX2_INBRACE arm
1525            // (lex.rs:1791) treats a leading `{` as the brace-
1526            // expansion marker (`bct++; add(Inbrace)`) instead of
1527            // returning STRING("{") + promoting to INBRACE_TOK.
1528            // Without this, `for i in {1..3}` saw `{` as the body-
1529            // opener brace, so the word-collection loop got an
1530            // empty word list and the loop body silently ran 0
1531            // iterations.
1532            set_incmdpos(false);
1533            zshlex();
1534            let mut words = Vec::new();
1535            while tok() == STRING_LEX {
1536                let _ts_s = tokstr();
1537                if let Some(s) = _ts_s.as_deref() {
1538                    words.push(s.to_string());
1539                }
1540                zshlex();
1541            }
1542            // c:Src/parse.c:1162 — `incmdpos = 1;` after the
1543            // wordlist + SEPER are consumed, so the next token
1544            // (`do` / `{` body opener) lexes at command position.
1545            set_incmdpos(true);
1546            ForList::Words(words)
1547        } else {
1548            ForList::Positional
1549        }
1550    } else if tok() == INPAR_TOK {
1551        // for var (...) — `for x ({1..3})`: inside the parens, the
1552        // list is in WORD position so `{` must lex as the brace-
1553        // expansion Inbrace marker, NOT as a body-opener INBRACE_TOK.
1554        // Without resetting incmdpos before the next zshlex, the
1555        // lexer's LX2_INBRACE arm promotes `{` to INBRACE_TOK and
1556        // the word-collection loop exits empty, giving
1557        // `for x ({1..3})` an empty iteration.
1558        set_incmdpos(false);
1559        zshlex();
1560        let mut words = Vec::new();
1561        while tok() == STRING_LEX || tok() == SEPER {
1562            if tok() == STRING_LEX {
1563                let _ts_s = tokstr();
1564                if let Some(s) = _ts_s.as_deref() {
1565                    words.push(s.to_string());
1566                }
1567            }
1568            zshlex();
1569        }
1570        if tok() == OUTPAR_TOK {
1571            // After the `)` of a for-list, the next token is the
1572            // body opener — `do`/`{`. zsh's lexer needs incmdpos
1573            // set so `{` lexes as Inbrace (not as a literal). C
1574            // analogue: parse.c::par_for sets `incmdpos = 1`
1575            // after consuming the Outpar before the body parse.
1576            set_incmdpos(true);
1577            zshlex();
1578        }
1579        ForList::Words(words)
1580    } else {
1581        ForList::Positional
1582    };
1583
1584    // Skip to body
1585    skip_separators();
1586
1587    // Parse body
1588    let body = parse_loop_body(is_foreach, false)?;
1589
1590    Some(ZshCommand::For(ZshFor {
1591        var,
1592        list,
1593        body: Box::new(body),
1594        is_select: false,
1595    }))
1596}
1597
1598/// Parse case statement
1599/// Parse `case WORD in PATTERN) BODY ;; ... esac`. Direct port
1600/// of zsh/Src/parse.c:1209 `par_case`. Each case arm is a
1601/// (pattern_list, body, terminator) tuple where terminator is
1602/// `;;` (default), `;&` (fallthrough), or `;|` (continue testing).
1603fn par_case() -> Option<ZshCommand> {
1604    // C par_case (parse.c:1209-1241). Order of state toggles
1605    // matters — the lexer reads the case word in `incmdpos=0`
1606    // (so it's not promoted to a reswd), then the `in`/`{` in
1607    // `incmdpos=1, noaliases=1, nocorrect=1` (so the `in` literal
1608    // isn't alias-expanded or spell-corrected), then sets
1609    // `incasepat=1, incmdpos=0` before the first pattern.
1610    set_incmdpos(false);
1611    zshlex(); // skip 'case'
1612
1613    let word = match tok() {
1614        STRING_LEX => {
1615            let w = tokstr().unwrap_or_default();
1616            // c:1222 — `incmdpos = 1;` before the next zshlex so the
1617            // `in` keyword is recognised. c:1223-1225 — save+force
1618            // noaliases / nocorrect.
1619            set_incmdpos(true);
1620            let ona = noaliases();
1621            let onc = nocorrect();
1622            set_noaliases(true);
1623            set_nocorrect(1);
1624            zshlex();
1625            // Restore noaliases/nocorrect after the `in`-or-`{` token
1626            // is in hand; both are unconditionally restored at c:1238-1239.
1627            let restore = |ona: bool, onc: i32| {
1628                set_noaliases(ona);
1629                set_nocorrect(onc);
1630            };
1631            (w, ona, onc, restore)
1632        }
1633        _ => {
1634            zerr("expected word after case");
1635            return None;
1636        }
1637    };
1638    let (word, ona, onc, restore) = word;
1639
1640    skip_separators();
1641
1642    // Expect 'in' or {
1643    let use_brace = tok() == INBRACE_TOK;
1644    if tok() == STRING_LEX {
1645        let s = tokstr();
1646        if s.map(|s| s != "in").unwrap_or(true) {
1647            // c:1228-1232 — restore noaliases/nocorrect on error path.
1648            restore(ona, onc);
1649            zerr("expected 'in' in case");
1650            return None;
1651        }
1652    } else if !use_brace {
1653        restore(ona, onc);
1654        zerr("expected 'in' or '{' in case");
1655        return None;
1656    }
1657    // c:1236-1239 — `incasepat = 1; incmdpos = 0; noaliases = ona;
1658    // nocorrect = onc;` — set the case-pattern context AND restore
1659    // alias/correct state BEFORE the zshlex that consumes `in`/`{`.
1660    set_incasepat(1);
1661    set_incmdpos(false);
1662    restore(ona, onc);
1663    zshlex();
1664
1665    let mut arms = Vec::new();
1666
1667    loop {
1668        // No arm-count cap: zsh `case` statements have no limit, and compsys
1669        // dispatchers (`_describe`, `_git`, generated `_arguments` bodies) emit
1670        // huge case bodies. The loop terminates on `esac` / EOF below; a former
1671        // 10_000-arm cap silently truncated (and errored on) larger ones.
1672
1673        // Set incasepat BEFORE skipping separators so lexer knows we're in case pattern context
1674        // This affects how [ and | are lexed
1675        set_incasepat(1);
1676
1677        skip_separators();
1678
1679        // Check for end
1680        // Note: 'esac' might be String "esac" if incasepat > 0 prevents reserved word recognition
1681        let is_esac = tok() == ESAC
1682            || (tok() == STRING_LEX && tokstr().map(|s| s == "esac").unwrap_or(false));
1683        if (use_brace && tok() == OUTBRACE_TOK) || (!use_brace && is_esac) {
1684            set_incasepat(0);
1685            zshlex();
1686            break;
1687        }
1688
1689        // Also break on EOF. c:Src/parse.c:1209 par_case requires
1690        // ESAC (or `}` in brace form) to close the block — reaching
1691        // ENDINPUT without either is a parse error (`case ... esack`
1692        // typo absorbs `esack` as part of the body and silently
1693        // terminates rc=0 otherwise). Bug #400.
1694        if tok() == ENDINPUT || tok() == LEXERR {
1695            set_incasepat(0);
1696            crate::ported::utils::zerr("unmatched `case'");
1697            yyerror(0);
1698            break;
1699        }
1700
1701        // c:1250 — `if (tok == INPAR) zshlex();` — leading-paren
1702        // skip path. Used when the lexer DID return INPAR_TOK (e.g.
1703        // SHGLOB or incmdpos forced it). In the normal case-pattern
1704        // path the lexer absorbs `(...)` into one Stringg and the
1705        // hack at c:1322 strips the surrounding parens later. Both
1706        // paths land here.
1707        let leading_inpar_consumed = tok() == INPAR_TOK;
1708        if leading_inpar_consumed {
1709            zshlex();
1710        }
1711
1712        // c:1255-1262 — read pattern STRING. zsh's parser falls
1713        // straight into the STRING reader after the optional INPAR.
1714        // BAR before any pattern means empty string.
1715        let mut patterns = Vec::new();
1716        // Tracks whether the c:1322-1354 hack has fired (paren-
1717        // wrapped Stringg absorbed by the lexer). When it has, the
1718        // closing `)` was already absorbed — no separate OUTPAR
1719        // arm-close to consume.
1720        let mut absorbed_outpar = false;
1721
1722        // Nested-paren pattern: the user wrote `((alt|alt)tail)` —
1723        // the leading arm-INPAR was consumed; the NEXT token is also
1724        // INPAR. zinit.zsh:2946 hits this with `((add-|)fpath)`
1725        // meaning "fpath or add-fpath".
1726        //
1727        // The current lexer (cmd_or_math rewind path interacting
1728        // with gettokstr) drops ONE of the two `)` chars on the
1729        // way out, so the token stream is INPAR INPAR STRING BAR
1730        // STRING OUTPAR — only ONE OUTPAR for two source `)`. We
1731        // can't reconstruct the exact pattern from these tokens.
1732        // Proper fix needs to land in lex.rs (cmd_or_math rewind +
1733        // gettokstr LX2_INPAR/OUTPAR interplay) — see docs/BUGS.md
1734        // entry "case-pattern nested-paren lexer gap".
1735        //
1736        // Workaround: consume every token up to and INCLUDING the
1737        // arm-closing OUTPAR, build a best-effort pattern string,
1738        // set `absorbed_outpar = true` so the post-pattern OUTPAR
1739        // check below skips. The resulting glob may match a slightly
1740        // wider set than the original (`(add-|fpath)` vs
1741        // `(add-|)fpath`) but the rest of the script parses + runs.
1742        // Unblocking sourcing zinit-style configs is the priority.
1743        if leading_inpar_consumed && tok() == INPAR_TOK {
1744            let mut buf = String::new();
1745            let mut depth = 0i32;
1746            loop {
1747                match tok() {
1748                    INPAR_TOK => {
1749                        buf.push('(');
1750                        depth += 1;
1751                        zshlex();
1752                    }
1753                    OUTPAR_TOK => {
1754                        // The single OUTPAR token in the stream is
1755                        // simultaneously the inner glob-group close
1756                        // AND the arm close (lexer collapses both).
1757                        // Consume it, balance the inner depth, exit.
1758                        // `absorbed_outpar = true` tells the shared
1759                        // post-pattern check below to skip its own
1760                        // consume.
1761                        if depth > 0 {
1762                            buf.push(')');
1763                            depth -= 1;
1764                        }
1765                        zshlex();
1766                        break;
1767                    }
1768                    STRING_LEX => {
1769                        if let Some(s) = tokstr() {
1770                            if s == "esac" {
1771                                break;
1772                            }
1773                            buf.push_str(&s);
1774                        }
1775                        set_incasepat(2);
1776                        zshlex();
1777                    }
1778                    BAR_TOK => {
1779                        buf.push('|');
1780                        set_incasepat(1);
1781                        zshlex();
1782                    }
1783                    _ => break,
1784                }
1785            }
1786            patterns.push(buf);
1787            set_incasepat(0);
1788            set_incmdpos(true);
1789            absorbed_outpar = true;
1790        }
1791
1792        // Skip the legacy STRING/BAR pattern-read loop when the
1793        // nested-paren branch above already populated `patterns` and
1794        // consumed the arm-close. Otherwise the legacy loop would
1795        // greedily absorb the body's first command token (`echo`) as
1796        // another alt-pattern.
1797        if !patterns.is_empty() && absorbed_outpar {
1798            // skip to body parse
1799        } else {
1800            loop {
1801                if tok() == STRING_LEX {
1802                    let s = tokstr();
1803                    if s.as_deref().map(|s| s == "esac").unwrap_or(false) {
1804                        break;
1805                    }
1806                    let mut str_val = s.unwrap_or_default();
1807
1808                    // c:1322-1354 hack: when this is the first alt AND
1809                    // the string starts with the Inpar marker, the lexer
1810                    // absorbed the whole `(...)` as one token. Chuck the
1811                    // blanks around `|`/parens at depth 1 (c:1332-1338 —
1812                    // `( d | e )` must become `(d|e)`), then strip the
1813                    // surrounding parens — the remainder IS the pattern.
1814                    // The closing arm-paren was absorbed too, so we don't
1815                    // expect a separate OUTPAR token afterward.
1816                    if patterns.is_empty() && str_val.starts_with(crate::ported::zsh_h::Inpar) {
1817                        use crate::ported::zsh_h::{Bar, Inpar, Outpar};
1818                        let meta = crate::ported::zsh_h::Meta as char;
1819                        let blank =
1820                            |c: char| c.is_ascii() && crate::ported::ztype_h::iblank(c as u8);
1821                        let mut chars: Vec<char> = str_val.chars().collect();
1822                        let mut pct = 0i32;
1823                        let mut i = 0usize;
1824                        let mut end_idx: Option<usize> = None;
1825                        while i < chars.len() {
1826                            if chars[i] == Inpar {
1827                                pct += 1;
1828                            }
1829                            if pct == 1 {
1830                                // c:1332-1334 — chuck blanks AFTER `|`/`(`.
1831                                if chars[i] == Bar || chars[i] == Inpar {
1832                                    while i + 1 < chars.len() && blank(chars[i + 1]) {
1833                                        chars.remove(i + 1);
1834                                    }
1835                                }
1836                                // c:1335-1338 — chuck blanks BEFORE `|`/`)`
1837                                // (not Meta-escaped blanks).
1838                                if chars[i] == Bar || chars[i] == Outpar {
1839                                    while i >= 1
1840                                        && blank(chars[i - 1])
1841                                        && (i < 2 || chars[i - 2] != meta)
1842                                    {
1843                                        chars.remove(i - 1);
1844                                        i -= 1;
1845                                    }
1846                                }
1847                            }
1848                            if chars[i] == Outpar {
1849                                pct -= 1;
1850                                if pct == 0 {
1851                                    end_idx = Some(i);
1852                                    break;
1853                                }
1854                            }
1855                            i += 1;
1856                        }
1857                        if let Some(idx) = end_idx {
1858                            // c:1346-1352 — the surrounding-paren strip is only
1859                            // valid when the WHOLE string is `(...)` (close-paren
1860                            // is the last char). C asserts this with
1861                            // `DPUTS(*str != Inpar || str[sl-1] != Outpar)` and
1862                            // `YYERRORV` on `if (*s || pct ...)` when anything
1863                            // follows the matched close-paren. When trailing
1864                            // content exists (`(a|b)*`, `(a|b)c`), the `(...)` is
1865                            // a glob GROUP, not the optional case-opener — keep
1866                            // the whole string verbatim as one pattern and let
1867                            // the separate arm-closing `)` (OUTPAR_TOK) be
1868                            // consumed below. Stripping here turned `(a|b)*` into
1869                            // `a|b*` ("a" or "b*"), breaking OS-dispatch case arms.
1870                            if idx == chars.len() - 1 {
1871                                chars.remove(idx);
1872                                chars.remove(0);
1873                                str_val = chars.into_iter().collect();
1874                                absorbed_outpar = true;
1875                            }
1876                        }
1877                    }
1878                    patterns.push(str_val);
1879                    if absorbed_outpar {
1880                        // c:Src/parse.c:1300-1302 — after a whole-`(...)`
1881                        // pattern the next token may be the body's first
1882                        // command word; C lexes it with `incasepat = -1;
1883                        // incmdpos = 1;` so assignments (`out+=hit`)
1884                        // become ENVSTRING instead of a plain STRING
1885                        // (lex.c:1229-1230 gates ENVSTRING on incmdpos).
1886                        set_incasepat(-1);
1887                        set_incmdpos(true);
1888                    } else {
1889                        set_incasepat(2);
1890                    }
1891                    zshlex();
1892                    // When the hack fired the closing `)` is already
1893                    // consumed; don't read alt-`|` continuations either.
1894                    if absorbed_outpar {
1895                        break;
1896                    }
1897                } else if tok() != BAR_TOK {
1898                    break;
1899                }
1900
1901                if tok() == BAR_TOK {
1902                    set_incasepat(1);
1903                    zshlex();
1904                } else {
1905                    break;
1906                }
1907            }
1908        } // end else of "skip legacy loop when nested-paren branch fired"
1909        set_incasepat(0);
1910
1911        // c:1305 — expect OUTPAR (arm-close) when the hack didn't
1912        // already swallow it.
1913        //
1914        // Bug #34 in docs/BUGS.md: the absorbed-pattern hack assumed
1915        // the leading `(` and the case-arm closing `)` were both
1916        // absorbed into the single STRING token. That's true for
1917        // `(x))` (the inner `)` closes the absorbed group; the second
1918        // `)` is the arm closer) only when the lexer slurps BOTH.
1919        // The Rust lexer slurps just `(x|y)` (one balanced pair); the
1920        // second `)` arrives as a separate OUTPAR_TOK that must still
1921        // be consumed as the case-arm closer. Detect and consume it.
1922        if !absorbed_outpar {
1923            if tok() != OUTPAR_TOK {
1924                zerr("expected ')' in case pattern");
1925                return None;
1926            }
1927            // c:Src/parse.c:1257-1258 — `if (tok != STRING)
1928            // YYERRORV(oecused);` C requires at least one pattern
1929            // STRING before `)`. zshrs accepted empty `case x in)`
1930            // and silently fell through to the next iteration with
1931            // an empty pattern arm, swallowing the rest of the
1932            // script. Reject the empty-pattern shape unless a
1933            // leading INPAR was consumed (the `(pat)` form has
1934            // already validated the pattern inside). Bug #161 in
1935            // docs/BUGS.md.
1936            if patterns.is_empty() && !leading_inpar_consumed {
1937                zerr("parse error near `)'");
1938                return None;
1939            }
1940            set_incmdpos(true);
1941            zshlex();
1942            // When the lexer emitted a separate INPAR_TOK at the
1943            // arm start (consumed via `leading_inpar_consumed`
1944            // above), the OUTPAR_TOK we just consumed closed the
1945            // alternation GROUP. If the next token is ALSO
1946            // OUTPAR_TOK, the user wrote `(pat))` and that second
1947            // `)` is the case-arm closer that still needs to be
1948            // consumed before body parsing. Bug #34 in
1949            // docs/BUGS.md.
1950            if leading_inpar_consumed && tok() == OUTPAR_TOK {
1951                zshlex();
1952            }
1953        } else if tok() == OUTPAR_TOK {
1954            // The lexer absorbed `(pat)` as the pattern but left the
1955            // case-arm closing `)` as a separate OUTPAR_TOK. Consume
1956            // it now so body parsing starts at the body, not at `)`.
1957            set_incmdpos(true);
1958            zshlex();
1959        } else {
1960            set_incmdpos(true);
1961        }
1962
1963        // Parse body. Pass end_tokens explicitly so the body's
1964        // parser stops at DSEMI/SEMIAMP/SEMIBAR/ESAC without
1965        // tripping parse_program_until's orphan-terminator check
1966        // (line 7131) which only fires when end_tokens is None.
1967        // Without this, a case arm whose body has no trailing
1968        // `;;` before `esac` (last arm — zsh accepts the dangling
1969        // form) produced "parse error near orphan terminator" on
1970        // the closing `esac`. zsh's par_case at parse.c:1318 sets
1971        // up the case-arm reader to recognize the same terminator
1972        // set; the Rust port was passing the implicit-None and
1973        // hitting the top-level orphan check.
1974        let body = parse_program_until(Some(&[DSEMI, SEMIAMP, SEMIBAR, ESAC]), false);
1975
1976        // Get terminator. Set incasepat=1 BEFORE the zshlex
1977        // advance so the next token (the next arm's pattern, like
1978        // `[a-z]`) gets tokenized in pattern context. Without
1979        // this, a `[`-prefixed pattern after the FIRST arm became
1980        // Inbrack instead of String and the pattern-loop bailed
1981        // out with "expected ')' in case pattern".
1982        // c:Src/parse.c:1391 — `incasepat = 1; incmdpos = 0;` BEFORE
1983        // the zshlex that advances past `;;` to the next arm's first
1984        // token. The incmdpos=0 setting is what makes the lexer
1985        // absorb the next arm's `((add-|)fpath)` into a single STRING
1986        // (gettokstr's LX2_INPAR path at lex.c:1080+ runs only when
1987        // incmdpos is FALSE — at incmdpos=1 the lexer emits raw
1988        // INPAR_TOK for the inner `(` and par_case's pattern-read
1989        // loop has no path to recover). Our Rust port previously set
1990        // only incasepat=1 and not incmdpos=0, which forced
1991        // multi-arm patterns like `((add-|)fpath)` to fail with
1992        // "expected ')' in case pattern" — surfaced on
1993        // zinit.zsh:2946.
1994        let terminator = match tok() {
1995            DSEMI => {
1996                set_incasepat(1);
1997                set_incmdpos(false);
1998                zshlex();
1999                CaseTerm::Break
2000            }
2001            SEMIAMP => {
2002                set_incasepat(1);
2003                set_incmdpos(false);
2004                zshlex();
2005                CaseTerm::Continue
2006            }
2007            SEMIBAR => {
2008                set_incasepat(1);
2009                set_incmdpos(false);
2010                zshlex();
2011                CaseTerm::TestNext
2012            }
2013            _ => CaseTerm::Break,
2014        };
2015
2016        if !patterns.is_empty() {
2017            arms.push(CaseArm {
2018                patterns,
2019                body,
2020                terminator,
2021            });
2022        }
2023    }
2024
2025    Some(ZshCommand::Case(ZshCase { word, arms }))
2026}
2027
2028/// Parse if statement
2029/// Parse `if COND; then BODY; [elif COND; then BODY;]* [else BODY;] fi`, plus
2030/// the brace forms `if COND { BODY } [elif COND { BODY }]* [else { BODY }]`.
2031///
2032/// Faithful port of zsh/Src/parse.c:1411 `par_if` — a SINGLE `for(;;)` loop that
2033/// processes the `if` arm and every `elif` arm through the same body code, so the
2034/// C's one `if (tok == SEPER) break;` after a brace body (c:1471-1472) covers
2035/// if- and elif-bodies uniformly. (The previous port split this into a pre-loop
2036/// if-body + a separate elif/else loop, which forced that single break to be
2037/// duplicated per arm and needed a special `fi`-gate; the single loop retires
2038/// all of that.) The C emits WC_IF wordcodes per arm; zshrs builds a ZshIf AST —
2039/// arm 0 -> (cond, then), later arms -> elif, plus an optional else_.
2040fn par_if() -> Option<ZshCommand> {
2041    // tok() == IF on entry (the dispatcher matched `if`); the loop consumes it.
2042    let mut if_cond: Option<Box<ZshProgram>> = None;
2043    let mut if_then: Option<Box<ZshProgram>> = None;
2044    let mut elif: Vec<(ZshProgram, ZshProgram)> = Vec::new();
2045    let mut else_: Option<Box<ZshProgram>> = None;
2046    // `usebrace` mirrors c:1449/1458 — the body form of the LAST arm parsed; it
2047    // gates whether a trailing `else {` is a brace-else (c:1492 `tok == INBRACE
2048    // && usebrace`) or, for a then-form arm, a brace-group statement inside the
2049    // else body that still ends at `fi` (p10k.zsh:5575).
2050    let mut usebrace = false;
2051    // Mirrors C's `xtok` where the loop exits, for the post-loop else check
2052    // (c:1487 `if (xtok == ELSE || tok == ELSE)`).
2053    let mut xtok_at_exit = IF;
2054
2055    loop {
2056        let xtok = tok(); // c:1420
2057                          // c:1422-1425 — `fi` at the top closes a then-form if/elif chain.
2058        if xtok == FI {
2059            set_incmdpos(false);
2060            zshlex();
2061            xtok_at_exit = FI;
2062            break;
2063        }
2064        zshlex(); // c:1426 — consume IF / ELIF / ELSE
2065                  // c:1428-1429 — `else` ends the arm loop (handled after it).
2066        if xtok == ELSE {
2067            xtok_at_exit = ELSE;
2068            break;
2069        }
2070        skip_separators(); // c:1430
2071                           // c:1432-1435 — only IF / ELIF may open an arm.
2072        if xtok != IF && xtok != ELIF {
2073            zerr("parse error near `if'");
2074            return None;
2075        }
2076        // c:1438 — the cond is an ordinary list; a `{ … }` body opener ends it
2077        // via the no-separator chaining rule, so only THEN is a hard stop.
2078        // fast-syntax-highlighting.plugin.zsh:365-375:
2079        //   if [[ … ]] { … } elif { type curl … } { … }
2080        let cond = parse_program_until(Some(&[THEN]), false);
2081        set_incmdpos(true); // c:1438
2082                            // c:1439-1442 — ENDINPUT right after the cond is an unterminated if.
2083        if tok() == ENDINPUT {
2084            zerr("parse error: unterminated if");
2085            return None;
2086        }
2087        skip_separators(); // c:1444
2088        xtok_at_exit = FI; // c:1446 — `xtok = FI` sentinel for the else check
2089
2090        let mut this_arm_brace = false;
2091        let body = if tok() == THEN {
2092            // c:1448-1456 — classic `then … (fi|elif|else)`.
2093            zshlex(); // consume then
2094            let b = parse_program_until(Some(&[ELSE, ELIF, FI]), false);
2095            set_incmdpos(true);
2096            b
2097        } else if tok() == INBRACE_TOK {
2098            // c:1457-1473 — brace body `{ … }`.
2099            this_arm_brace = true;
2100            zshlex(); // consume {
2101            let b = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2102            if tok() != OUTBRACE_TOK {
2103                zerr("parse error: expected `}'");
2104                return None;
2105            }
2106            zshlex(); // c:1469 — consume }
2107            set_incmdpos(true); // c:1470
2108            b
2109        } else {
2110            // c:1474-1476 — `unset(SHORTLOOPS)` is the default for `if`: a body
2111            // that is neither `then …` nor `{ … }` is a parse error.
2112            zerr("expected `then' or `{' after if condition");
2113            return None;
2114        };
2115        usebrace = this_arm_brace;
2116
2117        // Record the arm: the first is the `if`, the rest are `elif`.
2118        if if_cond.is_none() {
2119            if_cond = Some(Box::new(cond));
2120            if_then = Some(Box::new(body));
2121        } else {
2122            elif.push((cond, body));
2123        }
2124
2125        // c:1471-1472 — after a brace body, a following separator ends the
2126        // construct; break WITHOUT consuming it so the outer par_list keeps the
2127        // separator between the if and the next command (otherwise that command
2128        // becomes a "STRING after compound" parse error). A then-form body
2129        // instead falls through to the loop top to read the next ELSE/ELIF/FI;
2130        // a brace body with no separator also falls through (e.g. `} elif`,
2131        // `} else` on the same line). Because a brace arm never reaches the
2132        // loop-top `fi`-consume, it cannot steal an enclosing then-form's `fi`.
2133        if this_arm_brace && matches!(tok(), SEPER | NEWLIN | SEMI | ENDINPUT) {
2134            break;
2135        }
2136    }
2137
2138    // c:1487-1505 — the optional else clause. When the loop exited via
2139    // `xtok == ELSE`, the `else` keyword was already consumed by the loop's
2140    // zshlex; the inner guard re-consumes only if we are somehow still on it.
2141    if xtok_at_exit == ELSE || tok() == ELSE {
2142        if tok() == ELSE {
2143            zshlex();
2144        }
2145        skip_separators(); // c:1490
2146        let ebody = if usebrace && tok() == INBRACE_TOK {
2147            // c:1492-1497 — brace else (only when the preceding arm was brace).
2148            zshlex(); // consume {
2149            let b = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2150            if tok() != OUTBRACE_TOK {
2151                zerr("parse error: expected `}'");
2152                return None;
2153            }
2154            zshlex(); // consume }
2155            b
2156        } else {
2157            // c:1498-1502 — then-form else, stops at and consumes `fi`.
2158            let b = parse_program_until(Some(&[FI]), false);
2159            if tok() != FI {
2160                zerr("parse error: unterminated if");
2161                return None;
2162            }
2163            zshlex(); // consume fi
2164            b
2165        };
2166        set_incmdpos(false); // c:1502 — incmdpos = 0 after the else body
2167        else_ = Some(Box::new(ebody));
2168    }
2169
2170    let cond = match if_cond {
2171        Some(c) => c,
2172        None => {
2173            zerr("parse error: empty if");
2174            return None;
2175        }
2176    };
2177    let then = if_then.expect("if_then set with if_cond");
2178    Some(ZshCommand::If(ZshIf {
2179        cond,
2180        then,
2181        elif,
2182        else_,
2183    }))
2184}
2185
2186/// Parse while/until loop
2187/// Parse `while COND; do BODY; done` and `until COND; do BODY; done`.
2188/// Direct port of zsh/Src/parse.c:1521 `par_while`. The
2189/// `until` variant is the same loop with the condition negated.
2190fn par_while(until: bool) -> Option<ZshCommand> {
2191    zshlex(); // skip while/until
2192
2193    // c:1521-1551 par_while — the condition's parser must stop at
2194    // `do` or `{`. Without an explicit end-token set, parse_program
2195    // consumes the brace-form body as additional condition lists,
2196    // leaving parse_loop_body with nothing — `while (( i++ < 3 )) {
2197    // echo $i }` silently parsed but executed nothing.
2198    // c:1528 — `par_save_list(cmplx);` — the cond is an ORDINARY
2199    // list: a leading `{...}` block is the cond's first command
2200    // (`while {false} {body}`), and the body INBRACE is reachable
2201    // because a list followed by `{` without a separator ends (the
2202    // chaining rule in parse_program_until). DOLOOP stays in the
2203    // stop set because `do` at command position can't start a
2204    // command. INBRACE_TOK was previously (wrongly) in the set too,
2205    // which made a brace-form COND parse as an empty cond + body.
2206    let cond = Box::new(parse_program_until(Some(&[DOLOOP]), false));
2207
2208    skip_separators();
2209    let body = parse_loop_body(false, false)?;
2210
2211    // c:Src/parse.c:1521-1551 par_while — WC_WHILE wordcode is tagged
2212    // with WC_WHILE_TYPE differentiating WHILE vs UNTIL at the wordcode
2213    // layer. The AST mirror in zsh_ast.rs has separate Until(ZshWhile)
2214    // and While(ZshWhile) variants; route by the `until` flag here so
2215    // downstream pattern-matchers can distinguish without poking
2216    // inside the payload's bool.
2217    let w = ZshWhile {
2218        cond,
2219        body: Box::new(body),
2220        until,
2221    };
2222    Some(if until {
2223        ZshCommand::Until(w) // c:1521 (WC_WHILE_TYPE = WC_WHILE_UNTIL)
2224    } else {
2225        ZshCommand::While(w) // c:1521 (WC_WHILE_TYPE = WC_WHILE_WHILE)
2226    })
2227}
2228
2229/// Parse repeat loop
2230/// Parse `repeat N; do BODY; done`. Direct port of
2231/// zsh/Src/parse.c:1565 `par_repeat`. The C source supports
2232/// the SHORTLOOPS short-form `repeat N CMD` (no do/done) — zshrs's
2233/// parser doesn't yet special-case that variant.
2234fn par_repeat() -> Option<ZshCommand> {
2235    // c:1572 — `incmdpos = 0;` BEFORE lexing the count word, so
2236    // checkalias's `(incmdpos && tok == STRING)` gate stays closed and
2237    // the count is NEVER alias-expanded. OMZL::directories.zsh defines
2238    // `alias 1='cd -1'`; without this, `repeat 1; do …; done` (hsmw
2239    // file:114) expanded `1` mid-header and errored "parse error near
2240    // `do'" (#665, sibling of the funcdef-name bug #662).
2241    set_incmdpos(false);
2242    zshlex(); // skip 'repeat'
2243
2244    let count = match tok() {
2245        STRING_LEX => {
2246            let c = tokstr().unwrap_or_default();
2247            // c:1577-1578 — `incmdpos = 1; zshlex();` — restore command
2248            // position before the separator/`do` lex so the body's
2249            // reserved words promote again.
2250            set_incmdpos(true);
2251            zshlex();
2252            c
2253        }
2254        _ => {
2255            zerr("expected count after repeat");
2256            return None;
2257        }
2258    };
2259
2260    skip_separators();
2261    // c:1600 — par_repeat's short-form gate is wider: it unlocks
2262    // when SHORTLOOPS OR SHORTREPEAT is set (vs SHORTLOOPS alone for
2263    // for/while). Pass `is_repeat=true` so parse_loop_body
2264    // applies that widened gate.
2265    let body = parse_loop_body(false, true)?;
2266
2267    Some(ZshCommand::Repeat(ZshRepeat {
2268        count,
2269        body: Box::new(body),
2270    }))
2271}
2272
2273/// Parse (...) subshell
2274/// Parse a subshell `( ... )`. Direct port of zsh/Src/parse.c:1619
2275/// `par_subsh`. Body parses as a normal list; the subshell wrapper
2276/// fork-isolates execution in the executor.
2277fn par_subsh() -> Option<ZshCommand> {
2278    zshlex(); // skip (
2279              // c:Src/parse.c:par_subsh — `parse_event(OUTPAR)` parses until
2280              // the matching `)`. zshrs's previous port called bare
2281              // `parse_program()` (parse_program_until(None, false)) which has no
2282              // way to know it should stop at OUTPAR_TOK — at top-level
2283              // that's fine (the outer loop just sees an extra OUTPAR after
2284              // the inner body), but the parse_event-equivalent's new
2285              // yyerror-on-unconsumed-token behavior at parse_program_until's
2286              // None arm now reports a spurious "parse error near `)'" when
2287              // the construct ends. Pass OUTPAR_TOK so parse_program_until
2288              // stops cleanly at the closing paren.
2289    let prog = parse_program_until(Some(&[OUTPAR_TOK]), false);
2290    // c:1630-1631 — `if (tok != ((otok == INPAR) ? OUTPAR : OUTBRACE))
2291    // YYERRORV(oecused);`. This arm is only reached from par_cmd's
2292    // INPAR_TOK case (c:1010-1014), so the expected closer is OUTPAR.
2293    // Silently accepting a missing `)` let an unterminated subshell parse
2294    // clean: `zsh -fc '('` is a parse error with status 1, while zshrs
2295    // returned 0 and ran nothing. `((` inherits the same path (the lexer
2296    // emits two INPARs), which is why `((1` reached the command layer and
2297    // reported `command not found: 1` instead of a parse error.
2298    if tok() != OUTPAR_TOK {
2299        yyerror(0); // c:1631 YYERRORV — sets ERRFLAG_ERROR (c:2751)
2300        return None;
2301    }
2302    zshlex(); // c:1633
2303    Some(ZshCommand::Subsh(Box::new(prog)))
2304}
2305
2306/// Parse function definition
2307/// Parse `function NAME { BODY }` or `NAME () { BODY }`. Direct
2308/// port of zsh/Src/parse.c:1672 `par_funcdef`. zsh handles
2309/// the multiple keyword shapes (function FOO, FOO (), function FOO ()),
2310/// the optional `[fname1 fname2 ...]` for multi-name function defs,
2311/// and the `function FOO () { ... }` traditional/POSIX hybrid form.
2312fn par_funcdef() -> Option<ZshCommand> {
2313    // c:1680-1681 — `nocorrect = 1; incmdpos = 0;` BEFORE lexing the
2314    // name. incmdpos=0 keeps checkalias's `(incmdpos && tok == STRING)`
2315    // gate closed, so the word after `function` is NEVER alias-expanded
2316    // (`alias ts='cd x'; function ts { }` must define `ts`, not `cd`).
2317    // nocorrect=1 likewise suppresses spell-correction on the name.
2318    set_nocorrect(1);
2319    set_incmdpos(false);
2320    zshlex(); // skip 'function'
2321
2322    let mut names = Vec::new();
2323    let mut tracing = false;
2324
2325    // Handle options like -T and function names. Two subtleties:
2326    //
2327    //   1. Flags: zsh's lexer encodes a leading `-` as
2328    //      `zsh_h::Dash` (`\u{9b}`, `Src/zsh.h:182`) inside the String tokstr.
2329    //      The previous `s.starts_with('-')` check failed for
2330    //      `\u{9b}T`, so `function -T NAME { body }` slipped the
2331    //      `-T` token into `names` and the function got registered
2332    //      as `T` plus the intended `NAME`.
2333    //
2334    //   2. Body opener: zsh's lexer emits the opening `{` as a
2335    //      String (not INBRACE_TOK) when it follows the String
2336    //      NAME — the preceding name token resets incmdpos to
2337    //      false, and only `{` immediately followed by `}` (the
2338    //      empty-body case) gets promoted to Inbrace. The funcdef
2339    //      parser must recognise the bare-`{` String as the body
2340    //      opener; otherwise `function NAME { body }` falls through
2341    //      to `_ => break`, no body parses, and the FuncDef never
2342    //      lands in the AST. This is consistent with C zsh's
2343    //      par_funcdef which knows it's in funcdef-header context
2344    //      and accepts the brace either way.
2345    loop {
2346        match tok() {
2347            STRING_LEX => {
2348                let _ts_s = tokstr()?;
2349                let s = _ts_s.as_str();
2350                // c:1702 — `if ((*tokstr == Inbrace || *tokstr == '{') && !tokstr[1])`.
2351                // Body opener can be either the literal `{` (early-return
2352                // path at lex.c:1141-1144 / lex.rs LX2_INBRACE cmdpos
2353                // branch) or the Inbrace marker `\u{8f}` (lex.c:1420
2354                // post-switch add(c) where c was rewritten via lextok2).
2355                if s == "{" || s == "\u{8f}" {
2356                    break;
2357                }
2358                // c:Src/parse.c par_funcdef — `if (tokstr[0] == Dash &&
2359                // tokstr[1] == 'T' && !tokstr[2])`: ONLY the exact word
2360                // `-T` (Dash + 'T' + nothing else) is the tracing option.
2361                // Every other word — INCLUDING names that merely start
2362                // with `-` or `+` such as `-zui_std_button_ext` or `+foo`
2363                // — is a function NAME. The previous check skipped any
2364                // `-`/`+`-prefixed word as an option, so all 47 of ZUI's
2365                // `function -zui_std_*()` names were dropped, leaving an
2366                // anonymous `function { body }` that executed its body at
2367                // parse time (the cascade of "command not found: -zui_std_*"
2368                // and "bad substitution" while sourcing stdlib.lzui).
2369                let sc: Vec<char> = s.chars().collect();
2370                let is_trace_opt =
2371                    sc.len() == 2 && (sc[0] == '-' || sc[0] == Dash) && sc[1] == 'T';
2372                if is_trace_opt {
2373                    tracing = true;
2374                    zshlex();
2375                    continue;
2376                }
2377                // c:Src/exec.c::execcmd_args — function name tokens
2378                // in `function NAME { ... }` form go through globbing
2379                // at parse time. zsh's `function with[bracket] { ... }`
2380                // triggers a glob expansion of `with[bracket]`; no file
2381                // matches → "no matches found: NAME" + rc=1 (when
2382                // NOMATCH is set, the default). Bug #536: zshrs accepted
2383                // the literal bracket-containing name and registered
2384                // the function silently. Mirror C by probing for glob
2385                // metachars on the name; if present AND no file
2386                // matches, emit the diagnostic and abort the parse.
2387                let has_glob_chars = s.chars().any(|c| {
2388                    matches!(
2389                        c,
2390                        '[' | ']'
2391                            | '*'
2392                            | '?'
2393                            | crate::ported::zsh_h::Inbrack
2394                            | crate::ported::zsh_h::Outbrack
2395                            | crate::ported::zsh_h::Star
2396                            | crate::ported::zsh_h::Quest
2397                    )
2398                });
2399                if has_glob_chars && crate::ported::zsh_h::isset(crate::ported::zsh_h::NOMATCH) {
2400                    // Probe the funcname pattern through the canonical
2401                    // glob entry — C `zglob(args, firstnode(args), 0)`
2402                    // (c:3318). zglob runs the tokenized word and, under
2403                    // NOMATCH with no match (nullglob off), emits
2404                    // "no matches found: PAT" + errflag itself
2405                    // (c:1876-1880). Tokenize first (the funcname word
2406                    // still holds literal `*`/`?`; haswilds keys on the
2407                    // Star/Quest tokens) and propagate the abort.
2408                    let mut probe = vec![s.to_string()];
2409                    crate::ported::glob::tokenize(&mut probe[0]);
2410                    crate::ported::glob::zglob(&mut probe, 0, 0);
2411                    if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed)
2412                        & crate::ported::utils::ERRFLAG_ERROR
2413                        != 0
2414                    {
2415                        return None;
2416                    }
2417                }
2418                names.push(s.to_string());
2419                zshlex();
2420            }
2421            INBRACE_TOK | INOUTPAR | SEPER | NEWLIN => break,
2422            _ => break,
2423        }
2424    }
2425
2426    // c:1715-1716 — `nocorrect = 0; incmdpos = 1;` after the name
2427    // wordlist, BEFORE lexing past `()`/separators into the body, so
2428    // body keywords (`if`, `while`, …) get reserved-word promotion
2429    // again (the header ran with incmdpos=0 to keep names alias-free).
2430    set_nocorrect(0);
2431    set_incmdpos(true);
2432
2433    // Optional ()
2434    let saw_paren = tok() == INOUTPAR;
2435    // Track the byte position just past `()` / the most-recent separator, so an
2436    // UNBRACED short body (`function f () cmd`) can be sliced for `functions`
2437    // rendering. pos() AFTER a zshlex is already past the next token (one-token
2438    // lookahead), so record it BEFORE each advance (mirrors parse.rs:2484).
2439    let mut unbraced_body_start = pos();
2440    if saw_paren {
2441        zshlex();
2442    }
2443
2444    while tok() == SEPER || tok() == NEWLIN {
2445        unbraced_body_start = pos();
2446        zshlex();
2447    }
2448
2449    // Body opener: real Inbrace OR a String containing the literal `{`
2450    // (early-return path) OR a String containing the Inbrace marker
2451    // `\u{8f}` (bct++ path post-switch add). C parse.c:1702 handles
2452    // both string forms via `*tokstr == Inbrace || *tokstr == '{'`.
2453    let body_opener_is_string_brace =
2454        tok() == STRING_LEX && tokstr().map(|s| s == "{" || s == "\u{8f}").unwrap_or(false);
2455    if tok() == INBRACE_TOK || body_opener_is_string_brace {
2456        // Capture body_start BEFORE the lexer advances past the
2457        // first body token. After the previous zshlex consumed
2458        // `{`, lexer.pos points just past `{` (which is where the
2459        // body source starts). The next `zshlex()` would advance
2460        // past the first token (`echo`), making body_start land
2461        // mid-body and lose the first word — `typeset -f f` would
2462        // print `a; echo b` for `{ echo a; echo b }`.
2463        // c:Src/parse.c:1690-1706 — par_funcdef requires a clean
2464        //   body-opener brace when the anonymous form `function {body}`
2465        //   is used (no names AND no `()`). zsh's lexer keeps the `{`
2466        //   as its own STRING token via the lex.c:1141-1144 early-
2467        //   return at command position, but the body brace must be
2468        //   followed by whitespace for the inner par_list to find a
2469        //   matching OUTBRACE — without a separator, the closing `}`
2470        //   gets merged into the last word (`X}`) and par_list ends
2471        //   without OUTBRACE, which C zsh reports as `parse error near
2472        //   \`}'`. zshrs's lexer has the same `bct` semantics; reject
2473        //   here at the parse step so the funcdef doesn't silently run
2474        //   with the stray `}` attached. With names or `()` present,
2475        //   the body brace is allowed even without a separator
2476        //   (`function name {body}` and `function () {body}` both work
2477        //   in zsh). Bug #60 in docs/BUGS.md.
2478        if names.is_empty() && !saw_paren {
2479            // Peek the next source byte after the current lexer position
2480            // (`{` was just tokenized — `pos()` points just past it).
2481            // A whitespace separator means proper `function { body }`
2482            // form; anything else is the malformed `function {body}`
2483            // shape zsh rejects.
2484            let next_byte = input_slice(pos(), pos() + 1)
2485                .and_then(|s| s.bytes().next())
2486                .unwrap_or(b' ');
2487            if !matches!(next_byte, b' ' | b'\t' | b'\n' | b';') {
2488                zerr("parse error near `}'"); // c:Src/parse.c YYERRORV
2489                return None;
2490            }
2491        }
2492        let body_start = pos();
2493        zshlex();
2494        // c:Src/parse.c — func body terminates at OUTBRACE_TOK.
2495        // Explicit end-token keeps the inner parse from hitting the
2496        // top-level stray-`}` arm (#168). Bug #167 family.
2497        let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2498        // c:Src/parse.c:1733-1737 — `if (tok != OUTBRACE) { cmdpop();
2499        // ... YYERRORV(oecused); }`. Hard-error on missing close brace
2500        // so `function f { echo hi` doesn't silently register a half-
2501        // parsed body. Bug #405.
2502        if tok() != OUTBRACE_TOK {
2503            zerr("parse error: expected `}'");
2504            return None;
2505        }
2506        // See the matching `NAME ()` arm below and Bug #642: slice
2507        // through pos() so the funcdef `}` is always inside the slice
2508        // (at EOF `pos()-1` excluded it), then strip that single trailing
2509        // brace — so a body ending in a balanced `} always { ... }` keeps
2510        // its close on the `.zwc` round-trip.
2511        let body_end = pos();
2512        let body_source = input_slice(body_start, body_end)
2513            .map(|s| {
2514                let t = s.trim();
2515                // Strip the statement SEPARATOR that followed the funcdef
2516                // `}` FIRST — for `name() { body }; next` the slice ends in
2517                // `};`, so stripping the brace before the `;` left the `}`
2518                // stranded (`${functions[name]}` → `body }`, then re-parsed
2519                // to a `parse error near '}'`). Order: trailing separators →
2520                // closing brace → the body's own last-statement separator.
2521                let t = t.trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace());
2522                let t = t.strip_suffix('}').unwrap_or(t).trim_end();
2523                let t = t
2524                    .trim_end_matches(|c: char| c == ';' || c == '\n')
2525                    .trim_end();
2526                t.to_string()
2527            })
2528            .filter(|s| !s.is_empty());
2529        zshlex();
2530
2531        // Anonymous form `function () { body } a b c` (with `()`) or
2532        // `function { body } a b c` (zsh-only shorthand, no `()`). No
2533        // name was collected. Mirror parse_anon_funcdef: synthesize
2534        // `_zshrs_anon_N`, collect trailing args, set auto_call_args
2535        // so compile_funcdef registers + immediately calls the
2536        // function with the args as positional params.
2537        if names.is_empty() {
2538            let mut args = Vec::new();
2539            while tok() == STRING_LEX {
2540                if let Some(s) = tokstr() {
2541                    args.push(s);
2542                }
2543                zshlex();
2544            }
2545            static ANON_COUNTER: AtomicUsize = AtomicUsize::new(0);
2546            let n = ANON_COUNTER.fetch_add(1, Ordering::Relaxed);
2547            let name = format!("_zshrs_anon_kw_{}", n);
2548            return Some(ZshCommand::FuncDef(ZshFuncDef {
2549                names: vec![name],
2550                body: Box::new(body),
2551                tracing,
2552                auto_call_args: Some(args),
2553                body_source,
2554            }));
2555        }
2556
2557        Some(ZshCommand::FuncDef(ZshFuncDef {
2558            names,
2559            body: Box::new(body),
2560            tracing,
2561            auto_call_args: None,
2562            body_source,
2563        }))
2564    } else if unset(SHORTLOOPS) {
2565        // c:Src/parse.c:1742 — `else if (unset(SHORTLOOPS)) YYERRORV`.
2566        // Braceless short body (`function f () CMD`) requires SHORTLOOPS.
2567        zerr("parse error: short function body form requires SHORTLOOPS option");
2568        None
2569    } else {
2570        // c:Src/parse.c:1747-1748 — `else par_list1(&c)`. The short body
2571        // is ONE sublist (a single command), NOT a full list. The prior
2572        // `par_list(false)` greedily consumed past the `;` (so `function
2573        // foo () print bar; foo` swallowed the trailing `foo` call and
2574        // errored "parse error near `foo'"). Use par_cmd for the single
2575        // command, matching the brace-less `foo() CMD` path
2576        // (parse_inline_funcdef). Bug surface: C04funcdef `function foo ()
2577        // print bar`.
2578        // Slice the raw short-body text (unbraced_body_start was captured
2579        // before the body token was lexed, above) so `functions`/`typeset -f`
2580        // can list it (fusevm shfuncs render from this raw `body`, not
2581        // getpermtext — hashtable.rs:1397). Without it `function f () print x`
2582        // listed as `f () { }` (empty).
2583        par_cmd().map(|cmd| {
2584            let body_source = input_slice(unbraced_body_start, pos())
2585                .map(|s| {
2586                    s.trim()
2587                        .trim_end_matches(|c: char| {
2588                            c == ';' || c == '\n' || c.is_whitespace()
2589                        })
2590                        .to_string()
2591                })
2592                .filter(|s| !s.is_empty());
2593            let list = ZshList {
2594                sublist: ZshSublist {
2595                    pipe: ZshPipe {
2596                        cmd,
2597                        next: None,
2598                        lineno: lineno(),
2599                        merge_stderr: false,
2600                    },
2601                    next: None,
2602                    flags: SublistFlags::default(),
2603                },
2604                flags: ListFlags::default(),
2605            };
2606            ZshCommand::FuncDef(ZshFuncDef {
2607                names,
2608                body: Box::new(ZshProgram { lists: vec![list] }),
2609                tracing,
2610                auto_call_args: None,
2611                body_source,
2612            })
2613        })
2614    }
2615}
2616
2617/// Parse time command
2618/// Parse `time CMD` (POSIX time keyword). Direct port of
2619/// zsh/Src/parse.c:1787 `par_time`. The `time` keyword
2620/// times the execution of the following pipeline / cmd.
2621fn par_time() -> Option<ZshCommand> {
2622    zshlex(); // skip 'time'
2623
2624    // Check if there's a pipeline to time
2625    if tok() == SEPER || tok() == NEWLIN || tok() == ENDINPUT {
2626        Some(ZshCommand::Time(None))
2627    } else {
2628        let sublist = par_sublist();
2629        Some(ZshCommand::Time(sublist.map(Box::new)))
2630    }
2631}
2632
2633/// Port of `par_dinbrack(void)` from `Src/parse.c:1810`. Body
2634/// parser inside `[[ ... ]]` — calls `par_cond` to emit the
2635/// condition wordcode then advances past `]]`.
2636pub fn par_dinbrack() -> Option<()> {
2637    // c:1810
2638    set_incond(1); // c:1814
2639    set_incmdpos(false); // c:1815
2640    zshlex(); // c:1816
2641    let _ = par_cond(); // c:1817
2642    if tok() != DOUTBRACK {
2643        // c:1818
2644        crate::ported::utils::zerr("missing ]]");
2645        yyerror(0);
2646        return None;
2647    }
2648    set_incond(0); // c:1820
2649    set_incmdpos(true); // c:1821
2650    zshlex(); // c:1822
2651    Some(())
2652}
2653
2654/// Parse a simple command
2655/// Parse a simple command (assignments + words + redirections).
2656/// Direct port of zsh/Src/parse.c:1836 `par_simple` —
2657/// the largest single function in parse.c. Handles ENVSTRING/
2658/// ENVARRAY assignments at command head, intermixed redirs,
2659/// typeset-style multi-assignment commands, and the trailing
2660/// inout-par `()` that converts a simple command into an inline
2661/// function definition.
2662fn par_simple(mut redirs: Vec<ZshRedir>) -> Option<ZshCommand> {
2663    let mut assigns = Vec::new();
2664    let mut words = Vec::new();
2665
2666    // c:1934-1974 — `{var}>file` brace-FD detection is wired
2667    // INSIDE the words loop below (parse.rs:4940-4956) rather than
2668    // here at the head. The words-loop site sees the tok=STRING
2669    // `{varname}` followed by a REDIROP and routes into par_redir
2670    // with redir.varid populated. C does it inline at the start of
2671    // each STRING/TYPESET arm iteration; functionally equivalent.
2672
2673    // c:1843-1846 — leading-NOCORRECT prefix: `nocorrect echo hello`
2674    // emits a NOCORRECT token at the start of par_simple. C sets
2675    // `nocorrect = 1` and skips past via the `zshlex();` at the
2676    // for-loop tail (c:1907). zshrs's par_simple (AST) had no
2677    // NOCORRECT arm so the token was silently dropped and the
2678    // following command line evaporated — `nocorrect echo hello`
2679    // produced empty output.
2680    while tok() == NOCORRECT {
2681        set_nocorrect(1); // c:1846
2682        zshlex(); // c:1907 (loop-tail zshlex)
2683    }
2684
2685    // Parse leading assignments
2686    while tok() == ENVSTRING || tok() == ENVARRAY {
2687        if let Some(assign) = parse_assign() {
2688            assigns.push(assign);
2689        }
2690        zshlex();
2691    }
2692
2693    // Parse words and redirections
2694    loop {
2695        match tok() {
2696            ENVSTRING | ENVARRAY if words.is_empty() => {
2697                // c:1843-1907 — still in command position (no command
2698                // word collected yet): a `name=val` token here is a
2699                // PRE-COMMAND assignment, even when it follows a
2700                // redirection (`a=b 2>/dev/null c=d`). C's single
2701                // par_simple loop collects assignments and redirs in any
2702                // order until the command word; zshrs split that into a
2703                // leading-assignments while-loop (above) plus this main
2704                // loop, and the leading loop stopped at the first redir.
2705                // Without this arm the trailing `c=d` fell into the
2706                // typeset-word path below and was silently dropped, so
2707                // `a=b 2>/dev/null c=d` set neither parameter.
2708                if let Some(assign) = parse_assign() {
2709                    assigns.push(assign);
2710                }
2711                zshlex();
2712            }
2713            ENVSTRING | ENVARRAY => {
2714                // Mid-command assignment-shape arg under typeset
2715                // / declare / local / etc. (intypeset gates the
2716                // lexer to emit Envstring/Envarray for `name=val`
2717                // and `name=()` past the command name). Parse the
2718                // assignment, then emit a synthetic word
2719                // `NAME=value` (scalar) or `NAME=( … )` (array)
2720                // string so typeset's builtin arg list sees the
2721                // assignment-shape arg. Avoids the inline-env
2722                // scope path that mistakenly treats it like a
2723                // pre-cmd `X=Y cmd` assignment.
2724                if let Some(assign) = parse_assign() {
2725                    let synthetic = match &assign.value {
2726                        ZshAssignValue::Scalar(v) => format!("{}={}", assign.name, v),
2727                        ZshAssignValue::Array(elems) => {
2728                            // c:Src/builtin.c — assoc paren-init `h=( "" v
2729                            //   k2 v2 )` must preserve empty-string
2730                            //   elements (zsh stores key="" + value="v").
2731                            //   The bin_typeset paren-init splitter at
2732                            //   `builtin.rs:4358` recognizes the
2733                            //   REJOIN_SEP (`\u{1f}`) sentinel between
2734                            //   array elements and skips the leading/
2735                            //   trailing parens trim; using it here
2736                            //   round-trips empties end-to-end through
2737                            //   the synthetic-arg rebuild. Space-join
2738                            //   collapses adjacent empties (`(` + `""` +
2739                            //   `empty-val` becomes `( empty-val`) so
2740                            //   bin_typeset never sees the empty key.
2741                            //   Bug #93 in docs/BUGS.md.
2742                            let mut buf = String::with_capacity(
2743                                assign.name.len()
2744                                    + 4
2745                                    + elems.iter().map(|e| e.len() + 1).sum::<usize>(),
2746                            );
2747                            buf.push_str(&assign.name);
2748                            buf.push_str("=(");
2749                            for elem in elems {
2750                                buf.push('\u{1f}');
2751                                buf.push_str(elem);
2752                            }
2753                            buf.push('\u{1f}');
2754                            buf.push(')');
2755                            buf
2756                        }
2757                    };
2758                    words.push(synthetic);
2759                }
2760                zshlex();
2761            }
2762            STRING_LEX | TYPESET => {
2763                let s = tokstr();
2764                if let Some(s) = s {
2765                    words.push(s);
2766                }
2767                // c:1929 — `incmdpos = 0;` so the next zshlex() does
2768                // not re-promote `{`/`[[`/reserved words at the
2769                // continuation position. Without this, `echo {a,b}`
2770                // re-lexes `{` as INBRACE_TOK (current-shell block)
2771                // and the brace expansion never reaches par_simple.
2772                set_incmdpos(false);
2773                // c:1931-1932 — `if (tok == TYPESET) intypeset = is_typeset = 1;`
2774                // Multi-assign `typeset a=1 b=2` relies on the lexer
2775                // re-emitting `b=2` as ENVSTRING; that path is gated
2776                // on `intypeset`. Without this, follow-on assignment
2777                // words arrive as STRING and the typeset builtin's
2778                // multi-assign form silently degrades.
2779                if tok() == TYPESET {
2780                    set_intypeset(true);
2781                }
2782                zshlex();
2783                // Check for function definition foo() { ... }
2784                if words.len() == 1 && tok() == INOUTPAR {
2785                    return parse_inline_funcdef(std::mem::take(&mut words));
2786                }
2787                // `{name}>file` named-fd redirect: the lexer doesn't
2788                // recognize this shape, so the bare word `{name}`
2789                // arrives as a String. If it matches `{IDENT}` and
2790                // the NEXT token is a redirop, pop it off as the
2791                // varid for that redir.
2792                if !words.is_empty() && IS_REDIROP(tok()) {
2793                    let last = words.last().unwrap();
2794                    let untoked = super::lex::untokenize(last);
2795                    if untoked.starts_with('{') && untoked.ends_with('}') && untoked.len() > 2 {
2796                        let name = &untoked[1..untoked.len() - 1];
2797                        // c:1944 — `if (itype_end(tokstr+1, IIDENT, 0) >= ptr)`:
2798                        // every char between the braces must be an IIDENT char.
2799                        // IIDENT (utils.c:4173/4190) covers digits, letters and
2800                        // `_` — with NO first-char restriction, so an all-digit
2801                        // name like `{1}` (a positional param, used by p10k's
2802                        // `exec {1}>&-`) is a valid fd var. The prior check also
2803                        // required the first char to be `_`/alphabetic, which
2804                        // rejected `{1}`/`{2}`/`{12}` — zsh accepts them.
2805                        if !name.is_empty()
2806                            && name
2807                                .bytes()
2808                                .all(crate::ported::ztype_h::iident)
2809                        {
2810                            let varid = name.to_string();
2811                            words.pop();
2812                            if let Some(mut redir) = par_redir() {
2813                                redir.varid = Some(varid);
2814                                redirs.push(redir);
2815                            }
2816                            continue;
2817                        }
2818                    }
2819                }
2820            }
2821            _ if IS_REDIROP(tok()) => {
2822                match par_redir() {
2823                    Some(redir) => redirs.push(redir),
2824                    None => break, // Error in redir parsing, stop
2825                }
2826            }
2827            INOUTPAR if !words.is_empty() => {
2828                // c:2055-2057 — `if (!isset(MULTIFUNCDEF) && argc > 1)
2829                // YYERROR(oecused);` — multi-name funcdef gate:
2830                // `f1 f2() { ... }` defines f1 AND f2 to the same
2831                // body, but only when MULTIFUNCDEF is set.
2832                if !isset(MULTIFUNCDEF) && words.len() > 1 {
2833                    zerr("parse error: multiple names in function definition without MULTIFUNCDEF");
2834                    return None;
2835                }
2836                // c:2061-2068 — `if (isset(EXECOPT) && hasalias &&
2837                // !isset(ALIASFUNCDEF) && argc && hasalias !=
2838                // input_hasalias()) { zwarn(...); YYERROR(...); }`
2839                // Alias-as-funcdef warning. zshrs's parser doesn't
2840                // track `hasalias` (alias-expansion provenance
2841                // during parse) yet, so `had_alias` stays false —
2842                // the gate is wired here as a marker so the canonical
2843                // C predicate is visible. Once alias-provenance lands,
2844                // swap `false` for the actual provenance compare.
2845                let had_alias = false;
2846                if isset(EXECOPT) && had_alias && !isset(ALIASFUNCDEF) && !words.is_empty() {
2847                    crate::ported::utils::zwarn("defining function based on alias `(unknown)'");
2848                    return None;
2849                }
2850                // foo() { ... } / multi-name `f1 f2 f3() { ... }` style
2851                // function. c:Src/parse.c:2055-2068 — under MULTIFUNCDEF
2852                // every preceding word names the SAME body; pass them all
2853                // so each is registered (the gate above already rejected
2854                // multi-name without MULTIFUNCDEF). Previously only the
2855                // last word (`words.pop()`) was defined, so `f1 f2 f3()
2856                // {...}` left f1/f2 as "command not found".
2857                return parse_inline_funcdef(std::mem::take(&mut words));
2858            }
2859            _ => break,
2860        }
2861    }
2862
2863    if assigns.is_empty() && words.is_empty() && redirs.is_empty() {
2864        return None;
2865    }
2866
2867    Some(ZshCommand::Simple(ZshSimple {
2868        assigns,
2869        words,
2870        redirs,
2871    }))
2872}
2873
2874/// Parse a redirection
2875/// Parse a redirection (>file, <file, >>file, <<HEREDOC, etc.).
2876/// Direct port of zsh/Src/parse.c:2229 `par_redir`. Returns
2877/// a ZshRedir node carrying the operator type, fd, target word
2878/// (or here-doc body / pipe-redir command), and any `{var}` style
2879/// fd-binding parameter.
2880fn par_redir() -> Option<ZshRedir> {
2881    par_redir_with_id(None)
2882}
2883
2884/// Wire a here-document body onto the redirection token that
2885/// requested it. Direct port of zsh/Src/parse.c:2347
2886/// `setheredoc`. Called when a heredoc terminator has been
2887/// matched and the body is ready to be attached to the redir.
2888///
2889/// zshrs port note: zsh's setheredoc patches the wordcode
2890/// in-place via `pc[1] = ecstrcode(doc); pc[2] = ecstrcode(term);`.
2891/// zshrs threads heredoc bodies through `HereDocInfo` structs
2892/// attached inline during the post-parse `fill_heredoc_bodies` walk.
2893/// This method is the AST-side equivalent: writes back to the
2894/// matching redir node by index.
2895/// Port of `setheredoc(int pc, int type, char *str, char *termstr,
2896/// char *munged_termstr)` from `Src/parse.c:2347-2355`. Patches the
2897/// pending heredoc redir at `pc` with its body string + raw and
2898/// munged terminator forms.
2899pub fn setheredoc(pc: usize, redir_type: i32, doc: &str, term: &str, munged_term: &str) {
2900    // zshrs-only guard: AST-path heredocs use `pc = -1 as usize`
2901    // (i.e. `usize::MAX`) as a sentinel meaning "no wordcode slot to
2902    // patch". C never passes a negative pc since the wordcode emitter
2903    // is always active. Skip silently for the AST-only case.
2904    if pc == usize::MAX {
2905        return;
2906    }
2907    // c:2350 — `int varid = WC_REDIR_VARID(ecbuf[pc]) ? REDIR_VARID_MASK : 0;`
2908    let cur = ECBUF.with_borrow(|b| b.get(pc).copied().unwrap_or(0));
2909    let varid = if WC_REDIR_VARID(cur) != 0 {
2910        REDIR_VARID_MASK
2911    } else {
2912        0
2913    };
2914    // c:2351 — `ecbuf[pc] = WCB_REDIR(type | REDIR_FROM_HEREDOC_MASK | varid);`
2915    let new_header = WCB_REDIR((redir_type | REDIR_FROM_HEREDOC_MASK | varid) as wordcode);
2916    // c:2352 — `ecbuf[pc + 2] = ecstrcode(str);`
2917    let coded_str = ecstrcode(doc);
2918    // c:2353 — `ecbuf[pc + 3] = ecstrcode(termstr);`
2919    let coded_term = ecstrcode(term);
2920    // c:2354 — `ecbuf[pc + 4] = ecstrcode(munged_termstr);`
2921    let coded_munged = ecstrcode(munged_term);
2922    ECBUF.with_borrow_mut(|b| {
2923        b[pc] = new_header;
2924        b[pc + 2] = coded_str;
2925        b[pc + 3] = coded_term;
2926        b[pc + 4] = coded_munged;
2927    });
2928}
2929
2930/// Parse a wordlist for `for ... in WORDS;`. Direct port of
2931/// zsh/Src/parse.c:2362 `par_wordlist`. Reads STRING tokens
2932/// until the next SEPER / SEMI / NEWLIN.
2933pub fn par_wordlist() -> Vec<String> {
2934    let mut out = Vec::new();
2935    // parse.c:2362-2378 — collect STRINGs into the wordlist.
2936    while tok() == STRING_LEX {
2937        if let Some(text) = tokstr() {
2938            out.push(text);
2939        }
2940        zshlex();
2941    }
2942    out
2943}
2944
2945/// Parse a newline-separated wordlist. Direct port of
2946/// zsh/Src/parse.c:2379 `par_nl_wordlist`. Like
2947/// par_wordlist but tolerates leading/trailing newlines.
2948pub fn par_nl_wordlist() -> Vec<String> {
2949    // parse.c:2380-2381 — skip leading newlines.
2950    while tok() == NEWLIN {
2951        zshlex();
2952    }
2953    let out = par_wordlist();
2954    // parse.c:2395-2397 — skip trailing newlines.
2955    while tok() == NEWLIN {
2956        zshlex();
2957    }
2958    out
2959}
2960
2961/// `COND_SEP()` macro from `Src/parse.c:2405`:
2962///   `(tok == SEPER && condlex != testlex && *zshlextext != ';')`
2963/// A newline (but NOT a `;`) between cond terms inside `[[ … ]]` is
2964/// skippable whitespace; a `;` is a parse error (`[[ a ; ]]`).
2965///
2966/// zshrs has two lexer modes feeding the two cond parsers:
2967///   - wordcode path (autoload / parse_string): LEXFLAGS_NEWLINE off,
2968///     so a newline is FOLDED to SEPER (lex.rs:265). C's `[[ a &&\n b ]]`
2969///     lands here; matching only NEWLIN missed it → "condition
2970///     expected" (broke every multi-line `&&`/`||` cond in compinit and
2971///     the compsys completers).
2972///   - AST path (`-c`, source): LEXFLAGS_NEWLINE on, newline stays
2973///     NEWLIN (unfolded).
2974/// `;` ALWAYS folds to SEPER (lex.rs:265, unconditional for SEMI) with
2975/// LEX_ISNEWLIN==0, so gating the SEPER arm on LEX_ISNEWLIN!=0
2976/// reproduces C's `*zshlextext != ';'` exclusion in both modes.
2977#[inline]
2978pub fn COND_SEP() -> bool {
2979    match tok() {
2980        NEWLIN => true,
2981        SEPER => crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) != 0,
2982        _ => false,
2983    }
2984}
2985
2986/// Parse [[ ... ]] conditional
2987/// Parse `[[ EXPR ]]` conditional expression. Direct port of
2988/// zsh/Src/parse.c:2409 `par_cond` (and helpers par_cond_1,
2989/// par_cond_2, par_cond_double, par_cond_triple, par_cond_multi
2990/// at parse.c:2434-2731). Expression operators: `||` `&&` `!`
2991/// + unary tests (-f, -d, -n, -z, etc.) + binary tests (=, !=,
2992///   <, >, ==, =~, -eq, -ne, -lt, -le, -gt, -ge, -nt, -ot, -ef).
2993fn par_cond() -> Option<ZshCommand> {
2994    // C par_dinbrack (parse.c:1810-1822) wraps the body parse with
2995    // `incond = 1; incmdpos = 0;` BEFORE the first zshlex past `[[`,
2996    // and resets to `incond = 0; incmdpos = 1;` after `]]`. Without
2997    // `incond = 1`, lex.c does not promote `]]` to DOUTBRACK and the
2998    // cond body bleeds past the close bracket — the parser then
2999    // sees `]]` as a separate STRING command. Every `if [[ ... ]]; then`
3000    // failed with `command not found: ]]` before this fix.
3001    set_incond(1);
3002    set_incmdpos(false);
3003    zshlex(); // skip [[
3004              // Empty cond `[[ ]]` is a parse error in zsh — emit the
3005              // diagnostic and return None so the caller produces a
3006              // non-zero exit. Without this, `[[ ]]` silently passed and
3007              // returned exit 0.
3008    if tok() == DOUTBRACK {
3009        zerr("parse error near `]]'");
3010        set_incond(0);
3011        set_incmdpos(true);
3012        zshlex();
3013        return None;
3014    }
3015    let cond = parse_cond_expr();
3016
3017    if tok() == DOUTBRACK {
3018        set_incond(0);
3019        set_incmdpos(true);
3020        zshlex();
3021    } else {
3022        // c:Src/parse.c:1818-1819 — `if (tok != DOUTBRACK)
3023        // YYERRORV(oecused);`. par_dinbrack hard-requires DOUTBRACK
3024        // after par_cond; anything else is a parse error and the
3025        // outer parser's yyerror at c:2747 emits `parse error near
3026        // \`%s'` using zshlextext. Bug #473: BAR (`|`) inside
3027        // `[[ ab == a|b ]]` slipped past par_cond_or (which only
3028        // checks DBAR), the cond returned cleanly, and then the
3029        // top-level parser interpreted BAR as a pipe — running `b`
3030        // as a command (security-relevant if pattern RHS is user
3031        // input). Mirror C: emit parse error and abort.
3032        let tok_text = match tok() {
3033            BAR_TOK => "|".to_string(),
3034            DBAR => "||".to_string(),
3035            AMPER => "&".to_string(),
3036            DAMPER => "&&".to_string(),
3037            SEMI => ";".to_string(),
3038            DSEMI => ";;".to_string(),
3039            NEWLIN | SEPER => String::new(),
3040            _ => tokstr()
3041                .map(|s| crate::ported::lex::untokenize(&s))
3042                .unwrap_or_default(),
3043        };
3044        if tok_text.is_empty() {
3045            zerr("parse error");
3046        } else {
3047            zerr(&format!("parse error near `{}'", tok_text));
3048        }
3049        set_incond(0);
3050        set_incmdpos(true);
3051        return None;
3052    }
3053
3054    cond.map(ZshCommand::Cond)
3055}
3056
3057/// Port of `par_cond_1(void)` from `Src/parse.c:2434`. Parses one
3058/// `||`-separated cond expression. Emits `WCB_COND(COND_AND, …)`
3059/// when an `&&` is found and recurses.
3060pub fn par_cond_1() -> i32 {
3061    // c:2434
3062
3063    let p = ECUSED.with(|c| c.get()) as usize;
3064    let r = par_cond_2();
3065    while COND_SEP() {
3066        condlex();
3067    }
3068    if tok() == DAMPER {
3069        condlex();
3070        while COND_SEP() {
3071            condlex();
3072        }
3073        ecispace(p, 1);
3074        par_cond_1();
3075        let ecused = ECUSED.with(|c| c.get()) as usize;
3076        ECBUF.with(|c| {
3077            c.borrow_mut()[p] = WCB_COND(COND_AND as u32, (ecused - 1 - p) as u32);
3078        });
3079        return 1;
3080    }
3081    r
3082}
3083
3084/// Port of `par_cond_2(void)` from `Src/parse.c:2476`. The heavy
3085/// cond-term parser: handles `! cond`, `(cond)`, unary `[ -X arg ]`,
3086/// binary `[ A op B ]`, and `[ A op1 B op2 C … ]` n-ary chains.
3087pub fn par_cond_2() -> i32 {
3088    // c:2476
3089    // `n_testargs` only applies in `testlex` mode (=== /bin/test
3090    // compat). zshrs has no testlex yet, so always 0.
3091    let n_testargs: i32 = 0;
3092
3093    // c:2481 — handled inline; this Rust port skips the n_testargs
3094    // arm since zshrs invokes par_cond via [[ ... ]] only.
3095
3096    while COND_SEP() {
3097        condlex();
3098    }
3099    if tok() == BANG_TOK {
3100        // c:2522 — `[[ ! cond ]]`
3101        condlex();
3102        ecadd(WCB_COND(COND_NOT as u32, 0));
3103        return par_cond_2();
3104    }
3105    if tok() == INPAR_TOK {
3106        // c:2533 — `[[ (cond) ]]`. Recurse into the WORDCODE cond OR-chain
3107        // `par_cond_top` (C's `par_cond`, parse.c:2409). The bare `par_cond`
3108        // in this crate is the AST `[[ ]]` parser — it opens with
3109        // `zshlex(); // skip [[`, so calling it here consumed the FIRST term
3110        // of the group (e.g. the `-z` in `[[ ( -z x ) ]]`) as if it were the
3111        // `[[`, then failed. Every parenthesised sub-condition parsed via
3112        // parse_string (autoload's function-body parse, `.zcompdump`, etc.)
3113        // hit `parse error`, which broke compinit/compaudit and thus all of
3114        // compsys.
3115        condlex();
3116        while COND_SEP() {
3117            condlex();
3118        }
3119        let r = par_cond_top();
3120        while COND_SEP() {
3121            condlex();
3122        }
3123        if tok() != OUTPAR_TOK {
3124            crate::ported::utils::zerr("missing )");
3125            yyerror(0);
3126            return 0;
3127        }
3128        condlex();
3129        return r;
3130    }
3131    let s1 = tokstr().unwrap_or_default();
3132    // c:2549 — `dble = (s1 && IS_DASH(*s1) && (!n_testargs ||
3133    // strspn(s1+1, "abcd...") == 1) && !s1[2]);` — IS_DASH covers
3134    // BOTH `-` and Dash (`\u{9b}`). The raw tokstr inside `[[ ... ]]`
3135    // carries Dash as a marker byte, so `starts_with('-')` alone
3136    // matches only ASCII dashes and misses every `-z`, `-d`, `-r`
3137    // etc. — every such cond emitted the AST-only `condition
3138    // expected` error from par_cond_double. Use IS_DASH and count
3139    // chars (Dash is a single code point) instead of bytes.
3140    let s1_chars: Vec<char> = s1.chars().collect();
3141    let dble = !s1_chars.is_empty()
3142        && IS_DASH(s1_chars[0])
3143        && s1_chars.len() == 2
3144        && "abcdefghknoprstuvwxzLONGS".contains(s1_chars[1]);
3145    if tok() != STRING_LEX {
3146        if !s1.is_empty() && tok() != LEXERR && (!dble || n_testargs != 0) {
3147            // c:2486-2497 — `if (n_testargs == 1)` block: under
3148            // POSIXBUILTINS-off, `[ -t ]` rewrites to `[ -t 1 ]`
3149            // (ksh behavior). The C gate is `unset(POSIXBUILTINS)
3150            // && check_cond(s1, "t")`. zshrs's parser has
3151            // n_testargs=0 (no testlex), so this rewrite path is
3152            // unreachable from zshrs's [[ ]] / [ ] entry points;
3153            // wired here as a marker for parity. When testlex is
3154            // ported the call below activates.
3155            if n_testargs == 1 && unset(POSIXBUILTINS) && check_cond(&s1, "t") {
3156                condlex();
3157                return par_cond_double(&s1, "1");
3158            }
3159            // c:2557 — `[[ STRING ]]` re-interpreted as `[[ -n STRING ]]`.
3160            condlex();
3161            while COND_SEP() {
3162                condlex();
3163            }
3164            return par_cond_double("-n", &s1);
3165        }
3166        crate::ported::utils::zerr("condition expected");
3167        yyerror(0);
3168        return 0;
3169    }
3170    condlex();
3171    while COND_SEP() {
3172        condlex();
3173    }
3174    if tok() == INANG_TOK || tok() == OUTANG_TOK {
3175        // c:2576 — `<` / `>` string compare.
3176        let xtok = tok();
3177        condlex();
3178        while COND_SEP() {
3179            condlex();
3180        }
3181        if tok() != STRING_LEX {
3182            crate::ported::utils::zerr("string expected");
3183            yyerror(0);
3184            return 0;
3185        }
3186        let s3 = tokstr().unwrap_or_default();
3187        condlex();
3188        while COND_SEP() {
3189            condlex();
3190        }
3191        let op = if xtok == INANG_TOK {
3192            COND_STRLT
3193        } else {
3194            COND_STRGTR
3195        };
3196        ecadd(WCB_COND(op as u32, 0));
3197        ecstr(&s1);
3198        ecstr(&s3);
3199        return 1;
3200    }
3201    if tok() != STRING_LEX {
3202        // c:2592 — only one operand seen → `[ -n s1 ]`.
3203        if tok() != LEXERR {
3204            if !dble || n_testargs != 0 {
3205                return par_cond_double("-n", &s1);
3206            }
3207            return par_cond_multi(&s1, &[]);
3208        }
3209        crate::ported::utils::zerr("syntax error");
3210        yyerror(0);
3211        return 0;
3212    }
3213    let s2 = tokstr().unwrap_or_default();
3214    set_incond(incond() + 1);
3215    condlex();
3216    while COND_SEP() {
3217        condlex();
3218    }
3219    set_incond(incond() - 1);
3220    // c:Src/parse.c:2598-2600 — `if (!n_testargs) dble = (s2 &&
3221    // IS_DASH(*s2) && !s2[2]);` — RECOMPUTE dble based on s2 once
3222    // it's been read, so `[[ A -X B ]]` is treated as a 2-arg cond
3223    // `[ -X B ]` (par_cond_double) rather than a 3-arg triple. This
3224    // is what routes `[[ "" -a "x" ]]` to par_cond_double("", "-a")
3225    // → COND_ERROR "parse error: condition expected: ". Without
3226    // this, the original `dble` from s1 stayed false, the parser
3227    // grabbed s3 and built COND_MODI silently. parity bug #25.
3228    let s2_chars: Vec<char> = s2.chars().collect();
3229    let dble = !s2_chars.is_empty() && IS_DASH(s2_chars[0]) && s2_chars.len() == 2;
3230    if tok() == STRING_LEX && !dble {
3231        let s3 = tokstr().unwrap_or_default();
3232        condlex();
3233        while COND_SEP() {
3234            condlex();
3235        }
3236        if tok() == STRING_LEX {
3237            // c:2615 — n-ary `[ A op B C D ... ]`.
3238            let mut l: Vec<String> = vec![s2, s3];
3239            while tok() == STRING_LEX {
3240                l.push(tokstr().unwrap_or_default());
3241                condlex();
3242                while COND_SEP() {
3243                    condlex();
3244                }
3245            }
3246            return par_cond_multi(&s1, &l);
3247        }
3248        return par_cond_triple(&s1, &s2, &s3);
3249    }
3250    par_cond_double(&s1, &s2)
3251}
3252
3253/// Port of `par_cond_double(char *a, char *b)` from `Src/parse.c:2626`.
3254/// Emits wordcode for unary cond `[ -X b ]` or modular `[ -mod b ]`.
3255pub fn par_cond_double(a: &str, b: &str) -> i32 {
3256    // c:2628 — `if (!IS_DASH(a[0]) || !a[1])` — char-based, since
3257    // Dash is a single code point (`\u{9b}`) and `a.len() < 2` on
3258    // BYTES would still pass for "-z" but fail for the marker form
3259    // `\u{9b}z` (2 bytes). Walk by chars.
3260    let ac: Vec<char> = a.chars().collect();
3261    if ac.is_empty() || !IS_DASH(ac[0]) || ac.len() < 2 {
3262        // c:Src/parse.c:2629 COND_ERROR macro expansion:
3263        //   zwarn(...); herrflush(); errflag |= ERRFLAG_ERROR;
3264        //   YYERROR(ecused) /* sets tok = LEXERR */
3265        // The YYERROR portion is critical — without it the outer
3266        // parser keeps walking the wordcode and execution proceeds
3267        // (e.g. `[[ "" -a "x" ]] && echo m || echo n` runs the
3268        // `|| echo n` branch). Setting LEXERR aborts the upper
3269        // parse so the whole line is rejected, matching zsh's
3270        // observable behavior of stdout="" on parse error.
3271        zerr(&format!("parse error: condition expected: {}", a));
3272        errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::SeqCst);
3273        set_tok(LEXERR);
3274        return 1;
3275    }
3276    // c:2630 — `else if (!a[2] && strspn(a+1, "abcd...zhLONGS") == 1)`
3277    let unary_set = "abcdefgknoprstuvwxzhLONGS";
3278    if ac.len() == 2 && unary_set.contains(ac[1]) {
3279        // c:2631 — `ecadd(WCB_COND(a[1], 0));` uses the raw cond-op
3280        // letter byte as the opcode payload. Use the ASCII char's
3281        // code-point value directly — every letter in `unary_set`
3282        // fits in 7 bits.
3283        ecadd(WCB_COND(ac[1] as u32, 0));
3284        ecstr(b);
3285    } else {
3286        ecadd(WCB_COND(COND_MOD as u32, 1));
3287        ecstr(a);
3288        ecstr(b);
3289    }
3290    1
3291}
3292
3293/// Port of `get_cond_num(char *tst)` from `Src/parse.c:2643`. Returns
3294/// the index of `tst` in `{"nt","ot","ef","eq","ne","lt","gt","le","ge"}`
3295/// or `-1` if not a recognized binary cond operator.
3296pub fn get_cond_num(tst: &str) -> i32 {
3297    // c:2643
3298    const CONDSTRS: [&str; 9] = [
3299        "nt", "ot", "ef", "eq", "ne", "lt", "gt", "le", "ge", // c:2647
3300    ];
3301    for (i, &c) in CONDSTRS.iter().enumerate() {
3302        if c == tst {
3303            return i as i32; // c:2654
3304        }
3305    }
3306    -1 // c:2656
3307}
3308
3309/// par_time's `static int inpartime` guard at C parse.c:1038
3310/// preventing infinite recursion on `time time foo`. The wordcode
3311/// path keeps this as a thread_local since C uses a function-level
3312/// `static int` (per-process; per-evaluator semantically matches).
3313thread_local! {
3314    static PARSER_INPARTIME: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3315}
3316
3317/// Port of `par_cond_triple(char *a, char *b, char *c)` from
3318/// `Src/parse.c:2659`. Emits wordcode for the binary forms
3319/// `[ A op B ]` — `=` / `==` / `!=` / `<` / `>` / `=~` / `-X`.
3320///
3321/// C does `(b[0] == Equals || b[0] == '=')` etc., matching BOTH the
3322/// raw ASCII operator char AND its tokenized marker form per
3323/// `Src/zsh.h:159-194`:
3324///   Equals = `\u{8d}`, Outang = `\u{95}`, Inang  = `\u{94}`,
3325///   Tilde  = `\u{98}`, Bang   = `\u{9c}`, Dash   = `\u{9b}`.
3326/// Inside `[[ ... ]]` the lexer emits the marker bytes — comparing
3327/// against literal-only `b"=="` misses every cond op.
3328/// (The previous Rust port had the doc comment values wrong:
3329/// Outang=0x8e was actually Bar; Inang=0x91 was Inbrack;
3330/// Tilde=0x96 was OutangProc; Bang=0x8b was Outparmath. The code
3331/// itself uses the correct const names, so this was a docs-only fix.)
3332pub fn par_cond_triple(a: &str, b: &str, c: &str) -> i32 {
3333    // c:2659
3334    let bc: Vec<char> = b.chars().collect();
3335    let is_eq = |ch: char| ch == '=' || ch == Equals;
3336    let is_gt = |ch: char| ch == '>' || ch == Outang;
3337    let is_lt = |ch: char| ch == '<' || ch == Inang;
3338    let is_tilde = |ch: char| ch == '~' || ch == Tilde;
3339    let is_bang = |ch: char| ch == '!' || ch == Bang;
3340
3341    // c:2663 — `(b[0] == Equals || b[0] == '=') && !b[1]` → `=` (single).
3342    if bc.len() == 1 && is_eq(bc[0]) {
3343        ecadd(WCB_COND(COND_STREQ as u32, 0));
3344        ecstr(a);
3345        ecstr(c);
3346        let np = ECNPATS.with(|cc| {
3347            let v = cc.get();
3348            cc.set(v + 1);
3349            v
3350        }) as u32;
3351        ecadd(np);
3352        return 1;
3353    }
3354    // c:2668-2673 — `(t0 = b[0]=='>' || Outang) || b[0]=='<' || Inang`.
3355    if bc.len() == 1 && (is_gt(bc[0]) || is_lt(bc[0])) {
3356        let op = if is_gt(bc[0]) {
3357            COND_STRGTR
3358        } else {
3359            COND_STRLT
3360        };
3361        ecadd(WCB_COND(op as u32, 0));
3362        ecstr(a);
3363        ecstr(c);
3364        let np = ECNPATS.with(|cc| {
3365            let v = cc.get();
3366            cc.set(v + 1);
3367            v
3368        }) as u32;
3369        ecadd(np);
3370        return 1;
3371    }
3372    // c:2674-2679 — `==` STRDEQ.
3373    if bc.len() == 2 && is_eq(bc[0]) && is_eq(bc[1]) {
3374        ecadd(WCB_COND(COND_STRDEQ as u32, 0));
3375        ecstr(a);
3376        ecstr(c);
3377        let np = ECNPATS.with(|cc| {
3378            let v = cc.get();
3379            cc.set(v + 1);
3380            v
3381        }) as u32;
3382        ecadd(np);
3383        return 1;
3384    }
3385    // c:2680-2684 — `!=` STRNEQ.
3386    if bc.len() == 2 && is_bang(bc[0]) && is_eq(bc[1]) {
3387        ecadd(WCB_COND(COND_STRNEQ as u32, 0));
3388        ecstr(a);
3389        ecstr(c);
3390        let np = ECNPATS.with(|cc| {
3391            let v = cc.get();
3392            cc.set(v + 1);
3393            v
3394        }) as u32;
3395        ecadd(np);
3396        return 1;
3397    }
3398    // c:2685-2691 — `=~` REGEX (no pattern slot — implicit COND_MODI).
3399    if bc.len() == 2 && is_eq(bc[0]) && is_tilde(bc[1]) {
3400        ecadd(WCB_COND(COND_REGEX as u32, 0));
3401        ecstr(a);
3402        ecstr(c);
3403        return 1;
3404    }
3405    // c:2692-2702 — `-OP` numeric-or-modular cond (e.g. `-eq`, `-nt`).
3406    if !bc.is_empty() && IS_DASH(bc[0]) {
3407        let rest: String = bc[1..].iter().collect();
3408        let t = get_cond_num(&rest);
3409        if t > -1 {
3410            ecadd(WCB_COND((t + COND_NT) as u32, 0));
3411            ecstr(a);
3412            ecstr(c);
3413            return 1;
3414        }
3415        ecadd(WCB_COND(COND_MODI as u32, 0));
3416        ecstr(b);
3417        ecstr(a);
3418        ecstr(c);
3419        return 1;
3420    }
3421    // c:2703-2707 — `-mod A B C` modular cond on `a`.
3422    let ac: Vec<char> = a.chars().collect();
3423    if !ac.is_empty() && IS_DASH(ac[0]) && ac.len() > 1 {
3424        ecadd(WCB_COND(COND_MOD as u32, 2));
3425        ecstr(a);
3426        ecstr(b);
3427        ecstr(c);
3428        return 1;
3429    }
3430    zerr(&format!("condition expected: {}", b));
3431    1
3432}
3433
3434/// Port of `par_cond_multi(char *a, LinkList l)` from `Src/parse.c:2716`.
3435/// Emits wordcode for `[ -OP A B C … ]` n-ary cond (alternation).
3436pub fn par_cond_multi(a: &str, l: &[String]) -> i32 {
3437    // c:2716 — `if (!IS_DASH(a[0]) || !a[1])`; same Dash/`-` dual
3438    // matching as par_cond_double, char-walked because Dash is a
3439    // single code point.
3440    let ac: Vec<char> = a.chars().collect();
3441    if ac.is_empty() || !IS_DASH(ac[0]) || ac.len() < 2 {
3442        zerr(&format!("condition expected: {}", a));
3443        return 1;
3444    }
3445    ecadd(WCB_COND(COND_MOD as u32, l.len() as u32));
3446    ecstr(a);
3447    for item in l {
3448        ecstr(item);
3449    }
3450    1
3451}
3452
3453/// Emit a parser-level error. Direct port of zsh/Src/parse.c
3454/// 2733-2766 `yyerror`. C version fills a per-event error buffer
3455/// and sets errflag. zshrs pushes onto errors which the
3456/// caller drains via parse()'s Result return.
3457/// WARNING: param-name divergence — Rust takes `&str message`, C takes
3458/// Port of `static void yyerror(int noerr)` from `Src/parse.c:2733`.
3459///
3460/// Faithful C body (verbatim):
3461/// ```c
3462/// int t0; char *t;
3463/// if ((t = dupstring(zshlextext))) untokenize(t);
3464/// for (t0 = 0; t0 != 20; t0++)
3465///     if (!t || !t[t0] || t[t0] == '\n') break;
3466/// if (!(histdone & HISTFLAG_NOEXEC) && !(errflag & ERRFLAG_INT)) {
3467///     if (t0) {
3468///         t = metafy(t, t0, META_STATIC);
3469///         zwarn("parse error near `%s%s'", t, t0 == 20 ? "..." : "");
3470///     } else
3471///         zwarn("parse error");
3472/// }
3473/// if (!noerr && noerrs != 2)
3474///     errflag |= ERRFLAG_ERROR;
3475/// ```
3476///
3477/// `zshlextext` is the C lexer's current-token text (`Src/lex.c:170`
3478/// `char *tokstr`); zshrs's equivalent is `lex::tokstr()`. The "20"
3479/// is C's tail-truncation length for the error message.
3480pub fn yyerror(noerr: i32) {
3481    // c:2733
3482    // c:2738 — `if ((t = dupstring(zshlextext))) untokenize(t);`.
3483    // In C, `zshlextext` falls back to `tokstrings[tok]` (lex.c:1965)
3484    // for punctuation tokens that didn't capture a tokstr — that's
3485    // how "parse error near `)'" gets the `)` for OUTPAR. Mirror by
3486    // consulting `lex::tokstring(tok())` when the captured tokstr is
3487    // None.
3488    let t_opt: Option<String> = match crate::ported::lex::tokstr() {
3489        Some(raw) => Some(crate::ported::lex::untokenize(&raw).to_string()),
3490        None => {
3491            let t = crate::ported::lex::tok();
3492            let i = t as usize;
3493            if i < crate::ported::lex::tokstrings.len() {
3494                crate::ported::lex::tokstrings[i].map(|s| s.to_string())
3495            } else {
3496                None
3497            }
3498        }
3499    };
3500    let t_bytes: Vec<u8> = t_opt
3501        .as_ref()
3502        .map(|s| s.as_bytes().to_vec())
3503        .unwrap_or_default();
3504
3505    // c:2741-2743 — `for (t0 = 0; t0 != 20; t0++) if (!t || !t[t0]
3506    //   || t[t0] == '\n') break;`
3507    let mut t0: usize = 0;
3508    while t0 != 20 {
3509        // c:2741
3510        let stop =
3511            t_opt.is_none() || t0 >= t_bytes.len() || t_bytes[t0] == 0 || t_bytes[t0] == b'\n';
3512        if stop {
3513            break;
3514        }
3515        t0 += 1;
3516    }
3517
3518    // c:2744 — `if (!(histdone & HISTFLAG_NOEXEC) && !(errflag &
3519    //   ERRFLAG_INT))`. The HISTFLAG_NOEXEC gate suppresses warnings
3520    //   from history-recall paths that aren't actually executing.
3521    let histdone_v = crate::ported::hist::histdone.load(Ordering::SeqCst);
3522    let hist_noexec = (histdone_v & crate::ported::zsh_h::HISTFLAG_NOEXEC as i32) != 0;
3523    let int_flagged = (errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_INT) != 0;
3524    if !hist_noexec && !int_flagged {
3525        // c:2744
3526        if t0 != 0 {
3527            // c:2745
3528            // c:2746 — `t = metafy(t, t0, META_STATIC);` — re-metafy
3529            //   the truncated head so embedded Meta bytes display
3530            //   correctly. The Rust port already holds an untokenized
3531            //   string; use the byte slice [0..t0] directly.
3532            let head = std::str::from_utf8(&t_bytes[..t0]).unwrap_or_default();
3533            let suffix = if t0 == 20 { "..." } else { "" };
3534            crate::ported::utils::zwarn(&format!("parse error near `{}{}'", head, suffix));
3535        // c:2747
3536        } else {
3537            // c:2748
3538            crate::ported::utils::zwarn("parse error"); // c:2749
3539        }
3540    }
3541    // c:2751 — `if (!noerr && noerrs != 2) errflag |= ERRFLAG_ERROR;`.
3542    //   The `noerrs != 2` gate (suppress-only-fatal-errors) is preserved
3543    //   for parity with zerr/zwarn's matching check.
3544    let noerrs_v = *crate::ported::utils::noerrs_lock().lock().unwrap();
3545    if noerr == 0 && noerrs_v != 2 {
3546        // c:2751
3547        errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::SeqCst);
3548        // c:2752
3549    }
3550}
3551
3552// ============================================================
3553// Eprog runtime ops (parse.c:2767-2853)
3554//
3555// dupeprog / useeprog / freeeprog are zsh's reference-counting
3556// helpers for executable programs. zshrs's AST is owned by
3557// value (Rust ownership); cloning is a tree-deep copy via
3558// Clone, "use" is a no-op (the executor borrows the AST), and
3559// "free" is automatic on drop.
3560// ============================================================
3561
3562/// Duplicate an Eprog. Direct port of zsh/Src/parse.c:2813
3563/// Port of `Eprog dupeprog(Eprog p, int heap)` from
3564/// `Src/parse.c:2767`. Deep-copies the wordcode array, string
3565/// table, and pattern-prog slots. `dummy_eprog` is returned
3566/// unchanged. `heap`-allocated copies get `nref = -1` (never
3567/// freed); real ones get `nref = 1`.
3568pub fn dupeprog(p: &eprog, heap: bool) -> eprog {
3569    // c:2774-2775 — `if (p == &dummy_eprog) return p;` — caller-
3570    // observable identity in C uses a pointer compare; Rust's
3571    // equivalent is "if it has the dummy's shape (single WCB_END
3572    // word and no strs), return a copy of the same shape".
3573    // c:2796-2797 — `for (i = r->npats; i--; pp++) *pp = dummy_patprog1;`
3574    // C uses `dummy_patprog1` as a placeholder; the Rust port has
3575    // `Vec<Patprog>` (Box<patprog>) — synthesize an equivalent zero-
3576    // initialized patprog for each slot (resolved later by
3577    // pattern.c::patcompile-on-first-use).
3578    let dummy_pat = || crate::ported::zsh_h::patprog {
3579        startoff: 0,
3580        size: 0,
3581        mustoff: 0,
3582        patmlen: 0,
3583        globflags: 0,
3584        globend: 0,
3585        flags: 0,
3586        patnpar: 0,
3587        patstartch: 0,
3588    };
3589    let r = eprog {
3590        // c:2778 — `flags = (heap ? EF_HEAP : EF_REAL) | (p->flags & EF_RUN);`
3591        flags: (if heap { EF_HEAP } else { EF_REAL }) | (p.flags & EF_RUN),
3592        len: p.len,
3593        npats: p.npats,
3594        // c:2787 — `nref = heap ? -1 : 1;`
3595        nref: if heap { -1 } else { 1 },
3596        prog: p.prog.clone(),
3597        strs: p.strs.clone(), // pool copied verbatim — provenance below
3598        pats: (0..p.npats).map(|_| Box::new(dummy_pat())).collect(),
3599        shf: None,
3600        dump: None,
3601        strs_metafied: p.strs_metafied, // pool copied verbatim — carry provenance
3602    };
3603    r
3604}
3605
3606/// Port of `void useeprog(Eprog p)` from `Src/parse.c:2813`.
3607/// `if (p && p != &dummy_eprog && p->nref >= 0) p->nref++;` —
3608/// pin a real (non-heap, non-dummy) Eprog so it survives the
3609/// next `freeeprog`.
3610pub fn useeprog(p: &mut eprog) {
3611    // c:2815 — `if (p && p != &dummy_eprog && p->nref >= 0)`
3612    if p.nref >= 0 {
3613        p.nref += 1; // c:2816
3614    }
3615}
3616
3617/// Port of `void freeeprog(Eprog p)` from `Src/parse.c:2823`.
3618/// Refcount-decrement; when it hits zero, drops the pattern progs,
3619/// decrements the dump refcount if any, and releases the eprog.
3620/// `dummy_eprog` is never freed. Heap-eprogs (`nref < 0`) are
3621/// never freed either — they live as long as the heap arena.
3622pub fn freeeprog(p: &mut eprog) {
3623    // c:2829 — `if (p && p != &dummy_eprog) { ... }`
3624    if p.nref > 0 {
3625        p.nref -= 1; // c:2832
3626        if p.nref == 0 {
3627            // c:2833-2840 — drop pats, dump refcount, then the eprog.
3628            // Rust's Drop handles the per-field cleanup; we just
3629            // need to decrement the dump count first.
3630            if let Some(dump) = p.dump.take() {
3631                let dumped = (*dump).clone();
3632                decrdumpcount(&dumped); // c:2837
3633            }
3634            p.prog.clear();
3635            p.strs = None;
3636            p.pats.clear();
3637        }
3638    }
3639}
3640
3641// =============================================================================
3642// Wordcode read helpers — used by text.rs's `gettext2` and exec dispatch
3643// to walk a compiled Eprog without re-running the parser. These are the
3644// only `Src/parse.c` functions ported so far in this file; the recursive-
3645// descent parser (par_event / par_list / par_cmd / par_*) follows
3646// below as free ported at module scope.
3647// =============================================================================
3648
3649/// Port of `ecgetstr(Estate s, int dup, int *tokflag)` from `Src/parse.c:2855`.
3650/// `s->pc` advances through the wordcode buffer; `s->strs` indexes the
3651/// string pool. Returns the interned string (or a 1-3-char literal
3652/// inlined directly into the wordcode word).
3653pub fn ecgetstr(s: &mut estate, dup: i32, tokflag: Option<&mut i32>) -> String {
3654    let prog = &s.prog.prog;
3655    if s.pc >= prog.len() {
3656        return String::new();
3657    }
3658    let c = prog[s.pc]; // c:2858 `wordcode c = *s->pc++;`
3659    s.pc += 1;
3660    if let Some(tf) = tokflag {
3661        *tf = i32::from((c & 1) != 0); // c:2880 `*tokflag = (c & 1);`
3662    }
3663    if c == 6 || c == 7 {
3664        // c:2861 `if (c == 6 || c == 7) r = "";`
3665        return String::new();
3666    }
3667    let r: String = if (c & 2) != 0 {
3668        // c:2862 — `else if (c & 2)`
3669        // c:2863-2868 — 3-byte inline string packed into the wordcode
3670        // word; followed by `buf[3] = '\0'; r = dupstring(buf);`.
3671        // C's `dupstring` uses `strlen(buf)` which TRUNCATES at the
3672        // first NUL byte — short strings of 1 or 2 chars get padded
3673        // with NULs and truncated cleanly. The previous Rust port
3674        // used `retain(|&x| x != 0)` which would silently SPLICE OUT
3675        // an interior NUL (e.g. `[a, 0, b]` → "ab"), diverging from
3676        // C's strlen-truncate (`[a, 0, b]` → "a"). Fix: truncate at
3677        // first NUL to match C exactly.
3678        let b0 = ((c >> 3) & 0xff) as u8;
3679        let b1 = ((c >> 11) & 0xff) as u8;
3680        let b2 = ((c >> 19) & 0xff) as u8;
3681        let v = [b0, b1, b2];
3682        let end = v.iter().position(|&x| x == 0).unwrap_or(v.len()); // c:2869 strlen(buf)
3683                                                                     // C reads raw bytes (token codes included) — widen via the
3684                                                                     // wordcode-pool bridge, never from_utf8_lossy (which mangles
3685                                                                     // raw token bytes from C-zsh-written .zwc dumps to U+FFFD).
3686        crate::zwc::wordcode_pool_str_unmeta(&v[..end], s.prog.strs_metafied)
3687    } else {
3688        // c:2877 `else r = s->strs + (c >> 2);`
3689        let off = (c >> 2) as usize + s.strs_offset;
3690        let strs_bytes = s.strs.as_deref().unwrap_or("").as_bytes();
3691        if off >= strs_bytes.len() {
3692            String::new()
3693        } else {
3694            let tail = &strs_bytes[off..];
3695            let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
3696            crate::zwc::wordcode_pool_str_unmeta(&tail[..end], s.prog.strs_metafied)
3697        }
3698    };
3699    // c:2891 `return ((dup == EC_DUP || (dup && (c & 1))) ? dupstring(r) : r);`
3700    // Rust owns the String already; `dup` flag has no observable effect.
3701    let _ = (dup, EC_DUP, EC_NODUP);
3702    r
3703}
3704
3705// ============================================================
3706// Wordcode runtime getters (parse.c:2853-3060)
3707//
3708// Direct ports of the wordcode-read helpers (ecrawstr,
3709// ecgetstr, ecgetarr, ecgetredirs, ecgetlist, eccopyredirs).
3710// Read packed wordcode out of an Eprog at execution time.
3711// Used by exec_wordcode and the wordcode-walking dispatch in
3712// src/vm_helper.
3713// ============================================================
3714
3715/// Port of `ecrawstr(Eprog p, Wordcode pc, int *tokflag)` from
3716/// `Src/parse.c:2891`. Like `ecgetstr` but reads at the given pc
3717/// without advancing — caller steps `pc` separately.
3718pub fn ecrawstr(p: &eprog, pc: usize, tokflag: Option<&mut i32>) -> String {
3719    if pc >= p.prog.len() {
3720        return String::new();
3721    }
3722    let c = p.prog[pc]; // c:2894
3723    if let Some(tf) = tokflag {
3724        *tf = i32::from((c & 1) != 0); // c:2898/2906/2912
3725    }
3726    if c == 6 || c == 7 {
3727        // c:2897
3728        return String::new();
3729    }
3730    if (c & 2) != 0 {
3731        // c:2902-2906 — same 3-byte inline string as ecgetstr, then
3732        // `buf[3] = '\0'; return dupstring(buf);` — truncate at first
3733        // NUL via strlen (NOT splice out interior NULs).
3734        let b0 = ((c >> 3) & 0xff) as u8;
3735        let b1 = ((c >> 11) & 0xff) as u8;
3736        let b2 = ((c >> 19) & 0xff) as u8;
3737        let v = [b0, b1, b2];
3738        let end = v.iter().position(|&x| x == 0).unwrap_or(v.len()); // c:2906 strlen(buf)
3739                                                                     // Raw-byte widening — see ecgetstr (same C-convention bridge).
3740        crate::zwc::wordcode_pool_str_unmeta(&v[..end], p.strs_metafied)
3741    } else {
3742        // c:2911
3743        let off = (c >> 2) as usize;
3744        let strs_bytes = p.strs.as_deref().unwrap_or("").as_bytes();
3745        if off >= strs_bytes.len() {
3746            return String::new();
3747        }
3748        let tail = &strs_bytes[off..];
3749        let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
3750        crate::zwc::wordcode_pool_str_unmeta(&tail[..end], p.strs_metafied)
3751    }
3752}
3753
3754/// Port of `ecgetarr(Estate s, int num, int dup, int *tokflag)` from
3755/// `Src/parse.c:2917`. Reads `num` strings from wordcode at `s->pc`
3756/// and OR-folds each entry's token flag into `*tokflag`.
3757pub fn ecgetarr(s: &mut estate, num: usize, dup: i32, tokflag: Option<&mut i32>) -> Vec<String> {
3758    let mut ret: Vec<String> = Vec::with_capacity(num); // c:2922
3759    let mut tf: i32 = 0;
3760    for _ in 0..num {
3761        // c:2924 `while (num--)`
3762        let mut tmp = 0;
3763        ret.push(ecgetstr(s, dup, Some(&mut tmp))); // c:2925
3764        tf |= tmp; // c:2926
3765    }
3766    if let Some(out) = tokflag {
3767        // c:2929
3768        *out = tf;
3769    }
3770    ret
3771}
3772
3773/// Port of `ecgetlist(Estate s, int num, int dup, int *tokflag)` from
3774/// `Src/parse.c:2937`. Same shape as `ecgetarr` but C returns
3775/// `LinkList`; zshrs uses `Vec<String>` for both.
3776pub fn ecgetlist(s: &mut estate, num: usize, dup: i32, tokflag: Option<&mut i32>) -> Vec<String> {
3777    if num == 0 {
3778        // c:2949-2952
3779        if let Some(tf) = tokflag {
3780            *tf = 0;
3781        }
3782        return Vec::new();
3783    }
3784    ecgetarr(s, num, dup, tokflag)
3785}
3786
3787/// Port of `ecgetredirs(Estate s)` from `Src/parse.c:2959`.
3788///
3789/// `strs` must be the same tail `ecgetstr` uses (`s->strs` / `estate.strs` from offset).
3790/// WARNING: param names don't match C — Rust=(prog, strs, pc) vs C=(s)
3791pub fn ecgetredirs(s: &mut estate) -> Vec<redir> {
3792    let mut ret: Vec<redir> = Vec::new(); // c:2959 `LinkList ret = newlinklist();`
3793    let prog_len = s.prog.prog.len();
3794    if s.pc >= prog_len {
3795        return ret;
3796    }
3797    let mut code = s.prog.prog[s.pc]; // c:2962 `wordcode code = *s->pc++;`
3798    s.pc += 1;
3799
3800    loop {
3801        if wc_code(code) != WC_REDIR {
3802            // c:2988-2989 `s->pc--` then break from while
3803            s.pc = s.pc.saturating_sub(1);
3804            break;
3805        }
3806
3807        let typ = WC_REDIR_TYPE(code); // c:2967 `r->type = WC_REDIR_TYPE(code);`
3808        if s.pc >= prog_len {
3809            break;
3810        }
3811        let fd1_w = s.prog.prog[s.pc]; // c:2968 `r->fd1 = *s->pc++;`
3812        s.pc += 1;
3813
3814        let name = ecgetstr(s, EC_DUP, None); // c:2969 `r->name = ecgetstr(...)`
3815
3816        let (flags, here_terminator, munged_here_terminator) = if WC_REDIR_FROM_HEREDOC(code) != 0 {
3817            // c:2970-2973
3818            let term = ecgetstr(s, EC_DUP, None);
3819            let munged = ecgetstr(s, EC_DUP, None);
3820            (REDIRF_FROM_HEREDOC, Some(term), Some(munged))
3821        } else {
3822            // c:2974-2977
3823            (0, None, None)
3824        };
3825
3826        let varid = if WC_REDIR_VARID(code) != 0 {
3827            // c:2979-2980
3828            Some(ecgetstr(s, EC_DUP, None))
3829        } else {
3830            None // c:2981-2982
3831        };
3832
3833        ret.push(redir {
3834            // c:2965-2982 fields + c:2984 `addlinknode`
3835            typ,
3836            flags,
3837            fd1: fd1_w as i32,
3838            fd2: 0,
3839            name: Some(name),
3840            varid,
3841            here_terminator,
3842            munged_here_terminator,
3843        });
3844
3845        if s.pc >= prog_len {
3846            break;
3847        }
3848        code = s.prog.prog[s.pc]; // c:2986 `code = *s->pc++;`
3849        s.pc += 1;
3850    }
3851
3852    ret // c:2990 `return ret`
3853}
3854
3855/// Port of `eccopyredirs(Estate s)` from `Src/parse.c:3003`. Reads
3856/// the WC_REDIR run at `s->pc`, counts the wordcodes needed,
3857/// reserves space in `ecbuf` via `ecispace`, then re-walks `s->pc`
3858/// re-emitting each redir's wordcodes into the reserved slot —
3859/// finally calls `bld_eprog(0)` to package the result as an Eprog.
3860pub fn eccopyredirs(s: &mut estate) -> Option<eprog> {
3861    let prog_len = s.prog.prog.len();
3862    if s.pc >= prog_len {
3863        return None;
3864    }
3865    // c:3007-3009 — `if (wc_code(*pc) != WC_REDIR) return NULL;`
3866    let first_code = s.prog.prog[s.pc];
3867    if wc_code(first_code) != WC_REDIR {
3868        return None;
3869    }
3870    // c:3011 — `init_parse();`
3871    init_parse();
3872
3873    // c:3013-3027 — count wordcodes the redir run will need.
3874    // Each WC_REDIR contributes `code + fd1 + name` = 3, plus
3875    // `+2` if WC_REDIR_FROM_HEREDOC (terminator + munged), plus
3876    // `+1` if WC_REDIR_VARID.
3877    let mut probe = s.pc;
3878    let mut ncodes = 0usize;
3879    loop {
3880        if probe >= prog_len {
3881            break;
3882        }
3883        let code = s.prog.prog[probe];
3884        if wc_code(code) != WC_REDIR {
3885            break;
3886        }
3887        let mut ncode = if WC_REDIR_FROM_HEREDOC(code) != 0 {
3888            5
3889        } else {
3890            3
3891        };
3892        if WC_REDIR_VARID(code) != 0 {
3893            ncode += 1;
3894        }
3895        probe += ncode;
3896        ncodes += ncode;
3897    }
3898
3899    // c:3028-3029 — `r = ecused; ecispace(r, ncodes);`
3900    let r0 = ECUSED.get() as usize;
3901    ecispace(r0, ncodes);
3902
3903    // c:3031-3053 — re-walk `s->pc` and write into ecbuf[r..].
3904    let mut r = r0;
3905    loop {
3906        if s.pc >= prog_len {
3907            break;
3908        }
3909        let code = s.prog.prog[s.pc];
3910        if wc_code(code) != WC_REDIR {
3911            break;
3912        }
3913        s.pc += 1;
3914        // c:3036 — `ecbuf[r++] = code;`
3915        ECBUF.with_borrow_mut(|buf| {
3916            if r >= buf.len() {
3917                buf.resize(r + 1, 0);
3918            }
3919            buf[r] = code;
3920        });
3921        r += 1;
3922        // c:3038 — `ecbuf[r++] = *s->pc++;` (the fd1 word)
3923        let fd1 = s.prog.prog[s.pc];
3924        s.pc += 1;
3925        ECBUF.with_borrow_mut(|buf| {
3926            if r >= buf.len() {
3927                buf.resize(r + 1, 0);
3928            }
3929            buf[r] = fd1;
3930        });
3931        r += 1;
3932        // c:3041 — `ecbuf[r++] = ecstrcode(ecgetstr(s, EC_NODUP, NULL));`
3933        let name = ecgetstr(s, EC_NODUP, None);
3934        let nc = ecstrcode(&name);
3935        ECBUF.with_borrow_mut(|buf| {
3936            if r >= buf.len() {
3937                buf.resize(r + 1, 0);
3938            }
3939            buf[r] = nc;
3940        });
3941        r += 1;
3942        // c:3042-3047 — heredoc terminators.
3943        if WC_REDIR_FROM_HEREDOC(code) != 0 {
3944            let term = ecgetstr(s, EC_NODUP, None);
3945            let tc = ecstrcode(&term);
3946            ECBUF.with_borrow_mut(|buf| {
3947                if r >= buf.len() {
3948                    buf.resize(r + 1, 0);
3949                }
3950                buf[r] = tc;
3951            });
3952            r += 1;
3953            let munged = ecgetstr(s, EC_NODUP, None);
3954            let mc = ecstrcode(&munged);
3955            ECBUF.with_borrow_mut(|buf| {
3956                if r >= buf.len() {
3957                    buf.resize(r + 1, 0);
3958                }
3959                buf[r] = mc;
3960            });
3961            r += 1;
3962        }
3963        // c:3048-3049 — varid.
3964        if WC_REDIR_VARID(code) != 0 {
3965            let varid = ecgetstr(s, EC_NODUP, None);
3966            let vc = ecstrcode(&varid);
3967            ECBUF.with_borrow_mut(|buf| {
3968                if r >= buf.len() {
3969                    buf.resize(r + 1, 0);
3970                }
3971                buf[r] = vc;
3972            });
3973            r += 1;
3974        }
3975    }
3976
3977    // c:3056 — `return bld_eprog(0);` — `bld_eprog` appends the
3978    // WC_END marker and packages ECBUF/ECSTRS into an Eprog.
3979    Some(bld_eprog(false))
3980}
3981
3982/// Port of `init_eprog(void)` from `Src/parse.c:3069`. Sets up
3983/// `dummy_eprog_code = WCB_END(); dummy_eprog.len = sizeof(wordcode);
3984/// dummy_eprog.prog = &dummy_eprog_code; dummy_eprog.strs = NULL;`.
3985/// Called once at shell startup (init_main → init_misc → init_eprog).
3986pub fn init_eprog() {
3987    let mut d = DUMMY_EPROG.lock().unwrap();
3988    d.prog = vec![WCB_END()]; // c:3071/3073
3989    d.len = size_of::<wordcode>() as i32; // c:3072
3990    d.strs = None; // c:3074
3991    d.flags = 0;
3992    d.npats = 0;
3993    d.nref = 0;
3994}
3995
3996// =====================================================================
3997// `bin_zcompile` and wordcode-dump helpers — port of `Src/parse.c:3104+`.
3998//
3999// The wordcode dump format (`.zwc`) is a serialized parse tree zsh can
4000// `mmap()` and dispatch from without re-parsing on every shell start.
4001// File layout (one struct = `FD_PRELEN` `u32`s):
4002//   - `pre[0]` = magic word (FD_MAGIC native byte-order, FD_OMAGIC
4003//     opposite byte-order).
4004//   - `pre[1]` = packed `{flags(8) | other_offset(24)}` byte field.
4005//   - `pre[2..12]` = `ZSH_VERSION` C-string padded to 40 bytes.
4006//   - `pre[12]` = `fdheaderlen` (total prelude+header word count).
4007//   - Then a sequence of `struct fdhead` records, one per function,
4008//     each followed by its NUL-terminated name (padded to 4-byte).
4009//   - Then the wordcode bytes for every function back-to-back.
4010//
4011// On a little-endian host writing a dump twice: first `FD_MAGIC` for
4012// native readers, then re-walks the body byte-swapped and emits a
4013// second `FD_OMAGIC` copy so big-endian readers can mmap it too.
4014// =====================================================================
4015
4016// File-format constants — port of `Src/parse.c:3104-3150`.
4017
4018/// `#define FD_EXT ".zwc"` from `Src/parse.c:3104`.
4019pub const FD_EXT: &str = ".zwc";
4020
4021/// `#define FD_MINMAP 4096` from `Src/parse.c:3105`. mmap threshold
4022/// — `-M` mode only kicks in when the wordcode body is at least
4023/// this many bytes (otherwise read(2) is preferred).
4024pub const FD_MINMAP: usize = 4096;
4025
4026/// `#define FD_PRELEN 12` from `Src/parse.c:3107`. File-header
4027/// length in u32 words: magic + packed-flags-byte + 10 version words.
4028pub const FD_PRELEN: usize = 12;
4029
4030/// `#define FD_MAGIC 0x04050607` from `Src/parse.c:3108`. Sentinel
4031/// for native-byte-order dumps.
4032pub const FD_MAGIC: u32 = 0x04050607;
4033
4034/// `#define FD_OMAGIC 0x07060504` from `Src/parse.c:3109`. Sentinel
4035/// for opposite-byte-order dumps (byte-swapped FD_MAGIC).
4036pub const FD_OMAGIC: u32 = 0x07060504;
4037
4038/// `#define FDF_MAP 1` from `Src/parse.c:3111`. Bit set when the
4039/// dump should be `mmap()`-ed (`-M` flag) vs read normally (`-R`).
4040pub const FDF_MAP: u32 = 1;
4041
4042/// `#define FDF_OTHER 2` from `Src/parse.c:3112`. Bit indicating
4043/// this dump has an opposite-byte-order copy at `fdother(f)`.
4044pub const FDF_OTHER: u32 = 2;
4045
4046/// Port of `struct fdhead` from `Src/parse.c:3116`. One per function
4047/// inside a wordcode dump. All fields are `wordcode` (u32).
4048#[allow(non_camel_case_types)]
4049#[derive(Debug, Clone, Copy)]
4050pub struct fdhead {
4051    /// Offset (in u32 words) to the start of this function's
4052    /// wordcode body inside the dump.
4053    pub start: u32, // c:3117
4054    /// Wordcode-byte length of the body (excludes pattern-prog slots).
4055    pub len: u32, // c:3118
4056    /// Number of compiled patterns the body references.
4057    pub npats: u32, // c:3119
4058    /// Offset of the string table inside `prog->prog`.
4059    pub strs: u32, // c:3120
4060    /// Header-record length in u32 words (record + name).
4061    pub hlen: u32, // c:3121
4062    /// Packed `{ kshload_bits(2) | name_tail_offset(30) }` field.
4063    pub flags: u32, // c:3122
4064}
4065
4066/// `#define FDHF_KSHLOAD 1` from `Src/parse.c:3149`. Function-header
4067/// flag word — `-k` ksh-style autoload marker.
4068pub const FDHF_KSHLOAD: u32 = 1;
4069
4070/// `#define FDHF_ZSHLOAD 2` from `Src/parse.c:3150`. `-z` zsh-style
4071/// autoload marker.
4072pub const FDHF_ZSHLOAD: u32 = 2;
4073
4074/// Port of `struct wcfunc` from `Src/parse.c:3158`. Build-time
4075/// per-function aggregate before write_dump emits it: the function
4076/// (or source-file) name, the compiled `Eprog` from `parse_string`,
4077/// and the FDHF_* autoload-style flag word.
4078#[allow(non_camel_case_types)]
4079#[derive(Debug, Clone)]
4080pub struct wcfunc {
4081    pub name: String, // c:3159
4082    /// Compiled program (`Eprog prog` c:3160) — wordcode + strs +
4083    /// npats as built by `bld_eprog`.
4084    pub prog: eprog, // c:3160
4085    pub flags: u32,   // c:3161
4086}
4087
4088/// Port of `dump_find_func(Wordcode h, char *name)` from
4089/// `Src/parse.c:3167`. Walks the header table inside a loaded
4090/// dump for a function with the given basename; returns the
4091/// matching `fdhead` record (C returns the `FDHead` pointer).
4092pub fn dump_find_func(h: &[u32], name: &str) -> Option<fdhead> {
4093    // c:3167
4094    let header_words = fdheaderlen(h) as usize;
4095    let end = header_words; // walking u32 offsets, end-exclusive
4096    let mut cur = firstfdhead_offset();
4097    while cur < end {
4098        if let Some(fh) = read_fdhead(h, cur) {
4099            let full = fdname(h, cur);
4100            let tail = fdhtail(&fh) as usize;
4101            let basename = if tail <= full.len() {
4102                &full[tail..]
4103            } else {
4104                ""
4105            };
4106            if basename == name {
4107                return Some(fh); // c:3173 `return n;`
4108            }
4109            cur = nextfdhead_offset(h, cur);
4110        } else {
4111            break;
4112        }
4113    }
4114    None // c:3175
4115}
4116
4117/// Port of `bin_zcompile(char *nam, char **args, Options ops, UNUSED(int func))`
4118/// from `Src/parse.c:3180`. Validates the option set, then dispatches
4119/// to one of: `-t` (test/list), `-c`/`-a` (dump current functions),
4120/// or the default (compile source files to `.zwc`).
4121pub fn bin_zcompile(
4122    nam: &str, // c:3180
4123    args: &[String],
4124    ops: &crate::ported::zsh_h::options,
4125    _func: i32,
4126) -> i32 {
4127    // c:3185-3192 — illegal-combination guard.
4128    if (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z'))
4129        || (OPT_ISSET(ops, b'R') && OPT_ISSET(ops, b'M'))
4130        || (OPT_ISSET(ops, b'c')
4131            && (OPT_ISSET(ops, b'U') || OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'z')))
4132        || (!(OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) && OPT_ISSET(ops, b'm'))
4133    {
4134        zwarnnam(nam, "illegal combination of options"); // c:3192
4135        return 1;
4136    }
4137
4138    // c:3194 — `-c`/`-a` + KSHAUTOLOAD warning.
4139    if (OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) && isset(crate::ported::zsh_h::KSHAUTOLOAD) {
4140        zwarnnam(nam, "functions will use zsh style autoloading"); // c:3195
4141    }
4142
4143    // c:3196-3197 — flag word from `-k` / `-z`.
4144    let flags: u32 = if OPT_ISSET(ops, b'k') {
4145        FDHF_KSHLOAD
4146    } else if OPT_ISSET(ops, b'z') {
4147        FDHF_ZSHLOAD
4148    } else {
4149        0
4150    };
4151
4152    // c:3199 — `-t` test/list mode.
4153    if OPT_ISSET(ops, b't') {
4154        // c:3199
4155        if args.is_empty() {
4156            zwarnnam(nam, "too few arguments"); // c:3202
4157            return 1;
4158        }
4159        let dump_name = if args[0].ends_with(FD_EXT) {
4160            args[0].clone()
4161        } else {
4162            format!("{}{}", args[0], FD_EXT)
4163        };
4164        let f = match load_dump_header(nam, &dump_name, 1) {
4165            // c:3206
4166            Some(buf) => buf,
4167            None => return 1,
4168        };
4169        // c:3209 — per-function check.
4170        if args.len() > 1 {
4171            for name in &args[1..] {
4172                // c:3210
4173                if dump_find_func(&f, name).is_none() {
4174                    // c:3212
4175                    return 1;
4176                }
4177            }
4178            return 0;
4179        }
4180        // c:3215-3221 — listing arm. Walk every fdhead, print
4181        // each function's full name. C uses `fdname(h)` which
4182        // includes the path prefix; matches our `fdname()` impl.
4183        let mapped = if (fdflags(&f) & FDF_MAP) != 0 {
4184            "mapped"
4185        } else {
4186            "read"
4187        };
4188        println!("zwc file ({}) for zsh-{}", mapped, fdversion(&f));
4189        let header_words = fdheaderlen(&f) as usize;
4190        let mut cur = firstfdhead_offset();
4191        while cur < header_words {
4192            if read_fdhead(&f, cur).is_none() {
4193                break;
4194            }
4195            println!("{}", fdname(&f, cur));
4196            cur = nextfdhead_offset(&f, cur);
4197        }
4198        return 0;
4199    }
4200
4201    if args.is_empty() {
4202        zwarnnam(nam, "too few arguments"); // c:3226
4203        return 1;
4204    }
4205
4206    // c:3228 — map mode discriminant.
4207    let map: i32 = if OPT_ISSET(ops, b'M') {
4208        2
4209    } else if OPT_ISSET(ops, b'R') {
4210        0
4211    } else {
4212        1
4213    };
4214
4215    // c:3230-3236 — single-file default-mode short path.
4216    if args.len() == 1 && !(OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) {
4217        let dump = format!("{}{}", args[0], FD_EXT);
4218        return build_dump(nam, &dump, args, OPT_ISSET(ops, b'U') as i32, map, flags);
4219    }
4220
4221    // c:3239-3247 — multi-file or `-c`/`-a` mode.
4222    let dump = if args[0].ends_with(FD_EXT) {
4223        args[0].clone()
4224    } else {
4225        format!("{}{}", args[0], FD_EXT)
4226    };
4227    let rest = &args[1..];
4228    if OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a') {
4229        let what =
4230            (if OPT_ISSET(ops, b'c') { 1 } else { 0 }) | (if OPT_ISSET(ops, b'a') { 2 } else { 0 });
4231        build_cur_dump(nam, &dump, rest, OPT_ISSET(ops, b'm') as i32, map, what)
4232    } else {
4233        build_dump(nam, &dump, rest, OPT_ISSET(ops, b'U') as i32, map, flags)
4234    }
4235}
4236
4237/// Port of `load_dump_header(char *nam, char *name, int err)` from
4238/// `Src/parse.c:3258`. Opens the file, reads + validates the magic
4239/// and version, then slurps the full header table into memory.
4240/// Returns the header u32-array on success or None on any failure
4241/// (emitting C-shaped warnings when `err != 0`).
4242pub fn load_dump_header(nam: &str, name: &str, err: i32) -> Option<Vec<u32>> {
4243    // c:3258
4244
4245    let mut f = match File::open(name) {
4246        // c:3263
4247        Ok(h) => h,
4248        Err(_) => {
4249            if err != 0 {
4250                zwarnnam(nam, &format!("can't open zwc file: {}", name)); // c:3265
4251            }
4252            return None;
4253        }
4254    };
4255
4256    // Read FD_PRELEN+1 u32 words = 52 bytes.
4257    let mut buf_bytes = vec![0u8; (FD_PRELEN + 1) * 4];
4258    if f.read_exact(&mut buf_bytes).is_err() {
4259        if err != 0 {
4260            zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3277
4261        }
4262        return None;
4263    }
4264    let mut buf: Vec<u32> = buf_bytes
4265        .chunks_exact(4)
4266        .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
4267        .collect();
4268
4269    // c:3270 — magic + version check against `ZSH_VERSION` (C global;
4270    // zshrs mirrors it in `patchlevel::ZSH_VERSION`).
4271    let magic_ok = fdmagic(&buf) == FD_MAGIC || fdmagic(&buf) == FD_OMAGIC;
4272    let v_ok = fdversion(&buf) == crate::ported::patchlevel::ZSH_VERSION;
4273    if !magic_ok {
4274        if err != 0 {
4275            zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3277
4276        }
4277        return None;
4278    }
4279    if !v_ok {
4280        if err != 0 {
4281            zwarnnam(
4282                nam,
4283                &format!(
4284                    "zwc file has wrong version (zsh-{}): {}", // c:3274
4285                    fdversion(&buf),
4286                    name
4287                ),
4288            );
4289        }
4290        return None;
4291    }
4292
4293    // c:3285 — if magic matches host byte order, head len is `pre[FD_PRELEN]`.
4294    // Else seek to `fdother(buf)` and re-read.
4295    if fdmagic(&buf) != FD_MAGIC {
4296        let other = fdother(&buf) as u64; // c:3290
4297        if f.seek(SeekFrom::Start(other)).is_err() || f.read_exact(&mut buf_bytes).is_err() {
4298            zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3295
4299            return None;
4300        }
4301        buf = buf_bytes
4302            .chunks_exact(4)
4303            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
4304            .collect();
4305    }
4306
4307    let total_words = fdheaderlen(&buf) as usize; // c:3286/3299
4308    if total_words < FD_PRELEN + 1 {
4309        zwarnnam(nam, &format!("invalid zwc file: {}", name));
4310        return None;
4311    }
4312
4313    // Read the remaining header words.
4314    let mut head: Vec<u32> = Vec::with_capacity(total_words);
4315    head.extend_from_slice(&buf);
4316    let remaining_words = total_words - (FD_PRELEN + 1);
4317    if remaining_words > 0 {
4318        let mut rest_bytes = vec![0u8; remaining_words * 4]; // c:3305
4319        if f.read_exact(&mut rest_bytes).is_err() {
4320            zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3307
4321            return None;
4322        }
4323        for c in rest_bytes.chunks_exact(4) {
4324            head.push(u32::from_le_bytes([c[0], c[1], c[2], c[3]]));
4325        }
4326    }
4327    Some(head) // c:3311
4328}
4329
4330/// Port of `fdswap(Wordcode p, int n)` from `Src/parse.c:3318`.
4331/// Byte-swap each u32 in `p[..n]` in place. Used when writing the
4332/// opposite-byte-order copy of a wordcode dump.
4333pub fn fdswap(p: &mut [u32]) {
4334    // c:3318
4335    for w in p.iter_mut() {
4336        *w = w.swap_bytes();
4337    }
4338}
4339
4340/// Port of `write_dump(int dfd, LinkList progs, int map, int hlen, int tlen)`
4341/// from `Src/parse.c:3334`. Writes the prelude + header records +
4342/// body wordcode bytes to the dump file descriptor.
4343///
4344/// Two passes: first native-byte-order (`FD_MAGIC`), then opposite-
4345/// byte-order (`FD_OMAGIC`) so big-endian readers can mmap the
4346/// same file. Bodies are byte-swapped via `fdswap` on the second pass.
4347pub fn write_dump(
4348    dfd: &mut File, // c:3334
4349    progs: &[wcfunc],
4350    mut map: i32,
4351    hlen: i32,
4352    tlen: i32,
4353) -> std::io::Result<()> {
4354    // c:3345-3346 — `if (map == 1) map = (tlen >= FD_MINMAP);`
4355    if map == 1 {
4356        map = ((tlen as usize) >= FD_MINMAP) as i32;
4357    }
4358
4359    // C `sizeof(Patprog)` — pointer size (see bld_eprog len arithmetic).
4360    let patprog_size = size_of::<*const u8>() as i32;
4361
4362    let mut other = 0u32; // c:3338
4363    let ohlen = hlen;
4364
4365    loop {
4366        // c:3349 — `for (ohlen = hlen; ; hlen = ohlen)`.
4367        let mut cur_hlen = ohlen;
4368        // c:3348 — `memset(pre, 0, sizeof(wordcode) * FD_PRELEN);`
4369        let mut pre = vec![0u32; FD_PRELEN];
4370        pre[0] = if other != 0 { FD_OMAGIC } else { FD_MAGIC }; // c:3350
4371        let flags = (if map != 0 { FDF_MAP } else { 0 }) | other;
4372        fdsetflags(&mut pre, flags as u8); // c:3351
4373        fdsetother(&mut pre, tlen as u32); // c:3352
4374                                           // c:3353 — copy ZSH_VERSION C-string into pre[2..].
4375        let ver = crate::ported::patchlevel::ZSH_VERSION.as_bytes();
4376        for (i, &b) in ver.iter().enumerate() {
4377            let word = 2 + i / 4;
4378            let shift = (i % 4) * 8;
4379            pre[word] |= (b as u32) << shift;
4380        }
4381        // c:3354 — write prelude.
4382        for w in &pre {
4383            dfd.write_all(&w.to_le_bytes())?;
4384        }
4385        // c:3356 — per-fn header records.
4386        for wcf in progs {
4387            let n = &wcf.name;
4388            let prog = &wcf.prog;
4389            // c:3362-3363 — body length in bytes excluding the
4390            // pattern-prog slots: `prog->len - (prog->npats *
4391            // sizeof(Patprog))`.
4392            let len_bytes = prog.len - prog.npats * patprog_size;
4393            let mut head = fdhead {
4394                start: cur_hlen as u32,   // c:3360
4395                len: len_bytes as u32,    // c:3363
4396                npats: prog.npats as u32, // c:3364
4397                // c:3365 — `head.strs = prog->strs - ((char *) prog->prog);`
4398                // In bld_eprog's layout strs sits right after the code
4399                // words, so the byte offset is ecused * 4.
4400                strs: (prog.prog.len() * 4) as u32, // c:3365
4401                hlen: ((FDHEAD_WORDS as u32) + ((n.len() as u32 + 4) / 4)), // c:3366
4402                flags: 0,
4403            };
4404            // c:3361 — `hlen += (prog->len - npats*sizeof(Patprog) +
4405            //                    sizeof(wordcode) - 1) / sizeof(wordcode);`
4406            cur_hlen += (len_bytes + 3) / 4;
4407            // c:3368-3371 — name tail offset from path basename.
4408            let tail = n.rfind('/').map(|p| p + 1).unwrap_or(0);
4409            head.flags = fdhbldflags(wcf.flags, tail as u32); // c:3372
4410                                                              // c:3373 — opposite-byte-order swap on second pass.
4411            let mut head_words: Vec<u32> = vec![
4412                head.start, head.len, head.npats, head.strs, head.hlen, head.flags,
4413            ];
4414            if other != 0 {
4415                fdswap(&mut head_words);
4416            }
4417            for w in &head_words {
4418                dfd.write_all(&w.to_le_bytes())?;
4419            }
4420            // c:3376-3379 — write the name + NUL, then pad to a word
4421            // boundary with the leading bytes of `head` (C: `write_loop
4422            // (dfd, (char *)&head, sizeof(wordcode) - tmp);`).
4423            dfd.write_all(n.as_bytes())?;
4424            dfd.write_all(&[0u8])?;
4425            let tmp = (n.len() + 1) & 3;
4426            if tmp != 0 {
4427                let head_bytes = head_words[0].to_le_bytes();
4428                dfd.write_all(&head_bytes[..4 - tmp])?;
4429            }
4430        }
4431        // c:3381-3388 — per-fn bodies: code words then the strs region,
4432        // padded to a word boundary. `tmp = (prog->len - npats*
4433        // sizeof(Patprog) + sizeof(wordcode) - 1) / sizeof(wordcode);
4434        // write_loop(dfd, (char *)prog->prog, tmp * sizeof(wordcode));`
4435        for wcf in progs {
4436            let prog = &wcf.prog;
4437            let len_bytes = (prog.len - prog.npats * patprog_size) as usize;
4438            let tmp = (len_bytes + 3) / 4;
4439            // c:3386-3387 — on the other pass only the code words are
4440            // swapped (`fdswap(prog->prog, ((Wordcode) prog->strs) -
4441            // prog->prog)`); the strs chars are byte-order neutral.
4442            let mut body_bytes: Vec<u8> = Vec::with_capacity(tmp * 4);
4443            for &w in &prog.prog {
4444                let w = if other != 0 { w.swap_bytes() } else { w };
4445                body_bytes.extend_from_slice(&w.to_le_bytes());
4446            }
4447            if let Some(s) = &prog.strs {
4448                body_bytes.extend_from_slice(s.as_bytes());
4449            }
4450            // C reads up to 3 bytes of heap slop past strs; emit NULs
4451            // (readers never look past head.len so the value is free).
4452            body_bytes.resize(tmp * 4, 0);
4453            dfd.write_all(&body_bytes)?;
4454        }
4455        if other != 0 {
4456            // c:3389
4457            break;
4458        }
4459        other = FDF_OTHER; // c:3391
4460    }
4461    Ok(())
4462}
4463
4464/// Port of `build_dump(char *nam, char *dump, char **files, int ali, int map, int flags)`
4465/// from `Src/parse.c:3396`. Source-file → wordcode dump compiler:
4466/// parses each source file via `parse_string` and serializes the
4467/// resulting `Eprog`s through `write_dump` into `<dump>.zwc`.
4468pub fn build_dump(
4469    nam: &str, // c:3397
4470    dump: &str,
4471    files: &[String],
4472    ali: i32,
4473    map: i32,
4474    flags: u32,
4475) -> i32 {
4476    use crate::ported::utils::{errflag, ERRFLAG_ERROR};
4477    use std::os::unix::fs::OpenOptionsExt;
4478    use std::sync::atomic::Ordering;
4479
4480    // c:3403-3404 — append FD_EXT unless already suffixed.
4481    let dump: String = if dump.ends_with(FD_EXT) {
4482        dump.to_string()
4483    } else {
4484        format!("{}{}", dump, FD_EXT)
4485    };
4486
4487    // c:3406 — `unlink(dump);`
4488    let _ = fs::remove_file(&dump);
4489    // c:3407-3410 — `open(dump, O_WRONLY|O_CREAT, 0444)`.
4490    let mut dfd = match fs::OpenOptions::new()
4491        .write(true)
4492        .create(true)
4493        .mode(0o444)
4494        .open(&dump)
4495    {
4496        Ok(f) => f,
4497        Err(_) => {
4498            zwarnnam(nam, &format!("can't write zwc file: {}", dump)); // c:3408
4499            return 1;
4500        }
4501    };
4502
4503    let patprog_size = size_of::<*const u8>() as i32; // C sizeof(Patprog)
4504    let ona = crate::ported::lex::noaliases(); // c:3398 `ona = noaliases`
4505    crate::ported::lex::set_noaliases(ali != 0); // c:3412 `noaliases = ali;`
4506
4507    let mut progs: Vec<wcfunc> = Vec::new(); // c:3411
4508    let mut flags = flags;
4509    let mut hlen = FD_PRELEN as i32; // c:3414
4510    let mut tlen: i32 = 0;
4511
4512    for fname in files {
4513        // c:3418-3425 — `-k` / `-z` pseudo-args flip the autoload style.
4514        if check_cond(fname, "k") {
4515            flags = (flags & !(FDHF_KSHLOAD | FDHF_ZSHLOAD)) | FDHF_KSHLOAD; // c:3419
4516            continue;
4517        } else if check_cond(fname, "z") {
4518            flags = (flags & !(FDHF_KSHLOAD | FDHF_ZSHLOAD)) | FDHF_ZSHLOAD; // c:3422
4519            continue;
4520        }
4521        // c:3426-3437 — open + fstat + S_ISREG + read the whole file.
4522        let fnam = crate::ported::utils::unmeta(fname); // c:3426
4523        let is_reg = fs::metadata(&fnam).map(|m| m.is_file()).unwrap_or(false);
4524        let bytes = if is_reg { fs::read(&fnam).ok() } else { None };
4525        let bytes = match bytes {
4526            Some(b) => b,
4527            None => {
4528                zwarnnam(nam, &format!("can't open file: {}", fname)); // c:3432
4529                crate::ported::lex::set_noaliases(ona); // c:3433
4530                let _ = fs::remove_file(&dump); // c:3434
4531                return 1;
4532            }
4533        };
4534        // c:3450 — `file = metafy(file, flen, META_REALLOC);` — keep
4535        // raw bytes intact through the &str boundary (see bld_eprog's
4536        // from_utf8_unchecked rationale).
4537        let raw = unsafe { String::from_utf8_unchecked(bytes) };
4538        let file = crate::ported::utils::metafy(&raw);
4539
4540        // c:3452-3460 — parse; any error aborts the whole dump.
4541        let prog = crate::ported::exec::parse_string(&file, 1);
4542        let errored = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
4543        let prog = match prog {
4544            Some(p) if !errored => p,
4545            _ => {
4546                errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:3453
4547                zwarnnam(nam, &format!("can't read file: {}", fname)); // c:3456
4548                crate::ported::lex::set_noaliases(ona); // c:3457
4549                let _ = fs::remove_file(&dump); // c:3458
4550                return 1;
4551            }
4552        };
4553
4554        // c:3467-3472 — accumulate header + body length budgets.
4555        let flen = (fname.len() as i32 + 4) / 4; // c:3469
4556        hlen += (FDHEAD_WORDS as i32) + flen; // c:3470
4557        tlen += (prog.len - prog.npats * patprog_size + 3) / 4; // c:3471
4558
4559        // c:3463-3466 — wcfunc node.
4560        let wcf_flags = if (prog.flags & EF_RUN) != 0 {
4561            FDHF_KSHLOAD // c:3465
4562        } else {
4563            flags
4564        };
4565        progs.push(wcfunc {
4566            name: fname.clone(),
4567            prog,
4568            flags: wcf_flags,
4569        });
4570    }
4571    crate::ported::lex::set_noaliases(ona); // c:3474
4572
4573    let tlen = (tlen + hlen) * 4; // c:3476
4574
4575    // c:3478 — `write_dump(dfd, progs, map, hlen, tlen);` (void in C).
4576    let _ = write_dump(&mut dfd, &progs, map, hlen, tlen);
4577
4578    0 // c:3482
4579}
4580
4581/// Port of `cur_add_func(char *nam, Shfunc shf, LinkList names, LinkList progs, int *hlen, int *tlen, int what)`
4582/// from `Src/parse.c:3489`. Adds a shfunc to the in-build dump
4583/// progs+names lists. Stub: `Eprog` for the function body isn't
4584/// yet wired through `shfunc.funcdef` to be serializable here.
4585pub fn cur_add_func(
4586    nam: &str, // c:3489
4587    shf_name: &str,
4588    shf_flags: i32,
4589    names: &mut Vec<String>,
4590    progs: &mut Vec<wcfunc>,
4591    hlen: &mut i32,
4592    tlen: &mut i32,
4593    what: i32,
4594) -> i32 {
4595    let is_undef = (shf_flags as u32 & PM_UNDEFINED) != 0;
4596    if is_undef {
4597        if (what & 2) == 0 {
4598            // c:3498
4599            zwarnnam(nam, &format!("function is not loaded: {}", shf_name));
4600            return 1;
4601        }
4602        // c:3503 — would call `getfpfunc` to load body for dump.
4603        zwarnnam(nam, &format!("can't load function: {}", shf_name));
4604        return 1;
4605    } else if (what & 1) == 0 {
4606        zwarnnam(nam, &format!("function is already loaded: {}", shf_name)); // c:3514
4607        return 1;
4608    }
4609    // c:3517 — would `dupeprog(shf->funcdef)`. Stub: empty program.
4610    let wcf = wcfunc {
4611        name: shf_name.to_string(),
4612        flags: FDHF_ZSHLOAD,
4613        prog: eprog::default(),
4614    };
4615    progs.push(wcf);
4616    names.push(shf_name.to_string());
4617
4618    // c:3526 — bump hlen / tlen.
4619    let name_words = (shf_name.len() as i32 + 4) / 4;
4620    *hlen += (FDHEAD_WORDS as i32) + name_words;
4621    *tlen += 0; // body is empty in stub; real path adds prog->len in words.
4622
4623    0
4624}
4625
4626/// Port of `build_cur_dump(char *nam, char *dump, char **names, int match, int map, int what)`
4627/// from `Src/parse.c:3536`. Serializes the currently-loaded shell
4628/// functions (`-c` → `what & 1`) and/or autoloadable ones (`-a` →
4629/// `what & 2`) into a `.zwc` dump. Shares `write_dump` with the
4630/// source-file variant `build_dump`.
4631///
4632/// C keeps the per-function collection in a static `cur_add_func`
4633/// helper (`Src/parse.c:3489`). The build gate forbids adding
4634/// Rust-only helper fns under `src/ported/`, and the sibling
4635/// `cur_add_func` in this file is a divergent stub that emits an
4636/// empty program, so the faithful collection logic is inlined here
4637/// (see the `for (name, flags, funcdef, body) in candidates` loop).
4638///
4639/// zshrs divergence: C parses every function into `shf->funcdef`
4640/// (wordcode) at definition time, so `dupeprog(shf->funcdef, 1)`
4641/// always has a program to copy. zshrs defers the compile — a
4642/// loaded user function stores its source in `shf.body` with
4643/// `funcdef == None` (`Src/exec.c:5540-5545`). The emitter therefore
4644/// falls back to `parse_string(body, 1)` to obtain the same wordcode
4645/// `Eprog` C would have had eagerly, using the exact substrate
4646/// `build_dump` feeds to `write_dump`.
4647pub fn build_cur_dump(
4648    nam: &str, // c:3536
4649    dump: &str,
4650    names: &[String],
4651    match_: i32,
4652    map: i32,
4653    what: i32,
4654) -> i32 {
4655    use crate::ported::utils::ERRFLAG_ERROR;
4656    use std::os::unix::fs::OpenOptionsExt;
4657    use std::sync::atomic::Ordering;
4658
4659    // c:3542-3543 — `if (!strsfx(FD_EXT, dump)) dump = dyncat(dump, FD_EXT);`
4660    let dump: String = if dump.ends_with(FD_EXT) {
4661        dump.to_string()
4662    } else {
4663        format!("{}{}", dump, FD_EXT)
4664    };
4665
4666    // c:3545 — `unlink(dump);`
4667    let _ = fs::remove_file(&dump);
4668    // c:3546-3549 — `open(dump, O_WRONLY|O_CREAT, 0444)`.
4669    let mut dfd = match fs::OpenOptions::new()
4670        .write(true)
4671        .create(true)
4672        .mode(0o444)
4673        .open(&dump)
4674    {
4675        Ok(f) => f,
4676        Err(_) => {
4677            zwarnnam(nam, &format!("can't write zwc file: {}", dump)); // c:3547
4678            return 1;
4679        }
4680    };
4681
4682    let patprog_size = size_of::<*const u8>() as i32; // C `sizeof(Patprog)`
4683
4684    // c:3551-3555 — `progs`/`lnames` lists, `hlen = FD_PRELEN`, `tlen = 0`.
4685    let mut progs: Vec<wcfunc> = Vec::new();
4686    let mut lnames: Vec<String> = Vec::new();
4687    let mut hlen = FD_PRELEN as i32;
4688    let mut tlen: i32 = 0;
4689
4690    // Per-function candidates gathered from `shfunctab` before the
4691    // program serialization pass. Held as owned data so the table's
4692    // read lock is released before `getfpfunc` (which re-locks the
4693    // table on the PM_UNDEFINED autoload path) runs.
4694    let mut candidates: Vec<(String, i32, Option<eprog>, Option<String>)> = Vec::new();
4695
4696    if names.is_empty() {
4697        // c:3557-3567 — no names: dump every function in the table.
4698        let tab = crate::ported::hashtable::shfunctab_lock()
4699            .read()
4700            .expect("shfunctab poisoned");
4701        for (fname, shf) in tab.iter() {
4702            lnames.push(fname.clone()); // c:3529 addlinknode(names, ...)
4703            candidates.push((
4704                fname.clone(),
4705                shf.node.flags,
4706                shf.funcdef.as_deref().cloned(),
4707                shf.body.clone(),
4708            ));
4709        }
4710    } else if match_ != 0 {
4711        // c:3568-3597 — pattern match against the whole table per arg.
4712        for pat_name in names {
4713            // c:3577 — `tokenize(pat = dupstring(*names));`
4714            let mut tok = pat_name.clone();
4715            crate::ported::glob::tokenize(&mut tok);
4716            // c:3579 — `patcompile(pat, PAT_STATIC, NULL)`.
4717            let pprog = match crate::ported::pattern::patcompile(
4718                &tok,
4719                crate::ported::zsh_h::PAT_STATIC,
4720                None::<&mut String>,
4721            ) {
4722                Some(p) => p,
4723                None => {
4724                    zwarnnam(nam, &format!("bad pattern: {}", pat_name)); // c:3581
4725                    let _ = fs::remove_file(&dump); // c:3583
4726                    return 1;
4727                }
4728            };
4729            let tab = crate::ported::hashtable::shfunctab_lock()
4730                .read()
4731                .expect("shfunctab poisoned");
4732            // c:3586-3595 — for each function not already collected that
4733            // matches the pattern, add it. `lnames` is the dedup list
4734            // (C: `!linknodebydatum(lnames, hn->nam)`).
4735            for (fname, shf) in tab.iter() {
4736                if !lnames.contains(fname) && crate::ported::pattern::pattry(&pprog, fname) {
4737                    lnames.push(fname.clone());
4738                    candidates.push((
4739                        fname.clone(),
4740                        shf.node.flags,
4741                        shf.funcdef.as_deref().cloned(),
4742                        shf.body.clone(),
4743                    ));
4744                }
4745            }
4746            drop(tab);
4747        }
4748    } else {
4749        // c:3598-3617 — explicit names: each must resolve to a function.
4750        for fname in names {
4751            let errored = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
4752            let tab = crate::ported::hashtable::shfunctab_lock()
4753                .read()
4754                .expect("shfunctab poisoned");
4755            match (errored, tab.get(fname)) {
4756                // c:3600-3601 — `if (errflag || !(shf = getnode(*names)))`.
4757                (false, Some(shf)) => {
4758                    lnames.push(fname.clone());
4759                    candidates.push((
4760                        fname.clone(),
4761                        shf.node.flags,
4762                        shf.funcdef.as_deref().cloned(),
4763                        shf.body.clone(),
4764                    ));
4765                }
4766                _ => {
4767                    drop(tab);
4768                    zwarnnam(nam, &format!("unknown function: {}", fname)); // c:3603
4769                    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:3604
4770                    let _ = fs::remove_file(&dump); // c:3606
4771                    return 1;
4772                }
4773            }
4774        }
4775    }
4776
4777    // c:3489-3534 — `cur_add_func` inlined: resolve each candidate to a
4778    // wordcode `Eprog` and accumulate the header/body length budgets.
4779    for (fname, flags, funcdef, body) in candidates {
4780        let prog: eprog = if (flags & PM_UNDEFINED as i32) != 0 {
4781            // c:3495-3512 — autoload stub: only dumpable with `-a`.
4782            if (what & 2) == 0 {
4783                zwarnnam(nam, &format!("function is not loaded: {}", fname)); // c:3499
4784                errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
4785                let _ = fs::remove_file(&dump);
4786                return 1;
4787            }
4788            // c:3502 — `noaliases = (shf->node.flags & PM_UNALIASED);`
4789            let ona = crate::ported::lex::noaliases();
4790            crate::ported::lex::set_noaliases(
4791                (flags & crate::ported::zsh_h::PM_UNALIASED as i32) != 0,
4792            );
4793            // c:3503 — `getfpfunc(shf->node.nam, NULL, NULL, NULL, 0)`.
4794            let mut dir_out: Option<String> = None;
4795            let mut dump_out: Option<(eprog, i32)> = None;
4796            let found =
4797                crate::ported::exec::getfpfunc(&fname, &mut dir_out, None, 0, &mut dump_out);
4798            crate::ported::lex::set_noaliases(ona); // c:3506 / c:3511
4799            let loaded: Option<eprog> = match dump_out {
4800                // c:3509-3510 — `if (prog->dump) prog = dupeprog(prog, 1);`
4801                Some((p, _)) => Some(if p.dump.is_some() { dupeprog(&p, true) } else { p }),
4802                None => match found {
4803                    // zshrs's `getfpfunc` only materializes an `Eprog`
4804                    // for `.zwc` digest hits; a plain autoload source
4805                    // comes back as a path. C's `getfpfunc` parses the
4806                    // source itself — reproduce that with the same
4807                    // read + `parse_string` path `build_dump` uses.
4808                    Some(path) => {
4809                        let fnam = crate::ported::utils::unmeta(&path);
4810                        match fs::read(&fnam) {
4811                            Ok(bytes) => {
4812                                let raw = unsafe { String::from_utf8_unchecked(bytes) };
4813                                let file = crate::ported::utils::metafy(&raw);
4814                                crate::ported::exec::parse_string(&file, 1)
4815                            }
4816                            Err(_) => None,
4817                        }
4818                    }
4819                    None => None,
4820                },
4821            };
4822            match loaded {
4823                Some(p) => p,
4824                None => {
4825                    zwarnnam(nam, &format!("can't load function: {}", fname)); // c:3505
4826                    errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
4827                    let _ = fs::remove_file(&dump);
4828                    return 1;
4829                }
4830            }
4831        } else {
4832            // c:3513-3518 — loaded function: dump only with `-c`.
4833            if (what & 1) == 0 {
4834                zwarnnam(nam, &format!("function is already loaded: {}", fname)); // c:3515
4835                errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
4836                let _ = fs::remove_file(&dump);
4837                return 1;
4838            }
4839            // c:3517 — `prog = dupeprog(shf->funcdef, 1);`. In zshrs a
4840            // loaded function usually carries its source in `body` with
4841            // `funcdef == None` (deferred compile); compile it now to
4842            // recover the wordcode C stored eagerly.
4843            match funcdef {
4844                Some(fd) => dupeprog(&fd, true),
4845                None => match body {
4846                    Some(b) => match crate::ported::exec::parse_string(&b, 1) {
4847                        Some(p) => p,
4848                        None => {
4849                            zwarnnam(nam, &format!("can't load function: {}", fname));
4850                            errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
4851                            let _ = fs::remove_file(&dump);
4852                            return 1;
4853                        }
4854                    },
4855                    // Empty-bodied function (no funcdef, no source):
4856                    // emit a zero-length program, matching the
4857                    // degenerate `Eprog` C would carry for `f() { }`.
4858                    None => eprog::default(),
4859                },
4860            }
4861        };
4862
4863        // c:3521-3527 — build the wcfunc node.
4864        let wcf_flags = if (prog.flags & EF_RUN) != 0 {
4865            FDHF_KSHLOAD // c:3526
4866        } else {
4867            FDHF_ZSHLOAD // c:3526
4868        };
4869        // c:3531-3534 — accumulate header + body word budgets.
4870        hlen += (FDHEAD_WORDS as i32) + ((fname.len() as i32 + 4) / 4); // c:3531-3532
4871        tlen += (prog.len - prog.npats * patprog_size + 3) / 4; // c:3533-3534
4872        progs.push(wcfunc {
4873            name: fname,
4874            prog,
4875            flags: wcf_flags,
4876        });
4877    }
4878
4879    // c:3619-3625 — `if (empty(progs)) { zwarnnam(nam, "no functions"); ... }`
4880    if progs.is_empty() {
4881        zwarnnam(nam, "no functions"); // c:3620
4882        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:3621
4883        let _ = fs::remove_file(&dump); // c:3623
4884        return 1;
4885    }
4886
4887    // c:3626 — `tlen = (tlen + hlen) * sizeof(wordcode);`
4888    let tlen = (tlen + hlen) * 4;
4889
4890    // c:3628 — `write_dump(dfd, progs, map, hlen, tlen);`
4891    let _ = write_dump(&mut dfd, &progs, map, hlen, tlen);
4892
4893    // c:3630 — `close(dfd);` (Rust: `dfd` drops here). Keep `lnames`
4894    // referenced — it mirrors C's parallel names list.
4895    let _ = lnames;
4896
4897    0 // c:3632
4898}
4899
4900/// Port of `zwcstat(char *filename, struct stat *buf)` from
4901/// `Src/parse.c:3656`. Stats a `.zwc` file, falling back to
4902/// `.zwc.old` if the primary doesn't exist (zsh uses the `.old`
4903/// suffix to keep a previous dump readable while a rewrite is in
4904/// progress).
4905pub fn zwcstat(filename: &str) -> Option<fs::Metadata> {
4906    // c:3656
4907    if let Ok(m) = fs::metadata(filename) {
4908        return Some(m);
4909    }
4910    let old = format!("{}.old", filename);
4911    fs::metadata(&old).ok()
4912}
4913
4914/// Port of `load_dump_file(char *dump, struct stat *sbuf, int other, int len)`
4915/// from `Src/parse.c:3675`. Reads (or mmap()'s) a complete `.zwc`
4916/// file into memory. Returns the u32 buffer or None on I/O error.
4917pub fn load_dump_file(
4918    dump: &str, // c:3675
4919    _sbuf: &fs::Metadata,
4920    other: i32,
4921    _len: usize,
4922) -> Option<Vec<u32>> {
4923    let mut f = File::open(dump).ok()?;
4924    if other != 0 {
4925        f.seek(SeekFrom::Start(other as u64)).ok()?;
4926    }
4927    let mut bytes = Vec::new();
4928    f.read_to_end(&mut bytes).ok()?;
4929    Some(
4930        bytes
4931            .chunks_exact(4)
4932            .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
4933            .collect(),
4934    )
4935}
4936
4937/// Port of `try_dump_file(char *path, char *name, char *file, int *ksh, int test_only)`
4938/// from `Src/parse.c:3746`. Tries to load function `name` from a
4939/// `.zwc` digest (`<path>.zwc`) or per-function compiled file
4940/// (`<file>.zwc`) when each is newer than its uncompiled source.
4941pub fn try_dump_file(
4942    path: &str,
4943    name: &str,
4944    file: &str, // c:3746
4945    test_only: bool,
4946) -> Option<(eprog, i32)> {
4947    use std::fs;
4948
4949    // c:3753-3758 — if path ends in .zwc, treat as direct digest.
4950    if path.ends_with(FD_EXT) {
4951        crate::ported::signals::queue_signals();
4952        let result = fs::metadata(path)
4953            .ok()
4954            .and_then(|m| check_dump_file(path, &m, name, test_only));
4955        unqueue_signals();
4956        return result;
4957    }
4958
4959    // c:3759-3760 — dig = "<path>.zwc", wc = "<file>.zwc".
4960    let dig = format!("{}{}", path, FD_EXT);
4961    let wc = format!("{}{}", file, FD_EXT);
4962
4963    // c:3762-3764 — zwcstat(dig, &std); stat(wc, &stc); stat(file, &stn);
4964    let std_meta = fs::metadata(&dig);
4965    let stc_meta = fs::metadata(&wc);
4966    let stn_meta = fs::metadata(file);
4967
4968    crate::ported::signals::queue_signals();
4969
4970    // zsh compares `st_mtime` (time_t SECONDS) with `>=` (c:3772-3780). Use
4971    // second-granularity mtime (MetadataExt::mtime, imported at top) — NOT
4972    // SystemTime (`.modified()`), whose nanosecond precision disagrees with zsh
4973    // whenever the dump and source share a second but differ in nsec, making
4974    // autoload .zwc preference flaky.
4975
4976    // c:3771-3777 — use the digest when it is >= both the per-fn .zwc and the
4977    // source (or those are absent).
4978    if let Ok(std_m) = &std_meta {
4979        let dig_s = std_m.mtime();
4980        let dig_ge_wc = stc_meta.as_ref().map_or(true, |c| dig_s >= c.mtime()); // c:3772
4981        let dig_ge_src = stn_meta.as_ref().map_or(true, |n| dig_s >= n.mtime()); // c:3773
4982        if dig_ge_wc && dig_ge_src {
4983            if let Some(prog) = check_dump_file(&dig, std_m, name, test_only) {
4984                unqueue_signals();
4985                return Some(prog);
4986            }
4987        }
4988    }
4989
4990    // c:3779-3784 — try per-function .zwc when it is >= the source (or absent).
4991    if let Ok(stc_m) = &stc_meta {
4992        let wc_s = stc_m.mtime();
4993        let src_newer_or_missing = stn_meta.as_ref().map_or(true, |n| wc_s >= n.mtime()); // c:3780
4994        if src_newer_or_missing {
4995            if let Some(prog) = check_dump_file(&wc, stc_m, name, test_only) {
4996                unqueue_signals();
4997                return Some(prog);
4998            }
4999        }
5000    }
5001
5002    unqueue_signals(); // c:3787
5003    None // c:3788
5004}
5005
5006/// Port of `try_source_file(char *file)` from `Src/parse.c:3795`.
5007/// Returns an Eprog (the wordcode dump body) if `<file>.zwc` exists
5008/// and is newer than `<file>`, else None. The dump entry searched is
5009/// the file's basename (`tail`), matching how `zcompile` names
5010/// source-file entries.
5011pub fn try_source_file(file: &str) -> Option<eprog> {
5012    // c:3795
5013
5014    // c:3802-3805 — if ((tail = strrchr(file, '/'))) tail++; else tail = file;
5015    let tail = match file.rfind('/') {
5016        Some(i) => &file[i + 1..],
5017        None => file,
5018    };
5019
5020    // c:3807-3812 — if (strsfx(FD_EXT, file)) { ... return check_dump_file(file, NULL, tail, NULL, 0); }
5021    if file.ends_with(FD_EXT) {
5022        crate::ported::signals::queue_signals(); // c:3808
5023        let meta = fs::metadata(file);
5024        let prog = match meta {
5025            Ok(m) => check_dump_file(file, &m, tail, false).map(|(p, _)| p), // c:3809
5026            Err(_) => None,
5027        };
5028        unqueue_signals(); // c:3810
5029        return prog;
5030    }
5031
5032    // c:3813 — wc = dyncat(file, FD_EXT);
5033    let wc = format!("{}{}", file, FD_EXT);
5034
5035    // c:3815-3816 — rc = stat(wc, &stc); rn = stat(file, &stn);
5036    let stc = fs::metadata(&wc);
5037    let stn = fs::metadata(file);
5038
5039    crate::ported::signals::queue_signals(); // c:3818
5040                                             // c:3819-3823 — if (!rc && (rn || stc.st_mtime >= stn.st_mtime) && (prog = check_dump_file(...))) return prog;
5041    if let Ok(meta_c) = &stc {
5042        // c:3819 — `stc.st_mtime >= stn.st_mtime`: second-granularity (time_t,
5043        // via MetadataExt::mtime imported at top), not SystemTime nsec, to agree
5044        // with zsh on equal-second boundaries.
5045        let newer_than_src = match (&stc, &stn) {
5046            (Ok(c), Ok(n)) => c.mtime() >= n.mtime(),
5047            (Ok(_), Err(_)) => true, // c:3819 — `rn` (src missing) ⇒ accept .zwc
5048            _ => false,
5049        };
5050        if newer_than_src {
5051            let prog = check_dump_file(&wc, meta_c, tail, false); // c:3820
5052            if let Some((p, _)) = prog {
5053                unqueue_signals(); // c:3821
5054                return Some(p); // c:3822
5055            }
5056        }
5057    }
5058    unqueue_signals(); // c:3824
5059    None // c:3825
5060}
5061
5062/// Port of `Eprog check_dump_file(char *file, struct stat *sbuf,
5063/// char *name, int *ksh, int test_only)` from `Src/parse.c:3833`.
5064/// Walks the `dumps` mmap list looking for `(dev, ino)` matching
5065/// `sbuf`; on miss, calls `load_dump_header` to read the .zwc
5066/// header. Then `dump_find_func(d, name)` locates the function
5067/// table entry. Returns the wordcode slice + ksh-load flag.
5068///
5069/// ```c
5070/// Eprog
5071/// check_dump_file(char *file, struct stat *sbuf, char *name,
5072///                 int *ksh, int test_only)
5073/// {
5074///     int isrec = 0;
5075///     Wordcode d;
5076///     FDHead h;
5077///     FuncDump f;
5078///     struct stat lsbuf;
5079///     if (!sbuf) {
5080///         if (zwcstat(file, &lsbuf)) return NULL;
5081///         sbuf = &lsbuf;
5082///     }
5083///   rec:
5084///     d = NULL;
5085///     for (f = dumps; f; f = f->next)
5086///         if (f->dev == sbuf->st_dev && f->ino == sbuf->st_ino)
5087///             { d = f->map; break; }
5088///     if (!f && (isrec || !(d = load_dump_header(NULL, file, 0))))
5089///         return NULL;
5090///     if ((h = dump_find_func(d, name))) {
5091///         if (test_only) return &dummy_eprog;
5092///         /* allocate Eprog from f->map at h offset, incrdumpcount,
5093///            return prog */
5094///     }
5095///     return NULL;
5096/// }
5097/// ```
5098/// Rust port returns `Option<(eprog, i32)>` instead of the C
5099/// `Eprog` pointer + `*ksh` out-param: tuple element 0 is the
5100/// loaded program (wordcode + string table per the fdhead record),
5101/// element 1 is the ksh-load mode exactly as C writes `*ksh`:
5102/// `FDHF_KSHLOAD → 2`, `FDHF_ZSHLOAD → 0`, neither → `1`
5103/// (c:3954-3956).
5104pub fn check_dump_file(
5105    // c:3833
5106    file: &str,
5107    sbuf: &fs::Metadata,
5108    name: &str,
5109    test_only: bool,
5110) -> Option<(eprog, i32)> {
5111    use std::os::unix::fs::MetadataExt;
5112
5113    // c:3842-3846 — `if (!sbuf) { zwcstat(file, &lsbuf); sbuf = &lsbuf; }`
5114    // Rust takes sbuf by &Metadata — never null.
5115    let dev = sbuf.dev(); // c:3859
5116    let ino = sbuf.ino(); // c:3859
5117
5118    // c:3854 — `d = NULL;`
5119    let mut d: Option<Vec<u32>> = None;
5120    let mut found_mmap = false; // c:3858 `for (f = dumps; f; ...)`
5121
5122    // c:3858-3862 — walk DUMPS for matching dev/ino.
5123    {
5124        let dumps_guard = DUMPS.lock().expect("dumps poisoned");
5125        for f in dumps_guard.iter() {
5126            // c:3858
5127            if f.dev == dev && f.ino == ino {
5128                // c:3859
5129                d = Some(f.map.clone()); // c:3860
5130                found_mmap = true;
5131                break; // c:3861
5132            }
5133        }
5134    }
5135
5136    // c:3870-3871 — `if (!f && (isrec || !(d = load_dump_header(NULL, file, 0)))) return NULL;`
5137    if !found_mmap {
5138        // c:3870
5139        match load_dump_header("", file, 0) {
5140            // c:3870 load_dump_header
5141            Some(loaded) => d = Some(loaded),
5142            None => return None, // c:3871
5143        }
5144    }
5145
5146    // c:3873 — `if ((h = dump_find_func(d, name)))`
5147    let dump = d?;
5148    let h = dump_find_func(&dump, name)?; // c:3873
5149
5150    // c:3876-3879 — `if (test_only) return &dummy_eprog;`
5151    if test_only {
5152        // c:3876
5153        return Some((eprog::default(), 0)); // c:3879 dummy
5154    }
5155
5156    // c:3954-3956 — `*ksh = ((fdhflags(h) & FDHF_KSHLOAD) ? 2 :
5157    //                        ((fdhflags(h) & FDHF_ZSHLOAD) ? 0 : 1));`
5158    let ksh = if (fdhflags(&h) & FDHF_KSHLOAD) != 0 {
5159        2
5160    } else if (fdhflags(&h) & FDHF_ZSHLOAD) != 0 {
5161        0
5162    } else {
5163        1
5164    };
5165
5166    // c:3919-3958 — the read (non-mmap) branch: open the file, seek
5167    // to the function's wordcode, read `h->len` bytes (wordcode +
5168    // string pool), and build an EF_REAL Eprog around it. zshrs has
5169    // no mmap'd EF_MAP variant — DUMPS entries store the file words
5170    // in `map`, so both branches funnel into the same read-and-copy.
5171    //
5172    //   if ((fd = open(file, O_RDONLY)) < 0 ||
5173    //       lseek(fd, ((h->start * sizeof(wordcode)) +
5174    //                  ((fdflags(d) & FDF_OTHER) ? fdother(d) : 0)), 0) < 0)
5175    //       return NULL;
5176    //   d = (Wordcode) zalloc(h->len + po);
5177    //   if (read(fd, ((char *) d) + po, h->len) != (int)h->len) return NULL;
5178    //   prog->flags = EF_REAL;
5179    //   prog->len = h->len + po;
5180    //   prog->npats = np = h->npats;
5181    //   prog->prog = (Wordcode) (((char *) d) + po);
5182    //   prog->strs = ((char *) prog->prog) + h->strs;
5183    let body_off = (h.start as u64) * 4
5184        + if (fdflags(&dump) & FDF_OTHER) != 0 {
5185            fdother(&dump) as u64 // c:3924
5186        } else {
5187            0
5188        };
5189    let mut f = File::open(file).ok()?; // c:3922
5190    f.seek(SeekFrom::Start(body_off)).ok()?; // c:3923
5191    let mut bytes = vec![0u8; h.len as usize];
5192    if f.read_exact(&mut bytes).is_err() {
5193        // c:3931 `read(...) != h->len`
5194        return None;
5195    }
5196    // `h->strs` is the byte offset of the string pool inside the read
5197    // region; everything before it is wordcode.
5198    let strs_off = (h.strs as usize).min(bytes.len());
5199    let prog_words: Vec<u32> = bytes[..strs_off]
5200        .chunks_exact(4)
5201        .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
5202        .collect();
5203    // SAFETY: same byte-not-char convention as `bld_eprog` (c:566) —
5204    // consumers index `strs` by byte offset and never require UTF-8.
5205    let strs_string = unsafe { String::from_utf8_unchecked(bytes[strs_off..].to_vec()) };
5206    let po = h.npats as usize * size_of::<*const u8>(); // c:3920
5207    let prog = eprog {
5208        flags: EF_REAL,                    // c:3941
5209        len: (h.len as usize + po) as i32, // c:3942
5210        npats: h.npats as i32,             // c:3943
5211        nref: 1,                           // c:3944
5212        pats: Vec::new(),                  // c:3945/3952 dummy_patprog1 fill
5213        prog: prog_words,                  // c:3946
5214        strs: Some(strs_string),           // c:3947
5215        shf: None,                         // c:3948
5216        dump: None,                        // c:3949
5217        strs_metafied: true, // .zwc pool read verbatim — still METAFIED (see eprog field doc)
5218    };
5219
5220    // c:3899 — incrdumpcount(f) on the mmap-cache hit path.
5221    if found_mmap {
5222        let dumps_guard = DUMPS.lock().expect("dumps poisoned");
5223        if let Some(f) = dumps_guard.iter().find(|f| f.dev == dev && f.ino == ino) {
5224            incrdumpcount(f); // c:3899
5225        }
5226    }
5227
5228    Some((prog, ksh)) // c:3958
5229}
5230
5231/// Port of `incrdumpcount(FuncDump f)` from `Src/parse.c:3970/4021`.
5232/// `f->count++;` — refcount-up a loaded dump entry. The Rust port
5233/// keys lookup by `filename` because Rust can't raw-pointer-compare
5234/// funcdump values inside a `Mutex<Vec<...>>`; same observable
5235/// effect (the count of the matching entry increments).
5236pub fn incrdumpcount(f: &funcdump) {
5237    // c:3970 — `f->count++;`
5238    if let Some(d) = DUMPS
5239        .lock()
5240        .unwrap()
5241        .iter_mut()
5242        .find(|d| d.filename.as_deref() == f.filename.as_deref())
5243    {
5244        d.count += 1; // c:3973
5245    }
5246}
5247
5248/// Port of `freedump(FuncDump f)` from `Src/parse.c:3976`. Public
5249/// helper for the rare external caller; locks the dumps mutex and
5250/// drops the entry with the given filename.
5251pub fn freedump(f: &funcdump) {
5252    // c:3976
5253    let mut g = DUMPS.lock().unwrap();
5254    if let Some(name) = f.filename.as_deref() {
5255        freedump_locked(&mut g, name);
5256    }
5257}
5258
5259/// Port of `decrdumpcount(FuncDump f)` from `Src/parse.c:3988/4026`.
5260/// `f->count--; if (!f->count) { unlink from dumps; freedump(f); }`.
5261pub fn decrdumpcount(f: &funcdump) {
5262    // c:3988
5263    let key = f.filename.clone();
5264    let mut g = DUMPS.lock().unwrap();
5265    let mut hit_zero: Option<String> = None;
5266    for d in g.iter_mut() {
5267        if d.filename == key {
5268            d.count -= 1; // c:3991
5269            if d.count == 0 {
5270                // c:3992
5271                hit_zero = d.filename.clone();
5272            }
5273            break;
5274        }
5275    }
5276    if let Some(name) = hit_zero {
5277        // c:3994-4001
5278        freedump_locked(&mut g, &name);
5279    }
5280}
5281
5282/// Port of `closedumps(void)` from `Src/parse.c:4008/4033`. Walks
5283/// `dumps` freeing every entry. Called on shell exit (exec.c:522).
5284pub fn closedumps() {
5285    // c:4008
5286    let mut g = DUMPS.lock().unwrap();
5287    g.clear(); // c:4011-4014 `while (dumps) { ... freedump(...); ... }`
5288}
5289
5290/// Port of `dump_autoload(char *nam, char *file, int on, Options ops, int func)`
5291/// from `Src/parse.c:4042`. Registers every function in a `.zwc`
5292/// for autoload via `shfunctab`.
5293pub fn dump_autoload(
5294    nam: &str,
5295    file: &str, // c:4042
5296    on: i32,
5297    ops: &crate::ported::zsh_h::options,
5298    func: i32,
5299) -> i32 {
5300    use crate::ported::zsh_h::shfunc;
5301    let mut ret = 0; // c:4047
5302
5303    // c:4049-4050 — if (!strsfx(FD_EXT, file)) file = dyncat(file, FD_EXT);
5304    let file_owned;
5305    let file = if !file.ends_with(FD_EXT) {
5306        file_owned = format!("{}{}", file, FD_EXT);
5307        file_owned.as_str()
5308    } else {
5309        file
5310    };
5311
5312    // c:4052-4053 — if (!(h = load_dump_header(nam, file, 1))) return 1;
5313    let h = match load_dump_header(nam, file, 1) {
5314        Some(buf) => buf,
5315        None => return 1,
5316    };
5317
5318    // c:4055-4056 — for (n = firstfdhead(h); n < e; n = nextfdhead(n))
5319    let hlen = fdheaderlen(&h) as usize; // c:4055
5320    let mut n_off = firstfdhead_offset();
5321    while n_off < hlen {
5322        let head = match read_fdhead(&h, n_off) {
5323            Some(hd) => hd,
5324            None => break,
5325        };
5326        // c:4057-4061 — shf = zshcalloc; shf->node.flags = on; ...addnode(fdname + fdhtail)
5327        let name_full = fdname(&h, n_off);
5328        let tail = fdhtail(&head) as usize;
5329        let basename: String = name_full.chars().skip(tail).collect();
5330        let mut shf = shfunc {
5331            node: crate::ported::zsh_h::hashnode {
5332                next: None,
5333                nam: basename.clone(),
5334                flags: on, // c:4058
5335            },
5336            filename: None,
5337            lineno: 0,
5338            funcdef: None,
5339            redir: None,
5340            sticky: None, // c:4060 NULL
5341            body: None,
5342        };
5343        // c:4059 — shf->funcdef = mkautofn(shf);  (placeholder Eprog ptr)
5344        let _ = crate::ported::builtin::mkautofn(&mut shf as *mut _);
5345        // c:4061 — shfunctab->addnode(...)
5346        let snapshot = shf.clone();
5347        {
5348            let mut tab = crate::ported::hashtable::shfunctab_lock()
5349                .write()
5350                .expect("shfunctab poisoned");
5351            tab.add(shf);
5352        }
5353        // c:4062-4063 — if (OPT_ISSET(ops,'X') && eval_autoload(...)) ret = 1;
5354        if OPT_ISSET(ops, b'X') {
5355            let mut shf_ref = snapshot;
5356            if crate::ported::builtin::eval_autoload(&mut shf_ref as *mut _, &basename, ops, func)
5357                != 0
5358            {
5359                ret = 1;
5360            }
5361        }
5362        n_off = nextfdhead_offset(&h, n_off);
5363    }
5364    let _ = nam;
5365    ret // c:4065
5366}
5367
5368/// Port of C `struct eccstr` (zsh.h:836) — the long-string dedup BST
5369/// node. The dedup-walk and cmp logic in `ecstrcode` is faithful to
5370/// parse.c:447-453 including the conditional cmp chain
5371/// (nfunc → hashval → strcmp), so corpus inputs where C's eccstr BST walk
5372/// finds-or-misses match get the same outcome on the Rust side.
5373struct EccstrNode {
5374    left: Option<Box<EccstrNode>>,
5375    right: Option<Box<EccstrNode>>,
5376    /// C-byte form of the string (single byte per char ≤ 0xff).
5377    /// Owned because Rust doesn't have C zsh's "stable pointers into
5378    /// the lexer's tokstr arena" — every tokstr lives as a fresh
5379    /// Rust String allocation.
5380    str: Vec<u8>,
5381    /// Wordcode-encoded offset: `(byte_offset << 2) | token_bit`.
5382    /// Same shape as `Eccstr::offs` (parse.c:459).
5383    offs: u32,
5384    /// Absolute byte offset in the final strs region (= `ecsoffs` at
5385    /// insert time). C `Eccstr::aoffs` (parse.c:464). copy_ecstr uses
5386    /// THIS for the write position — distinct from `offs` which is
5387    /// ecssub-relative and collides across funcdef scopes.
5388    aoffs: u32,
5389    /// `nfunc` snapshot at insert time. Per-function namespace key
5390    /// — top-level scripts use 0; each funcdef bumps it.
5391    nfunc: i32,
5392    /// Hash of `str` computed via zsh's `hasher` (hashtable.c:86).
5393    hashval: u32,
5394}
5395// === end AST relocation ===
5396
5397// Parser state lives in file-scope thread_locals:
5398//   - LEX_* (lexer side, matching Src/lex.c file-statics)
5399//   - ECBUF / ECLEN / ECUSED / ECNPATS / ECSOFFS / ECSSUB / ECNFUNC /
5400//     ECSTRS_INDEX / ECSTRS_REVERSE (wordcode-emission state, matching
5401//     Src/parse.c file-statics)
5402//
5403// Callers use the free-fn entry points directly:
5404//   crate::ported::parse::parse_init(input);
5405//   let prog = crate::ported::parse::parse();
5406
5407const MAX_RECURSION_DEPTH: usize = 500;
5408
5409/// Direct port of `struct parse_stack` at `Src/zsh.h:3099-3109`.
5410/// Used by `parse_context_save` / `parse_context_restore`
5411/// (parse.c:295-355) to snapshot per-parse-call state so a nested
5412/// parse (e.g. inside command substitution) doesn't clobber the
5413/// outer parse.
5414///
5415/// A second port of `struct parse_stack` exists at
5416/// `crate::ported::zsh_h::parse_stack` (zsh.h:1066) using canonical
5417/// Wordcode / Eccstr / `struct heredocs` types — that port is unused
5418/// today and will become authoritative when Phase 9b (PORT_PLAN.md)
5419/// wires wordcode emission. This local version uses the working-set
5420/// shapes (`Vec<HereDoc>`, stubbed wordcode fields) suited to zshrs's
5421/// pre-wordcode AST architecture; the consolidation happens in P9b.
5422#[allow(non_camel_case_types)]
5423#[derive(Debug, Default, Clone)]
5424pub struct parse_stack {
5425    // ── Direct port of struct parse_stack at zsh.h:3099-3109 ──
5426    /// Pending heredocs awaiting body collection (canonical C
5427    /// linked-list shape). C: `struct heredocs *hdocs` (zsh.h:3100).
5428    /// Mirrors `parse::HDOCS` thread_local across nested parses.
5429    pub hdocs: Option<Box<crate::ported::zsh_h::heredocs>>,
5430    /// !!! WARNING: NOT IN PARSE_STACK — Rust-only AST-glue !!!
5431    /// Snapshot of `lex::LEX_HEREDOCS` (the parallel Rust-only Vec
5432    /// carrying terminator / strip_tabs / quoted metadata).
5433    /// Saved/restored alongside the canonical `hdocs` so nested
5434    /// parses get a clean AST view. C's parse_stack has no analog
5435    /// because C tracks terminator metadata implicitly via tokstr.
5436    pub lex_heredocs: Vec<HereDoc>,
5437    /// C: `int incmdpos` (zsh.h:3102).
5438    pub incmdpos: bool,
5439    /// C: `int aliasspaceflag` (zsh.h:3103).
5440    pub aliasspaceflag: i32,
5441    /// C: `int incond` (zsh.h:3104).
5442    pub incond: i32,
5443    /// C: `int inredir` (zsh.h:3105).
5444    pub inredir: bool,
5445    /// C: `int incasepat` (zsh.h:3106).
5446    pub incasepat: i32,
5447    /// C: `int isnewlin` (zsh.h:3107).
5448    pub isnewlin: i32,
5449    /// C: `int infor` (zsh.h:3108).
5450    pub infor: i32,
5451    /// C: `int inrepeat_` (zsh.h:3109).
5452    pub inrepeat_: i32,
5453    /// C: `int intypeset` (zsh.h:3110).
5454    pub intypeset: bool,
5455    // ── Wordcode-buffer state — STUB until Phase 9b ──
5456    // C `Wordcode ecbuf` (zsh.h:3112) + `Eccstr ecstrs` (zsh.h:3113) +
5457    // `int eclen/ecused/ecnpats/ecsoffs/ecssub/ecnfunc` (zsh.h:3112-3114).
5458    // zshrs hasn't emitted wordcode yet — these fields exist to
5459    // preserve the C shape but read/write nothing until P9b lands.
5460    pub eclen: i32,
5461    pub ecused: i32,
5462    pub ecnpats: i32,
5463    pub ecbuf: Option<Vec<u32>>,
5464    pub ecstrs: Option<Vec<u8>>,
5465    pub ecsoffs: i32,
5466    pub ecssub: i32,
5467    pub ecnfunc: i32,
5468}
5469
5470// Old uppercase Rust-only `ParseStack` is gone. Compat alias so
5471// existing call sites (context.rs) keep resolving until the
5472// rename ripples through.
5473/// `ParseStack` type alias.
5474#[allow(non_camel_case_types)]
5475pub type ParseStack = parse_stack;
5476
5477/// `mod_export struct eprog dummy_eprog;` from `Src/parse.c:3066`.
5478/// Placeholder Eprog used by `shf->funcdef = &dummy_eprog;` in
5479/// builtin.c when clearing a stale autoload stub. Held in a Mutex
5480/// so `init_eprog` can set it once at shell startup.
5481pub static DUMMY_EPROG: std::sync::Mutex<eprog> = std::sync::Mutex::new(eprog {
5482    flags: 0,
5483    len: 0,
5484    npats: 0,
5485    nref: 0,
5486    prog: Vec::new(),
5487    strs: None,
5488    pats: Vec::new(),
5489    shf: None,
5490    dump: None,
5491    strs_metafied: false, // native pool — clean UTF-8
5492});
5493
5494/// Walk every ZshRedir in the program and, for any with a `heredoc_idx`,
5495/// pull the body+terminator out of `bodies` and stuff into `heredoc`.
5496/// `bodies[i]` corresponds to the i-th heredoc registered by the lexer
5497/// during scanning (in source order).
5498fn fill_heredoc_bodies(prog: &mut ZshProgram, bodies: &[HereDocInfo]) {
5499    for list in &mut prog.lists {
5500        fill_in_sublist(&mut list.sublist, bodies);
5501    }
5502}
5503
5504fn fill_in_sublist(sub: &mut ZshSublist, bodies: &[HereDocInfo]) {
5505    fill_in_pipe(&mut sub.pipe, bodies);
5506    if let Some(next) = &mut sub.next {
5507        fill_in_sublist(&mut next.1, bodies);
5508    }
5509}
5510
5511fn fill_in_pipe(pipe: &mut ZshPipe, bodies: &[HereDocInfo]) {
5512    fill_in_command(&mut pipe.cmd, bodies);
5513    if let Some(next) = &mut pipe.next {
5514        fill_in_pipe(next, bodies);
5515    }
5516}
5517
5518fn fill_in_command(cmd: &mut ZshCommand, bodies: &[HereDocInfo]) {
5519    match cmd {
5520        ZshCommand::Simple(s) => {
5521            for r in &mut s.redirs {
5522                if let Some(idx) = r.heredoc_idx {
5523                    if let Some(info) = bodies.get(idx) {
5524                        r.heredoc = Some(info.clone());
5525                    }
5526                }
5527            }
5528        }
5529        ZshCommand::Subsh(p) | ZshCommand::Cursh(p) => fill_heredoc_bodies(p, bodies),
5530        ZshCommand::FuncDef(f) => fill_heredoc_bodies(&mut f.body, bodies),
5531        ZshCommand::If(i) => {
5532            fill_heredoc_bodies(&mut i.cond, bodies);
5533            fill_heredoc_bodies(&mut i.then, bodies);
5534            for (c, b) in &mut i.elif {
5535                fill_heredoc_bodies(c, bodies);
5536                fill_heredoc_bodies(b, bodies);
5537            }
5538            if let Some(e) = &mut i.else_ {
5539                fill_heredoc_bodies(e, bodies);
5540            }
5541        }
5542        ZshCommand::While(w) | ZshCommand::Until(w) => {
5543            fill_heredoc_bodies(&mut w.cond, bodies);
5544            fill_heredoc_bodies(&mut w.body, bodies);
5545        }
5546        ZshCommand::For(f) => fill_heredoc_bodies(&mut f.body, bodies),
5547        ZshCommand::Case(c) => {
5548            for arm in &mut c.arms {
5549                fill_heredoc_bodies(&mut arm.body, bodies);
5550            }
5551        }
5552        ZshCommand::Repeat(r) => fill_heredoc_bodies(&mut r.body, bodies),
5553        ZshCommand::Time(Some(sublist)) => fill_in_sublist(sublist, bodies),
5554        ZshCommand::Try(t) => {
5555            fill_heredoc_bodies(&mut t.try_block, bodies);
5556            fill_heredoc_bodies(&mut t.always, bodies);
5557        }
5558        ZshCommand::Redirected(inner, redirs) => {
5559            for r in redirs {
5560                if let Some(idx) = r.heredoc_idx {
5561                    if let Some(info) = bodies.get(idx) {
5562                        r.heredoc = Some(info.clone());
5563                    }
5564                }
5565            }
5566            fill_in_command(inner, bodies);
5567        }
5568        ZshCommand::Time(None) | ZshCommand::Cond(_) | ZshCommand::Arith(_) => {}
5569    }
5570}
5571
5572/// If `list` is a Simple containing one word that ends in the
5573/// `<Inpar><Outpar>` token pair (the lexer-port encoding of `()`),
5574/// return the bare name. Used by `parse_program_until` to detect
5575/// `name() {body}` style function definitions where the lexer
5576/// hasn't split the `()` from the name.
5577/// Detect the `name() …` shape inside a Simple. Returns the function
5578/// name and (when the body was already inlined into the same Simple,
5579/// e.g. `foo() echo hi`) the rest of the words as the body's argv.
5580/// Returns None for non-funcdef shapes.
5581fn simple_name_with_inoutpar(list: &ZshList) -> Option<(Vec<String>, Vec<String>)> {
5582    if list.flags.async_ || list.sublist.next.is_some() {
5583        return None;
5584    }
5585    let pipe = &list.sublist.pipe;
5586    if pipe.next.is_some() {
5587        return None;
5588    }
5589    let simple = match &pipe.cmd {
5590        ZshCommand::Simple(s) => s,
5591        _ => return None,
5592    };
5593    if simple.words.is_empty() || !simple.assigns.is_empty() {
5594        return None;
5595    }
5596    let suffix = "\u{88}\u{8a}"; // Inpar + Outpar
5597                                 // Find the FIRST word ending in `()`. zsh accepts the
5598                                 // multi-name shorthand `fna fnb fnc() { body }` (parse.c:
5599                                 // par_funcdef wordlist) — words[0..i-1] are extra names,
5600                                 // words[i] is `lastname()`. Words after are the body argv
5601                                 // (one-line shorthand, `name() cmd args`).
5602    let par_idx = simple.words.iter().position(|w| w.ends_with(suffix))?;
5603    let mut names: Vec<String> = Vec::with_capacity(par_idx + 1);
5604    for w in &simple.words[..par_idx] {
5605        // Earlier names must be bare identifiers, NOT contain
5606        // tokens that imply they're not function names (no `()`,
5607        // no quotes, no expansions). zsh's lexer enforces this
5608        // at the wordlist level; we approximate by requiring the
5609        // word be an identifier-shaped token after untokenize.
5610        let bare = super::lex::untokenize(w);
5611        let valid = !bare.is_empty()
5612            && bare
5613                .chars()
5614                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '$');
5615        if !valid {
5616            return None;
5617        }
5618        names.push(bare);
5619    }
5620    let last = &simple.words[par_idx];
5621    let bare = &last[..last.len() - suffix.len()];
5622    if bare.is_empty() {
5623        return None;
5624    }
5625    names.push(super::lex::untokenize(bare));
5626    let rest = simple.words[par_idx + 1..].to_vec();
5627    Some((names, rest))
5628}
5629
5630/// Initialize parser state for a fresh parse of `input`.
5631/// Free-fn entry point — resets parser thread_locals and loads input.
5632pub fn parse_init(input: &str) {
5633    // Seed the option defaults the parser/lexer inspect. Real zsh
5634    // installs these via `install_emulation_defaults` (options.c:172)
5635    // at shell startup; zshrs's parse-only test entry path bypasses
5636    // init_main, so we mirror the `zsh` emulation defaults here.
5637    //
5638    // Under `cfg(test)` (lib unit tests share one process) we
5639    // unconditionally OVERWRITE these on every parse_init so cross-
5640    // test option pollution (a prior test that flipped one of these
5641    // and panicked before its restore ran) doesn't leak into the
5642    // parser's reserved-word recognition / one-liner detection.
5643    //
5644    // In the REAL shell this overwrite is WRONG: parse_init runs for
5645    // every -c string, eval, and cmd-subst body, so resetting
5646    // `posixbuiltins` here silently wiped `zshrs -o POSIX_BUILTINS`
5647    // and made `setopt posix_builtins` evaporate at the next parse
5648    // (A04redirect.ztst POSIX_BUILTINS chunks). C zsh's parser READS
5649    // options; it never writes them. Production seeds only entries
5650    // that are missing entirely (parse-only test paths that bypass
5651    // init_main still get the zsh-emulation defaults).
5652    let overwrite = cfg!(test);
5653    for (name, default) in [
5654        ("shortloops", true),
5655        ("shortrepeat", false),
5656        ("multifuncdef", true),
5657        ("aliasfuncdef", false),
5658        ("ignorebraces", false),
5659        ("cshjunkieloops", false),
5660        ("posixbuiltins", false),
5661        ("execopt", true),
5662        ("kshautoload", false),
5663        ("aliases", true),
5664    ] {
5665        if overwrite || crate::ported::options::opt_state_get(name).is_none() {
5666            crate::ported::options::opt_state_set(name, default);
5667        }
5668    }
5669    lex_init(input);
5670}
5671
5672/// P9b decoder (wordcode-pipeline variant): direct port of
5673/// `ecgetstr(Estate s, int dup, int *tokflag)` from
5674/// `Src/parse.c:2855-2890`. Reads a wordcode at `pc`, decodes the
5675/// encoded string back to owned String. Returns (string,
5676/// pc_after_consumed). Distinct from the existing `ecgetstr` (which
5677/// takes a separate strs buffer for text.rs) — this variant uses
5678/// the live ECSTRS_REVERSE HashMap populated at ecstrcode time.
5679pub fn ecgetstr_wordcode(buf: &[u32], pc: usize) -> (String, usize) {
5680    if pc >= buf.len() {
5681        return (String::new(), pc);
5682    }
5683    let c = buf[pc];
5684    let next = pc + 1;
5685    // parse.c:2862-2863 — empty-string sentinels.
5686    if c == 6 || c == 7 {
5687        return (String::new(), next);
5688    }
5689    // parse.c:2864-2871 — inline-packed short string.
5690    if (c & 2) != 0 {
5691        let b0 = ((c >> 3) & 0xff) as u8;
5692        let b1 = ((c >> 11) & 0xff) as u8;
5693        let b2 = ((c >> 19) & 0xff) as u8;
5694        let mut bytes: Vec<u8> = Vec::new();
5695        for b in [b0, b1, b2] {
5696            if b == 0 {
5697                break;
5698            }
5699            bytes.push(b);
5700        }
5701        return (String::from_utf8_lossy(&bytes).into_owned(), next);
5702    }
5703    // parse.c:2872-2873 — long string via offs lookup. Map value is
5704    // metafied Vec<u8>; convert back to display String. Unmetafy is
5705    // the caller's job (the wordcode-parity dumper does it; other
5706    // callers may want raw bytes).
5707    let s = ECSTRS_REVERSE
5708        .with_borrow(|m| m.get(&c).cloned())
5709        .map(|v| String::from_utf8_lossy(&v).into_owned())
5710        .unwrap_or_default();
5711    (s, next)
5712}
5713
5714/// Parse the complete input. Direct port of `parse_event` /
5715/// `par_list` from `Src/parse.c:614-720`. On syntax error,
5716/// sets `errflag |= ERRFLAG_ERROR` (via `zerr`) and returns the
5717/// partial program — callers check `errflag` to detect failure,
5718/// matching C's `Eprog parse_event(...)` + `if (errflag) {...}`.
5719pub fn parse() -> ZshProgram {
5720    zshlex();
5721
5722    let mut program = parse_program_until(None, false);
5723
5724    // Post-pass: wire heredoc bodies (collected by the inline NEWLIN
5725    // walk in zshlex into LEX_HEREDOCS) back into ZshRedir.heredoc
5726    // fields via heredoc_idx. No C analog — LEX_HEREDOCS is the
5727    // Rust-only AST-glue Vec.
5728    let bodies: Vec<HereDocInfo> = LEX_HEREDOCS
5729        .with_borrow(|v| v.clone())
5730        .into_iter()
5731        .map(|h| HereDocInfo {
5732            content: h.content,
5733            terminator: h.terminator,
5734            quoted: h.quoted,
5735        })
5736        .collect();
5737    if !bodies.is_empty() {
5738        fill_heredoc_bodies(&mut program, &bodies);
5739    }
5740
5741    program
5742}
5743
5744/// Wordcode-emission top-level driver. Closest C analog is
5745/// `parse_list(void)` at `Src/parse.c:697-712`: init_parse +
5746/// zshlex + par_list(&c) + bld_eprog. This entry omits init_parse
5747/// and bld_eprog (caller responsibilities) and inlines a guard
5748/// loop around par_list_wordcode for cases where the lexer leaves
5749/// a non-ENDINPUT terminator (LEXERR, missing close-token, etc.).
5750pub fn par_event_wordcode() -> usize {
5751    let start = ECUSED.get() as usize;
5752    // C `parse_list` (parse.c:697-712) calls par_list ONCE — par_list's
5753    // own goto-rec loop handles all SEPER-separated sublists. The
5754    // outer loop here exists for safety against early-return cases
5755    // (LEXERR, missing terminator) but normally par_list_wordcode
5756    // consumes everything in one call.
5757    let mut cmplx: i32 = 0;
5758    while tok() != ENDINPUT && tok() != LEXERR {
5759        par_list_wordcode(&mut cmplx);
5760        match tok() {
5761            SEMI | NEWLIN | AMPER | AMPERBANG | SEPER => {
5762                zshlex();
5763            }
5764            _ => break,
5765        }
5766    }
5767    // No trailing `ecadd(WCB_END())` here: C's `par_list` (c:769)
5768    // emits none — the single terminating `WCB_END` comes from
5769    // `bld_eprog` (c:555). Emitting one here too made every program
5770    // one word longer than C's (double END), breaking .zwc dump
5771    // byte-parity with `zcompile`.
5772    start
5773}
5774
5775/// Port of `par_list(int *cmplx)` from `Src/parse.c:769-803`.
5776/// `list : { SEPER } [ sublist [ { SEPER | AMPER | AMPERBANG } list ] ]`.
5777/// True line-by-line port: takes `cmplx: &mut i32` matching C's
5778/// `int *cmplx` out-parameter, uses stack-local `c` per iteration
5779/// like C (so inner sublist cmplx is independent of outer).
5780pub fn par_list_wordcode(cmplx: &mut i32) {
5781    // c:773 — `int p, lp = -1, c;`
5782    let mut p: usize;
5783    let mut lp: i32 = -1;
5784    let mut c: i32;
5785    loop {
5786        // c:775 `rec:` — c:777-778 `while (tok == SEPER) zshlex();`
5787        while tok() == SEPER {
5788            zshlex();
5789        }
5790        // c:780 — `p = ecadd(0);`
5791        p = ecadd(0);
5792        // c:781 — `c = 0;`
5793        c = 0;
5794        // c:783 — `if (par_sublist(&c)) { ... }`
5795        if par_sublist_wordcode(&mut c) {
5796            // c:784 — `*cmplx |= c;`
5797            *cmplx |= c;
5798            // c:785 — `if (tok == SEPER || tok == AMPER || tok == AMPERBANG)`
5799            let t = tok();
5800            if t == SEPER || t == AMPER || t == AMPERBANG {
5801                // c:786-787 — `if (tok != SEPER) *cmplx = 1;`
5802                if t != SEPER {
5803                    *cmplx = 1;
5804                }
5805                // c:788-790 — `set_list_code(p, ..., c);`
5806                let z = if t == SEPER {
5807                    Z_SYNC
5808                } else if t == AMPER {
5809                    Z_ASYNC
5810                } else {
5811                    Z_ASYNC | Z_DISOWN
5812                };
5813                set_list_code(p, z, c != 0);
5814                // c:791 — `incmdpos = 1;`
5815                set_incmdpos(true);
5816                // c:792-794 — `do { zshlex(); } while (tok == SEPER);`
5817                loop {
5818                    zshlex();
5819                    if tok() != SEPER {
5820                        break;
5821                    }
5822                }
5823                // c:795 — `lp = p;` c:796 — `goto rec;`
5824                lp = p as i32;
5825                continue;
5826            } else {
5827                // c:798 — `set_list_code(p, (Z_SYNC | Z_END), c);`
5828                set_list_code(p, Z_SYNC | Z_END, c != 0);
5829            }
5830        } else {
5831            // c:800-802 — `ecused--; if (lp >= 0) ecbuf[lp] |= wc_bdata(Z_END);`
5832            ECUSED.set((ECUSED.get() - 1).max(0));
5833            if lp >= 0 {
5834                ECBUF.with_borrow_mut(|b| {
5835                    if (lp as usize) < b.len() {
5836                        b[lp as usize] |= wc_bdata(Z_END as wordcode);
5837                    }
5838                });
5839            }
5840        }
5841        break;
5842    }
5843}
5844
5845/// Port of `par_list1(int *cmplx)` from `Src/parse.c:806-817`.
5846/// Single-sublist variant used by funcdef bodies and the short
5847/// `for`/`while`/`repeat` forms — exactly one sublist with
5848/// `Z_SYNC|Z_END`, no chain.
5849pub fn par_list1_wordcode(cmplx: &mut i32) {
5850    // c:810 — `int p = ecadd(0), c = 0;`
5851    let p = ecadd(0);
5852    let mut c: i32 = 0;
5853    // c:812 — `if (par_sublist(&c)) { ... }`
5854    if par_sublist_wordcode(&mut c) {
5855        // c:813 — `set_list_code(p, (Z_SYNC | Z_END), c);`
5856        set_list_code(p, Z_SYNC | Z_END, c != 0);
5857        // c:814 — `*cmplx |= c;`
5858        *cmplx |= c;
5859    } else {
5860        // c:816 — `ecused--;`
5861        ECUSED.set((ECUSED.get() - 1).max(0));
5862    }
5863}
5864
5865/// Port of `par_save_list(C)` macro from `Src/parse.c:475-480`.
5866///   do { int eu = ecused; par_list(C); if (eu == ecused) ecadd(WCB_END()); } while (0)
5867pub fn par_save_list_wordcode(cmplx: &mut i32) {
5868    let eu = ECUSED.get();
5869    par_list_wordcode(cmplx);
5870    if ECUSED.get() == eu {
5871        ecadd(WCB_END());
5872    }
5873}
5874
5875/// Port of `par_save_list1(C)` macro from `Src/parse.c:481-486`.
5876pub fn par_save_list1_wordcode(cmplx: &mut i32) {
5877    let eu = ECUSED.get();
5878    par_list1_wordcode(cmplx);
5879    if ECUSED.get() == eu {
5880        ecadd(WCB_END());
5881    }
5882}
5883
5884/// Port of `par_sublist(int *cmplx)` from `Src/parse.c:823-865`.
5885/// `sublist : sublist2 [ ( DBAR | DAMPER ) { SEPER } sublist ]`.
5886/// Emits a WCB_SUBLIST header, recurses into par_sublist2 for
5887/// the !/coproc prefix + pipeline, then chains via DBAR (`||`)
5888/// or DAMPER (`&&`) recursively. Returns true if at least one
5889/// pipeline was emitted.
5890pub fn par_sublist_wordcode(cmplx: &mut i32) -> bool {
5891    // c:827 — `int f, p, c = 0;`
5892    let mut c: i32 = 0;
5893    // c:829 — `p = ecadd(0);`
5894    let p = ecadd(0);
5895    // c:831 — `if ((f = par_sublist2(&c)) != -1) { ... }`
5896    match par_sublist2(&mut c) {
5897        Some(f) => {
5898            // c:832 — `int e = ecused;`
5899            let e = ECUSED.get() as usize;
5900            // c:834 — `*cmplx |= c;`
5901            *cmplx |= c;
5902            if tok() == DBAR || tok() == DAMPER {
5903                // c:836 — `enum lextok qtok = tok;`
5904                let qtok = tok();
5905                // c:839 — `cmdpush(tok == DBAR ? CS_CMDOR : CS_CMDAND);`
5906                cmdpush(if qtok == DBAR {
5907                    CS_CMDOR as u8
5908                } else {
5909                    CS_CMDAND as u8
5910                });
5911                // c:840 — `zshlex();`
5912                zshlex();
5913                // c:841-842 — `while (tok == SEPER) zshlex();`
5914                while tok() == SEPER {
5915                    zshlex();
5916                }
5917                // c:843 — `sl = par_sublist(cmplx);`
5918                let sl = par_sublist_wordcode(cmplx);
5919                // c:844-847 — `set_sublist_code(p, (sl ? ... : WC_SUBLIST_END),
5920                // f, (e - 1 - p), c);`
5921                let st = if sl {
5922                    if qtok == DBAR {
5923                        WC_SUBLIST_OR
5924                    } else {
5925                        WC_SUBLIST_AND
5926                    }
5927                } else {
5928                    WC_SUBLIST_END
5929                };
5930                set_sublist_code(p, st as i32, f, (e - 1 - p) as i32, c != 0);
5931                // c:848 — `cmdpop();`
5932                cmdpop();
5933            } else {
5934                // c:850-853 — `if (tok == AMPER || tok == AMPERBANG)
5935                // { c = 1; *cmplx |= c; }`
5936                if tok() == AMPER || tok() == AMPERBANG {
5937                    c = 1;
5938                    *cmplx |= c;
5939                }
5940                // c:854 — `set_sublist_code(p, WC_SUBLIST_END, f,
5941                // (e - 1 - p), c);`
5942                set_sublist_code(p, WC_SUBLIST_END as i32, f, (e - 1 - p) as i32, c != 0);
5943            }
5944            // c:856 — `return 1;`
5945            true
5946        }
5947        None => {
5948            // c:858-859 — `ecused--; return 0;`
5949            ECUSED.set((ECUSED.get() - 1).max(0));
5950            false
5951        }
5952    }
5953}
5954
5955/// Port of `par_pline(int *cmplx)` from `Src/parse.c:894-955`.
5956/// `pline : cmd [ ( BAR | BARAMP ) { SEPER } pline ]`. Emits a
5957/// WCB_PIPE header (mid for chain links, end for the last cmd)
5958/// plus the optional BARAMP `2>&1` synthetic redir.
5959/// Port of `par_pline(int *cmplx)` from `Src/parse.c:893-947`.
5960/// (Named `par_pipe_wordcode` to disambiguate from the AST
5961/// `par_pline` at parse.rs:3744 — semantically the same `pline`
5962/// production.)
5963pub fn par_pipe_wordcode(cmplx: &mut i32) -> bool {
5964    // c:897 — `zlong line = toklineno;`
5965    let line = toklineno() as i64;
5966    // c:899 — `p = ecadd(0);`
5967    let p = ecadd(0);
5968    // c:901-904 — `if (!par_cmd(cmplx, 0)) { ecused--; return 0; }`
5969    if !par_cmd_wordcode(cmplx, 0) {
5970        ECUSED.set((ECUSED.get() - 1).max(0));
5971        return false;
5972    }
5973    if tok() == BAR_TOK {
5974        // c:906 — `*cmplx = 1;`
5975        *cmplx = 1;
5976        // c:907 — `cmdpush(CS_PIPE);`
5977        cmdpush(CS_PIPE as u8);
5978        // c:908 — `zshlex();`
5979        zshlex();
5980        // c:909-910 — `while (tok == SEPER) zshlex();`
5981        while tok() == SEPER {
5982            zshlex();
5983        }
5984        // c:911 — `ecbuf[p] = WCB_PIPE(WC_PIPE_MID, line>=0 ? line+1 : 0);`
5985        ECBUF.with_borrow_mut(|b| {
5986            if p < b.len() {
5987                b[p] = WCB_PIPE(
5988                    WC_PIPE_MID,
5989                    if line >= 0 { (line + 1) as wordcode } else { 0 },
5990                );
5991            }
5992        });
5993        // c:912 — `ecispace(p+1, 1);`
5994        ecispace(p + 1, 1);
5995        // c:913 — `ecbuf[p+1] = ecused - 1 - p;`
5996        let used = ECUSED.get() as usize;
5997        ECBUF.with_borrow_mut(|b| {
5998            if p + 1 < b.len() {
5999                b[p + 1] = (used.saturating_sub(1 + p)) as wordcode;
6000            }
6001        });
6002        // c:914-916 — `if (!par_pline(cmplx)) { tok = LEXERR; }`
6003        if !par_pipe_wordcode(cmplx) {
6004            set_tok(LEXERR);
6005        }
6006        // c:917 — `cmdpop();`
6007        cmdpop();
6008        true
6009    } else if tok() == BARAMP {
6010        // c:920-923 — walk past inline WC_REDIR to find r.
6011        let mut r = p + 1;
6012        loop {
6013            let code = ECBUF.with_borrow(|b| b.get(r).copied().unwrap_or(0));
6014            if wc_code(code) != WC_REDIR {
6015                break;
6016            }
6017            r += WC_REDIR_WORDS(code) as usize;
6018        }
6019        // c:925-928 — `ecispace(r, 3);` + synthetic `2>&1` redir
6020        ecispace(r, 3);
6021        ECBUF.with_borrow_mut(|b| {
6022            if r + 2 < b.len() {
6023                b[r] = WCB_REDIR(REDIR_MERGEOUT as wordcode);
6024                b[r + 1] = 2;
6025                b[r + 2] = ecstrcode("1");
6026            }
6027        });
6028        // c:930 — `*cmplx = 1;`
6029        *cmplx = 1;
6030        cmdpush(CS_ERRPIPE as u8);
6031        zshlex();
6032        while tok() == SEPER {
6033            zshlex();
6034        }
6035        ECBUF.with_borrow_mut(|b| {
6036            if p < b.len() {
6037                b[p] = WCB_PIPE(
6038                    WC_PIPE_MID,
6039                    if line >= 0 { (line + 1) as wordcode } else { 0 },
6040                );
6041            }
6042        });
6043        ecispace(p + 1, 1);
6044        let used = ECUSED.get() as usize;
6045        ECBUF.with_borrow_mut(|b| {
6046            if p + 1 < b.len() {
6047                b[p + 1] = (used.saturating_sub(1 + p)) as wordcode;
6048            }
6049        });
6050        if !par_pipe_wordcode(cmplx) {
6051            set_tok(LEXERR);
6052        }
6053        cmdpop();
6054        true
6055    } else {
6056        // c:944 — `ecbuf[p] = WCB_PIPE(WC_PIPE_END, line>=0 ? line+1 : 0);`
6057        ECBUF.with_borrow_mut(|b| {
6058            if p < b.len() {
6059                b[p] = WCB_PIPE(
6060                    WC_PIPE_END,
6061                    if line >= 0 { (line + 1) as wordcode } else { 0 },
6062                );
6063            }
6064        });
6065        true
6066    }
6067}
6068
6069/// Port of `par_cmd(int *cmplx, int zsh_construct)` from
6070/// `Src/parse.c:958-1085`. Parses leading + trailing redirs and
6071/// dispatches on the current token to the right par_* builder.
6072/// Returns false only when no command was emitted (no redirs +
6073/// par_simple returned 0).
6074/// Port of `par_cmd(int *cmplx, int zsh_construct)` from
6075/// `Src/parse.c:957-1077`.
6076pub fn par_cmd_wordcode(cmplx: &mut i32, zsh_construct: i32) -> bool {
6077    // c:960 — `int r, nr = 0;`
6078    let mut nr: i32 = 0;
6079    // c:962 — `r = ecused;`
6080    let mut r: usize = ECUSED.get() as usize;
6081    // c:964-968 — leading redirs.
6082    if IS_REDIROP(tok()) {
6083        // c:965 — `*cmplx = 1;`
6084        *cmplx = 1;
6085        // c:966-968 — `while (IS_REDIROP(tok)) { nr += par_redir(&r, NULL); }`
6086        while IS_REDIROP(tok()) {
6087            nr += par_redir_wordcode(&mut r, None);
6088        }
6089    }
6090    // c:970-1066 — token-dispatch switch.
6091    match tok() {
6092        FOR => {
6093            cmdpush(CS_FOR as u8);
6094            par_for_wordcode(cmplx);
6095            cmdpop();
6096        }
6097        FOREACH => {
6098            cmdpush(CS_FOREACH as u8);
6099            par_for_wordcode(cmplx);
6100            cmdpop();
6101        }
6102        SELECT => {
6103            // c:982 — `*cmplx = 1;`
6104            *cmplx = 1;
6105            cmdpush(CS_SELECT as u8);
6106            par_for_wordcode(cmplx);
6107            cmdpop();
6108        }
6109        CASE => {
6110            cmdpush(CS_CASE as u8);
6111            par_case_wordcode(cmplx);
6112            cmdpop();
6113        }
6114        IF => {
6115            par_if_wordcode(cmplx);
6116        }
6117        WHILE => {
6118            cmdpush(CS_WHILE as u8);
6119            par_while_wordcode(cmplx);
6120            cmdpop();
6121        }
6122        UNTIL => {
6123            cmdpush(CS_UNTIL as u8);
6124            par_while_wordcode(cmplx);
6125            cmdpop();
6126        }
6127        REPEAT => {
6128            cmdpush(CS_REPEAT as u8);
6129            par_repeat_wordcode(cmplx);
6130            cmdpop();
6131        }
6132        INPAR_TOK => {
6133            // c:1011 — `*cmplx = 1;`
6134            *cmplx = 1;
6135            cmdpush(CS_SUBSH as u8);
6136            par_subsh_wordcode(cmplx, zsh_construct);
6137            cmdpop();
6138        }
6139        INBRACE_TOK => {
6140            cmdpush(CS_CURSH as u8);
6141            par_subsh_wordcode(cmplx, zsh_construct);
6142            cmdpop();
6143        }
6144        FUNC => {
6145            cmdpush(CS_FUNCDEF as u8);
6146            par_funcdef_wordcode(cmplx);
6147            cmdpop();
6148        }
6149        DINBRACK => {
6150            cmdpush(CS_COND as u8);
6151            par_cond_wordcode();
6152            cmdpop();
6153        }
6154        DINPAR => {
6155            par_arith_wordcode();
6156        }
6157        TIME => {
6158            // c:1037-1050 — `static int inpartime` guard so
6159            // `time time foo` doesn't recurse infinitely.
6160            if !PARSER_INPARTIME.with(|c| c.get()) {
6161                // c:1041 — `*cmplx = 1;`
6162                *cmplx = 1;
6163                PARSER_INPARTIME.with(|c| c.set(true));
6164                par_time_wordcode();
6165                PARSER_INPARTIME.with(|c| c.set(false));
6166            } else {
6167                set_tok(STRING_LEX);
6168                let sr = par_simple_wordcode(cmplx, nr);
6169                if sr == 0 && nr == 0 {
6170                    return false;
6171                }
6172                if sr > 1 {
6173                    *cmplx = 1;
6174                    r += (sr - 1) as usize;
6175                }
6176            }
6177        }
6178        _ => {
6179            // c:1054 — `if (!(sr = par_simple(cmplx, nr)))`
6180            let sr = par_simple_wordcode(cmplx, nr);
6181            if sr == 0 {
6182                if nr == 0 {
6183                    return false;
6184                }
6185            } else if sr > 1 {
6186                // c:1060-1061 — `*cmplx = 1; r += sr - 1;`
6187                *cmplx = 1;
6188                r += (sr - 1) as usize;
6189            }
6190        }
6191    }
6192    // c:1067-1071 — trailing redirs.
6193    // c:1067 — `if (IS_REDIROP(tok)) { *cmplx = 1; while (...) (void)par_redir(&r, NULL); }`
6194    if IS_REDIROP(tok()) {
6195        *cmplx = 1;
6196        while IS_REDIROP(tok()) {
6197            let _ = par_redir_wordcode(&mut r, None);
6198        }
6199    }
6200    // c:1072-1075 — `incmdpos=1; incasepat=0; incond=0; intypeset=0;`
6201    set_incmdpos(true);
6202    set_incasepat(0);
6203    set_incond(0);
6204    set_intypeset(false);
6205    let _ = r;
6206    // c:1076 — `return 1;`
6207    true
6208}
6209
6210/// Port of `par_for(int *cmplx)` from `Src/parse.c:1086-1198`.
6211pub fn par_for_wordcode(cmplx: &mut i32) {
6212    // c:1089 — `int oecused = ecused, csh = (tok == FOREACH), p, sel = (tok == SELECT);`
6213    let _oecused = ECUSED.get() as usize;
6214    let csh = tok() == FOREACH;
6215    let sel = tok() == SELECT;
6216    let p: usize;
6217    // c:1090 — `int type;`
6218    let r#type: wordcode;
6219
6220    // c:1092 — `p = ecadd(0);`
6221    p = ecadd(0);
6222
6223    // c:1094 — `incmdpos = 0;`
6224    set_incmdpos(false);
6225    // c:1095 — `infor = tok == FOR ? 2 : 0;`
6226    set_infor(if tok() == FOR { 2 } else { 0 });
6227    // c:1096 — `zshlex();`
6228    zshlex();
6229    // c:1097 — `if (tok == DINPAR) {`
6230    if tok() == DINPAR {
6231        // c:1098 — `zshlex();`
6232        zshlex();
6233        // c:1099-1100 — `if (tok != DINPAR) YYERRORV(oecused);`
6234        if tok() != DINPAR {
6235            zerr("par_for: expected init");
6236            return;
6237        }
6238        // c:1101 — `ecstr(tokstr);`
6239        ecstr(&tokstr().unwrap_or_default());
6240        // c:1102 — `zshlex();`
6241        zshlex();
6242        // c:1103-1104
6243        if tok() != DINPAR {
6244            zerr("par_for: expected cond");
6245            return;
6246        }
6247        // c:1105
6248        ecstr(&tokstr().unwrap_or_default());
6249        // c:1106
6250        zshlex();
6251        // c:1107-1108
6252        if tok() != DOUTPAR {
6253            zerr("par_for: expected ))");
6254            return;
6255        }
6256        // c:1109
6257        ecstr(&tokstr().unwrap_or_default());
6258        // c:1110 — `infor = 0;`
6259        set_infor(0);
6260        // c:1111 — `incmdpos = 1;`
6261        set_incmdpos(true);
6262        // c:1112 — `zshlex();`
6263        zshlex();
6264        // c:1113 — `type = WC_FOR_COND;`
6265        r#type = WC_FOR_COND;
6266    } else {
6267        // c:1115 — `int np = 0, n, posix_in, ona = noaliases, onc = nocorrect;`
6268        let mut np: usize = 0;
6269        let mut n: u32;
6270        let posix_in: bool;
6271        let ona = noaliases();
6272        let onc = nocorrect();
6273        // c:1116 — `infor = 0;`
6274        set_infor(0);
6275        // c:1117-1118 — `if (tok != STRING || !isident(tokstr)) YYERRORV(oecused);`
6276        if tok() != STRING_LEX || !crate::ported::params::isident(&tokstr().unwrap_or_default()) {
6277            zerr("par_for: expected identifier");
6278            return;
6279        }
6280        // c:1119-1120 — `if (!sel) np = ecadd(0);`
6281        if !sel {
6282            np = ecadd(0);
6283        }
6284        // c:1121 — `n = 0;`
6285        n = 0;
6286        // c:1122 — `incmdpos = 1;`
6287        set_incmdpos(true);
6288        // c:1123 — `noaliases = nocorrect = 1;`
6289        set_noaliases(true);
6290        set_nocorrect(1);
6291        // c:1124 — `for (;;) {`
6292        loop {
6293            // c:1125 — `n++;`
6294            n += 1;
6295            // c:1126 — `ecstr(tokstr);`
6296            ecstr(&tokstr().unwrap_or_default());
6297            // c:1127 — `zshlex();`
6298            zshlex();
6299            // c:1128-1129 — `if (tok != STRING || !strcmp(tokstr, "in") || sel) break;`
6300            if tok() != STRING_LEX || tokstr().as_deref() == Some("in") || sel {
6301                break;
6302            }
6303            // c:1130-1135 — `if (!isident(tokstr) || errflag) { ... YYERRORV; }`
6304            if !crate::ported::params::isident(&tokstr().unwrap_or_default())
6305                || (errflag.load(Ordering::Relaxed) & 1) != 0
6306            {
6307                set_noaliases(ona);
6308                set_nocorrect(onc);
6309                zerr("par_for: expected identifier in name list");
6310                return;
6311            }
6312        }
6313        // c:1137-1138 — `noaliases = ona; nocorrect = onc;`
6314        set_noaliases(ona);
6315        set_nocorrect(onc);
6316        // c:1139-1140 — `if (!sel) ecbuf[np] = n;`
6317        if !sel {
6318            ECBUF.with_borrow_mut(|b| {
6319                b[np] = n;
6320            });
6321        }
6322        // c:1141 — `posix_in = isnewlin;`
6323        posix_in = isnewlin() != 0;
6324        // c:1142-1143 — `while (isnewlin) zshlex();`
6325        while isnewlin() != 0 {
6326            zshlex();
6327        }
6328        // c:1144 — `if (tok == STRING && !strcmp(tokstr, "in")) {`
6329        if tok() == STRING_LEX && tokstr().as_deref() == Some("in") {
6330            // c:1145 — `incmdpos = 0;`
6331            set_incmdpos(false);
6332            // c:1146 — `zshlex();`
6333            zshlex();
6334            // c:1147 — `np = ecadd(0);`
6335            np = ecadd(0);
6336            // c:1148 — `n = par_wordlist();`
6337            let n2 = par_wordlist_wordcode();
6338            // c:1149-1150 — `if (tok != SEPER) YYERRORV(oecused);`
6339            if tok() != SEPER {
6340                zerr("par_for: expected separator after `in`");
6341                return;
6342            }
6343            // c:1151 — `ecbuf[np] = n;`
6344            ECBUF.with_borrow_mut(|b| {
6345                b[np] = n2 as wordcode;
6346            });
6347            // c:1152 — `type = (sel ? WC_SELECT_LIST : WC_FOR_LIST);`
6348            r#type = if sel { WC_SELECT_LIST } else { WC_FOR_LIST };
6349        } else if !posix_in && tok() == INPAR_TOK {
6350            // c:1153-1154 — `else if (!posix_in && tok == INPAR)`
6351            // c:1154 — `incmdpos = 0;`
6352            set_incmdpos(false);
6353            // c:1155 — `zshlex();`
6354            zshlex();
6355            // c:1156 — `np = ecadd(0);`
6356            np = ecadd(0);
6357            // c:1157 — `n = par_nl_wordlist();`
6358            let n2 = par_nl_wordlist_wordcode();
6359            // c:1158-1159 — `if (tok != OUTPAR) YYERRORV(oecused);`
6360            if tok() != OUTPAR_TOK {
6361                zerr("par_for: expected `)`");
6362                return;
6363            }
6364            // c:1160 — `ecbuf[np] = n;`
6365            ECBUF.with_borrow_mut(|b| {
6366                b[np] = n2 as wordcode;
6367            });
6368            // c:1161 — `incmdpos = 1;`
6369            set_incmdpos(true);
6370            // c:1162 — `zshlex();`
6371            zshlex();
6372            // c:1163 — `type = (sel ? WC_SELECT_LIST : WC_FOR_LIST);`
6373            r#type = if sel { WC_SELECT_LIST } else { WC_FOR_LIST };
6374        } else {
6375            // c:1165 — `type = (sel ? WC_SELECT_PPARAM : WC_FOR_PPARAM);`
6376            r#type = if sel { WC_SELECT_PPARAM } else { WC_FOR_PPARAM };
6377        }
6378        let _ = np;
6379    }
6380    // c:1167 — `incmdpos = 1;`
6381    set_incmdpos(true);
6382    // c:1168-1169 — `while (tok == SEPER) zshlex();`
6383    while tok() == SEPER {
6384        zshlex();
6385    }
6386    // c:1170-1193 — body dispatch (inline in C, factored here for
6387    // reuse by par_while/par_repeat — same control flow, same calls).
6388    par_loop_body_wordcode(cmplx, csh);
6389    // c:1195-1197 — `ecbuf[p] = (sel ? WCB_SELECT(...) : WCB_FOR(...));`
6390    let used = ECUSED.get() as usize;
6391    let off = used.saturating_sub(1 + p) as wordcode;
6392    ECBUF.with_borrow_mut(|b| {
6393        b[p] = if sel {
6394            WCB_SELECT(r#type, off)
6395        } else {
6396            WCB_FOR(r#type, off)
6397        };
6398    });
6399}
6400
6401/// Port of `par_wordlist(void)` from `Src/parse.c:2361-2371` —
6402/// emits wordcode form. Returns the number of strings emitted.
6403fn par_wordlist_wordcode() -> u32 {
6404    // c:2364 — `int num = 0;`
6405    let mut num: u32 = 0;
6406    // c:2365 — `while (tok == STRING) {`
6407    while tok() == STRING_LEX {
6408        // c:2366 — `ecstr(tokstr);`
6409        ecstr(&tokstr().unwrap_or_default());
6410        // c:2367 — `num++;`
6411        num += 1;
6412        // c:2368 — `zshlex();`
6413        zshlex();
6414    }
6415    // c:2370 — `return num;`
6416    num
6417}
6418
6419/// Port of `par_nl_wordlist(void)` from `Src/parse.c:2378-2390` —
6420/// emits wordcode form. Like par_wordlist but tolerates SEPER
6421/// between words.
6422fn par_nl_wordlist_wordcode() -> u32 {
6423    // c:2381 — `int num = 0;`
6424    let mut num: u32 = 0;
6425    // c:2383 — `while (tok == STRING || tok == SEPER) {`
6426    while tok() == STRING_LEX || tok() == SEPER || tok() == NEWLIN {
6427        // c:2384-2387 — `if (tok != SEPER) { ecstr(tokstr); num++; }`
6428        if tok() == STRING_LEX {
6429            ecstr(&tokstr().unwrap_or_default());
6430            num += 1;
6431        }
6432        // c:2388 — `zshlex();`
6433        zshlex();
6434    }
6435    // c:2390 — `return num;`
6436    num
6437}
6438
6439/// Body dispatch shared by par_for / par_while / par_repeat.
6440/// Direct port of `Src/parse.c:1170-1194`.
6441fn par_loop_body_wordcode(cmplx: &mut i32, csh: bool) {
6442    if tok() == DOLOOP {
6443        zshlex();
6444        // c:1172 — `par_save_list(cmplx);`
6445        par_save_list_wordcode(cmplx);
6446        if tok() != DONE {
6447            zerr("missing `done`");
6448            return;
6449        }
6450        set_incmdpos(false);
6451        zshlex();
6452    } else if tok() == INBRACE_TOK {
6453        zshlex();
6454        // c:1179 — `par_save_list(cmplx);`
6455        par_save_list_wordcode(cmplx);
6456        if tok() != OUTBRACE_TOK {
6457            zerr("missing `}`");
6458            return;
6459        }
6460        set_incmdpos(false);
6461        zshlex();
6462    } else if csh || isset(CSHJUNKIELOOPS) {
6463        // c:1185 — `par_save_list(cmplx);`
6464        par_save_list_wordcode(cmplx);
6465        if tok() != ZEND {
6466            zerr("missing `end`");
6467            return;
6468        }
6469        set_incmdpos(false);
6470        zshlex();
6471    } else if unset(SHORTLOOPS) {
6472        zerr("short loop form requires SHORTLOOPS");
6473    } else {
6474        // c:1193 — `par_save_list1(cmplx);`
6475        par_save_list1_wordcode(cmplx);
6476    }
6477}
6478
6479/// `select` shares par_for body (c:983-985 routes SELECT to par_for).
6480pub fn par_select_wordcode(cmplx: &mut i32) {
6481    par_for_wordcode(cmplx);
6482}
6483
6484/// Port of `par_case(int *cmplx)` from `Src/parse.c:1208-1400`.
6485pub fn par_case_wordcode(_cmplx: &mut i32) {
6486    // c:1211 — `int oecused = ecused, brflag, p, pp, palts, type, nalts;`
6487    let _oecused = ECUSED.get() as usize;
6488    let brflag: bool;
6489    let p: usize;
6490    let mut pp: usize;
6491    let mut palts: usize;
6492    let mut r#type: wordcode;
6493    let mut nalts: u32;
6494    // c:1212 — `int ona, onc;`
6495    let ona: bool;
6496    let onc: i32;
6497
6498    // c:1214 — `p = ecadd(0);`
6499    p = ecadd(0);
6500
6501    // c:1216 — `incmdpos = 0;`
6502    set_incmdpos(false);
6503    // c:1217 — `zshlex();`
6504    zshlex();
6505    // c:1218-1219 — `if (tok != STRING) YYERRORV(oecused);`
6506    if tok() != STRING_LEX {
6507        zerr("par_case: expected scrutinee");
6508        return;
6509    }
6510    // c:1220 — `ecstr(tokstr);`
6511    ecstr(&tokstr().unwrap_or_default());
6512
6513    // c:1222 — `incmdpos = 1;`
6514    set_incmdpos(true);
6515    // c:1223-1224 — `ona = noaliases; onc = nocorrect;`
6516    ona = noaliases();
6517    onc = nocorrect();
6518    // c:1225 — `noaliases = nocorrect = 1;`
6519    set_noaliases(true);
6520    set_nocorrect(1);
6521    // c:1226 — `zshlex();`
6522    zshlex();
6523    // c:1227-1228 — `while (tok == SEPER) zshlex();`
6524    while tok() == SEPER {
6525        zshlex();
6526    }
6527    // c:1229 — `if (!(tok == STRING && !strcmp(tokstr, "in")) && tok != INBRACE)`
6528    if !(tok() == STRING_LEX && tokstr().as_deref() == Some("in")) && tok() != INBRACE_TOK {
6529        // c:1231-1233 — restore noaliases/nocorrect + ERROR
6530        set_noaliases(ona);
6531        set_nocorrect(onc);
6532        zerr("par_case: expected `in` or `{`");
6533        return;
6534    }
6535    // c:1235 — `brflag = (tok == INBRACE);`
6536    brflag = tok() == INBRACE_TOK;
6537    // c:1236 — `incasepat = 1;`
6538    set_incasepat(1);
6539    // c:1237 — `incmdpos = 0;`
6540    set_incmdpos(false);
6541    // c:1238-1239 — `noaliases = ona; nocorrect = onc;`
6542    set_noaliases(ona);
6543    set_nocorrect(onc);
6544    // c:1240 — `zshlex();`
6545    zshlex();
6546
6547    // c:1242 — `for (;;) {`
6548    'arms: loop {
6549        // c:1243 — `char *str;`
6550        let mut str: String;
6551        // c:1244 — `int skip_zshlex;`
6552        let skip_zshlex: bool;
6553
6554        // c:1246-1247 — `while (tok == SEPER) zshlex();`
6555        while tok() == SEPER {
6556            zshlex();
6557        }
6558        // c:1248-1249 — `if (tok == OUTBRACE) break;`
6559        if tok() == OUTBRACE_TOK {
6560            break 'arms;
6561        }
6562        // c:1250-1251 — `if (tok == INPAR) zshlex();`
6563        if tok() == INPAR_TOK {
6564            zshlex();
6565        }
6566        // c:1252-1254 — `if (tok == BAR) { str = ""; skip_zshlex = 1; }`
6567        if tok() == BAR_TOK {
6568            str = String::new();
6569            skip_zshlex = true;
6570        } else {
6571            // c:1256-1257 — `if (tok != STRING) YYERRORV(oecused);`
6572            if tok() != STRING_LEX {
6573                zerr("par_case: expected pattern");
6574                return;
6575            }
6576            // c:1258-1259 — `if (!strcmp(tokstr, "esac")) break;`
6577            if tokstr().as_deref() == Some("esac") {
6578                break 'arms;
6579            }
6580            // c:1260 — `str = dupstring(tokstr);`
6581            str = tokstr().unwrap_or_default();
6582            // c:1261 — `skip_zshlex = 0;`
6583            skip_zshlex = false;
6584        }
6585        // c:1263 — `type = WC_CASE_OR;`
6586        r#type = WC_CASE_OR;
6587        // c:1264-1266 — `pp = ecadd(0); palts = ecadd(0); nalts = 0;`
6588        pp = ecadd(0);
6589        palts = ecadd(0);
6590        nalts = 0;
6591        // c:1300 — `incasepat = -1;`
6592        set_incasepat(-1);
6593        // c:1301 — `incmdpos = 1;`
6594        set_incmdpos(true);
6595        // c:1302-1303 — `if (!skip_zshlex) zshlex();`
6596        if !skip_zshlex {
6597            zshlex();
6598        }
6599        // c:1304 — `for (;;) {`
6600        loop {
6601            // c:1305-1313 — `if (tok == OUTPAR) { ecstr(str);
6602            //   ecadd(ecnpats++); nalts++; incasepat = 0;
6603            //   incmdpos = 1; zshlex(); break; }`
6604            if tok() == OUTPAR_TOK {
6605                ecstr(&str);
6606                let np = ECNPATS.with(|cc| {
6607                    let v = cc.get();
6608                    cc.set(v + 1);
6609                    v
6610                }) as u32;
6611                ecadd(np);
6612                nalts += 1;
6613                set_incasepat(0);
6614                set_incmdpos(true);
6615                zshlex();
6616                break;
6617            }
6618            // c:1314-1320 — `else if (tok == BAR) { ecstr(str);
6619            //   ecadd(ecnpats++); nalts++; incasepat = 1;
6620            //   incmdpos = 0; }`
6621            else if tok() == BAR_TOK {
6622                ecstr(&str);
6623                let np = ECNPATS.with(|cc| {
6624                    let v = cc.get();
6625                    cc.set(v + 1);
6626                    v
6627                }) as u32;
6628                ecadd(np);
6629                nalts += 1;
6630                set_incasepat(1);
6631                set_incmdpos(false);
6632            }
6633            // c:1321-1357 — else { ... `(...)` whole-pattern hack:
6634            // the lexer absorbed a complete `(...)` as one STRING
6635            // (str[0] == Inpar) and the current tok is already the
6636            // body's first token. Massage blanks around `|`/parens
6637            // at depth 1, validate balance, strip the surrounding
6638            // parens; the remainder IS the pattern. }
6639            else {
6640                use crate::ported::zsh_h::{Bar, Inpar, Outpar};
6641                if nalts == 0 && str.starts_with(Inpar) {
6642                    let meta = crate::ported::zsh_h::Meta as char;
6643                    let blank = |c: char| c.is_ascii() && crate::ported::ztype_h::iblank(c as u8);
6644                    let mut chars: Vec<char> = str.chars().collect();
6645                    // c:1323 — `int pct = 0, sl;`
6646                    let mut pct = 0i32;
6647                    let mut i = 0usize;
6648                    // c:1326-1344 — scan/massage loop. `s` ↔ `i`;
6649                    // chuck(p) ↔ chars.remove(idx).
6650                    let mut early_break = false;
6651                    while i < chars.len() {
6652                        if chars[i] == Inpar {
6653                            pct += 1;
6654                        }
6655                        // c:1329-1330 — `if (!pct) break;` (char past
6656                        // the balanced close → trailing garbage).
6657                        if pct == 0 {
6658                            early_break = true;
6659                            break;
6660                        }
6661                        if pct == 1 {
6662                            // c:1332-1334 — chuck blanks AFTER `|`/`(`.
6663                            if chars[i] == Bar || chars[i] == Inpar {
6664                                while i + 1 < chars.len() && blank(chars[i + 1]) {
6665                                    chars.remove(i + 1);
6666                                }
6667                            }
6668                            // c:1335-1338 — chuck blanks BEFORE `|`/`)`
6669                            // (not Meta-escaped blanks).
6670                            if chars[i] == Bar || chars[i] == Outpar {
6671                                while i >= 1
6672                                    && blank(chars[i - 1])
6673                                    && (i < 2 || chars[i - 2] != meta)
6674                                {
6675                                    chars.remove(i - 1);
6676                                    i -= 1;
6677                                }
6678                            }
6679                        }
6680                        if chars[i] == Outpar {
6681                            pct -= 1;
6682                        }
6683                        i += 1;
6684                    }
6685                    // c:1345-1346 — `if (*s || pct || s == str)
6686                    // YYERRORV(oecused);`
6687                    if early_break || pct != 0 || chars.is_empty() {
6688                        zerr("par_case: expected `)` or `|`");
6689                        return;
6690                    }
6691                    // c:1347-1352 — strip surrounding `(...)`.
6692                    chars.pop();
6693                    chars.remove(0);
6694                    let stripped: String = chars.into_iter().collect();
6695                    // c:1353-1355 — `ecstr(str); ecadd(ecnpats++); nalts++;`
6696                    ecstr(&stripped);
6697                    let np = ECNPATS.with(|cc| {
6698                        let v = cc.get();
6699                        cc.set(v + 1);
6700                        v
6701                    }) as u32;
6702                    ecadd(np);
6703                    nalts += 1;
6704                    // c:1356 — `break;` — tok is already the body's
6705                    // first token; fall through to par_save_list.
6706                    break;
6707                }
6708                // c:1358 — `YYERRORV(oecused);`
6709                zerr("par_case: expected `)` or `|`");
6710                return;
6711            }
6712
6713            // c:1359 — `zshlex();`
6714            zshlex();
6715            // c:1360-1377 — switch on next tok.
6716            match tok() {
6717                STRING_LEX => {
6718                    // c:1361-1365
6719                    str = tokstr().unwrap_or_default();
6720                    zshlex();
6721                }
6722                OUTPAR_TOK | BAR_TOK => {
6723                    // c:1367-1371 — empty string
6724                    str = String::new();
6725                }
6726                _ => {
6727                    // c:1374-1376 — `YYERRORV(oecused);`
6728                    zerr("par_case: expected pattern, `)` or `|`");
6729                    return;
6730                }
6731            }
6732        }
6733        // c:1379 — `incasepat = 0;`
6734        set_incasepat(0);
6735        // c:1380 — `par_save_list(cmplx);`
6736        par_save_list_wordcode(_cmplx);
6737        // c:1381-1384 — terminator → arm type
6738        if tok() == SEMIAMP {
6739            r#type = WC_CASE_AND;
6740        } else if tok() == SEMIBAR {
6741            r#type = WC_CASE_TESTAND;
6742        }
6743        // c:1385 — `ecbuf[pp] = WCB_CASE(type, ecused - 1 - pp);`
6744        let used = ECUSED.get() as usize;
6745        ECBUF.with_borrow_mut(|b| {
6746            b[pp] = WCB_CASE(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6747        });
6748        // c:1386 — `ecbuf[palts] = nalts;`
6749        ECBUF.with_borrow_mut(|b| {
6750            b[palts] = nalts;
6751        });
6752        // c:1387-1388 — terminator (ESAC w/o brace OR OUTBRACE w/ brace) → break
6753        if (tok() == ESAC && !brflag) || (tok() == OUTBRACE_TOK && brflag) {
6754            break 'arms;
6755        }
6756        // c:1389-1390 — `if (tok != DSEMI && tok != SEMIAMP && tok != SEMIBAR) YYERRORV;`
6757        if tok() != DSEMI && tok() != SEMIAMP && tok() != SEMIBAR {
6758            zerr("par_case: expected `;;`, `;&`, or `;|`");
6759            return;
6760        }
6761        // c:1391 — `incasepat = 1;`
6762        set_incasepat(1);
6763        // c:1392 — `incmdpos = 0;`
6764        set_incmdpos(false);
6765        // c:1393 — `zshlex();`
6766        zshlex();
6767    }
6768    // c:1395 — `incmdpos = 1;`
6769    set_incmdpos(true);
6770    // c:1396 — `incasepat = 0;`
6771    set_incasepat(0);
6772    // c:1397 — `zshlex();`
6773    zshlex();
6774
6775    // c:1399 — `ecbuf[p] = WCB_CASE(WC_CASE_HEAD, ecused - 1 - p);`
6776    let used = ECUSED.get() as usize;
6777    ECBUF.with_borrow_mut(|b| {
6778        b[p] = WCB_CASE(WC_CASE_HEAD, (used.saturating_sub(1 + p)) as wordcode);
6779    });
6780}
6781
6782/// Port of `par_if(int *cmplx)` from `Src/parse.c:1410-1512`.
6783pub fn par_if_wordcode(cmplx: &mut i32) {
6784    // c:1413 — `int oecused = ecused, p, pp, type, usebrace = 0;`
6785    let _oecused = ECUSED.get() as usize;
6786    let p: usize;
6787    let mut pp: usize = 0;
6788    let mut r#type: wordcode = WC_IF_IF;
6789    let mut usebrace: i32 = 0;
6790    // c:1414 — `enum lextok xtok;`
6791    let mut xtok: lextok;
6792    // c:1415 — `unsigned char nc;`
6793    let nc: u8;
6794    let _ = nc;
6795
6796    // c:1417 — `p = ecadd(0);`
6797    p = ecadd(0);
6798
6799    // c:1419 — `for (;;) {`
6800    loop {
6801        // c:1420 — `xtok = tok;`
6802        xtok = tok();
6803        // c:1421 — `cmdpush(xtok == IF ? CS_IF : CS_ELIF);`
6804        cmdpush(if xtok == IF {
6805            CS_IF as u8
6806        } else {
6807            CS_ELIF as u8
6808        });
6809        // c:1422-1426 — `if (xtok == FI) { incmdpos = 0; zshlex(); break; }`
6810        if xtok == FI {
6811            set_incmdpos(false);
6812            zshlex();
6813            break;
6814        }
6815        // c:1427 — `zshlex();`
6816        zshlex();
6817        // c:1428-1429 — `if (xtok == ELSE) break;`
6818        if xtok == ELSE {
6819            break;
6820        }
6821        // c:1430-1431 — `while (tok == SEPER) zshlex();`
6822        while tok() == SEPER {
6823            zshlex();
6824        }
6825        // c:1432-1435 — `if (!(xtok == IF || xtok == ELIF)) { cmdpop(); YYERRORV; }`
6826        if !(xtok == IF || xtok == ELIF) {
6827            cmdpop();
6828            zerr("par_if: expected `if` or `elif`");
6829            return;
6830        }
6831        // c:1436 — `pp = ecadd(0);`
6832        pp = ecadd(0);
6833        // c:1437 — `type = (xtok == IF ? WC_IF_IF : WC_IF_ELIF);`
6834        r#type = if xtok == IF { WC_IF_IF } else { WC_IF_ELIF };
6835        // c:1438 — `par_save_list(cmplx);` — condition body
6836        par_save_list_wordcode(cmplx);
6837        // c:1439 — `incmdpos = 1;`
6838        set_incmdpos(true);
6839        // c:1440-1443 — `if (tok == ENDINPUT) { cmdpop(); YYERRORV; }`
6840        if tok() == ENDINPUT {
6841            cmdpop();
6842            zerr("par_if: unexpected end-of-input after condition");
6843            return;
6844        }
6845        // c:1444-1445 — `while (tok == SEPER) zshlex();`
6846        while tok() == SEPER {
6847            zshlex();
6848        }
6849        // c:1446 — `xtok = FI;` — pre-set so the post-loop check works
6850        xtok = FI;
6851        // c:1447 — `nc = cmdstack[cmdsp - 1] == CS_IF ? CS_IFTHEN : CS_ELIFTHEN;`
6852        // (Not tracked separately in zshrs cmdstack — derive from cur top
6853        // by reading CMDSTACK; for safety use CS_IFTHEN as default.)
6854        // We don't have a way to read top easily — match by tracking
6855        // whether we just pushed CS_IF or CS_ELIF.
6856        // For wordcode emission this only affects cmdstack debug output;
6857        // not the emitted wordcode. Use CS_IFTHEN.
6858        let nc_local: u8 = CS_IFTHEN as u8;
6859        if tok() == THEN {
6860            // c:1448-1456 — THEN branch
6861            // c:1449 — `usebrace = 0;`
6862            usebrace = 0;
6863            // c:1450 — `cmdpop();`
6864            cmdpop();
6865            // c:1451 — `cmdpush(nc);`
6866            cmdpush(nc_local);
6867            // c:1452 — `zshlex();`
6868            zshlex();
6869            // c:1453 — `par_save_list(cmplx);` — then body
6870            par_save_list_wordcode(cmplx);
6871            // c:1454 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
6872            let used = ECUSED.get() as usize;
6873            ECBUF.with_borrow_mut(|b| {
6874                b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6875            });
6876            // c:1455 — `incmdpos = 1;`
6877            set_incmdpos(true);
6878            // c:1456 — `cmdpop();`
6879            cmdpop();
6880        } else if tok() == INBRACE_TOK {
6881            // c:1457-1473 — INBRACE branch
6882            // c:1458 — `usebrace = 1;`
6883            usebrace = 1;
6884            // c:1459 — `cmdpop();`
6885            cmdpop();
6886            // c:1460 — `cmdpush(nc);`
6887            cmdpush(nc_local);
6888            // c:1461 — `zshlex();`
6889            zshlex();
6890            // c:1462 — `par_save_list(cmplx);`
6891            par_save_list_wordcode(cmplx);
6892            // c:1463-1466 — `if (tok != OUTBRACE) { cmdpop(); YYERRORV; }`
6893            if tok() != OUTBRACE_TOK {
6894                cmdpop();
6895                zerr("par_if: expected `}`");
6896                return;
6897            }
6898            // c:1467 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
6899            let used = ECUSED.get() as usize;
6900            ECBUF.with_borrow_mut(|b| {
6901                b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6902            });
6903            // c:1469 — `zshlex();`
6904            zshlex();
6905            // c:1470 — `incmdpos = 1;`
6906            set_incmdpos(true);
6907            // c:1471-1472 — `if (tok == SEPER) break;`
6908            if tok() == SEPER {
6909                break;
6910            }
6911            // c:1473 — `cmdpop();`
6912            cmdpop();
6913        } else if unset(SHORTLOOPS) {
6914            // c:1474-1476 — `cmdpop(); YYERRORV;`
6915            cmdpop();
6916            zerr("par_if: short body requires SHORTLOOPS");
6917            return;
6918        } else {
6919            // c:1477-1484 — short loop form
6920            // c:1478 — `cmdpop();`
6921            cmdpop();
6922            // c:1479 — `cmdpush(nc);`
6923            cmdpush(nc_local);
6924            // c:1480 — `par_save_list1(cmplx);`
6925            par_save_list1_wordcode(cmplx);
6926            // c:1481 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
6927            let used = ECUSED.get() as usize;
6928            ECBUF.with_borrow_mut(|b| {
6929                b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6930            });
6931            // c:1482 — `incmdpos = 1;`
6932            set_incmdpos(true);
6933            // c:1483 — `break;`
6934            break;
6935        }
6936    }
6937    // c:1486 — `cmdpop();`
6938    cmdpop();
6939    // c:1487 — `if (xtok == ELSE || tok == ELSE) {`
6940    if xtok == ELSE || tok() == ELSE {
6941        // c:1488 — `pp = ecadd(0);`
6942        pp = ecadd(0);
6943        // c:1489 — `cmdpush(CS_ELSE);`
6944        cmdpush(CS_ELSE as u8);
6945        // c:1490-1491 — `while (tok == SEPER) zshlex();`
6946        while tok() == SEPER {
6947            zshlex();
6948        }
6949        // c:1492-1498 — `if (tok == INBRACE && usebrace) { ... } else { ... }`
6950        if tok() == INBRACE_TOK && usebrace != 0 {
6951            // c:1493 — `zshlex();`
6952            zshlex();
6953            // c:1494 — `par_save_list(cmplx);`
6954            par_save_list_wordcode(cmplx);
6955            // c:1495-1498 — `if (tok != OUTBRACE) { cmdpop(); YYERRORV; }`
6956            if tok() != OUTBRACE_TOK {
6957                cmdpop();
6958                zerr("par_if: else expected `}`");
6959                return;
6960            }
6961        } else {
6962            // c:1500 — `par_save_list(cmplx);`
6963            par_save_list_wordcode(cmplx);
6964            // c:1501-1504 — `if (tok != FI) { cmdpop(); YYERRORV; }`
6965            if tok() != FI {
6966                cmdpop();
6967                zerr("par_if: else expected `fi`");
6968                return;
6969            }
6970        }
6971        // c:1506 — `incmdpos = 0;`
6972        set_incmdpos(false);
6973        // c:1507 — `ecbuf[pp] = WCB_IF(WC_IF_ELSE, ecused - 1 - pp);`
6974        let used = ECUSED.get() as usize;
6975        ECBUF.with_borrow_mut(|b| {
6976            b[pp] = WCB_IF(WC_IF_ELSE, (used.saturating_sub(1 + pp)) as wordcode);
6977        });
6978        // c:1508 — `zshlex();`
6979        zshlex();
6980        // c:1509 — `cmdpop();`
6981        cmdpop();
6982    }
6983    // c:1511 — `ecbuf[p] = WCB_IF(WC_IF_HEAD, ecused - 1 - p);`
6984    let used = ECUSED.get() as usize;
6985    ECBUF.with_borrow_mut(|b| {
6986        b[p] = WCB_IF(WC_IF_HEAD, (used.saturating_sub(1 + p)) as wordcode);
6987    });
6988}
6989
6990/// Port of `par_while(int *cmplx)` from `Src/parse.c:1520-1557`.
6991pub fn par_while_wordcode(cmplx: &mut i32) {
6992    // c:1523 — `int oecused = ecused, p;`
6993    let _oecused = ECUSED.get() as usize;
6994    let p: usize;
6995    // c:1524 — `int type = (tok == UNTIL ? WC_WHILE_UNTIL : WC_WHILE_WHILE);`
6996    let r#type: wordcode = if tok() == UNTIL {
6997        WC_WHILE_UNTIL
6998    } else {
6999        WC_WHILE_WHILE
7000    };
7001
7002    // c:1526 — `p = ecadd(0);`
7003    p = ecadd(0);
7004    // c:1527 — `zshlex();`
7005    zshlex();
7006    // c:1528 — `par_save_list(cmplx);` — condition.
7007    par_save_list_wordcode(cmplx);
7008    // c:1529 — `incmdpos = 1;`
7009    set_incmdpos(true);
7010    // c:1530-1531 — `while (tok == SEPER) zshlex();`
7011    while tok() == SEPER {
7012        zshlex();
7013    }
7014    // c:1532-1545 — body dispatch (inlined in C; we factor via
7015    // par_loop_body_wordcode since for/while/repeat share this
7016    // identical block).
7017    if tok() == DOLOOP {
7018        // c:1533 — `zshlex();`
7019        zshlex();
7020        // c:1534 — `par_save_list(cmplx);`
7021        par_save_list_wordcode(cmplx);
7022        // c:1535-1536 — `if (tok != DONE) YYERRORV(oecused);`
7023        if tok() != DONE {
7024            zerr("par_while: expected `done`");
7025            return;
7026        }
7027        // c:1537 — `incmdpos = 0;`
7028        set_incmdpos(false);
7029        // c:1538 — `zshlex();`
7030        zshlex();
7031    } else if tok() == INBRACE_TOK {
7032        // c:1540 — `zshlex();`
7033        zshlex();
7034        // c:1541 — `par_save_list(cmplx);`
7035        par_save_list_wordcode(cmplx);
7036        // c:1542-1543 — `if (tok != OUTBRACE) YYERRORV(oecused);`
7037        if tok() != OUTBRACE_TOK {
7038            zerr("par_while: expected `}`");
7039            return;
7040        }
7041        // c:1544 — `incmdpos = 0;`
7042        set_incmdpos(false);
7043        // c:1545 — `zshlex();`
7044        zshlex();
7045    } else if isset(CSHJUNKIELOOPS) {
7046        // c:1546-1550
7047        par_save_list_wordcode(cmplx);
7048        if tok() != ZEND {
7049            zerr("par_while: expected `end`");
7050            return;
7051        }
7052        zshlex();
7053    } else if unset(SHORTLOOPS) {
7054        // c:1551-1552 — `YYERRORV(oecused);`
7055        zerr("par_while: short body requires SHORTLOOPS");
7056        return;
7057    } else {
7058        // c:1554 — `par_save_list1(cmplx);`
7059        par_save_list1_wordcode(cmplx);
7060    }
7061
7062    // c:1556 — `ecbuf[p] = WCB_WHILE(type, ecused - 1 - p);`
7063    let used = ECUSED.get() as usize;
7064    ECBUF.with_borrow_mut(|b| {
7065        b[p] = WCB_WHILE(r#type, (used.saturating_sub(1 + p)) as wordcode);
7066    });
7067}
7068
7069/// `until` shares par_while body — tok==UNTIL flips the type.
7070pub fn par_until_wordcode(cmplx: &mut i32) {
7071    par_while_wordcode(cmplx);
7072}
7073
7074/// Port of `par_repeat(int *cmplx)` from `Src/parse.c:1564-1606`.
7075pub fn par_repeat_wordcode(cmplx: &mut i32) {
7076    // c:1567 — `/* ### what to do about inrepeat_ here? */`
7077    // c:1568 — `int oecused = ecused, p;`
7078    let _oecused = ECUSED.get() as usize;
7079    let p: usize;
7080
7081    // c:1570 — `p = ecadd(0);`
7082    p = ecadd(0);
7083
7084    // c:1572 — `incmdpos = 0;`
7085    set_incmdpos(false);
7086    // c:1573 — `zshlex();`
7087    zshlex();
7088    // c:1574-1575 — `if (tok != STRING) YYERRORV(oecused);`
7089    if tok() != STRING_LEX {
7090        zerr("par_repeat: expected count");
7091        return;
7092    }
7093    // c:1576 — `ecstr(tokstr);`
7094    ecstr(&tokstr().unwrap_or_default());
7095    // c:1577 — `incmdpos = 1;`
7096    set_incmdpos(true);
7097    // c:1578 — `zshlex();`
7098    zshlex();
7099    // c:1579-1580 — `while (tok == SEPER) zshlex();`
7100    while tok() == SEPER {
7101        zshlex();
7102    }
7103    // c:1581-1604 — body dispatch (inlined here matching C exactly).
7104    if tok() == DOLOOP {
7105        // c:1582-1587
7106        zshlex();
7107        par_save_list_wordcode(cmplx);
7108        if tok() != DONE {
7109            zerr("par_repeat: expected `done`");
7110            return;
7111        }
7112        set_incmdpos(false);
7113        zshlex();
7114    } else if tok() == INBRACE_TOK {
7115        // c:1589-1594
7116        zshlex();
7117        par_save_list_wordcode(cmplx);
7118        if tok() != OUTBRACE_TOK {
7119            zerr("par_repeat: expected `}`");
7120            return;
7121        }
7122        set_incmdpos(false);
7123        zshlex();
7124    } else if isset(CSHJUNKIELOOPS) {
7125        // c:1596-1599
7126        par_save_list_wordcode(cmplx);
7127        if tok() != ZEND {
7128            zerr("par_repeat: expected `end`");
7129            return;
7130        }
7131        zshlex();
7132    } else if unset(SHORTLOOPS) && unset(SHORTREPEAT) {
7133        // c:1601-1602 — par_repeat needs BOTH SHORTLOOPS and SHORTREPEAT
7134        // unset to refuse short form (more permissive than par_while).
7135        zerr("par_repeat: short body requires SHORTLOOPS or SHORTREPEAT");
7136        return;
7137    } else {
7138        // c:1604 — `par_save_list1(cmplx);`
7139        par_save_list1_wordcode(cmplx);
7140    }
7141
7142    // c:1606 — `ecbuf[p] = WCB_REPEAT(ecused - 1 - p);`
7143    let used = ECUSED.get() as usize;
7144    ECBUF.with_borrow_mut(|b| {
7145        b[p] = WCB_REPEAT((used.saturating_sub(1 + p)) as wordcode);
7146    });
7147}
7148
7149/// Port of `par_funcdef(int *cmplx)` from `Src/parse.c:1672-1779`.
7150///
7151/// The `function NAME { ... }` form. Emits a WCB_FUNCDEF header
7152/// followed by a names-count slot, the names themselves, four
7153/// metadata slots (string-area start, string-area length, npats,
7154/// do_tracing), then the body wordcode, then WCB_END.
7155///
7156/// Critical: saves/resets `ecnpats` + `ecssub` + `ecsoffs` around
7157/// the body parse so per-function pattern counts don't leak into
7158/// the enclosing scope's `ecnpats` accumulator (parse.c:1723-1758).
7159pub fn par_funcdef_wordcode(cmplx: &mut i32) {
7160    // c:1674 — `int oecused = ecused, num = 0, onp, p, c = 0;`
7161    let _oecused = ECUSED.get() as usize;
7162    let mut num: i32 = 0;
7163    let onp: i32;
7164    let p: usize;
7165    let mut c: i32 = 0;
7166    // c:1675 — `int so, oecssub = ecssub;`
7167    let so: i32;
7168    let oecssub = ECSSUB.get();
7169    // c:1676 — `zlong oldlineno = lineno;`
7170    let oldlineno = lineno();
7171    // c:1677 — `int do_tracing = 0;`
7172    let mut do_tracing: i32 = 0;
7173
7174    // c:1679 — `lineno = 0;`
7175    set_lineno(0);
7176    // c:1680 — `nocorrect = 1;`
7177    set_nocorrect(1);
7178    // c:1681 — `incmdpos = 0;`
7179    set_incmdpos(false);
7180    // c:1682 — `zshlex();`
7181    zshlex();
7182
7183    // c:1684 — `p = ecadd(0);`
7184    p = ecadd(0);
7185    // c:1685 — `ecadd(0); /* p + 1 */`
7186    let p1 = ecadd(0);
7187
7188    // c:1687-1699 — `Consume an initial (-T), (--), or (-T --).`
7189    // c:1690 — `if (tok == STRING && tokstr[0] == Dash) {`
7190    if tok() == STRING_LEX {
7191        let s = tokstr().unwrap_or_default();
7192        let bytes = s.as_bytes();
7193        // C: `tokstr[0] == Dash` (Dash = 0x9b = 0xc2 0x9b in UTF-8).
7194        // First byte of UTF-8 `\u{9b}` is 0xc2; the char `'-'` is 0x2d.
7195        // Match either form.
7196        let first_is_dash = (bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x9b)
7197            || (bytes.len() >= 1 && bytes[0] == b'-');
7198        if first_is_dash {
7199            // c:1691-1694 — `if (tokstr[1] == 'T' && !tokstr[2]) { ++do_tracing; zshlex(); }`
7200            // After the leading dash byte(s), check remaining bytes.
7201            let after_dash = if bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x9b {
7202                &bytes[2..]
7203            } else {
7204                &bytes[1..]
7205            };
7206            if after_dash.len() == 1 && after_dash[0] == b'T' {
7207                do_tracing += 1;
7208                zshlex();
7209            }
7210            // c:1695-1698 — `if (tok == STRING && tokstr[0] == Dash &&
7211            //                  tokstr[1] == Dash && !tokstr[2]) zshlex();`
7212            if tok() == STRING_LEX {
7213                let s2 = tokstr().unwrap_or_default();
7214                let b2 = s2.as_bytes();
7215                let mut idx = 0;
7216                let mut dashes = 0;
7217                while idx < b2.len() && dashes < 2 {
7218                    if b2[idx] == 0xc2 && idx + 1 < b2.len() && b2[idx + 1] == 0x9b {
7219                        idx += 2;
7220                        dashes += 1;
7221                    } else if b2[idx] == b'-' {
7222                        idx += 1;
7223                        dashes += 1;
7224                    } else {
7225                        break;
7226                    }
7227                }
7228                if dashes == 2 && idx == b2.len() {
7229                    zshlex();
7230                }
7231            }
7232        }
7233    }
7234
7235    // c:1701-1709 — names loop.
7236    // `while (tok == STRING) { if ((*tokstr == Inbrace || *tokstr == '{')
7237    //   && !tokstr[1]) { tok = INBRACE; break; } ecstr(tokstr); num++; zshlex(); }`
7238    while tok() == STRING_LEX {
7239        let s = tokstr().unwrap_or_default();
7240        let bytes = s.as_bytes();
7241        // First byte tests for Inbrace marker (0x8f → UTF-8 `0xc2 0x8f`) or `{`,
7242        // and length-1 check (`!tokstr[1]`).
7243        let is_inbrace_only = (bytes.len() == 1 && bytes[0] == b'{')
7244            || (bytes.len() == 2 && bytes[0] == 0xc2 && bytes[1] == 0x8f);
7245        if is_inbrace_only {
7246            set_tok(INBRACE_TOK);
7247            break;
7248        }
7249        ecstr(&s);
7250        num += 1;
7251        zshlex();
7252    }
7253
7254    // c:1711-1714 — four metadata placeholder slots.
7255    let m2 = ecadd(0);
7256    let m3 = ecadd(0);
7257    let m4 = ecadd(0);
7258    let m5 = ecadd(0);
7259
7260    // c:1716 — `nocorrect = 0;`
7261    set_nocorrect(0);
7262    // c:1717 — `incmdpos = 1;`
7263    set_incmdpos(true);
7264    // c:1718-1719 — `if (tok == INOUTPAR) zshlex();`
7265    if tok() == INOUTPAR {
7266        zshlex();
7267    }
7268    // c:1720-1721 — `while (tok == SEPER) zshlex();`
7269    while tok() == SEPER {
7270        zshlex();
7271    }
7272
7273    // c:1723 — `ecnfunc++;`
7274    ECNFUNC.set(ECNFUNC.get() + 1);
7275    // c:1724 — `ecssub = so = ecsoffs;`
7276    so = ECSOFFS.get();
7277    ECSSUB.set(so);
7278    // c:1725 — `onp = ecnpats;`
7279    onp = ECNPATS.with(|cc| cc.get());
7280    // c:1726 — `ecnpats = 0;`
7281    ECNPATS.with(|cc| cc.set(0));
7282
7283    // c:1728 — `if (tok == INBRACE) {`
7284    if tok() == INBRACE_TOK {
7285        // c:1729 — `zshlex();`
7286        zshlex();
7287        // c:1730 — `par_list(&c);`
7288        par_list_wordcode(&mut c);
7289        // c:1731-1736 — `if (tok != OUTBRACE) { lineno += oldlineno; ... }`
7290        if tok() != OUTBRACE_TOK {
7291            set_lineno(lineno() + oldlineno);
7292            ECNPATS.with(|cc| cc.set(onp));
7293            ECSSUB.set(oecssub);
7294            zerr("par_funcdef: expected `}`");
7295            return;
7296        }
7297        // c:1737-1740 — `if (num == 0) { incmdpos = 0; }`
7298        if num == 0 {
7299            set_incmdpos(false);
7300        }
7301        // c:1741 — `zshlex();`
7302        zshlex();
7303    } else if unset(SHORTLOOPS) {
7304        // c:1742-1746 — `lineno += oldlineno; ecnpats = onp; ecssub = oecssub; YYERRORV`
7305        set_lineno(lineno() + oldlineno);
7306        ECNPATS.with(|cc| cc.set(onp));
7307        ECSSUB.set(oecssub);
7308        zerr("par_funcdef: short body requires SHORTLOOPS");
7309        return;
7310    } else {
7311        // c:1748 — `par_list1(&c);`
7312        par_list1_wordcode(&mut c);
7313    }
7314
7315    // c:1750 — `ecadd(WCB_END());`
7316    ecadd(WCB_END());
7317    // c:1751-1754 — fill the 4 metadata slots
7318    let cur_sofs = ECSOFFS.get();
7319    let body_npats = ECNPATS.with(|cc| cc.get());
7320    ECBUF.with_borrow_mut(|b| {
7321        b[m2] = (so - oecssub) as wordcode;
7322        b[m3] = (cur_sofs - so) as wordcode;
7323        b[m4] = body_npats as wordcode;
7324        b[m5] = do_tracing as wordcode;
7325    });
7326    // c:1755 — `ecbuf[p + 1] = num;`
7327    ECBUF.with_borrow_mut(|b| {
7328        b[p1] = num as wordcode;
7329    });
7330
7331    // c:1757 — `ecnpats = onp;`
7332    ECNPATS.with(|cc| cc.set(onp));
7333    // c:1758 — `ecssub = oecssub;`
7334    ECSSUB.set(oecssub);
7335    // c:1759 — `ecnfunc++;`
7336    ECNFUNC.set(ECNFUNC.get() + 1);
7337
7338    // c:1761 — `ecbuf[p] = WCB_FUNCDEF(ecused - 1 - p);`
7339    let used = ECUSED.get() as usize;
7340    ECBUF.with_borrow_mut(|b| {
7341        b[p] = WCB_FUNCDEF((used.saturating_sub(1 + p)) as wordcode);
7342    });
7343
7344    // c:1763-1777 — anonymous-function trailing args (num == 0 case).
7345    if num == 0 {
7346        // c:1766 — `int parg = ecadd(0);`
7347        let parg = ecadd(0);
7348        // c:1767 — `ecadd(0);`
7349        ecadd(0);
7350        // c:1768-1772 — `while (tok == STRING) { ecstr(tokstr); num++; zshlex(); }`
7351        while tok() == STRING_LEX {
7352            ecstr(&tokstr().unwrap_or_default());
7353            num += 1;
7354            zshlex();
7355        }
7356        // c:1773-1774 — `if (num > 0) *cmplx = 1;`
7357        if num > 0 {
7358            *cmplx = 1;
7359        }
7360        // c:1775 — `ecbuf[parg] = ecused - parg;`
7361        // c:1776 — `ecbuf[parg+1] = num;`
7362        let used2 = ECUSED.get() as usize;
7363        ECBUF.with_borrow_mut(|b| {
7364            b[parg] = (used2 - parg) as wordcode;
7365            b[parg + 1] = num as wordcode;
7366        });
7367    }
7368    // c:1778 — `lineno += oldlineno;`
7369    set_lineno(lineno() + oldlineno);
7370}
7371
7372/// Size of `struct fdhead` in `wordcode` (u32) units. Used by all
7373/// the header-walk macros below.
7374pub const FDHEAD_WORDS: usize = size_of::<fdhead>() / 4;
7375
7376/// `Src/parse.c:1619-1665`. Handles both `(...)` subshell and
7377/// `{...}` brace group (cursh) plus optional `always { ... }`
7378/// trailing block. C uses a single function with `zsh_construct=1`
7379/// for `{...}` and 0 for `(...)`.
7380pub fn par_subsh_wordcode(cmplx: &mut i32, zsh_construct: i32) {
7381    // c:1621 — `enum lextok otok = tok;`
7382    let otok = tok();
7383    // c:1622 — `int oecused = ecused, p, pp;`
7384    let _oecused = ECUSED.get() as usize;
7385    let p: usize;
7386    let pp: usize;
7387
7388    // c:1624 — `p = ecadd(0);`
7389    p = ecadd(0);
7390    // c:1625 — `/* Extra word only needed for always block */`
7391    // c:1626 — `pp = ecadd(0);`
7392    pp = ecadd(0);
7393    // c:1627 — `zshlex();`
7394    zshlex();
7395    // c:1628 — `par_list(cmplx);`
7396    par_list_wordcode(cmplx);
7397    // c:1629 — `ecadd(WCB_END());`
7398    ecadd(WCB_END());
7399    // c:1630-1631 — `if (tok != ((otok == INPAR) ? OUTPAR : OUTBRACE))
7400    // YYERRORV(oecused);`
7401    if tok()
7402        != (if otok == INPAR_TOK {
7403            OUTPAR_TOK
7404        } else {
7405            OUTBRACE_TOK
7406        })
7407    {
7408        zerr("par_subsh: missing closing token");
7409        return;
7410    }
7411    // c:1632 — `incmdpos = !zsh_construct;`
7412    set_incmdpos(zsh_construct == 0);
7413    // c:1633 — `zshlex();`
7414    zshlex();
7415
7416    // c:1635 — `/* Optional always block. No intervening SEPERs allowed. */`
7417    // c:1636 — `if (otok == INBRACE && tok == STRING && !strcmp(tokstr, "always")) {`
7418    if otok == INBRACE_TOK && tok() == STRING_LEX && tokstr().as_deref() == Some("always") {
7419        // c:1637 — `ecbuf[pp] = WCB_TRY(ecused - 1 - pp);`
7420        let used = ECUSED.get() as usize;
7421        ECBUF.with_borrow_mut(|b| {
7422            b[pp] = WCB_TRY((used.saturating_sub(1 + pp)) as wordcode);
7423        });
7424        // c:1638 — `incmdpos = 1;`
7425        set_incmdpos(true);
7426        // c:1639-1641 — `do { zshlex(); } while (tok == SEPER);`
7427        loop {
7428            zshlex();
7429            if tok() != SEPER {
7430                break;
7431            }
7432        }
7433
7434        // c:1643-1644 — `if (tok != INBRACE) YYERRORV(oecused);`
7435        if tok() != INBRACE_TOK {
7436            zerr("par_subsh: 'always' expects `{`");
7437            return;
7438        }
7439        // c:1645 — `cmdpop();`
7440        cmdpop();
7441        // c:1646 — `cmdpush(CS_ALWAYS);`
7442        cmdpush(CS_ALWAYS as u8);
7443
7444        // c:1648 — `zshlex();`
7445        zshlex();
7446        // c:1649 — `par_save_list(cmplx);`
7447        par_save_list_wordcode(cmplx);
7448        // c:1650-1651 — `while (tok == SEPER) zshlex();`
7449        while tok() == SEPER {
7450            zshlex();
7451        }
7452
7453        // c:1653 — `incmdpos = 1;`
7454        set_incmdpos(true);
7455
7456        // c:1655-1656 — `if (tok != OUTBRACE) YYERRORV(oecused);`
7457        if tok() != OUTBRACE_TOK {
7458            zerr("par_subsh: 'always' block missing `}`");
7459            return;
7460        }
7461        // c:1657 — `zshlex();`
7462        zshlex();
7463        // c:1658 — `ecbuf[p] = WCB_TRY(ecused - 1 - p);`
7464        let used = ECUSED.get() as usize;
7465        ECBUF.with_borrow_mut(|b| {
7466            b[p] = WCB_TRY((used.saturating_sub(1 + p)) as wordcode);
7467        });
7468    } else {
7469        // c:1660-1661 — `ecbuf[p] = (otok == INPAR ? WCB_SUBSH(...) : WCB_CURSH(...));`
7470        let used = ECUSED.get() as usize;
7471        let off = used.saturating_sub(1 + p);
7472        ECBUF.with_borrow_mut(|b| {
7473            b[p] = if otok == INPAR_TOK {
7474                WCB_SUBSH(off as wordcode)
7475            } else {
7476                WCB_CURSH(off as wordcode)
7477            };
7478        });
7479    }
7480}
7481
7482/// Port of `par_time(void)` from `Src/parse.c:1787`. `time PIPE`
7483/// emits WCB_TIMED(WC_TIMED_PIPE) + the sublist code; bare `time`
7484/// with no pipeline emits WCB_TIMED(WC_TIMED_EMPTY).
7485pub fn par_time_wordcode() {
7486    // c:1791 — `zshlex();`
7487    zshlex();
7488    // c:1793-1794 — `p = ecadd(0); ecadd(0);`
7489    let p = ecadd(0);
7490    ecadd(0);
7491    // c:1795 — `if ((f = par_sublist2(&c)) < 0)`
7492    let mut c = 0i32;
7493    let f = par_sublist2(&mut c);
7494    match f {
7495        Some(flags) => {
7496            // c:1799 — `ecbuf[p] = WCB_TIMED(WC_TIMED_PIPE);`
7497            ECBUF.with_borrow_mut(|b| {
7498                if p < b.len() {
7499                    b[p] = WCB_TIMED(WC_TIMED_PIPE);
7500                }
7501            });
7502            // c:1800 — `set_sublist_code(p+1, WC_SUBLIST_END, f,
7503            // ecused-2-p, c);`
7504            let used = ECUSED.get() as usize;
7505            let skip = used.saturating_sub(2 + p) as i32;
7506            set_sublist_code(p + 1, WC_SUBLIST_END as i32, flags, skip, c != 0);
7507        }
7508        None => {
7509            // c:1796-1798 — `ecused--; ecbuf[p] = WCB_TIMED(WC_TIMED_EMPTY);`
7510            ECUSED.set((ECUSED.get() - 1).max(0));
7511            ECBUF.with_borrow_mut(|b| {
7512                if p < b.len() {
7513                    b[p] = WCB_TIMED(WC_TIMED_EMPTY);
7514                }
7515            });
7516        }
7517    }
7518}
7519
7520/// Port of `par_dinbrack(void)` from `Src/parse.c:1810`. Wraps
7521/// `par_cond` (the cond-expression emitter at parse.c:2409) with
7522/// the `[[ ... ]]` framing: incond/incmdpos toggles + DOUTBRACK
7523/// expectation.
7524pub fn par_cond_wordcode() {
7525    let oecused = ECUSED.get();
7526    // c:1814 — `incond = 1;`
7527    set_incond(1);
7528    // c:1815 — `incmdpos = 0;`
7529    set_incmdpos(false);
7530    // c:1816 — `zshlex();` past `[[`.
7531    zshlex();
7532    // c:1817 — `par_cond();` — call the no-skip cond-expression
7533    // entry that EMITS WORDCODE (par_cond_top → par_cond_1 →
7534    // par_cond_2 → par_cond_double/triple/multi). NOT the AST
7535    // `par_cond` at parse.rs:4644 which is a misnamed `par_dinbrack`
7536    // that skips `[[` AND `]]` and returns a ZshCommand AST node
7537    // instead of pushing WC_COND opcodes. NOT `parse_cond_expr`
7538    // either — that's also AST-only, returning ZshCond. With
7539    // `parse_cond_expr` here, every `[[ ... ]]` test produced ZERO
7540    // wordcode payload and parity dropped ~148 words on /etc/zshrc.
7541    let _ = par_cond_top();
7542    // c:1818-1819 — `if (tok != DOUTBRACK) YYERRORV(oecused);`
7543    if tok() != DOUTBRACK {
7544        let _ = oecused;
7545        zerr("missing ]]");
7546        return;
7547    }
7548    // c:1820 — `incond = 0;`
7549    set_incond(0);
7550    // c:1821 — `incmdpos = 1;`
7551    set_incmdpos(true);
7552    // c:1822 — `zshlex();` past `]]`.
7553    zshlex();
7554}
7555
7556/// Port of the `case DINPAR:` arm of `par_cmd` from
7557/// `Src/parse.c:1031-1034`:
7558/// ```c
7559/// ecadd(WCB_ARITH());
7560/// ecstr(tokstr);
7561/// zshlex();
7562/// ```
7563/// `(( EXPR ))` arithmetic at command position — emits the ARITH
7564/// opcode followed by the interned EXPR string, then advances past
7565/// the DINPAR token (which already carries the body text).
7566pub fn par_arith_wordcode() {
7567    // c:1032 — `ecadd(WCB_ARITH());`
7568    ecadd(WCB_ARITH());
7569    // c:1033 — `ecstr(tokstr);` — interns the expression string and
7570    // appends its strcode index to the wordcode buffer.
7571    let expr = tokstr().unwrap_or_default();
7572    ecstr(&expr);
7573    // c:1034 — `zshlex();`
7574    zshlex();
7575}
7576
7577/// Port of `par_simple(int *cmplx, int nr)` from
7578/// `Src/parse.c:1836-2227`. Emits WC_SIMPLE + word count +
7579/// interned string offsets. Returns `0` when nothing was emitted,
7580/// otherwise `1 + (number of code words consumed by redirections)`.
7581/// The full C body handles assignments (ENVSTRING/ENVARRAY),
7582/// inline `{var}>file` brace-FDs, prefix modifiers (NOCORRECT etc),
7583/// and `name() { body }` funcdef detection — those paths are
7584/// progressively wired into the AST parser; this wordcode-emitter
7585/// covers the simple `cmd args...` case + interleaved redirs.
7586pub fn par_simple_wordcode(cmplx: &mut i32, mut nr: i32) -> i32 {
7587    // c:1838-1841 — `int oecused = ecused, isnull = 1, r, argc = 0,
7588    //   p, isfunc = 0, sr = 0;`
7589    //   `int c = *cmplx, nrediradd, assignments = 0, ppost = 0,
7590    //   is_typeset = 0;`
7591    // c is the SAVED initial cmplx so INOUTPAR can restore via
7592    // `*cmplx = c;` at c:2070.
7593    let _oecused = ECUSED.get() as usize;
7594    let c_saved = *cmplx;
7595    let mut isnull = true;
7596    let mut argc: u32 = 0;
7597    let mut sr: i32 = 0;
7598    let mut assignments = false;
7599    let mut isfunc = false;
7600
7601    // c:1843 — `r = ecused;` — saves the offset where redirs get
7602    // INSERTED (via ecispace). Each redir shifts later words DOWN
7603    // by ncodes, so the SIMPLE placeholder at `p` (set later) must
7604    // also bump by ncodes when a redir lands. C uses `&r` to pass
7605    // the cursor by reference; Rust uses a mutable local + manual
7606    // bumps after each par_redir_wordcode call.
7607    let mut r: usize = ECUSED.get() as usize;
7608
7609    // c:1844-1919 — pre-cmd loop: NOCORRECT, ENVSTRING (scalar
7610    // assigns), ENVARRAY (array assigns), IS_REDIROP. Loops until
7611    // a non-assignment token is seen.
7612    loop {
7613        match tok() {
7614            NOCORRECT => {
7615                // c:1846-1849
7616                *cmplx = 1;
7617                set_nocorrect(1);
7618            }
7619            ENVSTRING => {
7620                // c:1848-1898 — scalar assignment `name=value` or
7621                // `name+=value`. Emits WCB_ASSIGN(SCALAR, NEW|INC, 0)
7622                // followed by ecstr(name), ecstr(value).
7623                let raw = tokstr().unwrap_or_default();
7624                // Find first of Inbrack / '=' / '+' (the C scan at
7625                // c:1851-1853). Inside Inbrack we skipparens — i.e.
7626                // skip `name[...]` index, then continue.
7627                // c:1851-1853 — `for (ptr = tokstr; *ptr && *ptr != Inbrack
7628                // && *ptr != '=' && *ptr != '+'; ptr++); if (*ptr == Inbrack)
7629                // skipparens(Inbrack, Outbrack, &ptr);`. Walk to the first
7630                // `[`/`=`/`+`/Equals-token, then if we landed on `[`, skip
7631                // the balanced `name[index]` pair via skipparens.
7632                let bytes: Vec<char> = raw.chars().collect();
7633                let raw_str: String = bytes.iter().collect();
7634                let mut idx = 0usize;
7635                while idx < bytes.len() {
7636                    let ch = bytes[idx];
7637                    if ch == '\u{91}' /* Inbrack */
7638                        || ch == '=' || ch == '+' || ch == '\u{8d}'
7639                    /* Equals */
7640                    {
7641                        break;
7642                    }
7643                    idx += 1;
7644                }
7645                if idx < bytes.len() && bytes[idx] == '\u{91}'
7646                /* Inbrack */
7647                {
7648                    // c:1855 — `skipparens(Inbrack, Outbrack, &ptr);`.
7649                    let byte_off: usize = bytes[..idx].iter().map(|c| c.len_utf8()).sum();
7650                    let mut cursor: &str = &raw_str[byte_off..];
7651                    let _ = crate::ported::utils::skipparens('\u{91}', '\u{92}', &mut cursor);
7652                    let consumed = raw_str.len() - byte_off - cursor.len();
7653                    let advance_chars = raw_str[byte_off..byte_off + consumed].chars().count();
7654                    idx += advance_chars;
7655                    // Continue scanning for `=` / `+` after the `]`.
7656                    while idx < bytes.len() {
7657                        let ch = bytes[idx];
7658                        if ch == '=' || ch == '+' || ch == '\u{8d}' {
7659                            break;
7660                        }
7661                        idx += 1;
7662                    }
7663                }
7664                let is_inc = idx < bytes.len() && bytes[idx] == '+';
7665                // c:1856-1858 — `if (*ptr == '+') { *ptr++ = '\0';
7666                // ecadd(WCB_ASSIGN(SCALAR, INC, 0)); } else WCB_NEW`
7667                // C nulls the `+` AT THAT POSITION then advances ptr.
7668                // `name` is bytes BEFORE the `+`, NOT including it.
7669                let name_end = idx;
7670                if is_inc {
7671                    idx += 1;
7672                }
7673                let flag = if is_inc { WC_ASSIGN_INC } else { WC_ASSIGN_NEW };
7674                ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, flag, 0));
7675                // c:1860 — `if (*ptr == '=') { *ptr = '\0'; str = ptr + 1; }
7676                //          else equalsplit(tokstr, &str);`
7677                let name: String = bytes[..name_end].iter().collect();
7678                let str_off = if idx < bytes.len() && (bytes[idx] == '=' || bytes[idx] == '\u{8d}')
7679                {
7680                    idx + 1
7681                } else {
7682                    idx
7683                };
7684                let value: String = bytes[str_off..].iter().collect();
7685                // c:1866-1877 — scan value for `=(`/`<(`/`>(` (proc
7686                // subst); if found, bump cmplx (suppresses Z_SIMPLE).
7687                let vbytes: Vec<char> = value.chars().collect();
7688                for (i, ch) in vbytes.iter().enumerate() {
7689                    if i + 1 < vbytes.len() && vbytes[i + 1] == '\u{88}'
7690                    /* Inpar */
7691                    {
7692                        if *ch == '\u{8d}' /* Equals */
7693                            || *ch == '\u{94}' /* Inang */
7694                            || *ch == '\u{96}'
7695                        /* OutangProc */
7696                        {
7697                            *cmplx = 1;
7698                            break;
7699                        }
7700                    }
7701                }
7702                ecstr(&name);
7703                ecstr(&value);
7704                isnull = false;
7705                assignments = true;
7706            }
7707            ENVARRAY => {
7708                // c:1883-1908 — array assignment `name=( ... )` in the
7709                // pre-cmd loop (no `typeset`-style typeset_force flag).
7710                // c:1884 — `int oldcmdpos = incmdpos, n, type2;`
7711                let oldcmdpos = incmdpos();
7712                let n: u32;
7713                let type2: wordcode;
7714                let p: usize;
7715
7716                // c:1886-1889 — `array setting is cmplx because it can
7717                //   contain process substitutions`
7718                // c:1890 — `*cmplx = c = 1;`
7719                *cmplx = 1;
7720                // c:1891 — `p = ecadd(0);`
7721                p = ecadd(0);
7722                // c:1892 — `incmdpos = 0;`
7723                set_incmdpos(false);
7724                // c:1893-1897 — `+=` detection: if tokstr ends in `+`,
7725                // strip the `+` and use WC_ASSIGN_INC; else WC_ASSIGN_NEW.
7726                let raw = tokstr().unwrap_or_default();
7727                let (name, t2) = if raw.ends_with('+') {
7728                    (raw[..raw.len() - 1].to_string(), WC_ASSIGN_INC)
7729                } else {
7730                    (raw.clone(), WC_ASSIGN_NEW)
7731                };
7732                type2 = t2;
7733                // c:1898 — `ecstr(tokstr);` (tokstr now NUL-trimmed)
7734                ecstr(&name);
7735                // c:1899 — `cmdpush(CS_ARRAY);`
7736                cmdpush(CS_ARRAY as u8);
7737                // c:1900 — `zshlex();`
7738                zshlex();
7739                // c:1901 — `n = par_nl_wordlist();`
7740                n = par_nl_wordlist_wordcode();
7741                // c:1902 — `ecbuf[p] = WCB_ASSIGN(WC_ASSIGN_ARRAY, type2, n);`
7742                ECBUF.with_borrow_mut(|b| {
7743                    b[p] = WCB_ASSIGN(WC_ASSIGN_ARRAY, type2, n);
7744                });
7745                // c:1903 — `cmdpop();`
7746                cmdpop();
7747                // c:1904-1905 — `if (tok != OUTPAR) YYERROR(oecused);`
7748                if tok() != OUTPAR_TOK {
7749                    zerr("par_simple: expected `)' after array assignment");
7750                    return 0;
7751                }
7752                // c:1906 — `incmdpos = oldcmdpos;`
7753                set_incmdpos(oldcmdpos);
7754                // c:1907 — `isnull = 0;`
7755                isnull = false;
7756                // c:1908 — `assignments = 1;`
7757                assignments = true;
7758            }
7759            t if IS_REDIROP(t) => {
7760                // c:1900-1904 — `*cmplx = c = 1; nr += par_redir(&r,
7761                // NULL); continue;`. The wordcode-emitting redir is
7762                // distinct from the AST par_redir — it INSERTS
7763                // WCB_REDIR + fd + ecstrcode(name) at offset `r`
7764                // via ecispace, shifting any later words down.
7765                *cmplx = 1;
7766                let added = par_redir_wordcode(&mut r, None);
7767                if added == 0 {
7768                    break;
7769                }
7770                nr += added;
7771                continue;
7772            }
7773            _ => break,
7774        }
7775        zshlex(); // c:1907 `zshlex();`
7776    }
7777
7778    // c:1920-1921 — `if (tok == AMPER || tok == AMPERBANG) YYERROR;`
7779    if tok() == AMPER || tok() == AMPERBANG {
7780        zerr("par_simple: unexpected &");
7781        return 0;
7782    }
7783
7784    // c:1923 — `p = ecadd(WCB_SIMPLE(0));`
7785    let mut p = ecadd(WCB_SIMPLE(0));
7786
7787    // c:1924-2105 — main words loop. is_typeset tracks whether the
7788    // outer command was `typeset`/`export`/etc. so the final
7789    // placeholder gets WCB_TYPESET instead of WCB_SIMPLE.
7790    let mut is_typeset = false;
7791    let mut postassigns: u32 = 0;
7792    let mut ppost: usize = 0;
7793    loop {
7794        match tok() {
7795            STRING_LEX | TYPESET => {
7796                // c:1926 — `int redir_var = 0;`
7797                let mut redir_var = false;
7798                // c:1928-1929 — `*cmplx = 1; incmdpos = 0;`
7799                *cmplx = 1;
7800                set_incmdpos(false);
7801                // c:1931-1932 — TYPESET → intypeset = is_typeset = 1.
7802                if tok() == TYPESET {
7803                    set_intypeset(true);
7804                    is_typeset = true;
7805                }
7806                let s = tokstr().unwrap_or_default();
7807                // c:1934-1974 — `{var}>file` brace-FD detection.
7808                // `if (!isset(IGNOREBRACES) && *tokstr == Inbrace)`
7809                let bytes = s.as_bytes();
7810                let first_is_inbrace = (bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x8f)
7811                    || (bytes.len() >= 1 && bytes[0] == b'{');
7812                if !isset(IGNOREBRACES) && first_is_inbrace {
7813                    // c:1937-1938 — `char *eptr = tokstr + strlen(tokstr) - 1;`
7814                    //                `char *ptr = eptr;`
7815                    // C tests `*eptr == Outbrace` (0x90 marker or `}`) AND
7816                    // there's content between `{` and `}` (`ptr > tokstr + 1`).
7817                    let last_two_outbrace = bytes.len() >= 2
7818                        && (bytes.ends_with(&[0xc2, 0x90]) || bytes.last() == Some(&b'}'));
7819                    let opener_len = if bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x8f {
7820                        2
7821                    } else {
7822                        1
7823                    };
7824                    let closer_len = if bytes.len() >= 2 && bytes.ends_with(&[0xc2, 0x90]) {
7825                        2
7826                    } else if bytes.last() == Some(&b'}') {
7827                        1
7828                    } else {
7829                        0
7830                    };
7831                    if last_two_outbrace && bytes.len() > opener_len + closer_len {
7832                        // c:1944 — `if (itype_end(tokstr+1, IIDENT, 0) >= ptr)`
7833                        // Inner content is the identifier between `{` and `}`.
7834                        let inner_start = opener_len;
7835                        let inner_end = bytes.len() - closer_len;
7836                        let inner = &s[inner_start..inner_end];
7837                        if !inner.is_empty() && crate::ported::params::isident(inner) {
7838                            // c:1946-1948 — `char *idstring = dupstrpfx(...);`
7839                            //                `redir_var = 1; zshlex();`
7840                            let idstring = inner.to_string();
7841                            redir_var = true;
7842                            zshlex();
7843                            // c:1953-1958 — `if (IS_REDIROP(tok) && tokfd == -1)
7844                            //   { *cmplx = c = 1; nrediradd = par_redir(&r, id);
7845                            //     p += nrediradd; sr += nrediradd; }`
7846                            if IS_REDIROP(tok()) && tokfd() == -1 {
7847                                *cmplx = 1;
7848                                let nrediradd = par_redir_wordcode(&mut r, Some(&idstring));
7849                                p += nrediradd as usize;
7850                                sr += nrediradd;
7851                            } else if postassigns > 0 {
7852                                // c:1959-1966 — postassigns path: emit
7853                                // WCB_ASSIGN(SCALAR, INC, 0) + name + ""
7854                                postassigns += 1;
7855                                ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_INC, 0));
7856                                ecstr(&s);
7857                                ecstr("");
7858                            } else {
7859                                // c:1968-1972 — `else { ecstr(toksave); argc++; }`
7860                                ecstr(&s);
7861                                argc += 1;
7862                            }
7863                        }
7864                    }
7865                }
7866                if !redir_var {
7867                    // c:1977-1996 — normal (non-redir-var) STRING/TYPESET.
7868                    if postassigns > 0 {
7869                        // c:1979-1989 — typeset with bare-name arg → INC
7870                        postassigns += 1;
7871                        ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_INC, 0));
7872                        ecstr(&s);
7873                        ecstr("");
7874                    } else {
7875                        ecstr(&s);
7876                        argc += 1;
7877                    }
7878                    zshlex();
7879                }
7880                isnull = false;
7881            }
7882            ENVSTRING => {
7883                // c:2005-2026 — mid-cmd ENVSTRING (under intypeset
7884                // context). Emits WCB_ASSIGN(SCALAR, NEW, 0) then
7885                // ecstr(name) + ecstr(value), tracking the first
7886                // postassign offset in `ppost` (which the trailing
7887                // WCB_TYPESET header points to).
7888                if postassigns == 0 {
7889                    ppost = ecadd(0);
7890                }
7891                postassigns += 1;
7892                // c:2010-2014 — `for (ptr = tokstr; *ptr && *ptr != Inbrack
7893                // && *ptr != '=' && *ptr != '+'; ptr++); if (*ptr == Inbrack)
7894                // skipparens(Inbrack, Outbrack, &ptr);`.
7895                let raw = tokstr().unwrap_or_default();
7896                let bytes: Vec<char> = raw.chars().collect();
7897                let mut idx = 0usize;
7898                while idx < bytes.len() {
7899                    let ch = bytes[idx];
7900                    if ch == '\u{91}' /* Inbrack */
7901                        || ch == '=' || ch == '+' || ch == '\u{8d}'
7902                    /* Equals */
7903                    {
7904                        break;
7905                    }
7906                    idx += 1;
7907                }
7908                if idx < bytes.len() && bytes[idx] == '\u{91}'
7909                /* Inbrack */
7910                {
7911                    // c:2014 — `skipparens(Inbrack, Outbrack, &ptr);`.
7912                    let byte_off: usize = bytes[..idx].iter().map(|c| c.len_utf8()).sum();
7913                    let mut cursor: &str = &raw[byte_off..];
7914                    let _ = crate::ported::utils::skipparens('\u{91}', '\u{92}', &mut cursor);
7915                    let consumed = raw.len() - byte_off - cursor.len();
7916                    let advance_chars = raw[byte_off..byte_off + consumed].chars().count();
7917                    idx += advance_chars;
7918                    while idx < bytes.len() {
7919                        let ch = bytes[idx];
7920                        if ch == '=' || ch == '+' || ch == '\u{8d}' {
7921                            break;
7922                        }
7923                        idx += 1;
7924                    }
7925                }
7926                let name: String = bytes[..idx].iter().collect();
7927                let str_off = if idx < bytes.len() && (bytes[idx] == '=' || bytes[idx] == '\u{8d}')
7928                {
7929                    idx + 1
7930                } else {
7931                    idx
7932                };
7933                let value: String = bytes[str_off..].iter().collect();
7934                ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_NEW, 0));
7935                ecstr(&name);
7936                ecstr(&value);
7937                isnull = false;
7938                zshlex();
7939            }
7940            ENVARRAY => {
7941                // c:2027-2050 — mid-cmd ENVARRAY (typeset N=(…) form).
7942                // C tracks postassigns + ppost the same as ENVSTRING,
7943                // but the inner emit is WCB_ASSIGN(ARRAY, NEW, n)
7944                // with `n` patched in after par_nl_wordlist consumes
7945                // the elements. C also toggles intypeset=0 around the
7946                // wordlist so the lexer doesn't try to re-emit
7947                // assignments inside the array.
7948                *cmplx = 1;
7949                if postassigns == 0 {
7950                    ppost = ecadd(0);
7951                }
7952                postassigns += 1;
7953                let parr = ecadd(0);
7954                let raw = tokstr().unwrap_or_default();
7955                let is_inc = raw.ends_with('+');
7956                let name = if is_inc {
7957                    &raw[..raw.len() - 1]
7958                } else {
7959                    raw.as_str()
7960                };
7961                let flag = if is_inc { WC_ASSIGN_INC } else { WC_ASSIGN_NEW };
7962                ecstr(name);
7963                cmdpush(CS_ARRAY as u8);
7964                set_intypeset(false);
7965                zshlex();
7966                // c:2044 — `n = par_nl_wordlist();` (parse.c:2379-2391).
7967                // SEPER + NEWLIN both allowed between elements.
7968                let mut nelem = 0u32;
7969                loop {
7970                    let t = tok();
7971                    if t != STRING_LEX && t != SEPER && t != NEWLIN {
7972                        break;
7973                    }
7974                    if t == STRING_LEX {
7975                        ecstr(&tokstr().unwrap_or_default());
7976                        nelem += 1;
7977                    }
7978                    zshlex();
7979                }
7980                ECBUF.with_borrow_mut(|b| {
7981                    if parr < b.len() {
7982                        b[parr] = WCB_ASSIGN(WC_ASSIGN_ARRAY, flag, nelem);
7983                    }
7984                });
7985                cmdpop();
7986                set_intypeset(true);
7987                if tok() != OUTPAR_TOK {
7988                    zerr("expected `)' after array assignment");
7989                    return 0;
7990                }
7991                isnull = false;
7992                zshlex();
7993            }
7994            t if IS_REDIROP(t) => {
7995                // c:1999-2010 — `nrediradd = par_redir(&r, NULL);
7996                // p += nrediradd; if (ppost) ppost += nrediradd;
7997                // sr += nrediradd;`
7998                *cmplx = 1;
7999                let added = par_redir_wordcode(&mut r, None);
8000                if added == 0 {
8001                    break;
8002                }
8003                p += added as usize;
8004                if ppost != 0 {
8005                    ppost += added as usize;
8006                }
8007                sr += added;
8008            }
8009            INOUTPAR => {
8010                // c:2051 — `} else if (tok == INOUTPAR) {`
8011                // c:2052 — `zlong oldlineno = lineno;`
8012                let oldlineno = lineno();
8013                // c:2053 — `int onp, so, oecssub = ecssub;`
8014                let oecssub = ECSSUB.get();
8015                // c:2055-2057 — `if (!isset(MULTIFUNCDEF) && argc > 1) YYERROR;`
8016                if !isset(MULTIFUNCDEF) && argc > 1 {
8017                    zerr("par_simple: too many function names for funcdef");
8018                    return 0;
8019                }
8020                // c:2058-2060 — `if (assignments || postassigns) YYERROR;`
8021                if assignments || postassigns > 0 {
8022                    zerr("par_simple: assignments before funcdef");
8023                    return 0;
8024                }
8025                // c:2061-2068 — hasalias check + zwarn — skipped (no
8026                // alias tracking on the wordcode path).
8027
8028                // c:2070 — `*cmplx = c;`
8029                *cmplx = c_saved;
8030                // c:2071 — `lineno = 0;`
8031                set_lineno(0);
8032                // c:2072 — `incmdpos = 1;`
8033                set_incmdpos(true);
8034                // c:2073 — `cmdpush(CS_FUNCDEF);`
8035                cmdpush(CS_FUNCDEF as u8);
8036                // c:2074 — `zshlex();`
8037                zshlex();
8038                // c:2075-2076 — `while (tok == SEPER) zshlex();`
8039                while tok() == SEPER {
8040                    zshlex();
8041                }
8042                // c:2079 — `ecispace(p + 1, 1); ecbuf[p+1] = argc;
8043                // ecadd(0)*4`. Insert the argc word at p+1, then
8044                // append 4 placeholder words.
8045                ecispace(p + 1, 1);
8046                ECBUF.with_borrow_mut(|b| {
8047                    if p + 1 < b.len() {
8048                        b[p + 1] = argc;
8049                    }
8050                });
8051                // c:2080-2083 — four metadata placeholder slots.
8052                ecadd(0);
8053                ecadd(0);
8054                ecadd(0);
8055                ecadd(0);
8056
8057                // c:2085 — `ecnfunc++;`
8058                ECNFUNC.set(ECNFUNC.get() + 1);
8059                // c:2086 — `ecssub = so = ecsoffs;`
8060                let so = ECSOFFS.get();
8061                ECSSUB.set(so);
8062                // c:2087 — `onp = ecnpats;`
8063                let onp = ECNPATS.with(|cc| cc.get());
8064                // c:2088 — `ecnpats = 0;`
8065                ECNPATS.with(|cc| cc.set(0));
8066
8067                // c:2091 — `int c = 0;` — INNER cmplx for the body
8068                // parse. Local to each branch; C's enclosing *cmplx
8069                // is NOT modified by the body.
8070                let mut body_c: i32 = 0;
8071                // c:2090 — `if (tok == INBRACE) {`
8072                if tok() == INBRACE_TOK {
8073                    // c:2093 — `zshlex();`
8074                    zshlex();
8075                    // c:2094 — `par_list(&c);`
8076                    par_list_wordcode(&mut body_c);
8077                    // c:2095-2101 — `if (tok != OUTBRACE) { cmdpop();
8078                    //   lineno += oldlineno; ecnpats = onp;
8079                    //   ecssub = oecssub; YYERROR; }`
8080                    if tok() != OUTBRACE_TOK {
8081                        cmdpop();
8082                        set_lineno(lineno() + oldlineno);
8083                        ECNPATS.with(|cc| cc.set(onp));
8084                        ECSSUB.set(oecssub);
8085                        zerr("par_simple: funcdef expected `}`");
8086                        return 0;
8087                    }
8088                    // c:2102-2105 — `if (argc == 0) incmdpos = 0;`
8089                    if argc == 0 {
8090                        set_incmdpos(false);
8091                    }
8092                    // c:2106 — `zshlex();`
8093                    zshlex();
8094                } else {
8095                    // c:2107-2132 — short-body funcdef form: `f() cmd`
8096                    // or `() cmd`. Wraps single par_cmd result in a
8097                    // synthetic WC_LIST / WC_SUBLIST /
8098                    // WC_PIPE(WC_PIPE_END, 0) header trio.
8099                    let ll = ecadd(0);
8100                    let sl = ecadd(0);
8101                    ecadd(WCB_PIPE(WC_PIPE_END, 0));
8102                    let ok = par_cmd_wordcode(&mut body_c, if argc == 0 { 1 } else { 0 });
8103                    if !ok {
8104                        cmdpop();
8105                        zerr("par_simple: funcdef short-body: missing command");
8106                        return 0;
8107                    }
8108                    if argc == 0 {
8109                        // c:2118-2127 — anonymous funcdef may take args
8110                        // after the body; first one already read.
8111                        set_incmdpos(false);
8112                    }
8113                    // c:2130-2131 — inner sublist/list use inner cmplx.
8114                    let used = ECUSED.get() as usize;
8115                    set_sublist_code(
8116                        sl,
8117                        WC_SUBLIST_END as i32,
8118                        0,
8119                        (used.saturating_sub(1 + sl)) as i32,
8120                        body_c != 0,
8121                    );
8122                    set_list_code(ll, Z_SYNC | Z_END, body_c != 0);
8123                }
8124                let _ = body_c;
8125                // c:2133 — `cmdpop();`
8126                cmdpop();
8127
8128                // c:2135 — `ecadd(WCB_END());`
8129                ecadd(WCB_END());
8130                // c:2136-2139 — fill 4 metadata slots at p+argc+2..5
8131                let p_argc = (p + (argc as usize) + 2) as usize;
8132                let cur_so = ECSOFFS.get();
8133                let np_now = ECNPATS.with(|cc| cc.get());
8134                ECBUF.with_borrow_mut(|b| {
8135                    b[p_argc] = (so - oecssub) as wordcode;
8136                    b[p_argc + 1] = (cur_so - so) as wordcode;
8137                    b[p_argc + 2] = np_now as wordcode;
8138                    b[p_argc + 3] = 0;
8139                });
8140
8141                // c:2141-2143 — `ecnpats = onp; ecssub = oecssub; ecnfunc++;`
8142                ECNPATS.with(|cc| cc.set(onp));
8143                ECSSUB.set(oecssub);
8144                ECNFUNC.set(ECNFUNC.get() + 1);
8145
8146                // c:2145 — `ecbuf[p] = WCB_FUNCDEF(ecused - 1 - p);`
8147                let used = ECUSED.get() as usize;
8148                let header_off = used.saturating_sub(1 + p) as wordcode;
8149                ECBUF.with_borrow_mut(|b| {
8150                    b[p] = WCB_FUNCDEF(header_off);
8151                });
8152
8153                // c:2147-2172 — `if (argc == 0) { /* anonymous fn args */ }`
8154                if argc == 0 {
8155                    // c:2150 — `int parg = ecadd(0);`
8156                    let mut parg = ecadd(0);
8157                    // c:2151 — `ecadd(0);`
8158                    ecadd(0);
8159                    // c:2152 — `while (tok == STRING || IS_REDIROP(tok)) {`
8160                    while tok() == STRING_LEX || IS_REDIROP(tok()) {
8161                        if tok() == STRING_LEX {
8162                            // c:2155-2157
8163                            ecstr(&tokstr().unwrap_or_default());
8164                            argc += 1;
8165                            zshlex();
8166                        } else {
8167                            // c:2159-2165 — *cmplx=c=1; nrediradd=par_redir;
8168                            // p += nrediradd; ppost += nrediradd if ppost;
8169                            // sr += nrediradd; parg += nrediradd;
8170                            *cmplx = 1;
8171                            let added = par_redir_wordcode(&mut r, None);
8172                            if added == 0 {
8173                                break;
8174                            }
8175                            p += added as usize;
8176                            if ppost != 0 {
8177                                ppost += added as usize;
8178                            }
8179                            sr += added;
8180                            parg += added as usize;
8181                        }
8182                    }
8183                    // c:2168-2169 — `if (argc > 0) *cmplx = 1;`
8184                    if argc > 0 {
8185                        *cmplx = 1;
8186                    }
8187                    // c:2170 — `ecbuf[parg] = ecused - parg;`
8188                    // c:2171 — `ecbuf[parg+1] = argc;`
8189                    let used2 = ECUSED.get() as usize;
8190                    ECBUF.with_borrow_mut(|b| {
8191                        b[parg] = (used2 - parg) as wordcode;
8192                        b[parg + 1] = argc;
8193                    });
8194                }
8195                // c:2173 — `lineno += oldlineno;`
8196                set_lineno(lineno() + oldlineno);
8197
8198                // c:2175-2177 — `isfunc = 1; isnull = 0; break;`
8199                isfunc = true;
8200                isnull = false;
8201                break;
8202            }
8203            _ => break,
8204        }
8205    }
8206
8207    // c:2173-2176 — `if (isnull && !(sr + nr)) { ecused = oecused;
8208    // return 0; }` — undo everything including pre-cmd assignments
8209    // if no actual command word emerged.
8210    if isnull && sr + nr == 0 && !assignments {
8211        ECUSED.set(p as i32);
8212        return 0;
8213    }
8214    // c:2186-2187 — `incmdpos = 1; intypeset = 0;` — reset before
8215    // the placeholder patch so the next-token lex doesn't carry
8216    // typeset/incond state.
8217    set_incmdpos(true);
8218    set_intypeset(false);
8219    // c:2189-2199 — `if (!isfunc) { if (is_typeset) ecbuf[p] =
8220    // WCB_TYPESET(argc); else ecbuf[p] = WCB_SIMPLE(argc); }`.
8221    // When isfunc=true the INOUTPAR branch already wrote WCB_FUNCDEF
8222    // at p; do NOT clobber it.
8223    if !isfunc {
8224        let header = if is_typeset {
8225            if postassigns > 0 {
8226                ECBUF.with_borrow_mut(|b| {
8227                    if ppost < b.len() {
8228                        b[ppost] = postassigns;
8229                    }
8230                });
8231            } else {
8232                ecadd(0);
8233            }
8234            WCB_TYPESET(argc)
8235        } else {
8236            WCB_SIMPLE(argc)
8237        };
8238        ECBUF.with_borrow_mut(|b| {
8239            if p < b.len() {
8240                b[p] = header;
8241            }
8242        });
8243    }
8244    1 + sr
8245}
8246
8247/// Port of `par_redir(int *rp, char *idstring)` from
8248/// `Src/parse.c:2229-2345` — the wordcode-emitting variant that
8249/// pushes WCB_REDIR + fd + ecstrcode(name) into ECBUF. Distinct
8250/// from the AST `par_redir` (parse.rs:3771) which builds a
8251/// ZshRedir struct for the AST executor pipeline.
8252///
8253/// Returns the number of wordcodes added (3 for the basic shape,
8254/// 4 with idstring, 5 for HEREDOC[DASH] which carries the
8255/// terminator strings inline). Returns 0 on parse error.
8256///
8257/// `idstring` mirrors C's `char *idstring` parameter — `None` =
8258/// NULL (no `{var}>file` brace-FD shape), `Some(id)` = the captured
8259/// `{var}` name. C callers without a var pass NULL inline; Rust
8260/// callers do the same with `None`.
8261fn par_redir_wordcode(rp: &mut usize, idstring: Option<&str>) -> i32 {
8262    // c:2231 — `int r = *rp, type, fd1, oldcmdpos, oldnc, ncodes;`
8263    let r: usize = *rp;
8264    let mut r#type: i32;
8265    let fd1: i32;
8266    let oldcmdpos: bool;
8267    let oldnc: i32;
8268    let mut ncodes: usize;
8269    // c:2232 — `char *name;`
8270    let name: String;
8271
8272    // c:2234 — `oldcmdpos = incmdpos;`
8273    oldcmdpos = incmdpos();
8274    // c:2235 — `incmdpos = 0;`
8275    set_incmdpos(false);
8276    // c:2236 — `oldnc = nocorrect;`
8277    oldnc = nocorrect();
8278    // c:2237-2238 — `if (tok != INANG && tok != INOUTANG) nocorrect = 1;`
8279    if tok() != INANG_TOK && tok() != INOUTANG {
8280        set_nocorrect(1);
8281    }
8282    // c:2239 — `type = redirtab[tok - OUTANG];`
8283    // Map current redirop token to redirtab index — matches order of
8284    // C `enum { OUTANG, OUTANGBANG, DOUTANG, DOUTANGBANG, INANG,
8285    // INOUTANG, DINANG, DINANGDASH, INANGAMP, OUTANGAMP, AMPOUTANG,
8286    // OUTANGAMPBANG, DOUTANGAMP, DOUTANGAMPBANG, TRINANG }`.
8287    r#type = match tok() {
8288        OUTANG_TOK => REDIR_WRITE,
8289        OUTANGBANG => REDIR_WRITENOW,
8290        DOUTANG => REDIR_APP,
8291        DOUTANGBANG => REDIR_APPNOW,
8292        INANG_TOK => REDIR_READ,
8293        INOUTANG => REDIR_READWRITE,
8294        DINANG => REDIR_HEREDOC,
8295        DINANGDASH => REDIR_HEREDOCDASH,
8296        INANGAMP => REDIR_MERGEIN,
8297        OUTANGAMP => REDIR_MERGEOUT,
8298        AMPOUTANG => REDIR_ERRWRITE,
8299        OUTANGAMPBANG => REDIR_ERRWRITENOW,
8300        DOUTANGAMP => REDIR_ERRAPP,
8301        DOUTANGAMPBANG => REDIR_ERRAPPNOW,
8302        TRINANG => REDIR_HERESTR,
8303        _ => {
8304            set_incmdpos(oldcmdpos);
8305            set_nocorrect(oldnc);
8306            return 0;
8307        }
8308    };
8309    // c:2240 — `fd1 = tokfd;`
8310    fd1 = tokfd();
8311    // c:2241 — `zshlex();`
8312    zshlex();
8313    // c:2242-2243 — `if (tok != STRING && tok != ENVSTRING) YYERROR(ecused);`
8314    if tok() != STRING_LEX && tok() != ENVSTRING {
8315        set_incmdpos(oldcmdpos);
8316        set_nocorrect(oldnc);
8317        zerr("expected word after redirection");
8318        return 0;
8319    }
8320    // c:2244 — `incmdpos = oldcmdpos;`
8321    set_incmdpos(oldcmdpos);
8322    // c:2245 — `nocorrect = oldnc;`
8323    set_nocorrect(oldnc);
8324
8325    // c:2248-2249 — `if (fd1 == -1) fd1 = IS_READFD(type) ? 0 : 1;`
8326    let fd1 = if fd1 == -1 {
8327        if is_readfd(r#type) {
8328            0
8329        } else {
8330            1
8331        }
8332    } else {
8333        fd1
8334    };
8335
8336    // c:2251 — `name = tokstr;`
8337    name = tokstr().unwrap_or_default();
8338
8339    // c:2253-2321 — switch on type:
8340    match r#type {
8341        // c:2254-2300 — REDIR_HEREDOC / REDIR_HEREDOCDASH
8342        x if x == REDIR_HEREDOC || x == REDIR_HEREDOCDASH => {
8343            // c:2257 — `struct heredocs **hd;`
8344            // c:2258 — `int htype = type;`
8345            let htype = r#type;
8346            // c:2260-2261 — `if (strchr(tokstr, '\n')) YYERROR(ecused);`
8347            if name.contains('\n') {
8348                zerr("here-doc terminator contains newline");
8349                return 0;
8350            }
8351            // c:2263-2273 — `ncodes = 5; if (idstring) { type |= MASK; ncodes = 6; }`
8352            if idstring.is_some() {
8353                r#type |= REDIR_VARID_MASK;
8354                ncodes = 6;
8355            } else {
8356                ncodes = 5;
8357            }
8358            // c:2277 — `ecispace(r, ncodes);`
8359            ecispace(r, ncodes);
8360            // c:2278 — `*rp = r + ncodes;`
8361            *rp = r + ncodes;
8362            // c:2279 — `ecbuf[r] = WCB_REDIR(type | REDIR_FROM_HEREDOC_MASK);`
8363            ECBUF.with_borrow_mut(|b| {
8364                b[r] = WCB_REDIR((r#type | REDIR_FROM_HEREDOC_MASK) as wordcode);
8365                // c:2280 — `ecbuf[r + 1] = fd1;`
8366                b[r + 1] = fd1 as wordcode;
8367            });
8368            // c:2282-2286 — r+2..4 are filled later by setheredoc.
8369            // c:2287-2288 — `if (idstring) ecbuf[r + 5] = ecstrcode(idstring);`
8370            if let Some(id) = idstring {
8371                let coded = ecstrcode(id);
8372                ECBUF.with_borrow_mut(|b| {
8373                    b[r + 5] = coded;
8374                });
8375            }
8376            // c:2290-2296 — `for (hd = &hdocs; *hd; hd = &(*hd)->next);
8377            //                 *hd = zalloc(sizeof(struct heredocs));
8378            //                 (*hd)->next = NULL;
8379            //                 (*hd)->type = htype;
8380            //                 (*hd)->pc = r;
8381            //                 (*hd)->str = tokstr;`
8382            HDOCS.with_borrow_mut(|head| {
8383                let mut cur = head;
8384                while cur.is_some() {
8385                    cur = &mut cur.as_mut().unwrap().next; // c:2290
8386                }
8387                *cur = Some(Box::new(crate::ported::zsh_h::heredocs {
8388                    // c:2292-2296
8389                    next: None,
8390                    typ: htype,
8391                    pc: r as i32,
8392                    str: Some(name.clone()),
8393                }));
8394            });
8395            // c:2298 — `zshlex();`
8396            zshlex();
8397            // c:2299 — `return ncodes;`
8398            return ncodes as i32;
8399        }
8400        // c:2301-2308 — REDIR_WRITE / REDIR_WRITENOW
8401        x if x == REDIR_WRITE || x == REDIR_WRITENOW => {
8402            // c:2303-2305 — `if (tokstr[0] == OutangProc && tokstr[1] == Inpar)
8403            //                  type = REDIR_OUTPIPE;`
8404            let nb: Vec<char> = name.chars().collect();
8405            if nb.len() >= 2 && nb[0] == '\u{96}' && nb[1] == '\u{88}' {
8406                r#type = REDIR_OUTPIPE;
8407            } else if nb.len() >= 2 && nb[0] == '\u{94}' && nb[1] == '\u{88}' {
8408                // c:2306-2307 — `else if (tokstr[0] == Inang && tokstr[1] == Inpar) YYERROR;`
8409                zerr("par_redir: < before >");
8410                return 0;
8411            }
8412        }
8413        // c:2309-2315 — REDIR_READ
8414        x if x == REDIR_READ => {
8415            let nb: Vec<char> = name.chars().collect();
8416            if nb.len() >= 2 && nb[0] == '\u{94}' && nb[1] == '\u{88}' {
8417                r#type = REDIR_INPIPE;
8418            } else if nb.len() >= 2 && nb[0] == '\u{96}' && nb[1] == '\u{88}' {
8419                zerr("par_redir: > before <");
8420                return 0;
8421            }
8422        }
8423        // c:2316-2320 — REDIR_READWRITE
8424        x if x == REDIR_READWRITE => {
8425            let nb: Vec<char> = name.chars().collect();
8426            if nb.len() >= 2 && (nb[0] == '\u{94}' || nb[0] == '\u{96}') && nb[1] == '\u{88}' {
8427                r#type = if nb[0] == '\u{94}' {
8428                    REDIR_INPIPE
8429                } else {
8430                    REDIR_OUTPIPE
8431                };
8432            }
8433        }
8434        _ => {}
8435    }
8436    // c:2322 — `zshlex();`
8437    zshlex();
8438
8439    // c:2326-2333 — `if (idstring) { type |= MASK; ncodes = 4; } else ncodes = 3;`
8440    if idstring.is_some() {
8441        r#type |= REDIR_VARID_MASK;
8442        ncodes = 4;
8443    } else {
8444        ncodes = 3;
8445    }
8446
8447    // c:2334 — `ecispace(r, ncodes);`
8448    ecispace(r, ncodes);
8449    // c:2335 — `*rp = r + ncodes;`
8450    *rp = r + ncodes;
8451    // c:2336 — `ecbuf[r] = WCB_REDIR(type);`
8452    let coded_name = ecstrcode(&name);
8453    ECBUF.with_borrow_mut(|b| {
8454        b[r] = WCB_REDIR(r#type as wordcode);
8455        // c:2337 — `ecbuf[r + 1] = fd1;`
8456        b[r + 1] = fd1 as wordcode;
8457        // c:2338 — `ecbuf[r + 2] = ecstrcode(name);`
8458        b[r + 2] = coded_name;
8459    });
8460    // c:2339-2340 — `if (idstring) ecbuf[r + 3] = ecstrcode(idstring);`
8461    if let Some(id) = idstring {
8462        let coded_id = ecstrcode(id);
8463        ECBUF.with_borrow_mut(|b| {
8464            b[r + 3] = coded_id;
8465        });
8466    }
8467    // c:2342 — `return ncodes;`
8468    ncodes as i32
8469}
8470
8471/// Port of `IS_READFD(type)` macro from `Src/zsh.h` — determines
8472/// default fd (0 for read-ish, 1 for write-ish) when none specified.
8473fn is_readfd(t: i32) -> bool {
8474    matches!(
8475        t,
8476        x if x == REDIR_READ
8477            || x == REDIR_READWRITE
8478            || x == REDIR_MERGEIN
8479            || x == REDIR_HEREDOC
8480            || x == REDIR_HEREDOCDASH
8481            || x == REDIR_HERESTR
8482    )
8483}
8484
8485/// Parse a program (list of lists)
8486/// Parse a complete program (top-level entry). Calls
8487/// parse_program_until with no end-token sentinel. Direct port of
8488/// zsh/Src/parse.c:614-720 `parse_event` / `par_list` /
8489/// `par_event` flow. C distinguishes COND_EVENT (single command
8490/// for here-string) from full event parse; zshrs's parse_program
8491/// is the full-event entry.
8492fn parse_program() -> ZshProgram {
8493    parse_program_until(None, false)
8494}
8495
8496/// Parse a program until we hit an end token
8497/// Parse a program until one of `end_tokens` is seen (or EOF).
8498/// Drives par_list in a loop. C equivalent: the body of par_event
8499/// (parse.c:635-695) iterating par_list against the lexer.
8500///
8501/// `single_event` mirrors C par_event's `endtok == ENDINPUT` top-level
8502/// mode: when true, parse ONE event (the sublists `;`/`&`-chained on one
8503/// logical line, including any multi-line compound) and STOP at the
8504/// terminating top-level newline WITHOUT reading the next line — so
8505/// loop() executes the event then re-reads for the next, interleaving
8506/// input with execution exactly as zsh does. Whole-program and
8507/// nested-body parses pass false (multi-list to EOF / end token).
8508fn parse_program_until(end_tokens: Option<&[lextok]>, single_event: bool) -> ZshProgram {
8509    let mut lists = Vec::new();
8510
8511    loop {
8512        // Skip separators
8513        while tok() == SEPER || tok() == NEWLIN {
8514            zshlex();
8515        }
8516
8517        if tok() == ENDINPUT {
8518            break;
8519        }
8520        if tok() == LEXERR {
8521            // c:Src/parse.c:671-680 par_event — when the lexer
8522            // returned LEXERR (e.g. unbalanced `$((1+(2))` math
8523            // sub, unterminated string, etc.), C emits `yyerror(1)`
8524            // and sets errflag so the script aborts with a parse
8525            // error diagnostic + non-zero exit. zshrs's
8526            // parse_program_until previously just `break`'d on
8527            // LEXERR, silently swallowing the malformed input and
8528            // exiting rc=0 — so `$((1+(2))` ran as if it were
8529            // empty. Bug #529 in docs/BUGS.md. Emit yyerror
8530            // mirroring the C behaviour; the broken script then
8531            // surfaces the parse error to the caller.
8532            // c:Src/parse.c — empty-msg yyerror call mapped to the
8533            // C-faithful `yyerror(0)` (zshrs's previous shape).
8534            yyerror(0);
8535            break;
8536        }
8537
8538        // Check for end tokens
8539        if let Some(end_toks) = end_tokens {
8540            if end_toks.contains(&tok()) {
8541                break;
8542            }
8543        }
8544
8545        // Also stop at these tokens when not explicitly looking for them
8546        // Note: Else/Elif/Then are NOT here - they're handled by par_if
8547        // to allow nested if statements inside case arms, loops, etc.
8548        //
8549        // c:Src/parse.c:par_event — when an orphan terminator (DONE
8550        // outside a loop, FI outside an if, ESAC outside a case)
8551        // appears at the top level (end_tokens=None), C errors via
8552        // YYERROR. zshrs's `break` silently accepted `done`/`fi`/
8553        // `esac` as no-op input. Error at the outermost call so
8554        // unscoped terminators don't sneak through; nested calls
8555        // still break cleanly via the end_tokens contains-check
8556        // above.
8557        match tok() {
8558            DONE | FI | ESAC | DOLOOP if end_tokens.is_none() => {
8559                // c:Src/parse.c:par_event — emit the specific token
8560                // name (`done`, `fi`, `esac`, `do`) so error-parsing
8561                // tools can identify the unmatched terminator. C zsh
8562                // writes `parse error near \`<tok>'`; the Rust port
8563                // was emitting a generic "orphan terminator" string.
8564                // Bug #142, #413.
8565                let name = match tok() {
8566                    DONE => "done",
8567                    FI => "fi",
8568                    ESAC => "esac",
8569                    DOLOOP => "do",
8570                    _ => "orphan terminator",
8571                };
8572                zerr(&format!("parse error near `{}'", name));
8573                break;
8574            }
8575            DSEMI | SEMIAMP | SEMIBAR if end_tokens.is_none() => {
8576                // c:Src/parse.c:par_event — case-arm terminators
8577                // (`;;`, `;&`, `;|`) outside a case construct are a
8578                // parse error. zshrs's `break` silently accepted them
8579                // at top level, truncating the rest of the script.
8580                // Bug #141 in docs/BUGS.md.
8581                let name = match tok() {
8582                    DSEMI => ";;",
8583                    SEMIAMP => ";&",
8584                    SEMIBAR => ";|",
8585                    _ => "case terminator",
8586                };
8587                zerr(&format!("parse error near `{}'", name));
8588                break;
8589            }
8590            OUTBRACE_TOK if end_tokens.is_none() => {
8591                // c:Src/parse.c:par_event — orphan `}` (no matching
8592                // `{` opener) at top level is a parse error. zshrs's
8593                // generic break swallowed it silently, leaving the
8594                // `echo a` in `echo a }` running and ignoring the
8595                // stray brace. Bug #168 in docs/BUGS.md.
8596                zerr("parse error near `}'");
8597                break;
8598            }
8599            OUTBRACE_TOK | DSEMI | SEMIAMP | SEMIBAR | DONE | FI | ESAC | ZEND => break,
8600            _ => {}
8601        }
8602
8603        match par_list(single_event) {
8604            Some((list, terminated)) => {
8605                let detected = simple_name_with_inoutpar(&list);
8606                let was_detected = detected.is_some();
8607                lists.push(list);
8608                // Synthesize a FuncDef for the `name() { body }` shape
8609                // at parse time so body_source is captured while the
8610                // lexer still has the input. The lexer port emits
8611                // `name(` as a single Word ending in `<Inpar><Outpar>`,
8612                // so the Simple list is followed by an Inbrace once
8613                // separators are skipped. For `name() cmd args` the
8614                // body has already been swallowed into the same
8615                // Simple's words tail — synthesize directly from there.
8616                if let Some((names, body_argv)) = detected {
8617                    if !body_argv.is_empty() {
8618                        // One-line body already in the Simple. Build
8619                        // a Simple from body_argv as the function body.
8620                        lists.pop();
8621                        let body_simple = ZshCommand::Simple(ZshSimple {
8622                            assigns: Vec::new(),
8623                            words: body_argv,
8624                            redirs: Vec::new(),
8625                        });
8626                        let body_list = ZshList {
8627                            sublist: ZshSublist {
8628                                pipe: ZshPipe {
8629                                    cmd: body_simple,
8630                                    next: None,
8631                                    lineno: lineno(),
8632                                    merge_stderr: false,
8633                                },
8634                                next: None,
8635                                flags: SublistFlags::default(),
8636                            },
8637                            flags: ListFlags::default(),
8638                        };
8639                        let funcdef = ZshCommand::FuncDef(ZshFuncDef {
8640                            names,
8641                            body: Box::new(ZshProgram {
8642                                lists: vec![body_list],
8643                            }),
8644                            tracing: false,
8645                            auto_call_args: None,
8646                            body_source: None,
8647                        });
8648                        let synthetic = ZshList {
8649                            sublist: ZshSublist {
8650                                pipe: ZshPipe {
8651                                    cmd: funcdef,
8652                                    next: None,
8653                                    lineno: lineno(),
8654                                    merge_stderr: false,
8655                                },
8656                                next: None,
8657                                flags: SublistFlags::default(),
8658                            },
8659                            flags: ListFlags::default(),
8660                        };
8661                        lists.push(synthetic);
8662                        continue;
8663                    }
8664                    // Else: words.len() == 1 (only the trailing `name()`
8665                    // word), brace body follows. `names` may carry
8666                    // multiple identifiers from the `fna fnb fnc()`
8667                    // shorthand — all share the same brace body per
8668                    // src/zsh/Src/parse.c:1666 par_funcdef wordlist.
8669                    // Skip separators on the real lexer; safe because
8670                    // parse_program's next iteration would also skip them.
8671                    while tok() == SEPER || tok() == NEWLIN {
8672                        zshlex();
8673                    }
8674                    if tok() == INBRACE_TOK {
8675                        // Capture body_start BEFORE the lexer
8676                        // advances past the first body token. The
8677                        // outer zshlex() consumed `{`; lexer.pos
8678                        // is now right after `{`. The next
8679                        // `zshlex()` would advance past `echo`,
8680                        // making body_start land mid-body and
8681                        // lose the first word — `typeset -f f`
8682                        // printed `a; echo b` instead of
8683                        // `echo a; echo b` for `f() { echo a;
8684                        // echo b }`.
8685                        let body_start = pos();
8686                        zshlex();
8687                        // c:Src/parse.c — synth funcdef body terminates
8688                        // at OUTBRACE_TOK. Explicit end-token avoids
8689                        // the top-level stray-`}` arm. Bug #167/#168.
8690                        let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
8691                        // Bug #642 family: slice THROUGH pos() so the funcdef
8692                        // `}` is always inside the slice, then strip that
8693                        // single trailing brace (+ the `;`/`\n` separator
8694                        // before it). The prior `pos()-1` form left the `}`
8695                        // in `body_source` for the inline `name() { body }`
8696                        // shape (this arm) — so `${functions[name]}` returned
8697                        // `body }` and `.zinit-diff-functions`'
8698                        // `${(qk)functions[@]}` re-parsed it into a
8699                        // `parse error near '}'` on every function. Match the
8700                        // canonical strip used by the other two funcdef arms
8701                        // (parse.rs:2457 / 9528).
8702                        let body_end = pos();
8703                        let body_source = input_slice(body_start, body_end)
8704                            .map(|s| {
8705                                let t = s.trim();
8706                                let t = t.strip_suffix('}').unwrap_or(t).trim_end();
8707                                let t = t
8708                                    .trim_end_matches(|c: char| c == ';' || c == '\n')
8709                                    .trim_end();
8710                                t.to_string()
8711                            })
8712                            .filter(|s| !s.is_empty());
8713                        if tok() == OUTBRACE_TOK {
8714                            zshlex();
8715                        }
8716                        // Replace the Simple list with a FuncDef list.
8717                        lists.pop();
8718                        let funcdef = ZshCommand::FuncDef(ZshFuncDef {
8719                            names,
8720                            body: Box::new(body),
8721                            tracing: false,
8722                            auto_call_args: None,
8723                            body_source,
8724                        });
8725                        let synthetic = ZshList {
8726                            sublist: ZshSublist {
8727                                pipe: ZshPipe {
8728                                    cmd: funcdef,
8729                                    next: None,
8730                                    lineno: lineno(),
8731                                    merge_stderr: false,
8732                                },
8733                                next: None,
8734                                flags: SublistFlags::default(),
8735                            },
8736                            flags: ListFlags::default(),
8737                        };
8738                        lists.push(synthetic);
8739                    } else if !matches!(tok(), ENDINPUT | OUTBRACE_TOK | SEPER | NEWLIN) {
8740                        // No-brace one-line body: `foo() echo hello`.
8741                        // Parse a single command for the body.
8742                        let body_cmd = par_cmd();
8743                        if let Some(cmd) = body_cmd {
8744                            let body_list = ZshList {
8745                                sublist: ZshSublist {
8746                                    pipe: ZshPipe {
8747                                        cmd,
8748                                        next: None,
8749                                        lineno: lineno(),
8750                                        merge_stderr: false,
8751                                    },
8752                                    next: None,
8753                                    flags: SublistFlags::default(),
8754                                },
8755                                flags: ListFlags::default(),
8756                            };
8757                            lists.pop();
8758                            let funcdef = ZshCommand::FuncDef(ZshFuncDef {
8759                                names: names.clone(),
8760                                body: Box::new(ZshProgram {
8761                                    lists: vec![body_list],
8762                                }),
8763                                tracing: false,
8764                                auto_call_args: None,
8765                                body_source: None,
8766                            });
8767                            let synthetic = ZshList {
8768                                sublist: ZshSublist {
8769                                    pipe: ZshPipe {
8770                                        cmd: funcdef,
8771                                        next: None,
8772                                        lineno: lineno(),
8773                                        merge_stderr: false,
8774                                    },
8775                                    next: None,
8776                                    flags: SublistFlags::default(),
8777                                },
8778                                flags: ListFlags::default(),
8779                            };
8780                            lists.push(synthetic);
8781                        }
8782                    }
8783                }
8784                // c:769-803 + c:644-682 — lists only chain across
8785                // explicit SEPER/AMPER/AMPERBANG. When par_list ended
8786                // WITHOUT a terminator, C's grammar says the list is
8787                // over: nested contexts (while-cond, brace bodies)
8788                // return to the caller with the dangling token (so
8789                // `while {false} {print no}` hands the second `{` to
8790                // par_while as the loop body), and the TOP level
8791                // (par_event, c:671-680) yyerrors — `{false} {print
8792                // no}` is `parse error near \`{'` in zsh. The funcdef
8793                // synthesis branch above manages its own following
8794                // tokens (`f() { body }` legitimately has INBRACE
8795                // right after the Simple), so it is exempt.
8796                if !was_detected && !terminated {
8797                    match tok() {
8798                        // End-of-input / lex error: loop top handles.
8799                        ENDINPUT | LEXERR => {}
8800                        // Construct closers and orphan terminators:
8801                        // the loop-top match already errors/breaks
8802                        // for these with the right diagnostics.
8803                        OUTBRACE_TOK | DSEMI | SEMIAMP | SEMIBAR | DONE | FI | ESAC | ZEND
8804                        | DOLOOP | ELSE | ELIF | THEN => {}
8805                        t if end_tokens.map_or(false, |e| e.contains(&t)) => {}
8806                        _ if end_tokens.is_some() => {
8807                            // Nested list context: C par_list just
8808                            // ends; the construct parser (par_while
8809                            // body dispatch, par_subsh closer check)
8810                            // deals with the token.
8811                            break;
8812                        }
8813                        offending => {
8814                            // Top level: C par_event yyerror(1) +
8815                            // errflag, c:671-682. Same tokstr
8816                            // injection as the None arm below so the
8817                            // "near `X'" tail survives punctuation
8818                            // tokens with no tokstr.
8819                            if crate::ported::lex::tokstr().is_none() {
8820                                let i = offending as usize;
8821                                if i < crate::ported::lex::tokstrings.len() {
8822                                    if let Some(s) = crate::ported::lex::tokstrings[i] {
8823                                        crate::ported::lex::set_tokstr(Some(s.to_string()));
8824                                    }
8825                                }
8826                            }
8827                            set_tok(LEXERR); // c:672
8828                            yyerror(1);
8829                            let noerrs_v = *crate::ported::utils::noerrs_lock().lock().unwrap();
8830                            if noerrs_v != 2 {
8831                                errflag.fetch_or(
8832                                    crate::ported::zsh_h::ERRFLAG_ERROR,
8833                                    Ordering::SeqCst,
8834                                );
8835                            }
8836                            break;
8837                        }
8838                    }
8839                }
8840            }
8841            None => {
8842                // c:Src/parse.c:644-645 par_event — `if (tok ==
8843                // ENDINPUT) return 0;`. End-of-input is NORMAL — no
8844                // diagnostic. The yyerror path at c:670-682 fires only
8845                // when par_sublist actually failed (the C `!r` branch).
8846                //
8847                // zshrs's previous fix here unconditionally called
8848                // yyerror on every None return, breaking legitimate
8849                // end-of-input scenarios like `(echo sub)` (par_list
8850                // returns None after the subshell parser consumes the
8851                // whole construct, leaving tok at ENDINPUT).
8852                if tok() == ENDINPUT {
8853                    break;
8854                }
8855                // c:Src/parse.c:671-680 par_event — par_sublist failed:
8856                // emit the canonical yyerror with the "near `X'" tail
8857                // derived from zshlextext/tokstr().
8858                //
8859                // c:Src/lex.c:1965 — `zshlextext = tokstrings[tok]`
8860                // is set DURING zshlex, before set_tok(LEXERR) here.
8861                // zshrs's zshlex doesn't update LEX_TOKSTR for
8862                // single-char punctuation tokens (OUTPAR/INPAR/etc.),
8863                // so by the time yyerror runs, tokstr() is None and
8864                // the tail "near `)'" is lost. Inject the current
8865                // tok's canonical text into LEX_TOKSTR here so the
8866                // C-faithful yyerror lookup finds it. Mirrors the
8867                // visible effect of C's zshlextext fallback.
8868                let already_flagged =
8869                    (errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_ERROR) != 0;
8870                let offending_tok = tok();
8871                if crate::ported::lex::tokstr().is_none() {
8872                    let i = offending_tok as usize;
8873                    if i < crate::ported::lex::tokstrings.len() {
8874                        if let Some(s) = crate::ported::lex::tokstrings[i] {
8875                            crate::ported::lex::set_tokstr(Some(s.to_string()));
8876                        }
8877                    }
8878                }
8879                set_tok(LEXERR); // c:672
8880                yyerror(if already_flagged { 0 } else { 1 });
8881                // c:Src/parse.c:679-680 — `if (noerrs != 2) errflag |=
8882                // ERRFLAG_ERROR;`. C sets errflag explicitly after the
8883                // yyerror(1) print-only branch. Without this, the
8884                // caller (`execute_script_zsh_pipeline` here, `bin_eval`
8885                // for the parse_string path) can't distinguish "no
8886                // parse error" from "parse error already printed", so
8887                // $? stays at 0 after `eval ')foo'`.
8888                let noerrs_v = *crate::ported::utils::noerrs_lock().lock().unwrap();
8889                if noerrs_v != 2 {
8890                    errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::SeqCst);
8891                }
8892                break;
8893            }
8894        }
8895
8896        // c:Src/parse.c par_event — single-event (top-level, endtok ==
8897        // ENDINPUT) mode stops at the terminating top-level newline. The
8898        // SEPER par_list arm above left `tok` AT the newline-SEPER (not
8899        // consumed, LEX_ISNEWLIN > 0), so break before the loop-top
8900        // separator skip would `zshlex()` past it and read the next
8901        // line. A `;`-SEPER (LEX_ISNEWLIN <= 0) or `&` was consumed by
8902        // par_list and leaves a different token here, so chaining keeps
8903        // looping (same logical line). Multi-line compounds were already
8904        // fully consumed inside par_list, so their internal newlines
8905        // never reach this check.
8906        if single_event && tok() == SEPER && crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) > 0
8907        {
8908            break;
8909        }
8910    }
8911
8912    ZshProgram { lists }
8913}
8914
8915/// Parse an assignment
8916/// Parse an assignment word `NAME=value` or `NAME=(arr items)`.
8917/// Sub-routine of par_simple. The C source handles assignments
8918/// inline in par_simple via the ENVSTRING/ENVARRAY token paths
8919/// (parse.c:1842-2000ish); zshrs splits it out to a dedicated
8920/// helper for clarity.
8921fn parse_assign() -> Option<ZshAssign> {
8922    // Helper: locate the Equals-marker that delimits NAME from
8923    // VALUE in an assignment-shaped tokstr. The lexer META-encodes
8924    // EVERY `=` (including those inside `${var%%=foo}` strip
8925    // patterns or `[idx]=...` subscripts), so a naive
8926    // `tokstr.find(Equals)` would split at the first inner `=`
8927    // and break the whole assignment. Walk the string skipping
8928    // brace and bracket depth so the assignment's `=` (the one
8929    // after the last `]` of the LHS subscript / or after the
8930    // bare name) is the one we land on.
8931    fn find_assign_equals(s: &str) -> Option<usize> {
8932        let target = Equals;
8933        let mut brace = 0i32;
8934        let mut bracket = 0i32;
8935        let mut paren = 0i32;
8936        let mut skip_next = false;
8937        for (i, c) in s.char_indices() {
8938            if skip_next {
8939                skip_next = false;
8940                continue;
8941            }
8942            // Bnull/Bnullkeep mark the next char as a backslash-escaped
8943            // literal — `A[\[]=v` keys on `[`, `A[\]]=v` on `]`. The
8944            // escaped bracket/paren/brace is CONTENT, not a depth
8945            // delimiter, so skip the marker and the char it escapes.
8946            // Without this, the escaped `[` over-counted bracket depth
8947            // and the assignment's `=` was never found (depth never
8948            // returned to 0), so the whole assignment was dropped and
8949            // `A[\[]=v; cmd` parse-errored on the `;`.
8950            if c == '\u{9f}' /* Bnull */ || c == '\u{a0}'
8951            /* Bnullkeep */
8952            {
8953                skip_next = true;
8954                continue;
8955            }
8956            match c {
8957                    '{' | '\u{8f}' /* Inbrace */ => brace += 1,
8958                    '}' | '\u{90}' /* Outbrace */ => {
8959                        if brace > 0 {
8960                            brace -= 1;
8961                        }
8962                    }
8963                    '[' | '\u{91}' /* Inbrack */ => bracket += 1,
8964                    ']' | '\u{92}' /* Outbrack */ => {
8965                        if bracket > 0 {
8966                            bracket -= 1;
8967                        }
8968                    }
8969                    '(' | '\u{88}' /* Inpar */ => paren += 1,
8970                    ')' | '\u{8a}' /* Outpar */ => {
8971                        if paren > 0 {
8972                            paren -= 1;
8973                        }
8974                    }
8975                    _ if c == target && brace == 0 && bracket == 0 && paren == 0 => {
8976                        return Some(i);
8977                    }
8978                    _ => {}
8979                }
8980        }
8981        None
8982    }
8983
8984    let _ts_tokstr = tokstr()?;
8985    let tokstr = _ts_tokstr.as_str();
8986
8987    // Parse name=value or name+=value.
8988    let (name, value_str, append) = if tok() == ENVARRAY {
8989        let (name, append) = if let Some(stripped) = tokstr.strip_suffix('+') {
8990            (stripped, true)
8991        } else {
8992            (tokstr, false)
8993        };
8994        (name.to_string(), String::new(), append)
8995    } else if let Some(pos) = find_assign_equals(tokstr) {
8996        let name_part = &tokstr[..pos];
8997        let (name, append) = if let Some(stripped) = name_part.strip_suffix('+') {
8998            (stripped, true)
8999        } else {
9000            (name_part, false)
9001        };
9002        (
9003            name.to_string(),
9004            tokstr[pos + Equals.len_utf8()..].to_string(),
9005            append,
9006        )
9007    } else if let Some(pos) = tokstr.find('=') {
9008        // Fallback to literal '=' for compatibility
9009        let name_part = &tokstr[..pos];
9010        let (name, append) = if let Some(stripped) = name_part.strip_suffix('+') {
9011            (stripped, true)
9012        } else {
9013            (name_part, false)
9014        };
9015        (name.to_string(), tokstr[pos + 1..].to_string(), append)
9016    } else {
9017        return None;
9018    };
9019
9020    let value = if tok() == ENVARRAY {
9021        // Array assignment: name=(...)
9022        // c:Src/parse.c:1895 par_simple ENVARRAY arm:
9023        //   `int oldcmdpos = incmdpos; ... incmdpos = 0; ... zshlex();`
9024        // Reset incmdpos to false BEFORE the array body's first lex so
9025        // a leading `{...}` (brace expansion) doesn't trip the
9026        // empty-buf+incmdpos rule at lex.c:1141 that returns `{` as
9027        // STRING and lets the reswd_lookup promote it to INBRACE_TOK.
9028        let oldcmdpos = crate::ported::lex::incmdpos();
9029        crate::ported::lex::set_incmdpos(false);
9030        // c:Src/parse.c:2041-2046 — `intypeset = 0;` BEFORE reading
9031        // the array body, restored to the saved value after. Without
9032        // this, an element containing `=` (`typeset -a w=( VAR=1
9033        // sudo )`) re-triggers the lexer's intypeset assignment
9034        // detection and lexes `VAR=1` as ENVSTRING instead of a plain
9035        // STRING element — the body loop ends early on the non-STRING
9036        // token and the parser errors "near `)'".
9037        let old_intypeset = crate::ported::lex::intypeset();
9038        crate::ported::lex::set_intypeset(false);
9039        let mut elements = Vec::new();
9040        zshlex(); // skip past token
9041
9042        // c:Src/parse.c:2043 par_nl_wordlist — read array elements until the
9043        // token is no longer a word/separator (the closing `)` ends it). zsh
9044        // imposes NO element-count limit; arrays grow dynamically. A former
9045        // `MAX_ARRAY_ELEMENTS = 10_000` cap here silently truncated (and
9046        // errored on) any larger literal — which broke loading `.zcompdump`
9047        // (`_comps`/`_lastcomp` hold tens of thousands of entries with
9048        // zsh-more-completions) and any big generated array. The loop
9049        // terminates on the closing paren via zshlex advancing each pass.
9050        while matches!(tok(), STRING_LEX | SEPER | NEWLIN) {
9051            if tok() == STRING_LEX {
9052                let _ts_s = crate::ported::lex::tokstr();
9053                if let Some(s) = _ts_s.as_deref() {
9054                    elements.push(s.to_string());
9055                }
9056            }
9057            zshlex();
9058        }
9059        // c:Src/parse.c — `incmdpos = oldcmdpos;` (restore at end of arm)
9060        crate::ported::lex::set_incmdpos(oldcmdpos);
9061        // c:Src/parse.c:2046 — `intypeset = 1;` (restore for any
9062        // follow-on `b=( … )` in `typeset a=(…) b=(…)`).
9063        crate::ported::lex::set_intypeset(old_intypeset);
9064
9065        // The closing Outpar is consumed here. The outer par_simple
9066        // loop will then `zshlex()` past whatever follows (typically
9067        // a separator or the next word) — calling zshlex twice in
9068        // tandem (here AND in par_simple) over-advances and merges
9069        // a following `name() { … }` funcdef into the same Simple.
9070        // We only consume Outpar; let the caller handle the rest.
9071        // Without this guard `g=(o1); f() { :; }` parsed as one
9072        // Simple with assigns=[g] and words=["f()"] (one token).
9073        if tok() == OUTPAR_TOK {
9074            // Note: do NOT zshlex() here. par_simple's `lexer
9075            // .zshlex()` after `parse_assign` returns advances past
9076            // the Outpar onto the next significant token.
9077            //
9078            // Force `incmdpos=true` so the next zshlex() recognizes
9079            // a follow-up `b=(...)` / `b=val` as Envarray/Envstring.
9080            // The lexer flips incmdpos to false on bare Outpar (which
9081            // is correct for subshell-close context), but for an
9082            // array-assignment close more assigns/words may follow.
9083            set_incmdpos(true);
9084        }
9085
9086        ZshAssignValue::Array(elements)
9087    } else {
9088        ZshAssignValue::Scalar(value_str)
9089    };
9090
9091    Some(ZshAssign {
9092        name,
9093        value,
9094        append,
9095    })
9096}
9097
9098/// AST `par_redir` variant accepting an idstring for the
9099/// `{var}>file` brace-FD shape. C signature
9100/// `par_redir(int *rp, char *idstring)` (parse.c:2229). The
9101/// idstring is stored in the resulting ZshRedir.varid for the
9102/// executor to bind the named variable to the chosen fd.
9103fn par_redir_with_id(idstring: Option<&str>) -> Option<ZshRedir> {
9104    let varid: Option<String> = idstring.map(|s| s.to_string());
9105    let rtype = match tok() {
9106        OUTANG_TOK => REDIR_WRITE,
9107        OUTANGBANG => REDIR_WRITENOW,
9108        DOUTANG => REDIR_APP,
9109        DOUTANGBANG => REDIR_APPNOW,
9110        INANG_TOK => REDIR_READ,
9111        INOUTANG => REDIR_READWRITE,
9112        DINANG => REDIR_HEREDOC,
9113        DINANGDASH => REDIR_HEREDOCDASH,
9114        TRINANG => REDIR_HERESTR,
9115        INANGAMP => REDIR_MERGEIN,
9116        OUTANGAMP => REDIR_MERGEOUT,
9117        AMPOUTANG => REDIR_ERRWRITE,
9118        OUTANGAMPBANG => REDIR_ERRWRITENOW,
9119        DOUTANGAMP => REDIR_ERRAPP,
9120        DOUTANGAMPBANG => REDIR_ERRAPPNOW,
9121        _ => return None,
9122    };
9123
9124    let fd = if tokfd() >= 0 {
9125        tokfd()
9126    } else if matches!(
9127        rtype,
9128        REDIR_READ
9129            | REDIR_READWRITE
9130            | REDIR_MERGEIN
9131            | REDIR_HEREDOC
9132            | REDIR_HEREDOCDASH
9133            | REDIR_HERESTR
9134    ) {
9135        0
9136    } else {
9137        1
9138    };
9139
9140    // c:2234-2245 — save/restore incmdpos and nocorrect around the
9141    // zshlex that consumes the redir target word:
9142    //   oldcmdpos = incmdpos; incmdpos = 0;
9143    //   oldnc = nocorrect;
9144    //   if (tok != INANG && tok != INOUTANG) nocorrect = 1;
9145    //   ... zshlex; check tok; ...
9146    //   incmdpos = oldcmdpos; nocorrect = oldnc;
9147    // Without this, a redir target lexes in the parent's incmdpos
9148    // (re-promoting `{` / reswords) AND with parent nocorrect (so
9149    // spelling-correction wrongly runs inside `> $(cmd)` etc.).
9150    let oldcmdpos = incmdpos();
9151    set_incmdpos(false);
9152    let oldnc = nocorrect();
9153    let cur = tok();
9154    if cur != INANG_TOK && cur != INOUTANG {
9155        set_nocorrect(1);
9156    }
9157    zshlex();
9158
9159    let name = match tok() {
9160        STRING_LEX | ENVSTRING => {
9161            let n = tokstr().unwrap_or_default();
9162            // c:2244-2245 — restore incmdpos / nocorrect right after
9163            // the redir target word is confirmed, BEFORE the trailing
9164            // zshlex advances past it. The advance itself is deferred
9165            // below so REDIR_HEREDOC[DASH] can push onto HDOCS first
9166            // (matching the wordcode variant at parse.rs:6894-6908) —
9167            // otherwise the NEWLIN drained by that zshlex sees an
9168            // empty HDOCS list and gethere never collects the body.
9169            set_incmdpos(oldcmdpos);
9170            set_nocorrect(oldnc);
9171            n
9172        }
9173        _ => {
9174            set_incmdpos(oldcmdpos);
9175            set_nocorrect(oldnc);
9176            zerr("expected word after redirection");
9177            return None;
9178        }
9179    };
9180
9181    // Heredoc terminator capture. C parse.c:2254-2317 par_redir builds
9182    // a `struct heredocs` entry here for REDIR_HEREDOC[DASH]. zshrs
9183    // pushes onto HDOCS (canonical C linked list, c:2290-2296) AND
9184    // onto LEX_HEREDOCS (Rust-only AST-glue Vec carrying parsed-out
9185    // terminator/strip_tabs/quoted metadata for downstream AST
9186    // consumers). Quoted terminators (`<<'EOF'` / `<<"EOF"` / `<<\EOF`)
9187    // disable expansion in the body — Snull `\u{9d}` marks single-quote,
9188    // Dnull `\u{9e}` marks double-quote, Bnull `\u{9f}` marks
9189    // backslash-escaped chars.
9190    let heredoc_idx = if matches!(rtype, REDIR_HEREDOC | REDIR_HEREDOCDASH) {
9191        let strip_tabs = rtype == REDIR_HEREDOCDASH;
9192        let quoted = name.contains('\u{9d}')
9193            || name.contains('\u{9e}')
9194            || name.contains('\u{9f}')
9195            || name.starts_with('\'')
9196            || name.starts_with('"');
9197        let term = name
9198            .chars()
9199            .filter(|c| {
9200                *c != '\'' && *c != '"' && *c != '\u{9d}' && *c != '\u{9e}' && *c != '\u{9f}'
9201            })
9202            .collect::<String>();
9203        // c:2290-2296 — `for (hd = &hdocs; *hd; hd = &(*hd)->next);
9204        //                 *hd = zalloc(sizeof(struct heredocs));
9205        //                 (*hd)->next = NULL;
9206        //                 (*hd)->type = htype;
9207        //                 (*hd)->pc = r;
9208        //                 (*hd)->str = tokstr;`
9209        // AST path has no wordcode pc to patch; use -1 sentinel so the
9210        // inline NEWLIN walk in `zshlex()` skips the setheredoc call.
9211        HDOCS.with_borrow_mut(|head| {
9212            let mut cur = head;
9213            while cur.is_some() {
9214                cur = &mut cur.as_mut().unwrap().next; // c:2290
9215            }
9216            *cur = Some(Box::new(crate::ported::zsh_h::heredocs {
9217                // c:2292-2296
9218                next: None,
9219                typ: rtype,
9220                pc: -1,
9221                str: Some(name.clone()),
9222            }));
9223        });
9224        // zshrs-only: push parallel AST-glue entry onto LEX_HEREDOCS.
9225        let idx = LEX_HEREDOCS.with_borrow_mut(|v| {
9226            v.push(HereDoc {
9227                terminator: term,
9228                strip_tabs,
9229                content: String::new(),
9230                quoted,
9231                processed: false,
9232            });
9233            v.len() - 1
9234        });
9235        Some(idx)
9236    } else {
9237        None
9238    };
9239
9240    // c:2298 (heredoc) / c:2322 (other redirs) — final zshlex() advance
9241    // past the redir target word. MUST run after the HDOCS push above
9242    // so the heredoc-drain inside this zshlex sees the new entry. For
9243    // non-heredoc forms the order is irrelevant; consolidating to a
9244    // single tail-call here matches the wordcode variant.
9245    zshlex();
9246
9247    Some(ZshRedir {
9248        rtype,
9249        fd,
9250        name,
9251        heredoc: None,
9252        varid,
9253        heredoc_idx,
9254    })
9255}
9256
9257/// Parse C-style for loop: for (( init; cond; step ))
9258/// Parse the c-style `for ((init; cond; incr)) do BODY done`.
9259/// Inner branch of zsh/Src/parse.c:1100-1140 inside par_for.
9260/// Recognized when the token after FOR is DINPAR (the `((`
9261/// detected by gettok via dbparens setup).
9262fn parse_for_cstyle() -> Option<ZshCommand> {
9263    // We're at (( (Dinpar None) - the opening ((
9264    // Lexer returns:
9265    //   Dinpar None     - opening ((
9266    //   Dinpar "init"   - init expression, semicolon consumed
9267    //   Dinpar "cond"   - cond expression, semicolon consumed
9268    //   Doutpar "step"  - step expression, closing )) consumed
9269    zshlex(); // Get init: Dinpar "i=0"
9270
9271    if tok() != DINPAR {
9272        zerr("expected init expression in for ((");
9273        return None;
9274    }
9275    let init = tokstr().unwrap_or_default();
9276
9277    zshlex(); // Get cond: Dinpar "i<10"
9278
9279    if tok() != DINPAR {
9280        zerr("expected condition in for ((");
9281        return None;
9282    }
9283    let cond = tokstr().unwrap_or_default();
9284
9285    zshlex(); // Get step: Doutpar "i++"
9286
9287    if tok() != DOUTPAR {
9288        zerr("expected )) in for");
9289        return None;
9290    }
9291    let step = tokstr().unwrap_or_default();
9292
9293    // c:1110 — `infor = 0;` before the body opener. The companion
9294    // `incmdpos = 1;` at c:1111 is intentionally skipped here for
9295    // the same reason c:1094's `incmdpos = 0;` is skipped in
9296    // par_for above — zshrs doesn't mirror the full
9297    // incmdpos state-machine inline.
9298    set_infor(0); // c:1110
9299    zshlex(); // Move past ))
9300
9301    skip_separators();
9302    let body = parse_loop_body(false, false)?;
9303
9304    Some(ZshCommand::For(ZshFor {
9305        var: String::new(),
9306        list: ForList::CStyle { init, cond, step },
9307        body: Box::new(body),
9308        is_select: false,
9309    }))
9310}
9311
9312/// Parse select loop (same syntax as for)
9313/// Parse `select NAME in WORDS; do BODY; done`. Same shape as
9314/// `for NAME in WORDS; do ...` but with menu-prompt semantics in
9315/// the executor. C equivalent: the SELECT case in par_for at
9316/// parse.c:1087-1207 (selects share parser flow with foreach).
9317fn parse_select() -> Option<ZshCommand> {
9318    // `select` shares par_for's grammar (var, words, body) but the
9319    // compile path is different (interactive prompt loop).
9320    match par_for()? {
9321        ZshCommand::For(mut f) => {
9322            f.is_select = true;
9323            Some(ZshCommand::For(f))
9324        }
9325        other => Some(other),
9326    }
9327}
9328
9329/// Parse loop body (do...done, {...}, or shortloop)
9330/// Parse the `do BODY done` body of a for/while/until/select/
9331/// repeat loop. Direct equivalent of zsh's parse.c handling
9332/// inside the loop builders — they all consume DOLOOP, parse a
9333/// list until DONE, and return the list. The `foreach_style`
9334/// flag signals foreach (where short-form `for NAME in WORDS;
9335/// CMD` may skip do/done) vs c-style (which always requires
9336/// do/done).
9337///
9338/// `is_repeat` widens the SHORTLOOPS gate so `SHORTREPEAT` also
9339/// unlocks the short form for `repeat N CMD` (per c:1600
9340/// `unset(SHORTLOOPS) && unset(SHORTREPEAT)`).
9341fn parse_loop_body(foreach_style: bool, is_repeat: bool) -> Option<ZshProgram> {
9342    // c:1180-1194 — body dispatch order per par_for:
9343    //   `do ... done` (DOLOOP) — primary form.
9344    //   `{ ... }`   (INBRACE) — alternate.
9345    //   csh/CSHJUNKIELOOPS — terminator is `end`.
9346    //   else if (unset(SHORTLOOPS)) — YYERROR.
9347    //   else — short form (single command).
9348    if tok() == DOLOOP {
9349        zshlex();
9350        // Body parse must declare DONE as an end-token so the
9351        // parse_program_until top-level orphan-DONE guard doesn't
9352        // mis-fire on the legitimate loop terminator.
9353        let body = parse_program_until(Some(&[DONE]), false);
9354        // c:Src/parse.c:1182-1183 / :1535-1536 / :1597-1598 —
9355        // `if (tok != DONE) YYERRORV(oecused);`. zshrs previously
9356        // silently accepted EOF as a substitute for `done`, so
9357        // `for i in a; do echo hi; don` ran the loop with `don` as
9358        // a command (which then failed "command not found") instead
9359        // of erroring at parse time. Bug #403, #404.
9360        if tok() != DONE {
9361            zerr("parse error: expected `done'");
9362            return None;
9363        }
9364        zshlex();
9365        Some(body)
9366    } else if tok() == INBRACE_TOK {
9367        zshlex();
9368        let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9369        // c:Src/parse.c:1186 / :1539 — `if (tok != OUTBRACE) YYERRORV`.
9370        if tok() != OUTBRACE_TOK {
9371            zerr("parse error: expected `}'");
9372            return None;
9373        }
9374        zshlex();
9375        Some(body)
9376    } else if foreach_style || isset(CSHJUNKIELOOPS) {
9377        // c:1184 / 1546 / 1595 — `else if (csh || isset(CSHJUNKIELOOPS))`.
9378        let body = parse_program_until(Some(&[ZEND]), false);
9379        // c:1190 / 1548 — `if (tok != ZEND) YYERRORV`.
9380        if tok() != ZEND {
9381            zerr("parse error: expected `end'");
9382            return None;
9383        }
9384        zshlex();
9385        Some(body)
9386    } else {
9387        // c:1190 / 1474 / 1551 / 1600 — short-form gate. C bails
9388        // with YYERROR when `unset(SHORTLOOPS) && (!is_repeat ||
9389        // unset(SHORTREPEAT))`. zshrs's option machinery isn't
9390        // initialised at parse-test time (no `init_main` →
9391        // `install_emulation_defaults`), so a strict port here
9392        // body. parse_init seeds SHORTLOOPS=on mirroring C
9393        // `install_emulation_defaults`, so this fires only when a
9394        // script explicitly disabled the option.
9395        if unset(SHORTLOOPS) && (!is_repeat || unset(SHORTREPEAT)) {
9396            zerr("parse error: short loop form requires SHORTLOOPS option");
9397            return None;
9398        }
9399        // c:Src/parse.c:1604 / :1474 / :1551 — short form calls
9400        // par_save_list1 → par_list1 → par_sublist, which parses
9401        // ONE sublist and leaves the trailing SEPER untouched for
9402        // the outer par_list to consume. zshrs previously routed
9403        // through par_list() which consumes the trailing `;`/`\n`
9404        // separator — that swallowed the separator between the
9405        // loop's body command and the next outer command, so
9406        // `repeat 2 print x; print y` parsed as repeat-then-eof
9407        // and par_cmd's post-compound STRING_LEX guard at parse.rs
9408        // line 1170 fired "parse error near `print'". Bug #593.
9409        par_list1().map(|sublist| ZshProgram {
9410            lists: vec![ZshList {
9411                sublist,
9412                flags: ListFlags::default(),
9413            }],
9414        })
9415    }
9416}
9417
9418/// `() { body } arg1 arg2 …` — anonymous function. Defines a fresh
9419/// function named `_zshrs_anon_N`, invokes it with the args, and the
9420/// body runs with positional params set. Implemented as the desugared
9421/// pair (FuncDef + Simple call) so the compile path doesn't need new
9422/// machinery.
9423/// Parse an anonymous function definition `() { BODY }` followed
9424/// by call args. zsh treats `() { echo hi; } a b c` as defining
9425/// and immediately calling an anon fn with args a/b/c. C
9426/// equivalent: the INOUTPAR shape in par_simple at parse.c:1836+
9427/// triggers an anon-funcdef path.
9428fn parse_anon_funcdef() -> Option<ZshCommand> {
9429    // Track the byte position just past the `()` / most-recent separator so an
9430    // UNBRACED body's raw text can be sliced for `functions` rendering. pos()
9431    // AFTER a zshlex is already past the next token (one-token lookahead), so
9432    // body_start must be recorded BEFORE each advance. Mirrors the braced
9433    // path's body_start-before-zshlex capture (parse.rs:2484).
9434    let mut body_start = pos();
9435    zshlex(); // skip ()
9436    while tok() == SEPER || tok() == NEWLIN {
9437        body_start = pos();
9438        zshlex();
9439    }
9440    // c:Src/parse.c:1728-1748 — after `()` (and any separators, skipped at
9441    // c:1720), the body is either a braced `{ … }` (par_list) OR, with
9442    // SHORTLOOPS set (the default), a single UNBRACED command (par_list1):
9443    // `() print hi` runs an anon fn whose body is `print hi`, and
9444    // `f() () print hi` nests one as another function's body. A bodyless `()`
9445    // is a parse error in zsh ("parse error near `()'"), which par_cmd()
9446    // produces naturally when the next token can't start a command (`}`, EOF).
9447    // The previous port returned an empty subshell here, so `() print hi`
9448    // hit the outer "parse error near `print'" and bare `()` wrongly succeeded.
9449    if tok() != INBRACE_TOK {
9450        if unset(SHORTLOOPS) {
9451            // c:1742 — `else if (unset(SHORTLOOPS)) YYERRORV`.
9452            zerr("parse error: short function body form requires SHORTLOOPS option");
9453            return None;
9454        }
9455        // c:1747-1748 — `else par_list1(&c)`: ONE command is the body.
9456        // Slice the raw body text (body_start was captured before the body
9457        // token was lexed, above) so `functions`/`typeset -f` can render it:
9458        // fusevm shfuncs carry no C-shaped Eprog, so hashtable.rs:1397-1414
9459        // renders from this raw `body` string, not getpermtext. Without it the
9460        // body listed as `f () { }` (empty).
9461        let cmd = par_cmd()?;
9462        let body_source = input_slice(body_start, pos())
9463            .map(|s| {
9464                s.trim()
9465                    .trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace())
9466                    .to_string()
9467            })
9468            .filter(|s| !s.is_empty());
9469        let list = ZshList {
9470            sublist: ZshSublist {
9471                pipe: ZshPipe {
9472                    cmd,
9473                    next: None,
9474                    lineno: lineno(),
9475                    merge_stderr: false,
9476                },
9477                next: None,
9478                flags: SublistFlags::default(),
9479            },
9480            flags: ListFlags::default(),
9481        };
9482        static ANON_UNBRACED_COUNTER: AtomicUsize = AtomicUsize::new(0);
9483        let n = ANON_UNBRACED_COUNTER.fetch_add(1, Ordering::Relaxed);
9484        let name = format!("_zshrs_anon_{}", n);
9485        // No trailing args for the unbraced form (the whole command IS the
9486        // body); call with an empty arg list so the anon fn still executes.
9487        return Some(ZshCommand::FuncDef(ZshFuncDef {
9488            names: vec![name],
9489            body: Box::new(ZshProgram { lists: vec![list] }),
9490            tracing: false,
9491            auto_call_args: Some(Vec::new()),
9492            body_source,
9493        }));
9494    }
9495    zshlex(); // skip {
9496              // c:Src/parse.c:par_subsh — anon `() { … }` body must terminate at
9497              // OUTBRACE_TOK. Pass it as the explicit end-token so the inner
9498              // parse stops cleanly at `}` rather than hitting the top-level
9499              // stray-`}` arm (#168). Bug #167 family.
9500    let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9501    // c:Src/parse.c:1733-1737 — same `if (tok != OUTBRACE) YYERRORV`
9502    // gate as the named-funcdef path. Bug #405 sibling.
9503    if tok() != OUTBRACE_TOK {
9504        zerr("parse error: expected `}'");
9505        return None;
9506    }
9507    zshlex();
9508    // Collect trailing args AND redirections, interleaved in any order,
9509    // until a separator. zsh's anon-fn form `() { body } a b c` runs
9510    // body with $1=a, $2=b, $3=c; redirs apply to that invocation:
9511    // `() { read v; print $1 $v } <input1 Shirley >output1 dude`
9512    // (c:Src/parse.c par_simple — the anon function is a simple command
9513    // whose words and redirs follow the body in any order). The previous
9514    // port collected ONLY leading STRING args here and left redirs to
9515    // par_cmd's trailing loop, so an arg AFTER a redir (`<in arg`) was
9516    // never reached and hit par_cmd's "parse error near `<arg>'" gate.
9517    let mut args = Vec::new();
9518    let mut redirs: Vec<ZshRedir> = Vec::new();
9519    loop {
9520        let t = tok();
9521        if t == STRING_LEX {
9522            if let Some(s) = tokstr() {
9523                args.push(s);
9524            }
9525            zshlex();
9526        } else if IS_REDIROP(t) {
9527            match par_redir() {
9528                Some(r) => redirs.push(r),
9529                None => break,
9530            }
9531        } else {
9532            break;
9533        }
9534    }
9535
9536    // Generate a unique name. Module-level static would be cleaner but
9537    // a thread-local atomic is enough — anonymous functions are
9538    // ephemeral and the name isn't user-visible.
9539    static ANON_COUNTER: AtomicUsize = AtomicUsize::new(0);
9540    let n = ANON_COUNTER.fetch_add(1, Ordering::Relaxed);
9541    let name = format!("_zshrs_anon_{}", n);
9542    let funcdef = ZshCommand::FuncDef(ZshFuncDef {
9543        names: vec![name],
9544        body: Box::new(body),
9545        tracing: false,
9546        auto_call_args: Some(args),
9547        body_source: None,
9548    });
9549    // Wrap in Redirected so the redirs bracket the single invocation
9550    // (compile_zsh.rs:929 wraps the body in a WithRedirectsBegin/End
9551    // scope for Redirected(FuncDef, …)). par_cmd's trailing-redir loop
9552    // then finds none and returns this node unwrapped.
9553    if redirs.is_empty() {
9554        Some(funcdef)
9555    } else {
9556        Some(ZshCommand::Redirected(Box::new(funcdef), redirs))
9557    }
9558}
9559
9560/// Parse {...} cursh
9561/// Parse a current-shell brace block `{ BODY }`. C source
9562/// par_cmd at parse.c:958-1085 handles Inbrace → emit WC_CURSH
9563/// and recurses into the list. zshrs's parse_cursh extracts that
9564/// arm into a dedicated method.
9565fn parse_cursh() -> Option<ZshCommand> {
9566    zshlex(); // skip {
9567              // c:Src/parse.c:par_subsh — pass OUTBRACE_TOK as the explicit
9568              // body terminator so the inner parse stops cleanly at `}` rather
9569              // than falling through the top-level `OUTBRACE_TOK if
9570              // end_tokens.is_none()` arm (which errors on stray `}` per bug
9571              // #168). Bug #167 in docs/BUGS.md.
9572    let prog = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9573
9574    // c:Src/parse.c:par_subsh — `{ … }` requires a matching `}`.
9575    // C errors via YYERRORV when the body parse returns without
9576    // seeing OUTBRACE_TOK (parse.c:1623 inbrack check). zshrs's
9577    // previous behavior silently returned `Cursh(prog)` and ran the
9578    // body as if the braces were absent. Bug #167 in docs/BUGS.md.
9579    if tok() != OUTBRACE_TOK {
9580        // Reuse the "parse error near `<tok>'" shape from #142/#161.
9581        // The offending token is whatever follows the unclosed brace
9582        // body. For EOF (`{ echo a` at end of input) C zsh errors
9583        // near the LAST consumed body token; we use the current
9584        // tokstr() or fall back to a "}" hint.
9585        let near = tokstr().unwrap_or_else(|| "}".to_string());
9586        zerr(&format!("parse error near `{}'", near));
9587        return None;
9588    }
9589    // Check for { ... } always { ... }. Direct port of zsh's
9590    // par_subsh at parse.c:1612-1660 — note the two `incmdpos = 1`
9591    // forces (parse.c:1632, 1637): after consuming the closing
9592    // Outbrace AND after matching the `always` keyword, the parser
9593    // explicitly resets command position so the next `{` lexes as
9594    // Inbrace. Without these resets the lexer's String-clears-cmdpos
9595    // rule (lex.rs:976-983) leaves the second `{` in word position,
9596    // turning `always { ... }` into a Simple `{` `echo` … and the
9597    // try/always pairing is silently lost.
9598    {
9599        set_incmdpos(true); // parse.c:1632 incmdpos = !zsh_construct
9600        zshlex();
9601
9602        // Check for 'always'
9603        if tok() == STRING_LEX {
9604            let s = tokstr();
9605            if s.map(|s| s == "always").unwrap_or(false) {
9606                set_incmdpos(true); // parse.c:1637 incmdpos = 1
9607                zshlex();
9608                skip_separators();
9609
9610                if tok() == INBRACE_TOK {
9611                    zshlex();
9612                    // c:Src/parse.c — always-clause body terminates at
9613                    // OUTBRACE_TOK. Bug #167/#168 family.
9614                    let always = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9615                    if tok() == OUTBRACE_TOK {
9616                        zshlex();
9617                    }
9618                    return Some(ZshCommand::Try(ZshTry {
9619                        try_block: Box::new(prog),
9620                        always: Box::new(always),
9621                    }));
9622                }
9623            }
9624        }
9625    }
9626
9627    Some(ZshCommand::Cursh(Box::new(prog)))
9628}
9629
9630/// Parse inline function definition: name() { ... }
9631/// Parse the inline form `NAME () { BODY }` (POSIX-style funcdef
9632/// without the `function` keyword). The name has already been
9633/// consumed and pushed by par_simple before this method fires.
9634/// C source: handled inline in par_simple's INOUTPAR-after-name
9635/// arm (parse.c:1836-2228).
9636fn parse_inline_funcdef(names: Vec<String>) -> Option<ZshCommand> {
9637    // par_simple's STRING loop left `incmdpos = 0`; the funcdef body
9638    // `{ ... }` requires `incmdpos = 1` so the lexer recognises `{`
9639    // as INBRACE_TOK (current-shell block opener) instead of a
9640    // literal `{` STRING. Without this, `myfunc() { echo body }`
9641    // parsed the body as the single STRING `"{"`, then `echo body`
9642    // fell out at top level. Mirrors the C path where par_cmd's
9643    // dispatcher (parse.c:958) is called with `incmdpos = 1` for
9644    // the funcdef body.
9645    set_incmdpos(true);
9646    // Track the byte position just past `()` / the most-recent separator, so an
9647    // UNBRACED short body (`f() cmd`) can be sliced for `functions` rendering.
9648    // pos() AFTER a zshlex is already past the next token (one-token
9649    // lookahead), so record it BEFORE each advance (mirrors parse.rs:2484).
9650    let mut unbraced_body_start = pos();
9651    // Skip ()
9652    if tok() == INOUTPAR {
9653        zshlex();
9654    }
9655
9656    while tok() == SEPER || tok() == NEWLIN {
9657        unbraced_body_start = pos();
9658        zshlex();
9659    }
9660
9661    // Parse body
9662    if tok() == INBRACE_TOK {
9663        // Same body_start-before-zshlex fix as par_funcdef.
9664        let body_start = pos();
9665        zshlex();
9666        // c:Src/parse.c — inline funcdef body terminates at OUTBRACE_TOK.
9667        // Explicit end-token keeps the inner parse from hitting the
9668        // top-level stray-`}` arm (#168). Bug #167 family.
9669        let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9670        // c:Src/parse.c:1733-1737 — `if (tok != OUTBRACE) { cmdpop();
9671        // lineno += oldlineno; ecnpats = onp; ecssub = oecssub;
9672        // YYERRORV(oecused); }`. Without this gate, `f() { echo hi`
9673        // silently registered as a complete fn with body `echo hi`.
9674        // Bug #405.
9675        if tok() != OUTBRACE_TOK {
9676            zerr("parse error: expected `}'");
9677            return None;
9678        }
9679        // Slice THROUGH pos() (not `pos()-1`). pos() sits just past the
9680        // funcdef `}` and any trailing whitespace/newline the lexer
9681        // consumed, so `[body_start, pos())` always contains the funcdef
9682        // `}`. Using `pos()-1` landed ON the `}` at end-of-input (no
9683        // trailing separator) and thus EXCLUDED it, so the subsequent
9684        // `strip_suffix('}')` chopped a legitimate inner `}` instead —
9685        // corrupting any body ending in a balanced `} always { ... }`
9686        // into `par_subsh: 'always' block missing }` on the `.zwc`
9687        // source path (getpermtext re-emits the file with no trailing
9688        // newline, so the last funcdef `}` sits at EOF). Bug #642.
9689        let body_end = pos();
9690        let body_source = input_slice(body_start, body_end)
9691            .map(|s| {
9692                // The slice now always ends with the funcdef `}` (plus
9693                // optional trailing whitespace). Trim, strip that single
9694                // closing brace, then drop the structural separator
9695                // (`;`/`\n`) that preceded it — C zsh's getpermtext emits
9696                // each command without the trailing `;`/`\n`.
9697                let t = s.trim();
9698                // Strip the statement SEPARATOR that followed the funcdef
9699                // `}` FIRST — for `name() { body }; next` the slice ends in
9700                // `};`, so stripping the brace before the `;` left the `}`
9701                // stranded (`${functions[name]}` → `body }`, then re-parsed
9702                // to a `parse error near '}'`). Order: trailing separators →
9703                // closing brace → the body's own last-statement separator.
9704                let t = t.trim_end_matches(|c: char| c == ';' || c == '\n' || c.is_whitespace());
9705                let t = t.strip_suffix('}').unwrap_or(t).trim_end();
9706                let t = t
9707                    .trim_end_matches(|c: char| c == ';' || c == '\n')
9708                    .trim_end();
9709                t.to_string()
9710            })
9711            .filter(|s| !s.is_empty());
9712        zshlex();
9713        Some(ZshCommand::FuncDef(ZshFuncDef {
9714            names,
9715            body: Box::new(body),
9716            tracing: false,
9717            auto_call_args: None,
9718            body_source,
9719        }))
9720    } else if unset(SHORTLOOPS) {
9721        // c:1742 — `else if (unset(SHORTLOOPS)) YYERRORV(oecused);` —
9722        // funcdef short body (`name() cmd` without `{...}`) only
9723        // accepted when SHORTLOOPS is set. parse_init seeds
9724        // SHORTLOOPS=on so this fires only when a script
9725        // explicitly disabled the option.
9726        zerr("parse error: short function body form requires SHORTLOOPS option");
9727        None
9728    } else {
9729        // Slice the raw short-body text (unbraced_body_start was captured
9730        // before the body token was lexed, above) so `functions`/`typeset -f`
9731        // can list it (fusevm shfuncs render from this raw `body`, not
9732        // getpermtext — hashtable.rs:1397). Without it `f() print x` listed as
9733        // `f () { }` (empty).
9734        match par_cmd() {
9735            Some(cmd) => {
9736                let body_source = input_slice(unbraced_body_start, pos())
9737                    .map(|s| {
9738                        s.trim()
9739                            .trim_end_matches(|c: char| {
9740                                c == ';' || c == '\n' || c.is_whitespace()
9741                            })
9742                            .to_string()
9743                    })
9744                    .filter(|s| !s.is_empty());
9745                let list = ZshList {
9746                    sublist: ZshSublist {
9747                        pipe: ZshPipe {
9748                            cmd,
9749                            next: None,
9750                            lineno: lineno(),
9751                            merge_stderr: false,
9752                        },
9753                        next: None,
9754                        flags: SublistFlags::default(),
9755                    },
9756                    flags: ListFlags::default(),
9757                };
9758                Some(ZshCommand::FuncDef(ZshFuncDef {
9759                    names,
9760                    body: Box::new(ZshProgram { lists: vec![list] }),
9761                    tracing: false,
9762                    auto_call_args: None,
9763                    body_source,
9764                }))
9765            }
9766            None => None,
9767        }
9768    }
9769}
9770
9771/// Parse conditional expression
9772/// Top of `[[ ]]` cond-expression parsing — entry to recursive
9773/// descent (or → and → not → primary). Direct port of zsh's
9774/// par_cond_1 at parse.c:2434-2475.
9775fn parse_cond_expr() -> Option<ZshCond> {
9776    parse_cond_or()
9777}
9778
9779/// Cond-expression `||` level. C: inside par_cond_1 at
9780/// parse.c:2434-2475 (the `cond_or` ladder).
9781fn parse_cond_or() -> Option<ZshCond> {
9782    let left = parse_cond_and()?;
9783    skip_cond_separators();
9784
9785    if tok() == DBAR {
9786        zshlex();
9787        skip_cond_separators();
9788        parse_cond_or().map(|right| ZshCond::Or(Box::new(left), Box::new(right)))
9789    } else {
9790        Some(left)
9791    }
9792}
9793
9794/// Cond-expression `&&` level. C: par_cond_2 at parse.c:2476-2625.
9795fn parse_cond_and() -> Option<ZshCond> {
9796    let left = parse_cond_not()?;
9797    skip_cond_separators();
9798
9799    if tok() == DAMPER {
9800        zshlex();
9801        skip_cond_separators();
9802        parse_cond_and().map(|right| ZshCond::And(Box::new(left), Box::new(right)))
9803    } else {
9804        Some(left)
9805    }
9806}
9807
9808/// `static FuncDump dumps;` from `Src/parse.c:3652` — head of the
9809/// loaded-`.zwc` linked list. C walks `dumps`/`p->next` directly;
9810/// the Rust port uses a `Mutex<Vec<funcdump>>` indexed by filename
9811/// so refcount ops can find an entry without raw-pointer compare.
9812pub static DUMPS: std::sync::Mutex<Vec<funcdump>> = std::sync::Mutex::new(Vec::new());
9813
9814/// Cond-expression `!` negation level. C: handled inside
9815/// par_cond_2 at parse.c:2476-2625 via the Bang token check.
9816fn parse_cond_not() -> Option<ZshCond> {
9817    skip_cond_separators();
9818
9819    // ! can be either BANG_TOK or String "!"
9820    let is_not =
9821        tok() == BANG_TOK || (tok() == STRING_LEX && tokstr().map(|s| s == "!").unwrap_or(false));
9822    if is_not {
9823        zshlex();
9824        let inner = parse_cond_not()?;
9825        return Some(ZshCond::Not(Box::new(inner)));
9826    }
9827
9828    if tok() == INPAR_TOK {
9829        zshlex();
9830        skip_cond_separators();
9831        // c:Src/parse.c:2534-2547 par_cond_2 INPAR branch — empty
9832        // body `[[ ( ) ]]` makes the inner par_cond's recursive
9833        // par_cond_2 see OUTPAR with no leading STRING/BANG/INPAR
9834        // and YYERROR immediately. Mirror that here: if the very
9835        // next token after `(` (post separator skip) is `)`, emit
9836        // a parse error so the script aborts cleanly instead of
9837        // silently swallowing every following command. Bug #538.
9838        if tok() == OUTPAR_TOK {
9839            crate::ported::utils::zerr("condition expected");
9840            yyerror(0);
9841            return None;
9842        }
9843        let inner = parse_cond_expr()?;
9844        skip_cond_separators();
9845        if tok() == OUTPAR_TOK {
9846            zshlex();
9847        }
9848        return Some(inner);
9849    }
9850
9851    parse_cond_primary()
9852}
9853
9854/// Cond-expression primary: unary tests (-f, -d, ...), binary
9855/// tests (=, !=, <, >, ==, =~, -eq, -ne, ...), and parenthesized
9856/// sub-expressions. Direct port of par_cond_double / par_cond_triple
9857/// / par_cond_multi at parse.c:2626-2731 (chosen by arg count).
9858fn parse_cond_primary() -> Option<ZshCond> {
9859    let s1 = match tok() {
9860        STRING_LEX => {
9861            let s = tokstr().unwrap_or_default();
9862            zshlex();
9863            s
9864        }
9865        _ => return None,
9866    };
9867
9868    skip_cond_separators();
9869
9870    // Check for unary operator. zsh's lexer tokenizes leading `-` as
9871    // `zsh_h::Dash` (`\u{9b}`, `Src/zsh.h:182`) inside gettokstr (lex.c:1390-1400
9872    // LX2_DASH — `-` always becomes Dash, untokenized later). Match
9873    // either form here, and use char-count not byte-count since Dash
9874    // is 2 UTF-8 bytes (`\xc2\x9b`).
9875    //
9876    // c:Src/parse.c par_cond — when the leading token is `-` followed
9877    // ENTIRELY by digits (`-5`, `-123`), it's a numeric literal
9878    // operand, not a unary test flag. zsh's parser checks the C
9879    // `isdigit` of the trailing chars to disambiguate; without the
9880    // check, `[[ -5 -lt -3 ]]` reads `-5` as a one-arg test flag,
9881    // then `-lt` as the operand, then `-3` as a leftover token —
9882    // emitting "unknown condition: -5" and falling through to a
9883    // command-not-found dispatch on `-3`. Bug #121 in docs/BUGS.md.
9884    let s1_chars: Vec<char> = s1.chars().collect();
9885    let is_negative_number = s1_chars.len() >= 2
9886        && IS_DASH(s1_chars[0])
9887        && s1_chars[1..].iter().all(|c| c.is_ascii_digit());
9888    if s1_chars.len() == 2 && IS_DASH(s1_chars[0]) && !is_negative_number {
9889        let s2 = match tok() {
9890            STRING_LEX => {
9891                let s = tokstr().unwrap_or_default();
9892                zshlex();
9893                s
9894            }
9895            _ => {
9896                // c:Src/parse.c par_cond_2 — when the leading `-X`
9897                // is a 2-char dash form, zsh ALWAYS treats it as a
9898                // unary test op (the operand-missing case errors
9899                // immediately with `unknown condition: -X`). Don't
9900                // fall back to `Unary("-n", "-X")` — that path
9901                // silently let `[[ -z ]]` evaluate as
9902                // `[[ -n "-z" ]]` → true. Bug #480/#481.
9903                //
9904                // Convert Dash (\u{9b}) back to ASCII `-` for the
9905                // user-visible diagnostic so it reads "unknown
9906                // condition: -z" not "unknown condition: <Dash>z".
9907                let display: String = s1
9908                    .chars()
9909                    .map(|c| if IS_DASH(c) { '-' } else { c })
9910                    .collect();
9911                crate::ported::utils::zerr(&format!("unknown condition: {}", display));
9912                return None;
9913            }
9914        };
9915        return Some(ZshCond::Unary(s1, s2));
9916    }
9917
9918    // Check for binary operator. Direct port of zsh/Src/parse.c:2601-2603:
9919    //   incond++;  /* parentheses do globbing */
9920    //   do condlex(); while (COND_SEP());
9921    //   incond--;  /* parentheses do grouping */
9922    // The bump makes the lexer treat `(` as a literal character inside
9923    // the RHS word (e.g. `[[ x =~ (foo) ]]`) instead of returning Inpar
9924    // and splitting the regex into multiple tokens.
9925    let op = match tok() {
9926        STRING_LEX => {
9927            let s = tokstr().unwrap_or_default();
9928            set_incond(incond() + 1);
9929            zshlex();
9930            set_incond(incond() - 1);
9931            s
9932        }
9933        INANG_TOK => {
9934            set_incond(incond() + 1);
9935            zshlex();
9936            set_incond(incond() - 1);
9937            "<".to_string()
9938        }
9939        OUTANG_TOK => {
9940            set_incond(incond() + 1);
9941            zshlex();
9942            set_incond(incond() - 1);
9943            ">".to_string()
9944        }
9945        _ => return Some(ZshCond::Unary("-n".to_string(), s1)),
9946    };
9947
9948    skip_cond_separators();
9949
9950    // c:Src/parse.c:2601-2625 par_cond_2 — only the documented binary
9951    // operators are accepted inside `[[ ... ]]`. zsh rejects ksh/bash
9952    // forms `-a` (logical AND) and `-o` (logical OR) with a parse
9953    // error ("condition expected") because they're not in the
9954    // par_cond_2 binary-op set — zsh uses `&&` / `||` instead.
9955    // Verified: `zsh -fc '[[ "" -a "x" ]]'` → exit 1, "parse error:
9956    // condition expected: ...". Without this gate, zshrs silently
9957    // built ZshCond::Binary("", "-a", "x") and ran an unknown-op
9958    // path that always evaluated false.
9959    // c:Src/parse.c:2601-2625 par_cond_2 — `-a` / `-o` n-ary chain
9960    // operators are not valid binary operators inside `[[ ... ]]`
9961    // (zsh uses `&&` / `||` instead). Match both the ASCII `-a`/
9962    // `-o` form and the tokenized `Dash+a`/`Dash+o` form that the
9963    // lexer emits inside cond bodies (Dash = \u{9b}, Src/zsh.h:182).
9964    let op_chars: Vec<char> = op.chars().collect();
9965    let is_dash_a_or_o =
9966        op_chars.len() == 2 && IS_DASH(op_chars[0]) && (op_chars[1] == 'a' || op_chars[1] == 'o');
9967    if is_dash_a_or_o {
9968        crate::ported::utils::zerr(&format!("parse error: condition expected: {}", s1));
9969        crate::ported::utils::errflag.fetch_or(
9970            crate::ported::zsh_h::ERRFLAG_ERROR,
9971            std::sync::atomic::Ordering::Relaxed,
9972        );
9973        set_tok(LEXERR);
9974        return None;
9975    }
9976
9977    let s2 = match tok() {
9978        STRING_LEX => {
9979            let s = tokstr().unwrap_or_default();
9980            zshlex();
9981            s
9982        }
9983        _ => {
9984            // c:Src/parse.c par_cond_2 — when a binary op is
9985            // recognized but the RHS operand is missing, zsh emits
9986            // `parse error: condition expected: <LHS>` at par_cond_2's
9987            // missing-rhs branch. zshrs's previous fallback returned
9988            // `Binary(s1, op, "")` which silently evaluated as if the
9989            // RHS were empty string → rc=1. Bug #482.
9990            //
9991            // Convert Dash (\u{9b}) back to ASCII `-` in the LHS
9992            // display so the diagnostic reads cleanly.
9993            let display: String = s1
9994                .chars()
9995                .map(|c| if IS_DASH(c) { '-' } else { c })
9996                .collect();
9997            crate::ported::utils::zerr(&format!("parse error: condition expected: {}", display));
9998            crate::ported::utils::errflag.fetch_or(
9999                crate::ported::zsh_h::ERRFLAG_ERROR,
10000                std::sync::atomic::Ordering::Relaxed,
10001            );
10002            set_tok(LEXERR);
10003            return None;
10004        }
10005    };
10006
10007    // c:Src/parse.c:2685-2691 par_cond_triple —
10008    //   `(b[0] == Equals || b[0] == '=') && (b[1] == '~' || b[1] == Tilde)
10009    //    && !b[2]` → COND_REGEX.
10010    // The lexer emits the TOKEN forms inside cond bodies (`=` at word
10011    // start → Equals `\u{8d}`, `~` → Tilde `\u{98}`), so an ASCII-only
10012    // `op == "=~"` check missed every real `[[ x =~ pat ]]` and fell
10013    // through to Binary.
10014    let opc: Vec<char> = op.chars().collect();
10015    let is_regex_op =
10016        opc.len() == 2 && (opc[0] == '=' || opc[0] == Equals) && (opc[1] == '~' || opc[1] == Tilde);
10017    // c:2659-2710 par_cond_triple — only the documented binary operators are
10018    // valid. String ops: `=`/`==`/`!=`/`<`/`>` (and their tokenized forms
10019    // Equals/Bang/Inang/Outang). Dash ops (`-eq`, `-nt`, `-pcre-match`, any
10020    // `-X`) parse-accept and the evaluator reports "unknown condition" if
10021    // unsupported. A `-mod A B C` (s1 is a dash op) is the COND_MOD form. Any
10022    // other middle word is `COND_ERROR("condition expected: %s", op)` — a parse
10023    // error that YYERRORs (errflag + LEXERR) so the line aborts. Without this
10024    // gate `[[ a b c ]]` silently built Binary("a","b","c") and ran on.
10025    let is_eq = |c: char| c == '=' || c == Equals;
10026    let is_bang = |c: char| c == '!' || c == Bang;
10027    let is_recognized_op = (opc.len() == 1
10028        && (is_eq(opc[0]) || matches!(opc[0], '<' | '>') || opc[0] == Inang || opc[0] == Outang))
10029        || (opc.len() == 2 && is_eq(opc[0]) && is_eq(opc[1]))
10030        || (opc.len() == 2 && is_bang(opc[0]) && is_eq(opc[1]))
10031        || (!opc.is_empty() && IS_DASH(opc[0]))
10032        || (!s1_chars.is_empty() && IS_DASH(s1_chars[0]));
10033    if is_regex_op {
10034        Some(ZshCond::Regex(s1, s2))
10035    } else if is_recognized_op {
10036        Some(ZshCond::Binary(s1, op, s2))
10037    } else {
10038        let display: String = op
10039            .chars()
10040            .map(|c| if IS_DASH(c) { '-' } else { c })
10041            .collect();
10042        crate::ported::utils::zerr(&format!("condition expected: {}", display));
10043        crate::ported::utils::errflag.fetch_or(
10044            crate::ported::zsh_h::ERRFLAG_ERROR,
10045            std::sync::atomic::Ordering::Relaxed,
10046        );
10047        set_tok(LEXERR);
10048        None
10049    }
10050}
10051
10052fn skip_cond_separators() {
10053    while tok() == SEPER && {
10054        let s = tokstr();
10055        s.map(|s| !s.contains(';')).unwrap_or(true)
10056    } {
10057        zshlex();
10058    }
10059}
10060
10061/// Parse (( ... )) arithmetic command
10062/// Parse `(( EXPR ))` arithmetic command. C source: parse.c:1810-1834
10063/// `par_dinbrack` (despite the name; the function actually handles
10064/// DINPAR `(( ))` blocks too).
10065fn parse_arith() -> Option<ZshCommand> {
10066    let expr = tokstr().unwrap_or_default();
10067    zshlex();
10068    Some(ZshCommand::Arith(expr))
10069}
10070
10071/// Skip separator tokens
10072fn skip_separators() {
10073    while tok() == SEPER || tok() == NEWLIN {
10074        zshlex();
10075    }
10076}
10077
10078// `fdheaderlen` / `fdmagic` / `fdflags` / etc. macros from
10079// `Src/parse.c:3125-3152`. C uses raw pointer arithmetic on a
10080// `Wordcode` (= `u32 *`); the Rust port takes a slice and indexes.
10081
10082/// Port of `fdheaderlen(f)` macro (`Src/parse.c:3125`) — header
10083/// length in u32 words (read from prelude word `FD_PRELEN`).
10084#[inline]
10085pub fn fdheaderlen(f: &[u32]) -> u32 {
10086    f[FD_PRELEN]
10087}
10088
10089/// Port of `fdmagic(f)` macro (`Src/parse.c:3127`) — first prelude
10090/// word, either `FD_MAGIC` or `FD_OMAGIC`.
10091#[inline]
10092pub fn fdmagic(f: &[u32]) -> u32 {
10093    f[0]
10094}
10095
10096/// Port of `fdflags(f)` macro (`Src/parse.c:3131`) — low byte of
10097/// the packed `pre[1]` word.
10098#[inline]
10099pub fn fdflags(f: &[u32]) -> u32 {
10100    // `pre[1]` is a u32 viewed as 4 bytes; flags = byte 0.
10101    f[1] & 0xff
10102}
10103
10104/// Port of `fdsetflags(f, v)` macro (`Src/parse.c:3132`) — write
10105/// the low byte of `pre[1]`.
10106#[inline]
10107pub fn fdsetflags(f: &mut [u32], v: u8) {
10108    f[1] = (f[1] & !0xff) | (v as u32);
10109}
10110
10111/// Port of `fdother(f)` macro (`Src/parse.c:3133`) — high 24 bits
10112/// of `pre[1]`, holds the byte-offset to the opposite-byte-order
10113/// dump copy.
10114#[inline]
10115pub fn fdother(f: &[u32]) -> u32 {
10116    (f[1] >> 8) & 0x00ff_ffff
10117}
10118
10119/// Port of `fdsetother(f, o)` macro (`Src/parse.c:3134`).
10120#[inline]
10121pub fn fdsetother(f: &mut [u32], o: u32) {
10122    f[1] = (f[1] & 0xff) | ((o & 0x00ff_ffff) << 8);
10123}
10124
10125/// Port of `fdversion(f)` macro (`Src/parse.c:3140`) — read the
10126/// `ZSH_VERSION` C-string from `pre[2..]`.
10127pub fn fdversion(f: &[u32]) -> String {
10128    let bytes: Vec<u8> = f[2..]
10129        .iter()
10130        .take(10)
10131        .flat_map(|w| w.to_le_bytes().into_iter())
10132        .collect();
10133    let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
10134    String::from_utf8_lossy(&bytes[..end]).into_owned()
10135}
10136
10137/// Port of `firstfdhead(f)` macro (`Src/parse.c:3142`) — pointer
10138/// to the first `struct fdhead` past the prelude.
10139#[inline]
10140pub fn firstfdhead_offset() -> usize {
10141    FD_PRELEN
10142}
10143
10144/// Port of `nextfdhead(f)` macro (`Src/parse.c:3143`) — advance to
10145/// the next header by reading the current `hlen` slot.
10146#[inline]
10147pub fn nextfdhead_offset(f: &[u32], cur: usize) -> usize {
10148    cur + (f[cur + 4] as usize) // .hlen is field 4 of fdhead
10149}
10150
10151/// Port of `fdhflags(f)` macro (`Src/parse.c:3145`) — low 2 bits
10152/// of the header's `flags` field (the kshload/zshload marker).
10153#[inline]
10154pub fn fdhflags(h: &fdhead) -> u32 {
10155    h.flags & 0x3
10156}
10157
10158/// Port of `fdhtail(f)` macro (`Src/parse.c:3146`) — high 30 bits
10159/// of `flags`, byte offset from the name start to its basename.
10160#[inline]
10161pub fn fdhtail(h: &fdhead) -> u32 {
10162    h.flags >> 2
10163}
10164
10165/// Port of `fdhbldflags(f, t)` macro (`Src/parse.c:3147`) — pack
10166/// `(flags, tail)` into one u32 (low 2 bits = flags, high 30 = tail).
10167#[inline]
10168pub fn fdhbldflags(flags: u32, tail: u32) -> u32 {
10169    flags | (tail << 2)
10170}
10171
10172/// Port of `fdname(f)` macro (`Src/parse.c:3152`) — name string
10173/// follows the fdhead record immediately. Reads bytes from the
10174/// dump buffer until NUL.
10175pub fn fdname(buf: &[u32], header_offset: usize) -> String {
10176    let name_word_off = header_offset + FDHEAD_WORDS;
10177    let bytes: Vec<u8> = buf[name_word_off..]
10178        .iter()
10179        .flat_map(|w| w.to_le_bytes().into_iter())
10180        .take_while(|&b| b != 0)
10181        .collect();
10182    String::from_utf8_lossy(&bytes).into_owned()
10183}
10184
10185/// Decode a `fdhead` record at the given u32-word offset in the
10186/// dump buffer. Used by the header-walk loops in `bin_zcompile -t`.
10187pub fn read_fdhead(buf: &[u32], offset: usize) -> Option<fdhead> {
10188    if offset + FDHEAD_WORDS > buf.len() {
10189        return None;
10190    }
10191    Some(fdhead {
10192        start: buf[offset],
10193        len: buf[offset + 1],
10194        npats: buf[offset + 2],
10195        strs: buf[offset + 3],
10196        hlen: buf[offset + 4],
10197        flags: buf[offset + 5],
10198    })
10199}
10200
10201/// Port of `freedump(FuncDump f)` from `Src/parse.c:3976`. C
10202/// `munmap`s, `zclose`s the fd, and frees the struct. The Rust
10203/// port relies on Drop for the `funcdump` (no mmap held in this
10204/// port — `addr`/`map` are byte-offset placeholders), so the
10205/// equivalent is removing the entry from the dumps list. Called
10206/// by `decrdumpcount` when the refcount hits zero (c:3988) and
10207/// by `closedumps` when shutting down (c:4008).
10208fn freedump_locked(g: &mut std::sync::MutexGuard<'_, Vec<funcdump>>, filename: &str) {
10209    // c:3976
10210    g.retain(|d| d.filename.as_deref() != Some(filename));
10211}
10212
10213// =====================================================================
10214// Remaining `Src/parse.c` ports (this section finishes the file).
10215//
10216// Several of these emit into the C-wordcode buffer (`ECBUF`/etc.) and
10217// are kept for completeness — the live zshrs runtime uses the
10218// `ZshProgram` AST path instead, but `bin_zcompile` (`-c`/`-a` modes)
10219// and any future `.zwc`-emit pipeline both call into these.
10220// =====================================================================
10221
10222/// `ecstr(s)` helper — `ecadd(ecstrcode(s))`. Mirrors the C macro at
10223/// `Src/parse.c:482` used everywhere by the par_* emitters.
10224#[inline]
10225pub fn ecstr(s: &str) {
10226    let code = ecstrcode(s);
10227    ecadd(code);
10228}
10229
10230/// Port of `condlex` function-pointer global from `Src/parse.c`. C
10231/// flips this between `zshlex` and `testlex` depending on whether
10232/// we're inside `[[ ]]` vs `/bin/test` builtin. zshrs has no
10233/// separate `testlex` yet, so this just defers to `zshlex`.
10234#[inline]
10235pub fn condlex() {
10236    zshlex();
10237}
10238
10239fn copy_ecstr_walk(node: &Option<Box<EccstrNode>>, p: &mut [u8]) {
10240    let mut cur = node.as_ref();
10241    while let Some(n) = cur {
10242        // c:540 — `memcpy(p + s->aoffs, s->str, strlen(s->str) + 1);`
10243        let off = n.aoffs as usize;
10244        let need = off + n.str.len() + 1;
10245        if need <= p.len() {
10246            p[off..off + n.str.len()].copy_from_slice(&n.str);
10247            p[off + n.str.len()] = 0;
10248        }
10249        // c:541 — `copy_ecstr(s->left, p);`
10250        copy_ecstr_walk(&n.left, p);
10251        // c:542 — `s = s->right;`
10252        cur = n.right.as_ref();
10253    }
10254}
10255
10256/// Port of `par_cond(void)` from `Src/parse.c:2409`. Top-level cond
10257/// OR-chain — drives `par_cond_1` and stitches `||`-separated terms
10258/// with `WCB_COND(COND_OR, …)`. This is the missing top of the
10259/// wordcode cond chain: `par_cond_wordcode` (the par_dinbrack port)
10260/// must call into HERE so that `[[ a || b ]]` and friends land
10261/// real WC_COND opcodes in `ecbuf`. Without this, the wordcode
10262/// emitter for `[[ ... ]]` produced zero words and parity dropped
10263/// 148 words on `/etc/zshrc` alone.
10264pub fn par_cond_top() -> i32 {
10265    // c:2411 — `int p = ecused, r;`
10266    let p = ECUSED.with(|c| c.get()) as usize;
10267    let r = par_cond_1();
10268    while COND_SEP() {
10269        condlex();
10270    }
10271    if tok() == DBAR {
10272        // c:2417 — `condlex(); while (COND_SEP()) condlex();`
10273        condlex();
10274        while COND_SEP() {
10275            condlex();
10276        }
10277        // c:2420-2422 — `ecispace(p, 1); par_cond(); ecbuf[p] =
10278        // WCB_COND(COND_OR, ecused-1-p);`
10279        ecispace(p, 1);
10280        par_cond_top();
10281        let ecused = ECUSED.with(|c| c.get()) as usize;
10282        ECBUF.with(|c| {
10283            c.borrow_mut()[p] = WCB_COND(COND_OR as u32, (ecused - 1 - p) as u32);
10284        });
10285        return 1;
10286    }
10287    r
10288}
10289
10290/// Port of `static int check_cond(const char *input, const char *cond)`
10291/// from `Src/parse.c:2459`. True iff `input` is the two-char `-X`
10292/// form whose `X` matches `cond` — used by par_cond_2 to detect
10293/// `-a` / `-o` n-ary chain operators and by build_dump for `-k` /
10294/// `-z`. C: `return !IS_DASH(input[0]) ? 0 : !strcmp(input+1, cond);`.
10295fn check_cond(input: &str, cond: &str) -> bool {
10296    let mut chars = input.chars();
10297    match chars.next() {
10298        Some(c) if IS_DASH(c) => chars.as_str() == cond,
10299        _ => false,
10300    }
10301}
10302
10303#[cfg(test)]
10304mod tests {
10305    use super::*;
10306    use crate::utils::{errflag, ERRFLAG_ERROR};
10307    use std::fs;
10308    use std::path::Path;
10309    use std::sync::atomic::Ordering;
10310    use std::sync::mpsc;
10311    use std::thread;
10312    use std::time::Duration;
10313
10314    /// `try_source_file` MUST refuse a stale `.zwc` cache when the
10315    /// uncompiled source has been modified more recently. The C body
10316    /// at c:3819 reads `stc.st_mtime >= stn.st_mtime` — explicitly
10317    /// `>=`, meaning only an equal-or-newer zwc is acceptable.
10318    ///
10319    /// A regression that ignored the mtime check (or used the wrong
10320    /// direction) would silently keep loading the OLD compiled body
10321    /// after the user edited the source file — every `source foo.zsh`
10322    /// would replay yesterday's code, the worst-class shell bug.
10323    ///
10324    /// Pin: create source + .zwc, then touch source to make it
10325    /// newer. try_source_file must return None.
10326    #[test]
10327    fn try_source_file_skips_stale_zwc() {
10328        let _g = crate::test_util::global_state_lock();
10329        let dir = tempfile::tempdir().expect("tempdir");
10330        let src = dir.path().join("script.zsh");
10331        let zwc = dir.path().join("script.zsh.zwc");
10332        // Create zwc FIRST (older), then source (newer).
10333        fs::write(&zwc, b"placeholder zwc").unwrap();
10334        thread::sleep(Duration::from_millis(20));
10335        fs::write(&src, b"echo hi").unwrap();
10336
10337        let result = try_source_file(src.to_str().unwrap());
10338        assert!(
10339            result.is_none(),
10340            "c:3819 — stale .zwc (older than source) MUST be rejected; \
10341             got {:?}",
10342            result
10343        );
10344    }
10345
10346    /// `try_source_file` returns None when no `.zwc` exists for the
10347    /// requested file (c:3819 `if let Ok(meta_c) = &stc` gate fails).
10348    /// This is the common case — most user scripts don't ship with
10349    /// a pre-compiled `.zwc`. The fn returning None lets the caller
10350    /// fall through to the source-read path. A regression that
10351    /// returned `Some(file)` on missing `.zwc` would route every
10352    /// `source foo.zsh` through `check_dump_file` against a
10353    /// non-existent file and crash.
10354    #[test]
10355    fn try_source_file_returns_none_when_no_zwc() {
10356        let _g = crate::test_util::global_state_lock();
10357        let dir = tempfile::tempdir().expect("tempdir");
10358        let src = dir.path().join("plain.zsh");
10359        fs::write(&src, b"echo hi").unwrap();
10360        // No .zwc sibling.
10361
10362        let result = try_source_file(src.to_str().unwrap());
10363        assert!(
10364            result.is_none(),
10365            "c:3819 gate fails when stat(wc) returns Err → None"
10366        );
10367    }
10368
10369    /// Test helper. Mirrors zsh's `errflag` save/clear/check pattern
10370    /// around a parse — see `Src/init.c:loop` which clears errflag
10371    /// before parse_event() and tests it after. Returns `Err` if the
10372    /// parse set `ERRFLAG_ERROR`; otherwise `Ok(program)`.
10373    fn parse(input: &str) -> Result<ZshProgram, String> {
10374        let saved = errflag.load(Ordering::Relaxed);
10375        errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
10376        parse_init(input);
10377        let prog = crate::ported::parse::parse();
10378        let had_err = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
10379        // Restore prior error bits; don't carry our new error into the
10380        // outer test runner.
10381        errflag.store(saved, Ordering::Relaxed);
10382        if had_err {
10383            Err("parse error".to_string())
10384        } else {
10385            Ok(prog)
10386        }
10387    }
10388
10389    #[test]
10390    fn test_simple_command() {
10391        let _g = crate::test_util::global_state_lock();
10392        let prog = parse("echo hello world").unwrap();
10393        assert_eq!(prog.lists.len(), 1);
10394        match &prog.lists[0].sublist.pipe.cmd {
10395            ZshCommand::Simple(s) => {
10396                assert_eq!(s.words, vec!["echo", "hello", "world"]);
10397            }
10398            _ => panic!("expected simple command"),
10399        }
10400    }
10401
10402    #[test]
10403    fn test_pipeline() {
10404        let _g = crate::test_util::global_state_lock();
10405        let prog = parse("ls | grep foo | wc -l").unwrap();
10406        assert_eq!(prog.lists.len(), 1);
10407
10408        let pipe = &prog.lists[0].sublist.pipe;
10409        assert!(pipe.next.is_some());
10410
10411        let pipe2 = pipe.next.as_ref().unwrap();
10412        assert!(pipe2.next.is_some());
10413    }
10414
10415    #[test]
10416    fn test_and_or() {
10417        let _g = crate::test_util::global_state_lock();
10418        let prog = parse("cmd1 && cmd2 || cmd3").unwrap();
10419        let sublist = &prog.lists[0].sublist;
10420
10421        assert!(sublist.next.is_some());
10422        let (op, _) = sublist.next.as_ref().unwrap();
10423        assert_eq!(*op, SublistOp::And);
10424    }
10425
10426    #[test]
10427    fn test_if_then() {
10428        let _g = crate::test_util::global_state_lock();
10429        let prog = parse("if test -f foo; then echo yes; fi").unwrap();
10430        match &prog.lists[0].sublist.pipe.cmd {
10431            ZshCommand::If(_) => {}
10432            _ => panic!("expected if command"),
10433        }
10434    }
10435
10436    #[test]
10437    fn test_for_loop() {
10438        let _g = crate::test_util::global_state_lock();
10439        let prog = parse("for i in a b c; do echo $i; done").unwrap();
10440        match &prog.lists[0].sublist.pipe.cmd {
10441            ZshCommand::For(f) => {
10442                assert_eq!(f.var, "i");
10443                match &f.list {
10444                    ForList::Words(w) => assert_eq!(w, &vec!["a", "b", "c"]),
10445                    _ => panic!("expected word list"),
10446                }
10447            }
10448            _ => panic!("expected for command"),
10449        }
10450    }
10451
10452    #[test]
10453    fn test_case() {
10454        let _g = crate::test_util::global_state_lock();
10455        let prog = parse("case $x in a) echo a;; b) echo b;; esac").unwrap();
10456        match &prog.lists[0].sublist.pipe.cmd {
10457            ZshCommand::Case(c) => {
10458                assert_eq!(c.arms.len(), 2);
10459            }
10460            _ => panic!("expected case command"),
10461        }
10462    }
10463
10464    #[test]
10465    fn test_function() {
10466        let _g = crate::test_util::global_state_lock();
10467        // First test just parsing "function foo" to see what happens
10468        let prog = parse("function foo { }").unwrap();
10469        match &prog.lists[0].sublist.pipe.cmd {
10470            ZshCommand::FuncDef(f) => {
10471                assert_eq!(f.names, vec!["foo"]);
10472            }
10473            _ => panic!(
10474                "expected function, got {:?}",
10475                prog.lists[0].sublist.pipe.cmd
10476            ),
10477        }
10478    }
10479
10480    #[test]
10481    fn test_redirection() {
10482        let _g = crate::test_util::global_state_lock();
10483        let prog = parse("echo hello > file.txt").unwrap();
10484        match &prog.lists[0].sublist.pipe.cmd {
10485            ZshCommand::Simple(s) => {
10486                assert_eq!(s.redirs.len(), 1);
10487                assert_eq!(s.redirs[0].rtype, REDIR_WRITE);
10488            }
10489            _ => panic!("expected simple command"),
10490        }
10491    }
10492
10493    /// Regression: `{var}>file` named-fd redirects accept any IIDENT name,
10494    /// including all-digit positional-parameter names like `{1}` (used by
10495    /// p10k's `_p9k_restore_prompt`: `exec {1}>&-`). The prior check required
10496    /// the first char to be alphabetic/`_`, which rejected `{1}`/`{2}`/`{12}`
10497    /// and made zshrs treat `{1}` as a command word — `command not found: {1}`.
10498    /// zsh's rule is `itype_end(tokstr+1, IIDENT, 0) >= ptr` (per-char IIDENT,
10499    /// no first-char restriction).
10500    #[test]
10501    fn test_fd_var_redir_digit_name() {
10502        let _g = crate::test_util::global_state_lock();
10503        for (src, want) in [
10504            ("echo hi {1}>file", "1"),
10505            ("echo hi {2}>file", "2"),
10506            ("echo hi {12}>file", "12"),
10507            ("echo hi {myfd}>file", "myfd"),
10508            ("echo hi {fd1}>file", "fd1"),
10509        ] {
10510            let prog = parse(src).unwrap_or_else(|_| panic!("failed to parse {src:?}"));
10511            match &prog.lists[0].sublist.pipe.cmd {
10512                ZshCommand::Simple(s) => {
10513                    assert_eq!(s.redirs.len(), 1, "{src:?}");
10514                    assert_eq!(
10515                        s.redirs[0].varid.as_deref(),
10516                        Some(want),
10517                        "{src:?} should carry fd-var {want:?}"
10518                    );
10519                }
10520                _ => panic!("expected simple command for {src:?}"),
10521            }
10522        }
10523    }
10524
10525    #[test]
10526    fn test_assignment() {
10527        let _g = crate::test_util::global_state_lock();
10528        let prog = parse("FOO=bar echo $FOO").unwrap();
10529        match &prog.lists[0].sublist.pipe.cmd {
10530            ZshCommand::Simple(s) => {
10531                assert_eq!(s.assigns.len(), 1);
10532                assert_eq!(s.assigns[0].name, "FOO");
10533            }
10534            _ => panic!("expected simple command"),
10535        }
10536    }
10537
10538    #[test]
10539    fn test_parse_completion_function() {
10540        let _g = crate::test_util::global_state_lock();
10541        let input = r#"_2to3_fixes() {
10542  local -a fixes
10543  fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
10544  (( ${#fixes} )) && _describe -t fixes 'fix' fixes
10545}"#;
10546        let result = parse(input);
10547        assert!(
10548            result.is_ok(),
10549            "Failed to parse completion function: {:?}",
10550            result.err()
10551        );
10552        let prog = result.unwrap();
10553        assert!(
10554            !prog.lists.is_empty(),
10555            "Expected at least one list in program"
10556        );
10557    }
10558
10559    #[test]
10560    fn test_parse_array_with_complex_elements() {
10561        let _g = crate::test_util::global_state_lock();
10562        let input = r#"arguments=(
10563  '(- * :)'{-h,--help}'[show this help message and exit]'
10564  {-d,--doctests_only}'[fix up doctests only]'
10565  '*:filename:_files'
10566)"#;
10567        let result = parse(input);
10568        assert!(
10569            result.is_ok(),
10570            "Failed to parse array assignment: {:?}",
10571            result.err()
10572        );
10573    }
10574
10575    #[test]
10576    fn test_parse_full_completion_file() {
10577        let _g = crate::test_util::global_state_lock();
10578        let input = r##"#compdef 2to3
10579
10580# zsh completions for '2to3'
10581
10582_2to3_fixes() {
10583  local -a fixes
10584  fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
10585  (( ${#fixes} )) && _describe -t fixes 'fix' fixes
10586}
10587
10588local -a arguments
10589
10590arguments=(
10591  '(- * :)'{-h,--help}'[show this help message and exit]'
10592  {-d,--doctests_only}'[fix up doctests only]'
10593  {-f,--fix}'[each FIX specifies a transformation; default: all]:fix name:_2to3_fixes'
10594  {-j,--processes}'[run 2to3 concurrently]:number: '
10595  {-x,--nofix}'[prevent a transformation from being run]:fix name:_2to3_fixes'
10596  {-l,--list-fixes}'[list available transformations]'
10597  {-p,--print-function}'[modify the grammar so that print() is a function]'
10598  {-v,--verbose}'[more verbose logging]'
10599  '--no-diffs[do not show diffs of the refactoring]'
10600  {-w,--write}'[write back modified files]'
10601  {-n,--nobackups}'[do not write backups for modified files]'
10602  {-o,--output-dir}'[put output files in this directory instead of overwriting]:directory:_directories'
10603  {-W,--write-unchanged-files}'[also write files even if no changes were required]'
10604  '--add-suffix[append this string to all output filenames]:suffix: '
10605  '*:filename:_files'
10606)
10607
10608_arguments -s -S $arguments
10609"##;
10610        let result = parse(input);
10611        assert!(
10612            result.is_ok(),
10613            "Failed to parse full completion file: {:?}",
10614            result.err()
10615        );
10616        let prog = result.unwrap();
10617        // Should have parsed successfully with at least one statement
10618        assert!(!prog.lists.is_empty(), "Expected at least one list");
10619    }
10620
10621    #[test]
10622    fn test_parse_logs_sh() {
10623        let _g = crate::test_util::global_state_lock();
10624        let input = r#"#!/usr/bin/env bash
10625shopt -s globstar
10626
10627if [[ $(uname) == Darwin ]]; then
10628    tail -f /var/log/**/*.log /var/log/**/*.out | lolcat
10629else
10630    if [[ $ZPWR_DISTRO_NAME == raspbian ]]; then
10631        tail -f /var/log/**/*.log | lolcat
10632    else
10633        printf "Unsupported...\n" >&2
10634    fi
10635fi
10636"#;
10637        let result = parse(input);
10638        assert!(
10639            result.is_ok(),
10640            "Failed to parse logs.sh: {:?}",
10641            result.err()
10642        );
10643    }
10644
10645    #[test]
10646    fn test_parse_case_with_glob() {
10647        let _g = crate::test_util::global_state_lock();
10648        let input = r#"case "$ZPWR_OS_TYPE" in
10649    darwin*)  open_cmd='open'
10650      ;;
10651    cygwin*)  open_cmd='cygstart'
10652      ;;
10653    linux*)
10654        open_cmd='xdg-open'
10655      ;;
10656esac"#;
10657        let result = parse(input);
10658        assert!(
10659            result.is_ok(),
10660            "Failed to parse case with glob: {:?}",
10661            result.err()
10662        );
10663    }
10664
10665    #[test]
10666    fn test_parse_case_with_nested_if() {
10667        let _g = crate::test_util::global_state_lock();
10668        // Test case with nested if and glob patterns
10669        let input = r##"function zpwrGetOpenCommand(){
10670    local open_cmd
10671    case "$ZPWR_OS_TYPE" in
10672        darwin*)  open_cmd='open' ;;
10673        cygwin*)  open_cmd='cygstart' ;;
10674        linux*)
10675            if [[ "$_zpwr_uname_r" != *icrosoft* ]];then
10676                open_cmd='nohup xdg-open'
10677            fi
10678            ;;
10679    esac
10680}"##;
10681        let result = parse(input);
10682        assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
10683    }
10684
10685    #[test]
10686    fn test_parse_zpwr_scripts() {
10687        let _g = crate::test_util::global_state_lock();
10688        let scripts_dir = Path::new("/Users/wizard/.zpwr/scripts");
10689        if !scripts_dir.exists() {
10690            eprintln!("Skipping test: scripts directory not found");
10691            return;
10692        }
10693
10694        let mut total = 0;
10695        let mut passed = 0;
10696        let mut failed_files = Vec::new();
10697        let mut timeout_files = Vec::new();
10698
10699        for ext in &["sh", "zsh"] {
10700            let pattern = scripts_dir.join(format!("*.{}", ext));
10701            if let Ok(entries) = glob::glob(pattern.to_str().unwrap()) {
10702                for entry in entries.flatten() {
10703                    total += 1;
10704                    let file_path = entry.display().to_string();
10705                    let content = match fs::read_to_string(&entry) {
10706                        Ok(c) => c,
10707                        Err(e) => {
10708                            failed_files.push((file_path, format!("read error: {}", e)));
10709                            continue;
10710                        }
10711                    };
10712
10713                    // Parse with timeout
10714                    let content_clone = content.clone();
10715                    let (tx, rx) = mpsc::channel();
10716                    let handle = thread::spawn(move || {
10717                        let result = parse(&content_clone);
10718                        let _ = tx.send(result);
10719                    });
10720
10721                    match rx.recv_timeout(Duration::from_secs(2)) {
10722                        Ok(Ok(_)) => passed += 1,
10723                        Ok(Err(err)) => {
10724                            failed_files.push((file_path, err));
10725                        }
10726                        Err(_) => {
10727                            timeout_files.push(file_path);
10728                            // Thread will be abandoned
10729                        }
10730                    }
10731                }
10732            }
10733        }
10734
10735        eprintln!("\n=== ZPWR Scripts Parse Results ===");
10736        eprintln!("Passed: {}/{}", passed, total);
10737
10738        if !timeout_files.is_empty() {
10739            eprintln!("\nTimeout files (>2s):");
10740            for file in &timeout_files {
10741                eprintln!("  {}", file);
10742            }
10743        }
10744
10745        if !failed_files.is_empty() {
10746            eprintln!("\nFailed files:");
10747            for (file, err) in &failed_files {
10748                eprintln!("  {} - {}", file, err);
10749            }
10750        }
10751
10752        // Allow some failures initially, but track progress
10753        let pass_rate = if total > 0 {
10754            (passed as f64 / total as f64) * 100.0
10755        } else {
10756            0.0
10757        };
10758        eprintln!("Pass rate: {:.1}%", pass_rate);
10759
10760        // Require at least 50% pass rate for now
10761        assert!(pass_rate >= 50.0, "Pass rate too low: {:.1}%", pass_rate);
10762    }
10763
10764    /// c:2643 — `get_cond_num` returns 0..=8 for the canonical binary
10765    /// test operators in order `nt ot ef eq ne lt gt le ge`. The
10766    /// index IS the wordcode opcode dispatch key; flipping any entry
10767    /// would silently mis-dispatch `[[ a -eq b ]]` to a different op.
10768    #[test]
10769    fn get_cond_num_canonical_order_matches_dispatch_table() {
10770        let _g = crate::test_util::global_state_lock();
10771        assert_eq!(get_cond_num("nt"), 0);
10772        assert_eq!(get_cond_num("ot"), 1);
10773        assert_eq!(get_cond_num("ef"), 2);
10774        assert_eq!(get_cond_num("eq"), 3);
10775        assert_eq!(get_cond_num("ne"), 4);
10776        assert_eq!(get_cond_num("lt"), 5);
10777        assert_eq!(get_cond_num("gt"), 6);
10778        assert_eq!(get_cond_num("le"), 7);
10779        assert_eq!(get_cond_num("ge"), 8);
10780    }
10781
10782    /// c:2643 — unknown operator returns -1 (sentinel for "not in the
10783    /// binary set"). Regression returning 0 silently would alias
10784    /// every unknown op to `-nt`, dispatching to the wrong handler.
10785    #[test]
10786    fn get_cond_num_unknown_operator_returns_minus_one() {
10787        let _g = crate::test_util::global_state_lock();
10788        assert_eq!(get_cond_num("xx"), -1);
10789        assert_eq!(get_cond_num(""), -1);
10790        assert_eq!(get_cond_num("eqnt"), -1, "exact-match required");
10791        assert_eq!(
10792            get_cond_num("NT"),
10793            -1,
10794            "case-sensitive — uppercase rejected"
10795        );
10796    }
10797
10798    /// c:2628 — `par_cond_double` requires arg `a` to start with `-`
10799    /// AND have at least one more char. Empty string OR single `-`
10800    /// must error (return 1 via zerr). Regression accepting empty
10801    /// would dispatch `[[ "" string ]]` as a unary test.
10802    #[test]
10803    fn par_cond_double_rejects_short_or_non_dash_first_arg() {
10804        let _g = crate::test_util::global_state_lock();
10805        // empty
10806        let _ = par_cond_double("", "b");
10807        // not-dash
10808        let _ = par_cond_double("foo", "b");
10809        // bare dash
10810        let _ = par_cond_double("-", "b");
10811        // All three must NOT crash + return 1 (error path).
10812    }
10813
10814    /// c:2647 CONDSTRS table — exhaustive iteration: every entry's
10815    /// index round-trips through get_cond_num. A regression that
10816    /// drops an entry would let `[[ a -ef b ]]` silently mis-dispatch.
10817    #[test]
10818    fn get_cond_num_round_trips_for_every_table_entry() {
10819        let _g = crate::test_util::global_state_lock();
10820        for (i, op) in ["nt", "ot", "ef", "eq", "ne", "lt", "gt", "le", "ge"]
10821            .iter()
10822            .enumerate()
10823        {
10824            assert_eq!(get_cond_num(op) as usize, i, "{op} must map to index {i}");
10825        }
10826    }
10827
10828    /// c:2643 — `get_cond_num` is byte-exact: a partial-prefix string
10829    /// must NOT match. `e` (one char) is not `eq`. Catches a
10830    /// regression using `starts_with` instead of equality.
10831    #[test]
10832    fn get_cond_num_partial_prefix_does_not_match() {
10833        let _g = crate::test_util::global_state_lock();
10834        assert_eq!(get_cond_num("e"), -1);
10835        assert_eq!(get_cond_num("eq2"), -1);
10836        assert_eq!(get_cond_num("n"), -1);
10837    }
10838
10839    /// c:2628 — `par_cond_double` checks `IS_DASH(ac[0])` so any
10840    /// non-dash first char fails. The lexed Dash sentinel `\u{9b}`
10841    /// MUST be accepted alongside ASCII `-` (the lexer emits it
10842    /// inside `[[ ... ]]`). Regression dropping the sentinel form
10843    /// would break every cond expression after lexing.
10844    #[test]
10845    fn par_cond_double_accepts_lexed_dash_sentinel() {
10846        let _g = crate::test_util::global_state_lock();
10847        // First char being the Dash sentinel + valid unary letter
10848        // must NOT trigger the "condition expected" error path.
10849        // We can't easily probe the wordcode emission here, but
10850        // the function MUST return without panic for both forms.
10851        let _ = par_cond_double("-z", "foo");
10852        let _ = par_cond_double("\u{9b}z", "foo");
10853    }
10854
10855    /// c:2643 — case sensitivity: uppercase `EQ` MUST NOT match `eq`.
10856    /// zsh's `[[ a -EQ b ]]` is documented as a parse error (only
10857    /// lowercase variants are recognised). Regression doing
10858    /// case-insensitive lookup would silently accept it.
10859    #[test]
10860    fn get_cond_num_is_case_sensitive() {
10861        let _g = crate::test_util::global_state_lock();
10862        assert_eq!(get_cond_num("EQ"), -1);
10863        assert_eq!(get_cond_num("Eq"), -1);
10864        assert_eq!(get_cond_num("eQ"), -1);
10865        // Lowercase still works.
10866        assert_eq!(get_cond_num("eq"), 3);
10867    }
10868
10869    /// `Src/parse.c:2862-2868` — `ecgetstr` inline-3-byte case packs
10870    /// up to 3 chars into bits 3-26 of the wordcode word, then C emits
10871    /// `buf[3] = '\0'; r = dupstring(buf);`. `dupstring` uses `strlen`
10872    /// so the resulting string TRUNCATES at the first NUL byte —
10873    /// short strings of 1 or 2 chars get their tail NUL-padded and
10874    /// silently dropped by strlen.
10875    ///
10876    /// The previous Rust port used `retain(|&x| x != 0)` which SPLICES
10877    /// OUT interior NULs (so `[a, 0, b]` would yield "ab" instead of
10878    /// C's "a"). Verify both endpoints work correctly:
10879    ///   * 1-char string ("a", 0, 0)        → "a"   (strlen-truncate)
10880    ///   * 2-char string ("ab", 0)          → "ab"  (strlen-truncate)
10881    ///   * 3-char string ("abc")            → "abc" (full)
10882    ///   * pathological ("a", 0, "b")       → "a"   (NOT "ab")
10883    #[test]
10884    fn ecgetstr_inline_string_truncates_at_first_nul_like_c_strlen() {
10885        let _g = crate::test_util::global_state_lock();
10886        // Build a wordcode word with `c & 2 != 0` (inline-string flag)
10887        // and the 3 bytes packed at offsets 3, 11, 19. `c & 1` is the
10888        // tokflag; clear it for this test.
10889        fn pack_inline(b0: u8, b1: u8, b2: u8) -> u32 {
10890            // c:2862 layout — bit0 = tokflag (0 here), bit1 = inline (1),
10891            // bits 3-10 = b0, bits 11-18 = b1, bits 19-26 = b2.
10892            (2u32) | ((b0 as u32) << 3) | ((b1 as u32) << 11) | ((b2 as u32) << 19)
10893        }
10894        let mk_state = |word: u32| -> estate {
10895            let p = eprog {
10896                flags: 0,
10897                len: 1,
10898                npats: 0,
10899                nref: 0,
10900                pats: Vec::new(),
10901                prog: vec![word],
10902                strs: None,
10903                shf: None,
10904                dump: None,
10905                strs_metafied: false, // native pool — clean UTF-8
10906            };
10907            estate {
10908                prog: Box::new(p),
10909                pc: 0,
10910                strs: None,
10911                strs_offset: 0,
10912            }
10913        };
10914
10915        // 1-char: ('a', 0, 0) → "a"
10916        let mut st = mk_state(pack_inline(b'a', 0, 0));
10917        assert_eq!(
10918            ecgetstr(&mut st, 0, None),
10919            "a",
10920            "c:2869 strlen truncates 1-char inline at the NUL tail"
10921        );
10922
10923        // 2-char: ('a', 'b', 0) → "ab"
10924        let mut st = mk_state(pack_inline(b'a', b'b', 0));
10925        assert_eq!(
10926            ecgetstr(&mut st, 0, None),
10927            "ab",
10928            "c:2869 strlen truncates 2-char inline at the NUL tail"
10929        );
10930
10931        // 3-char: ('a', 'b', 'c') → "abc"
10932        let mut st = mk_state(pack_inline(b'a', b'b', b'c'));
10933        assert_eq!(
10934            ecgetstr(&mut st, 0, None),
10935            "abc",
10936            "c:2869 full 3-byte inline preserved"
10937        );
10938
10939        // Pathological: ('a', 0, 'b') → "a" (NOT "ab" from retain-splice)
10940        let mut st = mk_state(pack_inline(b'a', 0, b'b'));
10941        assert_eq!(
10942            ecgetstr(&mut st, 0, None),
10943            "a",
10944            "c:2869 strlen STOPS at first NUL; must not splice 'b' through"
10945        );
10946    }
10947
10948    /// Pin: `init_parse_status` resets ALL six lexer-parser flags
10949    /// per `Src/parse.c:500-502`. Specifically `inrepeat_ = 0` at
10950    /// c:501 was previously missing in the Rust port. Pin every
10951    /// reset so a future regression that drops one is caught.
10952    #[test]
10953    fn init_parse_status_resets_all_lexer_parser_flags() {
10954        let _g = crate::test_util::global_state_lock();
10955        // Dirty every flag to a non-default value.
10956        set_incasepat(5);
10957        set_incond(7);
10958        set_inredir(true);
10959        set_infor(3);
10960        set_intypeset(true);
10961        set_inrepeat(2);
10962        set_incmdpos(false);
10963        // Reset.
10964        init_parse_status();
10965        // c:500-502 — every flag back to its default.
10966        assert_eq!(incasepat(), 0, "c:500 — incasepat = 0");
10967        assert_eq!(incond(), 0, "c:500 — incond = 0");
10968        assert!(!inredir(), "c:500 — inredir = 0");
10969        assert_eq!(infor(), 0, "c:500 — infor = 0");
10970        assert!(!intypeset(), "c:500 — intypeset = 0");
10971        assert_eq!(
10972            inrepeat(),
10973            0,
10974            "c:501 — inrepeat_ = 0 (was previously missing)"
10975        );
10976        assert!(incmdpos(), "c:502 — incmdpos = 1");
10977    }
10978
10979    // ═══════════════════════════════════════════════════════════════════
10980    // AST shape tests — feed source through parse(), walk the resulting
10981    // ZshProgram, assert structural properties. Each test uses the local
10982    // `parse(input)` helper that errors cleanly on parse failure.
10983    // Anchor: where applicable, behavior matches `zsh -n -c '...'`
10984    // (parse-only, no execution — which would error on syntax issues).
10985    // ═══════════════════════════════════════════════════════════════════
10986
10987    /// Empty input → ZshProgram with no lists.
10988    #[test]
10989    fn parse_empty_source_yields_zero_lists() {
10990        let _g = crate::test_util::global_state_lock();
10991        let prog = parse("").unwrap();
10992        assert_eq!(prog.lists.len(), 0);
10993    }
10994
10995    /// Comment-only input → no lists (comments are skipped at lex level).
10996    #[test]
10997    fn parse_only_comment_yields_zero_lists() {
10998        let _g = crate::test_util::global_state_lock();
10999        let prog = parse("# this is just a comment").unwrap();
11000        assert_eq!(prog.lists.len(), 0, "comments alone produce no cmds");
11001    }
11002
11003    /// Three commands separated by `;` → three lists.
11004    #[test]
11005    fn parse_three_semicolon_separated_commands_yield_three_lists() {
11006        let _g = crate::test_util::global_state_lock();
11007        let prog = parse("a; b; c").unwrap();
11008        assert_eq!(prog.lists.len(), 3);
11009    }
11010
11011    /// Background command — async flag set on the list.
11012    #[test]
11013    fn parse_background_command_sets_async_flag() {
11014        let _g = crate::test_util::global_state_lock();
11015        let prog = parse("sleep 1 &").unwrap();
11016        assert_eq!(prog.lists.len(), 1);
11017        assert!(
11018            prog.lists[0].flags.async_,
11019            "trailing `&` must set async_ flag"
11020        );
11021    }
11022
11023    /// Pipe count: `a | b | c | d` → 4 stages.
11024    #[test]
11025    fn parse_four_stage_pipeline_has_three_next_links() {
11026        let _g = crate::test_util::global_state_lock();
11027        let prog = parse("a | b | c | d").unwrap();
11028        let mut pipe = &prog.lists[0].sublist.pipe;
11029        let mut count = 1;
11030        while let Some(next) = &pipe.next {
11031            pipe = next;
11032            count += 1;
11033        }
11034        assert_eq!(count, 4, "4 commands should produce 4 pipe stages");
11035    }
11036
11037    /// `|&` between pipeline stages sets merge_stderr.
11038    #[test]
11039    fn parse_pipe_amp_sets_merge_stderr() {
11040        let _g = crate::test_util::global_state_lock();
11041        let prog = parse("a |& b").unwrap();
11042        let pipe = &prog.lists[0].sublist.pipe;
11043        assert!(pipe.next.is_some());
11044        assert!(pipe.merge_stderr, "|& must set merge_stderr");
11045    }
11046
11047    /// `cmd1 || cmd2`: sublist.next is Some with `Or`.
11048    #[test]
11049    fn parse_or_operator_sets_sublist_op_or() {
11050        let _g = crate::test_util::global_state_lock();
11051        let prog = parse("cmd1 || cmd2").unwrap();
11052        let sublist = &prog.lists[0].sublist;
11053        let (op, _) = sublist.next.as_ref().expect("must have next");
11054        assert_eq!(*op, SublistOp::Or);
11055    }
11056
11057    /// `! cmd` sets the not flag on the sublist.
11058    #[test]
11059    fn parse_bang_negation_sets_sublist_not_flag() {
11060        let _g = crate::test_util::global_state_lock();
11061        let prog = parse("! false").unwrap();
11062        let sublist = &prog.lists[0].sublist;
11063        assert!(sublist.flags.not, "`!` prefix must set sublist.flags.not");
11064    }
11065
11066    // ── Compound commands ────────────────────────────────────────────
11067    /// `while cond; do body; done` → ZshCommand::While.
11068    #[test]
11069    fn parse_while_loop_yields_while_command() {
11070        let _g = crate::test_util::global_state_lock();
11071        let prog = parse("while true; do echo x; done").unwrap();
11072        assert!(matches!(
11073            prog.lists[0].sublist.pipe.cmd,
11074            ZshCommand::While(_)
11075        ));
11076    }
11077
11078    /// `until cond; do body; done` → ZshCommand::Until.
11079    /// Anchor: `zsh -n -c 'until false; do echo; done'` accepts and parses
11080    /// as an until-loop. zshrs accepts but emits a DIFFERENT AST variant
11081    /// (not Until). Bug — until loop is mis-classified.
11082    #[test]
11083    fn parse_until_loop_yields_until_command_anchored_to_zsh() {
11084        let _g = crate::test_util::global_state_lock();
11085        let prog = parse("until false; do echo x; done").unwrap();
11086        assert!(
11087            matches!(prog.lists[0].sublist.pipe.cmd, ZshCommand::Until(_)),
11088            "zsh parses `until` as Until variant; zshrs uses different variant: {:?}",
11089            prog.lists[0].sublist.pipe.cmd
11090        );
11091    }
11092
11093    /// `(cmd)` → Subsh variant.
11094    #[test]
11095    fn parse_parens_yield_subsh_command() {
11096        let _g = crate::test_util::global_state_lock();
11097        let prog = parse("(echo hi)").unwrap();
11098        assert!(matches!(
11099            prog.lists[0].sublist.pipe.cmd,
11100            ZshCommand::Subsh(_)
11101        ));
11102    }
11103
11104    /// `{ cmd; }` → Cursh (current-shell) command.
11105    #[test]
11106    fn parse_braces_yield_cursh_command() {
11107        let _g = crate::test_util::global_state_lock();
11108        let prog = parse("{ echo hi; }").unwrap();
11109        assert!(matches!(
11110            prog.lists[0].sublist.pipe.cmd,
11111            ZshCommand::Cursh(_)
11112        ));
11113    }
11114
11115    /// `[[ a == b ]]` → ZshCommand::Cond.
11116    #[test]
11117    fn parse_double_brackets_yield_cond_command() {
11118        let _g = crate::test_util::global_state_lock();
11119        let prog = parse("[[ a == b ]]").unwrap();
11120        assert!(matches!(
11121            prog.lists[0].sublist.pipe.cmd,
11122            ZshCommand::Cond(_)
11123        ));
11124    }
11125
11126    /// `(( 1 + 2 ))` → ZshCommand::Arith.
11127    #[test]
11128    fn parse_double_parens_yield_arith_command() {
11129        let _g = crate::test_util::global_state_lock();
11130        let prog = parse("(( 1 + 2 ))").unwrap();
11131        assert!(matches!(
11132            prog.lists[0].sublist.pipe.cmd,
11133            ZshCommand::Arith(_)
11134        ));
11135    }
11136
11137    /// `repeat 3 do echo x; done` → ZshCommand::Repeat.
11138    #[test]
11139    fn parse_repeat_loop_yields_repeat_command() {
11140        let _g = crate::test_util::global_state_lock();
11141        let prog = parse("repeat 3 do echo x; done").unwrap();
11142        assert!(matches!(
11143            prog.lists[0].sublist.pipe.cmd,
11144            ZshCommand::Repeat(_)
11145        ));
11146    }
11147
11148    // ── Function definitions ─────────────────────────────────────────
11149    /// `name() { body; }` → FuncDef variant.
11150    #[test]
11151    fn parse_paren_funcdef_yields_funcdef_command() {
11152        let _g = crate::test_util::global_state_lock();
11153        let prog = parse("greet() { echo hi; }").unwrap();
11154        assert!(matches!(
11155            prog.lists[0].sublist.pipe.cmd,
11156            ZshCommand::FuncDef(_)
11157        ));
11158    }
11159
11160    /// `function name { body; }` → FuncDef variant (zsh keyword form).
11161    #[test]
11162    fn parse_function_keyword_funcdef_yields_funcdef_command() {
11163        let _g = crate::test_util::global_state_lock();
11164        let prog = parse("function greet { echo hi; }").unwrap();
11165        assert!(matches!(
11166            prog.lists[0].sublist.pipe.cmd,
11167            ZshCommand::FuncDef(_)
11168        ));
11169    }
11170
11171    /// Syntax error — `if` without `fi` → parse returns Err.
11172    /// Anchor: `echo 'if true; then echo' | zsh -n` → "parse error".
11173    #[test]
11174    fn parse_unterminated_if_returns_error_anchored_to_zsh() {
11175        let _g = crate::test_util::global_state_lock();
11176        let r = parse("if true; then echo yes");
11177        assert!(r.is_err(), "zsh -n: parse error near `\\n`");
11178    }
11179
11180    /// Syntax error — bare `done` without `for/while/until` → error.
11181    /// Anchor: `echo done | zsh -n` → "parse error near `done`".
11182    #[test]
11183    fn parse_orphan_done_returns_error_anchored_to_zsh() {
11184        let _g = crate::test_util::global_state_lock();
11185        let r = parse("done");
11186        assert!(r.is_err(), "zsh -n: parse error near `done`");
11187    }
11188
11189    /// Simple command's words are metafied at the AST layer (matches
11190    /// zsh's internal representation: `-` lexes to `Dash` = 0x9b, `*`
11191    /// to `Star`, etc.). zsh untokenizes via `untokenize()` BEFORE
11192    /// surfacing words at execution time (Src/exec.c:execcmd_args).
11193    /// This test pins the round-trip: `untokenize(word)` recovers the
11194    /// user-visible form. If parse-time unmetafy ever lands the
11195    /// untokenize call becomes a no-op; the test stays green either
11196    /// way. Companion test below pins the metafied internal form.
11197    #[test]
11198    fn parse_simple_command_words_unmetafied_like_zsh_anchored() {
11199        let _g = crate::test_util::global_state_lock();
11200        let prog = parse("ls -la /tmp").unwrap();
11201        match &prog.lists[0].sublist.pipe.cmd {
11202            ZshCommand::Simple(s) => {
11203                let untok: Vec<String> = s
11204                    .words
11205                    .iter()
11206                    .map(|w| crate::ported::lex::untokenize(w))
11207                    .collect();
11208                assert_eq!(
11209                    untok,
11210                    vec!["ls", "-la", "/tmp"],
11211                    "untokenize(word) must yield the user-visible form"
11212                );
11213            }
11214            other => panic!("expected Simple, got {other:?}"),
11215        }
11216    }
11217
11218    /// Pin the OBSERVED zshrs contract: simple-command word array
11219    /// contains metafied bytes. This is the active (passing) version
11220    /// of the anchor above — it documents zshrs's current internal
11221    /// representation. If zshrs starts unmetafying at parse time, this
11222    /// test will FAIL and the anchor-style test above will start passing.
11223    #[test]
11224    fn parse_simple_command_words_metafied_internal_form() {
11225        let _g = crate::test_util::global_state_lock();
11226        let prog = parse("ls -la /tmp").unwrap();
11227        match &prog.lists[0].sublist.pipe.cmd {
11228            ZshCommand::Simple(s) => {
11229                assert_eq!(s.words.len(), 3);
11230                assert_eq!(s.words[0], "ls");
11231                assert_eq!(s.words[2], "/tmp");
11232                // s.words[1] contains the metafied `-` (`\u{9b}` Dash byte)
11233                // followed by "la". Don't pin the exact byte form (it
11234                // may change); pin that the length is right.
11235                assert_eq!(s.words[1].chars().count(), 3, "`-la` is 3 chars");
11236                assert!(s.words[1].ends_with("la"));
11237            }
11238            other => panic!("expected Simple, got {other:?}"),
11239        }
11240    }
11241
11242    // ─── zsh-corpus pins for parser: structural shapes ────────────────
11243
11244    /// Empty input — parse succeeds, lists may be empty.
11245    #[test]
11246    fn parse_corpus_empty_input_no_error() {
11247        let _g = crate::test_util::global_state_lock();
11248        let prog = parse("").unwrap();
11249        assert!(
11250            prog.lists.is_empty() || prog.lists.len() <= 1,
11251            "empty input → 0 or 1 list, got {}",
11252            prog.lists.len()
11253        );
11254    }
11255
11256    /// Comment-only input parses as empty.
11257    #[test]
11258    fn parse_corpus_comment_only_no_error() {
11259        let _g = crate::test_util::global_state_lock();
11260        let r = parse("# just a comment");
11261        assert!(r.is_ok(), "comment-only parse should succeed");
11262    }
11263
11264    /// `cmd1; cmd2` — two top-level lists or two sublists.
11265    #[test]
11266    fn parse_corpus_semicolon_separates_commands() {
11267        let _g = crate::test_util::global_state_lock();
11268        let prog = parse("echo a; echo b").unwrap();
11269        // We pin: parse produces > 0 lists/sublists; details vary.
11270        assert!(!prog.lists.is_empty(), "non-empty parse");
11271    }
11272
11273    /// `a && b` — DAMPER joins into a sublist chain.
11274    #[test]
11275    fn parse_corpus_logical_and_parses() {
11276        let _g = crate::test_util::global_state_lock();
11277        let r = parse("true && false");
11278        assert!(r.is_ok(), "`a && b` parses cleanly");
11279    }
11280
11281    /// `a || b` — DBAR.
11282    #[test]
11283    fn parse_corpus_logical_or_parses() {
11284        let _g = crate::test_util::global_state_lock();
11285        let r = parse("false || true");
11286        assert!(r.is_ok(), "`a || b` parses cleanly");
11287    }
11288
11289    /// `a | b` pipeline.
11290    #[test]
11291    fn parse_corpus_pipeline_parses() {
11292        let _g = crate::test_util::global_state_lock();
11293        let r = parse("echo hi | cat");
11294        assert!(r.is_ok(), "`a | b` parses");
11295    }
11296
11297    /// `if true; then echo x; fi` — basic if-then-fi block.
11298    #[test]
11299    fn parse_corpus_if_then_fi_parses() {
11300        let _g = crate::test_util::global_state_lock();
11301        let r = parse("if true; then echo x; fi");
11302        assert!(r.is_ok(), "if/then/fi parses cleanly");
11303    }
11304
11305    /// `for i in 1 2 3; do echo $i; done`.
11306    #[test]
11307    fn parse_corpus_for_do_done_parses() {
11308        let _g = crate::test_util::global_state_lock();
11309        let r = parse("for i in 1 2 3; do echo $i; done");
11310        assert!(r.is_ok(), "for/do/done parses cleanly");
11311    }
11312
11313    /// `while true; do break; done`.
11314    #[test]
11315    fn parse_corpus_while_do_done_parses() {
11316        let _g = crate::test_util::global_state_lock();
11317        let r = parse("while true; do break; done");
11318        assert!(r.is_ok(), "while/do/done parses cleanly");
11319    }
11320
11321    /// `case x in (a) echo A;; esac` — case statement.
11322    #[test]
11323    fn parse_corpus_case_esac_parses() {
11324        let _g = crate::test_util::global_state_lock();
11325        let r = parse("case x in (a) echo A;; esac");
11326        assert!(r.is_ok(), "case/esac parses cleanly");
11327    }
11328
11329    /// Function definition `f() { echo x }`.
11330    #[test]
11331    fn parse_corpus_function_def_parses() {
11332        let _g = crate::test_util::global_state_lock();
11333        let r = parse("f() { echo x }");
11334        assert!(r.is_ok(), "f() {{ ... }} parses cleanly");
11335    }
11336
11337    /// `(subshell echo a)` — subshell.
11338    #[test]
11339    fn parse_corpus_subshell_parens_parses() {
11340        let _g = crate::test_util::global_state_lock();
11341        let r = parse("( echo a )");
11342        assert!(r.is_ok(), "subshell parses cleanly");
11343    }
11344
11345    // ═══════════════════════════════════════════════════════════════════
11346    // C-parity tests pinning Src/parse.c. Tests that capture KNOWN
11347    // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
11348    // ═══════════════════════════════════════════════════════════════════
11349
11350    /// `empty_eprog(p)` returns true on an eprog with empty `prog`.
11351    /// C `Src/parse.c:584`:
11352    ///   `return (!p || !p->prog || *p->prog == WCB_END());`
11353    /// Rust port at parse.rs:685 — `p.prog.is_empty() || p.prog[0] == WCB_END()`.
11354    #[test]
11355    fn empty_eprog_empty_prog_returns_true() {
11356        let _g = crate::test_util::global_state_lock();
11357        let p = crate::ported::zsh_h::eprog::default();
11358        assert!(empty_eprog(&p), "empty prog vec → empty_eprog true");
11359    }
11360
11361    /// `empty_eprog(p)` returns true when first wordcode is WCB_END.
11362    /// C: `*p->prog == WCB_END()`.
11363    #[test]
11364    fn empty_eprog_first_wcb_end_returns_true() {
11365        let _g = crate::test_util::global_state_lock();
11366        let mut p = crate::ported::zsh_h::eprog::default();
11367        p.prog.push(WCB_END());
11368        assert!(empty_eprog(&p), "prog[0]==WCB_END → empty_eprog true");
11369    }
11370
11371    /// `empty_eprog(p)` returns false for non-empty non-END prog.
11372    #[test]
11373    fn empty_eprog_non_empty_non_end_returns_false() {
11374        let _g = crate::test_util::global_state_lock();
11375        let mut p = crate::ported::zsh_h::eprog::default();
11376        // Push some non-END wordcode (1 is arbitrary non-zero, not WCB_END).
11377        p.prog.push(1);
11378        assert!(!empty_eprog(&p), "non-END first opcode → false");
11379    }
11380
11381    /// `ecstrcode("")` returns a wordcode for the empty string. C
11382    /// `Src/parse.c:346-ish` ecstrcode interns strings in `ecbuf`.
11383    /// Pin: same call returns same wordcode (deterministic intern).
11384    #[test]
11385    fn ecstrcode_empty_string_returns_deterministic_code() {
11386        let _g = crate::test_util::global_state_lock();
11387        init_parse();
11388        let a = ecstrcode("");
11389        let b = ecstrcode("");
11390        assert_eq!(a, b, "intern of '' must be deterministic");
11391    }
11392
11393    /// `ecstrcode` of two different strings returns different codes.
11394    #[test]
11395    fn ecstrcode_distinct_strings_get_distinct_codes() {
11396        let _g = crate::test_util::global_state_lock();
11397        init_parse();
11398        let a = ecstrcode("foo");
11399        let b = ecstrcode("bar");
11400        // Should differ — if equal, intern table collapsed two different
11401        // strings to the same key (bug).
11402        assert_ne!(a, b, "different strings must intern to different codes");
11403    }
11404
11405    /// `parse_event(ENDINPUT)` on empty input returns None.
11406    /// C `Src/parse.c:715-ish` — empty token stream → no program.
11407    #[test]
11408    fn parse_event_empty_returns_none() {
11409        let _g = crate::test_util::global_state_lock();
11410        init_parse();
11411        // Empty input typically yields no program; needs lex state.
11412        let r = parse_event(crate::ported::lex::ENDINPUT);
11413        assert!(r.is_none(), "no tokens → no event");
11414    }
11415
11416    // ═══════════════════════════════════════════════════════════════════
11417    // Additional C-parity tests for Src/parse.c.
11418    // ═══════════════════════════════════════════════════════════════════
11419
11420    /// c:399 — `ecadd(c)` returns the index where `c` was placed,
11421    /// not the post-increment value. Sequential ecadd calls return
11422    /// strictly increasing indices.
11423    #[test]
11424    fn ecadd_returns_strictly_increasing_indices() {
11425        let _g = crate::test_util::global_state_lock();
11426        init_parse();
11427        let i0 = ecadd(0xDEAD);
11428        let i1 = ecadd(0xBEEF);
11429        let i2 = ecadd(0xC0DE);
11430        assert!(
11431            i1 > i0,
11432            "ecadd indices must strictly increase, got {i0} then {i1}"
11433        );
11434        assert!(
11435            i2 > i1,
11436            "ecadd indices must strictly increase, got {i1} then {i2}"
11437        );
11438        assert_eq!(i1, i0 + 1, "consecutive ecadds advance by 1");
11439        assert_eq!(i2, i1 + 1, "consecutive ecadds advance by 1");
11440    }
11441
11442    /// c:413 — `ecdel(p)` removes one wordcode, shrinks ecused by 1.
11443    /// Pin: subsequent ecadd reuses freed slot (ecused decreased).
11444    #[test]
11445    fn ecdel_shrinks_ecused_by_one() {
11446        let _g = crate::test_util::global_state_lock();
11447        init_parse();
11448        let _i0 = ecadd(0xA);
11449        let i1 = ecadd(0xB);
11450        let _i2 = ecadd(0xC);
11451        let next_before = ECUSED.get();
11452        ecdel(i1);
11453        let next_after = ECUSED.get();
11454        assert_eq!(
11455            next_after,
11456            next_before - 1,
11457            "ecdel must decrement ecused by exactly 1"
11458        );
11459    }
11460
11461    /// c:399-405 — `ecadd` after exhausting buffer must grow it (no
11462    /// panic on push past current eclen). Pin: 1000 adds don't crash.
11463    #[test]
11464    fn ecadd_grows_buffer_on_demand() {
11465        let _g = crate::test_util::global_state_lock();
11466        init_parse();
11467        for i in 0..1000 {
11468            ecadd(i as u32);
11469        }
11470        // No panic = grow path works.
11471        assert!(ECUSED.get() >= 1000, "1000 adds → ecused ≥ 1000");
11472    }
11473
11474    /// c:426 — `ecstrcode` of short strings (≤4 bytes) returns a
11475    /// packed inline wordcode (not an offset into the string region).
11476    /// Pin: identical short strings get identical codes.
11477    #[test]
11478    fn ecstrcode_short_strings_are_deterministic() {
11479        let _g = crate::test_util::global_state_lock();
11480        init_parse();
11481        let a = ecstrcode("ab");
11482        let b = ecstrcode("ab");
11483        assert_eq!(a, b, "same short string must intern to same code");
11484    }
11485
11486    /// c:426 — long strings (>4 bytes) hit the deduped string region.
11487    /// Pin: same long string returns same code on repeat (registry
11488    /// dedupes).
11489    #[test]
11490    fn ecstrcode_long_strings_dedupe_in_registry() {
11491        let _g = crate::test_util::global_state_lock();
11492        init_parse();
11493        let a = ecstrcode("a-much-longer-test-string");
11494        let b = ecstrcode("a-much-longer-test-string");
11495        assert_eq!(a, b, "registry must dedupe identical long strings");
11496    }
11497
11498    /// `clear_hdocs()` is idempotent — calling twice in a row leaves
11499    /// HDOCS = None and LEX_HEREDOCS empty.
11500    #[test]
11501    fn clear_hdocs_is_idempotent() {
11502        let _g = crate::test_util::global_state_lock();
11503        clear_hdocs();
11504        clear_hdocs();
11505        HDOCS.with_borrow(|h| assert!(h.is_none(), "HDOCS must be None"));
11506        LEX_HEREDOCS.with_borrow(|v| assert!(v.is_empty(), "LEX_HEREDOCS must be empty"));
11507    }
11508
11509    /// `init_parse()` resets parse state to known empty defaults.
11510    /// Multiple init_parse calls are safe (idempotent).
11511    #[test]
11512    fn init_parse_is_idempotent() {
11513        let _g = crate::test_util::global_state_lock();
11514        init_parse();
11515        init_parse();
11516        // No panic = pass.
11517    }
11518
11519    /// `empty_eprog` returns true for a default-constructed eprog
11520    /// (empty prog vec).
11521    #[test]
11522    fn empty_eprog_true_for_empty_prog() {
11523        let _g = crate::test_util::global_state_lock();
11524        let p = eprog {
11525            prog: Vec::new(),
11526            ..Default::default()
11527        };
11528        assert!(empty_eprog(&p), "empty prog vec → empty eprog");
11529    }
11530
11531    /// `empty_eprog` returns true when prog[0] == WCB_END().
11532    #[test]
11533    fn empty_eprog_true_for_end_only_prog() {
11534        let _g = crate::test_util::global_state_lock();
11535        let p = eprog {
11536            prog: vec![WCB_END()],
11537            ..Default::default()
11538        };
11539        assert!(empty_eprog(&p), "WCB_END as first opcode → empty");
11540    }
11541
11542    /// `ecadjusthere(p, d)` is safe to call when HDOCS is None.
11543    #[test]
11544    fn ecadjusthere_safe_when_hdocs_none() {
11545        let _g = crate::test_util::global_state_lock();
11546        clear_hdocs();
11547        // No panic = pass.
11548        ecadjusthere(0, 0);
11549        ecadjusthere(100, -5);
11550        ecadjusthere(0, 10);
11551    }
11552
11553    /// `ecispace(p, n)` with n=0 is a no-op.
11554    #[test]
11555    fn ecispace_zero_n_is_noop() {
11556        let _g = crate::test_util::global_state_lock();
11557        init_parse();
11558        let before = ECUSED.get();
11559        ecispace(0, 0);
11560        let after = ECUSED.get();
11561        assert_eq!(before, after, "ecispace(_, 0) must not advance ecused");
11562    }
11563
11564    // ═══════════════════════════════════════════════════════════════════
11565    // Additional C-parity tests for Src/parse.c
11566    // c:146 parse_context_save / c:191 parse_context_restore /
11567    // c:225 ecadjusthere / c:293 ecadd / c:346 ecstrcode / c:574 init_parse /
11568    // c:685 empty_eprog / c:693 clear_hdocs / c:786 parse_list / c:815 parse_cond
11569    // c:2234 par_wordlist / c:2249 par_nl_wordlist
11570    // ═══════════════════════════════════════════════════════════════════
11571
11572    /// c:293 — `ecadd` returns usize (compile-time type pin).
11573    #[test]
11574    fn ecadd_returns_usize_type() {
11575        let _g = crate::test_util::global_state_lock();
11576        init_parse();
11577        let _: usize = ecadd(0);
11578    }
11579
11580    /// c:346 — `ecstrcode` returns u32 (compile-time type pin).
11581    #[test]
11582    fn ecstrcode_returns_u32_type() {
11583        let _g = crate::test_util::global_state_lock();
11584        init_parse();
11585        let _: u32 = ecstrcode("");
11586    }
11587
11588    /// c:346 — `ecstrcode("")` empty string is safe.
11589    #[test]
11590    fn ecstrcode_empty_string_no_panic() {
11591        let _g = crate::test_util::global_state_lock();
11592        init_parse();
11593        let _ = ecstrcode("");
11594    }
11595
11596    /// c:346 — `ecstrcode` is deterministic for same input.
11597    #[test]
11598    fn ecstrcode_is_deterministic() {
11599        let _g = crate::test_util::global_state_lock();
11600        init_parse();
11601        for s in ["", "a", "abc", "hello world"] {
11602            let first = ecstrcode(s);
11603            for _ in 0..3 {
11604                assert_eq!(
11605                    ecstrcode(s),
11606                    first,
11607                    "ecstrcode({:?}) must be deterministic",
11608                    s
11609                );
11610            }
11611        }
11612    }
11613
11614    /// c:786 — `parse_list` returns Option<eprog>.
11615    #[test]
11616    fn parse_list_returns_option_eprog_type() {
11617        let _g = crate::test_util::global_state_lock();
11618        init_parse();
11619        let _: Option<eprog> = parse_list();
11620    }
11621
11622    /// c:815 — `parse_cond` returns Option<eprog>.
11623    #[test]
11624    fn parse_cond_returns_option_eprog_type() {
11625        let _g = crate::test_util::global_state_lock();
11626        init_parse();
11627        let _: Option<eprog> = parse_cond();
11628    }
11629
11630    /// c:2234 — `par_wordlist` returns Vec<String>.
11631    #[test]
11632    fn par_wordlist_returns_vec_string_type() {
11633        let _g = crate::test_util::global_state_lock();
11634        init_parse();
11635        let _: Vec<String> = par_wordlist();
11636    }
11637
11638    /// c:2249 — `par_nl_wordlist` returns Vec<String>.
11639    #[test]
11640    fn par_nl_wordlist_returns_vec_string_type() {
11641        let _g = crate::test_util::global_state_lock();
11642        init_parse();
11643        let _: Vec<String> = par_nl_wordlist();
11644    }
11645
11646    /// c:693 — `clear_hdocs` deterministic state after call (no-panic).
11647    #[test]
11648    fn clear_hdocs_deterministic_after_call() {
11649        let _g = crate::test_util::global_state_lock();
11650        clear_hdocs();
11651        clear_hdocs();
11652    }
11653
11654    /// c:225 — `ecadjusthere(0, 0)` is a no-op (no delta).
11655    #[test]
11656    fn ecadjusthere_zero_delta_no_panic() {
11657        let _g = crate::test_util::global_state_lock();
11658        ecadjusthere(0, 0);
11659    }
11660
11661    /// c:225 — `ecadjusthere` is safe for arbitrary positions.
11662    #[test]
11663    fn ecadjusthere_arbitrary_pos_no_panic() {
11664        let _g = crate::test_util::global_state_lock();
11665        for p in [0usize, 1, 100, 9999] {
11666            ecadjusthere(p, 0);
11667            ecadjusthere(p, 1);
11668            ecadjusthere(p, -1);
11669        }
11670    }
11671
11672    // ═══════════════════════════════════════════════════════════════════
11673    // Additional C-parity tests for Src/parse.c FD_* accessors
11674    // c:3127 fdmagic / c:3131 fdflags / c:3133 fdother / c:3140 fdversion /
11675    // c:3145 fdhflags / c:3146 fdhtail / c:3147 fdhbldflags
11676    // ═══════════════════════════════════════════════════════════════════
11677
11678    fn build_fd_header() -> Vec<u32> {
11679        let mut buf = vec![0u32; FD_PRELEN + 32];
11680        buf[0] = FD_MAGIC; // pre[0] magic
11681        buf[1] = (0x12u32) | (0x00ABCDEFu32 << 8); // flags=0x12, other=0xABCDEF
11682                                                   // Embed version string starting at pre[2].
11683        let ver = b"5.9\0";
11684        for (i, chunk) in ver.chunks(4).enumerate() {
11685            let mut word = [0u8; 4];
11686            word[..chunk.len()].copy_from_slice(chunk);
11687            buf[2 + i] = u32::from_le_bytes(word);
11688        }
11689        buf[FD_PRELEN - 1] = (FD_PRELEN as u32) + 8; // header-len slot
11690        buf
11691    }
11692
11693    /// c:3127 — `fdmagic(f)` returns pre[0] verbatim.
11694    #[test]
11695    fn fdmagic_returns_pre_zero_word() {
11696        let buf = build_fd_header();
11697        assert_eq!(fdmagic(&buf), FD_MAGIC, "fdmagic = pre[0]");
11698    }
11699
11700    /// c:3131 — `fdflags` extracts low byte of pre[1].
11701    #[test]
11702    fn fdflags_low_byte_extraction() {
11703        let buf = build_fd_header();
11704        assert_eq!(fdflags(&buf), 0x12, "flags = pre[1] & 0xff");
11705    }
11706
11707    /// c:3133 — `fdother` extracts high 24 bits of pre[1].
11708    #[test]
11709    fn fdother_high_24_bits_extraction() {
11710        let buf = build_fd_header();
11711        assert_eq!(
11712            fdother(&buf),
11713            0x00ABCDEF,
11714            "other = pre[1] >> 8 & 0x00ffffff"
11715        );
11716    }
11717
11718    /// c:3132 — `fdsetflags` writes low byte, preserves high 24 bits.
11719    #[test]
11720    fn fdsetflags_preserves_high_24_bits() {
11721        let mut buf = build_fd_header();
11722        let other_before = fdother(&buf);
11723        fdsetflags(&mut buf, 0x42);
11724        assert_eq!(fdflags(&buf), 0x42, "new flags written");
11725        assert_eq!(fdother(&buf), other_before, "high 24 bits preserved");
11726    }
11727
11728    /// c:3134 — `fdsetother` writes high 24 bits, preserves low byte.
11729    #[test]
11730    fn fdsetother_preserves_low_byte() {
11731        let mut buf = build_fd_header();
11732        let flags_before = fdflags(&buf);
11733        fdsetother(&mut buf, 0x00DEADBE);
11734        assert_eq!(fdother(&buf), 0x00DEADBE, "new other written");
11735        assert_eq!(fdflags(&buf), flags_before, "low byte preserved");
11736    }
11737
11738    /// c:3134 — `fdsetother` clamps to 24 bits (caller-passed high bits dropped).
11739    #[test]
11740    fn fdsetother_clamps_to_24_bits() {
11741        let mut buf = build_fd_header();
11742        fdsetother(&mut buf, 0xFF_FFFF_FF);
11743        // Only the low 24 bits land in `other`.
11744        assert_eq!(fdother(&buf), 0x00FF_FFFF, "high bits dropped");
11745    }
11746
11747    /// c:3140 — `fdversion(buf)` returns String (compile-time type pin).
11748    #[test]
11749    fn fdversion_returns_string_type() {
11750        let buf = build_fd_header();
11751        let _: String = fdversion(&buf);
11752    }
11753
11754    /// c:3140 — `fdversion` reads the NUL-terminated string from pre[2..].
11755    #[test]
11756    fn fdversion_reads_until_nul() {
11757        let buf = build_fd_header();
11758        assert_eq!(fdversion(&buf), "5.9", "version read until NUL");
11759    }
11760
11761    /// c:3145 — `fdhflags(h)` returns low 2 bits of flags.
11762    #[test]
11763    fn fdhflags_low_two_bits() {
11764        let h = fdhead {
11765            start: 0,
11766            len: 0,
11767            npats: 0,
11768            strs: 0,
11769            hlen: 0,
11770            flags: 0b1011, // tail=2, kshload bits = 0b11
11771        };
11772        assert_eq!(fdhflags(&h), 0b11, "flags = h.flags & 0x3");
11773    }
11774
11775    /// c:3146 — `fdhtail(h)` returns high 30 bits (shifted right by 2).
11776    #[test]
11777    fn fdhtail_shift_right_two() {
11778        let h = fdhead {
11779            start: 0,
11780            len: 0,
11781            npats: 0,
11782            strs: 0,
11783            hlen: 0,
11784            flags: (0x12_3456 << 2) | 0x3,
11785        };
11786        assert_eq!(fdhtail(&h), 0x12_3456, "tail = h.flags >> 2");
11787    }
11788
11789    /// c:3147 — `fdhbldflags(flags, tail)` packs into single u32.
11790    #[test]
11791    fn fdhbldflags_packs_flags_low_tail_high() {
11792        let packed = fdhbldflags(0x3, 0x42);
11793        assert_eq!(packed & 0x3, 0x3, "low 2 bits = flags");
11794        assert_eq!(packed >> 2, 0x42, "high 30 bits = tail");
11795    }
11796
11797    /// c:3145-3147 — `fdhflags(h)`+`fdhtail(h)` round-trip via fdhbldflags.
11798    #[test]
11799    fn fdh_round_trip_via_bldflags() {
11800        for (flags, tail) in [(0u32, 0u32), (1, 100), (2, 0xABC), (3, 0xFFFF)] {
11801            let packed = fdhbldflags(flags, tail);
11802            let h = fdhead {
11803                start: 0,
11804                len: 0,
11805                npats: 0,
11806                strs: 0,
11807                hlen: 0,
11808                flags: packed,
11809            };
11810            assert_eq!(fdhflags(&h), flags, "flags round-trips");
11811            assert_eq!(fdhtail(&h), tail, "tail round-trips");
11812        }
11813    }
11814
11815    /// c:8271 — `firstfdhead_offset()` returns FD_PRELEN constant.
11816    #[test]
11817    fn firstfdhead_offset_returns_prelen() {
11818        assert_eq!(
11819            firstfdhead_offset(),
11820            FD_PRELEN,
11821            "first header starts after prelude"
11822        );
11823    }
11824
11825    /// c:3127 — `fdmagic` differentiates FD_MAGIC from FD_OMAGIC.
11826    #[test]
11827    fn fdmagic_differentiates_magic_omagic() {
11828        let mut buf = vec![FD_MAGIC; FD_PRELEN];
11829        assert_eq!(fdmagic(&buf), FD_MAGIC);
11830        buf[0] = FD_OMAGIC;
11831        assert_eq!(fdmagic(&buf), FD_OMAGIC, "swapped magic readable");
11832        assert_ne!(FD_MAGIC, FD_OMAGIC, "the two magics differ");
11833    }
11834}