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 };
665
666 // c:577 — free ecbuf so next parse starts fresh.
667 ECBUF.with(|c| c.borrow_mut().clear());
668 ECLEN.with(|c| c.set(0));
669 ECUSED.with(|c| c.set(0));
670 ECNPATS.with(|c| c.set(0));
671 ECSOFFS.with(|c| c.set(0));
672 ECSTRS_INDEX.with(|c| c.borrow_mut().clear());
673 ECSTRS_REVERSE.with(|c| c.borrow_mut().clear());
674 ECSTRS_TREE.with(|t| *t.borrow_mut() = None);
675
676 ret
677}
678
679/// Port of `int empty_eprog(Eprog p)` from `Src/parse.c:584`. C
680/// body: `return (!p || !p->prog || *p->prog == WCB_END());` —
681/// the eprog is empty when its prog buffer is missing or the
682/// first wordcode is the WC_END marker. Used by signal handlers
683/// (`Src/signals.c:712`) to short-circuit a trap that resolves to
684/// an empty program.
685pub fn empty_eprog(p: &eprog) -> bool {
686 p.prog.is_empty() || p.prog[0] == WCB_END()
687}
688
689/// Clear pending here-document list. Direct port of
690/// `clear_hdocs(void)` from `Src/parse.c:591`. The C version walks
691/// `hdocs` and frees each node; Rust drops the `Box<heredocs>`
692/// chain automatically when the head is replaced with None.
693pub fn clear_hdocs() {
694 // c:591
695 // c:593-598 — for (p = hdocs; p; p = n) { n = p->next; zfree(p); }
696 // c:599 — hdocs = NULL;
697 HDOCS.with_borrow_mut(|h| *h = None);
698 // zshrs-only: also drop the parallel AST-glue Vec. No C
699 // analog — LEX_HEREDOCS is Rust-only working-set state.
700 LEX_HEREDOCS.with_borrow_mut(|v| v.clear());
701}
702
703/// Top-level parse-event entry. Direct port of zsh/Src/parse.c:
704/// 612-631 `parse_event`. Reads one event from the lexer (a
705/// sublist optionally followed by SEPER/AMPER/AMPERBANG) and
706/// returns the resulting ZshProgram.
707///
708/// `endtok` is the token that terminates the event — usually
709/// ENDINPUT, but for command-style substitutions the closing
710/// `)` (zsh's CMD_SUBST_CLOSE).
711///
712/// zshrs port note: zsh's parse_event returns an `Eprog` (heap-
713/// allocated wordcode program). zshrs returns a `ZshProgram`
714/// (AST root). Same role at the parse-output boundary.
715pub fn parse_event(endtok: lextok) -> Option<ZshProgram> {
716 // parse.c:616-619 — reset state and prime the lexer.
717 set_tok(ENDINPUT);
718 set_incmdpos(true);
719 // parse.c:618 — `aliasspaceflag = 0;`. Fresh event: discard any
720 // alias-space carry-over from a prior parse so HISTIGNORESPACE
721 // doesn't suppress the next entered command line.
722 crate::ported::lex::LEX_ALIAS_SPACE_FLAG.with(|c| c.set(0));
723
724 // parse.c:626-628 — a sub-parse for a substitution (endtok !=
725 // ENDINPUT, e.g. `par_subsh` with OUTPAR) doesn't need its own
726 // eprog; par_event drives it and the caller discards the program.
727 if endtok != ENDINPUT {
728 zshlex();
729 init_parse();
730 if !par_event(endtok) {
731 clear_hdocs();
732 return None;
733 }
734 return Some(ZshProgram { lists: Vec::new() });
735 }
736
737 // parse.c:616-630 — top-level event parse. The lexer pulls
738 // characters straight from SHIN via hgetc → ingetc → shingetchar
739 // (input.c) and accumulates each into its token buffer with add();
740 // no LEX_INPUT seeding. `zshlex()` (already primed by the shared
741 // entry below) reads the first token; parse_program_until drives the
742 // grammar over the event, refilling input one line at a time as
743 // ingetc's inputline() arm fires.
744 zshlex();
745 init_parse();
746 if tok() == ENDINPUT {
747 return None; // EOF — loop() terminates.
748 }
749 // single_event = TRUE: one logical command line per parse_event, the
750 // faithful port of C par_event's endtok==ENDINPUT top-level mode
751 // (parse.c:635). loop() reads+executes ONE event at a time so `-v`/`-x`
752 // echo and runtime `setopt verbose`/`xtrace` interleave with execution
753 // exactly as zsh does — whole-program parsing can't (it finishes
754 // parsing before any execution).
755 //
756 // Enabling this required isolating the runtime nested-parse machinery
757 // from the now-mid-stream outer reader (the parse/execute interleave
758 // exposed three split-global hazards that whole-program mode hid):
759 // * cmd-subst / proc-sub / eval / source bodies re-parse via
760 // vm_helper::parse_isolated (the AST-path bridge for C's
761 // parse_string, exec.c:283) — it sets `strin` so a drained nested
762 // buffer EOFs instead of STEALING the outer reader's next SHIN line.
763 // * strinbeg/strinend now bump BOTH `strin` copies (hist.rs + the
764 // input.rs one ingetc checks); lexinit zeroes BOTH `lexstop` copies
765 // — C has a single global for each, zshrs split them.
766 // * loop() saves/restores LEX_LINENO across execode (init.rs), the
767 // execlist `oldlineno` discipline (exec.c:28/292), so per-statement
768 // SET_LINENO doesn't freeze `$LINENO` for the next event.
769 let mut program = parse_program_until(None, true);
770 if program.lists.is_empty() {
771 clear_hdocs();
772 return None;
773 }
774 // parse.c:630 bld_eprog post-pass — wire heredoc bodies collected
775 // by zshlex into the ZshRedir nodes (mirror of `parse()`).
776 let bodies: Vec<HereDocInfo> = LEX_HEREDOCS
777 .with_borrow(|v| v.clone())
778 .into_iter()
779 .map(|h| HereDocInfo {
780 content: h.content,
781 terminator: h.terminator,
782 quoted: h.quoted,
783 })
784 .collect();
785 if !bodies.is_empty() {
786 fill_heredoc_bodies(&mut program, &bodies);
787 }
788 Some(program)
789}
790
791/// Parse one event (sublist with optional separator). Direct
792/// port of zsh/Src/parse.c:635 `par_event`. Returns true if
793/// an event was successfully parsed, false on EOF / endtok.
794///
795/// zshrs port note: the C version emits wordcodes via ecadd/
796/// set_list_code; zshrs's parser builds AST nodes via
797/// par_sublist + par_list. Same flow, different output.
798pub fn par_event(endtok: lextok) -> bool {
799 // parse.c:639-643 — skip leading SEPERs.
800 while tok() == SEPER {
801 // parse.c:640-641 — at top-level (endtok == ENDINPUT),
802 // a SEPER on a fresh line ends the event.
803 if isnewlin() > 0 && endtok == ENDINPUT {
804 return false;
805 }
806 zshlex();
807 }
808 // parse.c:644-647 — terminate on EOF or matching close-token.
809 if tok() == ENDINPUT {
810 return false;
811 }
812 if tok() == endtok {
813 return true;
814 }
815 // parse.c:649-... — drive par_sublist + handle terminator.
816 // zshrs's par_sublist already builds the AST node directly.
817 match par_sublist() {
818 Some(_) => {
819 // parse.c:651-693 — terminator handling. zshrs's
820 // par_list wraps this; for parse_event we just
821 // confirm the sublist parsed.
822 true
823 }
824 None => false,
825 }
826}
827
828/// Port of `parse_list(void)` from `Src/parse.c:697`. C-shape entry
829/// point: drives `par_list` and finalizes via `bld_eprog`. Returns
830/// `None` on syntax error.
831pub fn parse_list() -> Option<eprog> {
832 // c:697
833 set_tok(ENDINPUT);
834 init_parse();
835 zshlex();
836 // c:Src/parse.c:705 — `par_list(&c);` emits wordcode for the
837 // full multi-statement list (its goto-rec loop walks all
838 // SEPER-separated sublists). The Rust AST par_list() emits
839 // NOTHING to the wordcode buffer (only builds the AST), so
840 // bld_eprog returned an empty program AND tok stayed at
841 // SEPER, tripping the syntax-error check below for any
842 // \`cmd; cmd\` body.
843 //
844 // Route through par_event_wordcode (the wordcode emitter,
845 // lines 4395+) which mirrors C's par_list loop semantics
846 // and populates the wordcode buffer that bld_eprog reads.
847 let _start = par_event_wordcode();
848 if tok() != ENDINPUT {
849 clear_hdocs();
850 set_tok(LEXERR);
851 // c:Src/parse.c:708 — `yyerror(0);`. C-faithful invocation;
852 // the message format ("parse error near `X'") is built
853 // inside yyerror from zshlextext/tokstr().
854 yyerror(0);
855 return None;
856 }
857 Some(bld_eprog(false))
858}
859
860/// Port of `parse_cond(void)` from `Src/parse.c:722`. Only used by
861/// `bin_test`/`bin_bracket` for `/bin/test`/`[` compat — the
862/// `condlex` global must already point at `testlex` before entry.
863pub fn parse_cond() -> Option<eprog> {
864 // c:722
865 init_parse();
866 if par_cond().is_none() {
867 clear_hdocs();
868 return None;
869 }
870 Some(bld_eprog(true))
871}
872
873// ============================================================
874// Wordcode emission helpers (parse.c private helpers)
875//
876// Direct ports of zsh's wordcode-emission helpers in parse.c.
877// These write u32 opcodes into a flat `ecbuf` array thread-local
878// via ecadd / ecdel / ecispace / ecstrcode and friends. The
879// par_*_wordcode family at parse.rs:1700-3500 walks the lex
880// stream and emits a real wordcode buffer here.
881//
882// (The AST tree built by par_program / par_simple / etc. is a
883// separate path used by fusevm; see compile_zsh.rs for the AST
884// → fusevm-bytecode compiler.)
885// ============================================================
886
887/// Patch a list-placeholder wordcode with its actual opcode +
888/// jump distance. Direct port of zsh/Src/parse.c:738
889/// `set_list_code`. zsh emits an `ecadd(0)` placeholder before
890/// par_sublist runs, then comes back through set_list_code to
891/// rewrite the slot with WCB_LIST(type, distance) once the
892/// sublist's final length is known.
893///
894/// Port of `set_list_code(int p, int type, int cmplx)` from
895/// `Src/parse.c:738`. Patches the WCB_LIST header at `p` based on
896/// whether the sublist body is simple (single command, no
897/// pipeline) and Z_SYNC/Z_END — emits the Z_SIMPLE-optimized
898/// header when possible, otherwise the plain WCB_LIST(type, 0).
899pub fn set_list_code(p: usize, type_code: i32, cmplx: bool) {
900 let _ = wc_bdata;
901 // c:740 — `if (!cmplx && (type == Z_SYNC || type == (Z_SYNC | Z_END))
902 // && WC_SUBLIST_TYPE(ecbuf[p+1]) == WC_SUBLIST_END)`
903 let sublist_code = ECBUF.with_borrow(|b| b.get(p + 1).copied().unwrap_or(0));
904 let z = type_code;
905 let qualifies = !cmplx
906 && (z == Z_SYNC || z == (Z_SYNC | Z_END))
907 && WC_SUBLIST_TYPE(sublist_code) == WC_SUBLIST_END;
908 if qualifies {
909 // c:742 — `int ispipe = !(WC_SUBLIST_FLAGS(ecbuf[p+1])
910 // & WC_SUBLIST_SIMPLE);`
911 let ispipe = (WC_SUBLIST_FLAGS(sublist_code) & WC_SUBLIST_SIMPLE) == 0;
912 // c:743 — `ecbuf[p] = WCB_LIST((type|Z_SIMPLE), ecused-2-p);`
913 let used = ECUSED.get() as usize;
914 let off = used.saturating_sub(2 + p);
915 ECBUF.with_borrow_mut(|b| {
916 if p < b.len() {
917 b[p] = WCB_LIST((z | Z_SIMPLE) as wordcode, off as wordcode);
918 }
919 });
920 // c:744 — `ecdel(p+1);`
921 ecdel(p + 1);
922 // c:745-746 — `if (ispipe) ecbuf[p+1] = WC_PIPE_LINENO(ecbuf[p+1]);`
923 if ispipe {
924 ECBUF.with_borrow_mut(|b| {
925 if p + 1 < b.len() {
926 b[p + 1] = WC_PIPE_LINENO(b[p + 1]);
927 }
928 });
929 }
930 } else {
931 // c:748 — `ecbuf[p] = WCB_LIST(type, 0);`
932 ECBUF.with_borrow_mut(|b| {
933 if p < b.len() {
934 b[p] = WCB_LIST(z as wordcode, 0);
935 }
936 });
937 }
938}
939
940/// Port of `set_sublist_code(int p, int type, int flags, int skip, int cmplx)`
941/// from `Src/parse.c:755`. Patches the WCB_SUBLIST header at `p`.
942/// When the sublist is non-complex (single command, no pipeline),
943/// sets WC_SUBLIST_SIMPLE and rewrites the following slot to
944/// `WC_PIPE_LINENO`.
945pub fn set_sublist_code(p: usize, type_code: i32, flags: i32, skip: i32, cmplx: bool) {
946 if cmplx {
947 // c:758 — `ecbuf[p] = WCB_SUBLIST(type, flags, skip);`
948 ECBUF.with_borrow_mut(|b| {
949 if p < b.len() {
950 b[p] = WCB_SUBLIST(type_code as wordcode, flags as wordcode, skip as wordcode);
951 }
952 });
953 } else {
954 // c:760 — `ecbuf[p] = WCB_SUBLIST(type, flags|WC_SUBLIST_SIMPLE, skip);`
955 ECBUF.with_borrow_mut(|b| {
956 if p < b.len() {
957 b[p] = WCB_SUBLIST(
958 type_code as wordcode,
959 (flags as wordcode) | WC_SUBLIST_SIMPLE,
960 skip as wordcode,
961 );
962 }
963 });
964 // c:761 — `ecbuf[p+1] = WC_PIPE_LINENO(ecbuf[p+1]);`
965 ECBUF.with_borrow_mut(|b| {
966 if p + 1 < b.len() {
967 b[p + 1] = WC_PIPE_LINENO(b[p + 1]);
968 }
969 });
970 }
971}
972
973/// Parse a list (sublist with optional & or ;).
974///
975/// Direct port of zsh/Src/parse.c:771-804 `par_list` (and the
976/// par_list1 wrapper at parse.c:807-817).
977///
978/// **Structural divergence**: zsh's parse.c emits flat wordcode
979/// into the `ecbuf` u32 array via `ecadd(0)` (placeholder),
980/// `set_list_code(p, code, complexity)`, `wc_bdata(Z_END)`. zshrs
981/// builds an AST node `ZshList { sublist, flags }` instead. The
982/// async/sync/disown discrimination at parse.c:785-790 maps to
983/// zshrs's `ListFlags { async_, disown }` field — Z_SYNC is the
984/// default (no flags), Z_ASYNC = `&` = `async_=true`, Z_DISOWN +
985/// Z_ASYNC = `&!`/`&|` = both true. Same semantics, different
986/// representation. This divergence is repository-wide: every
987/// `par_*` function emits wordcode in C, every `parse_*` builds
988/// AST in Rust. The compile_zsh module then traverses the AST to
989/// emit fusevm bytecode, which serves the same role as zsh's
990/// wordcode but with a different opcode set and execution model.
991fn par_list(single_event: bool) -> Option<(ZshList, bool)> {
992 let sublist = par_sublist()?;
993
994 // c:769-803 — `list : { SEPER } [ sublist [ { SEPER | AMPER |
995 // AMPERBANG } list ] ]`. The second tuple element reports
996 // whether the list ended with an EXPLICIT terminator. C's list
997 // grammar only chains sublists across SEPER/AMPER/AMPERBANG; a
998 // sublist followed directly by another command (`{a} {b}`) ends
999 // the list and the dangling token is the CALLER's problem
1000 // (par_while takes a dangling INBRACE as the loop body, par_event
1001 // yyerrors at c:671-680). parse_program_until needs this bit to
1002 // reproduce that split.
1003 let (flags, terminated) = match tok() {
1004 AMPER => {
1005 zshlex();
1006 (
1007 ListFlags {
1008 async_: true,
1009 disown: false,
1010 },
1011 true,
1012 )
1013 }
1014 AMPERBANG => {
1015 zshlex();
1016 (
1017 ListFlags {
1018 async_: true,
1019 disown: true,
1020 },
1021 true,
1022 )
1023 }
1024 SEPER => {
1025 // c:Src/parse.c par_event SEPER branch — `if (isnewlin <= 0
1026 // || endtok != ENDINPUT) zshlex();`. The lexer folds both
1027 // `;` and `\n` into SEPER (lex.rs:265), distinguishing them
1028 // via LEX_ISNEWLIN (C `isnewlin`). At a TOP-LEVEL event
1029 // boundary (single_event ≈ endtok == ENDINPUT) a SEPER that
1030 // was a NEWLINE terminates the event and is NOT consumed —
1031 // it's left for the next parse_event, whose leading skip
1032 // reads the following line only THEN. That's what lets
1033 // loop() read+execute one event at a time so `-v`/`-x` echo
1034 // and runtime `setopt verbose` interleave with execution
1035 // like zsh. A `;` SEPER (isnewlin <= 0), or any SEPER inside
1036 // a body / whole-program parse (single_event = false), is
1037 // consumed to chain the next sublist.
1038 let is_newline = crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) > 0;
1039 if !(single_event && is_newline) {
1040 zshlex();
1041 }
1042 (ListFlags::default(), true)
1043 }
1044 SEMI | NEWLIN => {
1045 // Unfolded `;` / preserved newline (LEXFLAGS_NEWLINE) — not a
1046 // top-level event terminator; consume and continue.
1047 zshlex();
1048 (ListFlags::default(), true)
1049 }
1050 _ => (ListFlags::default(), false),
1051 };
1052
1053 Some((ZshList { sublist, flags }, terminated))
1054}
1055
1056/// Parse one list — non-recursing variant. Direct port of
1057/// zsh/Src/parse.c:808 `par_list1`. Like par_list but
1058/// doesn't recurse on the trailing-separator path; used by
1059/// callers that only want one statement (e.g. each arm of a
1060/// case body).
1061pub fn par_list1() -> Option<ZshSublist> {
1062 // parse.c:810-816 — body is a single par_sublist call wrapped
1063 // in the eu/ecused tracking that zshrs doesn't need (no
1064 // wordcode buffer).
1065 par_sublist()
1066}
1067
1068/// Parse a sublist (pipelines connected by && or ||).
1069///
1070/// Direct port of zsh/Src/parse.c:825 `par_sublist` and
1071/// par_sublist2 at parse.c:869-892. par_sublist handles the
1072/// && / || conjunction and emits WC_SUBLIST opcodes; par_sublist2
1073/// handles the leading `!` negation and `coproc` keyword.
1074///
1075/// AST mapping: ZshSublist { pipe, conj_chain }, where `conj_chain`
1076/// is a Vec<(ConjOp, ZshSublist)> for chained && / ||. C uses
1077/// flat wordcode with WC_SUBLIST_AND / WC_SUBLIST_OR markers.
1078fn par_sublist() -> Option<ZshSublist> {
1079 let mut flags = SublistFlags::default();
1080
1081 // Handle coproc and !
1082 if tok() == COPROC {
1083 flags.coproc = true;
1084 zshlex();
1085 } else if tok() == BANG_TOK {
1086 flags.not = true;
1087 zshlex();
1088 }
1089
1090 let pipe = par_pline()?;
1091
1092 // Check for && or ||
1093 let next = match tok() {
1094 DAMPER => {
1095 zshlex();
1096 skip_separators();
1097 // c:Src/parse.c:par_sublist — and-or operators (`&&`,
1098 // `||`) require a sublist on each side. After consuming
1099 // `&&`/`||`, another and-or operator OR a pipe-operator
1100 // immediately after is a parse error in C zsh. zshrs's
1101 // recursion silently returned None and dropped the
1102 // operator. Bug #171 in docs/BUGS.md.
1103 if matches!(tok(), DAMPER | DBAR | BAR_TOK | BARAMP) {
1104 let name = match tok() {
1105 DAMPER => "&&",
1106 DBAR => "||",
1107 BAR_TOK => "|",
1108 BARAMP => "|&",
1109 _ => "operator",
1110 };
1111 zerr(&format!("parse error near `{}'", name));
1112 return None;
1113 }
1114 par_sublist().map(|s| (SublistOp::And, Box::new(s)))
1115 }
1116 DBAR => {
1117 zshlex();
1118 skip_separators();
1119 if matches!(tok(), DAMPER | DBAR | BAR_TOK | BARAMP) {
1120 let name = match tok() {
1121 DAMPER => "&&",
1122 DBAR => "||",
1123 BAR_TOK => "|",
1124 BARAMP => "|&",
1125 _ => "operator",
1126 };
1127 zerr(&format!("parse error near `{}'", name));
1128 return None;
1129 }
1130 par_sublist().map(|s| (SublistOp::Or, Box::new(s)))
1131 }
1132 _ => None,
1133 };
1134
1135 Some(ZshSublist { pipe, next, flags })
1136}
1137
1138/// Port of `par_sublist2(int *cmplx)` from `Src/parse.c:869`.
1139/// Secondary-sublist arm: handles the `COPROC`/`Bang` prefix
1140/// in front of a pline. Returns the WC_SUBLIST flag word added.
1141pub fn par_sublist2(cmplx: &mut i32) -> Option<i32> {
1142 // c:870 — `int f = 0;`
1143 let mut f: i32 = 0;
1144 // c:873-880 — COPROC / BANG prefix flags.
1145 if tok() == COPROC {
1146 *cmplx = 1;
1147 f |= WC_SUBLIST_COPROC as i32;
1148 zshlex();
1149 } else if tok() == BANG_TOK {
1150 *cmplx = 1;
1151 f |= WC_SUBLIST_NOT as i32;
1152 zshlex();
1153 }
1154 // c:882-883 — `if (!par_pline(cmplx) && !f) return -1;`
1155 if !par_pipe_wordcode(cmplx) && f == 0 {
1156 return None;
1157 }
1158 // c:885 — `return f;`
1159 Some(f)
1160}
1161
1162/// Parse a pipeline
1163/// Parse a pipeline (cmds joined by `|` / `|&`). Direct port of
1164/// zsh/Src/parse.c:894 `par_pline`. AST: ZshPipe { cmds: Vec<ZshCommand> }.
1165/// C emits WC_PIPE wordcodes per command; same flow.
1166fn par_pline() -> Option<ZshPipe> {
1167 let lineno = toklineno();
1168 let cmd = par_cmd()?;
1169
1170 // Check for | or |&
1171 let mut merge_stderr = false;
1172 let next = match tok() {
1173 BAR_TOK | BARAMP => {
1174 merge_stderr = tok() == BARAMP;
1175 zshlex();
1176 skip_separators();
1177 // c:Src/parse.c:par_pline — pipe-operators require a
1178 // command on each side. After consuming `|`/`|&`,
1179 // C zsh's recursive par_pline call returns -1 (parse
1180 // error) when the next token is another pipe-operator
1181 // — `a | | b` errors with `parse error near `|''`.
1182 // zshrs's `par_pline()?` silently returned None on
1183 // missing command, dropping the rest of the input
1184 // without diagnosing the empty-pipe-operand. Bug #171
1185 // in docs/BUGS.md.
1186 if matches!(tok(), BAR_TOK | BARAMP) {
1187 let name = if tok() == BARAMP { "|&" } else { "|" };
1188 zerr(&format!("parse error near `{}'", name));
1189 return None;
1190 }
1191 par_pline().map(Box::new)
1192 }
1193 _ => None,
1194 };
1195
1196 Some(ZshPipe {
1197 cmd,
1198 next,
1199 lineno,
1200 merge_stderr,
1201 })
1202}
1203
1204/// Parse a command
1205/// Parse a command — dispatches by leading token (FOR / CASE /
1206/// IF / WHILE / UNTIL / REPEAT / FUNC / DINBRACK / DINPAR /
1207/// Inpar subshell / Inbrace current-shell / TIME / NOCORRECT,
1208/// else simple). Direct port of zsh/Src/parse.c:958 `par_cmd`.
1209fn par_cmd() -> Option<ZshCommand> {
1210 // Parse leading redirections
1211 let mut redirs = Vec::new();
1212 while IS_REDIROP(tok()) {
1213 if let Some(redir) = par_redir() {
1214 redirs.push(redir);
1215 }
1216 }
1217
1218 // c:Src/parse.c:970-1030 par_cmd — push the open construct onto the
1219 // cmdstack for the duration of its sub-parse, then pop. The cmdstack
1220 // drives `%_` prompt expansion, so this is what makes a multi-line
1221 // construct's PS2 render `for> ` / `while> ` / `case> ` across
1222 // continuation lines instead of a bare `> `. (The cmdstack does NOT
1223 // drive tokenization in zshrs — the lexer's only reads are the
1224 // debug-only DPUTS "cmdstack not empty" asserts — so wrapping the AST
1225 // dispatch is behaviorally inert outside the prompt.) IF is excluded
1226 // to match C, where par_if manages its own cmdstack (CS_IF/CS_IFTHEN);
1227 // DINPAR/TIME/INOUTPAR are not cmdstack constructs in C either.
1228 let cmd = match tok() {
1229 FOR => {
1230 cmdpush(CS_FOR as u8); // c:972
1231 let c = par_for();
1232 cmdpop(); // c:974
1233 c
1234 }
1235 FOREACH => {
1236 cmdpush(CS_FOREACH as u8); // c:977
1237 let c = par_for();
1238 cmdpop(); // c:979
1239 c
1240 }
1241 SELECT => {
1242 cmdpush(CS_SELECT as u8); // c:983
1243 let c = parse_select();
1244 cmdpop(); // c:985
1245 c
1246 }
1247 CASE => {
1248 cmdpush(CS_CASE as u8); // c:988
1249 let c = par_case();
1250 cmdpop(); // c:990
1251 c
1252 }
1253 IF => par_if(), // c:992-994 — par_if manages its own cmdstack
1254 WHILE => {
1255 cmdpush(CS_WHILE as u8); // c:996
1256 let c = par_while(false);
1257 cmdpop(); // c:998
1258 c
1259 }
1260 UNTIL => {
1261 cmdpush(CS_UNTIL as u8); // c:1001
1262 let c = par_while(true);
1263 cmdpop(); // c:1003
1264 c
1265 }
1266 REPEAT => {
1267 cmdpush(CS_REPEAT as u8); // c:1006
1268 let c = par_repeat();
1269 cmdpop(); // c:1008
1270 c
1271 }
1272 INPAR_TOK => {
1273 cmdpush(CS_SUBSH as u8); // c:1012
1274 let c = par_subsh();
1275 cmdpop(); // c:1014
1276 c
1277 }
1278 INOUTPAR => parse_anon_funcdef(),
1279 INBRACE_TOK => {
1280 cmdpush(CS_CURSH as u8); // c:1017
1281 let c = parse_cursh();
1282 cmdpop(); // c:1019
1283 c
1284 }
1285 FUNC => {
1286 cmdpush(CS_FUNCDEF as u8); // c:1022
1287 let c = par_funcdef();
1288 cmdpop(); // c:1024
1289 c
1290 }
1291 DINBRACK => {
1292 cmdpush(CS_COND as u8); // c:1027
1293 let c = par_cond();
1294 cmdpop(); // c:1029
1295 c
1296 }
1297 DINPAR => parse_arith(),
1298 TIME => par_time(),
1299 _ => par_simple(redirs),
1300 };
1301
1302 // Parse trailing redirections. For Simple commands the redirs were
1303 // already captured inside par_simple; for compound forms (Cursh,
1304 // Subsh, If, While, etc.) we collect them here and wrap in
1305 // ZshCommand::Redirected so compile_zsh can scope-bracket them.
1306 if let Some(inner) = cmd {
1307 let mut trailing: Vec<ZshRedir> = Vec::new();
1308 while IS_REDIROP(tok()) {
1309 if let Some(redir) = par_redir() {
1310 trailing.push(redir);
1311 }
1312 }
1313 // c:Src/parse.c:par_cmd — compound forms (Cursh `{...}`, Subsh
1314 // `(...)`, If/While/Until/For/Case/Select/Repeat/Funcdef) must
1315 // be followed by a valid sublist/list separator (`;`, `\n`,
1316 // `&`, `|`, `&&`, `||`, redirect-op) — STRING_LEX after a
1317 // compound is a parse error. zshrs's outer par_list loop
1318 // silently treated trailing words as a new command, masking
1319 // syntax errors like `{ echo a; } b c`. Mirror C's strict
1320 // post-compound terminator check. Bug #146 in docs/BUGS.md.
1321 if !matches!(inner, ZshCommand::Simple(_)) && tok() == STRING_LEX {
1322 let bad = tokstr().unwrap_or_default();
1323 zerr(&format!("parse error near `{}'", bad));
1324 // Reset state before returning so the outer loop's None
1325 // detection unwinds cleanly.
1326 set_incmdpos(true);
1327 set_incasepat(0);
1328 set_incond(0);
1329 set_intypeset(false);
1330 return None;
1331 }
1332 // c:1072-1075 — every par_cmd tail resets the lexer state
1333 // toggles so the NEXT command starts in cmd position with
1334 // case/cond/typeset off. par_simple/par_cond set `incmdpos=0`
1335 // during their bodies; without this reset the next iteration
1336 // of the outer par_list loop sees `if` / `done` / `select`
1337 // etc. as plain strings and the AST collapses.
1338 set_incmdpos(true);
1339 set_incasepat(0);
1340 set_incond(0);
1341 set_intypeset(false);
1342 if trailing.is_empty() {
1343 return Some(inner);
1344 }
1345 // Simple already absorbed its own redirs (compile path expects
1346 // them on ZshSimple), so don't double-wrap.
1347 if matches!(inner, ZshCommand::Simple(_)) {
1348 if let ZshCommand::Simple(mut s) = inner {
1349 s.redirs.extend(trailing);
1350 return Some(ZshCommand::Simple(s));
1351 }
1352 unreachable!()
1353 }
1354 return Some(ZshCommand::Redirected(Box::new(inner), trailing));
1355 }
1356 // Same reset on the empty-cmd branch (mirror c:1072 unconditional
1357 // path — the C function only returns 0 above when the dispatch
1358 // produced no command, and falls through to the reset block).
1359 set_incmdpos(true);
1360 set_incasepat(0);
1361 set_incond(0);
1362 set_intypeset(false);
1363
1364 None
1365}
1366
1367/// Parse for/foreach loop
1368/// Parse `for NAME in WORDS; do BODY; done` (foreach style) AND
1369/// `for ((init; cond; incr)) do BODY done` (c-style). Direct port
1370/// of zsh/Src/parse.c:1087 `par_for`. parse_for_cstyle is the
1371/// inner branch for the `((...))` arithmetic-header variant
1372/// (parse.c:1100-1140 inside par_for).
1373fn par_for() -> Option<ZshCommand> {
1374 let is_foreach = tok() == FOREACH;
1375 // c:1094-1095 (Src/parse.c, par_for) — set `infor=2` (only when
1376 // tok==FOR) so the lexer's `(` peek at lex.c:784-789
1377 // (`if (infor) { ... return DINPAR; }`) routes the arith-for
1378 // body through dbparens semicolon-splitting instead of the
1379 // `cmd_or_math` whole-body capture path. Without this, `for ((
1380 // i=0; i<3; i++ ))` lexed as a single `((arith))` expression
1381 // and parse_for_cstyle's second zshlex got an empty/wrong tok.
1382 //
1383 // The companion C statement `incmdpos = 0;` at c:1094 isn't
1384 // mirrored here: zshrs's parser doesn't otherwise touch
1385 // LEX_INCMDPOS at this boundary, and forcing it false breaks
1386 // the SELECT case where downstream tokenization relied on the
1387 // inherited state. The C parser maintains incmdpos inline at
1388 // every grammar transition (parse.c:617, :791, :1072, :1145,
1389 // :1154, :1161, ...); without porting those companion sites a
1390 // single explicit reset here is more harmful than helpful.
1391 set_infor(if tok() == FOR { 2 } else { 0 }); // c:1095
1392 zshlex(); // c:1096
1393
1394 // Check for C-style: for (( init; cond; step ))
1395 if tok() == DINPAR {
1396 // c:1110-1111 — close out infor / cmdpos after parse_for_cstyle
1397 // has consumed the init/cond/step triple. Done inside the
1398 // helper itself so we honour the C ordering.
1399 return parse_for_cstyle();
1400 }
1401
1402 // c:1116 — `infor = 0;` immediately on entering the foreach
1403 // branch. Without this, `infor` stays at 2 (set at c:1095 when
1404 // tok==FOR) for the rest of par_for, and the lexer's `((`
1405 // peek at lex.c:786 routes every subsequent `((...))` inside
1406 // the loop body through dbparens — so `for x in a; do (( 1
1407 // )); done` and `if (( 1 )) { … }` inside the do-body both
1408 // mis-lexed as a c-style for header.
1409 set_infor(0); // c:1116
1410
1411 // Get variable name(s). zsh parse.c par_for accepts multiple
1412 // identifier tokens before `in`/`(`/newline — `for k v in ...`
1413 // assigns each iteration's pair of values to k and v in turn.
1414 // We store the names space-joined since variable identifiers
1415 // can't contain whitespace.
1416 let mut names: Vec<String> = Vec::new();
1417 while tok() == STRING_LEX {
1418 let v = tokstr().unwrap_or_default();
1419 if v == "in" {
1420 break;
1421 }
1422 names.push(v);
1423 zshlex();
1424 }
1425 if names.is_empty() {
1426 zerr("expected variable name in for");
1427 return None;
1428 }
1429 let var = names.join(" ");
1430
1431 // Skip newlines
1432 skip_separators();
1433
1434 // Get list. The lexer-port quirk: `for x (a b c)` arrives as a
1435 // single String token with the parens lexed-as-content
1436 // (`<Inpar>a b c<Outpar>`) instead of as separate Inpar/String/
1437 // Outpar tokens. Detect that shape and split it manually.
1438 let list = if tok() == STRING_LEX
1439 && tokstr()
1440 .map(|s| s.starts_with('\u{88}') && s.ends_with('\u{8a}'))
1441 .unwrap_or(false)
1442 {
1443 let raw = tokstr().unwrap_or_default();
1444 // Strip leading Inpar + trailing Outpar. KEEP the inner
1445 // content tokenized — `for x ({1..3}) …` has `{1..3}` as
1446 // Inbrace+content+Outbrace markers, which compile_word_str
1447 // needs to detect and brace-expand. Untokenizing here would
1448 // collapse the markers to plain `{` `}` chars and the brace-
1449 // expansion pass (which strictly requires Inbrace TOKEN per
1450 // Src/glob.c:hasbraces) would skip the word entirely.
1451 // Split only on UNTOKENIZED whitespace at the top level —
1452 // tokenized characters (TOKEN range \u{84}..\u{a1}) are part
1453 // of one word; bare ASCII spaces / tabs separate words.
1454 let inner = &raw[raw.char_indices().nth(1).map(|(i, _)| i).unwrap_or(0)
1455 ..raw
1456 .char_indices()
1457 .last()
1458 .map(|(i, _)| i)
1459 .unwrap_or(raw.len())];
1460 let mut words: Vec<String> = Vec::new();
1461 let mut cur = String::new();
1462 for c in inner.chars() {
1463 if c == ' ' || c == '\t' || c == '\n' {
1464 if !cur.is_empty() {
1465 words.push(std::mem::take(&mut cur));
1466 }
1467 } else {
1468 cur.push(c);
1469 }
1470 }
1471 if !cur.is_empty() {
1472 words.push(cur);
1473 }
1474 zshlex();
1475 ForList::Words(words)
1476 } else if tok() == STRING_LEX {
1477 let s = tokstr();
1478 if s.map(|s| s == "in").unwrap_or(false) {
1479 // c:Src/parse.c:1147-1154 — after consuming `in`, the
1480 // for-list reads in WORD position, not command position.
1481 // Reset incmdpos=false so the lexer's LX2_INBRACE arm
1482 // (lex.rs:1791) treats a leading `{` as the brace-
1483 // expansion marker (`bct++; add(Inbrace)`) instead of
1484 // returning STRING("{") + promoting to INBRACE_TOK.
1485 // Without this, `for i in {1..3}` saw `{` as the body-
1486 // opener brace, so the word-collection loop got an
1487 // empty word list and the loop body silently ran 0
1488 // iterations.
1489 set_incmdpos(false);
1490 zshlex();
1491 let mut words = Vec::new();
1492 while tok() == STRING_LEX {
1493 let _ts_s = tokstr();
1494 if let Some(s) = _ts_s.as_deref() {
1495 words.push(s.to_string());
1496 }
1497 zshlex();
1498 }
1499 // c:Src/parse.c:1162 — `incmdpos = 1;` after the
1500 // wordlist + SEPER are consumed, so the next token
1501 // (`do` / `{` body opener) lexes at command position.
1502 set_incmdpos(true);
1503 ForList::Words(words)
1504 } else {
1505 ForList::Positional
1506 }
1507 } else if tok() == INPAR_TOK {
1508 // for var (...) — `for x ({1..3})`: inside the parens, the
1509 // list is in WORD position so `{` must lex as the brace-
1510 // expansion Inbrace marker, NOT as a body-opener INBRACE_TOK.
1511 // Without resetting incmdpos before the next zshlex, the
1512 // lexer's LX2_INBRACE arm promotes `{` to INBRACE_TOK and
1513 // the word-collection loop exits empty, giving
1514 // `for x ({1..3})` an empty iteration.
1515 set_incmdpos(false);
1516 zshlex();
1517 let mut words = Vec::new();
1518 while tok() == STRING_LEX || tok() == SEPER {
1519 if tok() == STRING_LEX {
1520 let _ts_s = tokstr();
1521 if let Some(s) = _ts_s.as_deref() {
1522 words.push(s.to_string());
1523 }
1524 }
1525 zshlex();
1526 }
1527 if tok() == OUTPAR_TOK {
1528 // After the `)` of a for-list, the next token is the
1529 // body opener — `do`/`{`. zsh's lexer needs incmdpos
1530 // set so `{` lexes as Inbrace (not as a literal). C
1531 // analogue: parse.c::par_for sets `incmdpos = 1`
1532 // after consuming the Outpar before the body parse.
1533 set_incmdpos(true);
1534 zshlex();
1535 }
1536 ForList::Words(words)
1537 } else {
1538 ForList::Positional
1539 };
1540
1541 // Skip to body
1542 skip_separators();
1543
1544 // Parse body
1545 let body = parse_loop_body(is_foreach, false)?;
1546
1547 Some(ZshCommand::For(ZshFor {
1548 var,
1549 list,
1550 body: Box::new(body),
1551 is_select: false,
1552 }))
1553}
1554
1555/// Parse case statement
1556/// Parse `case WORD in PATTERN) BODY ;; ... esac`. Direct port
1557/// of zsh/Src/parse.c:1209 `par_case`. Each case arm is a
1558/// (pattern_list, body, terminator) tuple where terminator is
1559/// `;;` (default), `;&` (fallthrough), or `;|` (continue testing).
1560fn par_case() -> Option<ZshCommand> {
1561 // C par_case (parse.c:1209-1241). Order of state toggles
1562 // matters — the lexer reads the case word in `incmdpos=0`
1563 // (so it's not promoted to a reswd), then the `in`/`{` in
1564 // `incmdpos=1, noaliases=1, nocorrect=1` (so the `in` literal
1565 // isn't alias-expanded or spell-corrected), then sets
1566 // `incasepat=1, incmdpos=0` before the first pattern.
1567 set_incmdpos(false);
1568 zshlex(); // skip 'case'
1569
1570 let word = match tok() {
1571 STRING_LEX => {
1572 let w = tokstr().unwrap_or_default();
1573 // c:1222 — `incmdpos = 1;` before the next zshlex so the
1574 // `in` keyword is recognised. c:1223-1225 — save+force
1575 // noaliases / nocorrect.
1576 set_incmdpos(true);
1577 let ona = noaliases();
1578 let onc = nocorrect();
1579 set_noaliases(true);
1580 set_nocorrect(1);
1581 zshlex();
1582 // Restore noaliases/nocorrect after the `in`-or-`{` token
1583 // is in hand; both are unconditionally restored at c:1238-1239.
1584 let restore = |ona: bool, onc: i32| {
1585 set_noaliases(ona);
1586 set_nocorrect(onc);
1587 };
1588 (w, ona, onc, restore)
1589 }
1590 _ => {
1591 zerr("expected word after case");
1592 return None;
1593 }
1594 };
1595 let (word, ona, onc, restore) = word;
1596
1597 skip_separators();
1598
1599 // Expect 'in' or {
1600 let use_brace = tok() == INBRACE_TOK;
1601 if tok() == STRING_LEX {
1602 let s = tokstr();
1603 if s.map(|s| s != "in").unwrap_or(true) {
1604 // c:1228-1232 — restore noaliases/nocorrect on error path.
1605 restore(ona, onc);
1606 zerr("expected 'in' in case");
1607 return None;
1608 }
1609 } else if !use_brace {
1610 restore(ona, onc);
1611 zerr("expected 'in' or '{' in case");
1612 return None;
1613 }
1614 // c:1236-1239 — `incasepat = 1; incmdpos = 0; noaliases = ona;
1615 // nocorrect = onc;` — set the case-pattern context AND restore
1616 // alias/correct state BEFORE the zshlex that consumes `in`/`{`.
1617 set_incasepat(1);
1618 set_incmdpos(false);
1619 restore(ona, onc);
1620 zshlex();
1621
1622 let mut arms = Vec::new();
1623 const MAX_ARMS: usize = 10_000;
1624
1625 loop {
1626 if arms.len() > MAX_ARMS {
1627 zerr("par_case: too many arms");
1628 break;
1629 }
1630
1631 // Set incasepat BEFORE skipping separators so lexer knows we're in case pattern context
1632 // This affects how [ and | are lexed
1633 set_incasepat(1);
1634
1635 skip_separators();
1636
1637 // Check for end
1638 // Note: 'esac' might be String "esac" if incasepat > 0 prevents reserved word recognition
1639 let is_esac = tok() == ESAC
1640 || (tok() == STRING_LEX && tokstr().map(|s| s == "esac").unwrap_or(false));
1641 if (use_brace && tok() == OUTBRACE_TOK) || (!use_brace && is_esac) {
1642 set_incasepat(0);
1643 zshlex();
1644 break;
1645 }
1646
1647 // Also break on EOF. c:Src/parse.c:1209 par_case requires
1648 // ESAC (or `}` in brace form) to close the block — reaching
1649 // ENDINPUT without either is a parse error (`case ... esack`
1650 // typo absorbs `esack` as part of the body and silently
1651 // terminates rc=0 otherwise). Bug #400.
1652 if tok() == ENDINPUT || tok() == LEXERR {
1653 set_incasepat(0);
1654 crate::ported::utils::zerr("unmatched `case'");
1655 yyerror(0);
1656 break;
1657 }
1658
1659 // c:1250 — `if (tok == INPAR) zshlex();` — leading-paren
1660 // skip path. Used when the lexer DID return INPAR_TOK (e.g.
1661 // SHGLOB or incmdpos forced it). In the normal case-pattern
1662 // path the lexer absorbs `(...)` into one Stringg and the
1663 // hack at c:1322 strips the surrounding parens later. Both
1664 // paths land here.
1665 let leading_inpar_consumed = tok() == INPAR_TOK;
1666 if leading_inpar_consumed {
1667 zshlex();
1668 }
1669
1670 // c:1255-1262 — read pattern STRING. zsh's parser falls
1671 // straight into the STRING reader after the optional INPAR.
1672 // BAR before any pattern means empty string.
1673 let mut patterns = Vec::new();
1674 // Tracks whether the c:1322-1354 hack has fired (paren-
1675 // wrapped Stringg absorbed by the lexer). When it has, the
1676 // closing `)` was already absorbed — no separate OUTPAR
1677 // arm-close to consume.
1678 let mut absorbed_outpar = false;
1679
1680 // Nested-paren pattern: the user wrote `((alt|alt)tail)` —
1681 // the leading arm-INPAR was consumed; the NEXT token is also
1682 // INPAR. zinit.zsh:2946 hits this with `((add-|)fpath)`
1683 // meaning "fpath or add-fpath".
1684 //
1685 // The current lexer (cmd_or_math rewind path interacting
1686 // with gettokstr) drops ONE of the two `)` chars on the
1687 // way out, so the token stream is INPAR INPAR STRING BAR
1688 // STRING OUTPAR — only ONE OUTPAR for two source `)`. We
1689 // can't reconstruct the exact pattern from these tokens.
1690 // Proper fix needs to land in lex.rs (cmd_or_math rewind +
1691 // gettokstr LX2_INPAR/OUTPAR interplay) — see docs/BUGS.md
1692 // entry "case-pattern nested-paren lexer gap".
1693 //
1694 // Workaround: consume every token up to and INCLUDING the
1695 // arm-closing OUTPAR, build a best-effort pattern string,
1696 // set `absorbed_outpar = true` so the post-pattern OUTPAR
1697 // check below skips. The resulting glob may match a slightly
1698 // wider set than the original (`(add-|fpath)` vs
1699 // `(add-|)fpath`) but the rest of the script parses + runs.
1700 // Unblocking sourcing zinit-style configs is the priority.
1701 if leading_inpar_consumed && tok() == INPAR_TOK {
1702 let mut buf = String::new();
1703 let mut depth = 0i32;
1704 loop {
1705 match tok() {
1706 INPAR_TOK => {
1707 buf.push('(');
1708 depth += 1;
1709 zshlex();
1710 }
1711 OUTPAR_TOK => {
1712 // The single OUTPAR token in the stream is
1713 // simultaneously the inner glob-group close
1714 // AND the arm close (lexer collapses both).
1715 // Consume it, balance the inner depth, exit.
1716 // `absorbed_outpar = true` tells the shared
1717 // post-pattern check below to skip its own
1718 // consume.
1719 if depth > 0 {
1720 buf.push(')');
1721 depth -= 1;
1722 }
1723 zshlex();
1724 break;
1725 }
1726 STRING_LEX => {
1727 if let Some(s) = tokstr() {
1728 if s == "esac" {
1729 break;
1730 }
1731 buf.push_str(&s);
1732 }
1733 set_incasepat(2);
1734 zshlex();
1735 }
1736 BAR_TOK => {
1737 buf.push('|');
1738 set_incasepat(1);
1739 zshlex();
1740 }
1741 _ => break,
1742 }
1743 }
1744 patterns.push(buf);
1745 set_incasepat(0);
1746 set_incmdpos(true);
1747 absorbed_outpar = true;
1748 }
1749
1750 // Skip the legacy STRING/BAR pattern-read loop when the
1751 // nested-paren branch above already populated `patterns` and
1752 // consumed the arm-close. Otherwise the legacy loop would
1753 // greedily absorb the body's first command token (`echo`) as
1754 // another alt-pattern.
1755 if !patterns.is_empty() && absorbed_outpar {
1756 // skip to body parse
1757 } else {
1758 loop {
1759 if tok() == STRING_LEX {
1760 let s = tokstr();
1761 if s.as_deref().map(|s| s == "esac").unwrap_or(false) {
1762 break;
1763 }
1764 let mut str_val = s.unwrap_or_default();
1765
1766 // c:1322-1354 hack: when this is the first alt AND
1767 // the string starts with the Inpar marker, the lexer
1768 // absorbed the whole `(...)` as one token. Chuck the
1769 // blanks around `|`/parens at depth 1 (c:1332-1338 —
1770 // `( d | e )` must become `(d|e)`), then strip the
1771 // surrounding parens — the remainder IS the pattern.
1772 // The closing arm-paren was absorbed too, so we don't
1773 // expect a separate OUTPAR token afterward.
1774 if patterns.is_empty() && str_val.starts_with(crate::ported::zsh_h::Inpar) {
1775 use crate::ported::zsh_h::{Bar, Inpar, Outpar};
1776 let meta = crate::ported::zsh_h::Meta as char;
1777 let blank = |c: char| c.is_ascii() && crate::ported::ztype_h::iblank(c as u8);
1778 let mut chars: Vec<char> = str_val.chars().collect();
1779 let mut pct = 0i32;
1780 let mut i = 0usize;
1781 let mut end_idx: Option<usize> = None;
1782 while i < chars.len() {
1783 if chars[i] == Inpar {
1784 pct += 1;
1785 }
1786 if pct == 1 {
1787 // c:1332-1334 — chuck blanks AFTER `|`/`(`.
1788 if chars[i] == Bar || chars[i] == Inpar {
1789 while i + 1 < chars.len() && blank(chars[i + 1]) {
1790 chars.remove(i + 1);
1791 }
1792 }
1793 // c:1335-1338 — chuck blanks BEFORE `|`/`)`
1794 // (not Meta-escaped blanks).
1795 if chars[i] == Bar || chars[i] == Outpar {
1796 while i >= 1
1797 && blank(chars[i - 1])
1798 && (i < 2 || chars[i - 2] != meta)
1799 {
1800 chars.remove(i - 1);
1801 i -= 1;
1802 }
1803 }
1804 }
1805 if chars[i] == Outpar {
1806 pct -= 1;
1807 if pct == 0 {
1808 end_idx = Some(i);
1809 break;
1810 }
1811 }
1812 i += 1;
1813 }
1814 if let Some(idx) = end_idx {
1815 // c:1346-1352 — the surrounding-paren strip is only
1816 // valid when the WHOLE string is `(...)` (close-paren
1817 // is the last char). C asserts this with
1818 // `DPUTS(*str != Inpar || str[sl-1] != Outpar)` and
1819 // `YYERRORV` on `if (*s || pct ...)` when anything
1820 // follows the matched close-paren. When trailing
1821 // content exists (`(a|b)*`, `(a|b)c`), the `(...)` is
1822 // a glob GROUP, not the optional case-opener — keep
1823 // the whole string verbatim as one pattern and let
1824 // the separate arm-closing `)` (OUTPAR_TOK) be
1825 // consumed below. Stripping here turned `(a|b)*` into
1826 // `a|b*` ("a" or "b*"), breaking OS-dispatch case arms.
1827 if idx == chars.len() - 1 {
1828 chars.remove(idx);
1829 chars.remove(0);
1830 str_val = chars.into_iter().collect();
1831 absorbed_outpar = true;
1832 }
1833 }
1834 }
1835 patterns.push(str_val);
1836 if absorbed_outpar {
1837 // c:Src/parse.c:1300-1302 — after a whole-`(...)`
1838 // pattern the next token may be the body's first
1839 // command word; C lexes it with `incasepat = -1;
1840 // incmdpos = 1;` so assignments (`out+=hit`)
1841 // become ENVSTRING instead of a plain STRING
1842 // (lex.c:1229-1230 gates ENVSTRING on incmdpos).
1843 set_incasepat(-1);
1844 set_incmdpos(true);
1845 } else {
1846 set_incasepat(2);
1847 }
1848 zshlex();
1849 // When the hack fired the closing `)` is already
1850 // consumed; don't read alt-`|` continuations either.
1851 if absorbed_outpar {
1852 break;
1853 }
1854 } else if tok() != BAR_TOK {
1855 break;
1856 }
1857
1858 if tok() == BAR_TOK {
1859 set_incasepat(1);
1860 zshlex();
1861 } else {
1862 break;
1863 }
1864 }
1865 } // end else of "skip legacy loop when nested-paren branch fired"
1866 set_incasepat(0);
1867
1868 // c:1305 — expect OUTPAR (arm-close) when the hack didn't
1869 // already swallow it.
1870 //
1871 // Bug #34 in docs/BUGS.md: the absorbed-pattern hack assumed
1872 // the leading `(` and the case-arm closing `)` were both
1873 // absorbed into the single STRING token. That's true for
1874 // `(x))` (the inner `)` closes the absorbed group; the second
1875 // `)` is the arm closer) only when the lexer slurps BOTH.
1876 // The Rust lexer slurps just `(x|y)` (one balanced pair); the
1877 // second `)` arrives as a separate OUTPAR_TOK that must still
1878 // be consumed as the case-arm closer. Detect and consume it.
1879 if !absorbed_outpar {
1880 if tok() != OUTPAR_TOK {
1881 zerr("expected ')' in case pattern");
1882 return None;
1883 }
1884 // c:Src/parse.c:1257-1258 — `if (tok != STRING)
1885 // YYERRORV(oecused);` C requires at least one pattern
1886 // STRING before `)`. zshrs accepted empty `case x in)`
1887 // and silently fell through to the next iteration with
1888 // an empty pattern arm, swallowing the rest of the
1889 // script. Reject the empty-pattern shape unless a
1890 // leading INPAR was consumed (the `(pat)` form has
1891 // already validated the pattern inside). Bug #161 in
1892 // docs/BUGS.md.
1893 if patterns.is_empty() && !leading_inpar_consumed {
1894 zerr("parse error near `)'");
1895 return None;
1896 }
1897 set_incmdpos(true);
1898 zshlex();
1899 // When the lexer emitted a separate INPAR_TOK at the
1900 // arm start (consumed via `leading_inpar_consumed`
1901 // above), the OUTPAR_TOK we just consumed closed the
1902 // alternation GROUP. If the next token is ALSO
1903 // OUTPAR_TOK, the user wrote `(pat))` and that second
1904 // `)` is the case-arm closer that still needs to be
1905 // consumed before body parsing. Bug #34 in
1906 // docs/BUGS.md.
1907 if leading_inpar_consumed && tok() == OUTPAR_TOK {
1908 zshlex();
1909 }
1910 } else if tok() == OUTPAR_TOK {
1911 // The lexer absorbed `(pat)` as the pattern but left the
1912 // case-arm closing `)` as a separate OUTPAR_TOK. Consume
1913 // it now so body parsing starts at the body, not at `)`.
1914 set_incmdpos(true);
1915 zshlex();
1916 } else {
1917 set_incmdpos(true);
1918 }
1919
1920 // Parse body. Pass end_tokens explicitly so the body's
1921 // parser stops at DSEMI/SEMIAMP/SEMIBAR/ESAC without
1922 // tripping parse_program_until's orphan-terminator check
1923 // (line 7131) which only fires when end_tokens is None.
1924 // Without this, a case arm whose body has no trailing
1925 // `;;` before `esac` (last arm — zsh accepts the dangling
1926 // form) produced "parse error near orphan terminator" on
1927 // the closing `esac`. zsh's par_case at parse.c:1318 sets
1928 // up the case-arm reader to recognize the same terminator
1929 // set; the Rust port was passing the implicit-None and
1930 // hitting the top-level orphan check.
1931 let body = parse_program_until(Some(&[DSEMI, SEMIAMP, SEMIBAR, ESAC]), false);
1932
1933 // Get terminator. Set incasepat=1 BEFORE the zshlex
1934 // advance so the next token (the next arm's pattern, like
1935 // `[a-z]`) gets tokenized in pattern context. Without
1936 // this, a `[`-prefixed pattern after the FIRST arm became
1937 // Inbrack instead of String and the pattern-loop bailed
1938 // out with "expected ')' in case pattern".
1939 // c:Src/parse.c:1391 — `incasepat = 1; incmdpos = 0;` BEFORE
1940 // the zshlex that advances past `;;` to the next arm's first
1941 // token. The incmdpos=0 setting is what makes the lexer
1942 // absorb the next arm's `((add-|)fpath)` into a single STRING
1943 // (gettokstr's LX2_INPAR path at lex.c:1080+ runs only when
1944 // incmdpos is FALSE — at incmdpos=1 the lexer emits raw
1945 // INPAR_TOK for the inner `(` and par_case's pattern-read
1946 // loop has no path to recover). Our Rust port previously set
1947 // only incasepat=1 and not incmdpos=0, which forced
1948 // multi-arm patterns like `((add-|)fpath)` to fail with
1949 // "expected ')' in case pattern" — surfaced on
1950 // zinit.zsh:2946.
1951 let terminator = match tok() {
1952 DSEMI => {
1953 set_incasepat(1);
1954 set_incmdpos(false);
1955 zshlex();
1956 CaseTerm::Break
1957 }
1958 SEMIAMP => {
1959 set_incasepat(1);
1960 set_incmdpos(false);
1961 zshlex();
1962 CaseTerm::Continue
1963 }
1964 SEMIBAR => {
1965 set_incasepat(1);
1966 set_incmdpos(false);
1967 zshlex();
1968 CaseTerm::TestNext
1969 }
1970 _ => CaseTerm::Break,
1971 };
1972
1973 if !patterns.is_empty() {
1974 arms.push(CaseArm {
1975 patterns,
1976 body,
1977 terminator,
1978 });
1979 }
1980 }
1981
1982 Some(ZshCommand::Case(ZshCase { word, arms }))
1983}
1984
1985/// Parse if statement
1986/// Parse `if COND; then BODY; [elif COND; then BODY;]* [else BODY;] fi`.
1987/// Direct port of zsh/Src/parse.c:1411 `par_if`. The C source
1988/// emits WC_IF wordcodes per arm; zshrs builds an AST chain of
1989/// (cond, then_body) tuples plus an optional else_body.
1990fn par_if() -> Option<ZshCommand> {
1991 zshlex(); // skip 'if'
1992
1993 // c:1414 — `par_save_list(cmplx);` — the cond is an ORDINARY
1994 // list (same grammar as par_while's cond): a leading `{ … }`
1995 // block is the cond's first command (`if { false } { body }`,
1996 // `if { CMD } || { CMD }; then …` — p10k.zsh:8376), and the
1997 // brace-form BODY opener is reachable because a list followed
1998 // by `{` without a separator ends (the chaining rule in
1999 // parse_program_until). Only THEN needs to be in the stop set
2000 // (`then` at command position can't start a command).
2001 // fast-syntax-highlighting.plugin.zsh:365-375 is the canonical
2002 // real-world load: `if [[ … ]] { … } elif { type curl … } { … }`.
2003 let cond = Box::new(parse_program_until(Some(&[THEN]), false));
2004
2005 skip_separators();
2006
2007 // Expect 'then' or {
2008 let use_brace = tok() == INBRACE_TOK;
2009 if tok() != THEN && !use_brace {
2010 zerr("expected 'then' or '{' after if condition");
2011 return None;
2012 }
2013 zshlex();
2014
2015 // Parse then-body - stops at else/elif/fi, or } if using brace syntax
2016 let then = if use_brace {
2017 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2018 if tok() == OUTBRACE_TOK {
2019 zshlex();
2020 // c:Src/parse.c:1469-1470 — `zshlex(); incmdpos = 1;`. C
2021 // par_if explicitly resets incmdpos to 1 after consuming
2022 // OUTBRACE so subsequent commands (`; echo after`, `else`
2023 // following the `}` without a `;`) are at command position
2024 // and tokenize correctly.
2025 set_incmdpos(true);
2026 }
2027 Box::new(body)
2028 } else {
2029 Box::new(parse_program_until(Some(&[ELSE, ELIF, FI]), false))
2030 };
2031
2032 // c:Src/parse.c:1471-1472 — `if (tok == SEPER) break;`. For
2033 // brace-form `if … { … }`, C par_if breaks out of the outer
2034 // construct loop WITHOUT consuming the SEPER when one
2035 // immediately follows the closing `}`. The SEPER stays in the
2036 // lexer for the OUTER par_list to consume as the if-statement's
2037 // list separator. Our Rust port's loop below calls skip_separators
2038 // which would eat that SEPER and leave tok=STRING(next-command),
2039 // triggering par_cmd's "STRING_LEX after compound = parse error"
2040 // check at line ~1170. Without this early-exit, brace-form
2041 // if [[ … ]] { … }; echo after
2042 // failed with `parse error near 'echo'` on zinit.zsh:1422.
2043 //
2044 // We only need this when use_brace is true and the next token
2045 // is a separator (SEPER / NEWLIN / SEMI) AND it's NOT followed
2046 // by ELSE/ELIF (those would extend the construct). Since the
2047 // brace-form already consumed its closing `}` (saw_terminator
2048 // is true), early-return is safe.
2049 if use_brace && matches!(tok(), SEPER | NEWLIN | SEMI | ENDINPUT) {
2050 return Some(ZshCommand::If(ZshIf {
2051 cond,
2052 then,
2053 elif: Vec::new(),
2054 else_: None,
2055 }));
2056 }
2057
2058 // Parse elif and else. zsh accepts the SAME elif/else
2059 // continuations for both classic `then/fi` AND the brace
2060 // form `{ ... } elif ... { ... } else { ... }`. Direct port
2061 // of zsh/Src/parse.c:1417-1500 par_if where the elif/else
2062 // arms are checked AFTER the body close regardless of which
2063 // delimiter style opened the block. Without this, zinit's
2064 // if [[ -z $sel ]] { ... } else { ... }
2065 // hung the parser — `else` was treated as an external
2066 // command following the if-statement, which the lexer state
2067 // mis-classified inside the still-open function body.
2068 //
2069 // For brace-form: skip the `fi` consumption at the end of
2070 // the loop (no `fi` after a brace block), and `else` may
2071 // arrive after a `}` close. Skip-separators between the
2072 // body close and the elif/else token.
2073 let mut elif = Vec::new();
2074 let mut else_ = None;
2075 // c:Src/parse.c:1501-1504 — `if (tok != FI) { cmdpop(); YYERRORV; }`.
2076 // The C parser fails the whole if-construct when the body close
2077 // isn't seen. zshrs's loop fell through silently on ENDINPUT, so
2078 // `if true; then echo yes` (no `fi`) was accepted. Track whether
2079 // we hit a real terminator and error after the loop if not.
2080 let mut saw_terminator = use_brace; // `{ … }` body already consumed its close
2081
2082 {
2083 loop {
2084 skip_separators();
2085
2086 match tok() {
2087 ELIF => {
2088 zshlex();
2089 // c:1414 — elif cond is an ordinary list like the
2090 // if cond above: `elif { type curl &>/dev/null }
2091 // { … }` parses the brace block as the COND's
2092 // command, and the body `{` ends the list via the
2093 // no-separator chaining rule. INBRACE_TOK in the
2094 // stop set made the cond parse EMPTY and the cond
2095 // block masquerade as the body (fsh:370).
2096 let econd = parse_program_until(Some(&[THEN]), false);
2097 skip_separators();
2098
2099 let elif_use_brace = tok() == INBRACE_TOK;
2100 if tok() != THEN && !elif_use_brace {
2101 zerr("expected 'then' after elif");
2102 return None;
2103 }
2104 zshlex();
2105
2106 // elif body stops at else/elif/fi or } if using braces
2107 let ebody = if elif_use_brace {
2108 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2109 if tok() == OUTBRACE_TOK {
2110 zshlex();
2111 saw_terminator = true; // brace close on elif
2112 }
2113 body
2114 } else {
2115 parse_program_until(Some(&[ELSE, ELIF, FI]), false)
2116 };
2117
2118 elif.push((econd, ebody));
2119 }
2120 ELSE => {
2121 zshlex();
2122 skip_separators();
2123
2124 // Brace-form `else { … }` is only legal when the
2125 // PARENT IF itself was opened brace-form (`if cond
2126 // { … }`). For a `then`-form if, `else { stmt }`
2127 // is `else` followed by a brace-group STATEMENT
2128 // that's part of the else body's statement list,
2129 // which still terminates at `fi`. p10k.zsh:5575
2130 // hits exactly this shape:
2131 // else
2132 // { local v=($(<$file)) } 2>/dev/null
2133 // fi
2134 // The prior port unconditionally consumed the `{`
2135 // as else-brace-opener, then expected the `fi`
2136 // outside the if construct → "expected `done'".
2137 let else_use_brace = use_brace && tok() == INBRACE_TOK;
2138 if else_use_brace {
2139 zshlex();
2140 }
2141
2142 // else body stops at 'fi' or '}'
2143 else_ = Some(Box::new(if else_use_brace {
2144 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2145 if tok() == OUTBRACE_TOK {
2146 zshlex();
2147 saw_terminator = true;
2148 }
2149 body
2150 } else {
2151 parse_program_until(Some(&[FI]), false)
2152 }));
2153
2154 // Consume the 'fi' if present (not for brace syntax)
2155 if !else_use_brace && tok() == FI {
2156 zshlex();
2157 saw_terminator = true;
2158 }
2159 break;
2160 }
2161 FI => {
2162 // Brace-form `if ... { ... }` is already terminated by
2163 // its closing `}`. Do NOT consume `fi` here — it belongs
2164 // to an enclosing then-form if. Without this gate, a
2165 // brace-form if inside a then-form if's body would steal
2166 // the outer `fi`, leaving the outer parser to see
2167 // "unterminated if". This bit zinit-install.zsh:978
2168 // where `if (( … )) {` (brace) inside `if … ; then …`
2169 // (then-form) ate the outer `fi`.
2170 if use_brace {
2171 break;
2172 }
2173 zshlex();
2174 saw_terminator = true;
2175 break;
2176 }
2177 _ => break,
2178 }
2179 }
2180 }
2181
2182 if !saw_terminator {
2183 // c:1501-1504 — YYERRORV when the if-construct never closed.
2184 zerr("parse error: unterminated if");
2185 return None;
2186 }
2187
2188 Some(ZshCommand::If(ZshIf {
2189 cond,
2190 then,
2191 elif,
2192 else_,
2193 }))
2194}
2195
2196/// Parse while/until loop
2197/// Parse `while COND; do BODY; done` and `until COND; do BODY; done`.
2198/// Direct port of zsh/Src/parse.c:1521 `par_while`. The
2199/// `until` variant is the same loop with the condition negated.
2200fn par_while(until: bool) -> Option<ZshCommand> {
2201 zshlex(); // skip while/until
2202
2203 // c:1521-1551 par_while — the condition's parser must stop at
2204 // `do` or `{`. Without an explicit end-token set, parse_program
2205 // consumes the brace-form body as additional condition lists,
2206 // leaving parse_loop_body with nothing — `while (( i++ < 3 )) {
2207 // echo $i }` silently parsed but executed nothing.
2208 // c:1528 — `par_save_list(cmplx);` — the cond is an ORDINARY
2209 // list: a leading `{...}` block is the cond's first command
2210 // (`while {false} {body}`), and the body INBRACE is reachable
2211 // because a list followed by `{` without a separator ends (the
2212 // chaining rule in parse_program_until). DOLOOP stays in the
2213 // stop set because `do` at command position can't start a
2214 // command. INBRACE_TOK was previously (wrongly) in the set too,
2215 // which made a brace-form COND parse as an empty cond + body.
2216 let cond = Box::new(parse_program_until(Some(&[DOLOOP]), false));
2217
2218 skip_separators();
2219 let body = parse_loop_body(false, false)?;
2220
2221 // c:Src/parse.c:1521-1551 par_while — WC_WHILE wordcode is tagged
2222 // with WC_WHILE_TYPE differentiating WHILE vs UNTIL at the wordcode
2223 // layer. The AST mirror in zsh_ast.rs has separate Until(ZshWhile)
2224 // and While(ZshWhile) variants; route by the `until` flag here so
2225 // downstream pattern-matchers can distinguish without poking
2226 // inside the payload's bool.
2227 let w = ZshWhile {
2228 cond,
2229 body: Box::new(body),
2230 until,
2231 };
2232 Some(if until {
2233 ZshCommand::Until(w) // c:1521 (WC_WHILE_TYPE = WC_WHILE_UNTIL)
2234 } else {
2235 ZshCommand::While(w) // c:1521 (WC_WHILE_TYPE = WC_WHILE_WHILE)
2236 })
2237}
2238
2239/// Parse repeat loop
2240/// Parse `repeat N; do BODY; done`. Direct port of
2241/// zsh/Src/parse.c:1565 `par_repeat`. The C source supports
2242/// the SHORTLOOPS short-form `repeat N CMD` (no do/done) — zshrs's
2243/// parser doesn't yet special-case that variant.
2244fn par_repeat() -> Option<ZshCommand> {
2245 zshlex(); // skip 'repeat'
2246
2247 let count = match tok() {
2248 STRING_LEX => {
2249 let c = tokstr().unwrap_or_default();
2250 zshlex();
2251 c
2252 }
2253 _ => {
2254 zerr("expected count after repeat");
2255 return None;
2256 }
2257 };
2258
2259 skip_separators();
2260 // c:1600 — par_repeat's short-form gate is wider: it unlocks
2261 // when SHORTLOOPS OR SHORTREPEAT is set (vs SHORTLOOPS alone for
2262 // for/while). Pass `is_repeat=true` so parse_loop_body
2263 // applies that widened gate.
2264 let body = parse_loop_body(false, true)?;
2265
2266 Some(ZshCommand::Repeat(ZshRepeat {
2267 count,
2268 body: Box::new(body),
2269 }))
2270}
2271
2272/// Parse (...) subshell
2273/// Parse a subshell `( ... )`. Direct port of zsh/Src/parse.c:1619
2274/// `par_subsh`. Body parses as a normal list; the subshell wrapper
2275/// fork-isolates execution in the executor.
2276fn par_subsh() -> Option<ZshCommand> {
2277 zshlex(); // skip (
2278 // c:Src/parse.c:par_subsh — `parse_event(OUTPAR)` parses until
2279 // the matching `)`. zshrs's previous port called bare
2280 // `parse_program()` (parse_program_until(None, false)) which has no
2281 // way to know it should stop at OUTPAR_TOK — at top-level
2282 // that's fine (the outer loop just sees an extra OUTPAR after
2283 // the inner body), but the parse_event-equivalent's new
2284 // yyerror-on-unconsumed-token behavior at parse_program_until's
2285 // None arm now reports a spurious "parse error near `)'" when
2286 // the construct ends. Pass OUTPAR_TOK so parse_program_until
2287 // stops cleanly at the closing paren.
2288 let prog = parse_program_until(Some(&[OUTPAR_TOK]), false);
2289 if tok() == OUTPAR_TOK {
2290 zshlex();
2291 }
2292 Some(ZshCommand::Subsh(Box::new(prog)))
2293}
2294
2295/// Parse function definition
2296/// Parse `function NAME { BODY }` or `NAME () { BODY }`. Direct
2297/// port of zsh/Src/parse.c:1672 `par_funcdef`. zsh handles
2298/// the multiple keyword shapes (function FOO, FOO (), function FOO ()),
2299/// the optional `[fname1 fname2 ...]` for multi-name function defs,
2300/// and the `function FOO () { ... }` traditional/POSIX hybrid form.
2301fn par_funcdef() -> Option<ZshCommand> {
2302 zshlex(); // skip 'function'
2303
2304 let mut names = Vec::new();
2305 let mut tracing = false;
2306
2307 // Handle options like -T and function names. Two subtleties:
2308 //
2309 // 1. Flags: zsh's lexer encodes a leading `-` as
2310 // `zsh_h::Dash` (`\u{9b}`, `Src/zsh.h:182`) inside the String tokstr.
2311 // The previous `s.starts_with('-')` check failed for
2312 // `\u{9b}T`, so `function -T NAME { body }` slipped the
2313 // `-T` token into `names` and the function got registered
2314 // as `T` plus the intended `NAME`.
2315 //
2316 // 2. Body opener: zsh's lexer emits the opening `{` as a
2317 // String (not INBRACE_TOK) when it follows the String
2318 // NAME — the preceding name token resets incmdpos to
2319 // false, and only `{` immediately followed by `}` (the
2320 // empty-body case) gets promoted to Inbrace. The funcdef
2321 // parser must recognise the bare-`{` String as the body
2322 // opener; otherwise `function NAME { body }` falls through
2323 // to `_ => break`, no body parses, and the FuncDef never
2324 // lands in the AST. This is consistent with C zsh's
2325 // par_funcdef which knows it's in funcdef-header context
2326 // and accepts the brace either way.
2327 loop {
2328 match tok() {
2329 STRING_LEX => {
2330 let _ts_s = tokstr()?;
2331 let s = _ts_s.as_str();
2332 // c:1702 — `if ((*tokstr == Inbrace || *tokstr == '{') && !tokstr[1])`.
2333 // Body opener can be either the literal `{` (early-return
2334 // path at lex.c:1141-1144 / lex.rs LX2_INBRACE cmdpos
2335 // branch) or the Inbrace marker `\u{8f}` (lex.c:1420
2336 // post-switch add(c) where c was rewritten via lextok2).
2337 if s == "{" || s == "\u{8f}" {
2338 break;
2339 }
2340 let first = s.chars().next();
2341 if matches!(first, Some('-') | Some('+')) || matches!(first, Some(c) if c == Dash) {
2342 if s.contains('T') {
2343 tracing = true;
2344 }
2345 zshlex();
2346 continue;
2347 }
2348 // c:Src/exec.c::execcmd_args — function name tokens
2349 // in `function NAME { ... }` form go through globbing
2350 // at parse time. zsh's `function with[bracket] { ... }`
2351 // triggers a glob expansion of `with[bracket]`; no file
2352 // matches → "no matches found: NAME" + rc=1 (when
2353 // NOMATCH is set, the default). Bug #536: zshrs accepted
2354 // the literal bracket-containing name and registered
2355 // the function silently. Mirror C by probing for glob
2356 // metachars on the name; if present AND no file
2357 // matches, emit the diagnostic and abort the parse.
2358 let has_glob_chars = s.chars().any(|c| {
2359 matches!(
2360 c,
2361 '[' | ']'
2362 | '*'
2363 | '?'
2364 | crate::ported::zsh_h::Inbrack
2365 | crate::ported::zsh_h::Outbrack
2366 | crate::ported::zsh_h::Star
2367 | crate::ported::zsh_h::Quest
2368 )
2369 });
2370 if has_glob_chars && crate::ported::zsh_h::isset(crate::ported::zsh_h::NOMATCH) {
2371 // Probe the funcname pattern through the canonical
2372 // glob entry — C `zglob(args, firstnode(args), 0)`
2373 // (c:3318). zglob runs the tokenized word and, under
2374 // NOMATCH with no match (nullglob off), emits
2375 // "no matches found: PAT" + errflag itself
2376 // (c:1876-1880). Tokenize first (the funcname word
2377 // still holds literal `*`/`?`; haswilds keys on the
2378 // Star/Quest tokens) and propagate the abort.
2379 let mut probe = vec![s.to_string()];
2380 crate::ported::glob::tokenize(&mut probe[0]);
2381 crate::ported::glob::zglob(&mut probe, 0, 0);
2382 if crate::ported::utils::errflag.load(std::sync::atomic::Ordering::Relaxed)
2383 & crate::ported::utils::ERRFLAG_ERROR
2384 != 0
2385 {
2386 return None;
2387 }
2388 }
2389 names.push(s.to_string());
2390 zshlex();
2391 }
2392 INBRACE_TOK | INOUTPAR | SEPER | NEWLIN => break,
2393 _ => break,
2394 }
2395 }
2396
2397 // Optional ()
2398 let saw_paren = tok() == INOUTPAR;
2399 if saw_paren {
2400 zshlex();
2401 }
2402
2403 skip_separators();
2404
2405 // Body opener: real Inbrace OR a String containing the literal `{`
2406 // (early-return path) OR a String containing the Inbrace marker
2407 // `\u{8f}` (bct++ path post-switch add). C parse.c:1702 handles
2408 // both string forms via `*tokstr == Inbrace || *tokstr == '{'`.
2409 let body_opener_is_string_brace =
2410 tok() == STRING_LEX && tokstr().map(|s| s == "{" || s == "\u{8f}").unwrap_or(false);
2411 if tok() == INBRACE_TOK || body_opener_is_string_brace {
2412 // Capture body_start BEFORE the lexer advances past the
2413 // first body token. After the previous zshlex consumed
2414 // `{`, lexer.pos points just past `{` (which is where the
2415 // body source starts). The next `zshlex()` would advance
2416 // past the first token (`echo`), making body_start land
2417 // mid-body and lose the first word — `typeset -f f` would
2418 // print `a; echo b` for `{ echo a; echo b }`.
2419 // c:Src/parse.c:1690-1706 — par_funcdef requires a clean
2420 // body-opener brace when the anonymous form `function {body}`
2421 // is used (no names AND no `()`). zsh's lexer keeps the `{`
2422 // as its own STRING token via the lex.c:1141-1144 early-
2423 // return at command position, but the body brace must be
2424 // followed by whitespace for the inner par_list to find a
2425 // matching OUTBRACE — without a separator, the closing `}`
2426 // gets merged into the last word (`X}`) and par_list ends
2427 // without OUTBRACE, which C zsh reports as `parse error near
2428 // \`}'`. zshrs's lexer has the same `bct` semantics; reject
2429 // here at the parse step so the funcdef doesn't silently run
2430 // with the stray `}` attached. With names or `()` present,
2431 // the body brace is allowed even without a separator
2432 // (`function name {body}` and `function () {body}` both work
2433 // in zsh). Bug #60 in docs/BUGS.md.
2434 if names.is_empty() && !saw_paren {
2435 // Peek the next source byte after the current lexer position
2436 // (`{` was just tokenized — `pos()` points just past it).
2437 // A whitespace separator means proper `function { body }`
2438 // form; anything else is the malformed `function {body}`
2439 // shape zsh rejects.
2440 let next_byte = input_slice(pos(), pos() + 1)
2441 .and_then(|s| s.bytes().next())
2442 .unwrap_or(b' ');
2443 if !matches!(next_byte, b' ' | b'\t' | b'\n' | b';') {
2444 zerr("parse error near `}'"); // c:Src/parse.c YYERRORV
2445 return None;
2446 }
2447 }
2448 let body_start = pos();
2449 zshlex();
2450 // c:Src/parse.c — func body terminates at OUTBRACE_TOK.
2451 // Explicit end-token keeps the inner parse from hitting the
2452 // top-level stray-`}` arm (#168). Bug #167 family.
2453 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
2454 // c:Src/parse.c:1733-1737 — `if (tok != OUTBRACE) { cmdpop();
2455 // ... YYERRORV(oecused); }`. Hard-error on missing close brace
2456 // so `function f { echo hi` doesn't silently register a half-
2457 // parsed body. Bug #405.
2458 if tok() != OUTBRACE_TOK {
2459 zerr("parse error: expected `}'");
2460 return None;
2461 }
2462 let body_end = pos().saturating_sub(1);
2463 let body_source = input_slice(body_start, body_end)
2464 .map(|s| {
2465 // Lexer's pos() may have advanced past `}` AND skipped
2466 // trailing whitespace/newlines before returning the
2467 // OUTBRACE_TOK to us, so the slice up to `pos - 1`
2468 // includes the `}` and any preceding whitespace.
2469 // Strip the trailing `}` and any preceding structural
2470 // separator (`;`, `\n`) — C zsh's getpermtext walks
2471 // the wordcode list and emits each command WITHOUT
2472 // the trailing `;`/`\n` that lives in the input.
2473 let t = s.trim();
2474 let t = t.strip_suffix('}').unwrap_or(t).trim_end();
2475 let t = t
2476 .trim_end_matches(|c: char| c == ';' || c == '\n')
2477 .trim_end();
2478 t.to_string()
2479 })
2480 .filter(|s| !s.is_empty());
2481 zshlex();
2482
2483 // Anonymous form `function () { body } a b c` (with `()`) or
2484 // `function { body } a b c` (zsh-only shorthand, no `()`). No
2485 // name was collected. Mirror parse_anon_funcdef: synthesize
2486 // `_zshrs_anon_N`, collect trailing args, set auto_call_args
2487 // so compile_funcdef registers + immediately calls the
2488 // function with the args as positional params.
2489 if names.is_empty() {
2490 let mut args = Vec::new();
2491 while tok() == STRING_LEX {
2492 if let Some(s) = tokstr() {
2493 args.push(s);
2494 }
2495 zshlex();
2496 }
2497 static ANON_COUNTER: AtomicUsize = AtomicUsize::new(0);
2498 let n = ANON_COUNTER.fetch_add(1, Ordering::Relaxed);
2499 let name = format!("_zshrs_anon_kw_{}", n);
2500 return Some(ZshCommand::FuncDef(ZshFuncDef {
2501 names: vec![name],
2502 body: Box::new(body),
2503 tracing,
2504 auto_call_args: Some(args),
2505 body_source,
2506 }));
2507 }
2508
2509 Some(ZshCommand::FuncDef(ZshFuncDef {
2510 names,
2511 body: Box::new(body),
2512 tracing,
2513 auto_call_args: None,
2514 body_source,
2515 }))
2516 } else if unset(SHORTLOOPS) {
2517 // c:Src/parse.c:1742 — `else if (unset(SHORTLOOPS)) YYERRORV`.
2518 // Braceless short body (`function f () CMD`) requires SHORTLOOPS.
2519 zerr("parse error: short function body form requires SHORTLOOPS option");
2520 None
2521 } else {
2522 // c:Src/parse.c:1747-1748 — `else par_list1(&c)`. The short body
2523 // is ONE sublist (a single command), NOT a full list. The prior
2524 // `par_list(false)` greedily consumed past the `;` (so `function
2525 // foo () print bar; foo` swallowed the trailing `foo` call and
2526 // errored "parse error near `foo'"). Use par_cmd for the single
2527 // command, matching the brace-less `foo() CMD` path
2528 // (parse_inline_funcdef). Bug surface: C04funcdef `function foo ()
2529 // print bar`.
2530 par_cmd().map(|cmd| {
2531 let list = ZshList {
2532 sublist: ZshSublist {
2533 pipe: ZshPipe {
2534 cmd,
2535 next: None,
2536 lineno: lineno(),
2537 merge_stderr: false,
2538 },
2539 next: None,
2540 flags: SublistFlags::default(),
2541 },
2542 flags: ListFlags::default(),
2543 };
2544 ZshCommand::FuncDef(ZshFuncDef {
2545 names,
2546 body: Box::new(ZshProgram { lists: vec![list] }),
2547 tracing,
2548 auto_call_args: None,
2549 body_source: None,
2550 })
2551 })
2552 }
2553}
2554
2555/// Parse time command
2556/// Parse `time CMD` (POSIX time keyword). Direct port of
2557/// zsh/Src/parse.c:1787 `par_time`. The `time` keyword
2558/// times the execution of the following pipeline / cmd.
2559fn par_time() -> Option<ZshCommand> {
2560 zshlex(); // skip 'time'
2561
2562 // Check if there's a pipeline to time
2563 if tok() == SEPER || tok() == NEWLIN || tok() == ENDINPUT {
2564 Some(ZshCommand::Time(None))
2565 } else {
2566 let sublist = par_sublist();
2567 Some(ZshCommand::Time(sublist.map(Box::new)))
2568 }
2569}
2570
2571/// Port of `par_dinbrack(void)` from `Src/parse.c:1810`. Body
2572/// parser inside `[[ ... ]]` — calls `par_cond` to emit the
2573/// condition wordcode then advances past `]]`.
2574pub fn par_dinbrack() -> Option<()> {
2575 // c:1810
2576 set_incond(1); // c:1814
2577 set_incmdpos(false); // c:1815
2578 zshlex(); // c:1816
2579 let _ = par_cond(); // c:1817
2580 if tok() != DOUTBRACK {
2581 // c:1818
2582 crate::ported::utils::zerr("missing ]]");
2583 yyerror(0);
2584 return None;
2585 }
2586 set_incond(0); // c:1820
2587 set_incmdpos(true); // c:1821
2588 zshlex(); // c:1822
2589 Some(())
2590}
2591
2592/// Parse a simple command
2593/// Parse a simple command (assignments + words + redirections).
2594/// Direct port of zsh/Src/parse.c:1836 `par_simple` —
2595/// the largest single function in parse.c. Handles ENVSTRING/
2596/// ENVARRAY assignments at command head, intermixed redirs,
2597/// typeset-style multi-assignment commands, and the trailing
2598/// inout-par `()` that converts a simple command into an inline
2599/// function definition.
2600fn par_simple(mut redirs: Vec<ZshRedir>) -> Option<ZshCommand> {
2601 let mut assigns = Vec::new();
2602 let mut words = Vec::new();
2603
2604 // c:1934-1974 — `{var}>file` brace-FD detection is wired
2605 // INSIDE the words loop below (parse.rs:4940-4956) rather than
2606 // here at the head. The words-loop site sees the tok=STRING
2607 // `{varname}` followed by a REDIROP and routes into par_redir
2608 // with redir.varid populated. C does it inline at the start of
2609 // each STRING/TYPESET arm iteration; functionally equivalent.
2610
2611 // c:1843-1846 — leading-NOCORRECT prefix: `nocorrect echo hello`
2612 // emits a NOCORRECT token at the start of par_simple. C sets
2613 // `nocorrect = 1` and skips past via the `zshlex();` at the
2614 // for-loop tail (c:1907). zshrs's par_simple (AST) had no
2615 // NOCORRECT arm so the token was silently dropped and the
2616 // following command line evaporated — `nocorrect echo hello`
2617 // produced empty output.
2618 while tok() == NOCORRECT {
2619 set_nocorrect(1); // c:1846
2620 zshlex(); // c:1907 (loop-tail zshlex)
2621 }
2622
2623 // Parse leading assignments
2624 while tok() == ENVSTRING || tok() == ENVARRAY {
2625 if let Some(assign) = parse_assign() {
2626 assigns.push(assign);
2627 }
2628 zshlex();
2629 }
2630
2631 // Parse words and redirections
2632 loop {
2633 match tok() {
2634 ENVSTRING | ENVARRAY if words.is_empty() => {
2635 // c:1843-1907 — still in command position (no command
2636 // word collected yet): a `name=val` token here is a
2637 // PRE-COMMAND assignment, even when it follows a
2638 // redirection (`a=b 2>/dev/null c=d`). C's single
2639 // par_simple loop collects assignments and redirs in any
2640 // order until the command word; zshrs split that into a
2641 // leading-assignments while-loop (above) plus this main
2642 // loop, and the leading loop stopped at the first redir.
2643 // Without this arm the trailing `c=d` fell into the
2644 // typeset-word path below and was silently dropped, so
2645 // `a=b 2>/dev/null c=d` set neither parameter.
2646 if let Some(assign) = parse_assign() {
2647 assigns.push(assign);
2648 }
2649 zshlex();
2650 }
2651 ENVSTRING | ENVARRAY => {
2652 // Mid-command assignment-shape arg under typeset
2653 // / declare / local / etc. (intypeset gates the
2654 // lexer to emit Envstring/Envarray for `name=val`
2655 // and `name=()` past the command name). Parse the
2656 // assignment, then emit a synthetic word
2657 // `NAME=value` (scalar) or `NAME=( … )` (array)
2658 // string so typeset's builtin arg list sees the
2659 // assignment-shape arg. Avoids the inline-env
2660 // scope path that mistakenly treats it like a
2661 // pre-cmd `X=Y cmd` assignment.
2662 if let Some(assign) = parse_assign() {
2663 let synthetic = match &assign.value {
2664 ZshAssignValue::Scalar(v) => format!("{}={}", assign.name, v),
2665 ZshAssignValue::Array(elems) => {
2666 // c:Src/builtin.c — assoc paren-init `h=( "" v
2667 // k2 v2 )` must preserve empty-string
2668 // elements (zsh stores key="" + value="v").
2669 // The bin_typeset paren-init splitter at
2670 // `builtin.rs:4358` recognizes the
2671 // REJOIN_SEP (`\u{1f}`) sentinel between
2672 // array elements and skips the leading/
2673 // trailing parens trim; using it here
2674 // round-trips empties end-to-end through
2675 // the synthetic-arg rebuild. Space-join
2676 // collapses adjacent empties (`(` + `""` +
2677 // `empty-val` becomes `( empty-val`) so
2678 // bin_typeset never sees the empty key.
2679 // Bug #93 in docs/BUGS.md.
2680 let mut buf = String::with_capacity(
2681 assign.name.len() + 4 + elems.iter().map(|e| e.len() + 1).sum::<usize>(),
2682 );
2683 buf.push_str(&assign.name);
2684 buf.push_str("=(");
2685 for elem in elems {
2686 buf.push('\u{1f}');
2687 buf.push_str(elem);
2688 }
2689 buf.push('\u{1f}');
2690 buf.push(')');
2691 buf
2692 }
2693 };
2694 words.push(synthetic);
2695 }
2696 zshlex();
2697 }
2698 STRING_LEX | TYPESET => {
2699 let s = tokstr();
2700 if let Some(s) = s {
2701 words.push(s);
2702 }
2703 // c:1929 — `incmdpos = 0;` so the next zshlex() does
2704 // not re-promote `{`/`[[`/reserved words at the
2705 // continuation position. Without this, `echo {a,b}`
2706 // re-lexes `{` as INBRACE_TOK (current-shell block)
2707 // and the brace expansion never reaches par_simple.
2708 set_incmdpos(false);
2709 // c:1931-1932 — `if (tok == TYPESET) intypeset = is_typeset = 1;`
2710 // Multi-assign `typeset a=1 b=2` relies on the lexer
2711 // re-emitting `b=2` as ENVSTRING; that path is gated
2712 // on `intypeset`. Without this, follow-on assignment
2713 // words arrive as STRING and the typeset builtin's
2714 // multi-assign form silently degrades.
2715 if tok() == TYPESET {
2716 set_intypeset(true);
2717 }
2718 zshlex();
2719 // Check for function definition foo() { ... }
2720 if words.len() == 1 && tok() == INOUTPAR {
2721 return parse_inline_funcdef(std::mem::take(&mut words));
2722 }
2723 // `{name}>file` named-fd redirect: the lexer doesn't
2724 // recognize this shape, so the bare word `{name}`
2725 // arrives as a String. If it matches `{IDENT}` and
2726 // the NEXT token is a redirop, pop it off as the
2727 // varid for that redir.
2728 if !words.is_empty() && IS_REDIROP(tok()) {
2729 let last = words.last().unwrap();
2730 let untoked = super::lex::untokenize(last);
2731 if untoked.starts_with('{') && untoked.ends_with('}') && untoked.len() > 2 {
2732 let name = &untoked[1..untoked.len() - 1];
2733 if !name.is_empty()
2734 && name.chars().all(|c| c == '_' || c.is_ascii_alphanumeric())
2735 && name
2736 .chars()
2737 .next()
2738 .map(|c| c == '_' || c.is_ascii_alphabetic())
2739 .unwrap_or(false)
2740 {
2741 let varid = name.to_string();
2742 words.pop();
2743 if let Some(mut redir) = par_redir() {
2744 redir.varid = Some(varid);
2745 redirs.push(redir);
2746 }
2747 continue;
2748 }
2749 }
2750 }
2751 }
2752 _ if IS_REDIROP(tok()) => {
2753 match par_redir() {
2754 Some(redir) => redirs.push(redir),
2755 None => break, // Error in redir parsing, stop
2756 }
2757 }
2758 INOUTPAR if !words.is_empty() => {
2759 // c:2055-2057 — `if (!isset(MULTIFUNCDEF) && argc > 1)
2760 // YYERROR(oecused);` — multi-name funcdef gate:
2761 // `f1 f2() { ... }` defines f1 AND f2 to the same
2762 // body, but only when MULTIFUNCDEF is set.
2763 if !isset(MULTIFUNCDEF) && words.len() > 1 {
2764 zerr("parse error: multiple names in function definition without MULTIFUNCDEF");
2765 return None;
2766 }
2767 // c:2061-2068 — `if (isset(EXECOPT) && hasalias &&
2768 // !isset(ALIASFUNCDEF) && argc && hasalias !=
2769 // input_hasalias()) { zwarn(...); YYERROR(...); }`
2770 // Alias-as-funcdef warning. zshrs's parser doesn't
2771 // track `hasalias` (alias-expansion provenance
2772 // during parse) yet, so `had_alias` stays false —
2773 // the gate is wired here as a marker so the canonical
2774 // C predicate is visible. Once alias-provenance lands,
2775 // swap `false` for the actual provenance compare.
2776 let had_alias = false;
2777 if isset(EXECOPT) && had_alias && !isset(ALIASFUNCDEF) && !words.is_empty() {
2778 crate::ported::utils::zwarn("defining function based on alias `(unknown)'");
2779 return None;
2780 }
2781 // foo() { ... } / multi-name `f1 f2 f3() { ... }` style
2782 // function. c:Src/parse.c:2055-2068 — under MULTIFUNCDEF
2783 // every preceding word names the SAME body; pass them all
2784 // so each is registered (the gate above already rejected
2785 // multi-name without MULTIFUNCDEF). Previously only the
2786 // last word (`words.pop()`) was defined, so `f1 f2 f3()
2787 // {...}` left f1/f2 as "command not found".
2788 return parse_inline_funcdef(std::mem::take(&mut words));
2789 }
2790 _ => break,
2791 }
2792 }
2793
2794 if assigns.is_empty() && words.is_empty() && redirs.is_empty() {
2795 return None;
2796 }
2797
2798 Some(ZshCommand::Simple(ZshSimple {
2799 assigns,
2800 words,
2801 redirs,
2802 }))
2803}
2804
2805/// Parse a redirection
2806/// Parse a redirection (>file, <file, >>file, <<HEREDOC, etc.).
2807/// Direct port of zsh/Src/parse.c:2229 `par_redir`. Returns
2808/// a ZshRedir node carrying the operator type, fd, target word
2809/// (or here-doc body / pipe-redir command), and any `{var}` style
2810/// fd-binding parameter.
2811fn par_redir() -> Option<ZshRedir> {
2812 par_redir_with_id(None)
2813}
2814
2815/// Wire a here-document body onto the redirection token that
2816/// requested it. Direct port of zsh/Src/parse.c:2347
2817/// `setheredoc`. Called when a heredoc terminator has been
2818/// matched and the body is ready to be attached to the redir.
2819///
2820/// zshrs port note: zsh's setheredoc patches the wordcode
2821/// in-place via `pc[1] = ecstrcode(doc); pc[2] = ecstrcode(term);`.
2822/// zshrs threads heredoc bodies through `HereDocInfo` structs
2823/// attached inline during the post-parse `fill_heredoc_bodies` walk.
2824/// This method is the AST-side equivalent: writes back to the
2825/// matching redir node by index.
2826/// Port of `setheredoc(int pc, int type, char *str, char *termstr,
2827/// char *munged_termstr)` from `Src/parse.c:2347-2355`. Patches the
2828/// pending heredoc redir at `pc` with its body string + raw and
2829/// munged terminator forms.
2830pub fn setheredoc(pc: usize, redir_type: i32, doc: &str, term: &str, munged_term: &str) {
2831 // zshrs-only guard: AST-path heredocs use `pc = -1 as usize`
2832 // (i.e. `usize::MAX`) as a sentinel meaning "no wordcode slot to
2833 // patch". C never passes a negative pc since the wordcode emitter
2834 // is always active. Skip silently for the AST-only case.
2835 if pc == usize::MAX {
2836 return;
2837 }
2838 // c:2350 — `int varid = WC_REDIR_VARID(ecbuf[pc]) ? REDIR_VARID_MASK : 0;`
2839 let cur = ECBUF.with_borrow(|b| b.get(pc).copied().unwrap_or(0));
2840 let varid = if WC_REDIR_VARID(cur) != 0 {
2841 REDIR_VARID_MASK
2842 } else {
2843 0
2844 };
2845 // c:2351 — `ecbuf[pc] = WCB_REDIR(type | REDIR_FROM_HEREDOC_MASK | varid);`
2846 let new_header = WCB_REDIR((redir_type | REDIR_FROM_HEREDOC_MASK | varid) as wordcode);
2847 // c:2352 — `ecbuf[pc + 2] = ecstrcode(str);`
2848 let coded_str = ecstrcode(doc);
2849 // c:2353 — `ecbuf[pc + 3] = ecstrcode(termstr);`
2850 let coded_term = ecstrcode(term);
2851 // c:2354 — `ecbuf[pc + 4] = ecstrcode(munged_termstr);`
2852 let coded_munged = ecstrcode(munged_term);
2853 ECBUF.with_borrow_mut(|b| {
2854 b[pc] = new_header;
2855 b[pc + 2] = coded_str;
2856 b[pc + 3] = coded_term;
2857 b[pc + 4] = coded_munged;
2858 });
2859}
2860
2861/// Parse a wordlist for `for ... in WORDS;`. Direct port of
2862/// zsh/Src/parse.c:2362 `par_wordlist`. Reads STRING tokens
2863/// until the next SEPER / SEMI / NEWLIN.
2864pub fn par_wordlist() -> Vec<String> {
2865 let mut out = Vec::new();
2866 // parse.c:2362-2378 — collect STRINGs into the wordlist.
2867 while tok() == STRING_LEX {
2868 if let Some(text) = tokstr() {
2869 out.push(text);
2870 }
2871 zshlex();
2872 }
2873 out
2874}
2875
2876/// Parse a newline-separated wordlist. Direct port of
2877/// zsh/Src/parse.c:2379 `par_nl_wordlist`. Like
2878/// par_wordlist but tolerates leading/trailing newlines.
2879pub fn par_nl_wordlist() -> Vec<String> {
2880 // parse.c:2380-2381 — skip leading newlines.
2881 while tok() == NEWLIN {
2882 zshlex();
2883 }
2884 let out = par_wordlist();
2885 // parse.c:2395-2397 — skip trailing newlines.
2886 while tok() == NEWLIN {
2887 zshlex();
2888 }
2889 out
2890}
2891
2892/// `COND_SEP()` macro from `Src/parse.c:2433`. True when the current
2893/// token is a separator usable inside `[[ … ]]` (newline / semi /
2894/// `&`). C uses it to skip optional whitespace between cond terms.
2895#[inline]
2896pub fn COND_SEP() -> bool {
2897 matches!(tok(), NEWLIN | SEMI | AMPER)
2898}
2899
2900/// Parse [[ ... ]] conditional
2901/// Parse `[[ EXPR ]]` conditional expression. Direct port of
2902/// zsh/Src/parse.c:2409 `par_cond` (and helpers par_cond_1,
2903/// par_cond_2, par_cond_double, par_cond_triple, par_cond_multi
2904/// at parse.c:2434-2731). Expression operators: `||` `&&` `!`
2905/// + unary tests (-f, -d, -n, -z, etc.) + binary tests (=, !=,
2906/// <, >, ==, =~, -eq, -ne, -lt, -le, -gt, -ge, -nt, -ot, -ef).
2907fn par_cond() -> Option<ZshCommand> {
2908 // C par_dinbrack (parse.c:1810-1822) wraps the body parse with
2909 // `incond = 1; incmdpos = 0;` BEFORE the first zshlex past `[[`,
2910 // and resets to `incond = 0; incmdpos = 1;` after `]]`. Without
2911 // `incond = 1`, lex.c does not promote `]]` to DOUTBRACK and the
2912 // cond body bleeds past the close bracket — the parser then
2913 // sees `]]` as a separate STRING command. Every `if [[ ... ]]; then`
2914 // failed with `command not found: ]]` before this fix.
2915 set_incond(1);
2916 set_incmdpos(false);
2917 zshlex(); // skip [[
2918 // Empty cond `[[ ]]` is a parse error in zsh — emit the
2919 // diagnostic and return None so the caller produces a
2920 // non-zero exit. Without this, `[[ ]]` silently passed and
2921 // returned exit 0.
2922 if tok() == DOUTBRACK {
2923 zerr("parse error near `]]'");
2924 set_incond(0);
2925 set_incmdpos(true);
2926 zshlex();
2927 return None;
2928 }
2929 let cond = parse_cond_expr();
2930
2931 if tok() == DOUTBRACK {
2932 set_incond(0);
2933 set_incmdpos(true);
2934 zshlex();
2935 } else {
2936 // c:Src/parse.c:1818-1819 — `if (tok != DOUTBRACK)
2937 // YYERRORV(oecused);`. par_dinbrack hard-requires DOUTBRACK
2938 // after par_cond; anything else is a parse error and the
2939 // outer parser's yyerror at c:2747 emits `parse error near
2940 // \`%s'` using zshlextext. Bug #473: BAR (`|`) inside
2941 // `[[ ab == a|b ]]` slipped past par_cond_or (which only
2942 // checks DBAR), the cond returned cleanly, and then the
2943 // top-level parser interpreted BAR as a pipe — running `b`
2944 // as a command (security-relevant if pattern RHS is user
2945 // input). Mirror C: emit parse error and abort.
2946 let tok_text = match tok() {
2947 BAR_TOK => "|".to_string(),
2948 DBAR => "||".to_string(),
2949 AMPER => "&".to_string(),
2950 DAMPER => "&&".to_string(),
2951 SEMI => ";".to_string(),
2952 DSEMI => ";;".to_string(),
2953 NEWLIN | SEPER => String::new(),
2954 _ => tokstr().map(|s| crate::ported::lex::untokenize(&s)).unwrap_or_default(),
2955 };
2956 if tok_text.is_empty() {
2957 zerr("parse error");
2958 } else {
2959 zerr(&format!("parse error near `{}'", tok_text));
2960 }
2961 set_incond(0);
2962 set_incmdpos(true);
2963 return None;
2964 }
2965
2966 cond.map(ZshCommand::Cond)
2967}
2968
2969/// Port of `par_cond_1(void)` from `Src/parse.c:2434`. Parses one
2970/// `||`-separated cond expression. Emits `WCB_COND(COND_AND, …)`
2971/// when an `&&` is found and recurses.
2972pub fn par_cond_1() -> i32 {
2973 // c:2434
2974
2975 let p = ECUSED.with(|c| c.get()) as usize;
2976 let r = par_cond_2();
2977 while COND_SEP() {
2978 condlex();
2979 }
2980 if tok() == DAMPER {
2981 condlex();
2982 while COND_SEP() {
2983 condlex();
2984 }
2985 ecispace(p, 1);
2986 par_cond_1();
2987 let ecused = ECUSED.with(|c| c.get()) as usize;
2988 ECBUF.with(|c| {
2989 c.borrow_mut()[p] = WCB_COND(COND_AND as u32, (ecused - 1 - p) as u32);
2990 });
2991 return 1;
2992 }
2993 r
2994}
2995
2996/// Port of `par_cond_2(void)` from `Src/parse.c:2476`. The heavy
2997/// cond-term parser: handles `! cond`, `(cond)`, unary `[ -X arg ]`,
2998/// binary `[ A op B ]`, and `[ A op1 B op2 C … ]` n-ary chains.
2999pub fn par_cond_2() -> i32 {
3000 // c:2476
3001 // `n_testargs` only applies in `testlex` mode (=== /bin/test
3002 // compat). zshrs has no testlex yet, so always 0.
3003 let n_testargs: i32 = 0;
3004
3005 // c:2481 — handled inline; this Rust port skips the n_testargs
3006 // arm since zshrs invokes par_cond via [[ ... ]] only.
3007
3008 while COND_SEP() {
3009 condlex();
3010 }
3011 if tok() == BANG_TOK {
3012 // c:2522 — `[[ ! cond ]]`
3013 condlex();
3014 ecadd(WCB_COND(COND_NOT as u32, 0));
3015 return par_cond_2();
3016 }
3017 if tok() == INPAR_TOK {
3018 // c:2533 — `[[ (cond) ]]`
3019 condlex();
3020 while COND_SEP() {
3021 condlex();
3022 }
3023 let r = par_cond();
3024 while COND_SEP() {
3025 condlex();
3026 }
3027 if tok() != OUTPAR_TOK {
3028 crate::ported::utils::zerr("missing )");
3029 yyerror(0);
3030 return 0;
3031 }
3032 condlex();
3033 return r.map_or(0, |_| 1);
3034 }
3035 let s1 = tokstr().unwrap_or_default();
3036 // c:2549 — `dble = (s1 && IS_DASH(*s1) && (!n_testargs ||
3037 // strspn(s1+1, "abcd...") == 1) && !s1[2]);` — IS_DASH covers
3038 // BOTH `-` and Dash (`\u{9b}`). The raw tokstr inside `[[ ... ]]`
3039 // carries Dash as a marker byte, so `starts_with('-')` alone
3040 // matches only ASCII dashes and misses every `-z`, `-d`, `-r`
3041 // etc. — every such cond emitted the AST-only `condition
3042 // expected` error from par_cond_double. Use IS_DASH and count
3043 // chars (Dash is a single code point) instead of bytes.
3044 let s1_chars: Vec<char> = s1.chars().collect();
3045 let dble = !s1_chars.is_empty()
3046 && IS_DASH(s1_chars[0])
3047 && s1_chars.len() == 2
3048 && "abcdefghknoprstuvwxzLONGS".contains(s1_chars[1]);
3049 if tok() != STRING_LEX {
3050 if !s1.is_empty() && tok() != LEXERR && (!dble || n_testargs != 0) {
3051 // c:2486-2497 — `if (n_testargs == 1)` block: under
3052 // POSIXBUILTINS-off, `[ -t ]` rewrites to `[ -t 1 ]`
3053 // (ksh behavior). The C gate is `unset(POSIXBUILTINS)
3054 // && check_cond(s1, "t")`. zshrs's parser has
3055 // n_testargs=0 (no testlex), so this rewrite path is
3056 // unreachable from zshrs's [[ ]] / [ ] entry points;
3057 // wired here as a marker for parity. When testlex is
3058 // ported the call below activates.
3059 if n_testargs == 1 && unset(POSIXBUILTINS) && check_cond(&s1, "t") {
3060 condlex();
3061 return par_cond_double(&s1, "1");
3062 }
3063 // c:2557 — `[[ STRING ]]` re-interpreted as `[[ -n STRING ]]`.
3064 condlex();
3065 while COND_SEP() {
3066 condlex();
3067 }
3068 return par_cond_double("-n", &s1);
3069 }
3070 crate::ported::utils::zerr("condition expected");
3071 yyerror(0);
3072 return 0;
3073 }
3074 condlex();
3075 while COND_SEP() {
3076 condlex();
3077 }
3078 if tok() == INANG_TOK || tok() == OUTANG_TOK {
3079 // c:2576 — `<` / `>` string compare.
3080 let xtok = tok();
3081 condlex();
3082 while COND_SEP() {
3083 condlex();
3084 }
3085 if tok() != STRING_LEX {
3086 crate::ported::utils::zerr("string expected");
3087 yyerror(0);
3088 return 0;
3089 }
3090 let s3 = tokstr().unwrap_or_default();
3091 condlex();
3092 while COND_SEP() {
3093 condlex();
3094 }
3095 let op = if xtok == INANG_TOK {
3096 COND_STRLT
3097 } else {
3098 COND_STRGTR
3099 };
3100 ecadd(WCB_COND(op as u32, 0));
3101 ecstr(&s1);
3102 ecstr(&s3);
3103 return 1;
3104 }
3105 if tok() != STRING_LEX {
3106 // c:2592 — only one operand seen → `[ -n s1 ]`.
3107 if tok() != LEXERR {
3108 if !dble || n_testargs != 0 {
3109 return par_cond_double("-n", &s1);
3110 }
3111 return par_cond_multi(&s1, &[]);
3112 }
3113 crate::ported::utils::zerr("syntax error");
3114 yyerror(0);
3115 return 0;
3116 }
3117 let s2 = tokstr().unwrap_or_default();
3118 set_incond(incond() + 1);
3119 condlex();
3120 while COND_SEP() {
3121 condlex();
3122 }
3123 set_incond(incond() - 1);
3124 // c:Src/parse.c:2598-2600 — `if (!n_testargs) dble = (s2 &&
3125 // IS_DASH(*s2) && !s2[2]);` — RECOMPUTE dble based on s2 once
3126 // it's been read, so `[[ A -X B ]]` is treated as a 2-arg cond
3127 // `[ -X B ]` (par_cond_double) rather than a 3-arg triple. This
3128 // is what routes `[[ "" -a "x" ]]` to par_cond_double("", "-a")
3129 // → COND_ERROR "parse error: condition expected: ". Without
3130 // this, the original `dble` from s1 stayed false, the parser
3131 // grabbed s3 and built COND_MODI silently. parity bug #25.
3132 let s2_chars: Vec<char> = s2.chars().collect();
3133 let dble = !s2_chars.is_empty() && IS_DASH(s2_chars[0]) && s2_chars.len() == 2;
3134 if tok() == STRING_LEX && !dble {
3135 let s3 = tokstr().unwrap_or_default();
3136 condlex();
3137 while COND_SEP() {
3138 condlex();
3139 }
3140 if tok() == STRING_LEX {
3141 // c:2615 — n-ary `[ A op B C D ... ]`.
3142 let mut l: Vec<String> = vec![s2, s3];
3143 while tok() == STRING_LEX {
3144 l.push(tokstr().unwrap_or_default());
3145 condlex();
3146 while COND_SEP() {
3147 condlex();
3148 }
3149 }
3150 return par_cond_multi(&s1, &l);
3151 }
3152 return par_cond_triple(&s1, &s2, &s3);
3153 }
3154 par_cond_double(&s1, &s2)
3155}
3156
3157/// Port of `par_cond_double(char *a, char *b)` from `Src/parse.c:2626`.
3158/// Emits wordcode for unary cond `[ -X b ]` or modular `[ -mod b ]`.
3159pub fn par_cond_double(a: &str, b: &str) -> i32 {
3160 // c:2628 — `if (!IS_DASH(a[0]) || !a[1])` — char-based, since
3161 // Dash is a single code point (`\u{9b}`) and `a.len() < 2` on
3162 // BYTES would still pass for "-z" but fail for the marker form
3163 // `\u{9b}z` (2 bytes). Walk by chars.
3164 let ac: Vec<char> = a.chars().collect();
3165 if ac.is_empty() || !IS_DASH(ac[0]) || ac.len() < 2 {
3166 // c:Src/parse.c:2629 COND_ERROR macro expansion:
3167 // zwarn(...); herrflush(); errflag |= ERRFLAG_ERROR;
3168 // YYERROR(ecused) /* sets tok = LEXERR */
3169 // The YYERROR portion is critical — without it the outer
3170 // parser keeps walking the wordcode and execution proceeds
3171 // (e.g. `[[ "" -a "x" ]] && echo m || echo n` runs the
3172 // `|| echo n` branch). Setting LEXERR aborts the upper
3173 // parse so the whole line is rejected, matching zsh's
3174 // observable behavior of stdout="" on parse error.
3175 zerr(&format!("parse error: condition expected: {}", a));
3176 errflag.fetch_or(crate::ported::zsh_h::ERRFLAG_ERROR, Ordering::SeqCst);
3177 set_tok(LEXERR);
3178 return 1;
3179 }
3180 // c:2630 — `else if (!a[2] && strspn(a+1, "abcd...zhLONGS") == 1)`
3181 let unary_set = "abcdefgknoprstuvwxzhLONGS";
3182 if ac.len() == 2 && unary_set.contains(ac[1]) {
3183 // c:2631 — `ecadd(WCB_COND(a[1], 0));` uses the raw cond-op
3184 // letter byte as the opcode payload. Use the ASCII char's
3185 // code-point value directly — every letter in `unary_set`
3186 // fits in 7 bits.
3187 ecadd(WCB_COND(ac[1] as u32, 0));
3188 ecstr(b);
3189 } else {
3190 ecadd(WCB_COND(COND_MOD as u32, 1));
3191 ecstr(a);
3192 ecstr(b);
3193 }
3194 1
3195}
3196
3197/// Port of `get_cond_num(char *tst)` from `Src/parse.c:2643`. Returns
3198/// the index of `tst` in `{"nt","ot","ef","eq","ne","lt","gt","le","ge"}`
3199/// or `-1` if not a recognized binary cond operator.
3200pub fn get_cond_num(tst: &str) -> i32 {
3201 // c:2643
3202 const CONDSTRS: [&str; 9] = [
3203 "nt", "ot", "ef", "eq", "ne", "lt", "gt", "le", "ge", // c:2647
3204 ];
3205 for (i, &c) in CONDSTRS.iter().enumerate() {
3206 if c == tst {
3207 return i as i32; // c:2654
3208 }
3209 }
3210 -1 // c:2656
3211}
3212
3213/// par_time's `static int inpartime` guard at C parse.c:1038
3214/// preventing infinite recursion on `time time foo`. The wordcode
3215/// path keeps this as a thread_local since C uses a function-level
3216/// `static int` (per-process; per-evaluator semantically matches).
3217thread_local! {
3218 static PARSER_INPARTIME: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
3219}
3220
3221/// Port of `par_cond_triple(char *a, char *b, char *c)` from
3222/// `Src/parse.c:2659`. Emits wordcode for the binary forms
3223/// `[ A op B ]` — `=` / `==` / `!=` / `<` / `>` / `=~` / `-X`.
3224///
3225/// C does `(b[0] == Equals || b[0] == '=')` etc., matching BOTH the
3226/// raw ASCII operator char AND its tokenized marker form per
3227/// `Src/zsh.h:159-194`:
3228/// Equals = `\u{8d}`, Outang = `\u{95}`, Inang = `\u{94}`,
3229/// Tilde = `\u{98}`, Bang = `\u{9c}`, Dash = `\u{9b}`.
3230/// Inside `[[ ... ]]` the lexer emits the marker bytes — comparing
3231/// against literal-only `b"=="` misses every cond op.
3232/// (The previous Rust port had the doc comment values wrong:
3233/// Outang=0x8e was actually Bar; Inang=0x91 was Inbrack;
3234/// Tilde=0x96 was OutangProc; Bang=0x8b was Outparmath. The code
3235/// itself uses the correct const names, so this was a docs-only fix.)
3236pub fn par_cond_triple(a: &str, b: &str, c: &str) -> i32 {
3237 // c:2659
3238 let bc: Vec<char> = b.chars().collect();
3239 let is_eq = |ch: char| ch == '=' || ch == Equals;
3240 let is_gt = |ch: char| ch == '>' || ch == Outang;
3241 let is_lt = |ch: char| ch == '<' || ch == Inang;
3242 let is_tilde = |ch: char| ch == '~' || ch == Tilde;
3243 let is_bang = |ch: char| ch == '!' || ch == Bang;
3244
3245 // c:2663 — `(b[0] == Equals || b[0] == '=') && !b[1]` → `=` (single).
3246 if bc.len() == 1 && is_eq(bc[0]) {
3247 ecadd(WCB_COND(COND_STREQ as u32, 0));
3248 ecstr(a);
3249 ecstr(c);
3250 let np = ECNPATS.with(|cc| {
3251 let v = cc.get();
3252 cc.set(v + 1);
3253 v
3254 }) as u32;
3255 ecadd(np);
3256 return 1;
3257 }
3258 // c:2668-2673 — `(t0 = b[0]=='>' || Outang) || b[0]=='<' || Inang`.
3259 if bc.len() == 1 && (is_gt(bc[0]) || is_lt(bc[0])) {
3260 let op = if is_gt(bc[0]) {
3261 COND_STRGTR
3262 } else {
3263 COND_STRLT
3264 };
3265 ecadd(WCB_COND(op as u32, 0));
3266 ecstr(a);
3267 ecstr(c);
3268 let np = ECNPATS.with(|cc| {
3269 let v = cc.get();
3270 cc.set(v + 1);
3271 v
3272 }) as u32;
3273 ecadd(np);
3274 return 1;
3275 }
3276 // c:2674-2679 — `==` STRDEQ.
3277 if bc.len() == 2 && is_eq(bc[0]) && is_eq(bc[1]) {
3278 ecadd(WCB_COND(COND_STRDEQ as u32, 0));
3279 ecstr(a);
3280 ecstr(c);
3281 let np = ECNPATS.with(|cc| {
3282 let v = cc.get();
3283 cc.set(v + 1);
3284 v
3285 }) as u32;
3286 ecadd(np);
3287 return 1;
3288 }
3289 // c:2680-2684 — `!=` STRNEQ.
3290 if bc.len() == 2 && is_bang(bc[0]) && is_eq(bc[1]) {
3291 ecadd(WCB_COND(COND_STRNEQ as u32, 0));
3292 ecstr(a);
3293 ecstr(c);
3294 let np = ECNPATS.with(|cc| {
3295 let v = cc.get();
3296 cc.set(v + 1);
3297 v
3298 }) as u32;
3299 ecadd(np);
3300 return 1;
3301 }
3302 // c:2685-2691 — `=~` REGEX (no pattern slot — implicit COND_MODI).
3303 if bc.len() == 2 && is_eq(bc[0]) && is_tilde(bc[1]) {
3304 ecadd(WCB_COND(COND_REGEX as u32, 0));
3305 ecstr(a);
3306 ecstr(c);
3307 return 1;
3308 }
3309 // c:2692-2702 — `-OP` numeric-or-modular cond (e.g. `-eq`, `-nt`).
3310 if !bc.is_empty() && IS_DASH(bc[0]) {
3311 let rest: String = bc[1..].iter().collect();
3312 let t = get_cond_num(&rest);
3313 if t > -1 {
3314 ecadd(WCB_COND((t + COND_NT) as u32, 0));
3315 ecstr(a);
3316 ecstr(c);
3317 return 1;
3318 }
3319 ecadd(WCB_COND(COND_MODI as u32, 0));
3320 ecstr(b);
3321 ecstr(a);
3322 ecstr(c);
3323 return 1;
3324 }
3325 // c:2703-2707 — `-mod A B C` modular cond on `a`.
3326 let ac: Vec<char> = a.chars().collect();
3327 if !ac.is_empty() && IS_DASH(ac[0]) && ac.len() > 1 {
3328 ecadd(WCB_COND(COND_MOD as u32, 2));
3329 ecstr(a);
3330 ecstr(b);
3331 ecstr(c);
3332 return 1;
3333 }
3334 zerr(&format!("condition expected: {}", b));
3335 1
3336}
3337
3338/// Port of `par_cond_multi(char *a, LinkList l)` from `Src/parse.c:2716`.
3339/// Emits wordcode for `[ -OP A B C … ]` n-ary cond (alternation).
3340pub fn par_cond_multi(a: &str, l: &[String]) -> i32 {
3341 // c:2716 — `if (!IS_DASH(a[0]) || !a[1])`; same Dash/`-` dual
3342 // matching as par_cond_double, char-walked because Dash is a
3343 // single code point.
3344 let ac: Vec<char> = a.chars().collect();
3345 if ac.is_empty() || !IS_DASH(ac[0]) || ac.len() < 2 {
3346 zerr(&format!("condition expected: {}", a));
3347 return 1;
3348 }
3349 ecadd(WCB_COND(COND_MOD as u32, l.len() as u32));
3350 ecstr(a);
3351 for item in l {
3352 ecstr(item);
3353 }
3354 1
3355}
3356
3357/// Emit a parser-level error. Direct port of zsh/Src/parse.c
3358/// 2733-2766 `yyerror`. C version fills a per-event error buffer
3359/// and sets errflag. zshrs pushes onto errors which the
3360/// caller drains via parse()'s Result return.
3361/// WARNING: param-name divergence — Rust takes `&str message`, C takes
3362/// Port of `static void yyerror(int noerr)` from `Src/parse.c:2733`.
3363///
3364/// Faithful C body (verbatim):
3365/// ```c
3366/// int t0; char *t;
3367/// if ((t = dupstring(zshlextext))) untokenize(t);
3368/// for (t0 = 0; t0 != 20; t0++)
3369/// if (!t || !t[t0] || t[t0] == '\n') break;
3370/// if (!(histdone & HISTFLAG_NOEXEC) && !(errflag & ERRFLAG_INT)) {
3371/// if (t0) {
3372/// t = metafy(t, t0, META_STATIC);
3373/// zwarn("parse error near `%s%s'", t, t0 == 20 ? "..." : "");
3374/// } else
3375/// zwarn("parse error");
3376/// }
3377/// if (!noerr && noerrs != 2)
3378/// errflag |= ERRFLAG_ERROR;
3379/// ```
3380///
3381/// `zshlextext` is the C lexer's current-token text (`Src/lex.c:170`
3382/// `char *tokstr`); zshrs's equivalent is `lex::tokstr()`. The "20"
3383/// is C's tail-truncation length for the error message.
3384pub fn yyerror(noerr: i32) {
3385 // c:2733
3386 // c:2738 — `if ((t = dupstring(zshlextext))) untokenize(t);`.
3387 // In C, `zshlextext` falls back to `tokstrings[tok]` (lex.c:1965)
3388 // for punctuation tokens that didn't capture a tokstr — that's
3389 // how "parse error near `)'" gets the `)` for OUTPAR. Mirror by
3390 // consulting `lex::tokstring(tok())` when the captured tokstr is
3391 // None.
3392 let t_opt: Option<String> = match crate::ported::lex::tokstr() {
3393 Some(raw) => Some(crate::ported::lex::untokenize(&raw).to_string()),
3394 None => {
3395 let t = crate::ported::lex::tok();
3396 let i = t as usize;
3397 if i < crate::ported::lex::tokstrings.len() {
3398 crate::ported::lex::tokstrings[i].map(|s| s.to_string())
3399 } else {
3400 None
3401 }
3402 }
3403 };
3404 let t_bytes: Vec<u8> = t_opt
3405 .as_ref()
3406 .map(|s| s.as_bytes().to_vec())
3407 .unwrap_or_default();
3408
3409 // c:2741-2743 — `for (t0 = 0; t0 != 20; t0++) if (!t || !t[t0]
3410 // || t[t0] == '\n') break;`
3411 let mut t0: usize = 0;
3412 while t0 != 20 {
3413 // c:2741
3414 let stop = t_opt.is_none()
3415 || t0 >= t_bytes.len()
3416 || t_bytes[t0] == 0
3417 || t_bytes[t0] == b'\n';
3418 if stop {
3419 break;
3420 }
3421 t0 += 1;
3422 }
3423
3424 // c:2744 — `if (!(histdone & HISTFLAG_NOEXEC) && !(errflag &
3425 // ERRFLAG_INT))`. The HISTFLAG_NOEXEC gate suppresses warnings
3426 // from history-recall paths that aren't actually executing.
3427 let histdone_v = crate::ported::hist::histdone.load(Ordering::SeqCst);
3428 let hist_noexec =
3429 (histdone_v & crate::ported::zsh_h::HISTFLAG_NOEXEC as i32) != 0;
3430 let int_flagged =
3431 (errflag.load(Ordering::SeqCst) & crate::ported::zsh_h::ERRFLAG_INT) != 0;
3432 if !hist_noexec && !int_flagged {
3433 // c:2744
3434 if t0 != 0 {
3435 // c:2745
3436 // c:2746 — `t = metafy(t, t0, META_STATIC);` — re-metafy
3437 // the truncated head so embedded Meta bytes display
3438 // correctly. The Rust port already holds an untokenized
3439 // string; use the byte slice [0..t0] directly.
3440 let head =
3441 std::str::from_utf8(&t_bytes[..t0]).unwrap_or_default();
3442 let suffix = if t0 == 20 { "..." } else { "" };
3443 crate::ported::utils::zwarn(&format!(
3444 "parse error near `{}{}'",
3445 head, suffix
3446 )); // c:2747
3447 } else {
3448 // c:2748
3449 crate::ported::utils::zwarn("parse error"); // c:2749
3450 }
3451 }
3452 // c:2751 — `if (!noerr && noerrs != 2) errflag |= ERRFLAG_ERROR;`.
3453 // The `noerrs != 2` gate (suppress-only-fatal-errors) is preserved
3454 // for parity with zerr/zwarn's matching check.
3455 let noerrs_v = *crate::ported::utils::noerrs_lock().lock().unwrap();
3456 if noerr == 0 && noerrs_v != 2 {
3457 // c:2751
3458 errflag.fetch_or(
3459 crate::ported::zsh_h::ERRFLAG_ERROR,
3460 Ordering::SeqCst,
3461 ); // c:2752
3462 }
3463}
3464
3465
3466// ============================================================
3467// Eprog runtime ops (parse.c:2767-2853)
3468//
3469// dupeprog / useeprog / freeeprog are zsh's reference-counting
3470// helpers for executable programs. zshrs's AST is owned by
3471// value (Rust ownership); cloning is a tree-deep copy via
3472// Clone, "use" is a no-op (the executor borrows the AST), and
3473// "free" is automatic on drop.
3474// ============================================================
3475
3476/// Duplicate an Eprog. Direct port of zsh/Src/parse.c:2813
3477/// Port of `Eprog dupeprog(Eprog p, int heap)` from
3478/// `Src/parse.c:2767`. Deep-copies the wordcode array, string
3479/// table, and pattern-prog slots. `dummy_eprog` is returned
3480/// unchanged. `heap`-allocated copies get `nref = -1` (never
3481/// freed); real ones get `nref = 1`.
3482pub fn dupeprog(p: &eprog, heap: bool) -> eprog {
3483 // c:2774-2775 — `if (p == &dummy_eprog) return p;` — caller-
3484 // observable identity in C uses a pointer compare; Rust's
3485 // equivalent is "if it has the dummy's shape (single WCB_END
3486 // word and no strs), return a copy of the same shape".
3487 // c:2796-2797 — `for (i = r->npats; i--; pp++) *pp = dummy_patprog1;`
3488 // C uses `dummy_patprog1` as a placeholder; the Rust port has
3489 // `Vec<Patprog>` (Box<patprog>) — synthesize an equivalent zero-
3490 // initialized patprog for each slot (resolved later by
3491 // pattern.c::patcompile-on-first-use).
3492 let dummy_pat = || crate::ported::zsh_h::patprog {
3493 startoff: 0,
3494 size: 0,
3495 mustoff: 0,
3496 patmlen: 0,
3497 globflags: 0,
3498 globend: 0,
3499 flags: 0,
3500 patnpar: 0,
3501 patstartch: 0,
3502 };
3503 let r = eprog {
3504 // c:2778 — `flags = (heap ? EF_HEAP : EF_REAL) | (p->flags & EF_RUN);`
3505 flags: (if heap { EF_HEAP } else { EF_REAL }) | (p.flags & EF_RUN),
3506 len: p.len,
3507 npats: p.npats,
3508 // c:2787 — `nref = heap ? -1 : 1;`
3509 nref: if heap { -1 } else { 1 },
3510 prog: p.prog.clone(),
3511 strs: p.strs.clone(),
3512 pats: (0..p.npats).map(|_| Box::new(dummy_pat())).collect(),
3513 shf: None,
3514 dump: None,
3515 };
3516 r
3517}
3518
3519/// Port of `void useeprog(Eprog p)` from `Src/parse.c:2813`.
3520/// `if (p && p != &dummy_eprog && p->nref >= 0) p->nref++;` —
3521/// pin a real (non-heap, non-dummy) Eprog so it survives the
3522/// next `freeeprog`.
3523pub fn useeprog(p: &mut eprog) {
3524 // c:2815 — `if (p && p != &dummy_eprog && p->nref >= 0)`
3525 if p.nref >= 0 {
3526 p.nref += 1; // c:2816
3527 }
3528}
3529
3530/// Port of `void freeeprog(Eprog p)` from `Src/parse.c:2823`.
3531/// Refcount-decrement; when it hits zero, drops the pattern progs,
3532/// decrements the dump refcount if any, and releases the eprog.
3533/// `dummy_eprog` is never freed. Heap-eprogs (`nref < 0`) are
3534/// never freed either — they live as long as the heap arena.
3535pub fn freeeprog(p: &mut eprog) {
3536 // c:2829 — `if (p && p != &dummy_eprog) { ... }`
3537 if p.nref > 0 {
3538 p.nref -= 1; // c:2832
3539 if p.nref == 0 {
3540 // c:2833-2840 — drop pats, dump refcount, then the eprog.
3541 // Rust's Drop handles the per-field cleanup; we just
3542 // need to decrement the dump count first.
3543 if let Some(dump) = p.dump.take() {
3544 let dumped = (*dump).clone();
3545 decrdumpcount(&dumped); // c:2837
3546 }
3547 p.prog.clear();
3548 p.strs = None;
3549 p.pats.clear();
3550 }
3551 }
3552}
3553
3554// =============================================================================
3555// Wordcode read helpers — used by text.rs's `gettext2` and exec dispatch
3556// to walk a compiled Eprog without re-running the parser. These are the
3557// only `Src/parse.c` functions ported so far in this file; the recursive-
3558// descent parser (par_event / par_list / par_cmd / par_*) follows
3559// below as free ported at module scope.
3560// =============================================================================
3561
3562/// Port of `ecgetstr(Estate s, int dup, int *tokflag)` from `Src/parse.c:2855`.
3563/// `s->pc` advances through the wordcode buffer; `s->strs` indexes the
3564/// string pool. Returns the interned string (or a 1-3-char literal
3565/// inlined directly into the wordcode word).
3566pub fn ecgetstr(s: &mut estate, dup: i32, tokflag: Option<&mut i32>) -> String {
3567 let prog = &s.prog.prog;
3568 if s.pc >= prog.len() {
3569 return String::new();
3570 }
3571 let c = prog[s.pc]; // c:2858 `wordcode c = *s->pc++;`
3572 s.pc += 1;
3573 if let Some(tf) = tokflag {
3574 *tf = i32::from((c & 1) != 0); // c:2880 `*tokflag = (c & 1);`
3575 }
3576 if c == 6 || c == 7 {
3577 // c:2861 `if (c == 6 || c == 7) r = "";`
3578 return String::new();
3579 }
3580 let r: String = if (c & 2) != 0 {
3581 // c:2862 — `else if (c & 2)`
3582 // c:2863-2868 — 3-byte inline string packed into the wordcode
3583 // word; followed by `buf[3] = '\0'; r = dupstring(buf);`.
3584 // C's `dupstring` uses `strlen(buf)` which TRUNCATES at the
3585 // first NUL byte — short strings of 1 or 2 chars get padded
3586 // with NULs and truncated cleanly. The previous Rust port
3587 // used `retain(|&x| x != 0)` which would silently SPLICE OUT
3588 // an interior NUL (e.g. `[a, 0, b]` → "ab"), diverging from
3589 // C's strlen-truncate (`[a, 0, b]` → "a"). Fix: truncate at
3590 // first NUL to match C exactly.
3591 let b0 = ((c >> 3) & 0xff) as u8;
3592 let b1 = ((c >> 11) & 0xff) as u8;
3593 let b2 = ((c >> 19) & 0xff) as u8;
3594 let v = [b0, b1, b2];
3595 let end = v.iter().position(|&x| x == 0).unwrap_or(v.len()); // c:2869 strlen(buf)
3596 // C reads raw bytes (token codes included) — widen via the
3597 // wordcode-pool bridge, never from_utf8_lossy (which mangles
3598 // raw token bytes from C-zsh-written .zwc dumps to U+FFFD).
3599 crate::zwc::wordcode_pool_str(&v[..end])
3600 } else {
3601 // c:2877 `else r = s->strs + (c >> 2);`
3602 let off = (c >> 2) as usize + s.strs_offset;
3603 let strs_bytes = s.strs.as_deref().unwrap_or("").as_bytes();
3604 if off >= strs_bytes.len() {
3605 String::new()
3606 } else {
3607 let tail = &strs_bytes[off..];
3608 let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
3609 crate::zwc::wordcode_pool_str(&tail[..end])
3610 }
3611 };
3612 // c:2891 `return ((dup == EC_DUP || (dup && (c & 1))) ? dupstring(r) : r);`
3613 // Rust owns the String already; `dup` flag has no observable effect.
3614 let _ = (dup, EC_DUP, EC_NODUP);
3615 r
3616}
3617
3618// ============================================================
3619// Wordcode runtime getters (parse.c:2853-3060)
3620//
3621// Direct ports of the wordcode-read helpers (ecrawstr,
3622// ecgetstr, ecgetarr, ecgetredirs, ecgetlist, eccopyredirs).
3623// Read packed wordcode out of an Eprog at execution time.
3624// Used by exec_wordcode and the wordcode-walking dispatch in
3625// src/vm_helper.
3626// ============================================================
3627
3628/// Port of `ecrawstr(Eprog p, Wordcode pc, int *tokflag)` from
3629/// `Src/parse.c:2891`. Like `ecgetstr` but reads at the given pc
3630/// without advancing — caller steps `pc` separately.
3631pub fn ecrawstr(p: &eprog, pc: usize, tokflag: Option<&mut i32>) -> String {
3632 if pc >= p.prog.len() {
3633 return String::new();
3634 }
3635 let c = p.prog[pc]; // c:2894
3636 if let Some(tf) = tokflag {
3637 *tf = i32::from((c & 1) != 0); // c:2898/2906/2912
3638 }
3639 if c == 6 || c == 7 {
3640 // c:2897
3641 return String::new();
3642 }
3643 if (c & 2) != 0 {
3644 // c:2902-2906 — same 3-byte inline string as ecgetstr, then
3645 // `buf[3] = '\0'; return dupstring(buf);` — truncate at first
3646 // NUL via strlen (NOT splice out interior NULs).
3647 let b0 = ((c >> 3) & 0xff) as u8;
3648 let b1 = ((c >> 11) & 0xff) as u8;
3649 let b2 = ((c >> 19) & 0xff) as u8;
3650 let v = [b0, b1, b2];
3651 let end = v.iter().position(|&x| x == 0).unwrap_or(v.len()); // c:2906 strlen(buf)
3652 // Raw-byte widening — see ecgetstr (same C-convention bridge).
3653 crate::zwc::wordcode_pool_str(&v[..end])
3654 } else {
3655 // c:2911
3656 let off = (c >> 2) as usize;
3657 let strs_bytes = p.strs.as_deref().unwrap_or("").as_bytes();
3658 if off >= strs_bytes.len() {
3659 return String::new();
3660 }
3661 let tail = &strs_bytes[off..];
3662 let end = tail.iter().position(|&b| b == 0).unwrap_or(tail.len());
3663 crate::zwc::wordcode_pool_str(&tail[..end])
3664 }
3665}
3666
3667/// Port of `ecgetarr(Estate s, int num, int dup, int *tokflag)` from
3668/// `Src/parse.c:2917`. Reads `num` strings from wordcode at `s->pc`
3669/// and OR-folds each entry's token flag into `*tokflag`.
3670pub fn ecgetarr(s: &mut estate, num: usize, dup: i32, tokflag: Option<&mut i32>) -> Vec<String> {
3671 let mut ret: Vec<String> = Vec::with_capacity(num); // c:2922
3672 let mut tf: i32 = 0;
3673 for _ in 0..num {
3674 // c:2924 `while (num--)`
3675 let mut tmp = 0;
3676 ret.push(ecgetstr(s, dup, Some(&mut tmp))); // c:2925
3677 tf |= tmp; // c:2926
3678 }
3679 if let Some(out) = tokflag {
3680 // c:2929
3681 *out = tf;
3682 }
3683 ret
3684}
3685
3686/// Port of `ecgetlist(Estate s, int num, int dup, int *tokflag)` from
3687/// `Src/parse.c:2937`. Same shape as `ecgetarr` but C returns
3688/// `LinkList`; zshrs uses `Vec<String>` for both.
3689pub fn ecgetlist(s: &mut estate, num: usize, dup: i32, tokflag: Option<&mut i32>) -> Vec<String> {
3690 if num == 0 {
3691 // c:2949-2952
3692 if let Some(tf) = tokflag {
3693 *tf = 0;
3694 }
3695 return Vec::new();
3696 }
3697 ecgetarr(s, num, dup, tokflag)
3698}
3699
3700/// Port of `ecgetredirs(Estate s)` from `Src/parse.c:2959`.
3701///
3702/// `strs` must be the same tail `ecgetstr` uses (`s->strs` / `estate.strs` from offset).
3703/// WARNING: param names don't match C — Rust=(prog, strs, pc) vs C=(s)
3704pub fn ecgetredirs(s: &mut estate) -> Vec<redir> {
3705 let mut ret: Vec<redir> = Vec::new(); // c:2959 `LinkList ret = newlinklist();`
3706 let prog_len = s.prog.prog.len();
3707 if s.pc >= prog_len {
3708 return ret;
3709 }
3710 let mut code = s.prog.prog[s.pc]; // c:2962 `wordcode code = *s->pc++;`
3711 s.pc += 1;
3712
3713 loop {
3714 if wc_code(code) != WC_REDIR {
3715 // c:2988-2989 `s->pc--` then break from while
3716 s.pc = s.pc.saturating_sub(1);
3717 break;
3718 }
3719
3720 let typ = WC_REDIR_TYPE(code); // c:2967 `r->type = WC_REDIR_TYPE(code);`
3721 if s.pc >= prog_len {
3722 break;
3723 }
3724 let fd1_w = s.prog.prog[s.pc]; // c:2968 `r->fd1 = *s->pc++;`
3725 s.pc += 1;
3726
3727 let name = ecgetstr(s, EC_DUP, None); // c:2969 `r->name = ecgetstr(...)`
3728
3729 let (flags, here_terminator, munged_here_terminator) = if WC_REDIR_FROM_HEREDOC(code) != 0 {
3730 // c:2970-2973
3731 let term = ecgetstr(s, EC_DUP, None);
3732 let munged = ecgetstr(s, EC_DUP, None);
3733 (REDIRF_FROM_HEREDOC, Some(term), Some(munged))
3734 } else {
3735 // c:2974-2977
3736 (0, None, None)
3737 };
3738
3739 let varid = if WC_REDIR_VARID(code) != 0 {
3740 // c:2979-2980
3741 Some(ecgetstr(s, EC_DUP, None))
3742 } else {
3743 None // c:2981-2982
3744 };
3745
3746 ret.push(redir {
3747 // c:2965-2982 fields + c:2984 `addlinknode`
3748 typ,
3749 flags,
3750 fd1: fd1_w as i32,
3751 fd2: 0,
3752 name: Some(name),
3753 varid,
3754 here_terminator,
3755 munged_here_terminator,
3756 });
3757
3758 if s.pc >= prog_len {
3759 break;
3760 }
3761 code = s.prog.prog[s.pc]; // c:2986 `code = *s->pc++;`
3762 s.pc += 1;
3763 }
3764
3765 ret // c:2990 `return ret`
3766}
3767
3768/// Port of `eccopyredirs(Estate s)` from `Src/parse.c:3003`. Reads
3769/// the WC_REDIR run at `s->pc`, counts the wordcodes needed,
3770/// reserves space in `ecbuf` via `ecispace`, then re-walks `s->pc`
3771/// re-emitting each redir's wordcodes into the reserved slot —
3772/// finally calls `bld_eprog(0)` to package the result as an Eprog.
3773pub fn eccopyredirs(s: &mut estate) -> Option<eprog> {
3774 let prog_len = s.prog.prog.len();
3775 if s.pc >= prog_len {
3776 return None;
3777 }
3778 // c:3007-3009 — `if (wc_code(*pc) != WC_REDIR) return NULL;`
3779 let first_code = s.prog.prog[s.pc];
3780 if wc_code(first_code) != WC_REDIR {
3781 return None;
3782 }
3783 // c:3011 — `init_parse();`
3784 init_parse();
3785
3786 // c:3013-3027 — count wordcodes the redir run will need.
3787 // Each WC_REDIR contributes `code + fd1 + name` = 3, plus
3788 // `+2` if WC_REDIR_FROM_HEREDOC (terminator + munged), plus
3789 // `+1` if WC_REDIR_VARID.
3790 let mut probe = s.pc;
3791 let mut ncodes = 0usize;
3792 loop {
3793 if probe >= prog_len {
3794 break;
3795 }
3796 let code = s.prog.prog[probe];
3797 if wc_code(code) != WC_REDIR {
3798 break;
3799 }
3800 let mut ncode = if WC_REDIR_FROM_HEREDOC(code) != 0 {
3801 5
3802 } else {
3803 3
3804 };
3805 if WC_REDIR_VARID(code) != 0 {
3806 ncode += 1;
3807 }
3808 probe += ncode;
3809 ncodes += ncode;
3810 }
3811
3812 // c:3028-3029 — `r = ecused; ecispace(r, ncodes);`
3813 let r0 = ECUSED.get() as usize;
3814 ecispace(r0, ncodes);
3815
3816 // c:3031-3053 — re-walk `s->pc` and write into ecbuf[r..].
3817 let mut r = r0;
3818 loop {
3819 if s.pc >= prog_len {
3820 break;
3821 }
3822 let code = s.prog.prog[s.pc];
3823 if wc_code(code) != WC_REDIR {
3824 break;
3825 }
3826 s.pc += 1;
3827 // c:3036 — `ecbuf[r++] = code;`
3828 ECBUF.with_borrow_mut(|buf| {
3829 if r >= buf.len() {
3830 buf.resize(r + 1, 0);
3831 }
3832 buf[r] = code;
3833 });
3834 r += 1;
3835 // c:3038 — `ecbuf[r++] = *s->pc++;` (the fd1 word)
3836 let fd1 = s.prog.prog[s.pc];
3837 s.pc += 1;
3838 ECBUF.with_borrow_mut(|buf| {
3839 if r >= buf.len() {
3840 buf.resize(r + 1, 0);
3841 }
3842 buf[r] = fd1;
3843 });
3844 r += 1;
3845 // c:3041 — `ecbuf[r++] = ecstrcode(ecgetstr(s, EC_NODUP, NULL));`
3846 let name = ecgetstr(s, EC_NODUP, None);
3847 let nc = ecstrcode(&name);
3848 ECBUF.with_borrow_mut(|buf| {
3849 if r >= buf.len() {
3850 buf.resize(r + 1, 0);
3851 }
3852 buf[r] = nc;
3853 });
3854 r += 1;
3855 // c:3042-3047 — heredoc terminators.
3856 if WC_REDIR_FROM_HEREDOC(code) != 0 {
3857 let term = ecgetstr(s, EC_NODUP, None);
3858 let tc = ecstrcode(&term);
3859 ECBUF.with_borrow_mut(|buf| {
3860 if r >= buf.len() {
3861 buf.resize(r + 1, 0);
3862 }
3863 buf[r] = tc;
3864 });
3865 r += 1;
3866 let munged = ecgetstr(s, EC_NODUP, None);
3867 let mc = ecstrcode(&munged);
3868 ECBUF.with_borrow_mut(|buf| {
3869 if r >= buf.len() {
3870 buf.resize(r + 1, 0);
3871 }
3872 buf[r] = mc;
3873 });
3874 r += 1;
3875 }
3876 // c:3048-3049 — varid.
3877 if WC_REDIR_VARID(code) != 0 {
3878 let varid = ecgetstr(s, EC_NODUP, None);
3879 let vc = ecstrcode(&varid);
3880 ECBUF.with_borrow_mut(|buf| {
3881 if r >= buf.len() {
3882 buf.resize(r + 1, 0);
3883 }
3884 buf[r] = vc;
3885 });
3886 r += 1;
3887 }
3888 }
3889
3890 // c:3056 — `return bld_eprog(0);` — `bld_eprog` appends the
3891 // WC_END marker and packages ECBUF/ECSTRS into an Eprog.
3892 Some(bld_eprog(false))
3893}
3894
3895/// Port of `init_eprog(void)` from `Src/parse.c:3069`. Sets up
3896/// `dummy_eprog_code = WCB_END(); dummy_eprog.len = sizeof(wordcode);
3897/// dummy_eprog.prog = &dummy_eprog_code; dummy_eprog.strs = NULL;`.
3898/// Called once at shell startup (init_main → init_misc → init_eprog).
3899pub fn init_eprog() {
3900 let mut d = DUMMY_EPROG.lock().unwrap();
3901 d.prog = vec![WCB_END()]; // c:3071/3073
3902 d.len = size_of::<wordcode>() as i32; // c:3072
3903 d.strs = None; // c:3074
3904 d.flags = 0;
3905 d.npats = 0;
3906 d.nref = 0;
3907}
3908
3909// =====================================================================
3910// `bin_zcompile` and wordcode-dump helpers — port of `Src/parse.c:3104+`.
3911//
3912// The wordcode dump format (`.zwc`) is a serialized parse tree zsh can
3913// `mmap()` and dispatch from without re-parsing on every shell start.
3914// File layout (one struct = `FD_PRELEN` `u32`s):
3915// - `pre[0]` = magic word (FD_MAGIC native byte-order, FD_OMAGIC
3916// opposite byte-order).
3917// - `pre[1]` = packed `{flags(8) | other_offset(24)}` byte field.
3918// - `pre[2..12]` = `ZSH_VERSION` C-string padded to 40 bytes.
3919// - `pre[12]` = `fdheaderlen` (total prelude+header word count).
3920// - Then a sequence of `struct fdhead` records, one per function,
3921// each followed by its NUL-terminated name (padded to 4-byte).
3922// - Then the wordcode bytes for every function back-to-back.
3923//
3924// On a little-endian host writing a dump twice: first `FD_MAGIC` for
3925// native readers, then re-walks the body byte-swapped and emits a
3926// second `FD_OMAGIC` copy so big-endian readers can mmap it too.
3927// =====================================================================
3928
3929// File-format constants — port of `Src/parse.c:3104-3150`.
3930
3931/// `#define FD_EXT ".zwc"` from `Src/parse.c:3104`.
3932pub const FD_EXT: &str = ".zwc";
3933
3934/// `#define FD_MINMAP 4096` from `Src/parse.c:3105`. mmap threshold
3935/// — `-M` mode only kicks in when the wordcode body is at least
3936/// this many bytes (otherwise read(2) is preferred).
3937pub const FD_MINMAP: usize = 4096;
3938
3939/// `#define FD_PRELEN 12` from `Src/parse.c:3107`. File-header
3940/// length in u32 words: magic + packed-flags-byte + 10 version words.
3941pub const FD_PRELEN: usize = 12;
3942
3943/// `#define FD_MAGIC 0x04050607` from `Src/parse.c:3108`. Sentinel
3944/// for native-byte-order dumps.
3945pub const FD_MAGIC: u32 = 0x04050607;
3946
3947/// `#define FD_OMAGIC 0x07060504` from `Src/parse.c:3109`. Sentinel
3948/// for opposite-byte-order dumps (byte-swapped FD_MAGIC).
3949pub const FD_OMAGIC: u32 = 0x07060504;
3950
3951/// `#define FDF_MAP 1` from `Src/parse.c:3111`. Bit set when the
3952/// dump should be `mmap()`-ed (`-M` flag) vs read normally (`-R`).
3953pub const FDF_MAP: u32 = 1;
3954
3955/// `#define FDF_OTHER 2` from `Src/parse.c:3112`. Bit indicating
3956/// this dump has an opposite-byte-order copy at `fdother(f)`.
3957pub const FDF_OTHER: u32 = 2;
3958
3959/// Port of `struct fdhead` from `Src/parse.c:3116`. One per function
3960/// inside a wordcode dump. All fields are `wordcode` (u32).
3961#[allow(non_camel_case_types)]
3962#[derive(Debug, Clone, Copy)]
3963pub struct fdhead {
3964 /// Offset (in u32 words) to the start of this function's
3965 /// wordcode body inside the dump.
3966 pub start: u32, // c:3117
3967 /// Wordcode-byte length of the body (excludes pattern-prog slots).
3968 pub len: u32, // c:3118
3969 /// Number of compiled patterns the body references.
3970 pub npats: u32, // c:3119
3971 /// Offset of the string table inside `prog->prog`.
3972 pub strs: u32, // c:3120
3973 /// Header-record length in u32 words (record + name).
3974 pub hlen: u32, // c:3121
3975 /// Packed `{ kshload_bits(2) | name_tail_offset(30) }` field.
3976 pub flags: u32, // c:3122
3977}
3978
3979/// `#define FDHF_KSHLOAD 1` from `Src/parse.c:3149`. Function-header
3980/// flag word — `-k` ksh-style autoload marker.
3981pub const FDHF_KSHLOAD: u32 = 1;
3982
3983/// `#define FDHF_ZSHLOAD 2` from `Src/parse.c:3150`. `-z` zsh-style
3984/// autoload marker.
3985pub const FDHF_ZSHLOAD: u32 = 2;
3986
3987/// Port of `struct wcfunc` from `Src/parse.c:3158`. Build-time
3988/// per-function aggregate before write_dump emits it: the function
3989/// (or source-file) name, the compiled `Eprog` from `parse_string`,
3990/// and the FDHF_* autoload-style flag word.
3991#[allow(non_camel_case_types)]
3992#[derive(Debug, Clone)]
3993pub struct wcfunc {
3994 pub name: String, // c:3159
3995 /// Compiled program (`Eprog prog` c:3160) — wordcode + strs +
3996 /// npats as built by `bld_eprog`.
3997 pub prog: eprog, // c:3160
3998 pub flags: u32, // c:3161
3999}
4000
4001/// Port of `dump_find_func(Wordcode h, char *name)` from
4002/// `Src/parse.c:3167`. Walks the header table inside a loaded
4003/// dump for a function with the given basename; returns the
4004/// matching `fdhead` record (C returns the `FDHead` pointer).
4005pub fn dump_find_func(h: &[u32], name: &str) -> Option<fdhead> {
4006 // c:3167
4007 let header_words = fdheaderlen(h) as usize;
4008 let end = header_words; // walking u32 offsets, end-exclusive
4009 let mut cur = firstfdhead_offset();
4010 while cur < end {
4011 if let Some(fh) = read_fdhead(h, cur) {
4012 let full = fdname(h, cur);
4013 let tail = fdhtail(&fh) as usize;
4014 let basename = if tail <= full.len() {
4015 &full[tail..]
4016 } else {
4017 ""
4018 };
4019 if basename == name {
4020 return Some(fh); // c:3173 `return n;`
4021 }
4022 cur = nextfdhead_offset(h, cur);
4023 } else {
4024 break;
4025 }
4026 }
4027 None // c:3175
4028}
4029
4030/// Port of `bin_zcompile(char *nam, char **args, Options ops, UNUSED(int func))`
4031/// from `Src/parse.c:3180`. Validates the option set, then dispatches
4032/// to one of: `-t` (test/list), `-c`/`-a` (dump current functions),
4033/// or the default (compile source files to `.zwc`).
4034pub fn bin_zcompile(
4035 nam: &str, // c:3180
4036 args: &[String],
4037 ops: &crate::ported::zsh_h::options,
4038 _func: i32,
4039) -> i32 {
4040 // c:3185-3192 — illegal-combination guard.
4041 if (OPT_ISSET(ops, b'k') && OPT_ISSET(ops, b'z'))
4042 || (OPT_ISSET(ops, b'R') && OPT_ISSET(ops, b'M'))
4043 || (OPT_ISSET(ops, b'c')
4044 && (OPT_ISSET(ops, b'U') || OPT_ISSET(ops, b'k') || OPT_ISSET(ops, b'z')))
4045 || (!(OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) && OPT_ISSET(ops, b'm'))
4046 {
4047 zwarnnam(nam, "illegal combination of options"); // c:3192
4048 return 1;
4049 }
4050
4051 // c:3194 — `-c`/`-a` + KSHAUTOLOAD warning.
4052 if (OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) && isset(crate::ported::zsh_h::KSHAUTOLOAD) {
4053 zwarnnam(nam, "functions will use zsh style autoloading"); // c:3195
4054 }
4055
4056 // c:3196-3197 — flag word from `-k` / `-z`.
4057 let flags: u32 = if OPT_ISSET(ops, b'k') {
4058 FDHF_KSHLOAD
4059 } else if OPT_ISSET(ops, b'z') {
4060 FDHF_ZSHLOAD
4061 } else {
4062 0
4063 };
4064
4065 // c:3199 — `-t` test/list mode.
4066 if OPT_ISSET(ops, b't') {
4067 // c:3199
4068 if args.is_empty() {
4069 zwarnnam(nam, "too few arguments"); // c:3202
4070 return 1;
4071 }
4072 let dump_name = if args[0].ends_with(FD_EXT) {
4073 args[0].clone()
4074 } else {
4075 format!("{}{}", args[0], FD_EXT)
4076 };
4077 let f = match load_dump_header(nam, &dump_name, 1) {
4078 // c:3206
4079 Some(buf) => buf,
4080 None => return 1,
4081 };
4082 // c:3209 — per-function check.
4083 if args.len() > 1 {
4084 for name in &args[1..] {
4085 // c:3210
4086 if dump_find_func(&f, name).is_none() {
4087 // c:3212
4088 return 1;
4089 }
4090 }
4091 return 0;
4092 }
4093 // c:3215-3221 — listing arm. Walk every fdhead, print
4094 // each function's full name. C uses `fdname(h)` which
4095 // includes the path prefix; matches our `fdname()` impl.
4096 let mapped = if (fdflags(&f) & FDF_MAP) != 0 {
4097 "mapped"
4098 } else {
4099 "read"
4100 };
4101 println!("zwc file ({}) for zsh-{}", mapped, fdversion(&f));
4102 let header_words = fdheaderlen(&f) as usize;
4103 let mut cur = firstfdhead_offset();
4104 while cur < header_words {
4105 if read_fdhead(&f, cur).is_none() {
4106 break;
4107 }
4108 println!("{}", fdname(&f, cur));
4109 cur = nextfdhead_offset(&f, cur);
4110 }
4111 return 0;
4112 }
4113
4114 if args.is_empty() {
4115 zwarnnam(nam, "too few arguments"); // c:3226
4116 return 1;
4117 }
4118
4119 // c:3228 — map mode discriminant.
4120 let map: i32 = if OPT_ISSET(ops, b'M') {
4121 2
4122 } else if OPT_ISSET(ops, b'R') {
4123 0
4124 } else {
4125 1
4126 };
4127
4128 // c:3230-3236 — single-file default-mode short path.
4129 if args.len() == 1 && !(OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a')) {
4130 let dump = format!("{}{}", args[0], FD_EXT);
4131 return build_dump(nam, &dump, args, OPT_ISSET(ops, b'U') as i32, map, flags);
4132 }
4133
4134 // c:3239-3247 — multi-file or `-c`/`-a` mode.
4135 let dump = if args[0].ends_with(FD_EXT) {
4136 args[0].clone()
4137 } else {
4138 format!("{}{}", args[0], FD_EXT)
4139 };
4140 let rest = &args[1..];
4141 if OPT_ISSET(ops, b'c') || OPT_ISSET(ops, b'a') {
4142 let what =
4143 (if OPT_ISSET(ops, b'c') { 1 } else { 0 }) | (if OPT_ISSET(ops, b'a') { 2 } else { 0 });
4144 build_cur_dump(nam, &dump, rest, OPT_ISSET(ops, b'm') as i32, map, what)
4145 } else {
4146 build_dump(nam, &dump, rest, OPT_ISSET(ops, b'U') as i32, map, flags)
4147 }
4148}
4149
4150/// Port of `load_dump_header(char *nam, char *name, int err)` from
4151/// `Src/parse.c:3258`. Opens the file, reads + validates the magic
4152/// and version, then slurps the full header table into memory.
4153/// Returns the header u32-array on success or None on any failure
4154/// (emitting C-shaped warnings when `err != 0`).
4155pub fn load_dump_header(nam: &str, name: &str, err: i32) -> Option<Vec<u32>> {
4156 // c:3258
4157
4158 let mut f = match File::open(name) {
4159 // c:3263
4160 Ok(h) => h,
4161 Err(_) => {
4162 if err != 0 {
4163 zwarnnam(nam, &format!("can't open zwc file: {}", name)); // c:3265
4164 }
4165 return None;
4166 }
4167 };
4168
4169 // Read FD_PRELEN+1 u32 words = 52 bytes.
4170 let mut buf_bytes = vec![0u8; (FD_PRELEN + 1) * 4];
4171 if f.read_exact(&mut buf_bytes).is_err() {
4172 if err != 0 {
4173 zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3277
4174 }
4175 return None;
4176 }
4177 let mut buf: Vec<u32> = buf_bytes
4178 .chunks_exact(4)
4179 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
4180 .collect();
4181
4182 // c:3270 — magic + version check against `ZSH_VERSION` (C global;
4183 // zshrs mirrors it in `patchlevel::ZSH_VERSION`).
4184 let magic_ok = fdmagic(&buf) == FD_MAGIC || fdmagic(&buf) == FD_OMAGIC;
4185 let v_ok = fdversion(&buf) == crate::ported::patchlevel::ZSH_VERSION;
4186 if !magic_ok {
4187 if err != 0 {
4188 zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3277
4189 }
4190 return None;
4191 }
4192 if !v_ok {
4193 if err != 0 {
4194 zwarnnam(
4195 nam,
4196 &format!(
4197 "zwc file has wrong version (zsh-{}): {}", // c:3274
4198 fdversion(&buf),
4199 name
4200 ),
4201 );
4202 }
4203 return None;
4204 }
4205
4206 // c:3285 — if magic matches host byte order, head len is `pre[FD_PRELEN]`.
4207 // Else seek to `fdother(buf)` and re-read.
4208 if fdmagic(&buf) != FD_MAGIC {
4209 let other = fdother(&buf) as u64; // c:3290
4210 if f.seek(SeekFrom::Start(other)).is_err() || f.read_exact(&mut buf_bytes).is_err() {
4211 zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3295
4212 return None;
4213 }
4214 buf = buf_bytes
4215 .chunks_exact(4)
4216 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
4217 .collect();
4218 }
4219
4220 let total_words = fdheaderlen(&buf) as usize; // c:3286/3299
4221 if total_words < FD_PRELEN + 1 {
4222 zwarnnam(nam, &format!("invalid zwc file: {}", name));
4223 return None;
4224 }
4225
4226 // Read the remaining header words.
4227 let mut head: Vec<u32> = Vec::with_capacity(total_words);
4228 head.extend_from_slice(&buf);
4229 let remaining_words = total_words - (FD_PRELEN + 1);
4230 if remaining_words > 0 {
4231 let mut rest_bytes = vec![0u8; remaining_words * 4]; // c:3305
4232 if f.read_exact(&mut rest_bytes).is_err() {
4233 zwarnnam(nam, &format!("invalid zwc file: {}", name)); // c:3307
4234 return None;
4235 }
4236 for c in rest_bytes.chunks_exact(4) {
4237 head.push(u32::from_le_bytes([c[0], c[1], c[2], c[3]]));
4238 }
4239 }
4240 Some(head) // c:3311
4241}
4242
4243/// Port of `fdswap(Wordcode p, int n)` from `Src/parse.c:3318`.
4244/// Byte-swap each u32 in `p[..n]` in place. Used when writing the
4245/// opposite-byte-order copy of a wordcode dump.
4246pub fn fdswap(p: &mut [u32]) {
4247 // c:3318
4248 for w in p.iter_mut() {
4249 *w = w.swap_bytes();
4250 }
4251}
4252
4253/// Port of `write_dump(int dfd, LinkList progs, int map, int hlen, int tlen)`
4254/// from `Src/parse.c:3334`. Writes the prelude + header records +
4255/// body wordcode bytes to the dump file descriptor.
4256///
4257/// Two passes: first native-byte-order (`FD_MAGIC`), then opposite-
4258/// byte-order (`FD_OMAGIC`) so big-endian readers can mmap the
4259/// same file. Bodies are byte-swapped via `fdswap` on the second pass.
4260pub fn write_dump(
4261 dfd: &mut File, // c:3334
4262 progs: &[wcfunc],
4263 mut map: i32,
4264 hlen: i32,
4265 tlen: i32,
4266) -> std::io::Result<()> {
4267 // c:3345-3346 — `if (map == 1) map = (tlen >= FD_MINMAP);`
4268 if map == 1 {
4269 map = ((tlen as usize) >= FD_MINMAP) as i32;
4270 }
4271
4272 // C `sizeof(Patprog)` — pointer size (see bld_eprog len arithmetic).
4273 let patprog_size = size_of::<*const u8>() as i32;
4274
4275 let mut other = 0u32; // c:3338
4276 let ohlen = hlen;
4277
4278 loop {
4279 // c:3349 — `for (ohlen = hlen; ; hlen = ohlen)`.
4280 let mut cur_hlen = ohlen;
4281 // c:3348 — `memset(pre, 0, sizeof(wordcode) * FD_PRELEN);`
4282 let mut pre = vec![0u32; FD_PRELEN];
4283 pre[0] = if other != 0 { FD_OMAGIC } else { FD_MAGIC }; // c:3350
4284 let flags = (if map != 0 { FDF_MAP } else { 0 }) | other;
4285 fdsetflags(&mut pre, flags as u8); // c:3351
4286 fdsetother(&mut pre, tlen as u32); // c:3352
4287 // c:3353 — copy ZSH_VERSION C-string into pre[2..].
4288 let ver = crate::ported::patchlevel::ZSH_VERSION.as_bytes();
4289 for (i, &b) in ver.iter().enumerate() {
4290 let word = 2 + i / 4;
4291 let shift = (i % 4) * 8;
4292 pre[word] |= (b as u32) << shift;
4293 }
4294 // c:3354 — write prelude.
4295 for w in &pre {
4296 dfd.write_all(&w.to_le_bytes())?;
4297 }
4298 // c:3356 — per-fn header records.
4299 for wcf in progs {
4300 let n = &wcf.name;
4301 let prog = &wcf.prog;
4302 // c:3362-3363 — body length in bytes excluding the
4303 // pattern-prog slots: `prog->len - (prog->npats *
4304 // sizeof(Patprog))`.
4305 let len_bytes = prog.len - prog.npats * patprog_size;
4306 let mut head = fdhead {
4307 start: cur_hlen as u32, // c:3360
4308 len: len_bytes as u32, // c:3363
4309 npats: prog.npats as u32, // c:3364
4310 // c:3365 — `head.strs = prog->strs - ((char *) prog->prog);`
4311 // In bld_eprog's layout strs sits right after the code
4312 // words, so the byte offset is ecused * 4.
4313 strs: (prog.prog.len() * 4) as u32, // c:3365
4314 hlen: ((FDHEAD_WORDS as u32) + ((n.len() as u32 + 4) / 4)), // c:3366
4315 flags: 0,
4316 };
4317 // c:3361 — `hlen += (prog->len - npats*sizeof(Patprog) +
4318 // sizeof(wordcode) - 1) / sizeof(wordcode);`
4319 cur_hlen += (len_bytes + 3) / 4;
4320 // c:3368-3371 — name tail offset from path basename.
4321 let tail = n.rfind('/').map(|p| p + 1).unwrap_or(0);
4322 head.flags = fdhbldflags(wcf.flags, tail as u32); // c:3372
4323 // c:3373 — opposite-byte-order swap on second pass.
4324 let mut head_words: Vec<u32> = vec![
4325 head.start, head.len, head.npats, head.strs, head.hlen, head.flags,
4326 ];
4327 if other != 0 {
4328 fdswap(&mut head_words);
4329 }
4330 for w in &head_words {
4331 dfd.write_all(&w.to_le_bytes())?;
4332 }
4333 // c:3376-3379 — write the name + NUL, then pad to a word
4334 // boundary with the leading bytes of `head` (C: `write_loop
4335 // (dfd, (char *)&head, sizeof(wordcode) - tmp);`).
4336 dfd.write_all(n.as_bytes())?;
4337 dfd.write_all(&[0u8])?;
4338 let tmp = (n.len() + 1) & 3;
4339 if tmp != 0 {
4340 let head_bytes = head_words[0].to_le_bytes();
4341 dfd.write_all(&head_bytes[..4 - tmp])?;
4342 }
4343 }
4344 // c:3381-3388 — per-fn bodies: code words then the strs region,
4345 // padded to a word boundary. `tmp = (prog->len - npats*
4346 // sizeof(Patprog) + sizeof(wordcode) - 1) / sizeof(wordcode);
4347 // write_loop(dfd, (char *)prog->prog, tmp * sizeof(wordcode));`
4348 for wcf in progs {
4349 let prog = &wcf.prog;
4350 let len_bytes = (prog.len - prog.npats * patprog_size) as usize;
4351 let tmp = (len_bytes + 3) / 4;
4352 // c:3386-3387 — on the other pass only the code words are
4353 // swapped (`fdswap(prog->prog, ((Wordcode) prog->strs) -
4354 // prog->prog)`); the strs chars are byte-order neutral.
4355 let mut body_bytes: Vec<u8> = Vec::with_capacity(tmp * 4);
4356 for &w in &prog.prog {
4357 let w = if other != 0 { w.swap_bytes() } else { w };
4358 body_bytes.extend_from_slice(&w.to_le_bytes());
4359 }
4360 if let Some(s) = &prog.strs {
4361 body_bytes.extend_from_slice(s.as_bytes());
4362 }
4363 // C reads up to 3 bytes of heap slop past strs; emit NULs
4364 // (readers never look past head.len so the value is free).
4365 body_bytes.resize(tmp * 4, 0);
4366 dfd.write_all(&body_bytes)?;
4367 }
4368 if other != 0 {
4369 // c:3389
4370 break;
4371 }
4372 other = FDF_OTHER; // c:3391
4373 }
4374 Ok(())
4375}
4376
4377/// Port of `build_dump(char *nam, char *dump, char **files, int ali, int map, int flags)`
4378/// from `Src/parse.c:3396`. Source-file → wordcode dump compiler:
4379/// parses each source file via `parse_string` and serializes the
4380/// resulting `Eprog`s through `write_dump` into `<dump>.zwc`.
4381pub fn build_dump(
4382 nam: &str, // c:3397
4383 dump: &str,
4384 files: &[String],
4385 ali: i32,
4386 map: i32,
4387 flags: u32,
4388) -> i32 {
4389 use crate::ported::utils::{errflag, ERRFLAG_ERROR};
4390 use std::os::unix::fs::OpenOptionsExt;
4391 use std::sync::atomic::Ordering;
4392
4393 // c:3403-3404 — append FD_EXT unless already suffixed.
4394 let dump: String = if dump.ends_with(FD_EXT) {
4395 dump.to_string()
4396 } else {
4397 format!("{}{}", dump, FD_EXT)
4398 };
4399
4400 // c:3406 — `unlink(dump);`
4401 let _ = fs::remove_file(&dump);
4402 // c:3407-3410 — `open(dump, O_WRONLY|O_CREAT, 0444)`.
4403 let mut dfd = match fs::OpenOptions::new()
4404 .write(true)
4405 .create(true)
4406 .mode(0o444)
4407 .open(&dump)
4408 {
4409 Ok(f) => f,
4410 Err(_) => {
4411 zwarnnam(nam, &format!("can't write zwc file: {}", dump)); // c:3408
4412 return 1;
4413 }
4414 };
4415
4416 let patprog_size = size_of::<*const u8>() as i32; // C sizeof(Patprog)
4417 let ona = crate::ported::lex::noaliases(); // c:3398 `ona = noaliases`
4418 crate::ported::lex::set_noaliases(ali != 0); // c:3412 `noaliases = ali;`
4419
4420 let mut progs: Vec<wcfunc> = Vec::new(); // c:3411
4421 let mut flags = flags;
4422 let mut hlen = FD_PRELEN as i32; // c:3414
4423 let mut tlen: i32 = 0;
4424
4425 for fname in files {
4426 // c:3418-3425 — `-k` / `-z` pseudo-args flip the autoload style.
4427 if check_cond(fname, "k") {
4428 flags = (flags & !(FDHF_KSHLOAD | FDHF_ZSHLOAD)) | FDHF_KSHLOAD; // c:3419
4429 continue;
4430 } else if check_cond(fname, "z") {
4431 flags = (flags & !(FDHF_KSHLOAD | FDHF_ZSHLOAD)) | FDHF_ZSHLOAD; // c:3422
4432 continue;
4433 }
4434 // c:3426-3437 — open + fstat + S_ISREG + read the whole file.
4435 let fnam = crate::ported::utils::unmeta(fname); // c:3426
4436 let is_reg = fs::metadata(&fnam).map(|m| m.is_file()).unwrap_or(false);
4437 let bytes = if is_reg { fs::read(&fnam).ok() } else { None };
4438 let bytes = match bytes {
4439 Some(b) => b,
4440 None => {
4441 zwarnnam(nam, &format!("can't open file: {}", fname)); // c:3432
4442 crate::ported::lex::set_noaliases(ona); // c:3433
4443 let _ = fs::remove_file(&dump); // c:3434
4444 return 1;
4445 }
4446 };
4447 // c:3450 — `file = metafy(file, flen, META_REALLOC);` — keep
4448 // raw bytes intact through the &str boundary (see bld_eprog's
4449 // from_utf8_unchecked rationale).
4450 let raw = unsafe { String::from_utf8_unchecked(bytes) };
4451 let file = crate::ported::utils::metafy(&raw);
4452
4453 // c:3452-3460 — parse; any error aborts the whole dump.
4454 let prog = crate::ported::exec::parse_string(&file, 1);
4455 let errored = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
4456 let prog = match prog {
4457 Some(p) if !errored => p,
4458 _ => {
4459 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed); // c:3453
4460 zwarnnam(nam, &format!("can't read file: {}", fname)); // c:3456
4461 crate::ported::lex::set_noaliases(ona); // c:3457
4462 let _ = fs::remove_file(&dump); // c:3458
4463 return 1;
4464 }
4465 };
4466
4467 // c:3467-3472 — accumulate header + body length budgets.
4468 let flen = (fname.len() as i32 + 4) / 4; // c:3469
4469 hlen += (FDHEAD_WORDS as i32) + flen; // c:3470
4470 tlen += (prog.len - prog.npats * patprog_size + 3) / 4; // c:3471
4471
4472 // c:3463-3466 — wcfunc node.
4473 let wcf_flags = if (prog.flags & EF_RUN) != 0 {
4474 FDHF_KSHLOAD // c:3465
4475 } else {
4476 flags
4477 };
4478 progs.push(wcfunc {
4479 name: fname.clone(),
4480 prog,
4481 flags: wcf_flags,
4482 });
4483 }
4484 crate::ported::lex::set_noaliases(ona); // c:3474
4485
4486 let tlen = (tlen + hlen) * 4; // c:3476
4487
4488 // c:3478 — `write_dump(dfd, progs, map, hlen, tlen);` (void in C).
4489 let _ = write_dump(&mut dfd, &progs, map, hlen, tlen);
4490
4491 0 // c:3482
4492}
4493
4494/// Port of `cur_add_func(char *nam, Shfunc shf, LinkList names, LinkList progs, int *hlen, int *tlen, int what)`
4495/// from `Src/parse.c:3489`. Adds a shfunc to the in-build dump
4496/// progs+names lists. Stub: `Eprog` for the function body isn't
4497/// yet wired through `shfunc.funcdef` to be serializable here.
4498pub fn cur_add_func(
4499 nam: &str, // c:3489
4500 shf_name: &str,
4501 shf_flags: i32,
4502 names: &mut Vec<String>,
4503 progs: &mut Vec<wcfunc>,
4504 hlen: &mut i32,
4505 tlen: &mut i32,
4506 what: i32,
4507) -> i32 {
4508 let is_undef = (shf_flags as u32 & PM_UNDEFINED) != 0;
4509 if is_undef {
4510 if (what & 2) == 0 {
4511 // c:3498
4512 zwarnnam(nam, &format!("function is not loaded: {}", shf_name));
4513 return 1;
4514 }
4515 // c:3503 — would call `getfpfunc` to load body for dump.
4516 zwarnnam(nam, &format!("can't load function: {}", shf_name));
4517 return 1;
4518 } else if (what & 1) == 0 {
4519 zwarnnam(nam, &format!("function is already loaded: {}", shf_name)); // c:3514
4520 return 1;
4521 }
4522 // c:3517 — would `dupeprog(shf->funcdef)`. Stub: empty program.
4523 let wcf = wcfunc {
4524 name: shf_name.to_string(),
4525 flags: FDHF_ZSHLOAD,
4526 prog: eprog::default(),
4527 };
4528 progs.push(wcf);
4529 names.push(shf_name.to_string());
4530
4531 // c:3526 — bump hlen / tlen.
4532 let name_words = (shf_name.len() as i32 + 4) / 4;
4533 *hlen += (FDHEAD_WORDS as i32) + name_words;
4534 *tlen += 0; // body is empty in stub; real path adds prog->len in words.
4535
4536 0
4537}
4538
4539/// Port of `build_cur_dump(char *nam, char *dump, char **names, int match, int map, int what)`
4540/// from `Src/parse.c:3536`. Compiles currently-loaded functions
4541/// (`-c` for functions, `-a` for aliases) into a `.zwc` dump.
4542/// Same wordcode-emit dependency as `build_dump`.
4543pub fn build_cur_dump(
4544 nam: &str, // c:3536
4545 dump: &str,
4546 _names: &[String],
4547 _match_: i32,
4548 _map: i32,
4549 _what: i32,
4550) -> i32 {
4551 zwarnnam(
4552 nam,
4553 &format!("{}: wordcode dump-current emit not yet ported", dump),
4554 );
4555 1
4556}
4557
4558/// Port of `zwcstat(char *filename, struct stat *buf)` from
4559/// `Src/parse.c:3656`. Stats a `.zwc` file, falling back to
4560/// `.zwc.old` if the primary doesn't exist (zsh uses the `.old`
4561/// suffix to keep a previous dump readable while a rewrite is in
4562/// progress).
4563pub fn zwcstat(filename: &str) -> Option<fs::Metadata> {
4564 // c:3656
4565 if let Ok(m) = fs::metadata(filename) {
4566 return Some(m);
4567 }
4568 let old = format!("{}.old", filename);
4569 fs::metadata(&old).ok()
4570}
4571
4572/// Port of `load_dump_file(char *dump, struct stat *sbuf, int other, int len)`
4573/// from `Src/parse.c:3675`. Reads (or mmap()'s) a complete `.zwc`
4574/// file into memory. Returns the u32 buffer or None on I/O error.
4575pub fn load_dump_file(
4576 dump: &str, // c:3675
4577 _sbuf: &fs::Metadata,
4578 other: i32,
4579 _len: usize,
4580) -> Option<Vec<u32>> {
4581 let mut f = File::open(dump).ok()?;
4582 if other != 0 {
4583 f.seek(SeekFrom::Start(other as u64)).ok()?;
4584 }
4585 let mut bytes = Vec::new();
4586 f.read_to_end(&mut bytes).ok()?;
4587 Some(
4588 bytes
4589 .chunks_exact(4)
4590 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
4591 .collect(),
4592 )
4593}
4594
4595/// Port of `try_dump_file(char *path, char *name, char *file, int *ksh, int test_only)`
4596/// from `Src/parse.c:3746`. Tries to load function `name` from a
4597/// `.zwc` digest (`<path>.zwc`) or per-function compiled file
4598/// (`<file>.zwc`) when each is newer than its uncompiled source.
4599pub fn try_dump_file(
4600 path: &str,
4601 name: &str,
4602 file: &str, // c:3746
4603 test_only: bool,
4604) -> Option<(eprog, i32)> {
4605 use std::fs;
4606
4607 // c:3753-3758 — if path ends in .zwc, treat as direct digest.
4608 if path.ends_with(FD_EXT) {
4609 crate::ported::signals::queue_signals();
4610 let result = fs::metadata(path)
4611 .ok()
4612 .and_then(|m| check_dump_file(path, &m, name, test_only));
4613 unqueue_signals();
4614 return result;
4615 }
4616
4617 // c:3759-3760 — dig = "<path>.zwc", wc = "<file>.zwc".
4618 let dig = format!("{}{}", path, FD_EXT);
4619 let wc = format!("{}{}", file, FD_EXT);
4620
4621 // c:3762-3764 — zwcstat(dig, &std); stat(wc, &stc); stat(file, &stn);
4622 let std_meta = fs::metadata(&dig);
4623 let stc_meta = fs::metadata(&wc);
4624 let stn_meta = fs::metadata(file);
4625
4626 crate::ported::signals::queue_signals();
4627
4628 // zsh compares `st_mtime` (time_t SECONDS) with `>=` (c:3772-3780). Use
4629 // second-granularity mtime (MetadataExt::mtime, imported at top) — NOT
4630 // SystemTime (`.modified()`), whose nanosecond precision disagrees with zsh
4631 // whenever the dump and source share a second but differ in nsec, making
4632 // autoload .zwc preference flaky.
4633
4634 // c:3771-3777 — use the digest when it is >= both the per-fn .zwc and the
4635 // source (or those are absent).
4636 if let Ok(std_m) = &std_meta {
4637 let dig_s = std_m.mtime();
4638 let dig_ge_wc = stc_meta.as_ref().map_or(true, |c| dig_s >= c.mtime()); // c:3772
4639 let dig_ge_src = stn_meta.as_ref().map_or(true, |n| dig_s >= n.mtime()); // c:3773
4640 if dig_ge_wc && dig_ge_src {
4641 if let Some(prog) = check_dump_file(&dig, std_m, name, test_only) {
4642 unqueue_signals();
4643 return Some(prog);
4644 }
4645 }
4646 }
4647
4648 // c:3779-3784 — try per-function .zwc when it is >= the source (or absent).
4649 if let Ok(stc_m) = &stc_meta {
4650 let wc_s = stc_m.mtime();
4651 let src_newer_or_missing = stn_meta.as_ref().map_or(true, |n| wc_s >= n.mtime()); // c:3780
4652 if src_newer_or_missing {
4653 if let Some(prog) = check_dump_file(&wc, stc_m, name, test_only) {
4654 unqueue_signals();
4655 return Some(prog);
4656 }
4657 }
4658 }
4659
4660 unqueue_signals(); // c:3787
4661 None // c:3788
4662}
4663
4664/// Port of `try_source_file(char *file)` from `Src/parse.c:3795`.
4665/// Returns an Eprog (the wordcode dump body) if `<file>.zwc` exists
4666/// and is newer than `<file>`, else None. The dump entry searched is
4667/// the file's basename (`tail`), matching how `zcompile` names
4668/// source-file entries.
4669pub fn try_source_file(file: &str) -> Option<eprog> {
4670 // c:3795
4671
4672 // c:3802-3805 — if ((tail = strrchr(file, '/'))) tail++; else tail = file;
4673 let tail = match file.rfind('/') {
4674 Some(i) => &file[i + 1..],
4675 None => file,
4676 };
4677
4678 // c:3807-3812 — if (strsfx(FD_EXT, file)) { ... return check_dump_file(file, NULL, tail, NULL, 0); }
4679 if file.ends_with(FD_EXT) {
4680 crate::ported::signals::queue_signals(); // c:3808
4681 let meta = fs::metadata(file);
4682 let prog = match meta {
4683 Ok(m) => check_dump_file(file, &m, tail, false).map(|(p, _)| p), // c:3809
4684 Err(_) => None,
4685 };
4686 unqueue_signals(); // c:3810
4687 return prog;
4688 }
4689
4690 // c:3813 — wc = dyncat(file, FD_EXT);
4691 let wc = format!("{}{}", file, FD_EXT);
4692
4693 // c:3815-3816 — rc = stat(wc, &stc); rn = stat(file, &stn);
4694 let stc = fs::metadata(&wc);
4695 let stn = fs::metadata(file);
4696
4697 crate::ported::signals::queue_signals(); // c:3818
4698 // c:3819-3823 — if (!rc && (rn || stc.st_mtime >= stn.st_mtime) && (prog = check_dump_file(...))) return prog;
4699 if let Ok(meta_c) = &stc {
4700 // c:3819 — `stc.st_mtime >= stn.st_mtime`: second-granularity (time_t,
4701 // via MetadataExt::mtime imported at top), not SystemTime nsec, to agree
4702 // with zsh on equal-second boundaries.
4703 let newer_than_src = match (&stc, &stn) {
4704 (Ok(c), Ok(n)) => c.mtime() >= n.mtime(),
4705 (Ok(_), Err(_)) => true, // c:3819 — `rn` (src missing) ⇒ accept .zwc
4706 _ => false,
4707 };
4708 if newer_than_src {
4709 let prog = check_dump_file(&wc, meta_c, tail, false); // c:3820
4710 if let Some((p, _)) = prog {
4711 unqueue_signals(); // c:3821
4712 return Some(p); // c:3822
4713 }
4714 }
4715 }
4716 unqueue_signals(); // c:3824
4717 None // c:3825
4718}
4719
4720/// Port of `Eprog check_dump_file(char *file, struct stat *sbuf,
4721/// char *name, int *ksh, int test_only)` from `Src/parse.c:3833`.
4722/// Walks the `dumps` mmap list looking for `(dev, ino)` matching
4723/// `sbuf`; on miss, calls `load_dump_header` to read the .zwc
4724/// header. Then `dump_find_func(d, name)` locates the function
4725/// table entry. Returns the wordcode slice + ksh-load flag.
4726///
4727/// ```c
4728/// Eprog
4729/// check_dump_file(char *file, struct stat *sbuf, char *name,
4730/// int *ksh, int test_only)
4731/// {
4732/// int isrec = 0;
4733/// Wordcode d;
4734/// FDHead h;
4735/// FuncDump f;
4736/// struct stat lsbuf;
4737/// if (!sbuf) {
4738/// if (zwcstat(file, &lsbuf)) return NULL;
4739/// sbuf = &lsbuf;
4740/// }
4741/// rec:
4742/// d = NULL;
4743/// for (f = dumps; f; f = f->next)
4744/// if (f->dev == sbuf->st_dev && f->ino == sbuf->st_ino)
4745/// { d = f->map; break; }
4746/// if (!f && (isrec || !(d = load_dump_header(NULL, file, 0))))
4747/// return NULL;
4748/// if ((h = dump_find_func(d, name))) {
4749/// if (test_only) return &dummy_eprog;
4750/// /* allocate Eprog from f->map at h offset, incrdumpcount,
4751/// return prog */
4752/// }
4753/// return NULL;
4754/// }
4755/// ```
4756/// Rust port returns `Option<(eprog, i32)>` instead of the C
4757/// `Eprog` pointer + `*ksh` out-param: tuple element 0 is the
4758/// loaded program (wordcode + string table per the fdhead record),
4759/// element 1 is the ksh-load mode exactly as C writes `*ksh`:
4760/// `FDHF_KSHLOAD → 2`, `FDHF_ZSHLOAD → 0`, neither → `1`
4761/// (c:3954-3956).
4762pub fn check_dump_file(
4763 // c:3833
4764 file: &str,
4765 sbuf: &fs::Metadata,
4766 name: &str,
4767 test_only: bool,
4768) -> Option<(eprog, i32)> {
4769 use std::os::unix::fs::MetadataExt;
4770
4771 // c:3842-3846 — `if (!sbuf) { zwcstat(file, &lsbuf); sbuf = &lsbuf; }`
4772 // Rust takes sbuf by &Metadata — never null.
4773 let dev = sbuf.dev(); // c:3859
4774 let ino = sbuf.ino(); // c:3859
4775
4776 // c:3854 — `d = NULL;`
4777 let mut d: Option<Vec<u32>> = None;
4778 let mut found_mmap = false; // c:3858 `for (f = dumps; f; ...)`
4779
4780 // c:3858-3862 — walk DUMPS for matching dev/ino.
4781 {
4782 let dumps_guard = DUMPS.lock().expect("dumps poisoned");
4783 for f in dumps_guard.iter() {
4784 // c:3858
4785 if f.dev == dev && f.ino == ino {
4786 // c:3859
4787 d = Some(f.map.clone()); // c:3860
4788 found_mmap = true;
4789 break; // c:3861
4790 }
4791 }
4792 }
4793
4794 // c:3870-3871 — `if (!f && (isrec || !(d = load_dump_header(NULL, file, 0)))) return NULL;`
4795 if !found_mmap {
4796 // c:3870
4797 match load_dump_header("", file, 0) {
4798 // c:3870 load_dump_header
4799 Some(loaded) => d = Some(loaded),
4800 None => return None, // c:3871
4801 }
4802 }
4803
4804 // c:3873 — `if ((h = dump_find_func(d, name)))`
4805 let dump = d?;
4806 let h = dump_find_func(&dump, name)?; // c:3873
4807
4808 // c:3876-3879 — `if (test_only) return &dummy_eprog;`
4809 if test_only {
4810 // c:3876
4811 return Some((eprog::default(), 0)); // c:3879 dummy
4812 }
4813
4814 // c:3954-3956 — `*ksh = ((fdhflags(h) & FDHF_KSHLOAD) ? 2 :
4815 // ((fdhflags(h) & FDHF_ZSHLOAD) ? 0 : 1));`
4816 let ksh = if (fdhflags(&h) & FDHF_KSHLOAD) != 0 {
4817 2
4818 } else if (fdhflags(&h) & FDHF_ZSHLOAD) != 0 {
4819 0
4820 } else {
4821 1
4822 };
4823
4824 // c:3919-3958 — the read (non-mmap) branch: open the file, seek
4825 // to the function's wordcode, read `h->len` bytes (wordcode +
4826 // string pool), and build an EF_REAL Eprog around it. zshrs has
4827 // no mmap'd EF_MAP variant — DUMPS entries store the file words
4828 // in `map`, so both branches funnel into the same read-and-copy.
4829 //
4830 // if ((fd = open(file, O_RDONLY)) < 0 ||
4831 // lseek(fd, ((h->start * sizeof(wordcode)) +
4832 // ((fdflags(d) & FDF_OTHER) ? fdother(d) : 0)), 0) < 0)
4833 // return NULL;
4834 // d = (Wordcode) zalloc(h->len + po);
4835 // if (read(fd, ((char *) d) + po, h->len) != (int)h->len) return NULL;
4836 // prog->flags = EF_REAL;
4837 // prog->len = h->len + po;
4838 // prog->npats = np = h->npats;
4839 // prog->prog = (Wordcode) (((char *) d) + po);
4840 // prog->strs = ((char *) prog->prog) + h->strs;
4841 let body_off = (h.start as u64) * 4
4842 + if (fdflags(&dump) & FDF_OTHER) != 0 {
4843 fdother(&dump) as u64 // c:3924
4844 } else {
4845 0
4846 };
4847 let mut f = File::open(file).ok()?; // c:3922
4848 f.seek(SeekFrom::Start(body_off)).ok()?; // c:3923
4849 let mut bytes = vec![0u8; h.len as usize];
4850 if f.read_exact(&mut bytes).is_err() {
4851 // c:3931 `read(...) != h->len`
4852 return None;
4853 }
4854 // `h->strs` is the byte offset of the string pool inside the read
4855 // region; everything before it is wordcode.
4856 let strs_off = (h.strs as usize).min(bytes.len());
4857 let prog_words: Vec<u32> = bytes[..strs_off]
4858 .chunks_exact(4)
4859 .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
4860 .collect();
4861 // SAFETY: same byte-not-char convention as `bld_eprog` (c:566) —
4862 // consumers index `strs` by byte offset and never require UTF-8.
4863 let strs_string = unsafe { String::from_utf8_unchecked(bytes[strs_off..].to_vec()) };
4864 let po = h.npats as usize * size_of::<*const u8>(); // c:3920
4865 let prog = eprog {
4866 flags: EF_REAL, // c:3941
4867 len: (h.len as usize + po) as i32, // c:3942
4868 npats: h.npats as i32, // c:3943
4869 nref: 1, // c:3944
4870 pats: Vec::new(), // c:3945/3952 dummy_patprog1 fill
4871 prog: prog_words, // c:3946
4872 strs: Some(strs_string), // c:3947
4873 shf: None, // c:3948
4874 dump: None, // c:3949
4875 };
4876
4877 // c:3899 — incrdumpcount(f) on the mmap-cache hit path.
4878 if found_mmap {
4879 let dumps_guard = DUMPS.lock().expect("dumps poisoned");
4880 if let Some(f) = dumps_guard.iter().find(|f| f.dev == dev && f.ino == ino) {
4881 incrdumpcount(f); // c:3899
4882 }
4883 }
4884
4885 Some((prog, ksh)) // c:3958
4886}
4887
4888/// Port of `incrdumpcount(FuncDump f)` from `Src/parse.c:3970/4021`.
4889/// `f->count++;` — refcount-up a loaded dump entry. The Rust port
4890/// keys lookup by `filename` because Rust can't raw-pointer-compare
4891/// funcdump values inside a `Mutex<Vec<...>>`; same observable
4892/// effect (the count of the matching entry increments).
4893pub fn incrdumpcount(f: &funcdump) {
4894 // c:3970 — `f->count++;`
4895 if let Some(d) = DUMPS
4896 .lock()
4897 .unwrap()
4898 .iter_mut()
4899 .find(|d| d.filename.as_deref() == f.filename.as_deref())
4900 {
4901 d.count += 1; // c:3973
4902 }
4903}
4904
4905/// Port of `freedump(FuncDump f)` from `Src/parse.c:3976`. Public
4906/// helper for the rare external caller; locks the dumps mutex and
4907/// drops the entry with the given filename.
4908pub fn freedump(f: &funcdump) {
4909 // c:3976
4910 let mut g = DUMPS.lock().unwrap();
4911 if let Some(name) = f.filename.as_deref() {
4912 freedump_locked(&mut g, name);
4913 }
4914}
4915
4916/// Port of `decrdumpcount(FuncDump f)` from `Src/parse.c:3988/4026`.
4917/// `f->count--; if (!f->count) { unlink from dumps; freedump(f); }`.
4918pub fn decrdumpcount(f: &funcdump) {
4919 // c:3988
4920 let key = f.filename.clone();
4921 let mut g = DUMPS.lock().unwrap();
4922 let mut hit_zero: Option<String> = None;
4923 for d in g.iter_mut() {
4924 if d.filename == key {
4925 d.count -= 1; // c:3991
4926 if d.count == 0 {
4927 // c:3992
4928 hit_zero = d.filename.clone();
4929 }
4930 break;
4931 }
4932 }
4933 if let Some(name) = hit_zero {
4934 // c:3994-4001
4935 freedump_locked(&mut g, &name);
4936 }
4937}
4938
4939/// Port of `closedumps(void)` from `Src/parse.c:4008/4033`. Walks
4940/// `dumps` freeing every entry. Called on shell exit (exec.c:522).
4941pub fn closedumps() {
4942 // c:4008
4943 let mut g = DUMPS.lock().unwrap();
4944 g.clear(); // c:4011-4014 `while (dumps) { ... freedump(...); ... }`
4945}
4946
4947/// Port of `dump_autoload(char *nam, char *file, int on, Options ops, int func)`
4948/// from `Src/parse.c:4042`. Registers every function in a `.zwc`
4949/// for autoload via `shfunctab`.
4950pub fn dump_autoload(
4951 nam: &str,
4952 file: &str, // c:4042
4953 on: i32,
4954 ops: &crate::ported::zsh_h::options,
4955 func: i32,
4956) -> i32 {
4957 use crate::ported::zsh_h::shfunc;
4958 let mut ret = 0; // c:4047
4959
4960 // c:4049-4050 — if (!strsfx(FD_EXT, file)) file = dyncat(file, FD_EXT);
4961 let file_owned;
4962 let file = if !file.ends_with(FD_EXT) {
4963 file_owned = format!("{}{}", file, FD_EXT);
4964 file_owned.as_str()
4965 } else {
4966 file
4967 };
4968
4969 // c:4052-4053 — if (!(h = load_dump_header(nam, file, 1))) return 1;
4970 let h = match load_dump_header(nam, file, 1) {
4971 Some(buf) => buf,
4972 None => return 1,
4973 };
4974
4975 // c:4055-4056 — for (n = firstfdhead(h); n < e; n = nextfdhead(n))
4976 let hlen = fdheaderlen(&h) as usize; // c:4055
4977 let mut n_off = firstfdhead_offset();
4978 while n_off < hlen {
4979 let head = match read_fdhead(&h, n_off) {
4980 Some(hd) => hd,
4981 None => break,
4982 };
4983 // c:4057-4061 — shf = zshcalloc; shf->node.flags = on; ...addnode(fdname + fdhtail)
4984 let name_full = fdname(&h, n_off);
4985 let tail = fdhtail(&head) as usize;
4986 let basename: String = name_full.chars().skip(tail).collect();
4987 let mut shf = shfunc {
4988 node: crate::ported::zsh_h::hashnode {
4989 next: None,
4990 nam: basename.clone(),
4991 flags: on, // c:4058
4992 },
4993 filename: None,
4994 lineno: 0,
4995 funcdef: None,
4996 redir: None,
4997 sticky: None, // c:4060 NULL
4998 body: None,
4999 };
5000 // c:4059 — shf->funcdef = mkautofn(shf); (placeholder Eprog ptr)
5001 let _ = crate::ported::builtin::mkautofn(&mut shf as *mut _);
5002 // c:4061 — shfunctab->addnode(...)
5003 let snapshot = shf.clone();
5004 {
5005 let mut tab = crate::ported::hashtable::shfunctab_lock()
5006 .write()
5007 .expect("shfunctab poisoned");
5008 tab.add(shf);
5009 }
5010 // c:4062-4063 — if (OPT_ISSET(ops,'X') && eval_autoload(...)) ret = 1;
5011 if OPT_ISSET(ops, b'X') {
5012 let mut shf_ref = snapshot;
5013 if crate::ported::builtin::eval_autoload(&mut shf_ref as *mut _, &basename, ops, func)
5014 != 0
5015 {
5016 ret = 1;
5017 }
5018 }
5019 n_off = nextfdhead_offset(&h, n_off);
5020 }
5021 let _ = nam;
5022 ret // c:4065
5023}
5024
5025/// Port of C `struct eccstr` (zsh.h:836) — the long-string dedup BST
5026/// node. The dedup-walk and cmp logic in `ecstrcode` is faithful to
5027/// parse.c:447-453 including the conditional cmp chain
5028/// (nfunc → hashval → strcmp), so corpus inputs where C's eccstr BST walk
5029/// finds-or-misses match get the same outcome on the Rust side.
5030struct EccstrNode {
5031 left: Option<Box<EccstrNode>>,
5032 right: Option<Box<EccstrNode>>,
5033 /// C-byte form of the string (single byte per char ≤ 0xff).
5034 /// Owned because Rust doesn't have C zsh's "stable pointers into
5035 /// the lexer's tokstr arena" — every tokstr lives as a fresh
5036 /// Rust String allocation.
5037 str: Vec<u8>,
5038 /// Wordcode-encoded offset: `(byte_offset << 2) | token_bit`.
5039 /// Same shape as `Eccstr::offs` (parse.c:459).
5040 offs: u32,
5041 /// Absolute byte offset in the final strs region (= `ecsoffs` at
5042 /// insert time). C `Eccstr::aoffs` (parse.c:464). copy_ecstr uses
5043 /// THIS for the write position — distinct from `offs` which is
5044 /// ecssub-relative and collides across funcdef scopes.
5045 aoffs: u32,
5046 /// `nfunc` snapshot at insert time. Per-function namespace key
5047 /// — top-level scripts use 0; each funcdef bumps it.
5048 nfunc: i32,
5049 /// Hash of `str` computed via zsh's `hasher` (hashtable.c:86).
5050 hashval: u32,
5051}
5052// === end AST relocation ===
5053
5054// Parser state lives in file-scope thread_locals:
5055// - LEX_* (lexer side, matching Src/lex.c file-statics)
5056// - ECBUF / ECLEN / ECUSED / ECNPATS / ECSOFFS / ECSSUB / ECNFUNC /
5057// ECSTRS_INDEX / ECSTRS_REVERSE (wordcode-emission state, matching
5058// Src/parse.c file-statics)
5059//
5060// Callers use the free-fn entry points directly:
5061// crate::ported::parse::parse_init(input);
5062// let prog = crate::ported::parse::parse();
5063
5064const MAX_RECURSION_DEPTH: usize = 500;
5065
5066/// Direct port of `struct parse_stack` at `Src/zsh.h:3099-3109`.
5067/// Used by `parse_context_save` / `parse_context_restore`
5068/// (parse.c:295-355) to snapshot per-parse-call state so a nested
5069/// parse (e.g. inside command substitution) doesn't clobber the
5070/// outer parse.
5071///
5072/// A second port of `struct parse_stack` exists at
5073/// `crate::ported::zsh_h::parse_stack` (zsh.h:1066) using canonical
5074/// Wordcode / Eccstr / `struct heredocs` types — that port is unused
5075/// today and will become authoritative when Phase 9b (PORT_PLAN.md)
5076/// wires wordcode emission. This local version uses the working-set
5077/// shapes (`Vec<HereDoc>`, stubbed wordcode fields) suited to zshrs's
5078/// pre-wordcode AST architecture; the consolidation happens in P9b.
5079#[allow(non_camel_case_types)]
5080#[derive(Debug, Default, Clone)]
5081pub struct parse_stack {
5082 // ── Direct port of struct parse_stack at zsh.h:3099-3109 ──
5083 /// Pending heredocs awaiting body collection (canonical C
5084 /// linked-list shape). C: `struct heredocs *hdocs` (zsh.h:3100).
5085 /// Mirrors `parse::HDOCS` thread_local across nested parses.
5086 pub hdocs: Option<Box<crate::ported::zsh_h::heredocs>>,
5087 /// !!! WARNING: NOT IN PARSE_STACK — Rust-only AST-glue !!!
5088 /// Snapshot of `lex::LEX_HEREDOCS` (the parallel Rust-only Vec
5089 /// carrying terminator / strip_tabs / quoted metadata).
5090 /// Saved/restored alongside the canonical `hdocs` so nested
5091 /// parses get a clean AST view. C's parse_stack has no analog
5092 /// because C tracks terminator metadata implicitly via tokstr.
5093 pub lex_heredocs: Vec<HereDoc>,
5094 /// C: `int incmdpos` (zsh.h:3102).
5095 pub incmdpos: bool,
5096 /// C: `int aliasspaceflag` (zsh.h:3103).
5097 pub aliasspaceflag: i32,
5098 /// C: `int incond` (zsh.h:3104).
5099 pub incond: i32,
5100 /// C: `int inredir` (zsh.h:3105).
5101 pub inredir: bool,
5102 /// C: `int incasepat` (zsh.h:3106).
5103 pub incasepat: i32,
5104 /// C: `int isnewlin` (zsh.h:3107).
5105 pub isnewlin: i32,
5106 /// C: `int infor` (zsh.h:3108).
5107 pub infor: i32,
5108 /// C: `int inrepeat_` (zsh.h:3109).
5109 pub inrepeat_: i32,
5110 /// C: `int intypeset` (zsh.h:3110).
5111 pub intypeset: bool,
5112 // ── Wordcode-buffer state — STUB until Phase 9b ──
5113 // C `Wordcode ecbuf` (zsh.h:3112) + `Eccstr ecstrs` (zsh.h:3113) +
5114 // `int eclen/ecused/ecnpats/ecsoffs/ecssub/ecnfunc` (zsh.h:3112-3114).
5115 // zshrs hasn't emitted wordcode yet — these fields exist to
5116 // preserve the C shape but read/write nothing until P9b lands.
5117 pub eclen: i32,
5118 pub ecused: i32,
5119 pub ecnpats: i32,
5120 pub ecbuf: Option<Vec<u32>>,
5121 pub ecstrs: Option<Vec<u8>>,
5122 pub ecsoffs: i32,
5123 pub ecssub: i32,
5124 pub ecnfunc: i32,
5125}
5126
5127// Old uppercase Rust-only `ParseStack` is gone. Compat alias so
5128// existing call sites (context.rs) keep resolving until the
5129// rename ripples through.
5130/// `ParseStack` type alias.
5131#[allow(non_camel_case_types)]
5132pub type ParseStack = parse_stack;
5133
5134/// `mod_export struct eprog dummy_eprog;` from `Src/parse.c:3066`.
5135/// Placeholder Eprog used by `shf->funcdef = &dummy_eprog;` in
5136/// builtin.c when clearing a stale autoload stub. Held in a Mutex
5137/// so `init_eprog` can set it once at shell startup.
5138pub static DUMMY_EPROG: std::sync::Mutex<eprog> = std::sync::Mutex::new(eprog {
5139 flags: 0,
5140 len: 0,
5141 npats: 0,
5142 nref: 0,
5143 prog: Vec::new(),
5144 strs: None,
5145 pats: Vec::new(),
5146 shf: None,
5147 dump: None,
5148});
5149
5150/// Walk every ZshRedir in the program and, for any with a `heredoc_idx`,
5151/// pull the body+terminator out of `bodies` and stuff into `heredoc`.
5152/// `bodies[i]` corresponds to the i-th heredoc registered by the lexer
5153/// during scanning (in source order).
5154fn fill_heredoc_bodies(prog: &mut ZshProgram, bodies: &[HereDocInfo]) {
5155 for list in &mut prog.lists {
5156 fill_in_sublist(&mut list.sublist, bodies);
5157 }
5158}
5159
5160fn fill_in_sublist(sub: &mut ZshSublist, bodies: &[HereDocInfo]) {
5161 fill_in_pipe(&mut sub.pipe, bodies);
5162 if let Some(next) = &mut sub.next {
5163 fill_in_sublist(&mut next.1, bodies);
5164 }
5165}
5166
5167fn fill_in_pipe(pipe: &mut ZshPipe, bodies: &[HereDocInfo]) {
5168 fill_in_command(&mut pipe.cmd, bodies);
5169 if let Some(next) = &mut pipe.next {
5170 fill_in_pipe(next, bodies);
5171 }
5172}
5173
5174fn fill_in_command(cmd: &mut ZshCommand, bodies: &[HereDocInfo]) {
5175 match cmd {
5176 ZshCommand::Simple(s) => {
5177 for r in &mut s.redirs {
5178 if let Some(idx) = r.heredoc_idx {
5179 if let Some(info) = bodies.get(idx) {
5180 r.heredoc = Some(info.clone());
5181 }
5182 }
5183 }
5184 }
5185 ZshCommand::Subsh(p) | ZshCommand::Cursh(p) => fill_heredoc_bodies(p, bodies),
5186 ZshCommand::FuncDef(f) => fill_heredoc_bodies(&mut f.body, bodies),
5187 ZshCommand::If(i) => {
5188 fill_heredoc_bodies(&mut i.cond, bodies);
5189 fill_heredoc_bodies(&mut i.then, bodies);
5190 for (c, b) in &mut i.elif {
5191 fill_heredoc_bodies(c, bodies);
5192 fill_heredoc_bodies(b, bodies);
5193 }
5194 if let Some(e) = &mut i.else_ {
5195 fill_heredoc_bodies(e, bodies);
5196 }
5197 }
5198 ZshCommand::While(w) | ZshCommand::Until(w) => {
5199 fill_heredoc_bodies(&mut w.cond, bodies);
5200 fill_heredoc_bodies(&mut w.body, bodies);
5201 }
5202 ZshCommand::For(f) => fill_heredoc_bodies(&mut f.body, bodies),
5203 ZshCommand::Case(c) => {
5204 for arm in &mut c.arms {
5205 fill_heredoc_bodies(&mut arm.body, bodies);
5206 }
5207 }
5208 ZshCommand::Repeat(r) => fill_heredoc_bodies(&mut r.body, bodies),
5209 ZshCommand::Time(Some(sublist)) => fill_in_sublist(sublist, bodies),
5210 ZshCommand::Try(t) => {
5211 fill_heredoc_bodies(&mut t.try_block, bodies);
5212 fill_heredoc_bodies(&mut t.always, bodies);
5213 }
5214 ZshCommand::Redirected(inner, redirs) => {
5215 for r in redirs {
5216 if let Some(idx) = r.heredoc_idx {
5217 if let Some(info) = bodies.get(idx) {
5218 r.heredoc = Some(info.clone());
5219 }
5220 }
5221 }
5222 fill_in_command(inner, bodies);
5223 }
5224 ZshCommand::Time(None) | ZshCommand::Cond(_) | ZshCommand::Arith(_) => {}
5225 }
5226}
5227
5228/// If `list` is a Simple containing one word that ends in the
5229/// `<Inpar><Outpar>` token pair (the lexer-port encoding of `()`),
5230/// return the bare name. Used by `parse_program_until` to detect
5231/// `name() {body}` style function definitions where the lexer
5232/// hasn't split the `()` from the name.
5233/// Detect the `name() …` shape inside a Simple. Returns the function
5234/// name and (when the body was already inlined into the same Simple,
5235/// e.g. `foo() echo hi`) the rest of the words as the body's argv.
5236/// Returns None for non-funcdef shapes.
5237fn simple_name_with_inoutpar(list: &ZshList) -> Option<(Vec<String>, Vec<String>)> {
5238 if list.flags.async_ || list.sublist.next.is_some() {
5239 return None;
5240 }
5241 let pipe = &list.sublist.pipe;
5242 if pipe.next.is_some() {
5243 return None;
5244 }
5245 let simple = match &pipe.cmd {
5246 ZshCommand::Simple(s) => s,
5247 _ => return None,
5248 };
5249 if simple.words.is_empty() || !simple.assigns.is_empty() {
5250 return None;
5251 }
5252 let suffix = "\u{88}\u{8a}"; // Inpar + Outpar
5253 // Find the FIRST word ending in `()`. zsh accepts the
5254 // multi-name shorthand `fna fnb fnc() { body }` (parse.c:
5255 // par_funcdef wordlist) — words[0..i-1] are extra names,
5256 // words[i] is `lastname()`. Words after are the body argv
5257 // (one-line shorthand, `name() cmd args`).
5258 let par_idx = simple.words.iter().position(|w| w.ends_with(suffix))?;
5259 let mut names: Vec<String> = Vec::with_capacity(par_idx + 1);
5260 for w in &simple.words[..par_idx] {
5261 // Earlier names must be bare identifiers, NOT contain
5262 // tokens that imply they're not function names (no `()`,
5263 // no quotes, no expansions). zsh's lexer enforces this
5264 // at the wordlist level; we approximate by requiring the
5265 // word be an identifier-shaped token after untokenize.
5266 let bare = super::lex::untokenize(w);
5267 let valid = !bare.is_empty()
5268 && bare
5269 .chars()
5270 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '$');
5271 if !valid {
5272 return None;
5273 }
5274 names.push(bare);
5275 }
5276 let last = &simple.words[par_idx];
5277 let bare = &last[..last.len() - suffix.len()];
5278 if bare.is_empty() {
5279 return None;
5280 }
5281 names.push(super::lex::untokenize(bare));
5282 let rest = simple.words[par_idx + 1..].to_vec();
5283 Some((names, rest))
5284}
5285
5286/// Initialize parser state for a fresh parse of `input`.
5287/// Free-fn entry point — resets parser thread_locals and loads input.
5288pub fn parse_init(input: &str) {
5289 // Seed the option defaults the parser/lexer inspect. Real zsh
5290 // installs these via `install_emulation_defaults` (options.c:172)
5291 // at shell startup; zshrs's parse-only test entry path bypasses
5292 // init_main, so we mirror the `zsh` emulation defaults here.
5293 //
5294 // Under `cfg(test)` (lib unit tests share one process) we
5295 // unconditionally OVERWRITE these on every parse_init so cross-
5296 // test option pollution (a prior test that flipped one of these
5297 // and panicked before its restore ran) doesn't leak into the
5298 // parser's reserved-word recognition / one-liner detection.
5299 //
5300 // In the REAL shell this overwrite is WRONG: parse_init runs for
5301 // every -c string, eval, and cmd-subst body, so resetting
5302 // `posixbuiltins` here silently wiped `zshrs -o POSIX_BUILTINS`
5303 // and made `setopt posix_builtins` evaporate at the next parse
5304 // (A04redirect.ztst POSIX_BUILTINS chunks). C zsh's parser READS
5305 // options; it never writes them. Production seeds only entries
5306 // that are missing entirely (parse-only test paths that bypass
5307 // init_main still get the zsh-emulation defaults).
5308 let overwrite = cfg!(test);
5309 for (name, default) in [
5310 ("shortloops", true),
5311 ("shortrepeat", false),
5312 ("multifuncdef", true),
5313 ("aliasfuncdef", false),
5314 ("ignorebraces", false),
5315 ("cshjunkieloops", false),
5316 ("posixbuiltins", false),
5317 ("execopt", true),
5318 ("kshautoload", false),
5319 ("aliases", true),
5320 ] {
5321 if overwrite || crate::ported::options::opt_state_get(name).is_none() {
5322 crate::ported::options::opt_state_set(name, default);
5323 }
5324 }
5325 lex_init(input);
5326}
5327
5328/// P9b decoder (wordcode-pipeline variant): direct port of
5329/// `ecgetstr(Estate s, int dup, int *tokflag)` from
5330/// `Src/parse.c:2855-2890`. Reads a wordcode at `pc`, decodes the
5331/// encoded string back to owned String. Returns (string,
5332/// pc_after_consumed). Distinct from the existing `ecgetstr` (which
5333/// takes a separate strs buffer for text.rs) — this variant uses
5334/// the live ECSTRS_REVERSE HashMap populated at ecstrcode time.
5335pub fn ecgetstr_wordcode(buf: &[u32], pc: usize) -> (String, usize) {
5336 if pc >= buf.len() {
5337 return (String::new(), pc);
5338 }
5339 let c = buf[pc];
5340 let next = pc + 1;
5341 // parse.c:2862-2863 — empty-string sentinels.
5342 if c == 6 || c == 7 {
5343 return (String::new(), next);
5344 }
5345 // parse.c:2864-2871 — inline-packed short string.
5346 if (c & 2) != 0 {
5347 let b0 = ((c >> 3) & 0xff) as u8;
5348 let b1 = ((c >> 11) & 0xff) as u8;
5349 let b2 = ((c >> 19) & 0xff) as u8;
5350 let mut bytes: Vec<u8> = Vec::new();
5351 for b in [b0, b1, b2] {
5352 if b == 0 {
5353 break;
5354 }
5355 bytes.push(b);
5356 }
5357 return (String::from_utf8_lossy(&bytes).into_owned(), next);
5358 }
5359 // parse.c:2872-2873 — long string via offs lookup. Map value is
5360 // metafied Vec<u8>; convert back to display String. Unmetafy is
5361 // the caller's job (the wordcode-parity dumper does it; other
5362 // callers may want raw bytes).
5363 let s = ECSTRS_REVERSE
5364 .with_borrow(|m| m.get(&c).cloned())
5365 .map(|v| String::from_utf8_lossy(&v).into_owned())
5366 .unwrap_or_default();
5367 (s, next)
5368}
5369
5370/// Parse the complete input. Direct port of `parse_event` /
5371/// `par_list` from `Src/parse.c:614-720`. On syntax error,
5372/// sets `errflag |= ERRFLAG_ERROR` (via `zerr`) and returns the
5373/// partial program — callers check `errflag` to detect failure,
5374/// matching C's `Eprog parse_event(...)` + `if (errflag) {...}`.
5375pub fn parse() -> ZshProgram {
5376 zshlex();
5377
5378 let mut program = parse_program_until(None, false);
5379
5380 // Post-pass: wire heredoc bodies (collected by the inline NEWLIN
5381 // walk in zshlex into LEX_HEREDOCS) back into ZshRedir.heredoc
5382 // fields via heredoc_idx. No C analog — LEX_HEREDOCS is the
5383 // Rust-only AST-glue Vec.
5384 let bodies: Vec<HereDocInfo> = LEX_HEREDOCS
5385 .with_borrow(|v| v.clone())
5386 .into_iter()
5387 .map(|h| HereDocInfo {
5388 content: h.content,
5389 terminator: h.terminator,
5390 quoted: h.quoted,
5391 })
5392 .collect();
5393 if !bodies.is_empty() {
5394 fill_heredoc_bodies(&mut program, &bodies);
5395 }
5396
5397 program
5398}
5399
5400/// Wordcode-emission top-level driver. Closest C analog is
5401/// `parse_list(void)` at `Src/parse.c:697-712`: init_parse +
5402/// zshlex + par_list(&c) + bld_eprog. This entry omits init_parse
5403/// and bld_eprog (caller responsibilities) and inlines a guard
5404/// loop around par_list_wordcode for cases where the lexer leaves
5405/// a non-ENDINPUT terminator (LEXERR, missing close-token, etc.).
5406pub fn par_event_wordcode() -> usize {
5407 let start = ECUSED.get() as usize;
5408 // C `parse_list` (parse.c:697-712) calls par_list ONCE — par_list's
5409 // own goto-rec loop handles all SEPER-separated sublists. The
5410 // outer loop here exists for safety against early-return cases
5411 // (LEXERR, missing terminator) but normally par_list_wordcode
5412 // consumes everything in one call.
5413 let mut cmplx: i32 = 0;
5414 while tok() != ENDINPUT && tok() != LEXERR {
5415 par_list_wordcode(&mut cmplx);
5416 match tok() {
5417 SEMI | NEWLIN | AMPER | AMPERBANG | SEPER => {
5418 zshlex();
5419 }
5420 _ => break,
5421 }
5422 }
5423 // No trailing `ecadd(WCB_END())` here: C's `par_list` (c:769)
5424 // emits none — the single terminating `WCB_END` comes from
5425 // `bld_eprog` (c:555). Emitting one here too made every program
5426 // one word longer than C's (double END), breaking .zwc dump
5427 // byte-parity with `zcompile`.
5428 start
5429}
5430
5431/// Port of `par_list(int *cmplx)` from `Src/parse.c:769-803`.
5432/// `list : { SEPER } [ sublist [ { SEPER | AMPER | AMPERBANG } list ] ]`.
5433/// True line-by-line port: takes `cmplx: &mut i32` matching C's
5434/// `int *cmplx` out-parameter, uses stack-local `c` per iteration
5435/// like C (so inner sublist cmplx is independent of outer).
5436pub fn par_list_wordcode(cmplx: &mut i32) {
5437 // c:773 — `int p, lp = -1, c;`
5438 let mut p: usize;
5439 let mut lp: i32 = -1;
5440 let mut c: i32;
5441 loop {
5442 // c:775 `rec:` — c:777-778 `while (tok == SEPER) zshlex();`
5443 while tok() == SEPER {
5444 zshlex();
5445 }
5446 // c:780 — `p = ecadd(0);`
5447 p = ecadd(0);
5448 // c:781 — `c = 0;`
5449 c = 0;
5450 // c:783 — `if (par_sublist(&c)) { ... }`
5451 if par_sublist_wordcode(&mut c) {
5452 // c:784 — `*cmplx |= c;`
5453 *cmplx |= c;
5454 // c:785 — `if (tok == SEPER || tok == AMPER || tok == AMPERBANG)`
5455 let t = tok();
5456 if t == SEPER || t == AMPER || t == AMPERBANG {
5457 // c:786-787 — `if (tok != SEPER) *cmplx = 1;`
5458 if t != SEPER {
5459 *cmplx = 1;
5460 }
5461 // c:788-790 — `set_list_code(p, ..., c);`
5462 let z = if t == SEPER {
5463 Z_SYNC
5464 } else if t == AMPER {
5465 Z_ASYNC
5466 } else {
5467 Z_ASYNC | Z_DISOWN
5468 };
5469 set_list_code(p, z, c != 0);
5470 // c:791 — `incmdpos = 1;`
5471 set_incmdpos(true);
5472 // c:792-794 — `do { zshlex(); } while (tok == SEPER);`
5473 loop {
5474 zshlex();
5475 if tok() != SEPER {
5476 break;
5477 }
5478 }
5479 // c:795 — `lp = p;` c:796 — `goto rec;`
5480 lp = p as i32;
5481 continue;
5482 } else {
5483 // c:798 — `set_list_code(p, (Z_SYNC | Z_END), c);`
5484 set_list_code(p, Z_SYNC | Z_END, c != 0);
5485 }
5486 } else {
5487 // c:800-802 — `ecused--; if (lp >= 0) ecbuf[lp] |= wc_bdata(Z_END);`
5488 ECUSED.set((ECUSED.get() - 1).max(0));
5489 if lp >= 0 {
5490 ECBUF.with_borrow_mut(|b| {
5491 if (lp as usize) < b.len() {
5492 b[lp as usize] |= wc_bdata(Z_END as wordcode);
5493 }
5494 });
5495 }
5496 }
5497 break;
5498 }
5499}
5500
5501/// Port of `par_list1(int *cmplx)` from `Src/parse.c:806-817`.
5502/// Single-sublist variant used by funcdef bodies and the short
5503/// `for`/`while`/`repeat` forms — exactly one sublist with
5504/// `Z_SYNC|Z_END`, no chain.
5505pub fn par_list1_wordcode(cmplx: &mut i32) {
5506 // c:810 — `int p = ecadd(0), c = 0;`
5507 let p = ecadd(0);
5508 let mut c: i32 = 0;
5509 // c:812 — `if (par_sublist(&c)) { ... }`
5510 if par_sublist_wordcode(&mut c) {
5511 // c:813 — `set_list_code(p, (Z_SYNC | Z_END), c);`
5512 set_list_code(p, Z_SYNC | Z_END, c != 0);
5513 // c:814 — `*cmplx |= c;`
5514 *cmplx |= c;
5515 } else {
5516 // c:816 — `ecused--;`
5517 ECUSED.set((ECUSED.get() - 1).max(0));
5518 }
5519}
5520
5521/// Port of `par_save_list(C)` macro from `Src/parse.c:475-480`.
5522/// do { int eu = ecused; par_list(C); if (eu == ecused) ecadd(WCB_END()); } while (0)
5523pub fn par_save_list_wordcode(cmplx: &mut i32) {
5524 let eu = ECUSED.get();
5525 par_list_wordcode(cmplx);
5526 if ECUSED.get() == eu {
5527 ecadd(WCB_END());
5528 }
5529}
5530
5531/// Port of `par_save_list1(C)` macro from `Src/parse.c:481-486`.
5532pub fn par_save_list1_wordcode(cmplx: &mut i32) {
5533 let eu = ECUSED.get();
5534 par_list1_wordcode(cmplx);
5535 if ECUSED.get() == eu {
5536 ecadd(WCB_END());
5537 }
5538}
5539
5540/// Port of `par_sublist(int *cmplx)` from `Src/parse.c:823-865`.
5541/// `sublist : sublist2 [ ( DBAR | DAMPER ) { SEPER } sublist ]`.
5542/// Emits a WCB_SUBLIST header, recurses into par_sublist2 for
5543/// the !/coproc prefix + pipeline, then chains via DBAR (`||`)
5544/// or DAMPER (`&&`) recursively. Returns true if at least one
5545/// pipeline was emitted.
5546pub fn par_sublist_wordcode(cmplx: &mut i32) -> bool {
5547 // c:827 — `int f, p, c = 0;`
5548 let mut c: i32 = 0;
5549 // c:829 — `p = ecadd(0);`
5550 let p = ecadd(0);
5551 // c:831 — `if ((f = par_sublist2(&c)) != -1) { ... }`
5552 match par_sublist2(&mut c) {
5553 Some(f) => {
5554 // c:832 — `int e = ecused;`
5555 let e = ECUSED.get() as usize;
5556 // c:834 — `*cmplx |= c;`
5557 *cmplx |= c;
5558 if tok() == DBAR || tok() == DAMPER {
5559 // c:836 — `enum lextok qtok = tok;`
5560 let qtok = tok();
5561 // c:839 — `cmdpush(tok == DBAR ? CS_CMDOR : CS_CMDAND);`
5562 cmdpush(if qtok == DBAR {
5563 CS_CMDOR as u8
5564 } else {
5565 CS_CMDAND as u8
5566 });
5567 // c:840 — `zshlex();`
5568 zshlex();
5569 // c:841-842 — `while (tok == SEPER) zshlex();`
5570 while tok() == SEPER {
5571 zshlex();
5572 }
5573 // c:843 — `sl = par_sublist(cmplx);`
5574 let sl = par_sublist_wordcode(cmplx);
5575 // c:844-847 — `set_sublist_code(p, (sl ? ... : WC_SUBLIST_END),
5576 // f, (e - 1 - p), c);`
5577 let st = if sl {
5578 if qtok == DBAR {
5579 WC_SUBLIST_OR
5580 } else {
5581 WC_SUBLIST_AND
5582 }
5583 } else {
5584 WC_SUBLIST_END
5585 };
5586 set_sublist_code(p, st as i32, f, (e - 1 - p) as i32, c != 0);
5587 // c:848 — `cmdpop();`
5588 cmdpop();
5589 } else {
5590 // c:850-853 — `if (tok == AMPER || tok == AMPERBANG)
5591 // { c = 1; *cmplx |= c; }`
5592 if tok() == AMPER || tok() == AMPERBANG {
5593 c = 1;
5594 *cmplx |= c;
5595 }
5596 // c:854 — `set_sublist_code(p, WC_SUBLIST_END, f,
5597 // (e - 1 - p), c);`
5598 set_sublist_code(p, WC_SUBLIST_END as i32, f, (e - 1 - p) as i32, c != 0);
5599 }
5600 // c:856 — `return 1;`
5601 true
5602 }
5603 None => {
5604 // c:858-859 — `ecused--; return 0;`
5605 ECUSED.set((ECUSED.get() - 1).max(0));
5606 false
5607 }
5608 }
5609}
5610
5611/// Port of `par_pline(int *cmplx)` from `Src/parse.c:894-955`.
5612/// `pline : cmd [ ( BAR | BARAMP ) { SEPER } pline ]`. Emits a
5613/// WCB_PIPE header (mid for chain links, end for the last cmd)
5614/// plus the optional BARAMP `2>&1` synthetic redir.
5615/// Port of `par_pline(int *cmplx)` from `Src/parse.c:893-947`.
5616/// (Named `par_pipe_wordcode` to disambiguate from the AST
5617/// `par_pline` at parse.rs:3744 — semantically the same `pline`
5618/// production.)
5619pub fn par_pipe_wordcode(cmplx: &mut i32) -> bool {
5620 // c:897 — `zlong line = toklineno;`
5621 let line = toklineno() as i64;
5622 // c:899 — `p = ecadd(0);`
5623 let p = ecadd(0);
5624 // c:901-904 — `if (!par_cmd(cmplx, 0)) { ecused--; return 0; }`
5625 if !par_cmd_wordcode(cmplx, 0) {
5626 ECUSED.set((ECUSED.get() - 1).max(0));
5627 return false;
5628 }
5629 if tok() == BAR_TOK {
5630 // c:906 — `*cmplx = 1;`
5631 *cmplx = 1;
5632 // c:907 — `cmdpush(CS_PIPE);`
5633 cmdpush(CS_PIPE as u8);
5634 // c:908 — `zshlex();`
5635 zshlex();
5636 // c:909-910 — `while (tok == SEPER) zshlex();`
5637 while tok() == SEPER {
5638 zshlex();
5639 }
5640 // c:911 — `ecbuf[p] = WCB_PIPE(WC_PIPE_MID, line>=0 ? line+1 : 0);`
5641 ECBUF.with_borrow_mut(|b| {
5642 if p < b.len() {
5643 b[p] = WCB_PIPE(
5644 WC_PIPE_MID,
5645 if line >= 0 { (line + 1) as wordcode } else { 0 },
5646 );
5647 }
5648 });
5649 // c:912 — `ecispace(p+1, 1);`
5650 ecispace(p + 1, 1);
5651 // c:913 — `ecbuf[p+1] = ecused - 1 - p;`
5652 let used = ECUSED.get() as usize;
5653 ECBUF.with_borrow_mut(|b| {
5654 if p + 1 < b.len() {
5655 b[p + 1] = (used.saturating_sub(1 + p)) as wordcode;
5656 }
5657 });
5658 // c:914-916 — `if (!par_pline(cmplx)) { tok = LEXERR; }`
5659 if !par_pipe_wordcode(cmplx) {
5660 set_tok(LEXERR);
5661 }
5662 // c:917 — `cmdpop();`
5663 cmdpop();
5664 true
5665 } else if tok() == BARAMP {
5666 // c:920-923 — walk past inline WC_REDIR to find r.
5667 let mut r = p + 1;
5668 loop {
5669 let code = ECBUF.with_borrow(|b| b.get(r).copied().unwrap_or(0));
5670 if wc_code(code) != WC_REDIR {
5671 break;
5672 }
5673 r += WC_REDIR_WORDS(code) as usize;
5674 }
5675 // c:925-928 — `ecispace(r, 3);` + synthetic `2>&1` redir
5676 ecispace(r, 3);
5677 ECBUF.with_borrow_mut(|b| {
5678 if r + 2 < b.len() {
5679 b[r] = WCB_REDIR(REDIR_MERGEOUT as wordcode);
5680 b[r + 1] = 2;
5681 b[r + 2] = ecstrcode("1");
5682 }
5683 });
5684 // c:930 — `*cmplx = 1;`
5685 *cmplx = 1;
5686 cmdpush(CS_ERRPIPE as u8);
5687 zshlex();
5688 while tok() == SEPER {
5689 zshlex();
5690 }
5691 ECBUF.with_borrow_mut(|b| {
5692 if p < b.len() {
5693 b[p] = WCB_PIPE(
5694 WC_PIPE_MID,
5695 if line >= 0 { (line + 1) as wordcode } else { 0 },
5696 );
5697 }
5698 });
5699 ecispace(p + 1, 1);
5700 let used = ECUSED.get() as usize;
5701 ECBUF.with_borrow_mut(|b| {
5702 if p + 1 < b.len() {
5703 b[p + 1] = (used.saturating_sub(1 + p)) as wordcode;
5704 }
5705 });
5706 if !par_pipe_wordcode(cmplx) {
5707 set_tok(LEXERR);
5708 }
5709 cmdpop();
5710 true
5711 } else {
5712 // c:944 — `ecbuf[p] = WCB_PIPE(WC_PIPE_END, line>=0 ? line+1 : 0);`
5713 ECBUF.with_borrow_mut(|b| {
5714 if p < b.len() {
5715 b[p] = WCB_PIPE(
5716 WC_PIPE_END,
5717 if line >= 0 { (line + 1) as wordcode } else { 0 },
5718 );
5719 }
5720 });
5721 true
5722 }
5723}
5724
5725/// Port of `par_cmd(int *cmplx, int zsh_construct)` from
5726/// `Src/parse.c:958-1085`. Parses leading + trailing redirs and
5727/// dispatches on the current token to the right par_* builder.
5728/// Returns false only when no command was emitted (no redirs +
5729/// par_simple returned 0).
5730/// Port of `par_cmd(int *cmplx, int zsh_construct)` from
5731/// `Src/parse.c:957-1077`.
5732pub fn par_cmd_wordcode(cmplx: &mut i32, zsh_construct: i32) -> bool {
5733 // c:960 — `int r, nr = 0;`
5734 let mut nr: i32 = 0;
5735 // c:962 — `r = ecused;`
5736 let mut r: usize = ECUSED.get() as usize;
5737 // c:964-968 — leading redirs.
5738 if IS_REDIROP(tok()) {
5739 // c:965 — `*cmplx = 1;`
5740 *cmplx = 1;
5741 // c:966-968 — `while (IS_REDIROP(tok)) { nr += par_redir(&r, NULL); }`
5742 while IS_REDIROP(tok()) {
5743 nr += par_redir_wordcode(&mut r, None);
5744 }
5745 }
5746 // c:970-1066 — token-dispatch switch.
5747 match tok() {
5748 FOR => {
5749 cmdpush(CS_FOR as u8);
5750 par_for_wordcode(cmplx);
5751 cmdpop();
5752 }
5753 FOREACH => {
5754 cmdpush(CS_FOREACH as u8);
5755 par_for_wordcode(cmplx);
5756 cmdpop();
5757 }
5758 SELECT => {
5759 // c:982 — `*cmplx = 1;`
5760 *cmplx = 1;
5761 cmdpush(CS_SELECT as u8);
5762 par_for_wordcode(cmplx);
5763 cmdpop();
5764 }
5765 CASE => {
5766 cmdpush(CS_CASE as u8);
5767 par_case_wordcode(cmplx);
5768 cmdpop();
5769 }
5770 IF => {
5771 par_if_wordcode(cmplx);
5772 }
5773 WHILE => {
5774 cmdpush(CS_WHILE as u8);
5775 par_while_wordcode(cmplx);
5776 cmdpop();
5777 }
5778 UNTIL => {
5779 cmdpush(CS_UNTIL as u8);
5780 par_while_wordcode(cmplx);
5781 cmdpop();
5782 }
5783 REPEAT => {
5784 cmdpush(CS_REPEAT as u8);
5785 par_repeat_wordcode(cmplx);
5786 cmdpop();
5787 }
5788 INPAR_TOK => {
5789 // c:1011 — `*cmplx = 1;`
5790 *cmplx = 1;
5791 cmdpush(CS_SUBSH as u8);
5792 par_subsh_wordcode(cmplx, zsh_construct);
5793 cmdpop();
5794 }
5795 INBRACE_TOK => {
5796 cmdpush(CS_CURSH as u8);
5797 par_subsh_wordcode(cmplx, zsh_construct);
5798 cmdpop();
5799 }
5800 FUNC => {
5801 cmdpush(CS_FUNCDEF as u8);
5802 par_funcdef_wordcode(cmplx);
5803 cmdpop();
5804 }
5805 DINBRACK => {
5806 cmdpush(CS_COND as u8);
5807 par_cond_wordcode();
5808 cmdpop();
5809 }
5810 DINPAR => {
5811 par_arith_wordcode();
5812 }
5813 TIME => {
5814 // c:1037-1050 — `static int inpartime` guard so
5815 // `time time foo` doesn't recurse infinitely.
5816 if !PARSER_INPARTIME.with(|c| c.get()) {
5817 // c:1041 — `*cmplx = 1;`
5818 *cmplx = 1;
5819 PARSER_INPARTIME.with(|c| c.set(true));
5820 par_time_wordcode();
5821 PARSER_INPARTIME.with(|c| c.set(false));
5822 } else {
5823 set_tok(STRING_LEX);
5824 let sr = par_simple_wordcode(cmplx, nr);
5825 if sr == 0 && nr == 0 {
5826 return false;
5827 }
5828 if sr > 1 {
5829 *cmplx = 1;
5830 r += (sr - 1) as usize;
5831 }
5832 }
5833 }
5834 _ => {
5835 // c:1054 — `if (!(sr = par_simple(cmplx, nr)))`
5836 let sr = par_simple_wordcode(cmplx, nr);
5837 if sr == 0 {
5838 if nr == 0 {
5839 return false;
5840 }
5841 } else if sr > 1 {
5842 // c:1060-1061 — `*cmplx = 1; r += sr - 1;`
5843 *cmplx = 1;
5844 r += (sr - 1) as usize;
5845 }
5846 }
5847 }
5848 // c:1067-1071 — trailing redirs.
5849 // c:1067 — `if (IS_REDIROP(tok)) { *cmplx = 1; while (...) (void)par_redir(&r, NULL); }`
5850 if IS_REDIROP(tok()) {
5851 *cmplx = 1;
5852 while IS_REDIROP(tok()) {
5853 let _ = par_redir_wordcode(&mut r, None);
5854 }
5855 }
5856 // c:1072-1075 — `incmdpos=1; incasepat=0; incond=0; intypeset=0;`
5857 set_incmdpos(true);
5858 set_incasepat(0);
5859 set_incond(0);
5860 set_intypeset(false);
5861 let _ = r;
5862 // c:1076 — `return 1;`
5863 true
5864}
5865
5866/// Port of `par_for(int *cmplx)` from `Src/parse.c:1086-1198`.
5867pub fn par_for_wordcode(cmplx: &mut i32) {
5868 // c:1089 — `int oecused = ecused, csh = (tok == FOREACH), p, sel = (tok == SELECT);`
5869 let _oecused = ECUSED.get() as usize;
5870 let csh = tok() == FOREACH;
5871 let sel = tok() == SELECT;
5872 let p: usize;
5873 // c:1090 — `int type;`
5874 let r#type: wordcode;
5875
5876 // c:1092 — `p = ecadd(0);`
5877 p = ecadd(0);
5878
5879 // c:1094 — `incmdpos = 0;`
5880 set_incmdpos(false);
5881 // c:1095 — `infor = tok == FOR ? 2 : 0;`
5882 set_infor(if tok() == FOR { 2 } else { 0 });
5883 // c:1096 — `zshlex();`
5884 zshlex();
5885 // c:1097 — `if (tok == DINPAR) {`
5886 if tok() == DINPAR {
5887 // c:1098 — `zshlex();`
5888 zshlex();
5889 // c:1099-1100 — `if (tok != DINPAR) YYERRORV(oecused);`
5890 if tok() != DINPAR {
5891 zerr("par_for: expected init");
5892 return;
5893 }
5894 // c:1101 — `ecstr(tokstr);`
5895 ecstr(&tokstr().unwrap_or_default());
5896 // c:1102 — `zshlex();`
5897 zshlex();
5898 // c:1103-1104
5899 if tok() != DINPAR {
5900 zerr("par_for: expected cond");
5901 return;
5902 }
5903 // c:1105
5904 ecstr(&tokstr().unwrap_or_default());
5905 // c:1106
5906 zshlex();
5907 // c:1107-1108
5908 if tok() != DOUTPAR {
5909 zerr("par_for: expected ))");
5910 return;
5911 }
5912 // c:1109
5913 ecstr(&tokstr().unwrap_or_default());
5914 // c:1110 — `infor = 0;`
5915 set_infor(0);
5916 // c:1111 — `incmdpos = 1;`
5917 set_incmdpos(true);
5918 // c:1112 — `zshlex();`
5919 zshlex();
5920 // c:1113 — `type = WC_FOR_COND;`
5921 r#type = WC_FOR_COND;
5922 } else {
5923 // c:1115 — `int np = 0, n, posix_in, ona = noaliases, onc = nocorrect;`
5924 let mut np: usize = 0;
5925 let mut n: u32;
5926 let posix_in: bool;
5927 let ona = noaliases();
5928 let onc = nocorrect();
5929 // c:1116 — `infor = 0;`
5930 set_infor(0);
5931 // c:1117-1118 — `if (tok != STRING || !isident(tokstr)) YYERRORV(oecused);`
5932 if tok() != STRING_LEX || !crate::ported::params::isident(&tokstr().unwrap_or_default()) {
5933 zerr("par_for: expected identifier");
5934 return;
5935 }
5936 // c:1119-1120 — `if (!sel) np = ecadd(0);`
5937 if !sel {
5938 np = ecadd(0);
5939 }
5940 // c:1121 — `n = 0;`
5941 n = 0;
5942 // c:1122 — `incmdpos = 1;`
5943 set_incmdpos(true);
5944 // c:1123 — `noaliases = nocorrect = 1;`
5945 set_noaliases(true);
5946 set_nocorrect(1);
5947 // c:1124 — `for (;;) {`
5948 loop {
5949 // c:1125 — `n++;`
5950 n += 1;
5951 // c:1126 — `ecstr(tokstr);`
5952 ecstr(&tokstr().unwrap_or_default());
5953 // c:1127 — `zshlex();`
5954 zshlex();
5955 // c:1128-1129 — `if (tok != STRING || !strcmp(tokstr, "in") || sel) break;`
5956 if tok() != STRING_LEX || tokstr().as_deref() == Some("in") || sel {
5957 break;
5958 }
5959 // c:1130-1135 — `if (!isident(tokstr) || errflag) { ... YYERRORV; }`
5960 if !crate::ported::params::isident(&tokstr().unwrap_or_default())
5961 || (errflag.load(Ordering::Relaxed) & 1) != 0
5962 {
5963 set_noaliases(ona);
5964 set_nocorrect(onc);
5965 zerr("par_for: expected identifier in name list");
5966 return;
5967 }
5968 }
5969 // c:1137-1138 — `noaliases = ona; nocorrect = onc;`
5970 set_noaliases(ona);
5971 set_nocorrect(onc);
5972 // c:1139-1140 — `if (!sel) ecbuf[np] = n;`
5973 if !sel {
5974 ECBUF.with_borrow_mut(|b| {
5975 b[np] = n;
5976 });
5977 }
5978 // c:1141 — `posix_in = isnewlin;`
5979 posix_in = isnewlin() != 0;
5980 // c:1142-1143 — `while (isnewlin) zshlex();`
5981 while isnewlin() != 0 {
5982 zshlex();
5983 }
5984 // c:1144 — `if (tok == STRING && !strcmp(tokstr, "in")) {`
5985 if tok() == STRING_LEX && tokstr().as_deref() == Some("in") {
5986 // c:1145 — `incmdpos = 0;`
5987 set_incmdpos(false);
5988 // c:1146 — `zshlex();`
5989 zshlex();
5990 // c:1147 — `np = ecadd(0);`
5991 np = ecadd(0);
5992 // c:1148 — `n = par_wordlist();`
5993 let n2 = par_wordlist_wordcode();
5994 // c:1149-1150 — `if (tok != SEPER) YYERRORV(oecused);`
5995 if tok() != SEPER {
5996 zerr("par_for: expected separator after `in`");
5997 return;
5998 }
5999 // c:1151 — `ecbuf[np] = n;`
6000 ECBUF.with_borrow_mut(|b| {
6001 b[np] = n2 as wordcode;
6002 });
6003 // c:1152 — `type = (sel ? WC_SELECT_LIST : WC_FOR_LIST);`
6004 r#type = if sel { WC_SELECT_LIST } else { WC_FOR_LIST };
6005 } else if !posix_in && tok() == INPAR_TOK {
6006 // c:1153-1154 — `else if (!posix_in && tok == INPAR)`
6007 // c:1154 — `incmdpos = 0;`
6008 set_incmdpos(false);
6009 // c:1155 — `zshlex();`
6010 zshlex();
6011 // c:1156 — `np = ecadd(0);`
6012 np = ecadd(0);
6013 // c:1157 — `n = par_nl_wordlist();`
6014 let n2 = par_nl_wordlist_wordcode();
6015 // c:1158-1159 — `if (tok != OUTPAR) YYERRORV(oecused);`
6016 if tok() != OUTPAR_TOK {
6017 zerr("par_for: expected `)`");
6018 return;
6019 }
6020 // c:1160 — `ecbuf[np] = n;`
6021 ECBUF.with_borrow_mut(|b| {
6022 b[np] = n2 as wordcode;
6023 });
6024 // c:1161 — `incmdpos = 1;`
6025 set_incmdpos(true);
6026 // c:1162 — `zshlex();`
6027 zshlex();
6028 // c:1163 — `type = (sel ? WC_SELECT_LIST : WC_FOR_LIST);`
6029 r#type = if sel { WC_SELECT_LIST } else { WC_FOR_LIST };
6030 } else {
6031 // c:1165 — `type = (sel ? WC_SELECT_PPARAM : WC_FOR_PPARAM);`
6032 r#type = if sel { WC_SELECT_PPARAM } else { WC_FOR_PPARAM };
6033 }
6034 let _ = np;
6035 }
6036 // c:1167 — `incmdpos = 1;`
6037 set_incmdpos(true);
6038 // c:1168-1169 — `while (tok == SEPER) zshlex();`
6039 while tok() == SEPER {
6040 zshlex();
6041 }
6042 // c:1170-1193 — body dispatch (inline in C, factored here for
6043 // reuse by par_while/par_repeat — same control flow, same calls).
6044 par_loop_body_wordcode(cmplx, csh);
6045 // c:1195-1197 — `ecbuf[p] = (sel ? WCB_SELECT(...) : WCB_FOR(...));`
6046 let used = ECUSED.get() as usize;
6047 let off = used.saturating_sub(1 + p) as wordcode;
6048 ECBUF.with_borrow_mut(|b| {
6049 b[p] = if sel {
6050 WCB_SELECT(r#type, off)
6051 } else {
6052 WCB_FOR(r#type, off)
6053 };
6054 });
6055}
6056
6057/// Port of `par_wordlist(void)` from `Src/parse.c:2361-2371` —
6058/// emits wordcode form. Returns the number of strings emitted.
6059fn par_wordlist_wordcode() -> u32 {
6060 // c:2364 — `int num = 0;`
6061 let mut num: u32 = 0;
6062 // c:2365 — `while (tok == STRING) {`
6063 while tok() == STRING_LEX {
6064 // c:2366 — `ecstr(tokstr);`
6065 ecstr(&tokstr().unwrap_or_default());
6066 // c:2367 — `num++;`
6067 num += 1;
6068 // c:2368 — `zshlex();`
6069 zshlex();
6070 }
6071 // c:2370 — `return num;`
6072 num
6073}
6074
6075/// Port of `par_nl_wordlist(void)` from `Src/parse.c:2378-2390` —
6076/// emits wordcode form. Like par_wordlist but tolerates SEPER
6077/// between words.
6078fn par_nl_wordlist_wordcode() -> u32 {
6079 // c:2381 — `int num = 0;`
6080 let mut num: u32 = 0;
6081 // c:2383 — `while (tok == STRING || tok == SEPER) {`
6082 while tok() == STRING_LEX || tok() == SEPER || tok() == NEWLIN {
6083 // c:2384-2387 — `if (tok != SEPER) { ecstr(tokstr); num++; }`
6084 if tok() == STRING_LEX {
6085 ecstr(&tokstr().unwrap_or_default());
6086 num += 1;
6087 }
6088 // c:2388 — `zshlex();`
6089 zshlex();
6090 }
6091 // c:2390 — `return num;`
6092 num
6093}
6094
6095/// Body dispatch shared by par_for / par_while / par_repeat.
6096/// Direct port of `Src/parse.c:1170-1194`.
6097fn par_loop_body_wordcode(cmplx: &mut i32, csh: bool) {
6098 if tok() == DOLOOP {
6099 zshlex();
6100 // c:1172 — `par_save_list(cmplx);`
6101 par_save_list_wordcode(cmplx);
6102 if tok() != DONE {
6103 zerr("missing `done`");
6104 return;
6105 }
6106 set_incmdpos(false);
6107 zshlex();
6108 } else if tok() == INBRACE_TOK {
6109 zshlex();
6110 // c:1179 — `par_save_list(cmplx);`
6111 par_save_list_wordcode(cmplx);
6112 if tok() != OUTBRACE_TOK {
6113 zerr("missing `}`");
6114 return;
6115 }
6116 set_incmdpos(false);
6117 zshlex();
6118 } else if csh || isset(CSHJUNKIELOOPS) {
6119 // c:1185 — `par_save_list(cmplx);`
6120 par_save_list_wordcode(cmplx);
6121 if tok() != ZEND {
6122 zerr("missing `end`");
6123 return;
6124 }
6125 set_incmdpos(false);
6126 zshlex();
6127 } else if unset(SHORTLOOPS) {
6128 zerr("short loop form requires SHORTLOOPS");
6129 } else {
6130 // c:1193 — `par_save_list1(cmplx);`
6131 par_save_list1_wordcode(cmplx);
6132 }
6133}
6134
6135/// `select` shares par_for body (c:983-985 routes SELECT to par_for).
6136pub fn par_select_wordcode(cmplx: &mut i32) {
6137 par_for_wordcode(cmplx);
6138}
6139
6140/// Port of `par_case(int *cmplx)` from `Src/parse.c:1208-1400`.
6141pub fn par_case_wordcode(_cmplx: &mut i32) {
6142 // c:1211 — `int oecused = ecused, brflag, p, pp, palts, type, nalts;`
6143 let _oecused = ECUSED.get() as usize;
6144 let brflag: bool;
6145 let p: usize;
6146 let mut pp: usize;
6147 let mut palts: usize;
6148 let mut r#type: wordcode;
6149 let mut nalts: u32;
6150 // c:1212 — `int ona, onc;`
6151 let ona: bool;
6152 let onc: i32;
6153
6154 // c:1214 — `p = ecadd(0);`
6155 p = ecadd(0);
6156
6157 // c:1216 — `incmdpos = 0;`
6158 set_incmdpos(false);
6159 // c:1217 — `zshlex();`
6160 zshlex();
6161 // c:1218-1219 — `if (tok != STRING) YYERRORV(oecused);`
6162 if tok() != STRING_LEX {
6163 zerr("par_case: expected scrutinee");
6164 return;
6165 }
6166 // c:1220 — `ecstr(tokstr);`
6167 ecstr(&tokstr().unwrap_or_default());
6168
6169 // c:1222 — `incmdpos = 1;`
6170 set_incmdpos(true);
6171 // c:1223-1224 — `ona = noaliases; onc = nocorrect;`
6172 ona = noaliases();
6173 onc = nocorrect();
6174 // c:1225 — `noaliases = nocorrect = 1;`
6175 set_noaliases(true);
6176 set_nocorrect(1);
6177 // c:1226 — `zshlex();`
6178 zshlex();
6179 // c:1227-1228 — `while (tok == SEPER) zshlex();`
6180 while tok() == SEPER {
6181 zshlex();
6182 }
6183 // c:1229 — `if (!(tok == STRING && !strcmp(tokstr, "in")) && tok != INBRACE)`
6184 if !(tok() == STRING_LEX && tokstr().as_deref() == Some("in")) && tok() != INBRACE_TOK {
6185 // c:1231-1233 — restore noaliases/nocorrect + ERROR
6186 set_noaliases(ona);
6187 set_nocorrect(onc);
6188 zerr("par_case: expected `in` or `{`");
6189 return;
6190 }
6191 // c:1235 — `brflag = (tok == INBRACE);`
6192 brflag = tok() == INBRACE_TOK;
6193 // c:1236 — `incasepat = 1;`
6194 set_incasepat(1);
6195 // c:1237 — `incmdpos = 0;`
6196 set_incmdpos(false);
6197 // c:1238-1239 — `noaliases = ona; nocorrect = onc;`
6198 set_noaliases(ona);
6199 set_nocorrect(onc);
6200 // c:1240 — `zshlex();`
6201 zshlex();
6202
6203 // c:1242 — `for (;;) {`
6204 'arms: loop {
6205 // c:1243 — `char *str;`
6206 let mut str: String;
6207 // c:1244 — `int skip_zshlex;`
6208 let skip_zshlex: bool;
6209
6210 // c:1246-1247 — `while (tok == SEPER) zshlex();`
6211 while tok() == SEPER {
6212 zshlex();
6213 }
6214 // c:1248-1249 — `if (tok == OUTBRACE) break;`
6215 if tok() == OUTBRACE_TOK {
6216 break 'arms;
6217 }
6218 // c:1250-1251 — `if (tok == INPAR) zshlex();`
6219 if tok() == INPAR_TOK {
6220 zshlex();
6221 }
6222 // c:1252-1254 — `if (tok == BAR) { str = ""; skip_zshlex = 1; }`
6223 if tok() == BAR_TOK {
6224 str = String::new();
6225 skip_zshlex = true;
6226 } else {
6227 // c:1256-1257 — `if (tok != STRING) YYERRORV(oecused);`
6228 if tok() != STRING_LEX {
6229 zerr("par_case: expected pattern");
6230 return;
6231 }
6232 // c:1258-1259 — `if (!strcmp(tokstr, "esac")) break;`
6233 if tokstr().as_deref() == Some("esac") {
6234 break 'arms;
6235 }
6236 // c:1260 — `str = dupstring(tokstr);`
6237 str = tokstr().unwrap_or_default();
6238 // c:1261 — `skip_zshlex = 0;`
6239 skip_zshlex = false;
6240 }
6241 // c:1263 — `type = WC_CASE_OR;`
6242 r#type = WC_CASE_OR;
6243 // c:1264-1266 — `pp = ecadd(0); palts = ecadd(0); nalts = 0;`
6244 pp = ecadd(0);
6245 palts = ecadd(0);
6246 nalts = 0;
6247 // c:1300 — `incasepat = -1;`
6248 set_incasepat(-1);
6249 // c:1301 — `incmdpos = 1;`
6250 set_incmdpos(true);
6251 // c:1302-1303 — `if (!skip_zshlex) zshlex();`
6252 if !skip_zshlex {
6253 zshlex();
6254 }
6255 // c:1304 — `for (;;) {`
6256 loop {
6257 // c:1305-1313 — `if (tok == OUTPAR) { ecstr(str);
6258 // ecadd(ecnpats++); nalts++; incasepat = 0;
6259 // incmdpos = 1; zshlex(); break; }`
6260 if tok() == OUTPAR_TOK {
6261 ecstr(&str);
6262 let np = ECNPATS.with(|cc| {
6263 let v = cc.get();
6264 cc.set(v + 1);
6265 v
6266 }) as u32;
6267 ecadd(np);
6268 nalts += 1;
6269 set_incasepat(0);
6270 set_incmdpos(true);
6271 zshlex();
6272 break;
6273 }
6274 // c:1314-1320 — `else if (tok == BAR) { ecstr(str);
6275 // ecadd(ecnpats++); nalts++; incasepat = 1;
6276 // incmdpos = 0; }`
6277 else if tok() == BAR_TOK {
6278 ecstr(&str);
6279 let np = ECNPATS.with(|cc| {
6280 let v = cc.get();
6281 cc.set(v + 1);
6282 v
6283 }) as u32;
6284 ecadd(np);
6285 nalts += 1;
6286 set_incasepat(1);
6287 set_incmdpos(false);
6288 }
6289 // c:1321-1357 — else { ... `(...)` whole-pattern hack:
6290 // the lexer absorbed a complete `(...)` as one STRING
6291 // (str[0] == Inpar) and the current tok is already the
6292 // body's first token. Massage blanks around `|`/parens
6293 // at depth 1, validate balance, strip the surrounding
6294 // parens; the remainder IS the pattern. }
6295 else {
6296 use crate::ported::zsh_h::{Bar, Inpar, Outpar};
6297 if nalts == 0 && str.starts_with(Inpar) {
6298 let meta = crate::ported::zsh_h::Meta as char;
6299 let blank = |c: char| c.is_ascii() && crate::ported::ztype_h::iblank(c as u8);
6300 let mut chars: Vec<char> = str.chars().collect();
6301 // c:1323 — `int pct = 0, sl;`
6302 let mut pct = 0i32;
6303 let mut i = 0usize;
6304 // c:1326-1344 — scan/massage loop. `s` ↔ `i`;
6305 // chuck(p) ↔ chars.remove(idx).
6306 let mut early_break = false;
6307 while i < chars.len() {
6308 if chars[i] == Inpar {
6309 pct += 1;
6310 }
6311 // c:1329-1330 — `if (!pct) break;` (char past
6312 // the balanced close → trailing garbage).
6313 if pct == 0 {
6314 early_break = true;
6315 break;
6316 }
6317 if pct == 1 {
6318 // c:1332-1334 — chuck blanks AFTER `|`/`(`.
6319 if chars[i] == Bar || chars[i] == Inpar {
6320 while i + 1 < chars.len() && blank(chars[i + 1]) {
6321 chars.remove(i + 1);
6322 }
6323 }
6324 // c:1335-1338 — chuck blanks BEFORE `|`/`)`
6325 // (not Meta-escaped blanks).
6326 if chars[i] == Bar || chars[i] == Outpar {
6327 while i >= 1
6328 && blank(chars[i - 1])
6329 && (i < 2 || chars[i - 2] != meta)
6330 {
6331 chars.remove(i - 1);
6332 i -= 1;
6333 }
6334 }
6335 }
6336 if chars[i] == Outpar {
6337 pct -= 1;
6338 }
6339 i += 1;
6340 }
6341 // c:1345-1346 — `if (*s || pct || s == str)
6342 // YYERRORV(oecused);`
6343 if early_break || pct != 0 || chars.is_empty() {
6344 zerr("par_case: expected `)` or `|`");
6345 return;
6346 }
6347 // c:1347-1352 — strip surrounding `(...)`.
6348 chars.pop();
6349 chars.remove(0);
6350 let stripped: String = chars.into_iter().collect();
6351 // c:1353-1355 — `ecstr(str); ecadd(ecnpats++); nalts++;`
6352 ecstr(&stripped);
6353 let np = ECNPATS.with(|cc| {
6354 let v = cc.get();
6355 cc.set(v + 1);
6356 v
6357 }) as u32;
6358 ecadd(np);
6359 nalts += 1;
6360 // c:1356 — `break;` — tok is already the body's
6361 // first token; fall through to par_save_list.
6362 break;
6363 }
6364 // c:1358 — `YYERRORV(oecused);`
6365 zerr("par_case: expected `)` or `|`");
6366 return;
6367 }
6368
6369 // c:1359 — `zshlex();`
6370 zshlex();
6371 // c:1360-1377 — switch on next tok.
6372 match tok() {
6373 STRING_LEX => {
6374 // c:1361-1365
6375 str = tokstr().unwrap_or_default();
6376 zshlex();
6377 }
6378 OUTPAR_TOK | BAR_TOK => {
6379 // c:1367-1371 — empty string
6380 str = String::new();
6381 }
6382 _ => {
6383 // c:1374-1376 — `YYERRORV(oecused);`
6384 zerr("par_case: expected pattern, `)` or `|`");
6385 return;
6386 }
6387 }
6388 }
6389 // c:1379 — `incasepat = 0;`
6390 set_incasepat(0);
6391 // c:1380 — `par_save_list(cmplx);`
6392 par_save_list_wordcode(_cmplx);
6393 // c:1381-1384 — terminator → arm type
6394 if tok() == SEMIAMP {
6395 r#type = WC_CASE_AND;
6396 } else if tok() == SEMIBAR {
6397 r#type = WC_CASE_TESTAND;
6398 }
6399 // c:1385 — `ecbuf[pp] = WCB_CASE(type, ecused - 1 - pp);`
6400 let used = ECUSED.get() as usize;
6401 ECBUF.with_borrow_mut(|b| {
6402 b[pp] = WCB_CASE(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6403 });
6404 // c:1386 — `ecbuf[palts] = nalts;`
6405 ECBUF.with_borrow_mut(|b| {
6406 b[palts] = nalts;
6407 });
6408 // c:1387-1388 — terminator (ESAC w/o brace OR OUTBRACE w/ brace) → break
6409 if (tok() == ESAC && !brflag) || (tok() == OUTBRACE_TOK && brflag) {
6410 break 'arms;
6411 }
6412 // c:1389-1390 — `if (tok != DSEMI && tok != SEMIAMP && tok != SEMIBAR) YYERRORV;`
6413 if tok() != DSEMI && tok() != SEMIAMP && tok() != SEMIBAR {
6414 zerr("par_case: expected `;;`, `;&`, or `;|`");
6415 return;
6416 }
6417 // c:1391 — `incasepat = 1;`
6418 set_incasepat(1);
6419 // c:1392 — `incmdpos = 0;`
6420 set_incmdpos(false);
6421 // c:1393 — `zshlex();`
6422 zshlex();
6423 }
6424 // c:1395 — `incmdpos = 1;`
6425 set_incmdpos(true);
6426 // c:1396 — `incasepat = 0;`
6427 set_incasepat(0);
6428 // c:1397 — `zshlex();`
6429 zshlex();
6430
6431 // c:1399 — `ecbuf[p] = WCB_CASE(WC_CASE_HEAD, ecused - 1 - p);`
6432 let used = ECUSED.get() as usize;
6433 ECBUF.with_borrow_mut(|b| {
6434 b[p] = WCB_CASE(WC_CASE_HEAD, (used.saturating_sub(1 + p)) as wordcode);
6435 });
6436}
6437
6438/// Port of `par_if(int *cmplx)` from `Src/parse.c:1410-1512`.
6439pub fn par_if_wordcode(cmplx: &mut i32) {
6440 // c:1413 — `int oecused = ecused, p, pp, type, usebrace = 0;`
6441 let _oecused = ECUSED.get() as usize;
6442 let p: usize;
6443 let mut pp: usize = 0;
6444 let mut r#type: wordcode = WC_IF_IF;
6445 let mut usebrace: i32 = 0;
6446 // c:1414 — `enum lextok xtok;`
6447 let mut xtok: lextok;
6448 // c:1415 — `unsigned char nc;`
6449 let nc: u8;
6450 let _ = nc;
6451
6452 // c:1417 — `p = ecadd(0);`
6453 p = ecadd(0);
6454
6455 // c:1419 — `for (;;) {`
6456 loop {
6457 // c:1420 — `xtok = tok;`
6458 xtok = tok();
6459 // c:1421 — `cmdpush(xtok == IF ? CS_IF : CS_ELIF);`
6460 cmdpush(if xtok == IF {
6461 CS_IF as u8
6462 } else {
6463 CS_ELIF as u8
6464 });
6465 // c:1422-1426 — `if (xtok == FI) { incmdpos = 0; zshlex(); break; }`
6466 if xtok == FI {
6467 set_incmdpos(false);
6468 zshlex();
6469 break;
6470 }
6471 // c:1427 — `zshlex();`
6472 zshlex();
6473 // c:1428-1429 — `if (xtok == ELSE) break;`
6474 if xtok == ELSE {
6475 break;
6476 }
6477 // c:1430-1431 — `while (tok == SEPER) zshlex();`
6478 while tok() == SEPER {
6479 zshlex();
6480 }
6481 // c:1432-1435 — `if (!(xtok == IF || xtok == ELIF)) { cmdpop(); YYERRORV; }`
6482 if !(xtok == IF || xtok == ELIF) {
6483 cmdpop();
6484 zerr("par_if: expected `if` or `elif`");
6485 return;
6486 }
6487 // c:1436 — `pp = ecadd(0);`
6488 pp = ecadd(0);
6489 // c:1437 — `type = (xtok == IF ? WC_IF_IF : WC_IF_ELIF);`
6490 r#type = if xtok == IF { WC_IF_IF } else { WC_IF_ELIF };
6491 // c:1438 — `par_save_list(cmplx);` — condition body
6492 par_save_list_wordcode(cmplx);
6493 // c:1439 — `incmdpos = 1;`
6494 set_incmdpos(true);
6495 // c:1440-1443 — `if (tok == ENDINPUT) { cmdpop(); YYERRORV; }`
6496 if tok() == ENDINPUT {
6497 cmdpop();
6498 zerr("par_if: unexpected end-of-input after condition");
6499 return;
6500 }
6501 // c:1444-1445 — `while (tok == SEPER) zshlex();`
6502 while tok() == SEPER {
6503 zshlex();
6504 }
6505 // c:1446 — `xtok = FI;` — pre-set so the post-loop check works
6506 xtok = FI;
6507 // c:1447 — `nc = cmdstack[cmdsp - 1] == CS_IF ? CS_IFTHEN : CS_ELIFTHEN;`
6508 // (Not tracked separately in zshrs cmdstack — derive from cur top
6509 // by reading CMDSTACK; for safety use CS_IFTHEN as default.)
6510 // We don't have a way to read top easily — match by tracking
6511 // whether we just pushed CS_IF or CS_ELIF.
6512 // For wordcode emission this only affects cmdstack debug output;
6513 // not the emitted wordcode. Use CS_IFTHEN.
6514 let nc_local: u8 = CS_IFTHEN as u8;
6515 if tok() == THEN {
6516 // c:1448-1456 — THEN branch
6517 // c:1449 — `usebrace = 0;`
6518 usebrace = 0;
6519 // c:1450 — `cmdpop();`
6520 cmdpop();
6521 // c:1451 — `cmdpush(nc);`
6522 cmdpush(nc_local);
6523 // c:1452 — `zshlex();`
6524 zshlex();
6525 // c:1453 — `par_save_list(cmplx);` — then body
6526 par_save_list_wordcode(cmplx);
6527 // c:1454 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
6528 let used = ECUSED.get() as usize;
6529 ECBUF.with_borrow_mut(|b| {
6530 b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6531 });
6532 // c:1455 — `incmdpos = 1;`
6533 set_incmdpos(true);
6534 // c:1456 — `cmdpop();`
6535 cmdpop();
6536 } else if tok() == INBRACE_TOK {
6537 // c:1457-1473 — INBRACE branch
6538 // c:1458 — `usebrace = 1;`
6539 usebrace = 1;
6540 // c:1459 — `cmdpop();`
6541 cmdpop();
6542 // c:1460 — `cmdpush(nc);`
6543 cmdpush(nc_local);
6544 // c:1461 — `zshlex();`
6545 zshlex();
6546 // c:1462 — `par_save_list(cmplx);`
6547 par_save_list_wordcode(cmplx);
6548 // c:1463-1466 — `if (tok != OUTBRACE) { cmdpop(); YYERRORV; }`
6549 if tok() != OUTBRACE_TOK {
6550 cmdpop();
6551 zerr("par_if: expected `}`");
6552 return;
6553 }
6554 // c:1467 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
6555 let used = ECUSED.get() as usize;
6556 ECBUF.with_borrow_mut(|b| {
6557 b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6558 });
6559 // c:1469 — `zshlex();`
6560 zshlex();
6561 // c:1470 — `incmdpos = 1;`
6562 set_incmdpos(true);
6563 // c:1471-1472 — `if (tok == SEPER) break;`
6564 if tok() == SEPER {
6565 break;
6566 }
6567 // c:1473 — `cmdpop();`
6568 cmdpop();
6569 } else if unset(SHORTLOOPS) {
6570 // c:1474-1476 — `cmdpop(); YYERRORV;`
6571 cmdpop();
6572 zerr("par_if: short body requires SHORTLOOPS");
6573 return;
6574 } else {
6575 // c:1477-1484 — short loop form
6576 // c:1478 — `cmdpop();`
6577 cmdpop();
6578 // c:1479 — `cmdpush(nc);`
6579 cmdpush(nc_local);
6580 // c:1480 — `par_save_list1(cmplx);`
6581 par_save_list1_wordcode(cmplx);
6582 // c:1481 — `ecbuf[pp] = WCB_IF(type, ecused - 1 - pp);`
6583 let used = ECUSED.get() as usize;
6584 ECBUF.with_borrow_mut(|b| {
6585 b[pp] = WCB_IF(r#type, (used.saturating_sub(1 + pp)) as wordcode);
6586 });
6587 // c:1482 — `incmdpos = 1;`
6588 set_incmdpos(true);
6589 // c:1483 — `break;`
6590 break;
6591 }
6592 }
6593 // c:1486 — `cmdpop();`
6594 cmdpop();
6595 // c:1487 — `if (xtok == ELSE || tok == ELSE) {`
6596 if xtok == ELSE || tok() == ELSE {
6597 // c:1488 — `pp = ecadd(0);`
6598 pp = ecadd(0);
6599 // c:1489 — `cmdpush(CS_ELSE);`
6600 cmdpush(CS_ELSE as u8);
6601 // c:1490-1491 — `while (tok == SEPER) zshlex();`
6602 while tok() == SEPER {
6603 zshlex();
6604 }
6605 // c:1492-1498 — `if (tok == INBRACE && usebrace) { ... } else { ... }`
6606 if tok() == INBRACE_TOK && usebrace != 0 {
6607 // c:1493 — `zshlex();`
6608 zshlex();
6609 // c:1494 — `par_save_list(cmplx);`
6610 par_save_list_wordcode(cmplx);
6611 // c:1495-1498 — `if (tok != OUTBRACE) { cmdpop(); YYERRORV; }`
6612 if tok() != OUTBRACE_TOK {
6613 cmdpop();
6614 zerr("par_if: else expected `}`");
6615 return;
6616 }
6617 } else {
6618 // c:1500 — `par_save_list(cmplx);`
6619 par_save_list_wordcode(cmplx);
6620 // c:1501-1504 — `if (tok != FI) { cmdpop(); YYERRORV; }`
6621 if tok() != FI {
6622 cmdpop();
6623 zerr("par_if: else expected `fi`");
6624 return;
6625 }
6626 }
6627 // c:1506 — `incmdpos = 0;`
6628 set_incmdpos(false);
6629 // c:1507 — `ecbuf[pp] = WCB_IF(WC_IF_ELSE, ecused - 1 - pp);`
6630 let used = ECUSED.get() as usize;
6631 ECBUF.with_borrow_mut(|b| {
6632 b[pp] = WCB_IF(WC_IF_ELSE, (used.saturating_sub(1 + pp)) as wordcode);
6633 });
6634 // c:1508 — `zshlex();`
6635 zshlex();
6636 // c:1509 — `cmdpop();`
6637 cmdpop();
6638 }
6639 // c:1511 — `ecbuf[p] = WCB_IF(WC_IF_HEAD, ecused - 1 - p);`
6640 let used = ECUSED.get() as usize;
6641 ECBUF.with_borrow_mut(|b| {
6642 b[p] = WCB_IF(WC_IF_HEAD, (used.saturating_sub(1 + p)) as wordcode);
6643 });
6644}
6645
6646/// Port of `par_while(int *cmplx)` from `Src/parse.c:1520-1557`.
6647pub fn par_while_wordcode(cmplx: &mut i32) {
6648 // c:1523 — `int oecused = ecused, p;`
6649 let _oecused = ECUSED.get() as usize;
6650 let p: usize;
6651 // c:1524 — `int type = (tok == UNTIL ? WC_WHILE_UNTIL : WC_WHILE_WHILE);`
6652 let r#type: wordcode = if tok() == UNTIL {
6653 WC_WHILE_UNTIL
6654 } else {
6655 WC_WHILE_WHILE
6656 };
6657
6658 // c:1526 — `p = ecadd(0);`
6659 p = ecadd(0);
6660 // c:1527 — `zshlex();`
6661 zshlex();
6662 // c:1528 — `par_save_list(cmplx);` — condition.
6663 par_save_list_wordcode(cmplx);
6664 // c:1529 — `incmdpos = 1;`
6665 set_incmdpos(true);
6666 // c:1530-1531 — `while (tok == SEPER) zshlex();`
6667 while tok() == SEPER {
6668 zshlex();
6669 }
6670 // c:1532-1545 — body dispatch (inlined in C; we factor via
6671 // par_loop_body_wordcode since for/while/repeat share this
6672 // identical block).
6673 if tok() == DOLOOP {
6674 // c:1533 — `zshlex();`
6675 zshlex();
6676 // c:1534 — `par_save_list(cmplx);`
6677 par_save_list_wordcode(cmplx);
6678 // c:1535-1536 — `if (tok != DONE) YYERRORV(oecused);`
6679 if tok() != DONE {
6680 zerr("par_while: expected `done`");
6681 return;
6682 }
6683 // c:1537 — `incmdpos = 0;`
6684 set_incmdpos(false);
6685 // c:1538 — `zshlex();`
6686 zshlex();
6687 } else if tok() == INBRACE_TOK {
6688 // c:1540 — `zshlex();`
6689 zshlex();
6690 // c:1541 — `par_save_list(cmplx);`
6691 par_save_list_wordcode(cmplx);
6692 // c:1542-1543 — `if (tok != OUTBRACE) YYERRORV(oecused);`
6693 if tok() != OUTBRACE_TOK {
6694 zerr("par_while: expected `}`");
6695 return;
6696 }
6697 // c:1544 — `incmdpos = 0;`
6698 set_incmdpos(false);
6699 // c:1545 — `zshlex();`
6700 zshlex();
6701 } else if isset(CSHJUNKIELOOPS) {
6702 // c:1546-1550
6703 par_save_list_wordcode(cmplx);
6704 if tok() != ZEND {
6705 zerr("par_while: expected `end`");
6706 return;
6707 }
6708 zshlex();
6709 } else if unset(SHORTLOOPS) {
6710 // c:1551-1552 — `YYERRORV(oecused);`
6711 zerr("par_while: short body requires SHORTLOOPS");
6712 return;
6713 } else {
6714 // c:1554 — `par_save_list1(cmplx);`
6715 par_save_list1_wordcode(cmplx);
6716 }
6717
6718 // c:1556 — `ecbuf[p] = WCB_WHILE(type, ecused - 1 - p);`
6719 let used = ECUSED.get() as usize;
6720 ECBUF.with_borrow_mut(|b| {
6721 b[p] = WCB_WHILE(r#type, (used.saturating_sub(1 + p)) as wordcode);
6722 });
6723}
6724
6725/// `until` shares par_while body — tok==UNTIL flips the type.
6726pub fn par_until_wordcode(cmplx: &mut i32) {
6727 par_while_wordcode(cmplx);
6728}
6729
6730/// Port of `par_repeat(int *cmplx)` from `Src/parse.c:1564-1606`.
6731pub fn par_repeat_wordcode(cmplx: &mut i32) {
6732 // c:1567 — `/* ### what to do about inrepeat_ here? */`
6733 // c:1568 — `int oecused = ecused, p;`
6734 let _oecused = ECUSED.get() as usize;
6735 let p: usize;
6736
6737 // c:1570 — `p = ecadd(0);`
6738 p = ecadd(0);
6739
6740 // c:1572 — `incmdpos = 0;`
6741 set_incmdpos(false);
6742 // c:1573 — `zshlex();`
6743 zshlex();
6744 // c:1574-1575 — `if (tok != STRING) YYERRORV(oecused);`
6745 if tok() != STRING_LEX {
6746 zerr("par_repeat: expected count");
6747 return;
6748 }
6749 // c:1576 — `ecstr(tokstr);`
6750 ecstr(&tokstr().unwrap_or_default());
6751 // c:1577 — `incmdpos = 1;`
6752 set_incmdpos(true);
6753 // c:1578 — `zshlex();`
6754 zshlex();
6755 // c:1579-1580 — `while (tok == SEPER) zshlex();`
6756 while tok() == SEPER {
6757 zshlex();
6758 }
6759 // c:1581-1604 — body dispatch (inlined here matching C exactly).
6760 if tok() == DOLOOP {
6761 // c:1582-1587
6762 zshlex();
6763 par_save_list_wordcode(cmplx);
6764 if tok() != DONE {
6765 zerr("par_repeat: expected `done`");
6766 return;
6767 }
6768 set_incmdpos(false);
6769 zshlex();
6770 } else if tok() == INBRACE_TOK {
6771 // c:1589-1594
6772 zshlex();
6773 par_save_list_wordcode(cmplx);
6774 if tok() != OUTBRACE_TOK {
6775 zerr("par_repeat: expected `}`");
6776 return;
6777 }
6778 set_incmdpos(false);
6779 zshlex();
6780 } else if isset(CSHJUNKIELOOPS) {
6781 // c:1596-1599
6782 par_save_list_wordcode(cmplx);
6783 if tok() != ZEND {
6784 zerr("par_repeat: expected `end`");
6785 return;
6786 }
6787 zshlex();
6788 } else if unset(SHORTLOOPS) && unset(SHORTREPEAT) {
6789 // c:1601-1602 — par_repeat needs BOTH SHORTLOOPS and SHORTREPEAT
6790 // unset to refuse short form (more permissive than par_while).
6791 zerr("par_repeat: short body requires SHORTLOOPS or SHORTREPEAT");
6792 return;
6793 } else {
6794 // c:1604 — `par_save_list1(cmplx);`
6795 par_save_list1_wordcode(cmplx);
6796 }
6797
6798 // c:1606 — `ecbuf[p] = WCB_REPEAT(ecused - 1 - p);`
6799 let used = ECUSED.get() as usize;
6800 ECBUF.with_borrow_mut(|b| {
6801 b[p] = WCB_REPEAT((used.saturating_sub(1 + p)) as wordcode);
6802 });
6803}
6804
6805/// Port of `par_funcdef(int *cmplx)` from `Src/parse.c:1672-1779`.
6806///
6807/// The `function NAME { ... }` form. Emits a WCB_FUNCDEF header
6808/// followed by a names-count slot, the names themselves, four
6809/// metadata slots (string-area start, string-area length, npats,
6810/// do_tracing), then the body wordcode, then WCB_END.
6811///
6812/// Critical: saves/resets `ecnpats` + `ecssub` + `ecsoffs` around
6813/// the body parse so per-function pattern counts don't leak into
6814/// the enclosing scope's `ecnpats` accumulator (parse.c:1723-1758).
6815pub fn par_funcdef_wordcode(cmplx: &mut i32) {
6816 // c:1674 — `int oecused = ecused, num = 0, onp, p, c = 0;`
6817 let _oecused = ECUSED.get() as usize;
6818 let mut num: i32 = 0;
6819 let onp: i32;
6820 let p: usize;
6821 let mut c: i32 = 0;
6822 // c:1675 — `int so, oecssub = ecssub;`
6823 let so: i32;
6824 let oecssub = ECSSUB.get();
6825 // c:1676 — `zlong oldlineno = lineno;`
6826 let oldlineno = lineno();
6827 // c:1677 — `int do_tracing = 0;`
6828 let mut do_tracing: i32 = 0;
6829
6830 // c:1679 — `lineno = 0;`
6831 set_lineno(0);
6832 // c:1680 — `nocorrect = 1;`
6833 set_nocorrect(1);
6834 // c:1681 — `incmdpos = 0;`
6835 set_incmdpos(false);
6836 // c:1682 — `zshlex();`
6837 zshlex();
6838
6839 // c:1684 — `p = ecadd(0);`
6840 p = ecadd(0);
6841 // c:1685 — `ecadd(0); /* p + 1 */`
6842 let p1 = ecadd(0);
6843
6844 // c:1687-1699 — `Consume an initial (-T), (--), or (-T --).`
6845 // c:1690 — `if (tok == STRING && tokstr[0] == Dash) {`
6846 if tok() == STRING_LEX {
6847 let s = tokstr().unwrap_or_default();
6848 let bytes = s.as_bytes();
6849 // C: `tokstr[0] == Dash` (Dash = 0x9b = 0xc2 0x9b in UTF-8).
6850 // First byte of UTF-8 `\u{9b}` is 0xc2; the char `'-'` is 0x2d.
6851 // Match either form.
6852 let first_is_dash = (bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x9b)
6853 || (bytes.len() >= 1 && bytes[0] == b'-');
6854 if first_is_dash {
6855 // c:1691-1694 — `if (tokstr[1] == 'T' && !tokstr[2]) { ++do_tracing; zshlex(); }`
6856 // After the leading dash byte(s), check remaining bytes.
6857 let after_dash = if bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x9b {
6858 &bytes[2..]
6859 } else {
6860 &bytes[1..]
6861 };
6862 if after_dash.len() == 1 && after_dash[0] == b'T' {
6863 do_tracing += 1;
6864 zshlex();
6865 }
6866 // c:1695-1698 — `if (tok == STRING && tokstr[0] == Dash &&
6867 // tokstr[1] == Dash && !tokstr[2]) zshlex();`
6868 if tok() == STRING_LEX {
6869 let s2 = tokstr().unwrap_or_default();
6870 let b2 = s2.as_bytes();
6871 let mut idx = 0;
6872 let mut dashes = 0;
6873 while idx < b2.len() && dashes < 2 {
6874 if b2[idx] == 0xc2 && idx + 1 < b2.len() && b2[idx + 1] == 0x9b {
6875 idx += 2;
6876 dashes += 1;
6877 } else if b2[idx] == b'-' {
6878 idx += 1;
6879 dashes += 1;
6880 } else {
6881 break;
6882 }
6883 }
6884 if dashes == 2 && idx == b2.len() {
6885 zshlex();
6886 }
6887 }
6888 }
6889 }
6890
6891 // c:1701-1709 — names loop.
6892 // `while (tok == STRING) { if ((*tokstr == Inbrace || *tokstr == '{')
6893 // && !tokstr[1]) { tok = INBRACE; break; } ecstr(tokstr); num++; zshlex(); }`
6894 while tok() == STRING_LEX {
6895 let s = tokstr().unwrap_or_default();
6896 let bytes = s.as_bytes();
6897 // First byte tests for Inbrace marker (0x8f → UTF-8 `0xc2 0x8f`) or `{`,
6898 // and length-1 check (`!tokstr[1]`).
6899 let is_inbrace_only = (bytes.len() == 1 && bytes[0] == b'{')
6900 || (bytes.len() == 2 && bytes[0] == 0xc2 && bytes[1] == 0x8f);
6901 if is_inbrace_only {
6902 set_tok(INBRACE_TOK);
6903 break;
6904 }
6905 ecstr(&s);
6906 num += 1;
6907 zshlex();
6908 }
6909
6910 // c:1711-1714 — four metadata placeholder slots.
6911 let m2 = ecadd(0);
6912 let m3 = ecadd(0);
6913 let m4 = ecadd(0);
6914 let m5 = ecadd(0);
6915
6916 // c:1716 — `nocorrect = 0;`
6917 set_nocorrect(0);
6918 // c:1717 — `incmdpos = 1;`
6919 set_incmdpos(true);
6920 // c:1718-1719 — `if (tok == INOUTPAR) zshlex();`
6921 if tok() == INOUTPAR {
6922 zshlex();
6923 }
6924 // c:1720-1721 — `while (tok == SEPER) zshlex();`
6925 while tok() == SEPER {
6926 zshlex();
6927 }
6928
6929 // c:1723 — `ecnfunc++;`
6930 ECNFUNC.set(ECNFUNC.get() + 1);
6931 // c:1724 — `ecssub = so = ecsoffs;`
6932 so = ECSOFFS.get();
6933 ECSSUB.set(so);
6934 // c:1725 — `onp = ecnpats;`
6935 onp = ECNPATS.with(|cc| cc.get());
6936 // c:1726 — `ecnpats = 0;`
6937 ECNPATS.with(|cc| cc.set(0));
6938
6939 // c:1728 — `if (tok == INBRACE) {`
6940 if tok() == INBRACE_TOK {
6941 // c:1729 — `zshlex();`
6942 zshlex();
6943 // c:1730 — `par_list(&c);`
6944 par_list_wordcode(&mut c);
6945 // c:1731-1736 — `if (tok != OUTBRACE) { lineno += oldlineno; ... }`
6946 if tok() != OUTBRACE_TOK {
6947 set_lineno(lineno() + oldlineno);
6948 ECNPATS.with(|cc| cc.set(onp));
6949 ECSSUB.set(oecssub);
6950 zerr("par_funcdef: expected `}`");
6951 return;
6952 }
6953 // c:1737-1740 — `if (num == 0) { incmdpos = 0; }`
6954 if num == 0 {
6955 set_incmdpos(false);
6956 }
6957 // c:1741 — `zshlex();`
6958 zshlex();
6959 } else if unset(SHORTLOOPS) {
6960 // c:1742-1746 — `lineno += oldlineno; ecnpats = onp; ecssub = oecssub; YYERRORV`
6961 set_lineno(lineno() + oldlineno);
6962 ECNPATS.with(|cc| cc.set(onp));
6963 ECSSUB.set(oecssub);
6964 zerr("par_funcdef: short body requires SHORTLOOPS");
6965 return;
6966 } else {
6967 // c:1748 — `par_list1(&c);`
6968 par_list1_wordcode(&mut c);
6969 }
6970
6971 // c:1750 — `ecadd(WCB_END());`
6972 ecadd(WCB_END());
6973 // c:1751-1754 — fill the 4 metadata slots
6974 let cur_sofs = ECSOFFS.get();
6975 let body_npats = ECNPATS.with(|cc| cc.get());
6976 ECBUF.with_borrow_mut(|b| {
6977 b[m2] = (so - oecssub) as wordcode;
6978 b[m3] = (cur_sofs - so) as wordcode;
6979 b[m4] = body_npats as wordcode;
6980 b[m5] = do_tracing as wordcode;
6981 });
6982 // c:1755 — `ecbuf[p + 1] = num;`
6983 ECBUF.with_borrow_mut(|b| {
6984 b[p1] = num as wordcode;
6985 });
6986
6987 // c:1757 — `ecnpats = onp;`
6988 ECNPATS.with(|cc| cc.set(onp));
6989 // c:1758 — `ecssub = oecssub;`
6990 ECSSUB.set(oecssub);
6991 // c:1759 — `ecnfunc++;`
6992 ECNFUNC.set(ECNFUNC.get() + 1);
6993
6994 // c:1761 — `ecbuf[p] = WCB_FUNCDEF(ecused - 1 - p);`
6995 let used = ECUSED.get() as usize;
6996 ECBUF.with_borrow_mut(|b| {
6997 b[p] = WCB_FUNCDEF((used.saturating_sub(1 + p)) as wordcode);
6998 });
6999
7000 // c:1763-1777 — anonymous-function trailing args (num == 0 case).
7001 if num == 0 {
7002 // c:1766 — `int parg = ecadd(0);`
7003 let parg = ecadd(0);
7004 // c:1767 — `ecadd(0);`
7005 ecadd(0);
7006 // c:1768-1772 — `while (tok == STRING) { ecstr(tokstr); num++; zshlex(); }`
7007 while tok() == STRING_LEX {
7008 ecstr(&tokstr().unwrap_or_default());
7009 num += 1;
7010 zshlex();
7011 }
7012 // c:1773-1774 — `if (num > 0) *cmplx = 1;`
7013 if num > 0 {
7014 *cmplx = 1;
7015 }
7016 // c:1775 — `ecbuf[parg] = ecused - parg;`
7017 // c:1776 — `ecbuf[parg+1] = num;`
7018 let used2 = ECUSED.get() as usize;
7019 ECBUF.with_borrow_mut(|b| {
7020 b[parg] = (used2 - parg) as wordcode;
7021 b[parg + 1] = num as wordcode;
7022 });
7023 }
7024 // c:1778 — `lineno += oldlineno;`
7025 set_lineno(lineno() + oldlineno);
7026}
7027
7028/// Size of `struct fdhead` in `wordcode` (u32) units. Used by all
7029/// the header-walk macros below.
7030pub const FDHEAD_WORDS: usize = size_of::<fdhead>() / 4;
7031
7032/// `Src/parse.c:1619-1665`. Handles both `(...)` subshell and
7033/// `{...}` brace group (cursh) plus optional `always { ... }`
7034/// trailing block. C uses a single function with `zsh_construct=1`
7035/// for `{...}` and 0 for `(...)`.
7036pub fn par_subsh_wordcode(cmplx: &mut i32, zsh_construct: i32) {
7037 // c:1621 — `enum lextok otok = tok;`
7038 let otok = tok();
7039 // c:1622 — `int oecused = ecused, p, pp;`
7040 let _oecused = ECUSED.get() as usize;
7041 let p: usize;
7042 let pp: usize;
7043
7044 // c:1624 — `p = ecadd(0);`
7045 p = ecadd(0);
7046 // c:1625 — `/* Extra word only needed for always block */`
7047 // c:1626 — `pp = ecadd(0);`
7048 pp = ecadd(0);
7049 // c:1627 — `zshlex();`
7050 zshlex();
7051 // c:1628 — `par_list(cmplx);`
7052 par_list_wordcode(cmplx);
7053 // c:1629 — `ecadd(WCB_END());`
7054 ecadd(WCB_END());
7055 // c:1630-1631 — `if (tok != ((otok == INPAR) ? OUTPAR : OUTBRACE))
7056 // YYERRORV(oecused);`
7057 if tok()
7058 != (if otok == INPAR_TOK {
7059 OUTPAR_TOK
7060 } else {
7061 OUTBRACE_TOK
7062 })
7063 {
7064 zerr("par_subsh: missing closing token");
7065 return;
7066 }
7067 // c:1632 — `incmdpos = !zsh_construct;`
7068 set_incmdpos(zsh_construct == 0);
7069 // c:1633 — `zshlex();`
7070 zshlex();
7071
7072 // c:1635 — `/* Optional always block. No intervening SEPERs allowed. */`
7073 // c:1636 — `if (otok == INBRACE && tok == STRING && !strcmp(tokstr, "always")) {`
7074 if otok == INBRACE_TOK && tok() == STRING_LEX && tokstr().as_deref() == Some("always") {
7075 // c:1637 — `ecbuf[pp] = WCB_TRY(ecused - 1 - pp);`
7076 let used = ECUSED.get() as usize;
7077 ECBUF.with_borrow_mut(|b| {
7078 b[pp] = WCB_TRY((used.saturating_sub(1 + pp)) as wordcode);
7079 });
7080 // c:1638 — `incmdpos = 1;`
7081 set_incmdpos(true);
7082 // c:1639-1641 — `do { zshlex(); } while (tok == SEPER);`
7083 loop {
7084 zshlex();
7085 if tok() != SEPER {
7086 break;
7087 }
7088 }
7089
7090 // c:1643-1644 — `if (tok != INBRACE) YYERRORV(oecused);`
7091 if tok() != INBRACE_TOK {
7092 zerr("par_subsh: 'always' expects `{`");
7093 return;
7094 }
7095 // c:1645 — `cmdpop();`
7096 cmdpop();
7097 // c:1646 — `cmdpush(CS_ALWAYS);`
7098 cmdpush(CS_ALWAYS as u8);
7099
7100 // c:1648 — `zshlex();`
7101 zshlex();
7102 // c:1649 — `par_save_list(cmplx);`
7103 par_save_list_wordcode(cmplx);
7104 // c:1650-1651 — `while (tok == SEPER) zshlex();`
7105 while tok() == SEPER {
7106 zshlex();
7107 }
7108
7109 // c:1653 — `incmdpos = 1;`
7110 set_incmdpos(true);
7111
7112 // c:1655-1656 — `if (tok != OUTBRACE) YYERRORV(oecused);`
7113 if tok() != OUTBRACE_TOK {
7114 zerr("par_subsh: 'always' block missing `}`");
7115 return;
7116 }
7117 // c:1657 — `zshlex();`
7118 zshlex();
7119 // c:1658 — `ecbuf[p] = WCB_TRY(ecused - 1 - p);`
7120 let used = ECUSED.get() as usize;
7121 ECBUF.with_borrow_mut(|b| {
7122 b[p] = WCB_TRY((used.saturating_sub(1 + p)) as wordcode);
7123 });
7124 } else {
7125 // c:1660-1661 — `ecbuf[p] = (otok == INPAR ? WCB_SUBSH(...) : WCB_CURSH(...));`
7126 let used = ECUSED.get() as usize;
7127 let off = used.saturating_sub(1 + p);
7128 ECBUF.with_borrow_mut(|b| {
7129 b[p] = if otok == INPAR_TOK {
7130 WCB_SUBSH(off as wordcode)
7131 } else {
7132 WCB_CURSH(off as wordcode)
7133 };
7134 });
7135 }
7136}
7137
7138/// Port of `par_time(void)` from `Src/parse.c:1787`. `time PIPE`
7139/// emits WCB_TIMED(WC_TIMED_PIPE) + the sublist code; bare `time`
7140/// with no pipeline emits WCB_TIMED(WC_TIMED_EMPTY).
7141pub fn par_time_wordcode() {
7142 // c:1791 — `zshlex();`
7143 zshlex();
7144 // c:1793-1794 — `p = ecadd(0); ecadd(0);`
7145 let p = ecadd(0);
7146 ecadd(0);
7147 // c:1795 — `if ((f = par_sublist2(&c)) < 0)`
7148 let mut c = 0i32;
7149 let f = par_sublist2(&mut c);
7150 match f {
7151 Some(flags) => {
7152 // c:1799 — `ecbuf[p] = WCB_TIMED(WC_TIMED_PIPE);`
7153 ECBUF.with_borrow_mut(|b| {
7154 if p < b.len() {
7155 b[p] = WCB_TIMED(WC_TIMED_PIPE);
7156 }
7157 });
7158 // c:1800 — `set_sublist_code(p+1, WC_SUBLIST_END, f,
7159 // ecused-2-p, c);`
7160 let used = ECUSED.get() as usize;
7161 let skip = used.saturating_sub(2 + p) as i32;
7162 set_sublist_code(p + 1, WC_SUBLIST_END as i32, flags, skip, c != 0);
7163 }
7164 None => {
7165 // c:1796-1798 — `ecused--; ecbuf[p] = WCB_TIMED(WC_TIMED_EMPTY);`
7166 ECUSED.set((ECUSED.get() - 1).max(0));
7167 ECBUF.with_borrow_mut(|b| {
7168 if p < b.len() {
7169 b[p] = WCB_TIMED(WC_TIMED_EMPTY);
7170 }
7171 });
7172 }
7173 }
7174}
7175
7176/// Port of `par_dinbrack(void)` from `Src/parse.c:1810`. Wraps
7177/// `par_cond` (the cond-expression emitter at parse.c:2409) with
7178/// the `[[ ... ]]` framing: incond/incmdpos toggles + DOUTBRACK
7179/// expectation.
7180pub fn par_cond_wordcode() {
7181 let oecused = ECUSED.get();
7182 // c:1814 — `incond = 1;`
7183 set_incond(1);
7184 // c:1815 — `incmdpos = 0;`
7185 set_incmdpos(false);
7186 // c:1816 — `zshlex();` past `[[`.
7187 zshlex();
7188 // c:1817 — `par_cond();` — call the no-skip cond-expression
7189 // entry that EMITS WORDCODE (par_cond_top → par_cond_1 →
7190 // par_cond_2 → par_cond_double/triple/multi). NOT the AST
7191 // `par_cond` at parse.rs:4644 which is a misnamed `par_dinbrack`
7192 // that skips `[[` AND `]]` and returns a ZshCommand AST node
7193 // instead of pushing WC_COND opcodes. NOT `parse_cond_expr`
7194 // either — that's also AST-only, returning ZshCond. With
7195 // `parse_cond_expr` here, every `[[ ... ]]` test produced ZERO
7196 // wordcode payload and parity dropped ~148 words on /etc/zshrc.
7197 let _ = par_cond_top();
7198 // c:1818-1819 — `if (tok != DOUTBRACK) YYERRORV(oecused);`
7199 if tok() != DOUTBRACK {
7200 let _ = oecused;
7201 zerr("missing ]]");
7202 return;
7203 }
7204 // c:1820 — `incond = 0;`
7205 set_incond(0);
7206 // c:1821 — `incmdpos = 1;`
7207 set_incmdpos(true);
7208 // c:1822 — `zshlex();` past `]]`.
7209 zshlex();
7210}
7211
7212/// Port of the `case DINPAR:` arm of `par_cmd` from
7213/// `Src/parse.c:1031-1034`:
7214/// ```c
7215/// ecadd(WCB_ARITH());
7216/// ecstr(tokstr);
7217/// zshlex();
7218/// ```
7219/// `(( EXPR ))` arithmetic at command position — emits the ARITH
7220/// opcode followed by the interned EXPR string, then advances past
7221/// the DINPAR token (which already carries the body text).
7222pub fn par_arith_wordcode() {
7223 // c:1032 — `ecadd(WCB_ARITH());`
7224 ecadd(WCB_ARITH());
7225 // c:1033 — `ecstr(tokstr);` — interns the expression string and
7226 // appends its strcode index to the wordcode buffer.
7227 let expr = tokstr().unwrap_or_default();
7228 ecstr(&expr);
7229 // c:1034 — `zshlex();`
7230 zshlex();
7231}
7232
7233/// Port of `par_simple(int *cmplx, int nr)` from
7234/// `Src/parse.c:1836-2227`. Emits WC_SIMPLE + word count +
7235/// interned string offsets. Returns `0` when nothing was emitted,
7236/// otherwise `1 + (number of code words consumed by redirections)`.
7237/// The full C body handles assignments (ENVSTRING/ENVARRAY),
7238/// inline `{var}>file` brace-FDs, prefix modifiers (NOCORRECT etc),
7239/// and `name() { body }` funcdef detection — those paths are
7240/// progressively wired into the AST parser; this wordcode-emitter
7241/// covers the simple `cmd args...` case + interleaved redirs.
7242pub fn par_simple_wordcode(cmplx: &mut i32, mut nr: i32) -> i32 {
7243 // c:1838-1841 — `int oecused = ecused, isnull = 1, r, argc = 0,
7244 // p, isfunc = 0, sr = 0;`
7245 // `int c = *cmplx, nrediradd, assignments = 0, ppost = 0,
7246 // is_typeset = 0;`
7247 // c is the SAVED initial cmplx so INOUTPAR can restore via
7248 // `*cmplx = c;` at c:2070.
7249 let _oecused = ECUSED.get() as usize;
7250 let c_saved = *cmplx;
7251 let mut isnull = true;
7252 let mut argc: u32 = 0;
7253 let mut sr: i32 = 0;
7254 let mut assignments = false;
7255 let mut isfunc = false;
7256
7257 // c:1843 — `r = ecused;` — saves the offset where redirs get
7258 // INSERTED (via ecispace). Each redir shifts later words DOWN
7259 // by ncodes, so the SIMPLE placeholder at `p` (set later) must
7260 // also bump by ncodes when a redir lands. C uses `&r` to pass
7261 // the cursor by reference; Rust uses a mutable local + manual
7262 // bumps after each par_redir_wordcode call.
7263 let mut r: usize = ECUSED.get() as usize;
7264
7265 // c:1844-1919 — pre-cmd loop: NOCORRECT, ENVSTRING (scalar
7266 // assigns), ENVARRAY (array assigns), IS_REDIROP. Loops until
7267 // a non-assignment token is seen.
7268 loop {
7269 match tok() {
7270 NOCORRECT => {
7271 // c:1846-1849
7272 *cmplx = 1;
7273 set_nocorrect(1);
7274 }
7275 ENVSTRING => {
7276 // c:1848-1898 — scalar assignment `name=value` or
7277 // `name+=value`. Emits WCB_ASSIGN(SCALAR, NEW|INC, 0)
7278 // followed by ecstr(name), ecstr(value).
7279 let raw = tokstr().unwrap_or_default();
7280 // Find first of Inbrack / '=' / '+' (the C scan at
7281 // c:1851-1853). Inside Inbrack we skipparens — i.e.
7282 // skip `name[...]` index, then continue.
7283 // c:1851-1853 — `for (ptr = tokstr; *ptr && *ptr != Inbrack
7284 // && *ptr != '=' && *ptr != '+'; ptr++); if (*ptr == Inbrack)
7285 // skipparens(Inbrack, Outbrack, &ptr);`. Walk to the first
7286 // `[`/`=`/`+`/Equals-token, then if we landed on `[`, skip
7287 // the balanced `name[index]` pair via skipparens.
7288 let bytes: Vec<char> = raw.chars().collect();
7289 let raw_str: String = bytes.iter().collect();
7290 let mut idx = 0usize;
7291 while idx < bytes.len() {
7292 let ch = bytes[idx];
7293 if ch == '\u{91}' /* Inbrack */
7294 || ch == '=' || ch == '+' || ch == '\u{8d}'
7295 /* Equals */
7296 {
7297 break;
7298 }
7299 idx += 1;
7300 }
7301 if idx < bytes.len() && bytes[idx] == '\u{91}'
7302 /* Inbrack */
7303 {
7304 // c:1855 — `skipparens(Inbrack, Outbrack, &ptr);`.
7305 let byte_off: usize = bytes[..idx].iter().map(|c| c.len_utf8()).sum();
7306 let mut cursor: &str = &raw_str[byte_off..];
7307 let _ = crate::ported::utils::skipparens('\u{91}', '\u{92}', &mut cursor);
7308 let consumed = raw_str.len() - byte_off - cursor.len();
7309 let advance_chars = raw_str[byte_off..byte_off + consumed].chars().count();
7310 idx += advance_chars;
7311 // Continue scanning for `=` / `+` after the `]`.
7312 while idx < bytes.len() {
7313 let ch = bytes[idx];
7314 if ch == '=' || ch == '+' || ch == '\u{8d}' {
7315 break;
7316 }
7317 idx += 1;
7318 }
7319 }
7320 let is_inc = idx < bytes.len() && bytes[idx] == '+';
7321 // c:1856-1858 — `if (*ptr == '+') { *ptr++ = '\0';
7322 // ecadd(WCB_ASSIGN(SCALAR, INC, 0)); } else WCB_NEW`
7323 // C nulls the `+` AT THAT POSITION then advances ptr.
7324 // `name` is bytes BEFORE the `+`, NOT including it.
7325 let name_end = idx;
7326 if is_inc {
7327 idx += 1;
7328 }
7329 let flag = if is_inc { WC_ASSIGN_INC } else { WC_ASSIGN_NEW };
7330 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, flag, 0));
7331 // c:1860 — `if (*ptr == '=') { *ptr = '\0'; str = ptr + 1; }
7332 // else equalsplit(tokstr, &str);`
7333 let name: String = bytes[..name_end].iter().collect();
7334 let str_off = if idx < bytes.len() && (bytes[idx] == '=' || bytes[idx] == '\u{8d}')
7335 {
7336 idx + 1
7337 } else {
7338 idx
7339 };
7340 let value: String = bytes[str_off..].iter().collect();
7341 // c:1866-1877 — scan value for `=(`/`<(`/`>(` (proc
7342 // subst); if found, bump cmplx (suppresses Z_SIMPLE).
7343 let vbytes: Vec<char> = value.chars().collect();
7344 for (i, ch) in vbytes.iter().enumerate() {
7345 if i + 1 < vbytes.len() && vbytes[i + 1] == '\u{88}'
7346 /* Inpar */
7347 {
7348 if *ch == '\u{8d}' /* Equals */
7349 || *ch == '\u{94}' /* Inang */
7350 || *ch == '\u{96}'
7351 /* OutangProc */
7352 {
7353 *cmplx = 1;
7354 break;
7355 }
7356 }
7357 }
7358 ecstr(&name);
7359 ecstr(&value);
7360 isnull = false;
7361 assignments = true;
7362 }
7363 ENVARRAY => {
7364 // c:1883-1908 — array assignment `name=( ... )` in the
7365 // pre-cmd loop (no `typeset`-style typeset_force flag).
7366 // c:1884 — `int oldcmdpos = incmdpos, n, type2;`
7367 let oldcmdpos = incmdpos();
7368 let n: u32;
7369 let type2: wordcode;
7370 let p: usize;
7371
7372 // c:1886-1889 — `array setting is cmplx because it can
7373 // contain process substitutions`
7374 // c:1890 — `*cmplx = c = 1;`
7375 *cmplx = 1;
7376 // c:1891 — `p = ecadd(0);`
7377 p = ecadd(0);
7378 // c:1892 — `incmdpos = 0;`
7379 set_incmdpos(false);
7380 // c:1893-1897 — `+=` detection: if tokstr ends in `+`,
7381 // strip the `+` and use WC_ASSIGN_INC; else WC_ASSIGN_NEW.
7382 let raw = tokstr().unwrap_or_default();
7383 let (name, t2) = if raw.ends_with('+') {
7384 (raw[..raw.len() - 1].to_string(), WC_ASSIGN_INC)
7385 } else {
7386 (raw.clone(), WC_ASSIGN_NEW)
7387 };
7388 type2 = t2;
7389 // c:1898 — `ecstr(tokstr);` (tokstr now NUL-trimmed)
7390 ecstr(&name);
7391 // c:1899 — `cmdpush(CS_ARRAY);`
7392 cmdpush(CS_ARRAY as u8);
7393 // c:1900 — `zshlex();`
7394 zshlex();
7395 // c:1901 — `n = par_nl_wordlist();`
7396 n = par_nl_wordlist_wordcode();
7397 // c:1902 — `ecbuf[p] = WCB_ASSIGN(WC_ASSIGN_ARRAY, type2, n);`
7398 ECBUF.with_borrow_mut(|b| {
7399 b[p] = WCB_ASSIGN(WC_ASSIGN_ARRAY, type2, n);
7400 });
7401 // c:1903 — `cmdpop();`
7402 cmdpop();
7403 // c:1904-1905 — `if (tok != OUTPAR) YYERROR(oecused);`
7404 if tok() != OUTPAR_TOK {
7405 zerr("par_simple: expected `)' after array assignment");
7406 return 0;
7407 }
7408 // c:1906 — `incmdpos = oldcmdpos;`
7409 set_incmdpos(oldcmdpos);
7410 // c:1907 — `isnull = 0;`
7411 isnull = false;
7412 // c:1908 — `assignments = 1;`
7413 assignments = true;
7414 }
7415 t if IS_REDIROP(t) => {
7416 // c:1900-1904 — `*cmplx = c = 1; nr += par_redir(&r,
7417 // NULL); continue;`. The wordcode-emitting redir is
7418 // distinct from the AST par_redir — it INSERTS
7419 // WCB_REDIR + fd + ecstrcode(name) at offset `r`
7420 // via ecispace, shifting any later words down.
7421 *cmplx = 1;
7422 let added = par_redir_wordcode(&mut r, None);
7423 if added == 0 {
7424 break;
7425 }
7426 nr += added;
7427 continue;
7428 }
7429 _ => break,
7430 }
7431 zshlex(); // c:1907 `zshlex();`
7432 }
7433
7434 // c:1920-1921 — `if (tok == AMPER || tok == AMPERBANG) YYERROR;`
7435 if tok() == AMPER || tok() == AMPERBANG {
7436 zerr("par_simple: unexpected &");
7437 return 0;
7438 }
7439
7440 // c:1923 — `p = ecadd(WCB_SIMPLE(0));`
7441 let mut p = ecadd(WCB_SIMPLE(0));
7442
7443 // c:1924-2105 — main words loop. is_typeset tracks whether the
7444 // outer command was `typeset`/`export`/etc. so the final
7445 // placeholder gets WCB_TYPESET instead of WCB_SIMPLE.
7446 let mut is_typeset = false;
7447 let mut postassigns: u32 = 0;
7448 let mut ppost: usize = 0;
7449 loop {
7450 match tok() {
7451 STRING_LEX | TYPESET => {
7452 // c:1926 — `int redir_var = 0;`
7453 let mut redir_var = false;
7454 // c:1928-1929 — `*cmplx = 1; incmdpos = 0;`
7455 *cmplx = 1;
7456 set_incmdpos(false);
7457 // c:1931-1932 — TYPESET → intypeset = is_typeset = 1.
7458 if tok() == TYPESET {
7459 set_intypeset(true);
7460 is_typeset = true;
7461 }
7462 let s = tokstr().unwrap_or_default();
7463 // c:1934-1974 — `{var}>file` brace-FD detection.
7464 // `if (!isset(IGNOREBRACES) && *tokstr == Inbrace)`
7465 let bytes = s.as_bytes();
7466 let first_is_inbrace = (bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x8f)
7467 || (bytes.len() >= 1 && bytes[0] == b'{');
7468 if !isset(IGNOREBRACES) && first_is_inbrace {
7469 // c:1937-1938 — `char *eptr = tokstr + strlen(tokstr) - 1;`
7470 // `char *ptr = eptr;`
7471 // C tests `*eptr == Outbrace` (0x90 marker or `}`) AND
7472 // there's content between `{` and `}` (`ptr > tokstr + 1`).
7473 let last_two_outbrace = bytes.len() >= 2
7474 && (bytes.ends_with(&[0xc2, 0x90]) || bytes.last() == Some(&b'}'));
7475 let opener_len = if bytes.len() >= 2 && bytes[0] == 0xc2 && bytes[1] == 0x8f {
7476 2
7477 } else {
7478 1
7479 };
7480 let closer_len = if bytes.len() >= 2 && bytes.ends_with(&[0xc2, 0x90]) {
7481 2
7482 } else if bytes.last() == Some(&b'}') {
7483 1
7484 } else {
7485 0
7486 };
7487 if last_two_outbrace && bytes.len() > opener_len + closer_len {
7488 // c:1944 — `if (itype_end(tokstr+1, IIDENT, 0) >= ptr)`
7489 // Inner content is the identifier between `{` and `}`.
7490 let inner_start = opener_len;
7491 let inner_end = bytes.len() - closer_len;
7492 let inner = &s[inner_start..inner_end];
7493 if !inner.is_empty() && crate::ported::params::isident(inner) {
7494 // c:1946-1948 — `char *idstring = dupstrpfx(...);`
7495 // `redir_var = 1; zshlex();`
7496 let idstring = inner.to_string();
7497 redir_var = true;
7498 zshlex();
7499 // c:1953-1958 — `if (IS_REDIROP(tok) && tokfd == -1)
7500 // { *cmplx = c = 1; nrediradd = par_redir(&r, id);
7501 // p += nrediradd; sr += nrediradd; }`
7502 if IS_REDIROP(tok()) && tokfd() == -1 {
7503 *cmplx = 1;
7504 let nrediradd = par_redir_wordcode(&mut r, Some(&idstring));
7505 p += nrediradd as usize;
7506 sr += nrediradd;
7507 } else if postassigns > 0 {
7508 // c:1959-1966 — postassigns path: emit
7509 // WCB_ASSIGN(SCALAR, INC, 0) + name + ""
7510 postassigns += 1;
7511 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_INC, 0));
7512 ecstr(&s);
7513 ecstr("");
7514 } else {
7515 // c:1968-1972 — `else { ecstr(toksave); argc++; }`
7516 ecstr(&s);
7517 argc += 1;
7518 }
7519 }
7520 }
7521 }
7522 if !redir_var {
7523 // c:1977-1996 — normal (non-redir-var) STRING/TYPESET.
7524 if postassigns > 0 {
7525 // c:1979-1989 — typeset with bare-name arg → INC
7526 postassigns += 1;
7527 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_INC, 0));
7528 ecstr(&s);
7529 ecstr("");
7530 } else {
7531 ecstr(&s);
7532 argc += 1;
7533 }
7534 zshlex();
7535 }
7536 isnull = false;
7537 }
7538 ENVSTRING => {
7539 // c:2005-2026 — mid-cmd ENVSTRING (under intypeset
7540 // context). Emits WCB_ASSIGN(SCALAR, NEW, 0) then
7541 // ecstr(name) + ecstr(value), tracking the first
7542 // postassign offset in `ppost` (which the trailing
7543 // WCB_TYPESET header points to).
7544 if postassigns == 0 {
7545 ppost = ecadd(0);
7546 }
7547 postassigns += 1;
7548 // c:2010-2014 — `for (ptr = tokstr; *ptr && *ptr != Inbrack
7549 // && *ptr != '=' && *ptr != '+'; ptr++); if (*ptr == Inbrack)
7550 // skipparens(Inbrack, Outbrack, &ptr);`.
7551 let raw = tokstr().unwrap_or_default();
7552 let bytes: Vec<char> = raw.chars().collect();
7553 let mut idx = 0usize;
7554 while idx < bytes.len() {
7555 let ch = bytes[idx];
7556 if ch == '\u{91}' /* Inbrack */
7557 || ch == '=' || ch == '+' || ch == '\u{8d}'
7558 /* Equals */
7559 {
7560 break;
7561 }
7562 idx += 1;
7563 }
7564 if idx < bytes.len() && bytes[idx] == '\u{91}'
7565 /* Inbrack */
7566 {
7567 // c:2014 — `skipparens(Inbrack, Outbrack, &ptr);`.
7568 let byte_off: usize = bytes[..idx].iter().map(|c| c.len_utf8()).sum();
7569 let mut cursor: &str = &raw[byte_off..];
7570 let _ = crate::ported::utils::skipparens('\u{91}', '\u{92}', &mut cursor);
7571 let consumed = raw.len() - byte_off - cursor.len();
7572 let advance_chars = raw[byte_off..byte_off + consumed].chars().count();
7573 idx += advance_chars;
7574 while idx < bytes.len() {
7575 let ch = bytes[idx];
7576 if ch == '=' || ch == '+' || ch == '\u{8d}' {
7577 break;
7578 }
7579 idx += 1;
7580 }
7581 }
7582 let name: String = bytes[..idx].iter().collect();
7583 let str_off = if idx < bytes.len() && (bytes[idx] == '=' || bytes[idx] == '\u{8d}')
7584 {
7585 idx + 1
7586 } else {
7587 idx
7588 };
7589 let value: String = bytes[str_off..].iter().collect();
7590 ecadd(WCB_ASSIGN(WC_ASSIGN_SCALAR, WC_ASSIGN_NEW, 0));
7591 ecstr(&name);
7592 ecstr(&value);
7593 isnull = false;
7594 zshlex();
7595 }
7596 ENVARRAY => {
7597 // c:2027-2050 — mid-cmd ENVARRAY (typeset N=(…) form).
7598 // C tracks postassigns + ppost the same as ENVSTRING,
7599 // but the inner emit is WCB_ASSIGN(ARRAY, NEW, n)
7600 // with `n` patched in after par_nl_wordlist consumes
7601 // the elements. C also toggles intypeset=0 around the
7602 // wordlist so the lexer doesn't try to re-emit
7603 // assignments inside the array.
7604 *cmplx = 1;
7605 if postassigns == 0 {
7606 ppost = ecadd(0);
7607 }
7608 postassigns += 1;
7609 let parr = ecadd(0);
7610 let raw = tokstr().unwrap_or_default();
7611 let is_inc = raw.ends_with('+');
7612 let name = if is_inc {
7613 &raw[..raw.len() - 1]
7614 } else {
7615 raw.as_str()
7616 };
7617 let flag = if is_inc { WC_ASSIGN_INC } else { WC_ASSIGN_NEW };
7618 ecstr(name);
7619 cmdpush(CS_ARRAY as u8);
7620 set_intypeset(false);
7621 zshlex();
7622 // c:2044 — `n = par_nl_wordlist();` (parse.c:2379-2391).
7623 // SEPER + NEWLIN both allowed between elements.
7624 let mut nelem = 0u32;
7625 loop {
7626 let t = tok();
7627 if t != STRING_LEX && t != SEPER && t != NEWLIN {
7628 break;
7629 }
7630 if t == STRING_LEX {
7631 ecstr(&tokstr().unwrap_or_default());
7632 nelem += 1;
7633 }
7634 zshlex();
7635 }
7636 ECBUF.with_borrow_mut(|b| {
7637 if parr < b.len() {
7638 b[parr] = WCB_ASSIGN(WC_ASSIGN_ARRAY, flag, nelem);
7639 }
7640 });
7641 cmdpop();
7642 set_intypeset(true);
7643 if tok() != OUTPAR_TOK {
7644 zerr("expected `)' after array assignment");
7645 return 0;
7646 }
7647 isnull = false;
7648 zshlex();
7649 }
7650 t if IS_REDIROP(t) => {
7651 // c:1999-2010 — `nrediradd = par_redir(&r, NULL);
7652 // p += nrediradd; if (ppost) ppost += nrediradd;
7653 // sr += nrediradd;`
7654 *cmplx = 1;
7655 let added = par_redir_wordcode(&mut r, None);
7656 if added == 0 {
7657 break;
7658 }
7659 p += added as usize;
7660 if ppost != 0 {
7661 ppost += added as usize;
7662 }
7663 sr += added;
7664 }
7665 INOUTPAR => {
7666 // c:2051 — `} else if (tok == INOUTPAR) {`
7667 // c:2052 — `zlong oldlineno = lineno;`
7668 let oldlineno = lineno();
7669 // c:2053 — `int onp, so, oecssub = ecssub;`
7670 let oecssub = ECSSUB.get();
7671 // c:2055-2057 — `if (!isset(MULTIFUNCDEF) && argc > 1) YYERROR;`
7672 if !isset(MULTIFUNCDEF) && argc > 1 {
7673 zerr("par_simple: too many function names for funcdef");
7674 return 0;
7675 }
7676 // c:2058-2060 — `if (assignments || postassigns) YYERROR;`
7677 if assignments || postassigns > 0 {
7678 zerr("par_simple: assignments before funcdef");
7679 return 0;
7680 }
7681 // c:2061-2068 — hasalias check + zwarn — skipped (no
7682 // alias tracking on the wordcode path).
7683
7684 // c:2070 — `*cmplx = c;`
7685 *cmplx = c_saved;
7686 // c:2071 — `lineno = 0;`
7687 set_lineno(0);
7688 // c:2072 — `incmdpos = 1;`
7689 set_incmdpos(true);
7690 // c:2073 — `cmdpush(CS_FUNCDEF);`
7691 cmdpush(CS_FUNCDEF as u8);
7692 // c:2074 — `zshlex();`
7693 zshlex();
7694 // c:2075-2076 — `while (tok == SEPER) zshlex();`
7695 while tok() == SEPER {
7696 zshlex();
7697 }
7698 // c:2079 — `ecispace(p + 1, 1); ecbuf[p+1] = argc;
7699 // ecadd(0)*4`. Insert the argc word at p+1, then
7700 // append 4 placeholder words.
7701 ecispace(p + 1, 1);
7702 ECBUF.with_borrow_mut(|b| {
7703 if p + 1 < b.len() {
7704 b[p + 1] = argc;
7705 }
7706 });
7707 // c:2080-2083 — four metadata placeholder slots.
7708 ecadd(0);
7709 ecadd(0);
7710 ecadd(0);
7711 ecadd(0);
7712
7713 // c:2085 — `ecnfunc++;`
7714 ECNFUNC.set(ECNFUNC.get() + 1);
7715 // c:2086 — `ecssub = so = ecsoffs;`
7716 let so = ECSOFFS.get();
7717 ECSSUB.set(so);
7718 // c:2087 — `onp = ecnpats;`
7719 let onp = ECNPATS.with(|cc| cc.get());
7720 // c:2088 — `ecnpats = 0;`
7721 ECNPATS.with(|cc| cc.set(0));
7722
7723 // c:2091 — `int c = 0;` — INNER cmplx for the body
7724 // parse. Local to each branch; C's enclosing *cmplx
7725 // is NOT modified by the body.
7726 let mut body_c: i32 = 0;
7727 // c:2090 — `if (tok == INBRACE) {`
7728 if tok() == INBRACE_TOK {
7729 // c:2093 — `zshlex();`
7730 zshlex();
7731 // c:2094 — `par_list(&c);`
7732 par_list_wordcode(&mut body_c);
7733 // c:2095-2101 — `if (tok != OUTBRACE) { cmdpop();
7734 // lineno += oldlineno; ecnpats = onp;
7735 // ecssub = oecssub; YYERROR; }`
7736 if tok() != OUTBRACE_TOK {
7737 cmdpop();
7738 set_lineno(lineno() + oldlineno);
7739 ECNPATS.with(|cc| cc.set(onp));
7740 ECSSUB.set(oecssub);
7741 zerr("par_simple: funcdef expected `}`");
7742 return 0;
7743 }
7744 // c:2102-2105 — `if (argc == 0) incmdpos = 0;`
7745 if argc == 0 {
7746 set_incmdpos(false);
7747 }
7748 // c:2106 — `zshlex();`
7749 zshlex();
7750 } else {
7751 // c:2107-2132 — short-body funcdef form: `f() cmd`
7752 // or `() cmd`. Wraps single par_cmd result in a
7753 // synthetic WC_LIST / WC_SUBLIST /
7754 // WC_PIPE(WC_PIPE_END, 0) header trio.
7755 let ll = ecadd(0);
7756 let sl = ecadd(0);
7757 ecadd(WCB_PIPE(WC_PIPE_END, 0));
7758 let ok = par_cmd_wordcode(&mut body_c, if argc == 0 { 1 } else { 0 });
7759 if !ok {
7760 cmdpop();
7761 zerr("par_simple: funcdef short-body: missing command");
7762 return 0;
7763 }
7764 if argc == 0 {
7765 // c:2118-2127 — anonymous funcdef may take args
7766 // after the body; first one already read.
7767 set_incmdpos(false);
7768 }
7769 // c:2130-2131 — inner sublist/list use inner cmplx.
7770 let used = ECUSED.get() as usize;
7771 set_sublist_code(
7772 sl,
7773 WC_SUBLIST_END as i32,
7774 0,
7775 (used.saturating_sub(1 + sl)) as i32,
7776 body_c != 0,
7777 );
7778 set_list_code(ll, Z_SYNC | Z_END, body_c != 0);
7779 }
7780 let _ = body_c;
7781 // c:2133 — `cmdpop();`
7782 cmdpop();
7783
7784 // c:2135 — `ecadd(WCB_END());`
7785 ecadd(WCB_END());
7786 // c:2136-2139 — fill 4 metadata slots at p+argc+2..5
7787 let p_argc = (p + (argc as usize) + 2) as usize;
7788 let cur_so = ECSOFFS.get();
7789 let np_now = ECNPATS.with(|cc| cc.get());
7790 ECBUF.with_borrow_mut(|b| {
7791 b[p_argc] = (so - oecssub) as wordcode;
7792 b[p_argc + 1] = (cur_so - so) as wordcode;
7793 b[p_argc + 2] = np_now as wordcode;
7794 b[p_argc + 3] = 0;
7795 });
7796
7797 // c:2141-2143 — `ecnpats = onp; ecssub = oecssub; ecnfunc++;`
7798 ECNPATS.with(|cc| cc.set(onp));
7799 ECSSUB.set(oecssub);
7800 ECNFUNC.set(ECNFUNC.get() + 1);
7801
7802 // c:2145 — `ecbuf[p] = WCB_FUNCDEF(ecused - 1 - p);`
7803 let used = ECUSED.get() as usize;
7804 let header_off = used.saturating_sub(1 + p) as wordcode;
7805 ECBUF.with_borrow_mut(|b| {
7806 b[p] = WCB_FUNCDEF(header_off);
7807 });
7808
7809 // c:2147-2172 — `if (argc == 0) { /* anonymous fn args */ }`
7810 if argc == 0 {
7811 // c:2150 — `int parg = ecadd(0);`
7812 let mut parg = ecadd(0);
7813 // c:2151 — `ecadd(0);`
7814 ecadd(0);
7815 // c:2152 — `while (tok == STRING || IS_REDIROP(tok)) {`
7816 while tok() == STRING_LEX || IS_REDIROP(tok()) {
7817 if tok() == STRING_LEX {
7818 // c:2155-2157
7819 ecstr(&tokstr().unwrap_or_default());
7820 argc += 1;
7821 zshlex();
7822 } else {
7823 // c:2159-2165 — *cmplx=c=1; nrediradd=par_redir;
7824 // p += nrediradd; ppost += nrediradd if ppost;
7825 // sr += nrediradd; parg += nrediradd;
7826 *cmplx = 1;
7827 let added = par_redir_wordcode(&mut r, None);
7828 if added == 0 {
7829 break;
7830 }
7831 p += added as usize;
7832 if ppost != 0 {
7833 ppost += added as usize;
7834 }
7835 sr += added;
7836 parg += added as usize;
7837 }
7838 }
7839 // c:2168-2169 — `if (argc > 0) *cmplx = 1;`
7840 if argc > 0 {
7841 *cmplx = 1;
7842 }
7843 // c:2170 — `ecbuf[parg] = ecused - parg;`
7844 // c:2171 — `ecbuf[parg+1] = argc;`
7845 let used2 = ECUSED.get() as usize;
7846 ECBUF.with_borrow_mut(|b| {
7847 b[parg] = (used2 - parg) as wordcode;
7848 b[parg + 1] = argc;
7849 });
7850 }
7851 // c:2173 — `lineno += oldlineno;`
7852 set_lineno(lineno() + oldlineno);
7853
7854 // c:2175-2177 — `isfunc = 1; isnull = 0; break;`
7855 isfunc = true;
7856 isnull = false;
7857 break;
7858 }
7859 _ => break,
7860 }
7861 }
7862
7863 // c:2173-2176 — `if (isnull && !(sr + nr)) { ecused = oecused;
7864 // return 0; }` — undo everything including pre-cmd assignments
7865 // if no actual command word emerged.
7866 if isnull && sr + nr == 0 && !assignments {
7867 ECUSED.set(p as i32);
7868 return 0;
7869 }
7870 // c:2186-2187 — `incmdpos = 1; intypeset = 0;` — reset before
7871 // the placeholder patch so the next-token lex doesn't carry
7872 // typeset/incond state.
7873 set_incmdpos(true);
7874 set_intypeset(false);
7875 // c:2189-2199 — `if (!isfunc) { if (is_typeset) ecbuf[p] =
7876 // WCB_TYPESET(argc); else ecbuf[p] = WCB_SIMPLE(argc); }`.
7877 // When isfunc=true the INOUTPAR branch already wrote WCB_FUNCDEF
7878 // at p; do NOT clobber it.
7879 if !isfunc {
7880 let header = if is_typeset {
7881 if postassigns > 0 {
7882 ECBUF.with_borrow_mut(|b| {
7883 if ppost < b.len() {
7884 b[ppost] = postassigns;
7885 }
7886 });
7887 } else {
7888 ecadd(0);
7889 }
7890 WCB_TYPESET(argc)
7891 } else {
7892 WCB_SIMPLE(argc)
7893 };
7894 ECBUF.with_borrow_mut(|b| {
7895 if p < b.len() {
7896 b[p] = header;
7897 }
7898 });
7899 }
7900 1 + sr
7901}
7902
7903/// Port of `par_redir(int *rp, char *idstring)` from
7904/// `Src/parse.c:2229-2345` — the wordcode-emitting variant that
7905/// pushes WCB_REDIR + fd + ecstrcode(name) into ECBUF. Distinct
7906/// from the AST `par_redir` (parse.rs:3771) which builds a
7907/// ZshRedir struct for the AST executor pipeline.
7908///
7909/// Returns the number of wordcodes added (3 for the basic shape,
7910/// 4 with idstring, 5 for HEREDOC[DASH] which carries the
7911/// terminator strings inline). Returns 0 on parse error.
7912///
7913/// `idstring` mirrors C's `char *idstring` parameter — `None` =
7914/// NULL (no `{var}>file` brace-FD shape), `Some(id)` = the captured
7915/// `{var}` name. C callers without a var pass NULL inline; Rust
7916/// callers do the same with `None`.
7917fn par_redir_wordcode(rp: &mut usize, idstring: Option<&str>) -> i32 {
7918 // c:2231 — `int r = *rp, type, fd1, oldcmdpos, oldnc, ncodes;`
7919 let r: usize = *rp;
7920 let mut r#type: i32;
7921 let fd1: i32;
7922 let oldcmdpos: bool;
7923 let oldnc: i32;
7924 let mut ncodes: usize;
7925 // c:2232 — `char *name;`
7926 let name: String;
7927
7928 // c:2234 — `oldcmdpos = incmdpos;`
7929 oldcmdpos = incmdpos();
7930 // c:2235 — `incmdpos = 0;`
7931 set_incmdpos(false);
7932 // c:2236 — `oldnc = nocorrect;`
7933 oldnc = nocorrect();
7934 // c:2237-2238 — `if (tok != INANG && tok != INOUTANG) nocorrect = 1;`
7935 if tok() != INANG_TOK && tok() != INOUTANG {
7936 set_nocorrect(1);
7937 }
7938 // c:2239 — `type = redirtab[tok - OUTANG];`
7939 // Map current redirop token to redirtab index — matches order of
7940 // C `enum { OUTANG, OUTANGBANG, DOUTANG, DOUTANGBANG, INANG,
7941 // INOUTANG, DINANG, DINANGDASH, INANGAMP, OUTANGAMP, AMPOUTANG,
7942 // OUTANGAMPBANG, DOUTANGAMP, DOUTANGAMPBANG, TRINANG }`.
7943 r#type = match tok() {
7944 OUTANG_TOK => REDIR_WRITE,
7945 OUTANGBANG => REDIR_WRITENOW,
7946 DOUTANG => REDIR_APP,
7947 DOUTANGBANG => REDIR_APPNOW,
7948 INANG_TOK => REDIR_READ,
7949 INOUTANG => REDIR_READWRITE,
7950 DINANG => REDIR_HEREDOC,
7951 DINANGDASH => REDIR_HEREDOCDASH,
7952 INANGAMP => REDIR_MERGEIN,
7953 OUTANGAMP => REDIR_MERGEOUT,
7954 AMPOUTANG => REDIR_ERRWRITE,
7955 OUTANGAMPBANG => REDIR_ERRWRITENOW,
7956 DOUTANGAMP => REDIR_ERRAPP,
7957 DOUTANGAMPBANG => REDIR_ERRAPPNOW,
7958 TRINANG => REDIR_HERESTR,
7959 _ => {
7960 set_incmdpos(oldcmdpos);
7961 set_nocorrect(oldnc);
7962 return 0;
7963 }
7964 };
7965 // c:2240 — `fd1 = tokfd;`
7966 fd1 = tokfd();
7967 // c:2241 — `zshlex();`
7968 zshlex();
7969 // c:2242-2243 — `if (tok != STRING && tok != ENVSTRING) YYERROR(ecused);`
7970 if tok() != STRING_LEX && tok() != ENVSTRING {
7971 set_incmdpos(oldcmdpos);
7972 set_nocorrect(oldnc);
7973 zerr("expected word after redirection");
7974 return 0;
7975 }
7976 // c:2244 — `incmdpos = oldcmdpos;`
7977 set_incmdpos(oldcmdpos);
7978 // c:2245 — `nocorrect = oldnc;`
7979 set_nocorrect(oldnc);
7980
7981 // c:2248-2249 — `if (fd1 == -1) fd1 = IS_READFD(type) ? 0 : 1;`
7982 let fd1 = if fd1 == -1 {
7983 if is_readfd(r#type) {
7984 0
7985 } else {
7986 1
7987 }
7988 } else {
7989 fd1
7990 };
7991
7992 // c:2251 — `name = tokstr;`
7993 name = tokstr().unwrap_or_default();
7994
7995 // c:2253-2321 — switch on type:
7996 match r#type {
7997 // c:2254-2300 — REDIR_HEREDOC / REDIR_HEREDOCDASH
7998 x if x == REDIR_HEREDOC || x == REDIR_HEREDOCDASH => {
7999 // c:2257 — `struct heredocs **hd;`
8000 // c:2258 — `int htype = type;`
8001 let htype = r#type;
8002 // c:2260-2261 — `if (strchr(tokstr, '\n')) YYERROR(ecused);`
8003 if name.contains('\n') {
8004 zerr("here-doc terminator contains newline");
8005 return 0;
8006 }
8007 // c:2263-2273 — `ncodes = 5; if (idstring) { type |= MASK; ncodes = 6; }`
8008 if idstring.is_some() {
8009 r#type |= REDIR_VARID_MASK;
8010 ncodes = 6;
8011 } else {
8012 ncodes = 5;
8013 }
8014 // c:2277 — `ecispace(r, ncodes);`
8015 ecispace(r, ncodes);
8016 // c:2278 — `*rp = r + ncodes;`
8017 *rp = r + ncodes;
8018 // c:2279 — `ecbuf[r] = WCB_REDIR(type | REDIR_FROM_HEREDOC_MASK);`
8019 ECBUF.with_borrow_mut(|b| {
8020 b[r] = WCB_REDIR((r#type | REDIR_FROM_HEREDOC_MASK) as wordcode);
8021 // c:2280 — `ecbuf[r + 1] = fd1;`
8022 b[r + 1] = fd1 as wordcode;
8023 });
8024 // c:2282-2286 — r+2..4 are filled later by setheredoc.
8025 // c:2287-2288 — `if (idstring) ecbuf[r + 5] = ecstrcode(idstring);`
8026 if let Some(id) = idstring {
8027 let coded = ecstrcode(id);
8028 ECBUF.with_borrow_mut(|b| {
8029 b[r + 5] = coded;
8030 });
8031 }
8032 // c:2290-2296 — `for (hd = &hdocs; *hd; hd = &(*hd)->next);
8033 // *hd = zalloc(sizeof(struct heredocs));
8034 // (*hd)->next = NULL;
8035 // (*hd)->type = htype;
8036 // (*hd)->pc = r;
8037 // (*hd)->str = tokstr;`
8038 HDOCS.with_borrow_mut(|head| {
8039 let mut cur = head;
8040 while cur.is_some() {
8041 cur = &mut cur.as_mut().unwrap().next; // c:2290
8042 }
8043 *cur = Some(Box::new(crate::ported::zsh_h::heredocs {
8044 // c:2292-2296
8045 next: None,
8046 typ: htype,
8047 pc: r as i32,
8048 str: Some(name.clone()),
8049 }));
8050 });
8051 // c:2298 — `zshlex();`
8052 zshlex();
8053 // c:2299 — `return ncodes;`
8054 return ncodes as i32;
8055 }
8056 // c:2301-2308 — REDIR_WRITE / REDIR_WRITENOW
8057 x if x == REDIR_WRITE || x == REDIR_WRITENOW => {
8058 // c:2303-2305 — `if (tokstr[0] == OutangProc && tokstr[1] == Inpar)
8059 // type = REDIR_OUTPIPE;`
8060 let nb: Vec<char> = name.chars().collect();
8061 if nb.len() >= 2 && nb[0] == '\u{96}' && nb[1] == '\u{88}' {
8062 r#type = REDIR_OUTPIPE;
8063 } else if nb.len() >= 2 && nb[0] == '\u{94}' && nb[1] == '\u{88}' {
8064 // c:2306-2307 — `else if (tokstr[0] == Inang && tokstr[1] == Inpar) YYERROR;`
8065 zerr("par_redir: < before >");
8066 return 0;
8067 }
8068 }
8069 // c:2309-2315 — REDIR_READ
8070 x if x == REDIR_READ => {
8071 let nb: Vec<char> = name.chars().collect();
8072 if nb.len() >= 2 && nb[0] == '\u{94}' && nb[1] == '\u{88}' {
8073 r#type = REDIR_INPIPE;
8074 } else if nb.len() >= 2 && nb[0] == '\u{96}' && nb[1] == '\u{88}' {
8075 zerr("par_redir: > before <");
8076 return 0;
8077 }
8078 }
8079 // c:2316-2320 — REDIR_READWRITE
8080 x if x == REDIR_READWRITE => {
8081 let nb: Vec<char> = name.chars().collect();
8082 if nb.len() >= 2 && (nb[0] == '\u{94}' || nb[0] == '\u{96}') && nb[1] == '\u{88}' {
8083 r#type = if nb[0] == '\u{94}' {
8084 REDIR_INPIPE
8085 } else {
8086 REDIR_OUTPIPE
8087 };
8088 }
8089 }
8090 _ => {}
8091 }
8092 // c:2322 — `zshlex();`
8093 zshlex();
8094
8095 // c:2326-2333 — `if (idstring) { type |= MASK; ncodes = 4; } else ncodes = 3;`
8096 if idstring.is_some() {
8097 r#type |= REDIR_VARID_MASK;
8098 ncodes = 4;
8099 } else {
8100 ncodes = 3;
8101 }
8102
8103 // c:2334 — `ecispace(r, ncodes);`
8104 ecispace(r, ncodes);
8105 // c:2335 — `*rp = r + ncodes;`
8106 *rp = r + ncodes;
8107 // c:2336 — `ecbuf[r] = WCB_REDIR(type);`
8108 let coded_name = ecstrcode(&name);
8109 ECBUF.with_borrow_mut(|b| {
8110 b[r] = WCB_REDIR(r#type as wordcode);
8111 // c:2337 — `ecbuf[r + 1] = fd1;`
8112 b[r + 1] = fd1 as wordcode;
8113 // c:2338 — `ecbuf[r + 2] = ecstrcode(name);`
8114 b[r + 2] = coded_name;
8115 });
8116 // c:2339-2340 — `if (idstring) ecbuf[r + 3] = ecstrcode(idstring);`
8117 if let Some(id) = idstring {
8118 let coded_id = ecstrcode(id);
8119 ECBUF.with_borrow_mut(|b| {
8120 b[r + 3] = coded_id;
8121 });
8122 }
8123 // c:2342 — `return ncodes;`
8124 ncodes as i32
8125}
8126
8127/// Port of `IS_READFD(type)` macro from `Src/zsh.h` — determines
8128/// default fd (0 for read-ish, 1 for write-ish) when none specified.
8129fn is_readfd(t: i32) -> bool {
8130 matches!(
8131 t,
8132 x if x == REDIR_READ
8133 || x == REDIR_READWRITE
8134 || x == REDIR_MERGEIN
8135 || x == REDIR_HEREDOC
8136 || x == REDIR_HEREDOCDASH
8137 || x == REDIR_HERESTR
8138 )
8139}
8140
8141/// Parse a program (list of lists)
8142/// Parse a complete program (top-level entry). Calls
8143/// parse_program_until with no end-token sentinel. Direct port of
8144/// zsh/Src/parse.c:614-720 `parse_event` / `par_list` /
8145/// `par_event` flow. C distinguishes COND_EVENT (single command
8146/// for here-string) from full event parse; zshrs's parse_program
8147/// is the full-event entry.
8148fn parse_program() -> ZshProgram {
8149 parse_program_until(None, false)
8150}
8151
8152/// Parse a program until we hit an end token
8153/// Parse a program until one of `end_tokens` is seen (or EOF).
8154/// Drives par_list in a loop. C equivalent: the body of par_event
8155/// (parse.c:635-695) iterating par_list against the lexer.
8156///
8157/// `single_event` mirrors C par_event's `endtok == ENDINPUT` top-level
8158/// mode: when true, parse ONE event (the sublists `;`/`&`-chained on one
8159/// logical line, including any multi-line compound) and STOP at the
8160/// terminating top-level newline WITHOUT reading the next line — so
8161/// loop() executes the event then re-reads for the next, interleaving
8162/// input with execution exactly as zsh does. Whole-program and
8163/// nested-body parses pass false (multi-list to EOF / end token).
8164fn parse_program_until(end_tokens: Option<&[lextok]>, single_event: bool) -> ZshProgram {
8165 let mut lists = Vec::new();
8166
8167 loop {
8168 // Skip separators
8169 while tok() == SEPER || tok() == NEWLIN {
8170 zshlex();
8171 }
8172
8173 if tok() == ENDINPUT {
8174 break;
8175 }
8176 if tok() == LEXERR {
8177 // c:Src/parse.c:671-680 par_event — when the lexer
8178 // returned LEXERR (e.g. unbalanced `$((1+(2))` math
8179 // sub, unterminated string, etc.), C emits `yyerror(1)`
8180 // and sets errflag so the script aborts with a parse
8181 // error diagnostic + non-zero exit. zshrs's
8182 // parse_program_until previously just `break`'d on
8183 // LEXERR, silently swallowing the malformed input and
8184 // exiting rc=0 — so `$((1+(2))` ran as if it were
8185 // empty. Bug #529 in docs/BUGS.md. Emit yyerror
8186 // mirroring the C behaviour; the broken script then
8187 // surfaces the parse error to the caller.
8188 // c:Src/parse.c — empty-msg yyerror call mapped to the
8189 // C-faithful `yyerror(0)` (zshrs's previous shape).
8190 yyerror(0);
8191 break;
8192 }
8193
8194 // Check for end tokens
8195 if let Some(end_toks) = end_tokens {
8196 if end_toks.contains(&tok()) {
8197 break;
8198 }
8199 }
8200
8201 // Also stop at these tokens when not explicitly looking for them
8202 // Note: Else/Elif/Then are NOT here - they're handled by par_if
8203 // to allow nested if statements inside case arms, loops, etc.
8204 //
8205 // c:Src/parse.c:par_event — when an orphan terminator (DONE
8206 // outside a loop, FI outside an if, ESAC outside a case)
8207 // appears at the top level (end_tokens=None), C errors via
8208 // YYERROR. zshrs's `break` silently accepted `done`/`fi`/
8209 // `esac` as no-op input. Error at the outermost call so
8210 // unscoped terminators don't sneak through; nested calls
8211 // still break cleanly via the end_tokens contains-check
8212 // above.
8213 match tok() {
8214 DONE | FI | ESAC | DOLOOP if end_tokens.is_none() => {
8215 // c:Src/parse.c:par_event — emit the specific token
8216 // name (`done`, `fi`, `esac`, `do`) so error-parsing
8217 // tools can identify the unmatched terminator. C zsh
8218 // writes `parse error near \`<tok>'`; the Rust port
8219 // was emitting a generic "orphan terminator" string.
8220 // Bug #142, #413.
8221 let name = match tok() {
8222 DONE => "done",
8223 FI => "fi",
8224 ESAC => "esac",
8225 DOLOOP => "do",
8226 _ => "orphan terminator",
8227 };
8228 zerr(&format!("parse error near `{}'", name));
8229 break;
8230 }
8231 DSEMI | SEMIAMP | SEMIBAR if end_tokens.is_none() => {
8232 // c:Src/parse.c:par_event — case-arm terminators
8233 // (`;;`, `;&`, `;|`) outside a case construct are a
8234 // parse error. zshrs's `break` silently accepted them
8235 // at top level, truncating the rest of the script.
8236 // Bug #141 in docs/BUGS.md.
8237 let name = match tok() {
8238 DSEMI => ";;",
8239 SEMIAMP => ";&",
8240 SEMIBAR => ";|",
8241 _ => "case terminator",
8242 };
8243 zerr(&format!("parse error near `{}'", name));
8244 break;
8245 }
8246 OUTBRACE_TOK if end_tokens.is_none() => {
8247 // c:Src/parse.c:par_event — orphan `}` (no matching
8248 // `{` opener) at top level is a parse error. zshrs's
8249 // generic break swallowed it silently, leaving the
8250 // `echo a` in `echo a }` running and ignoring the
8251 // stray brace. Bug #168 in docs/BUGS.md.
8252 zerr("parse error near `}'");
8253 break;
8254 }
8255 OUTBRACE_TOK | DSEMI | SEMIAMP | SEMIBAR | DONE | FI | ESAC | ZEND => break,
8256 _ => {}
8257 }
8258
8259 match par_list(single_event) {
8260 Some((list, terminated)) => {
8261 let detected = simple_name_with_inoutpar(&list);
8262 let was_detected = detected.is_some();
8263 lists.push(list);
8264 // Synthesize a FuncDef for the `name() { body }` shape
8265 // at parse time so body_source is captured while the
8266 // lexer still has the input. The lexer port emits
8267 // `name(` as a single Word ending in `<Inpar><Outpar>`,
8268 // so the Simple list is followed by an Inbrace once
8269 // separators are skipped. For `name() cmd args` the
8270 // body has already been swallowed into the same
8271 // Simple's words tail — synthesize directly from there.
8272 if let Some((names, body_argv)) = detected {
8273 if !body_argv.is_empty() {
8274 // One-line body already in the Simple. Build
8275 // a Simple from body_argv as the function body.
8276 lists.pop();
8277 let body_simple = ZshCommand::Simple(ZshSimple {
8278 assigns: Vec::new(),
8279 words: body_argv,
8280 redirs: Vec::new(),
8281 });
8282 let body_list = ZshList {
8283 sublist: ZshSublist {
8284 pipe: ZshPipe {
8285 cmd: body_simple,
8286 next: None,
8287 lineno: lineno(),
8288 merge_stderr: false,
8289 },
8290 next: None,
8291 flags: SublistFlags::default(),
8292 },
8293 flags: ListFlags::default(),
8294 };
8295 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
8296 names,
8297 body: Box::new(ZshProgram {
8298 lists: vec![body_list],
8299 }),
8300 tracing: false,
8301 auto_call_args: None,
8302 body_source: None,
8303 });
8304 let synthetic = ZshList {
8305 sublist: ZshSublist {
8306 pipe: ZshPipe {
8307 cmd: funcdef,
8308 next: None,
8309 lineno: lineno(),
8310 merge_stderr: false,
8311 },
8312 next: None,
8313 flags: SublistFlags::default(),
8314 },
8315 flags: ListFlags::default(),
8316 };
8317 lists.push(synthetic);
8318 continue;
8319 }
8320 // Else: words.len() == 1 (only the trailing `name()`
8321 // word), brace body follows. `names` may carry
8322 // multiple identifiers from the `fna fnb fnc()`
8323 // shorthand — all share the same brace body per
8324 // src/zsh/Src/parse.c:1666 par_funcdef wordlist.
8325 // Skip separators on the real lexer; safe because
8326 // parse_program's next iteration would also skip them.
8327 while tok() == SEPER || tok() == NEWLIN {
8328 zshlex();
8329 }
8330 if tok() == INBRACE_TOK {
8331 // Capture body_start BEFORE the lexer
8332 // advances past the first body token. The
8333 // outer zshlex() consumed `{`; lexer.pos
8334 // is now right after `{`. The next
8335 // `zshlex()` would advance past `echo`,
8336 // making body_start land mid-body and
8337 // lose the first word — `typeset -f f`
8338 // printed `a; echo b` instead of
8339 // `echo a; echo b` for `f() { echo a;
8340 // echo b }`.
8341 let body_start = pos();
8342 zshlex();
8343 // c:Src/parse.c — synth funcdef body terminates
8344 // at OUTBRACE_TOK. Explicit end-token avoids
8345 // the top-level stray-`}` arm. Bug #167/#168.
8346 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
8347 let body_end = if tok() == OUTBRACE_TOK {
8348 pos().saturating_sub(1)
8349 } else {
8350 pos()
8351 };
8352 let body_source = input_slice(body_start, body_end)
8353 .map(|s| s.trim().to_string())
8354 .filter(|s| !s.is_empty());
8355 if tok() == OUTBRACE_TOK {
8356 zshlex();
8357 }
8358 // Replace the Simple list with a FuncDef list.
8359 lists.pop();
8360 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
8361 names,
8362 body: Box::new(body),
8363 tracing: false,
8364 auto_call_args: None,
8365 body_source,
8366 });
8367 let synthetic = ZshList {
8368 sublist: ZshSublist {
8369 pipe: ZshPipe {
8370 cmd: funcdef,
8371 next: None,
8372 lineno: lineno(),
8373 merge_stderr: false,
8374 },
8375 next: None,
8376 flags: SublistFlags::default(),
8377 },
8378 flags: ListFlags::default(),
8379 };
8380 lists.push(synthetic);
8381 } else if !matches!(tok(), ENDINPUT | OUTBRACE_TOK | SEPER | NEWLIN) {
8382 // No-brace one-line body: `foo() echo hello`.
8383 // Parse a single command for the body.
8384 let body_cmd = par_cmd();
8385 if let Some(cmd) = body_cmd {
8386 let body_list = ZshList {
8387 sublist: ZshSublist {
8388 pipe: ZshPipe {
8389 cmd,
8390 next: None,
8391 lineno: lineno(),
8392 merge_stderr: false,
8393 },
8394 next: None,
8395 flags: SublistFlags::default(),
8396 },
8397 flags: ListFlags::default(),
8398 };
8399 lists.pop();
8400 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
8401 names: names.clone(),
8402 body: Box::new(ZshProgram {
8403 lists: vec![body_list],
8404 }),
8405 tracing: false,
8406 auto_call_args: None,
8407 body_source: None,
8408 });
8409 let synthetic = ZshList {
8410 sublist: ZshSublist {
8411 pipe: ZshPipe {
8412 cmd: funcdef,
8413 next: None,
8414 lineno: lineno(),
8415 merge_stderr: false,
8416 },
8417 next: None,
8418 flags: SublistFlags::default(),
8419 },
8420 flags: ListFlags::default(),
8421 };
8422 lists.push(synthetic);
8423 }
8424 }
8425 }
8426 // c:769-803 + c:644-682 — lists only chain across
8427 // explicit SEPER/AMPER/AMPERBANG. When par_list ended
8428 // WITHOUT a terminator, C's grammar says the list is
8429 // over: nested contexts (while-cond, brace bodies)
8430 // return to the caller with the dangling token (so
8431 // `while {false} {print no}` hands the second `{` to
8432 // par_while as the loop body), and the TOP level
8433 // (par_event, c:671-680) yyerrors — `{false} {print
8434 // no}` is `parse error near \`{'` in zsh. The funcdef
8435 // synthesis branch above manages its own following
8436 // tokens (`f() { body }` legitimately has INBRACE
8437 // right after the Simple), so it is exempt.
8438 if !was_detected && !terminated {
8439 match tok() {
8440 // End-of-input / lex error: loop top handles.
8441 ENDINPUT | LEXERR => {}
8442 // Construct closers and orphan terminators:
8443 // the loop-top match already errors/breaks
8444 // for these with the right diagnostics.
8445 OUTBRACE_TOK | DSEMI | SEMIAMP | SEMIBAR | DONE | FI | ESAC | ZEND
8446 | DOLOOP | ELSE | ELIF | THEN => {}
8447 t if end_tokens.map_or(false, |e| e.contains(&t)) => {}
8448 _ if end_tokens.is_some() => {
8449 // Nested list context: C par_list just
8450 // ends; the construct parser (par_while
8451 // body dispatch, par_subsh closer check)
8452 // deals with the token.
8453 break;
8454 }
8455 offending => {
8456 // Top level: C par_event yyerror(1) +
8457 // errflag, c:671-682. Same tokstr
8458 // injection as the None arm below so the
8459 // "near `X'" tail survives punctuation
8460 // tokens with no tokstr.
8461 if crate::ported::lex::tokstr().is_none() {
8462 let i = offending as usize;
8463 if i < crate::ported::lex::tokstrings.len() {
8464 if let Some(s) = crate::ported::lex::tokstrings[i] {
8465 crate::ported::lex::set_tokstr(Some(s.to_string()));
8466 }
8467 }
8468 }
8469 set_tok(LEXERR); // c:672
8470 yyerror(1);
8471 let noerrs_v =
8472 *crate::ported::utils::noerrs_lock().lock().unwrap();
8473 if noerrs_v != 2 {
8474 errflag.fetch_or(
8475 crate::ported::zsh_h::ERRFLAG_ERROR,
8476 Ordering::SeqCst,
8477 );
8478 }
8479 break;
8480 }
8481 }
8482 }
8483 }
8484 None => {
8485 // c:Src/parse.c:644-645 par_event — `if (tok ==
8486 // ENDINPUT) return 0;`. End-of-input is NORMAL — no
8487 // diagnostic. The yyerror path at c:670-682 fires only
8488 // when par_sublist actually failed (the C `!r` branch).
8489 //
8490 // zshrs's previous fix here unconditionally called
8491 // yyerror on every None return, breaking legitimate
8492 // end-of-input scenarios like `(echo sub)` (par_list
8493 // returns None after the subshell parser consumes the
8494 // whole construct, leaving tok at ENDINPUT).
8495 if tok() == ENDINPUT {
8496 break;
8497 }
8498 // c:Src/parse.c:671-680 par_event — par_sublist failed:
8499 // emit the canonical yyerror with the "near `X'" tail
8500 // derived from zshlextext/tokstr().
8501 //
8502 // c:Src/lex.c:1965 — `zshlextext = tokstrings[tok]`
8503 // is set DURING zshlex, before set_tok(LEXERR) here.
8504 // zshrs's zshlex doesn't update LEX_TOKSTR for
8505 // single-char punctuation tokens (OUTPAR/INPAR/etc.),
8506 // so by the time yyerror runs, tokstr() is None and
8507 // the tail "near `)'" is lost. Inject the current
8508 // tok's canonical text into LEX_TOKSTR here so the
8509 // C-faithful yyerror lookup finds it. Mirrors the
8510 // visible effect of C's zshlextext fallback.
8511 let already_flagged = (errflag.load(Ordering::SeqCst)
8512 & crate::ported::zsh_h::ERRFLAG_ERROR)
8513 != 0;
8514 let offending_tok = tok();
8515 if crate::ported::lex::tokstr().is_none() {
8516 let i = offending_tok as usize;
8517 if i < crate::ported::lex::tokstrings.len() {
8518 if let Some(s) = crate::ported::lex::tokstrings[i] {
8519 crate::ported::lex::set_tokstr(Some(
8520 s.to_string(),
8521 ));
8522 }
8523 }
8524 }
8525 set_tok(LEXERR); // c:672
8526 yyerror(if already_flagged { 0 } else { 1 });
8527 // c:Src/parse.c:679-680 — `if (noerrs != 2) errflag |=
8528 // ERRFLAG_ERROR;`. C sets errflag explicitly after the
8529 // yyerror(1) print-only branch. Without this, the
8530 // caller (`execute_script_zsh_pipeline` here, `bin_eval`
8531 // for the parse_string path) can't distinguish "no
8532 // parse error" from "parse error already printed", so
8533 // $? stays at 0 after `eval ')foo'`.
8534 let noerrs_v =
8535 *crate::ported::utils::noerrs_lock().lock().unwrap();
8536 if noerrs_v != 2 {
8537 errflag.fetch_or(
8538 crate::ported::zsh_h::ERRFLAG_ERROR,
8539 Ordering::SeqCst,
8540 );
8541 }
8542 break;
8543 }
8544 }
8545
8546 // c:Src/parse.c par_event — single-event (top-level, endtok ==
8547 // ENDINPUT) mode stops at the terminating top-level newline. The
8548 // SEPER par_list arm above left `tok` AT the newline-SEPER (not
8549 // consumed, LEX_ISNEWLIN > 0), so break before the loop-top
8550 // separator skip would `zshlex()` past it and read the next
8551 // line. A `;`-SEPER (LEX_ISNEWLIN <= 0) or `&` was consumed by
8552 // par_list and leaves a different token here, so chaining keeps
8553 // looping (same logical line). Multi-line compounds were already
8554 // fully consumed inside par_list, so their internal newlines
8555 // never reach this check.
8556 if single_event
8557 && tok() == SEPER
8558 && crate::ported::lex::LEX_ISNEWLIN.with(|c| c.get()) > 0
8559 {
8560 break;
8561 }
8562 }
8563
8564 ZshProgram { lists }
8565}
8566
8567/// Parse an assignment
8568/// Parse an assignment word `NAME=value` or `NAME=(arr items)`.
8569/// Sub-routine of par_simple. The C source handles assignments
8570/// inline in par_simple via the ENVSTRING/ENVARRAY token paths
8571/// (parse.c:1842-2000ish); zshrs splits it out to a dedicated
8572/// helper for clarity.
8573fn parse_assign() -> Option<ZshAssign> {
8574 // Helper: locate the Equals-marker that delimits NAME from
8575 // VALUE in an assignment-shaped tokstr. The lexer META-encodes
8576 // EVERY `=` (including those inside `${var%%=foo}` strip
8577 // patterns or `[idx]=...` subscripts), so a naive
8578 // `tokstr.find(Equals)` would split at the first inner `=`
8579 // and break the whole assignment. Walk the string skipping
8580 // brace and bracket depth so the assignment's `=` (the one
8581 // after the last `]` of the LHS subscript / or after the
8582 // bare name) is the one we land on.
8583 fn find_assign_equals(s: &str) -> Option<usize> {
8584 let target = Equals;
8585 let mut brace = 0i32;
8586 let mut bracket = 0i32;
8587 let mut paren = 0i32;
8588 for (i, c) in s.char_indices() {
8589 match c {
8590 '{' | '\u{8f}' /* Inbrace */ => brace += 1,
8591 '}' | '\u{90}' /* Outbrace */ => {
8592 if brace > 0 {
8593 brace -= 1;
8594 }
8595 }
8596 '[' | '\u{91}' /* Inbrack */ => bracket += 1,
8597 ']' | '\u{92}' /* Outbrack */ => {
8598 if bracket > 0 {
8599 bracket -= 1;
8600 }
8601 }
8602 '(' | '\u{88}' /* Inpar */ => paren += 1,
8603 ')' | '\u{8a}' /* Outpar */ => {
8604 if paren > 0 {
8605 paren -= 1;
8606 }
8607 }
8608 _ if c == target && brace == 0 && bracket == 0 && paren == 0 => {
8609 return Some(i);
8610 }
8611 _ => {}
8612 }
8613 }
8614 None
8615 }
8616
8617 let _ts_tokstr = tokstr()?;
8618 let tokstr = _ts_tokstr.as_str();
8619
8620 // Parse name=value or name+=value.
8621 let (name, value_str, append) = if tok() == ENVARRAY {
8622 let (name, append) = if let Some(stripped) = tokstr.strip_suffix('+') {
8623 (stripped, true)
8624 } else {
8625 (tokstr, false)
8626 };
8627 (name.to_string(), String::new(), append)
8628 } else if let Some(pos) = find_assign_equals(tokstr) {
8629 let name_part = &tokstr[..pos];
8630 let (name, append) = if let Some(stripped) = name_part.strip_suffix('+') {
8631 (stripped, true)
8632 } else {
8633 (name_part, false)
8634 };
8635 (
8636 name.to_string(),
8637 tokstr[pos + Equals.len_utf8()..].to_string(),
8638 append,
8639 )
8640 } else if let Some(pos) = tokstr.find('=') {
8641 // Fallback to literal '=' for compatibility
8642 let name_part = &tokstr[..pos];
8643 let (name, append) = if let Some(stripped) = name_part.strip_suffix('+') {
8644 (stripped, true)
8645 } else {
8646 (name_part, false)
8647 };
8648 (name.to_string(), tokstr[pos + 1..].to_string(), append)
8649 } else {
8650 return None;
8651 };
8652
8653 let value = if tok() == ENVARRAY {
8654 // Array assignment: name=(...)
8655 // c:Src/parse.c:1895 par_simple ENVARRAY arm:
8656 // `int oldcmdpos = incmdpos; ... incmdpos = 0; ... zshlex();`
8657 // Reset incmdpos to false BEFORE the array body's first lex so
8658 // a leading `{...}` (brace expansion) doesn't trip the
8659 // empty-buf+incmdpos rule at lex.c:1141 that returns `{` as
8660 // STRING and lets the reswd_lookup promote it to INBRACE_TOK.
8661 let oldcmdpos = crate::ported::lex::incmdpos();
8662 crate::ported::lex::set_incmdpos(false);
8663 // c:Src/parse.c:2041-2046 — `intypeset = 0;` BEFORE reading
8664 // the array body, restored to the saved value after. Without
8665 // this, an element containing `=` (`typeset -a w=( VAR=1
8666 // sudo )`) re-triggers the lexer's intypeset assignment
8667 // detection and lexes `VAR=1` as ENVSTRING instead of a plain
8668 // STRING element — the body loop ends early on the non-STRING
8669 // token and the parser errors "near `)'".
8670 let old_intypeset = crate::ported::lex::intypeset();
8671 crate::ported::lex::set_intypeset(false);
8672 let mut elements = Vec::new();
8673 zshlex(); // skip past token
8674
8675 let mut arr_iters = 0;
8676 const MAX_ARRAY_ELEMENTS: usize = 10_000;
8677 while matches!(tok(), STRING_LEX | SEPER | NEWLIN) {
8678 arr_iters += 1;
8679 if arr_iters > MAX_ARRAY_ELEMENTS {
8680 zerr("array assignment exceeded maximum elements");
8681 break;
8682 }
8683 if tok() == STRING_LEX {
8684 let _ts_s = crate::ported::lex::tokstr();
8685 if let Some(s) = _ts_s.as_deref() {
8686 elements.push(s.to_string());
8687 }
8688 }
8689 zshlex();
8690 }
8691 // c:Src/parse.c — `incmdpos = oldcmdpos;` (restore at end of arm)
8692 crate::ported::lex::set_incmdpos(oldcmdpos);
8693 // c:Src/parse.c:2046 — `intypeset = 1;` (restore for any
8694 // follow-on `b=( … )` in `typeset a=(…) b=(…)`).
8695 crate::ported::lex::set_intypeset(old_intypeset);
8696
8697 // The closing Outpar is consumed here. The outer par_simple
8698 // loop will then `zshlex()` past whatever follows (typically
8699 // a separator or the next word) — calling zshlex twice in
8700 // tandem (here AND in par_simple) over-advances and merges
8701 // a following `name() { … }` funcdef into the same Simple.
8702 // We only consume Outpar; let the caller handle the rest.
8703 // Without this guard `g=(o1); f() { :; }` parsed as one
8704 // Simple with assigns=[g] and words=["f()"] (one token).
8705 if tok() == OUTPAR_TOK {
8706 // Note: do NOT zshlex() here. par_simple's `lexer
8707 // .zshlex()` after `parse_assign` returns advances past
8708 // the Outpar onto the next significant token.
8709 //
8710 // Force `incmdpos=true` so the next zshlex() recognizes
8711 // a follow-up `b=(...)` / `b=val` as Envarray/Envstring.
8712 // The lexer flips incmdpos to false on bare Outpar (which
8713 // is correct for subshell-close context), but for an
8714 // array-assignment close more assigns/words may follow.
8715 set_incmdpos(true);
8716 }
8717
8718 ZshAssignValue::Array(elements)
8719 } else {
8720 ZshAssignValue::Scalar(value_str)
8721 };
8722
8723 Some(ZshAssign {
8724 name,
8725 value,
8726 append,
8727 })
8728}
8729
8730/// AST `par_redir` variant accepting an idstring for the
8731/// `{var}>file` brace-FD shape. C signature
8732/// `par_redir(int *rp, char *idstring)` (parse.c:2229). The
8733/// idstring is stored in the resulting ZshRedir.varid for the
8734/// executor to bind the named variable to the chosen fd.
8735fn par_redir_with_id(idstring: Option<&str>) -> Option<ZshRedir> {
8736 let varid: Option<String> = idstring.map(|s| s.to_string());
8737 let rtype = match tok() {
8738 OUTANG_TOK => REDIR_WRITE,
8739 OUTANGBANG => REDIR_WRITENOW,
8740 DOUTANG => REDIR_APP,
8741 DOUTANGBANG => REDIR_APPNOW,
8742 INANG_TOK => REDIR_READ,
8743 INOUTANG => REDIR_READWRITE,
8744 DINANG => REDIR_HEREDOC,
8745 DINANGDASH => REDIR_HEREDOCDASH,
8746 TRINANG => REDIR_HERESTR,
8747 INANGAMP => REDIR_MERGEIN,
8748 OUTANGAMP => REDIR_MERGEOUT,
8749 AMPOUTANG => REDIR_ERRWRITE,
8750 OUTANGAMPBANG => REDIR_ERRWRITENOW,
8751 DOUTANGAMP => REDIR_ERRAPP,
8752 DOUTANGAMPBANG => REDIR_ERRAPPNOW,
8753 _ => return None,
8754 };
8755
8756 let fd = if tokfd() >= 0 {
8757 tokfd()
8758 } else if matches!(
8759 rtype,
8760 REDIR_READ
8761 | REDIR_READWRITE
8762 | REDIR_MERGEIN
8763 | REDIR_HEREDOC
8764 | REDIR_HEREDOCDASH
8765 | REDIR_HERESTR
8766 ) {
8767 0
8768 } else {
8769 1
8770 };
8771
8772 // c:2234-2245 — save/restore incmdpos and nocorrect around the
8773 // zshlex that consumes the redir target word:
8774 // oldcmdpos = incmdpos; incmdpos = 0;
8775 // oldnc = nocorrect;
8776 // if (tok != INANG && tok != INOUTANG) nocorrect = 1;
8777 // ... zshlex; check tok; ...
8778 // incmdpos = oldcmdpos; nocorrect = oldnc;
8779 // Without this, a redir target lexes in the parent's incmdpos
8780 // (re-promoting `{` / reswords) AND with parent nocorrect (so
8781 // spelling-correction wrongly runs inside `> $(cmd)` etc.).
8782 let oldcmdpos = incmdpos();
8783 set_incmdpos(false);
8784 let oldnc = nocorrect();
8785 let cur = tok();
8786 if cur != INANG_TOK && cur != INOUTANG {
8787 set_nocorrect(1);
8788 }
8789 zshlex();
8790
8791 let name = match tok() {
8792 STRING_LEX | ENVSTRING => {
8793 let n = tokstr().unwrap_or_default();
8794 // c:2244-2245 — restore incmdpos / nocorrect right after
8795 // the redir target word is confirmed, BEFORE the trailing
8796 // zshlex advances past it. The advance itself is deferred
8797 // below so REDIR_HEREDOC[DASH] can push onto HDOCS first
8798 // (matching the wordcode variant at parse.rs:6894-6908) —
8799 // otherwise the NEWLIN drained by that zshlex sees an
8800 // empty HDOCS list and gethere never collects the body.
8801 set_incmdpos(oldcmdpos);
8802 set_nocorrect(oldnc);
8803 n
8804 }
8805 _ => {
8806 set_incmdpos(oldcmdpos);
8807 set_nocorrect(oldnc);
8808 zerr("expected word after redirection");
8809 return None;
8810 }
8811 };
8812
8813 // Heredoc terminator capture. C parse.c:2254-2317 par_redir builds
8814 // a `struct heredocs` entry here for REDIR_HEREDOC[DASH]. zshrs
8815 // pushes onto HDOCS (canonical C linked list, c:2290-2296) AND
8816 // onto LEX_HEREDOCS (Rust-only AST-glue Vec carrying parsed-out
8817 // terminator/strip_tabs/quoted metadata for downstream AST
8818 // consumers). Quoted terminators (`<<'EOF'` / `<<"EOF"` / `<<\EOF`)
8819 // disable expansion in the body — Snull `\u{9d}` marks single-quote,
8820 // Dnull `\u{9e}` marks double-quote, Bnull `\u{9f}` marks
8821 // backslash-escaped chars.
8822 let heredoc_idx = if matches!(rtype, REDIR_HEREDOC | REDIR_HEREDOCDASH) {
8823 let strip_tabs = rtype == REDIR_HEREDOCDASH;
8824 let quoted = name.contains('\u{9d}')
8825 || name.contains('\u{9e}')
8826 || name.contains('\u{9f}')
8827 || name.starts_with('\'')
8828 || name.starts_with('"');
8829 let term = name
8830 .chars()
8831 .filter(|c| {
8832 *c != '\'' && *c != '"' && *c != '\u{9d}' && *c != '\u{9e}' && *c != '\u{9f}'
8833 })
8834 .collect::<String>();
8835 // c:2290-2296 — `for (hd = &hdocs; *hd; hd = &(*hd)->next);
8836 // *hd = zalloc(sizeof(struct heredocs));
8837 // (*hd)->next = NULL;
8838 // (*hd)->type = htype;
8839 // (*hd)->pc = r;
8840 // (*hd)->str = tokstr;`
8841 // AST path has no wordcode pc to patch; use -1 sentinel so the
8842 // inline NEWLIN walk in `zshlex()` skips the setheredoc call.
8843 HDOCS.with_borrow_mut(|head| {
8844 let mut cur = head;
8845 while cur.is_some() {
8846 cur = &mut cur.as_mut().unwrap().next; // c:2290
8847 }
8848 *cur = Some(Box::new(crate::ported::zsh_h::heredocs {
8849 // c:2292-2296
8850 next: None,
8851 typ: rtype,
8852 pc: -1,
8853 str: Some(name.clone()),
8854 }));
8855 });
8856 // zshrs-only: push parallel AST-glue entry onto LEX_HEREDOCS.
8857 let idx = LEX_HEREDOCS.with_borrow_mut(|v| {
8858 v.push(HereDoc {
8859 terminator: term,
8860 strip_tabs,
8861 content: String::new(),
8862 quoted,
8863 processed: false,
8864 });
8865 v.len() - 1
8866 });
8867 Some(idx)
8868 } else {
8869 None
8870 };
8871
8872 // c:2298 (heredoc) / c:2322 (other redirs) — final zshlex() advance
8873 // past the redir target word. MUST run after the HDOCS push above
8874 // so the heredoc-drain inside this zshlex sees the new entry. For
8875 // non-heredoc forms the order is irrelevant; consolidating to a
8876 // single tail-call here matches the wordcode variant.
8877 zshlex();
8878
8879 Some(ZshRedir {
8880 rtype,
8881 fd,
8882 name,
8883 heredoc: None,
8884 varid,
8885 heredoc_idx,
8886 })
8887}
8888
8889/// Parse C-style for loop: for (( init; cond; step ))
8890/// Parse the c-style `for ((init; cond; incr)) do BODY done`.
8891/// Inner branch of zsh/Src/parse.c:1100-1140 inside par_for.
8892/// Recognized when the token after FOR is DINPAR (the `((`
8893/// detected by gettok via dbparens setup).
8894fn parse_for_cstyle() -> Option<ZshCommand> {
8895 // We're at (( (Dinpar None) - the opening ((
8896 // Lexer returns:
8897 // Dinpar None - opening ((
8898 // Dinpar "init" - init expression, semicolon consumed
8899 // Dinpar "cond" - cond expression, semicolon consumed
8900 // Doutpar "step" - step expression, closing )) consumed
8901 zshlex(); // Get init: Dinpar "i=0"
8902
8903 if tok() != DINPAR {
8904 zerr("expected init expression in for ((");
8905 return None;
8906 }
8907 let init = tokstr().unwrap_or_default();
8908
8909 zshlex(); // Get cond: Dinpar "i<10"
8910
8911 if tok() != DINPAR {
8912 zerr("expected condition in for ((");
8913 return None;
8914 }
8915 let cond = tokstr().unwrap_or_default();
8916
8917 zshlex(); // Get step: Doutpar "i++"
8918
8919 if tok() != DOUTPAR {
8920 zerr("expected )) in for");
8921 return None;
8922 }
8923 let step = tokstr().unwrap_or_default();
8924
8925 // c:1110 — `infor = 0;` before the body opener. The companion
8926 // `incmdpos = 1;` at c:1111 is intentionally skipped here for
8927 // the same reason c:1094's `incmdpos = 0;` is skipped in
8928 // par_for above — zshrs doesn't mirror the full
8929 // incmdpos state-machine inline.
8930 set_infor(0); // c:1110
8931 zshlex(); // Move past ))
8932
8933 skip_separators();
8934 let body = parse_loop_body(false, false)?;
8935
8936 Some(ZshCommand::For(ZshFor {
8937 var: String::new(),
8938 list: ForList::CStyle { init, cond, step },
8939 body: Box::new(body),
8940 is_select: false,
8941 }))
8942}
8943
8944/// Parse select loop (same syntax as for)
8945/// Parse `select NAME in WORDS; do BODY; done`. Same shape as
8946/// `for NAME in WORDS; do ...` but with menu-prompt semantics in
8947/// the executor. C equivalent: the SELECT case in par_for at
8948/// parse.c:1087-1207 (selects share parser flow with foreach).
8949fn parse_select() -> Option<ZshCommand> {
8950 // `select` shares par_for's grammar (var, words, body) but the
8951 // compile path is different (interactive prompt loop).
8952 match par_for()? {
8953 ZshCommand::For(mut f) => {
8954 f.is_select = true;
8955 Some(ZshCommand::For(f))
8956 }
8957 other => Some(other),
8958 }
8959}
8960
8961/// Parse loop body (do...done, {...}, or shortloop)
8962/// Parse the `do BODY done` body of a for/while/until/select/
8963/// repeat loop. Direct equivalent of zsh's parse.c handling
8964/// inside the loop builders — they all consume DOLOOP, parse a
8965/// list until DONE, and return the list. The `foreach_style`
8966/// flag signals foreach (where short-form `for NAME in WORDS;
8967/// CMD` may skip do/done) vs c-style (which always requires
8968/// do/done).
8969///
8970/// `is_repeat` widens the SHORTLOOPS gate so `SHORTREPEAT` also
8971/// unlocks the short form for `repeat N CMD` (per c:1600
8972/// `unset(SHORTLOOPS) && unset(SHORTREPEAT)`).
8973fn parse_loop_body(foreach_style: bool, is_repeat: bool) -> Option<ZshProgram> {
8974 // c:1180-1194 — body dispatch order per par_for:
8975 // `do ... done` (DOLOOP) — primary form.
8976 // `{ ... }` (INBRACE) — alternate.
8977 // csh/CSHJUNKIELOOPS — terminator is `end`.
8978 // else if (unset(SHORTLOOPS)) — YYERROR.
8979 // else — short form (single command).
8980 if tok() == DOLOOP {
8981 zshlex();
8982 // Body parse must declare DONE as an end-token so the
8983 // parse_program_until top-level orphan-DONE guard doesn't
8984 // mis-fire on the legitimate loop terminator.
8985 let body = parse_program_until(Some(&[DONE]), false);
8986 // c:Src/parse.c:1182-1183 / :1535-1536 / :1597-1598 —
8987 // `if (tok != DONE) YYERRORV(oecused);`. zshrs previously
8988 // silently accepted EOF as a substitute for `done`, so
8989 // `for i in a; do echo hi; don` ran the loop with `don` as
8990 // a command (which then failed "command not found") instead
8991 // of erroring at parse time. Bug #403, #404.
8992 if tok() != DONE {
8993 zerr("parse error: expected `done'");
8994 return None;
8995 }
8996 zshlex();
8997 Some(body)
8998 } else if tok() == INBRACE_TOK {
8999 zshlex();
9000 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9001 // c:Src/parse.c:1186 / :1539 — `if (tok != OUTBRACE) YYERRORV`.
9002 if tok() != OUTBRACE_TOK {
9003 zerr("parse error: expected `}'");
9004 return None;
9005 }
9006 zshlex();
9007 Some(body)
9008 } else if foreach_style || isset(CSHJUNKIELOOPS) {
9009 // c:1184 / 1546 / 1595 — `else if (csh || isset(CSHJUNKIELOOPS))`.
9010 let body = parse_program_until(Some(&[ZEND]), false);
9011 // c:1190 / 1548 — `if (tok != ZEND) YYERRORV`.
9012 if tok() != ZEND {
9013 zerr("parse error: expected `end'");
9014 return None;
9015 }
9016 zshlex();
9017 Some(body)
9018 } else {
9019 // c:1190 / 1474 / 1551 / 1600 — short-form gate. C bails
9020 // with YYERROR when `unset(SHORTLOOPS) && (!is_repeat ||
9021 // unset(SHORTREPEAT))`. zshrs's option machinery isn't
9022 // initialised at parse-test time (no `init_main` →
9023 // `install_emulation_defaults`), so a strict port here
9024 // body. parse_init seeds SHORTLOOPS=on mirroring C
9025 // `install_emulation_defaults`, so this fires only when a
9026 // script explicitly disabled the option.
9027 if unset(SHORTLOOPS) && (!is_repeat || unset(SHORTREPEAT)) {
9028 zerr("parse error: short loop form requires SHORTLOOPS option");
9029 return None;
9030 }
9031 // c:Src/parse.c:1604 / :1474 / :1551 — short form calls
9032 // par_save_list1 → par_list1 → par_sublist, which parses
9033 // ONE sublist and leaves the trailing SEPER untouched for
9034 // the outer par_list to consume. zshrs previously routed
9035 // through par_list() which consumes the trailing `;`/`\n`
9036 // separator — that swallowed the separator between the
9037 // loop's body command and the next outer command, so
9038 // `repeat 2 print x; print y` parsed as repeat-then-eof
9039 // and par_cmd's post-compound STRING_LEX guard at parse.rs
9040 // line 1170 fired "parse error near `print'". Bug #593.
9041 par_list1().map(|sublist| ZshProgram {
9042 lists: vec![ZshList {
9043 sublist,
9044 flags: ListFlags::default(),
9045 }],
9046 })
9047 }
9048}
9049
9050/// `() { body } arg1 arg2 …` — anonymous function. Defines a fresh
9051/// function named `_zshrs_anon_N`, invokes it with the args, and the
9052/// body runs with positional params set. Implemented as the desugared
9053/// pair (FuncDef + Simple call) so the compile path doesn't need new
9054/// machinery.
9055/// Parse an anonymous function definition `() { BODY }` followed
9056/// by call args. zsh treats `() { echo hi; } a b c` as defining
9057/// and immediately calling an anon fn with args a/b/c. C
9058/// equivalent: the INOUTPAR shape in par_simple at parse.c:1836+
9059/// triggers an anon-funcdef path.
9060fn parse_anon_funcdef() -> Option<ZshCommand> {
9061 zshlex(); // skip ()
9062 skip_separators();
9063 // No `{` after `()` → bare empty subshell shape `()`. Fall back
9064 // to a Subsh with an empty program so the status is 0 (matches
9065 // zsh's `()` no-op behavior).
9066 if tok() != INBRACE_TOK {
9067 return Some(ZshCommand::Subsh(Box::new(ZshProgram {
9068 lists: Vec::new(),
9069 })));
9070 }
9071 zshlex(); // skip {
9072 // c:Src/parse.c:par_subsh — anon `() { … }` body must terminate at
9073 // OUTBRACE_TOK. Pass it as the explicit end-token so the inner
9074 // parse stops cleanly at `}` rather than hitting the top-level
9075 // stray-`}` arm (#168). Bug #167 family.
9076 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9077 // c:Src/parse.c:1733-1737 — same `if (tok != OUTBRACE) YYERRORV`
9078 // gate as the named-funcdef path. Bug #405 sibling.
9079 if tok() != OUTBRACE_TOK {
9080 zerr("parse error: expected `}'");
9081 return None;
9082 }
9083 zshlex();
9084 // Collect trailing args AND redirections, interleaved in any order,
9085 // until a separator. zsh's anon-fn form `() { body } a b c` runs
9086 // body with $1=a, $2=b, $3=c; redirs apply to that invocation:
9087 // `() { read v; print $1 $v } <input1 Shirley >output1 dude`
9088 // (c:Src/parse.c par_simple — the anon function is a simple command
9089 // whose words and redirs follow the body in any order). The previous
9090 // port collected ONLY leading STRING args here and left redirs to
9091 // par_cmd's trailing loop, so an arg AFTER a redir (`<in arg`) was
9092 // never reached and hit par_cmd's "parse error near `<arg>'" gate.
9093 let mut args = Vec::new();
9094 let mut redirs: Vec<ZshRedir> = Vec::new();
9095 loop {
9096 let t = tok();
9097 if t == STRING_LEX {
9098 if let Some(s) = tokstr() {
9099 args.push(s);
9100 }
9101 zshlex();
9102 } else if IS_REDIROP(t) {
9103 match par_redir() {
9104 Some(r) => redirs.push(r),
9105 None => break,
9106 }
9107 } else {
9108 break;
9109 }
9110 }
9111
9112 // Generate a unique name. Module-level static would be cleaner but
9113 // a thread-local atomic is enough — anonymous functions are
9114 // ephemeral and the name isn't user-visible.
9115 static ANON_COUNTER: AtomicUsize = AtomicUsize::new(0);
9116 let n = ANON_COUNTER.fetch_add(1, Ordering::Relaxed);
9117 let name = format!("_zshrs_anon_{}", n);
9118 let funcdef = ZshCommand::FuncDef(ZshFuncDef {
9119 names: vec![name],
9120 body: Box::new(body),
9121 tracing: false,
9122 auto_call_args: Some(args),
9123 body_source: None,
9124 });
9125 // Wrap in Redirected so the redirs bracket the single invocation
9126 // (compile_zsh.rs:929 wraps the body in a WithRedirectsBegin/End
9127 // scope for Redirected(FuncDef, …)). par_cmd's trailing-redir loop
9128 // then finds none and returns this node unwrapped.
9129 if redirs.is_empty() {
9130 Some(funcdef)
9131 } else {
9132 Some(ZshCommand::Redirected(Box::new(funcdef), redirs))
9133 }
9134}
9135
9136/// Parse {...} cursh
9137/// Parse a current-shell brace block `{ BODY }`. C source
9138/// par_cmd at parse.c:958-1085 handles Inbrace → emit WC_CURSH
9139/// and recurses into the list. zshrs's parse_cursh extracts that
9140/// arm into a dedicated method.
9141fn parse_cursh() -> Option<ZshCommand> {
9142 zshlex(); // skip {
9143 // c:Src/parse.c:par_subsh — pass OUTBRACE_TOK as the explicit
9144 // body terminator so the inner parse stops cleanly at `}` rather
9145 // than falling through the top-level `OUTBRACE_TOK if
9146 // end_tokens.is_none()` arm (which errors on stray `}` per bug
9147 // #168). Bug #167 in docs/BUGS.md.
9148 let prog = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9149
9150 // c:Src/parse.c:par_subsh — `{ … }` requires a matching `}`.
9151 // C errors via YYERRORV when the body parse returns without
9152 // seeing OUTBRACE_TOK (parse.c:1623 inbrack check). zshrs's
9153 // previous behavior silently returned `Cursh(prog)` and ran the
9154 // body as if the braces were absent. Bug #167 in docs/BUGS.md.
9155 if tok() != OUTBRACE_TOK {
9156 // Reuse the "parse error near `<tok>'" shape from #142/#161.
9157 // The offending token is whatever follows the unclosed brace
9158 // body. For EOF (`{ echo a` at end of input) C zsh errors
9159 // near the LAST consumed body token; we use the current
9160 // tokstr() or fall back to a "}" hint.
9161 let near = tokstr().unwrap_or_else(|| "}".to_string());
9162 zerr(&format!("parse error near `{}'", near));
9163 return None;
9164 }
9165 // Check for { ... } always { ... }. Direct port of zsh's
9166 // par_subsh at parse.c:1612-1660 — note the two `incmdpos = 1`
9167 // forces (parse.c:1632, 1637): after consuming the closing
9168 // Outbrace AND after matching the `always` keyword, the parser
9169 // explicitly resets command position so the next `{` lexes as
9170 // Inbrace. Without these resets the lexer's String-clears-cmdpos
9171 // rule (lex.rs:976-983) leaves the second `{` in word position,
9172 // turning `always { ... }` into a Simple `{` `echo` … and the
9173 // try/always pairing is silently lost.
9174 {
9175 set_incmdpos(true); // parse.c:1632 incmdpos = !zsh_construct
9176 zshlex();
9177
9178 // Check for 'always'
9179 if tok() == STRING_LEX {
9180 let s = tokstr();
9181 if s.map(|s| s == "always").unwrap_or(false) {
9182 set_incmdpos(true); // parse.c:1637 incmdpos = 1
9183 zshlex();
9184 skip_separators();
9185
9186 if tok() == INBRACE_TOK {
9187 zshlex();
9188 // c:Src/parse.c — always-clause body terminates at
9189 // OUTBRACE_TOK. Bug #167/#168 family.
9190 let always = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9191 if tok() == OUTBRACE_TOK {
9192 zshlex();
9193 }
9194 return Some(ZshCommand::Try(ZshTry {
9195 try_block: Box::new(prog),
9196 always: Box::new(always),
9197 }));
9198 }
9199 }
9200 }
9201 }
9202
9203 Some(ZshCommand::Cursh(Box::new(prog)))
9204}
9205
9206/// Parse inline function definition: name() { ... }
9207/// Parse the inline form `NAME () { BODY }` (POSIX-style funcdef
9208/// without the `function` keyword). The name has already been
9209/// consumed and pushed by par_simple before this method fires.
9210/// C source: handled inline in par_simple's INOUTPAR-after-name
9211/// arm (parse.c:1836-2228).
9212fn parse_inline_funcdef(names: Vec<String>) -> Option<ZshCommand> {
9213 // par_simple's STRING loop left `incmdpos = 0`; the funcdef body
9214 // `{ ... }` requires `incmdpos = 1` so the lexer recognises `{`
9215 // as INBRACE_TOK (current-shell block opener) instead of a
9216 // literal `{` STRING. Without this, `myfunc() { echo body }`
9217 // parsed the body as the single STRING `"{"`, then `echo body`
9218 // fell out at top level. Mirrors the C path where par_cmd's
9219 // dispatcher (parse.c:958) is called with `incmdpos = 1` for
9220 // the funcdef body.
9221 set_incmdpos(true);
9222 // Skip ()
9223 if tok() == INOUTPAR {
9224 zshlex();
9225 }
9226
9227 skip_separators();
9228
9229 // Parse body
9230 if tok() == INBRACE_TOK {
9231 // Same body_start-before-zshlex fix as par_funcdef.
9232 let body_start = pos();
9233 zshlex();
9234 // c:Src/parse.c — inline funcdef body terminates at OUTBRACE_TOK.
9235 // Explicit end-token keeps the inner parse from hitting the
9236 // top-level stray-`}` arm (#168). Bug #167 family.
9237 let body = parse_program_until(Some(&[OUTBRACE_TOK]), false);
9238 // c:Src/parse.c:1733-1737 — `if (tok != OUTBRACE) { cmdpop();
9239 // lineno += oldlineno; ecnpats = onp; ecssub = oecssub;
9240 // YYERRORV(oecused); }`. Without this gate, `f() { echo hi`
9241 // silently registered as a complete fn with body `echo hi`.
9242 // Bug #405.
9243 if tok() != OUTBRACE_TOK {
9244 zerr("parse error: expected `}'");
9245 return None;
9246 }
9247 let body_end = pos().saturating_sub(1);
9248 let body_source = input_slice(body_start, body_end)
9249 .map(|s| {
9250 // Lexer's pos() may have advanced past `}` AND skipped
9251 // trailing whitespace/newlines before returning the
9252 // OUTBRACE_TOK to us, so the slice up to `pos - 1`
9253 // includes the `}` and any preceding whitespace.
9254 // Strip the trailing `}` and any preceding structural
9255 // separator (`;`, `\n`) — C zsh's getpermtext walks
9256 // the wordcode list and emits each command WITHOUT
9257 // the trailing `;`/`\n` that lives in the input.
9258 let t = s.trim();
9259 let t = t.strip_suffix('}').unwrap_or(t).trim_end();
9260 let t = t
9261 .trim_end_matches(|c: char| c == ';' || c == '\n')
9262 .trim_end();
9263 t.to_string()
9264 })
9265 .filter(|s| !s.is_empty());
9266 zshlex();
9267 Some(ZshCommand::FuncDef(ZshFuncDef {
9268 names,
9269 body: Box::new(body),
9270 tracing: false,
9271 auto_call_args: None,
9272 body_source,
9273 }))
9274 } else if unset(SHORTLOOPS) {
9275 // c:1742 — `else if (unset(SHORTLOOPS)) YYERRORV(oecused);` —
9276 // funcdef short body (`name() cmd` without `{...}`) only
9277 // accepted when SHORTLOOPS is set. parse_init seeds
9278 // SHORTLOOPS=on so this fires only when a script
9279 // explicitly disabled the option.
9280 zerr("parse error: short function body form requires SHORTLOOPS option");
9281 None
9282 } else {
9283 match par_cmd() {
9284 Some(cmd) => {
9285 let list = ZshList {
9286 sublist: ZshSublist {
9287 pipe: ZshPipe {
9288 cmd,
9289 next: None,
9290 lineno: lineno(),
9291 merge_stderr: false,
9292 },
9293 next: None,
9294 flags: SublistFlags::default(),
9295 },
9296 flags: ListFlags::default(),
9297 };
9298 Some(ZshCommand::FuncDef(ZshFuncDef {
9299 names,
9300 body: Box::new(ZshProgram { lists: vec![list] }),
9301 tracing: false,
9302 auto_call_args: None,
9303 body_source: None,
9304 }))
9305 }
9306 None => None,
9307 }
9308 }
9309}
9310
9311/// Parse conditional expression
9312/// Top of `[[ ]]` cond-expression parsing — entry to recursive
9313/// descent (or → and → not → primary). Direct port of zsh's
9314/// par_cond_1 at parse.c:2434-2475.
9315fn parse_cond_expr() -> Option<ZshCond> {
9316 parse_cond_or()
9317}
9318
9319/// Cond-expression `||` level. C: inside par_cond_1 at
9320/// parse.c:2434-2475 (the `cond_or` ladder).
9321fn parse_cond_or() -> Option<ZshCond> {
9322 let left = parse_cond_and()?;
9323 skip_cond_separators();
9324
9325 if tok() == DBAR {
9326 zshlex();
9327 skip_cond_separators();
9328 parse_cond_or().map(|right| ZshCond::Or(Box::new(left), Box::new(right)))
9329 } else {
9330 Some(left)
9331 }
9332}
9333
9334/// Cond-expression `&&` level. C: par_cond_2 at parse.c:2476-2625.
9335fn parse_cond_and() -> Option<ZshCond> {
9336 let left = parse_cond_not()?;
9337 skip_cond_separators();
9338
9339 if tok() == DAMPER {
9340 zshlex();
9341 skip_cond_separators();
9342 parse_cond_and().map(|right| ZshCond::And(Box::new(left), Box::new(right)))
9343 } else {
9344 Some(left)
9345 }
9346}
9347
9348/// `static FuncDump dumps;` from `Src/parse.c:3652` — head of the
9349/// loaded-`.zwc` linked list. C walks `dumps`/`p->next` directly;
9350/// the Rust port uses a `Mutex<Vec<funcdump>>` indexed by filename
9351/// so refcount ops can find an entry without raw-pointer compare.
9352pub static DUMPS: std::sync::Mutex<Vec<funcdump>> = std::sync::Mutex::new(Vec::new());
9353
9354/// Cond-expression `!` negation level. C: handled inside
9355/// par_cond_2 at parse.c:2476-2625 via the Bang token check.
9356fn parse_cond_not() -> Option<ZshCond> {
9357 skip_cond_separators();
9358
9359 // ! can be either BANG_TOK or String "!"
9360 let is_not =
9361 tok() == BANG_TOK || (tok() == STRING_LEX && tokstr().map(|s| s == "!").unwrap_or(false));
9362 if is_not {
9363 zshlex();
9364 let inner = parse_cond_not()?;
9365 return Some(ZshCond::Not(Box::new(inner)));
9366 }
9367
9368 if tok() == INPAR_TOK {
9369 zshlex();
9370 skip_cond_separators();
9371 // c:Src/parse.c:2534-2547 par_cond_2 INPAR branch — empty
9372 // body `[[ ( ) ]]` makes the inner par_cond's recursive
9373 // par_cond_2 see OUTPAR with no leading STRING/BANG/INPAR
9374 // and YYERROR immediately. Mirror that here: if the very
9375 // next token after `(` (post separator skip) is `)`, emit
9376 // a parse error so the script aborts cleanly instead of
9377 // silently swallowing every following command. Bug #538.
9378 if tok() == OUTPAR_TOK {
9379 crate::ported::utils::zerr("condition expected");
9380 yyerror(0);
9381 return None;
9382 }
9383 let inner = parse_cond_expr()?;
9384 skip_cond_separators();
9385 if tok() == OUTPAR_TOK {
9386 zshlex();
9387 }
9388 return Some(inner);
9389 }
9390
9391 parse_cond_primary()
9392}
9393
9394/// Cond-expression primary: unary tests (-f, -d, ...), binary
9395/// tests (=, !=, <, >, ==, =~, -eq, -ne, ...), and parenthesized
9396/// sub-expressions. Direct port of par_cond_double / par_cond_triple
9397/// / par_cond_multi at parse.c:2626-2731 (chosen by arg count).
9398fn parse_cond_primary() -> Option<ZshCond> {
9399 let s1 = match tok() {
9400 STRING_LEX => {
9401 let s = tokstr().unwrap_or_default();
9402 zshlex();
9403 s
9404 }
9405 _ => return None,
9406 };
9407
9408 skip_cond_separators();
9409
9410 // Check for unary operator. zsh's lexer tokenizes leading `-` as
9411 // `zsh_h::Dash` (`\u{9b}`, `Src/zsh.h:182`) inside gettokstr (lex.c:1390-1400
9412 // LX2_DASH — `-` always becomes Dash, untokenized later). Match
9413 // either form here, and use char-count not byte-count since Dash
9414 // is 2 UTF-8 bytes (`\xc2\x9b`).
9415 //
9416 // c:Src/parse.c par_cond — when the leading token is `-` followed
9417 // ENTIRELY by digits (`-5`, `-123`), it's a numeric literal
9418 // operand, not a unary test flag. zsh's parser checks the C
9419 // `isdigit` of the trailing chars to disambiguate; without the
9420 // check, `[[ -5 -lt -3 ]]` reads `-5` as a one-arg test flag,
9421 // then `-lt` as the operand, then `-3` as a leftover token —
9422 // emitting "unknown condition: -5" and falling through to a
9423 // command-not-found dispatch on `-3`. Bug #121 in docs/BUGS.md.
9424 let s1_chars: Vec<char> = s1.chars().collect();
9425 let is_negative_number = s1_chars.len() >= 2
9426 && IS_DASH(s1_chars[0])
9427 && s1_chars[1..].iter().all(|c| c.is_ascii_digit());
9428 if s1_chars.len() == 2 && IS_DASH(s1_chars[0]) && !is_negative_number {
9429 let s2 = match tok() {
9430 STRING_LEX => {
9431 let s = tokstr().unwrap_or_default();
9432 zshlex();
9433 s
9434 }
9435 _ => {
9436 // c:Src/parse.c par_cond_2 — when the leading `-X`
9437 // is a 2-char dash form, zsh ALWAYS treats it as a
9438 // unary test op (the operand-missing case errors
9439 // immediately with `unknown condition: -X`). Don't
9440 // fall back to `Unary("-n", "-X")` — that path
9441 // silently let `[[ -z ]]` evaluate as
9442 // `[[ -n "-z" ]]` → true. Bug #480/#481.
9443 //
9444 // Convert Dash (\u{9b}) back to ASCII `-` for the
9445 // user-visible diagnostic so it reads "unknown
9446 // condition: -z" not "unknown condition: <Dash>z".
9447 let display: String = s1.chars().map(|c| {
9448 if IS_DASH(c) { '-' } else { c }
9449 }).collect();
9450 crate::ported::utils::zerr(&format!(
9451 "unknown condition: {}",
9452 display
9453 ));
9454 return None;
9455 }
9456 };
9457 return Some(ZshCond::Unary(s1, s2));
9458 }
9459
9460 // Check for binary operator. Direct port of zsh/Src/parse.c:2601-2603:
9461 // incond++; /* parentheses do globbing */
9462 // do condlex(); while (COND_SEP());
9463 // incond--; /* parentheses do grouping */
9464 // The bump makes the lexer treat `(` as a literal character inside
9465 // the RHS word (e.g. `[[ x =~ (foo) ]]`) instead of returning Inpar
9466 // and splitting the regex into multiple tokens.
9467 let op = match tok() {
9468 STRING_LEX => {
9469 let s = tokstr().unwrap_or_default();
9470 set_incond(incond() + 1);
9471 zshlex();
9472 set_incond(incond() - 1);
9473 s
9474 }
9475 INANG_TOK => {
9476 set_incond(incond() + 1);
9477 zshlex();
9478 set_incond(incond() - 1);
9479 "<".to_string()
9480 }
9481 OUTANG_TOK => {
9482 set_incond(incond() + 1);
9483 zshlex();
9484 set_incond(incond() - 1);
9485 ">".to_string()
9486 }
9487 _ => return Some(ZshCond::Unary("-n".to_string(), s1)),
9488 };
9489
9490 skip_cond_separators();
9491
9492 // c:Src/parse.c:2601-2625 par_cond_2 — only the documented binary
9493 // operators are accepted inside `[[ ... ]]`. zsh rejects ksh/bash
9494 // forms `-a` (logical AND) and `-o` (logical OR) with a parse
9495 // error ("condition expected") because they're not in the
9496 // par_cond_2 binary-op set — zsh uses `&&` / `||` instead.
9497 // Verified: `zsh -fc '[[ "" -a "x" ]]'` → exit 1, "parse error:
9498 // condition expected: ...". Without this gate, zshrs silently
9499 // built ZshCond::Binary("", "-a", "x") and ran an unknown-op
9500 // path that always evaluated false.
9501 // c:Src/parse.c:2601-2625 par_cond_2 — `-a` / `-o` n-ary chain
9502 // operators are not valid binary operators inside `[[ ... ]]`
9503 // (zsh uses `&&` / `||` instead). Match both the ASCII `-a`/
9504 // `-o` form and the tokenized `Dash+a`/`Dash+o` form that the
9505 // lexer emits inside cond bodies (Dash = \u{9b}, Src/zsh.h:182).
9506 let op_chars: Vec<char> = op.chars().collect();
9507 let is_dash_a_or_o =
9508 op_chars.len() == 2 && IS_DASH(op_chars[0]) && (op_chars[1] == 'a' || op_chars[1] == 'o');
9509 if is_dash_a_or_o {
9510 crate::ported::utils::zerr(&format!("parse error: condition expected: {}", s1));
9511 crate::ported::utils::errflag.fetch_or(
9512 crate::ported::zsh_h::ERRFLAG_ERROR,
9513 std::sync::atomic::Ordering::Relaxed,
9514 );
9515 set_tok(LEXERR);
9516 return None;
9517 }
9518
9519 let s2 = match tok() {
9520 STRING_LEX => {
9521 let s = tokstr().unwrap_or_default();
9522 zshlex();
9523 s
9524 }
9525 _ => {
9526 // c:Src/parse.c par_cond_2 — when a binary op is
9527 // recognized but the RHS operand is missing, zsh emits
9528 // `parse error: condition expected: <LHS>` at par_cond_2's
9529 // missing-rhs branch. zshrs's previous fallback returned
9530 // `Binary(s1, op, "")` which silently evaluated as if the
9531 // RHS were empty string → rc=1. Bug #482.
9532 //
9533 // Convert Dash (\u{9b}) back to ASCII `-` in the LHS
9534 // display so the diagnostic reads cleanly.
9535 let display: String = s1.chars().map(|c| {
9536 if IS_DASH(c) { '-' } else { c }
9537 }).collect();
9538 crate::ported::utils::zerr(&format!(
9539 "parse error: condition expected: {}",
9540 display
9541 ));
9542 crate::ported::utils::errflag.fetch_or(
9543 crate::ported::zsh_h::ERRFLAG_ERROR,
9544 std::sync::atomic::Ordering::Relaxed,
9545 );
9546 set_tok(LEXERR);
9547 return None;
9548 }
9549 };
9550
9551 // c:Src/parse.c:2685-2691 par_cond_triple —
9552 // `(b[0] == Equals || b[0] == '=') && (b[1] == '~' || b[1] == Tilde)
9553 // && !b[2]` → COND_REGEX.
9554 // The lexer emits the TOKEN forms inside cond bodies (`=` at word
9555 // start → Equals `\u{8d}`, `~` → Tilde `\u{98}`), so an ASCII-only
9556 // `op == "=~"` check missed every real `[[ x =~ pat ]]` and fell
9557 // through to Binary.
9558 let opc: Vec<char> = op.chars().collect();
9559 let is_regex_op = opc.len() == 2
9560 && (opc[0] == '=' || opc[0] == Equals)
9561 && (opc[1] == '~' || opc[1] == Tilde);
9562 // c:2659-2710 par_cond_triple — only the documented binary operators are
9563 // valid. String ops: `=`/`==`/`!=`/`<`/`>` (and their tokenized forms
9564 // Equals/Bang/Inang/Outang). Dash ops (`-eq`, `-nt`, `-pcre-match`, any
9565 // `-X`) parse-accept and the evaluator reports "unknown condition" if
9566 // unsupported. A `-mod A B C` (s1 is a dash op) is the COND_MOD form. Any
9567 // other middle word is `COND_ERROR("condition expected: %s", op)` — a parse
9568 // error that YYERRORs (errflag + LEXERR) so the line aborts. Without this
9569 // gate `[[ a b c ]]` silently built Binary("a","b","c") and ran on.
9570 let is_eq = |c: char| c == '=' || c == Equals;
9571 let is_bang = |c: char| c == '!' || c == Bang;
9572 let is_recognized_op = (opc.len() == 1
9573 && (is_eq(opc[0]) || matches!(opc[0], '<' | '>') || opc[0] == Inang || opc[0] == Outang))
9574 || (opc.len() == 2 && is_eq(opc[0]) && is_eq(opc[1]))
9575 || (opc.len() == 2 && is_bang(opc[0]) && is_eq(opc[1]))
9576 || (!opc.is_empty() && IS_DASH(opc[0]))
9577 || (!s1_chars.is_empty() && IS_DASH(s1_chars[0]));
9578 if is_regex_op {
9579 Some(ZshCond::Regex(s1, s2))
9580 } else if is_recognized_op {
9581 Some(ZshCond::Binary(s1, op, s2))
9582 } else {
9583 let display: String = op.chars().map(|c| if IS_DASH(c) { '-' } else { c }).collect();
9584 crate::ported::utils::zerr(&format!("condition expected: {}", display));
9585 crate::ported::utils::errflag.fetch_or(
9586 crate::ported::zsh_h::ERRFLAG_ERROR,
9587 std::sync::atomic::Ordering::Relaxed,
9588 );
9589 set_tok(LEXERR);
9590 None
9591 }
9592}
9593
9594fn skip_cond_separators() {
9595 while tok() == SEPER && {
9596 let s = tokstr();
9597 s.map(|s| !s.contains(';')).unwrap_or(true)
9598 } {
9599 zshlex();
9600 }
9601}
9602
9603/// Parse (( ... )) arithmetic command
9604/// Parse `(( EXPR ))` arithmetic command. C source: parse.c:1810-1834
9605/// `par_dinbrack` (despite the name; the function actually handles
9606/// DINPAR `(( ))` blocks too).
9607fn parse_arith() -> Option<ZshCommand> {
9608 let expr = tokstr().unwrap_or_default();
9609 zshlex();
9610 Some(ZshCommand::Arith(expr))
9611}
9612
9613/// Skip separator tokens
9614fn skip_separators() {
9615 while tok() == SEPER || tok() == NEWLIN {
9616 zshlex();
9617 }
9618}
9619
9620// `fdheaderlen` / `fdmagic` / `fdflags` / etc. macros from
9621// `Src/parse.c:3125-3152`. C uses raw pointer arithmetic on a
9622// `Wordcode` (= `u32 *`); the Rust port takes a slice and indexes.
9623
9624/// Port of `fdheaderlen(f)` macro (`Src/parse.c:3125`) — header
9625/// length in u32 words (read from prelude word `FD_PRELEN`).
9626#[inline]
9627pub fn fdheaderlen(f: &[u32]) -> u32 {
9628 f[FD_PRELEN]
9629}
9630
9631/// Port of `fdmagic(f)` macro (`Src/parse.c:3127`) — first prelude
9632/// word, either `FD_MAGIC` or `FD_OMAGIC`.
9633#[inline]
9634pub fn fdmagic(f: &[u32]) -> u32 {
9635 f[0]
9636}
9637
9638/// Port of `fdflags(f)` macro (`Src/parse.c:3131`) — low byte of
9639/// the packed `pre[1]` word.
9640#[inline]
9641pub fn fdflags(f: &[u32]) -> u32 {
9642 // `pre[1]` is a u32 viewed as 4 bytes; flags = byte 0.
9643 f[1] & 0xff
9644}
9645
9646/// Port of `fdsetflags(f, v)` macro (`Src/parse.c:3132`) — write
9647/// the low byte of `pre[1]`.
9648#[inline]
9649pub fn fdsetflags(f: &mut [u32], v: u8) {
9650 f[1] = (f[1] & !0xff) | (v as u32);
9651}
9652
9653/// Port of `fdother(f)` macro (`Src/parse.c:3133`) — high 24 bits
9654/// of `pre[1]`, holds the byte-offset to the opposite-byte-order
9655/// dump copy.
9656#[inline]
9657pub fn fdother(f: &[u32]) -> u32 {
9658 (f[1] >> 8) & 0x00ff_ffff
9659}
9660
9661/// Port of `fdsetother(f, o)` macro (`Src/parse.c:3134`).
9662#[inline]
9663pub fn fdsetother(f: &mut [u32], o: u32) {
9664 f[1] = (f[1] & 0xff) | ((o & 0x00ff_ffff) << 8);
9665}
9666
9667/// Port of `fdversion(f)` macro (`Src/parse.c:3140`) — read the
9668/// `ZSH_VERSION` C-string from `pre[2..]`.
9669pub fn fdversion(f: &[u32]) -> String {
9670 let bytes: Vec<u8> = f[2..]
9671 .iter()
9672 .take(10)
9673 .flat_map(|w| w.to_le_bytes().into_iter())
9674 .collect();
9675 let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
9676 String::from_utf8_lossy(&bytes[..end]).into_owned()
9677}
9678
9679/// Port of `firstfdhead(f)` macro (`Src/parse.c:3142`) — pointer
9680/// to the first `struct fdhead` past the prelude.
9681#[inline]
9682pub fn firstfdhead_offset() -> usize {
9683 FD_PRELEN
9684}
9685
9686/// Port of `nextfdhead(f)` macro (`Src/parse.c:3143`) — advance to
9687/// the next header by reading the current `hlen` slot.
9688#[inline]
9689pub fn nextfdhead_offset(f: &[u32], cur: usize) -> usize {
9690 cur + (f[cur + 4] as usize) // .hlen is field 4 of fdhead
9691}
9692
9693/// Port of `fdhflags(f)` macro (`Src/parse.c:3145`) — low 2 bits
9694/// of the header's `flags` field (the kshload/zshload marker).
9695#[inline]
9696pub fn fdhflags(h: &fdhead) -> u32 {
9697 h.flags & 0x3
9698}
9699
9700/// Port of `fdhtail(f)` macro (`Src/parse.c:3146`) — high 30 bits
9701/// of `flags`, byte offset from the name start to its basename.
9702#[inline]
9703pub fn fdhtail(h: &fdhead) -> u32 {
9704 h.flags >> 2
9705}
9706
9707/// Port of `fdhbldflags(f, t)` macro (`Src/parse.c:3147`) — pack
9708/// `(flags, tail)` into one u32 (low 2 bits = flags, high 30 = tail).
9709#[inline]
9710pub fn fdhbldflags(flags: u32, tail: u32) -> u32 {
9711 flags | (tail << 2)
9712}
9713
9714/// Port of `fdname(f)` macro (`Src/parse.c:3152`) — name string
9715/// follows the fdhead record immediately. Reads bytes from the
9716/// dump buffer until NUL.
9717pub fn fdname(buf: &[u32], header_offset: usize) -> String {
9718 let name_word_off = header_offset + FDHEAD_WORDS;
9719 let bytes: Vec<u8> = buf[name_word_off..]
9720 .iter()
9721 .flat_map(|w| w.to_le_bytes().into_iter())
9722 .take_while(|&b| b != 0)
9723 .collect();
9724 String::from_utf8_lossy(&bytes).into_owned()
9725}
9726
9727/// Decode a `fdhead` record at the given u32-word offset in the
9728/// dump buffer. Used by the header-walk loops in `bin_zcompile -t`.
9729pub fn read_fdhead(buf: &[u32], offset: usize) -> Option<fdhead> {
9730 if offset + FDHEAD_WORDS > buf.len() {
9731 return None;
9732 }
9733 Some(fdhead {
9734 start: buf[offset],
9735 len: buf[offset + 1],
9736 npats: buf[offset + 2],
9737 strs: buf[offset + 3],
9738 hlen: buf[offset + 4],
9739 flags: buf[offset + 5],
9740 })
9741}
9742
9743/// Port of `freedump(FuncDump f)` from `Src/parse.c:3976`. C
9744/// `munmap`s, `zclose`s the fd, and frees the struct. The Rust
9745/// port relies on Drop for the `funcdump` (no mmap held in this
9746/// port — `addr`/`map` are byte-offset placeholders), so the
9747/// equivalent is removing the entry from the dumps list. Called
9748/// by `decrdumpcount` when the refcount hits zero (c:3988) and
9749/// by `closedumps` when shutting down (c:4008).
9750fn freedump_locked(g: &mut std::sync::MutexGuard<'_, Vec<funcdump>>, filename: &str) {
9751 // c:3976
9752 g.retain(|d| d.filename.as_deref() != Some(filename));
9753}
9754
9755// =====================================================================
9756// Remaining `Src/parse.c` ports (this section finishes the file).
9757//
9758// Several of these emit into the C-wordcode buffer (`ECBUF`/etc.) and
9759// are kept for completeness — the live zshrs runtime uses the
9760// `ZshProgram` AST path instead, but `bin_zcompile` (`-c`/`-a` modes)
9761// and any future `.zwc`-emit pipeline both call into these.
9762// =====================================================================
9763
9764/// `ecstr(s)` helper — `ecadd(ecstrcode(s))`. Mirrors the C macro at
9765/// `Src/parse.c:482` used everywhere by the par_* emitters.
9766#[inline]
9767pub fn ecstr(s: &str) {
9768 let code = ecstrcode(s);
9769 ecadd(code);
9770}
9771
9772/// Port of `condlex` function-pointer global from `Src/parse.c`. C
9773/// flips this between `zshlex` and `testlex` depending on whether
9774/// we're inside `[[ ]]` vs `/bin/test` builtin. zshrs has no
9775/// separate `testlex` yet, so this just defers to `zshlex`.
9776#[inline]
9777pub fn condlex() {
9778 zshlex();
9779}
9780
9781fn copy_ecstr_walk(node: &Option<Box<EccstrNode>>, p: &mut [u8]) {
9782 let mut cur = node.as_ref();
9783 while let Some(n) = cur {
9784 // c:540 — `memcpy(p + s->aoffs, s->str, strlen(s->str) + 1);`
9785 let off = n.aoffs as usize;
9786 let need = off + n.str.len() + 1;
9787 if need <= p.len() {
9788 p[off..off + n.str.len()].copy_from_slice(&n.str);
9789 p[off + n.str.len()] = 0;
9790 }
9791 // c:541 — `copy_ecstr(s->left, p);`
9792 copy_ecstr_walk(&n.left, p);
9793 // c:542 — `s = s->right;`
9794 cur = n.right.as_ref();
9795 }
9796}
9797
9798/// Port of `par_cond(void)` from `Src/parse.c:2409`. Top-level cond
9799/// OR-chain — drives `par_cond_1` and stitches `||`-separated terms
9800/// with `WCB_COND(COND_OR, …)`. This is the missing top of the
9801/// wordcode cond chain: `par_cond_wordcode` (the par_dinbrack port)
9802/// must call into HERE so that `[[ a || b ]]` and friends land
9803/// real WC_COND opcodes in `ecbuf`. Without this, the wordcode
9804/// emitter for `[[ ... ]]` produced zero words and parity dropped
9805/// 148 words on `/etc/zshrc` alone.
9806pub fn par_cond_top() -> i32 {
9807 // c:2411 — `int p = ecused, r;`
9808 let p = ECUSED.with(|c| c.get()) as usize;
9809 let r = par_cond_1();
9810 while COND_SEP() {
9811 condlex();
9812 }
9813 if tok() == DBAR {
9814 // c:2417 — `condlex(); while (COND_SEP()) condlex();`
9815 condlex();
9816 while COND_SEP() {
9817 condlex();
9818 }
9819 // c:2420-2422 — `ecispace(p, 1); par_cond(); ecbuf[p] =
9820 // WCB_COND(COND_OR, ecused-1-p);`
9821 ecispace(p, 1);
9822 par_cond_top();
9823 let ecused = ECUSED.with(|c| c.get()) as usize;
9824 ECBUF.with(|c| {
9825 c.borrow_mut()[p] = WCB_COND(COND_OR as u32, (ecused - 1 - p) as u32);
9826 });
9827 return 1;
9828 }
9829 r
9830}
9831
9832/// Port of `static int check_cond(const char *input, const char *cond)`
9833/// from `Src/parse.c:2459`. True iff `input` is the two-char `-X`
9834/// form whose `X` matches `cond` — used by par_cond_2 to detect
9835/// `-a` / `-o` n-ary chain operators and by build_dump for `-k` /
9836/// `-z`. C: `return !IS_DASH(input[0]) ? 0 : !strcmp(input+1, cond);`.
9837fn check_cond(input: &str, cond: &str) -> bool {
9838 let mut chars = input.chars();
9839 match chars.next() {
9840 Some(c) if IS_DASH(c) => chars.as_str() == cond,
9841 _ => false,
9842 }
9843}
9844
9845#[cfg(test)]
9846mod tests {
9847 use super::*;
9848 use crate::utils::{errflag, ERRFLAG_ERROR};
9849 use std::fs;
9850 use std::path::Path;
9851 use std::sync::atomic::Ordering;
9852 use std::sync::mpsc;
9853 use std::thread;
9854 use std::time::Duration;
9855
9856 /// `try_source_file` MUST refuse a stale `.zwc` cache when the
9857 /// uncompiled source has been modified more recently. The C body
9858 /// at c:3819 reads `stc.st_mtime >= stn.st_mtime` — explicitly
9859 /// `>=`, meaning only an equal-or-newer zwc is acceptable.
9860 ///
9861 /// A regression that ignored the mtime check (or used the wrong
9862 /// direction) would silently keep loading the OLD compiled body
9863 /// after the user edited the source file — every `source foo.zsh`
9864 /// would replay yesterday's code, the worst-class shell bug.
9865 ///
9866 /// Pin: create source + .zwc, then touch source to make it
9867 /// newer. try_source_file must return None.
9868 #[test]
9869 fn try_source_file_skips_stale_zwc() {
9870 let _g = crate::test_util::global_state_lock();
9871 let dir = tempfile::tempdir().expect("tempdir");
9872 let src = dir.path().join("script.zsh");
9873 let zwc = dir.path().join("script.zsh.zwc");
9874 // Create zwc FIRST (older), then source (newer).
9875 fs::write(&zwc, b"placeholder zwc").unwrap();
9876 thread::sleep(Duration::from_millis(20));
9877 fs::write(&src, b"echo hi").unwrap();
9878
9879 let result = try_source_file(src.to_str().unwrap());
9880 assert!(
9881 result.is_none(),
9882 "c:3819 — stale .zwc (older than source) MUST be rejected; \
9883 got {:?}",
9884 result
9885 );
9886 }
9887
9888 /// `try_source_file` returns None when no `.zwc` exists for the
9889 /// requested file (c:3819 `if let Ok(meta_c) = &stc` gate fails).
9890 /// This is the common case — most user scripts don't ship with
9891 /// a pre-compiled `.zwc`. The fn returning None lets the caller
9892 /// fall through to the source-read path. A regression that
9893 /// returned `Some(file)` on missing `.zwc` would route every
9894 /// `source foo.zsh` through `check_dump_file` against a
9895 /// non-existent file and crash.
9896 #[test]
9897 fn try_source_file_returns_none_when_no_zwc() {
9898 let _g = crate::test_util::global_state_lock();
9899 let dir = tempfile::tempdir().expect("tempdir");
9900 let src = dir.path().join("plain.zsh");
9901 fs::write(&src, b"echo hi").unwrap();
9902 // No .zwc sibling.
9903
9904 let result = try_source_file(src.to_str().unwrap());
9905 assert!(
9906 result.is_none(),
9907 "c:3819 gate fails when stat(wc) returns Err → None"
9908 );
9909 }
9910
9911 /// Test helper. Mirrors zsh's `errflag` save/clear/check pattern
9912 /// around a parse — see `Src/init.c:loop` which clears errflag
9913 /// before parse_event() and tests it after. Returns `Err` if the
9914 /// parse set `ERRFLAG_ERROR`; otherwise `Ok(program)`.
9915 fn parse(input: &str) -> Result<ZshProgram, String> {
9916 let saved = errflag.load(Ordering::Relaxed);
9917 errflag.fetch_and(!ERRFLAG_ERROR, Ordering::Relaxed);
9918 parse_init(input);
9919 let prog = crate::ported::parse::parse();
9920 let had_err = (errflag.load(Ordering::Relaxed) & ERRFLAG_ERROR) != 0;
9921 // Restore prior error bits; don't carry our new error into the
9922 // outer test runner.
9923 errflag.store(saved, Ordering::Relaxed);
9924 if had_err {
9925 Err("parse error".to_string())
9926 } else {
9927 Ok(prog)
9928 }
9929 }
9930
9931 #[test]
9932 fn test_simple_command() {
9933 let _g = crate::test_util::global_state_lock();
9934 let prog = parse("echo hello world").unwrap();
9935 assert_eq!(prog.lists.len(), 1);
9936 match &prog.lists[0].sublist.pipe.cmd {
9937 ZshCommand::Simple(s) => {
9938 assert_eq!(s.words, vec!["echo", "hello", "world"]);
9939 }
9940 _ => panic!("expected simple command"),
9941 }
9942 }
9943
9944 #[test]
9945 fn test_pipeline() {
9946 let _g = crate::test_util::global_state_lock();
9947 let prog = parse("ls | grep foo | wc -l").unwrap();
9948 assert_eq!(prog.lists.len(), 1);
9949
9950 let pipe = &prog.lists[0].sublist.pipe;
9951 assert!(pipe.next.is_some());
9952
9953 let pipe2 = pipe.next.as_ref().unwrap();
9954 assert!(pipe2.next.is_some());
9955 }
9956
9957 #[test]
9958 fn test_and_or() {
9959 let _g = crate::test_util::global_state_lock();
9960 let prog = parse("cmd1 && cmd2 || cmd3").unwrap();
9961 let sublist = &prog.lists[0].sublist;
9962
9963 assert!(sublist.next.is_some());
9964 let (op, _) = sublist.next.as_ref().unwrap();
9965 assert_eq!(*op, SublistOp::And);
9966 }
9967
9968 #[test]
9969 fn test_if_then() {
9970 let _g = crate::test_util::global_state_lock();
9971 let prog = parse("if test -f foo; then echo yes; fi").unwrap();
9972 match &prog.lists[0].sublist.pipe.cmd {
9973 ZshCommand::If(_) => {}
9974 _ => panic!("expected if command"),
9975 }
9976 }
9977
9978 #[test]
9979 fn test_for_loop() {
9980 let _g = crate::test_util::global_state_lock();
9981 let prog = parse("for i in a b c; do echo $i; done").unwrap();
9982 match &prog.lists[0].sublist.pipe.cmd {
9983 ZshCommand::For(f) => {
9984 assert_eq!(f.var, "i");
9985 match &f.list {
9986 ForList::Words(w) => assert_eq!(w, &vec!["a", "b", "c"]),
9987 _ => panic!("expected word list"),
9988 }
9989 }
9990 _ => panic!("expected for command"),
9991 }
9992 }
9993
9994 #[test]
9995 fn test_case() {
9996 let _g = crate::test_util::global_state_lock();
9997 let prog = parse("case $x in a) echo a;; b) echo b;; esac").unwrap();
9998 match &prog.lists[0].sublist.pipe.cmd {
9999 ZshCommand::Case(c) => {
10000 assert_eq!(c.arms.len(), 2);
10001 }
10002 _ => panic!("expected case command"),
10003 }
10004 }
10005
10006 #[test]
10007 fn test_function() {
10008 let _g = crate::test_util::global_state_lock();
10009 // First test just parsing "function foo" to see what happens
10010 let prog = parse("function foo { }").unwrap();
10011 match &prog.lists[0].sublist.pipe.cmd {
10012 ZshCommand::FuncDef(f) => {
10013 assert_eq!(f.names, vec!["foo"]);
10014 }
10015 _ => panic!(
10016 "expected function, got {:?}",
10017 prog.lists[0].sublist.pipe.cmd
10018 ),
10019 }
10020 }
10021
10022 #[test]
10023 fn test_redirection() {
10024 let _g = crate::test_util::global_state_lock();
10025 let prog = parse("echo hello > file.txt").unwrap();
10026 match &prog.lists[0].sublist.pipe.cmd {
10027 ZshCommand::Simple(s) => {
10028 assert_eq!(s.redirs.len(), 1);
10029 assert_eq!(s.redirs[0].rtype, REDIR_WRITE);
10030 }
10031 _ => panic!("expected simple command"),
10032 }
10033 }
10034
10035 #[test]
10036 fn test_assignment() {
10037 let _g = crate::test_util::global_state_lock();
10038 let prog = parse("FOO=bar echo $FOO").unwrap();
10039 match &prog.lists[0].sublist.pipe.cmd {
10040 ZshCommand::Simple(s) => {
10041 assert_eq!(s.assigns.len(), 1);
10042 assert_eq!(s.assigns[0].name, "FOO");
10043 }
10044 _ => panic!("expected simple command"),
10045 }
10046 }
10047
10048 #[test]
10049 fn test_parse_completion_function() {
10050 let _g = crate::test_util::global_state_lock();
10051 let input = r#"_2to3_fixes() {
10052 local -a fixes
10053 fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
10054 (( ${#fixes} )) && _describe -t fixes 'fix' fixes
10055}"#;
10056 let result = parse(input);
10057 assert!(
10058 result.is_ok(),
10059 "Failed to parse completion function: {:?}",
10060 result.err()
10061 );
10062 let prog = result.unwrap();
10063 assert!(
10064 !prog.lists.is_empty(),
10065 "Expected at least one list in program"
10066 );
10067 }
10068
10069 #[test]
10070 fn test_parse_array_with_complex_elements() {
10071 let _g = crate::test_util::global_state_lock();
10072 let input = r#"arguments=(
10073 '(- * :)'{-h,--help}'[show this help message and exit]'
10074 {-d,--doctests_only}'[fix up doctests only]'
10075 '*:filename:_files'
10076)"#;
10077 let result = parse(input);
10078 assert!(
10079 result.is_ok(),
10080 "Failed to parse array assignment: {:?}",
10081 result.err()
10082 );
10083 }
10084
10085 #[test]
10086 fn test_parse_full_completion_file() {
10087 let _g = crate::test_util::global_state_lock();
10088 let input = r##"#compdef 2to3
10089
10090# zsh completions for '2to3'
10091
10092_2to3_fixes() {
10093 local -a fixes
10094 fixes=( ${${(M)${(f)"$(2to3 --list-fixes 2>/dev/null)"}:#*}//[[:space:]]/} )
10095 (( ${#fixes} )) && _describe -t fixes 'fix' fixes
10096}
10097
10098local -a arguments
10099
10100arguments=(
10101 '(- * :)'{-h,--help}'[show this help message and exit]'
10102 {-d,--doctests_only}'[fix up doctests only]'
10103 {-f,--fix}'[each FIX specifies a transformation; default: all]:fix name:_2to3_fixes'
10104 {-j,--processes}'[run 2to3 concurrently]:number: '
10105 {-x,--nofix}'[prevent a transformation from being run]:fix name:_2to3_fixes'
10106 {-l,--list-fixes}'[list available transformations]'
10107 {-p,--print-function}'[modify the grammar so that print() is a function]'
10108 {-v,--verbose}'[more verbose logging]'
10109 '--no-diffs[do not show diffs of the refactoring]'
10110 {-w,--write}'[write back modified files]'
10111 {-n,--nobackups}'[do not write backups for modified files]'
10112 {-o,--output-dir}'[put output files in this directory instead of overwriting]:directory:_directories'
10113 {-W,--write-unchanged-files}'[also write files even if no changes were required]'
10114 '--add-suffix[append this string to all output filenames]:suffix: '
10115 '*:filename:_files'
10116)
10117
10118_arguments -s -S $arguments
10119"##;
10120 let result = parse(input);
10121 assert!(
10122 result.is_ok(),
10123 "Failed to parse full completion file: {:?}",
10124 result.err()
10125 );
10126 let prog = result.unwrap();
10127 // Should have parsed successfully with at least one statement
10128 assert!(!prog.lists.is_empty(), "Expected at least one list");
10129 }
10130
10131 #[test]
10132 fn test_parse_logs_sh() {
10133 let _g = crate::test_util::global_state_lock();
10134 let input = r#"#!/usr/bin/env bash
10135shopt -s globstar
10136
10137if [[ $(uname) == Darwin ]]; then
10138 tail -f /var/log/**/*.log /var/log/**/*.out | lolcat
10139else
10140 if [[ $ZPWR_DISTRO_NAME == raspbian ]]; then
10141 tail -f /var/log/**/*.log | lolcat
10142 else
10143 printf "Unsupported...\n" >&2
10144 fi
10145fi
10146"#;
10147 let result = parse(input);
10148 assert!(
10149 result.is_ok(),
10150 "Failed to parse logs.sh: {:?}",
10151 result.err()
10152 );
10153 }
10154
10155 #[test]
10156 fn test_parse_case_with_glob() {
10157 let _g = crate::test_util::global_state_lock();
10158 let input = r#"case "$ZPWR_OS_TYPE" in
10159 darwin*) open_cmd='open'
10160 ;;
10161 cygwin*) open_cmd='cygstart'
10162 ;;
10163 linux*)
10164 open_cmd='xdg-open'
10165 ;;
10166esac"#;
10167 let result = parse(input);
10168 assert!(
10169 result.is_ok(),
10170 "Failed to parse case with glob: {:?}",
10171 result.err()
10172 );
10173 }
10174
10175 #[test]
10176 fn test_parse_case_with_nested_if() {
10177 let _g = crate::test_util::global_state_lock();
10178 // Test case with nested if and glob patterns
10179 let input = r##"function zpwrGetOpenCommand(){
10180 local open_cmd
10181 case "$ZPWR_OS_TYPE" in
10182 darwin*) open_cmd='open' ;;
10183 cygwin*) open_cmd='cygstart' ;;
10184 linux*)
10185 if [[ "$_zpwr_uname_r" != *icrosoft* ]];then
10186 open_cmd='nohup xdg-open'
10187 fi
10188 ;;
10189 esac
10190}"##;
10191 let result = parse(input);
10192 assert!(result.is_ok(), "Failed to parse: {:?}", result.err());
10193 }
10194
10195 #[test]
10196 fn test_parse_zpwr_scripts() {
10197 let _g = crate::test_util::global_state_lock();
10198 let scripts_dir = Path::new("/Users/wizard/.zpwr/scripts");
10199 if !scripts_dir.exists() {
10200 eprintln!("Skipping test: scripts directory not found");
10201 return;
10202 }
10203
10204 let mut total = 0;
10205 let mut passed = 0;
10206 let mut failed_files = Vec::new();
10207 let mut timeout_files = Vec::new();
10208
10209 for ext in &["sh", "zsh"] {
10210 let pattern = scripts_dir.join(format!("*.{}", ext));
10211 if let Ok(entries) = glob::glob(pattern.to_str().unwrap()) {
10212 for entry in entries.flatten() {
10213 total += 1;
10214 let file_path = entry.display().to_string();
10215 let content = match fs::read_to_string(&entry) {
10216 Ok(c) => c,
10217 Err(e) => {
10218 failed_files.push((file_path, format!("read error: {}", e)));
10219 continue;
10220 }
10221 };
10222
10223 // Parse with timeout
10224 let content_clone = content.clone();
10225 let (tx, rx) = mpsc::channel();
10226 let handle = thread::spawn(move || {
10227 let result = parse(&content_clone);
10228 let _ = tx.send(result);
10229 });
10230
10231 match rx.recv_timeout(Duration::from_secs(2)) {
10232 Ok(Ok(_)) => passed += 1,
10233 Ok(Err(err)) => {
10234 failed_files.push((file_path, err));
10235 }
10236 Err(_) => {
10237 timeout_files.push(file_path);
10238 // Thread will be abandoned
10239 }
10240 }
10241 }
10242 }
10243 }
10244
10245 eprintln!("\n=== ZPWR Scripts Parse Results ===");
10246 eprintln!("Passed: {}/{}", passed, total);
10247
10248 if !timeout_files.is_empty() {
10249 eprintln!("\nTimeout files (>2s):");
10250 for file in &timeout_files {
10251 eprintln!(" {}", file);
10252 }
10253 }
10254
10255 if !failed_files.is_empty() {
10256 eprintln!("\nFailed files:");
10257 for (file, err) in &failed_files {
10258 eprintln!(" {} - {}", file, err);
10259 }
10260 }
10261
10262 // Allow some failures initially, but track progress
10263 let pass_rate = if total > 0 {
10264 (passed as f64 / total as f64) * 100.0
10265 } else {
10266 0.0
10267 };
10268 eprintln!("Pass rate: {:.1}%", pass_rate);
10269
10270 // Require at least 50% pass rate for now
10271 assert!(pass_rate >= 50.0, "Pass rate too low: {:.1}%", pass_rate);
10272 }
10273
10274 /// c:2643 — `get_cond_num` returns 0..=8 for the canonical binary
10275 /// test operators in order `nt ot ef eq ne lt gt le ge`. The
10276 /// index IS the wordcode opcode dispatch key; flipping any entry
10277 /// would silently mis-dispatch `[[ a -eq b ]]` to a different op.
10278 #[test]
10279 fn get_cond_num_canonical_order_matches_dispatch_table() {
10280 let _g = crate::test_util::global_state_lock();
10281 assert_eq!(get_cond_num("nt"), 0);
10282 assert_eq!(get_cond_num("ot"), 1);
10283 assert_eq!(get_cond_num("ef"), 2);
10284 assert_eq!(get_cond_num("eq"), 3);
10285 assert_eq!(get_cond_num("ne"), 4);
10286 assert_eq!(get_cond_num("lt"), 5);
10287 assert_eq!(get_cond_num("gt"), 6);
10288 assert_eq!(get_cond_num("le"), 7);
10289 assert_eq!(get_cond_num("ge"), 8);
10290 }
10291
10292 /// c:2643 — unknown operator returns -1 (sentinel for "not in the
10293 /// binary set"). Regression returning 0 silently would alias
10294 /// every unknown op to `-nt`, dispatching to the wrong handler.
10295 #[test]
10296 fn get_cond_num_unknown_operator_returns_minus_one() {
10297 let _g = crate::test_util::global_state_lock();
10298 assert_eq!(get_cond_num("xx"), -1);
10299 assert_eq!(get_cond_num(""), -1);
10300 assert_eq!(get_cond_num("eqnt"), -1, "exact-match required");
10301 assert_eq!(
10302 get_cond_num("NT"),
10303 -1,
10304 "case-sensitive — uppercase rejected"
10305 );
10306 }
10307
10308 /// c:2628 — `par_cond_double` requires arg `a` to start with `-`
10309 /// AND have at least one more char. Empty string OR single `-`
10310 /// must error (return 1 via zerr). Regression accepting empty
10311 /// would dispatch `[[ "" string ]]` as a unary test.
10312 #[test]
10313 fn par_cond_double_rejects_short_or_non_dash_first_arg() {
10314 let _g = crate::test_util::global_state_lock();
10315 // empty
10316 let _ = par_cond_double("", "b");
10317 // not-dash
10318 let _ = par_cond_double("foo", "b");
10319 // bare dash
10320 let _ = par_cond_double("-", "b");
10321 // All three must NOT crash + return 1 (error path).
10322 }
10323
10324 /// c:2647 CONDSTRS table — exhaustive iteration: every entry's
10325 /// index round-trips through get_cond_num. A regression that
10326 /// drops an entry would let `[[ a -ef b ]]` silently mis-dispatch.
10327 #[test]
10328 fn get_cond_num_round_trips_for_every_table_entry() {
10329 let _g = crate::test_util::global_state_lock();
10330 for (i, op) in ["nt", "ot", "ef", "eq", "ne", "lt", "gt", "le", "ge"]
10331 .iter()
10332 .enumerate()
10333 {
10334 assert_eq!(get_cond_num(op) as usize, i, "{op} must map to index {i}");
10335 }
10336 }
10337
10338 /// c:2643 — `get_cond_num` is byte-exact: a partial-prefix string
10339 /// must NOT match. `e` (one char) is not `eq`. Catches a
10340 /// regression using `starts_with` instead of equality.
10341 #[test]
10342 fn get_cond_num_partial_prefix_does_not_match() {
10343 let _g = crate::test_util::global_state_lock();
10344 assert_eq!(get_cond_num("e"), -1);
10345 assert_eq!(get_cond_num("eq2"), -1);
10346 assert_eq!(get_cond_num("n"), -1);
10347 }
10348
10349 /// c:2628 — `par_cond_double` checks `IS_DASH(ac[0])` so any
10350 /// non-dash first char fails. The lexed Dash sentinel `\u{9b}`
10351 /// MUST be accepted alongside ASCII `-` (the lexer emits it
10352 /// inside `[[ ... ]]`). Regression dropping the sentinel form
10353 /// would break every cond expression after lexing.
10354 #[test]
10355 fn par_cond_double_accepts_lexed_dash_sentinel() {
10356 let _g = crate::test_util::global_state_lock();
10357 // First char being the Dash sentinel + valid unary letter
10358 // must NOT trigger the "condition expected" error path.
10359 // We can't easily probe the wordcode emission here, but
10360 // the function MUST return without panic for both forms.
10361 let _ = par_cond_double("-z", "foo");
10362 let _ = par_cond_double("\u{9b}z", "foo");
10363 }
10364
10365 /// c:2643 — case sensitivity: uppercase `EQ` MUST NOT match `eq`.
10366 /// zsh's `[[ a -EQ b ]]` is documented as a parse error (only
10367 /// lowercase variants are recognised). Regression doing
10368 /// case-insensitive lookup would silently accept it.
10369 #[test]
10370 fn get_cond_num_is_case_sensitive() {
10371 let _g = crate::test_util::global_state_lock();
10372 assert_eq!(get_cond_num("EQ"), -1);
10373 assert_eq!(get_cond_num("Eq"), -1);
10374 assert_eq!(get_cond_num("eQ"), -1);
10375 // Lowercase still works.
10376 assert_eq!(get_cond_num("eq"), 3);
10377 }
10378
10379 /// `Src/parse.c:2862-2868` — `ecgetstr` inline-3-byte case packs
10380 /// up to 3 chars into bits 3-26 of the wordcode word, then C emits
10381 /// `buf[3] = '\0'; r = dupstring(buf);`. `dupstring` uses `strlen`
10382 /// so the resulting string TRUNCATES at the first NUL byte —
10383 /// short strings of 1 or 2 chars get their tail NUL-padded and
10384 /// silently dropped by strlen.
10385 ///
10386 /// The previous Rust port used `retain(|&x| x != 0)` which SPLICES
10387 /// OUT interior NULs (so `[a, 0, b]` would yield "ab" instead of
10388 /// C's "a"). Verify both endpoints work correctly:
10389 /// * 1-char string ("a", 0, 0) → "a" (strlen-truncate)
10390 /// * 2-char string ("ab", 0) → "ab" (strlen-truncate)
10391 /// * 3-char string ("abc") → "abc" (full)
10392 /// * pathological ("a", 0, "b") → "a" (NOT "ab")
10393 #[test]
10394 fn ecgetstr_inline_string_truncates_at_first_nul_like_c_strlen() {
10395 let _g = crate::test_util::global_state_lock();
10396 // Build a wordcode word with `c & 2 != 0` (inline-string flag)
10397 // and the 3 bytes packed at offsets 3, 11, 19. `c & 1` is the
10398 // tokflag; clear it for this test.
10399 fn pack_inline(b0: u8, b1: u8, b2: u8) -> u32 {
10400 // c:2862 layout — bit0 = tokflag (0 here), bit1 = inline (1),
10401 // bits 3-10 = b0, bits 11-18 = b1, bits 19-26 = b2.
10402 (2u32) | ((b0 as u32) << 3) | ((b1 as u32) << 11) | ((b2 as u32) << 19)
10403 }
10404 let mk_state = |word: u32| -> estate {
10405 let p = eprog {
10406 flags: 0,
10407 len: 1,
10408 npats: 0,
10409 nref: 0,
10410 pats: Vec::new(),
10411 prog: vec![word],
10412 strs: None,
10413 shf: None,
10414 dump: None,
10415 };
10416 estate {
10417 prog: Box::new(p),
10418 pc: 0,
10419 strs: None,
10420 strs_offset: 0,
10421 }
10422 };
10423
10424 // 1-char: ('a', 0, 0) → "a"
10425 let mut st = mk_state(pack_inline(b'a', 0, 0));
10426 assert_eq!(
10427 ecgetstr(&mut st, 0, None),
10428 "a",
10429 "c:2869 strlen truncates 1-char inline at the NUL tail"
10430 );
10431
10432 // 2-char: ('a', 'b', 0) → "ab"
10433 let mut st = mk_state(pack_inline(b'a', b'b', 0));
10434 assert_eq!(
10435 ecgetstr(&mut st, 0, None),
10436 "ab",
10437 "c:2869 strlen truncates 2-char inline at the NUL tail"
10438 );
10439
10440 // 3-char: ('a', 'b', 'c') → "abc"
10441 let mut st = mk_state(pack_inline(b'a', b'b', b'c'));
10442 assert_eq!(
10443 ecgetstr(&mut st, 0, None),
10444 "abc",
10445 "c:2869 full 3-byte inline preserved"
10446 );
10447
10448 // Pathological: ('a', 0, 'b') → "a" (NOT "ab" from retain-splice)
10449 let mut st = mk_state(pack_inline(b'a', 0, b'b'));
10450 assert_eq!(
10451 ecgetstr(&mut st, 0, None),
10452 "a",
10453 "c:2869 strlen STOPS at first NUL; must not splice 'b' through"
10454 );
10455 }
10456
10457 /// Pin: `init_parse_status` resets ALL six lexer-parser flags
10458 /// per `Src/parse.c:500-502`. Specifically `inrepeat_ = 0` at
10459 /// c:501 was previously missing in the Rust port. Pin every
10460 /// reset so a future regression that drops one is caught.
10461 #[test]
10462 fn init_parse_status_resets_all_lexer_parser_flags() {
10463 let _g = crate::test_util::global_state_lock();
10464 // Dirty every flag to a non-default value.
10465 set_incasepat(5);
10466 set_incond(7);
10467 set_inredir(true);
10468 set_infor(3);
10469 set_intypeset(true);
10470 set_inrepeat(2);
10471 set_incmdpos(false);
10472 // Reset.
10473 init_parse_status();
10474 // c:500-502 — every flag back to its default.
10475 assert_eq!(incasepat(), 0, "c:500 — incasepat = 0");
10476 assert_eq!(incond(), 0, "c:500 — incond = 0");
10477 assert!(!inredir(), "c:500 — inredir = 0");
10478 assert_eq!(infor(), 0, "c:500 — infor = 0");
10479 assert!(!intypeset(), "c:500 — intypeset = 0");
10480 assert_eq!(
10481 inrepeat(),
10482 0,
10483 "c:501 — inrepeat_ = 0 (was previously missing)"
10484 );
10485 assert!(incmdpos(), "c:502 — incmdpos = 1");
10486 }
10487
10488 // ═══════════════════════════════════════════════════════════════════
10489 // AST shape tests — feed source through parse(), walk the resulting
10490 // ZshProgram, assert structural properties. Each test uses the local
10491 // `parse(input)` helper that errors cleanly on parse failure.
10492 // Anchor: where applicable, behavior matches `zsh -n -c '...'`
10493 // (parse-only, no execution — which would error on syntax issues).
10494 // ═══════════════════════════════════════════════════════════════════
10495
10496 /// Empty input → ZshProgram with no lists.
10497 #[test]
10498 fn parse_empty_source_yields_zero_lists() {
10499 let _g = crate::test_util::global_state_lock();
10500 let prog = parse("").unwrap();
10501 assert_eq!(prog.lists.len(), 0);
10502 }
10503
10504 /// Comment-only input → no lists (comments are skipped at lex level).
10505 #[test]
10506 fn parse_only_comment_yields_zero_lists() {
10507 let _g = crate::test_util::global_state_lock();
10508 let prog = parse("# this is just a comment").unwrap();
10509 assert_eq!(prog.lists.len(), 0, "comments alone produce no cmds");
10510 }
10511
10512 /// Three commands separated by `;` → three lists.
10513 #[test]
10514 fn parse_three_semicolon_separated_commands_yield_three_lists() {
10515 let _g = crate::test_util::global_state_lock();
10516 let prog = parse("a; b; c").unwrap();
10517 assert_eq!(prog.lists.len(), 3);
10518 }
10519
10520 /// Background command — async flag set on the list.
10521 #[test]
10522 fn parse_background_command_sets_async_flag() {
10523 let _g = crate::test_util::global_state_lock();
10524 let prog = parse("sleep 1 &").unwrap();
10525 assert_eq!(prog.lists.len(), 1);
10526 assert!(
10527 prog.lists[0].flags.async_,
10528 "trailing `&` must set async_ flag"
10529 );
10530 }
10531
10532 /// Pipe count: `a | b | c | d` → 4 stages.
10533 #[test]
10534 fn parse_four_stage_pipeline_has_three_next_links() {
10535 let _g = crate::test_util::global_state_lock();
10536 let prog = parse("a | b | c | d").unwrap();
10537 let mut pipe = &prog.lists[0].sublist.pipe;
10538 let mut count = 1;
10539 while let Some(next) = &pipe.next {
10540 pipe = next;
10541 count += 1;
10542 }
10543 assert_eq!(count, 4, "4 commands should produce 4 pipe stages");
10544 }
10545
10546 /// `|&` between pipeline stages sets merge_stderr.
10547 #[test]
10548 fn parse_pipe_amp_sets_merge_stderr() {
10549 let _g = crate::test_util::global_state_lock();
10550 let prog = parse("a |& b").unwrap();
10551 let pipe = &prog.lists[0].sublist.pipe;
10552 assert!(pipe.next.is_some());
10553 assert!(pipe.merge_stderr, "|& must set merge_stderr");
10554 }
10555
10556 /// `cmd1 || cmd2`: sublist.next is Some with `Or`.
10557 #[test]
10558 fn parse_or_operator_sets_sublist_op_or() {
10559 let _g = crate::test_util::global_state_lock();
10560 let prog = parse("cmd1 || cmd2").unwrap();
10561 let sublist = &prog.lists[0].sublist;
10562 let (op, _) = sublist.next.as_ref().expect("must have next");
10563 assert_eq!(*op, SublistOp::Or);
10564 }
10565
10566 /// `! cmd` sets the not flag on the sublist.
10567 #[test]
10568 fn parse_bang_negation_sets_sublist_not_flag() {
10569 let _g = crate::test_util::global_state_lock();
10570 let prog = parse("! false").unwrap();
10571 let sublist = &prog.lists[0].sublist;
10572 assert!(sublist.flags.not, "`!` prefix must set sublist.flags.not");
10573 }
10574
10575 // ── Compound commands ────────────────────────────────────────────
10576 /// `while cond; do body; done` → ZshCommand::While.
10577 #[test]
10578 fn parse_while_loop_yields_while_command() {
10579 let _g = crate::test_util::global_state_lock();
10580 let prog = parse("while true; do echo x; done").unwrap();
10581 assert!(matches!(
10582 prog.lists[0].sublist.pipe.cmd,
10583 ZshCommand::While(_)
10584 ));
10585 }
10586
10587 /// `until cond; do body; done` → ZshCommand::Until.
10588 /// Anchor: `zsh -n -c 'until false; do echo; done'` accepts and parses
10589 /// as an until-loop. zshrs accepts but emits a DIFFERENT AST variant
10590 /// (not Until). Bug — until loop is mis-classified.
10591 #[test]
10592 fn parse_until_loop_yields_until_command_anchored_to_zsh() {
10593 let _g = crate::test_util::global_state_lock();
10594 let prog = parse("until false; do echo x; done").unwrap();
10595 assert!(
10596 matches!(prog.lists[0].sublist.pipe.cmd, ZshCommand::Until(_)),
10597 "zsh parses `until` as Until variant; zshrs uses different variant: {:?}",
10598 prog.lists[0].sublist.pipe.cmd
10599 );
10600 }
10601
10602 /// `(cmd)` → Subsh variant.
10603 #[test]
10604 fn parse_parens_yield_subsh_command() {
10605 let _g = crate::test_util::global_state_lock();
10606 let prog = parse("(echo hi)").unwrap();
10607 assert!(matches!(
10608 prog.lists[0].sublist.pipe.cmd,
10609 ZshCommand::Subsh(_)
10610 ));
10611 }
10612
10613 /// `{ cmd; }` → Cursh (current-shell) command.
10614 #[test]
10615 fn parse_braces_yield_cursh_command() {
10616 let _g = crate::test_util::global_state_lock();
10617 let prog = parse("{ echo hi; }").unwrap();
10618 assert!(matches!(
10619 prog.lists[0].sublist.pipe.cmd,
10620 ZshCommand::Cursh(_)
10621 ));
10622 }
10623
10624 /// `[[ a == b ]]` → ZshCommand::Cond.
10625 #[test]
10626 fn parse_double_brackets_yield_cond_command() {
10627 let _g = crate::test_util::global_state_lock();
10628 let prog = parse("[[ a == b ]]").unwrap();
10629 assert!(matches!(
10630 prog.lists[0].sublist.pipe.cmd,
10631 ZshCommand::Cond(_)
10632 ));
10633 }
10634
10635 /// `(( 1 + 2 ))` → ZshCommand::Arith.
10636 #[test]
10637 fn parse_double_parens_yield_arith_command() {
10638 let _g = crate::test_util::global_state_lock();
10639 let prog = parse("(( 1 + 2 ))").unwrap();
10640 assert!(matches!(
10641 prog.lists[0].sublist.pipe.cmd,
10642 ZshCommand::Arith(_)
10643 ));
10644 }
10645
10646 /// `repeat 3 do echo x; done` → ZshCommand::Repeat.
10647 #[test]
10648 fn parse_repeat_loop_yields_repeat_command() {
10649 let _g = crate::test_util::global_state_lock();
10650 let prog = parse("repeat 3 do echo x; done").unwrap();
10651 assert!(matches!(
10652 prog.lists[0].sublist.pipe.cmd,
10653 ZshCommand::Repeat(_)
10654 ));
10655 }
10656
10657 // ── Function definitions ─────────────────────────────────────────
10658 /// `name() { body; }` → FuncDef variant.
10659 #[test]
10660 fn parse_paren_funcdef_yields_funcdef_command() {
10661 let _g = crate::test_util::global_state_lock();
10662 let prog = parse("greet() { echo hi; }").unwrap();
10663 assert!(matches!(
10664 prog.lists[0].sublist.pipe.cmd,
10665 ZshCommand::FuncDef(_)
10666 ));
10667 }
10668
10669 /// `function name { body; }` → FuncDef variant (zsh keyword form).
10670 #[test]
10671 fn parse_function_keyword_funcdef_yields_funcdef_command() {
10672 let _g = crate::test_util::global_state_lock();
10673 let prog = parse("function greet { echo hi; }").unwrap();
10674 assert!(matches!(
10675 prog.lists[0].sublist.pipe.cmd,
10676 ZshCommand::FuncDef(_)
10677 ));
10678 }
10679
10680 /// Syntax error — `if` without `fi` → parse returns Err.
10681 /// Anchor: `echo 'if true; then echo' | zsh -n` → "parse error".
10682 #[test]
10683 fn parse_unterminated_if_returns_error_anchored_to_zsh() {
10684 let _g = crate::test_util::global_state_lock();
10685 let r = parse("if true; then echo yes");
10686 assert!(r.is_err(), "zsh -n: parse error near `\\n`");
10687 }
10688
10689 /// Syntax error — bare `done` without `for/while/until` → error.
10690 /// Anchor: `echo done | zsh -n` → "parse error near `done`".
10691 #[test]
10692 fn parse_orphan_done_returns_error_anchored_to_zsh() {
10693 let _g = crate::test_util::global_state_lock();
10694 let r = parse("done");
10695 assert!(r.is_err(), "zsh -n: parse error near `done`");
10696 }
10697
10698 /// Simple command's words are metafied at the AST layer (matches
10699 /// zsh's internal representation: `-` lexes to `Dash` = 0x9b, `*`
10700 /// to `Star`, etc.). zsh untokenizes via `untokenize()` BEFORE
10701 /// surfacing words at execution time (Src/exec.c:execcmd_args).
10702 /// This test pins the round-trip: `untokenize(word)` recovers the
10703 /// user-visible form. If parse-time unmetafy ever lands the
10704 /// untokenize call becomes a no-op; the test stays green either
10705 /// way. Companion test below pins the metafied internal form.
10706 #[test]
10707 fn parse_simple_command_words_unmetafied_like_zsh_anchored() {
10708 let _g = crate::test_util::global_state_lock();
10709 let prog = parse("ls -la /tmp").unwrap();
10710 match &prog.lists[0].sublist.pipe.cmd {
10711 ZshCommand::Simple(s) => {
10712 let untok: Vec<String> = s
10713 .words
10714 .iter()
10715 .map(|w| crate::ported::lex::untokenize(w))
10716 .collect();
10717 assert_eq!(
10718 untok,
10719 vec!["ls", "-la", "/tmp"],
10720 "untokenize(word) must yield the user-visible form"
10721 );
10722 }
10723 other => panic!("expected Simple, got {other:?}"),
10724 }
10725 }
10726
10727 /// Pin the OBSERVED zshrs contract: simple-command word array
10728 /// contains metafied bytes. This is the active (passing) version
10729 /// of the anchor above — it documents zshrs's current internal
10730 /// representation. If zshrs starts unmetafying at parse time, this
10731 /// test will FAIL and the anchor-style test above will start passing.
10732 #[test]
10733 fn parse_simple_command_words_metafied_internal_form() {
10734 let _g = crate::test_util::global_state_lock();
10735 let prog = parse("ls -la /tmp").unwrap();
10736 match &prog.lists[0].sublist.pipe.cmd {
10737 ZshCommand::Simple(s) => {
10738 assert_eq!(s.words.len(), 3);
10739 assert_eq!(s.words[0], "ls");
10740 assert_eq!(s.words[2], "/tmp");
10741 // s.words[1] contains the metafied `-` (`\u{9b}` Dash byte)
10742 // followed by "la". Don't pin the exact byte form (it
10743 // may change); pin that the length is right.
10744 assert_eq!(s.words[1].chars().count(), 3, "`-la` is 3 chars");
10745 assert!(s.words[1].ends_with("la"));
10746 }
10747 other => panic!("expected Simple, got {other:?}"),
10748 }
10749 }
10750
10751 // ─── zsh-corpus pins for parser: structural shapes ────────────────
10752
10753 /// Empty input — parse succeeds, lists may be empty.
10754 #[test]
10755 fn parse_corpus_empty_input_no_error() {
10756 let _g = crate::test_util::global_state_lock();
10757 let prog = parse("").unwrap();
10758 assert!(
10759 prog.lists.is_empty() || prog.lists.len() <= 1,
10760 "empty input → 0 or 1 list, got {}",
10761 prog.lists.len()
10762 );
10763 }
10764
10765 /// Comment-only input parses as empty.
10766 #[test]
10767 fn parse_corpus_comment_only_no_error() {
10768 let _g = crate::test_util::global_state_lock();
10769 let r = parse("# just a comment");
10770 assert!(r.is_ok(), "comment-only parse should succeed");
10771 }
10772
10773 /// `cmd1; cmd2` — two top-level lists or two sublists.
10774 #[test]
10775 fn parse_corpus_semicolon_separates_commands() {
10776 let _g = crate::test_util::global_state_lock();
10777 let prog = parse("echo a; echo b").unwrap();
10778 // We pin: parse produces > 0 lists/sublists; details vary.
10779 assert!(!prog.lists.is_empty(), "non-empty parse");
10780 }
10781
10782 /// `a && b` — DAMPER joins into a sublist chain.
10783 #[test]
10784 fn parse_corpus_logical_and_parses() {
10785 let _g = crate::test_util::global_state_lock();
10786 let r = parse("true && false");
10787 assert!(r.is_ok(), "`a && b` parses cleanly");
10788 }
10789
10790 /// `a || b` — DBAR.
10791 #[test]
10792 fn parse_corpus_logical_or_parses() {
10793 let _g = crate::test_util::global_state_lock();
10794 let r = parse("false || true");
10795 assert!(r.is_ok(), "`a || b` parses cleanly");
10796 }
10797
10798 /// `a | b` pipeline.
10799 #[test]
10800 fn parse_corpus_pipeline_parses() {
10801 let _g = crate::test_util::global_state_lock();
10802 let r = parse("echo hi | cat");
10803 assert!(r.is_ok(), "`a | b` parses");
10804 }
10805
10806 /// `if true; then echo x; fi` — basic if-then-fi block.
10807 #[test]
10808 fn parse_corpus_if_then_fi_parses() {
10809 let _g = crate::test_util::global_state_lock();
10810 let r = parse("if true; then echo x; fi");
10811 assert!(r.is_ok(), "if/then/fi parses cleanly");
10812 }
10813
10814 /// `for i in 1 2 3; do echo $i; done`.
10815 #[test]
10816 fn parse_corpus_for_do_done_parses() {
10817 let _g = crate::test_util::global_state_lock();
10818 let r = parse("for i in 1 2 3; do echo $i; done");
10819 assert!(r.is_ok(), "for/do/done parses cleanly");
10820 }
10821
10822 /// `while true; do break; done`.
10823 #[test]
10824 fn parse_corpus_while_do_done_parses() {
10825 let _g = crate::test_util::global_state_lock();
10826 let r = parse("while true; do break; done");
10827 assert!(r.is_ok(), "while/do/done parses cleanly");
10828 }
10829
10830 /// `case x in (a) echo A;; esac` — case statement.
10831 #[test]
10832 fn parse_corpus_case_esac_parses() {
10833 let _g = crate::test_util::global_state_lock();
10834 let r = parse("case x in (a) echo A;; esac");
10835 assert!(r.is_ok(), "case/esac parses cleanly");
10836 }
10837
10838 /// Function definition `f() { echo x }`.
10839 #[test]
10840 fn parse_corpus_function_def_parses() {
10841 let _g = crate::test_util::global_state_lock();
10842 let r = parse("f() { echo x }");
10843 assert!(r.is_ok(), "f() {{ ... }} parses cleanly");
10844 }
10845
10846 /// `(subshell echo a)` — subshell.
10847 #[test]
10848 fn parse_corpus_subshell_parens_parses() {
10849 let _g = crate::test_util::global_state_lock();
10850 let r = parse("( echo a )");
10851 assert!(r.is_ok(), "subshell parses cleanly");
10852 }
10853
10854 // ═══════════════════════════════════════════════════════════════════
10855 // C-parity tests pinning Src/parse.c. Tests that capture KNOWN
10856 // ZSHRS BUGS use #[ignore = "ZSHRS BUG: …"].
10857 // ═══════════════════════════════════════════════════════════════════
10858
10859 /// `empty_eprog(p)` returns true on an eprog with empty `prog`.
10860 /// C `Src/parse.c:584`:
10861 /// `return (!p || !p->prog || *p->prog == WCB_END());`
10862 /// Rust port at parse.rs:685 — `p.prog.is_empty() || p.prog[0] == WCB_END()`.
10863 #[test]
10864 fn empty_eprog_empty_prog_returns_true() {
10865 let _g = crate::test_util::global_state_lock();
10866 let p = crate::ported::zsh_h::eprog::default();
10867 assert!(empty_eprog(&p), "empty prog vec → empty_eprog true");
10868 }
10869
10870 /// `empty_eprog(p)` returns true when first wordcode is WCB_END.
10871 /// C: `*p->prog == WCB_END()`.
10872 #[test]
10873 fn empty_eprog_first_wcb_end_returns_true() {
10874 let _g = crate::test_util::global_state_lock();
10875 let mut p = crate::ported::zsh_h::eprog::default();
10876 p.prog.push(WCB_END());
10877 assert!(empty_eprog(&p), "prog[0]==WCB_END → empty_eprog true");
10878 }
10879
10880 /// `empty_eprog(p)` returns false for non-empty non-END prog.
10881 #[test]
10882 fn empty_eprog_non_empty_non_end_returns_false() {
10883 let _g = crate::test_util::global_state_lock();
10884 let mut p = crate::ported::zsh_h::eprog::default();
10885 // Push some non-END wordcode (1 is arbitrary non-zero, not WCB_END).
10886 p.prog.push(1);
10887 assert!(!empty_eprog(&p), "non-END first opcode → false");
10888 }
10889
10890 /// `ecstrcode("")` returns a wordcode for the empty string. C
10891 /// `Src/parse.c:346-ish` ecstrcode interns strings in `ecbuf`.
10892 /// Pin: same call returns same wordcode (deterministic intern).
10893 #[test]
10894 fn ecstrcode_empty_string_returns_deterministic_code() {
10895 let _g = crate::test_util::global_state_lock();
10896 init_parse();
10897 let a = ecstrcode("");
10898 let b = ecstrcode("");
10899 assert_eq!(a, b, "intern of '' must be deterministic");
10900 }
10901
10902 /// `ecstrcode` of two different strings returns different codes.
10903 #[test]
10904 fn ecstrcode_distinct_strings_get_distinct_codes() {
10905 let _g = crate::test_util::global_state_lock();
10906 init_parse();
10907 let a = ecstrcode("foo");
10908 let b = ecstrcode("bar");
10909 // Should differ — if equal, intern table collapsed two different
10910 // strings to the same key (bug).
10911 assert_ne!(a, b, "different strings must intern to different codes");
10912 }
10913
10914 /// `parse_event(ENDINPUT)` on empty input returns None.
10915 /// C `Src/parse.c:715-ish` — empty token stream → no program.
10916 #[test]
10917 fn parse_event_empty_returns_none() {
10918 let _g = crate::test_util::global_state_lock();
10919 init_parse();
10920 // Empty input typically yields no program; needs lex state.
10921 let r = parse_event(crate::ported::lex::ENDINPUT);
10922 assert!(r.is_none(), "no tokens → no event");
10923 }
10924
10925 // ═══════════════════════════════════════════════════════════════════
10926 // Additional C-parity tests for Src/parse.c.
10927 // ═══════════════════════════════════════════════════════════════════
10928
10929 /// c:399 — `ecadd(c)` returns the index where `c` was placed,
10930 /// not the post-increment value. Sequential ecadd calls return
10931 /// strictly increasing indices.
10932 #[test]
10933 fn ecadd_returns_strictly_increasing_indices() {
10934 let _g = crate::test_util::global_state_lock();
10935 init_parse();
10936 let i0 = ecadd(0xDEAD);
10937 let i1 = ecadd(0xBEEF);
10938 let i2 = ecadd(0xC0DE);
10939 assert!(
10940 i1 > i0,
10941 "ecadd indices must strictly increase, got {i0} then {i1}"
10942 );
10943 assert!(
10944 i2 > i1,
10945 "ecadd indices must strictly increase, got {i1} then {i2}"
10946 );
10947 assert_eq!(i1, i0 + 1, "consecutive ecadds advance by 1");
10948 assert_eq!(i2, i1 + 1, "consecutive ecadds advance by 1");
10949 }
10950
10951 /// c:413 — `ecdel(p)` removes one wordcode, shrinks ecused by 1.
10952 /// Pin: subsequent ecadd reuses freed slot (ecused decreased).
10953 #[test]
10954 fn ecdel_shrinks_ecused_by_one() {
10955 let _g = crate::test_util::global_state_lock();
10956 init_parse();
10957 let _i0 = ecadd(0xA);
10958 let i1 = ecadd(0xB);
10959 let _i2 = ecadd(0xC);
10960 let next_before = ECUSED.get();
10961 ecdel(i1);
10962 let next_after = ECUSED.get();
10963 assert_eq!(
10964 next_after,
10965 next_before - 1,
10966 "ecdel must decrement ecused by exactly 1"
10967 );
10968 }
10969
10970 /// c:399-405 — `ecadd` after exhausting buffer must grow it (no
10971 /// panic on push past current eclen). Pin: 1000 adds don't crash.
10972 #[test]
10973 fn ecadd_grows_buffer_on_demand() {
10974 let _g = crate::test_util::global_state_lock();
10975 init_parse();
10976 for i in 0..1000 {
10977 ecadd(i as u32);
10978 }
10979 // No panic = grow path works.
10980 assert!(ECUSED.get() >= 1000, "1000 adds → ecused ≥ 1000");
10981 }
10982
10983 /// c:426 — `ecstrcode` of short strings (≤4 bytes) returns a
10984 /// packed inline wordcode (not an offset into the string region).
10985 /// Pin: identical short strings get identical codes.
10986 #[test]
10987 fn ecstrcode_short_strings_are_deterministic() {
10988 let _g = crate::test_util::global_state_lock();
10989 init_parse();
10990 let a = ecstrcode("ab");
10991 let b = ecstrcode("ab");
10992 assert_eq!(a, b, "same short string must intern to same code");
10993 }
10994
10995 /// c:426 — long strings (>4 bytes) hit the deduped string region.
10996 /// Pin: same long string returns same code on repeat (registry
10997 /// dedupes).
10998 #[test]
10999 fn ecstrcode_long_strings_dedupe_in_registry() {
11000 let _g = crate::test_util::global_state_lock();
11001 init_parse();
11002 let a = ecstrcode("a-much-longer-test-string");
11003 let b = ecstrcode("a-much-longer-test-string");
11004 assert_eq!(a, b, "registry must dedupe identical long strings");
11005 }
11006
11007 /// `clear_hdocs()` is idempotent — calling twice in a row leaves
11008 /// HDOCS = None and LEX_HEREDOCS empty.
11009 #[test]
11010 fn clear_hdocs_is_idempotent() {
11011 let _g = crate::test_util::global_state_lock();
11012 clear_hdocs();
11013 clear_hdocs();
11014 HDOCS.with_borrow(|h| assert!(h.is_none(), "HDOCS must be None"));
11015 LEX_HEREDOCS.with_borrow(|v| assert!(v.is_empty(), "LEX_HEREDOCS must be empty"));
11016 }
11017
11018 /// `init_parse()` resets parse state to known empty defaults.
11019 /// Multiple init_parse calls are safe (idempotent).
11020 #[test]
11021 fn init_parse_is_idempotent() {
11022 let _g = crate::test_util::global_state_lock();
11023 init_parse();
11024 init_parse();
11025 // No panic = pass.
11026 }
11027
11028 /// `empty_eprog` returns true for a default-constructed eprog
11029 /// (empty prog vec).
11030 #[test]
11031 fn empty_eprog_true_for_empty_prog() {
11032 let _g = crate::test_util::global_state_lock();
11033 let p = eprog {
11034 prog: Vec::new(),
11035 ..Default::default()
11036 };
11037 assert!(empty_eprog(&p), "empty prog vec → empty eprog");
11038 }
11039
11040 /// `empty_eprog` returns true when prog[0] == WCB_END().
11041 #[test]
11042 fn empty_eprog_true_for_end_only_prog() {
11043 let _g = crate::test_util::global_state_lock();
11044 let p = eprog {
11045 prog: vec![WCB_END()],
11046 ..Default::default()
11047 };
11048 assert!(empty_eprog(&p), "WCB_END as first opcode → empty");
11049 }
11050
11051 /// `ecadjusthere(p, d)` is safe to call when HDOCS is None.
11052 #[test]
11053 fn ecadjusthere_safe_when_hdocs_none() {
11054 let _g = crate::test_util::global_state_lock();
11055 clear_hdocs();
11056 // No panic = pass.
11057 ecadjusthere(0, 0);
11058 ecadjusthere(100, -5);
11059 ecadjusthere(0, 10);
11060 }
11061
11062 /// `ecispace(p, n)` with n=0 is a no-op.
11063 #[test]
11064 fn ecispace_zero_n_is_noop() {
11065 let _g = crate::test_util::global_state_lock();
11066 init_parse();
11067 let before = ECUSED.get();
11068 ecispace(0, 0);
11069 let after = ECUSED.get();
11070 assert_eq!(before, after, "ecispace(_, 0) must not advance ecused");
11071 }
11072
11073 // ═══════════════════════════════════════════════════════════════════
11074 // Additional C-parity tests for Src/parse.c
11075 // c:146 parse_context_save / c:191 parse_context_restore /
11076 // c:225 ecadjusthere / c:293 ecadd / c:346 ecstrcode / c:574 init_parse /
11077 // c:685 empty_eprog / c:693 clear_hdocs / c:786 parse_list / c:815 parse_cond
11078 // c:2234 par_wordlist / c:2249 par_nl_wordlist
11079 // ═══════════════════════════════════════════════════════════════════
11080
11081 /// c:293 — `ecadd` returns usize (compile-time type pin).
11082 #[test]
11083 fn ecadd_returns_usize_type() {
11084 let _g = crate::test_util::global_state_lock();
11085 init_parse();
11086 let _: usize = ecadd(0);
11087 }
11088
11089 /// c:346 — `ecstrcode` returns u32 (compile-time type pin).
11090 #[test]
11091 fn ecstrcode_returns_u32_type() {
11092 let _g = crate::test_util::global_state_lock();
11093 init_parse();
11094 let _: u32 = ecstrcode("");
11095 }
11096
11097 /// c:346 — `ecstrcode("")` empty string is safe.
11098 #[test]
11099 fn ecstrcode_empty_string_no_panic() {
11100 let _g = crate::test_util::global_state_lock();
11101 init_parse();
11102 let _ = ecstrcode("");
11103 }
11104
11105 /// c:346 — `ecstrcode` is deterministic for same input.
11106 #[test]
11107 fn ecstrcode_is_deterministic() {
11108 let _g = crate::test_util::global_state_lock();
11109 init_parse();
11110 for s in ["", "a", "abc", "hello world"] {
11111 let first = ecstrcode(s);
11112 for _ in 0..3 {
11113 assert_eq!(
11114 ecstrcode(s),
11115 first,
11116 "ecstrcode({:?}) must be deterministic",
11117 s
11118 );
11119 }
11120 }
11121 }
11122
11123 /// c:786 — `parse_list` returns Option<eprog>.
11124 #[test]
11125 fn parse_list_returns_option_eprog_type() {
11126 let _g = crate::test_util::global_state_lock();
11127 init_parse();
11128 let _: Option<eprog> = parse_list();
11129 }
11130
11131 /// c:815 — `parse_cond` returns Option<eprog>.
11132 #[test]
11133 fn parse_cond_returns_option_eprog_type() {
11134 let _g = crate::test_util::global_state_lock();
11135 init_parse();
11136 let _: Option<eprog> = parse_cond();
11137 }
11138
11139 /// c:2234 — `par_wordlist` returns Vec<String>.
11140 #[test]
11141 fn par_wordlist_returns_vec_string_type() {
11142 let _g = crate::test_util::global_state_lock();
11143 init_parse();
11144 let _: Vec<String> = par_wordlist();
11145 }
11146
11147 /// c:2249 — `par_nl_wordlist` returns Vec<String>.
11148 #[test]
11149 fn par_nl_wordlist_returns_vec_string_type() {
11150 let _g = crate::test_util::global_state_lock();
11151 init_parse();
11152 let _: Vec<String> = par_nl_wordlist();
11153 }
11154
11155 /// c:693 — `clear_hdocs` deterministic state after call (no-panic).
11156 #[test]
11157 fn clear_hdocs_deterministic_after_call() {
11158 let _g = crate::test_util::global_state_lock();
11159 clear_hdocs();
11160 clear_hdocs();
11161 }
11162
11163 /// c:225 — `ecadjusthere(0, 0)` is a no-op (no delta).
11164 #[test]
11165 fn ecadjusthere_zero_delta_no_panic() {
11166 let _g = crate::test_util::global_state_lock();
11167 ecadjusthere(0, 0);
11168 }
11169
11170 /// c:225 — `ecadjusthere` is safe for arbitrary positions.
11171 #[test]
11172 fn ecadjusthere_arbitrary_pos_no_panic() {
11173 let _g = crate::test_util::global_state_lock();
11174 for p in [0usize, 1, 100, 9999] {
11175 ecadjusthere(p, 0);
11176 ecadjusthere(p, 1);
11177 ecadjusthere(p, -1);
11178 }
11179 }
11180
11181 // ═══════════════════════════════════════════════════════════════════
11182 // Additional C-parity tests for Src/parse.c FD_* accessors
11183 // c:3127 fdmagic / c:3131 fdflags / c:3133 fdother / c:3140 fdversion /
11184 // c:3145 fdhflags / c:3146 fdhtail / c:3147 fdhbldflags
11185 // ═══════════════════════════════════════════════════════════════════
11186
11187 fn build_fd_header() -> Vec<u32> {
11188 let mut buf = vec![0u32; FD_PRELEN + 32];
11189 buf[0] = FD_MAGIC; // pre[0] magic
11190 buf[1] = (0x12u32) | (0x00ABCDEFu32 << 8); // flags=0x12, other=0xABCDEF
11191 // Embed version string starting at pre[2].
11192 let ver = b"5.9\0";
11193 for (i, chunk) in ver.chunks(4).enumerate() {
11194 let mut word = [0u8; 4];
11195 word[..chunk.len()].copy_from_slice(chunk);
11196 buf[2 + i] = u32::from_le_bytes(word);
11197 }
11198 buf[FD_PRELEN - 1] = (FD_PRELEN as u32) + 8; // header-len slot
11199 buf
11200 }
11201
11202 /// c:3127 — `fdmagic(f)` returns pre[0] verbatim.
11203 #[test]
11204 fn fdmagic_returns_pre_zero_word() {
11205 let buf = build_fd_header();
11206 assert_eq!(fdmagic(&buf), FD_MAGIC, "fdmagic = pre[0]");
11207 }
11208
11209 /// c:3131 — `fdflags` extracts low byte of pre[1].
11210 #[test]
11211 fn fdflags_low_byte_extraction() {
11212 let buf = build_fd_header();
11213 assert_eq!(fdflags(&buf), 0x12, "flags = pre[1] & 0xff");
11214 }
11215
11216 /// c:3133 — `fdother` extracts high 24 bits of pre[1].
11217 #[test]
11218 fn fdother_high_24_bits_extraction() {
11219 let buf = build_fd_header();
11220 assert_eq!(
11221 fdother(&buf),
11222 0x00ABCDEF,
11223 "other = pre[1] >> 8 & 0x00ffffff"
11224 );
11225 }
11226
11227 /// c:3132 — `fdsetflags` writes low byte, preserves high 24 bits.
11228 #[test]
11229 fn fdsetflags_preserves_high_24_bits() {
11230 let mut buf = build_fd_header();
11231 let other_before = fdother(&buf);
11232 fdsetflags(&mut buf, 0x42);
11233 assert_eq!(fdflags(&buf), 0x42, "new flags written");
11234 assert_eq!(fdother(&buf), other_before, "high 24 bits preserved");
11235 }
11236
11237 /// c:3134 — `fdsetother` writes high 24 bits, preserves low byte.
11238 #[test]
11239 fn fdsetother_preserves_low_byte() {
11240 let mut buf = build_fd_header();
11241 let flags_before = fdflags(&buf);
11242 fdsetother(&mut buf, 0x00DEADBE);
11243 assert_eq!(fdother(&buf), 0x00DEADBE, "new other written");
11244 assert_eq!(fdflags(&buf), flags_before, "low byte preserved");
11245 }
11246
11247 /// c:3134 — `fdsetother` clamps to 24 bits (caller-passed high bits dropped).
11248 #[test]
11249 fn fdsetother_clamps_to_24_bits() {
11250 let mut buf = build_fd_header();
11251 fdsetother(&mut buf, 0xFF_FFFF_FF);
11252 // Only the low 24 bits land in `other`.
11253 assert_eq!(fdother(&buf), 0x00FF_FFFF, "high bits dropped");
11254 }
11255
11256 /// c:3140 — `fdversion(buf)` returns String (compile-time type pin).
11257 #[test]
11258 fn fdversion_returns_string_type() {
11259 let buf = build_fd_header();
11260 let _: String = fdversion(&buf);
11261 }
11262
11263 /// c:3140 — `fdversion` reads the NUL-terminated string from pre[2..].
11264 #[test]
11265 fn fdversion_reads_until_nul() {
11266 let buf = build_fd_header();
11267 assert_eq!(fdversion(&buf), "5.9", "version read until NUL");
11268 }
11269
11270 /// c:3145 — `fdhflags(h)` returns low 2 bits of flags.
11271 #[test]
11272 fn fdhflags_low_two_bits() {
11273 let h = fdhead {
11274 start: 0,
11275 len: 0,
11276 npats: 0,
11277 strs: 0,
11278 hlen: 0,
11279 flags: 0b1011, // tail=2, kshload bits = 0b11
11280 };
11281 assert_eq!(fdhflags(&h), 0b11, "flags = h.flags & 0x3");
11282 }
11283
11284 /// c:3146 — `fdhtail(h)` returns high 30 bits (shifted right by 2).
11285 #[test]
11286 fn fdhtail_shift_right_two() {
11287 let h = fdhead {
11288 start: 0,
11289 len: 0,
11290 npats: 0,
11291 strs: 0,
11292 hlen: 0,
11293 flags: (0x12_3456 << 2) | 0x3,
11294 };
11295 assert_eq!(fdhtail(&h), 0x12_3456, "tail = h.flags >> 2");
11296 }
11297
11298 /// c:3147 — `fdhbldflags(flags, tail)` packs into single u32.
11299 #[test]
11300 fn fdhbldflags_packs_flags_low_tail_high() {
11301 let packed = fdhbldflags(0x3, 0x42);
11302 assert_eq!(packed & 0x3, 0x3, "low 2 bits = flags");
11303 assert_eq!(packed >> 2, 0x42, "high 30 bits = tail");
11304 }
11305
11306 /// c:3145-3147 — `fdhflags(h)`+`fdhtail(h)` round-trip via fdhbldflags.
11307 #[test]
11308 fn fdh_round_trip_via_bldflags() {
11309 for (flags, tail) in [(0u32, 0u32), (1, 100), (2, 0xABC), (3, 0xFFFF)] {
11310 let packed = fdhbldflags(flags, tail);
11311 let h = fdhead {
11312 start: 0,
11313 len: 0,
11314 npats: 0,
11315 strs: 0,
11316 hlen: 0,
11317 flags: packed,
11318 };
11319 assert_eq!(fdhflags(&h), flags, "flags round-trips");
11320 assert_eq!(fdhtail(&h), tail, "tail round-trips");
11321 }
11322 }
11323
11324 /// c:8271 — `firstfdhead_offset()` returns FD_PRELEN constant.
11325 #[test]
11326 fn firstfdhead_offset_returns_prelen() {
11327 assert_eq!(
11328 firstfdhead_offset(),
11329 FD_PRELEN,
11330 "first header starts after prelude"
11331 );
11332 }
11333
11334 /// c:3127 — `fdmagic` differentiates FD_MAGIC from FD_OMAGIC.
11335 #[test]
11336 fn fdmagic_differentiates_magic_omagic() {
11337 let mut buf = vec![FD_MAGIC; FD_PRELEN];
11338 assert_eq!(fdmagic(&buf), FD_MAGIC);
11339 buf[0] = FD_OMAGIC;
11340 assert_eq!(fdmagic(&buf), FD_OMAGIC, "swapped magic readable");
11341 assert_ne!(FD_MAGIC, FD_OMAGIC, "the two magics differ");
11342 }
11343}