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}