Skip to main content

zsh/extensions/
zwc.rs

1//! ZWC (Zsh Word Code) file parser. Direct port of the dump-file
2//! family in zsh/Src/parse.c:3077-end.
3//!
4//! Parses compiled zsh function files (.zwc) into function definitions
5//! that can be executed by zshrs. Counterpart to zsh's bin_zcompile
6//! (parse.c:3179-3257) for the build side and try_dump_file /
7//! try_source_file / check_dump_file (parse.c:3746-3833) for the
8//! load side.
9//!
10//! Format constants (FD_MAGIC, FD_OMAGIC, FD_PRELEN, FDF_MAP,
11//! FDF_OTHER, FDHF_KSHLOAD, FDHF_ZSHLOAD) match parse.c:3104-3151
12//! exactly so .zwc files written by stock zsh load in zshrs and
13//! vice versa.
14//!
15//! Direct port surface mapping:
16//!
17//!   ZwcFile::load            <- parse.c:3746-3793 try_dump_file
18//!   ZwcFile::get_function    <- parse.c:3166-3176 dump_find_func
19//!   ZwcFile::list_functions  <- parse.c:fdheaderlen + nextfdhead walk
20//!   ZwcFile::decode_function <- parse.c:3245-3543 dump_func / build
21//!   ZwcBuilder::new          <- parse.c:3179-3257 bin_zcompile init
22//!   ZwcBuilder::add_source   <- parse.c:3397-3535 build_dump body
23//!   ZwcBuilder::add_file     <- parse.c:3536-3631 build_cur_dump
24//!   ZwcBuilder::write        <- parse.c:bld_eprog + dump-write loop
25//!   WordcodeDecoder          <- parse.c:wc_code/wc_data helpers
26//!   wc_code / wc_data        <- parse.c:wc_code/wc_data macros
27
28use crate::parse::{
29    CaseTerminator, CompoundCommand, ListOp, Redirect, RedirectOp, ShellCommand, ShellWord,
30    SimpleCommand,
31};
32use std::fs::File;
33use std::io::Write;
34use std::io::{self, Read, Seek, SeekFrom};
35use std::path::Path;
36
37const FD_MAGIC: u32 = 0x04050607;
38const FD_OMAGIC: u32 = 0x07060504; // Other byte order
39const FD_PRELEN: usize = 12;
40use crate::ported::zsh_h::{
41    Bang, Bar, Bnull, Comma, Dash, Dnull, Equals, Hat, Inang, Inbrace, Inbrack, Inpar, Nularg,
42    Outang, Outbrace, Outbrack, Outpar, Pound, Quest, Snull, Star, Stringg, Tick, Tilde, WC_ARITH,
43    WC_ASSIGN, WC_ASSIGN_ARRAY, WC_ASSIGN_INC, WC_ASSIGN_SCALAR, WC_AUTOFN, WC_CASE, WC_CASE_AND,
44    WC_CASE_FREE, WC_CASE_HEAD, WC_CASE_OR, WC_CASE_TESTAND, WC_CODEBITS, WC_COND, WC_CURSH,
45    WC_END, WC_FOR, WC_FOR_COND, WC_FOR_LIST, WC_FOR_PPARAM, WC_FUNCDEF, WC_IF, WC_IF_ELIF,
46    WC_IF_ELSE, WC_IF_HEAD, WC_IF_IF, WC_LIST, WC_LIST_FREE, WC_PIPE, WC_PIPE_END, WC_PIPE_MID,
47    WC_REDIR, WC_REPEAT, WC_SELECT, WC_SELECT_LIST, WC_SELECT_PPARAM, WC_SIMPLE, WC_SUBLIST,
48    WC_SUBLIST_AND, WC_SUBLIST_COPROC, WC_SUBLIST_END, WC_SUBLIST_FREE, WC_SUBLIST_NOT,
49    WC_SUBLIST_OR, WC_SUBLIST_SIMPLE, WC_SUBSH, WC_TIMED, WC_TRY, WC_TYPESET, WC_WHILE,
50    WC_WHILE_UNTIL, WC_WHILE_WHILE,
51};
52// Z_END / Z_SIMPLE in zsh_h are i32 (matching C `int` for these flag bits).
53// Rebind to u32 for bitwise ops against `wordcode` data.
54const Z_END: u32 = crate::ported::zsh_h::Z_END as u32;
55const Z_SIMPLE: u32 = crate::ported::zsh_h::Z_SIMPLE as u32;
56
57/// Decode one NUL-delimited entry from a C-convention wordcode
58/// string pool into the Rust-internal token-char `String` form.
59///
60/// Bridge for `ecgetstr` / `ecrawstr` (`Src/parse.c:2855/2891` ports):
61/// C treats `prog->strs` as raw `char *` — token codes (Pound `0x84`
62/// .. Nularg `0xa1`), Meta (`0x83`), and multibyte text all live as
63/// raw bytes. zshrs's internal convention stores tokens as `char`s
64/// (so a native eprog's pool is valid UTF-8 and passes through
65/// unchanged), but an eprog loaded from a C-zsh-written `.zwc`
66/// (`check_dump_file`, parse.c:3833 port) carries the raw single
67/// bytes. Widening each non-UTF-8 byte to the char of the same
68/// codepoint yields exactly the token representation
69/// `lex::untokenize` and the rest of the pipeline expect, while
70/// genuine UTF-8 text (e.g. literal `é`) survives intact.
71pub fn wordcode_pool_str(bytes: &[u8]) -> String {
72    let mut out = String::with_capacity(bytes.len());
73    let mut rest = bytes;
74    loop {
75        match std::str::from_utf8(rest) {
76            Ok(s) => {
77                out.push_str(s);
78                break;
79            }
80            Err(e) => {
81                let (valid, after) = rest.split_at(e.valid_up_to());
82                // SAFETY: `valid_up_to` guarantees this prefix is UTF-8.
83                out.push_str(unsafe { std::str::from_utf8_unchecked(valid) });
84                out.push(after[0] as char);
85                rest = &after[1..];
86            }
87        }
88    }
89    out
90}
91
92/// Decode one raw wordcode-pool slice: unmetafy first when the pool
93/// came from a C-zsh-written `.zwc` dump (`eprog.strs_metafied` — see
94/// the field doc in zsh_h.rs), then widen through
95/// [`wordcode_pool_str`]. Meta pairs never contain NUL (escaped byte =
96/// x^32 for x ∈ {0, 0x83..=0xA2} → {0x20, 0xA3..=0xC2}), so the
97/// caller's NUL-terminated slicing is unaffected by the escapes.
98/// Without the unmetafy, multibyte glyphs whose bytes fall in the
99/// IMETA range decode as tofu: █ (E2 96 88) is dumped as E2 83 B6 88,
100/// whose prefix is VALID UTF-8 (U+20F6 ⃶) — the zpwr banner bug.
101pub fn wordcode_pool_str_unmeta(slice: &[u8], metafied: bool) -> String {
102    if metafied && slice.contains(&0x83) {
103        let mut b = slice.to_vec();
104        crate::ported::utils::unmetafy(&mut b);
105        wordcode_pool_str(&b)
106    } else {
107        wordcode_pool_str(slice)
108    }
109}
110
111/// Untokenize a zsh tokenized string back to shell syntax
112pub(crate) fn untokenize(bytes: &[u8]) -> String {
113    let mut result = String::new();
114    let mut i = 0;
115
116    while i < bytes.len() {
117        let b = bytes[i];
118        // Token constants in zsh_h are `char` (Unicode \u{84}..\u{a1}).
119        // Tokenized strings encode them as the same byte value, so widen
120        // the byte to char for the match.
121        let c = b as char;
122        match c {
123            Pound => result.push('#'),
124            Stringg => result.push('$'),
125            Hat => result.push('^'),
126            Star => result.push('*'),
127            Inpar => result.push('('),
128            Outpar => result.push(')'),
129            Equals => result.push('='),
130            Bar => result.push('|'),
131            Inbrace => result.push('{'),
132            Outbrace => result.push('}'),
133            Inbrack => result.push('['),
134            Outbrack => result.push(']'),
135            Tick => result.push('`'),
136            Inang => result.push('<'),
137            Outang => result.push('>'),
138            Quest => result.push('?'),
139            Tilde => result.push('~'),
140            Comma => result.push(','),
141            Dash => result.push('-'),
142            Bang => result.push('!'),
143            Snull | Dnull | Bnull | Nularg => {
144                // Skip null markers
145            }
146            '\u{89}' => result.push_str("(("), // Inparmath
147            '\u{8b}' => result.push_str("))"), // Outparmath
148            _ if b >= 0x80 => {
149                // Unknown token, skip or try to represent
150            }
151            _ => result.push(c),
152        }
153        i += 1;
154    }
155
156    result
157}
158
159/// Extract the opcode portion of a wordcode word.
160/// Port of the `wc_code()` macro from Src/zsh.h.
161#[inline]
162pub fn wc_code(c: u32) -> u32 {
163    c & ((1 << WC_CODEBITS) - 1)
164}
165
166/// Extract the data portion of a wordcode word.
167/// Port of the `wc_data()` macro from Src/zsh.h.
168#[inline]
169pub fn wc_data(c: u32) -> u32 {
170    c >> WC_CODEBITS
171}
172
173/// `.zwc` file header.
174/// Port of `struct fdhead` from Src/parse.c — the C source's
175/// `bld_eprog()` (line 547) writes this layout when persisting a
176/// compiled function to a `.zwc` cache file.
177#[derive(Debug)]
178pub struct ZwcHeader {
179    /// `magic` field.
180    pub magic: u32,
181    /// `flags` field.
182    pub flags: u8,
183    /// `version` field.
184    pub version: String,
185    /// `header_len` field.
186    pub header_len: u32,
187    /// `other_offset` field.
188    pub other_offset: u32,
189}
190
191/// One function's directory entry inside a `.zwc` file.
192/// Port of `struct fdname` from Src/parse.c — `bld_eprog()`
193/// (line 547) emits one entry per function autoloaded from the
194/// cache.
195#[derive(Debug)]
196pub struct ZwcFunction {
197    /// `name` field.
198    pub name: String,
199    /// `start` field.
200    pub start: u32,
201    /// `len` field.
202    pub len: u32,
203    /// `npats` field.
204    pub npats: u32,
205    /// `strs_offset` field.
206    pub strs_offset: u32,
207    /// `flags` field.
208    pub flags: u32,
209}
210
211/// A loaded `.zwc` file.
212/// Aggregates the header / function-table / wordcode / strings the
213/// C source's `try_source_file()` (Src/init.c) reads from a `.zwc`
214/// before caching the parsed eprog.
215#[derive(Debug)]
216pub struct ZwcFile {
217    /// `header` field.
218    pub header: ZwcHeader,
219    /// `functions` field.
220    pub functions: Vec<ZwcFunction>,
221    /// `wordcode` field.
222    pub wordcode: Vec<u32>,
223    /// `strings` field.
224    pub strings: Vec<u8>,
225}
226
227impl ZwcFile {
228    /// `load` — see implementation.
229    pub fn load<P: AsRef<Path>>(path: P) -> io::Result<Self> {
230        let mut file = File::open(path)?;
231        let mut buf = vec![0u8; (FD_PRELEN + 1) * 4];
232
233        file.read_exact(&mut buf)?;
234
235        let magic = u32::from_ne_bytes([buf[0], buf[1], buf[2], buf[3]]);
236
237        let swap_bytes = if magic == FD_MAGIC {
238            false
239        } else if magic == FD_OMAGIC {
240            true
241        } else {
242            return Err(io::Error::new(
243                io::ErrorKind::InvalidData,
244                format!("Invalid ZWC magic: 0x{:08x}", magic),
245            ));
246        };
247
248        let read_u32 = |bytes: &[u8], offset: usize| -> u32 {
249            let b = &bytes[offset..offset + 4];
250            let val = u32::from_ne_bytes([b[0], b[1], b[2], b[3]]);
251            if swap_bytes {
252                val.swap_bytes()
253            } else {
254                val
255            }
256        };
257
258        let flags = buf[4];
259        let other_offset = (buf[5] as u32) | ((buf[6] as u32) << 8) | ((buf[7] as u32) << 16);
260
261        // Version string starts at offset 8 (word 2)
262        let version_start = 8;
263        let version_end = buf[version_start..]
264            .iter()
265            .position(|&b| b == 0)
266            .map(|p| version_start + p)
267            .unwrap_or(buf.len());
268        let version = String::from_utf8_lossy(&buf[version_start..version_end]).to_string();
269
270        let header_len = read_u32(&buf, FD_PRELEN * 4);
271
272        let header = ZwcHeader {
273            magic,
274            flags,
275            version,
276            header_len,
277            other_offset,
278        };
279
280        // Read full header
281        file.seek(SeekFrom::Start(0))?;
282        let full_header_size = (header_len as usize) * 4;
283        let mut header_buf = vec![0u8; full_header_size];
284        file.read_exact(&mut header_buf)?;
285
286        // Parse function headers (start after FD_PRELEN words)
287        let mut functions = Vec::new();
288        let mut offset = FD_PRELEN * 4;
289
290        while offset < full_header_size {
291            if offset + 24 > full_header_size {
292                break;
293            }
294
295            let start = read_u32(&header_buf, offset);
296            let len = read_u32(&header_buf, offset + 4);
297            let npats = read_u32(&header_buf, offset + 8);
298            let strs = read_u32(&header_buf, offset + 12);
299            let hlen = read_u32(&header_buf, offset + 16);
300            let flags = read_u32(&header_buf, offset + 20);
301
302            // Name follows the header struct (6 words = 24 bytes)
303            let name_start = offset + 24;
304            let name_end = header_buf[name_start..]
305                .iter()
306                .position(|&b| b == 0)
307                .map(|p| name_start + p)
308                .unwrap_or(full_header_size);
309
310            let name = String::from_utf8_lossy(&header_buf[name_start..name_end]).to_string();
311
312            if name.is_empty() {
313                break;
314            }
315
316            functions.push(ZwcFunction {
317                name,
318                start,
319                len,
320                npats,
321                strs_offset: strs,
322                flags,
323            });
324
325            // Move to next function header
326            offset += (hlen as usize) * 4;
327        }
328
329        // Read the rest of the file (wordcode + strings)
330        let mut rest = Vec::new();
331        file.read_to_end(&mut rest)?;
332
333        // Parse wordcode as u32 array
334        let mut wordcode = Vec::new();
335        let mut i = 0;
336        while i + 4 <= rest.len() {
337            let val = u32::from_ne_bytes([rest[i], rest[i + 1], rest[i + 2], rest[i + 3]]);
338            wordcode.push(if swap_bytes { val.swap_bytes() } else { val });
339            i += 4;
340        }
341
342        // Strings are embedded after wordcode for each function
343        let strings = rest;
344
345        Ok(ZwcFile {
346            header,
347            functions,
348            wordcode,
349            strings,
350        })
351    }
352    /// `list_functions` — see implementation.
353    pub fn list_functions(&self) -> Vec<&str> {
354        self.functions.iter().map(|f| f.name.as_str()).collect()
355    }
356    /// `function_count` — see implementation.
357    pub fn function_count(&self) -> usize {
358        self.functions.len()
359    }
360
361    /// Create a new empty ZWC file for building
362    pub fn new_builder() -> ZwcBuilder {
363        ZwcBuilder::new()
364    }
365    /// `get_function` — see implementation.
366    pub fn get_function(&self, name: &str) -> Option<&ZwcFunction> {
367        self.functions
368            .iter()
369            .find(|f| f.name == name || f.name.ends_with(&format!("/{}", name)))
370    }
371    /// `decode_function` — see implementation.
372    pub fn decode_function(&self, func: &ZwcFunction) -> Option<DecodedFunction> {
373        let header_words = self.header.header_len as usize;
374        let start_idx = (func.start as usize).saturating_sub(header_words);
375
376        if start_idx >= self.wordcode.len() {
377            return None;
378        }
379
380        // Strings are embedded at strs_offset bytes from the start of this function's wordcode
381        // Convert byte offset to word offset to find where strings start
382        let func_wordcode = &self.wordcode[start_idx..];
383
384        // The strings are at byte offset strs_offset from the wordcode base
385        // Create a string table from the wordcode bytes
386        let mut string_bytes = Vec::new();
387        for &wc in func_wordcode {
388            string_bytes.extend_from_slice(&wc.to_ne_bytes());
389        }
390
391        let decoder = WordcodeDecoder::new(func_wordcode, &string_bytes, func.strs_offset as usize);
392
393        Some(DecodedFunction {
394            name: func.name.clone(),
395            body: decoder.decode(),
396        })
397    }
398}
399
400/// Builder for emitting `.zwc` files.
401/// Port of `bld_eprog(int heap)` from Src/parse.c:547 — accumulates
402/// function source / wordcode / strings, then writes them out in
403/// the canonical layout `try_source_file()` (Src/init.c) reads.
404#[derive(Debug)]
405pub struct ZwcBuilder {
406    functions: Vec<(String, Vec<u8>)>, // (name, source code)
407}
408
409impl Default for ZwcBuilder {
410    fn default() -> Self {
411        Self::new()
412    }
413}
414
415impl ZwcBuilder {
416    /// `new` — see implementation.
417    pub fn new() -> Self {
418        Self {
419            functions: Vec::new(),
420        }
421    }
422
423    /// Add a function from source code
424    pub fn add_source(&mut self, name: &str, source: &str) {
425        self.functions
426            .push((name.to_string(), source.as_bytes().to_vec()));
427    }
428
429    /// Add a function from a file
430    pub fn add_file(&mut self, path: &std::path::Path) -> io::Result<()> {
431        let name = path
432            .file_name()
433            .and_then(|n| n.to_str())
434            .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Invalid filename"))?;
435        let source = std::fs::read(path)?;
436        self.functions.push((name.to_string(), source));
437        Ok(())
438    }
439
440    /// Write the ZWC file
441    /// Note: This writes a simplified format that stores raw source code
442    /// rather than compiled wordcode. The loader handles both formats.
443    pub fn write<P: AsRef<std::path::Path>>(&self, path: P) -> io::Result<()> {
444        let mut file = std::fs::File::create(path)?;
445
446        // Write magic
447        file.write_all(&FD_MAGIC.to_ne_bytes())?;
448
449        // Write flags (0 = not mapped)
450        file.write_all(&[0u8])?;
451
452        // Write other offset placeholder (3 bytes)
453        file.write_all(&[0u8; 3])?;
454
455        // Write version string (padded to 4-byte boundary)
456        let version = env!("CARGO_PKG_VERSION");
457        let version_bytes = version.as_bytes();
458        file.write_all(version_bytes)?;
459        file.write_all(&[0u8])?; // null terminator
460                                 // Pad to 4-byte boundary
461        let padding = (4 - ((version_bytes.len() + 1) % 4)) % 4;
462        file.write_all(&vec![0u8; padding])?;
463
464        // Calculate header length (in words)
465        let mut header_words = FD_PRELEN;
466        for (name, _) in &self.functions {
467            // 6 words for fdhead struct + name (padded)
468            header_words += 6 + (name.len() + 1).div_ceil(4);
469        }
470
471        // Write header length
472        file.write_all(&(header_words as u32).to_ne_bytes())?;
473
474        // Track positions for function data
475        let mut data_offset = header_words;
476        let mut func_data: Vec<(u32, u32, Vec<u8>)> = Vec::new(); // (start, len, data)
477
478        // Write function headers
479        for (name, source) in &self.functions {
480            let source_words = source.len().div_ceil(4);
481
482            // fdhead: start, len, npats, strs, hlen, flags
483            file.write_all(&(data_offset as u32).to_ne_bytes())?; // start
484            file.write_all(&(source.len() as u32).to_ne_bytes())?; // len (in bytes)
485            file.write_all(&0u32.to_ne_bytes())?; // npats
486            file.write_all(&0u32.to_ne_bytes())?; // strs offset
487            let hlen = 6 + (name.len() + 1).div_ceil(4);
488            file.write_all(&(hlen as u32).to_ne_bytes())?; // hlen
489            file.write_all(&0u32.to_ne_bytes())?; // flags
490
491            // Write name (null-terminated, padded)
492            file.write_all(name.as_bytes())?;
493            file.write_all(&[0u8])?;
494            let name_padding = (4 - ((name.len() + 1) % 4)) % 4;
495            file.write_all(&vec![0u8; name_padding])?;
496
497            func_data.push((data_offset as u32, source.len() as u32, source.clone()));
498            data_offset += source_words;
499        }
500
501        // Write function data (source code, padded to 4 bytes)
502        for (_, _, data) in &func_data {
503            file.write_all(data)?;
504            let padding = (4 - (data.len() % 4)) % 4;
505            file.write_all(&vec![0u8; padding])?;
506        }
507
508        Ok(())
509    }
510}
511
512/// Decoded function body (one entry per `.zwc` directory entry).
513/// zshrs convenience for tooling (`zshrs zwc dump`); C zsh just
514/// re-eprogs from the wordcode (Src/parse.c) without exposing the
515/// per-op tree.
516#[derive(Debug, Clone)]
517pub struct DecodedFunction {
518    /// `name` field.
519    pub name: String,
520    /// `body` field.
521    pub body: Vec<DecodedOp>,
522}
523
524/// Decoded wordcode op variants.
525/// Mirrors the `WC_*` opcode dispatch tree the C source uses
526/// across Src/exec.c (`exectree()`) and Src/text.c
527/// (`gettext2()`). Each variant corresponds to one of the C
528/// source's `WC_*` opcodes.
529#[derive(Debug, Clone)]
530pub enum DecodedOp {
531    /// `End` variant.
532    End,
533    /// `LineNo` variant.
534    LineNo(u32),
535    /// `List` variant.
536    List {
537        list_type: u32,
538        is_end: bool,
539        ops: Vec<DecodedOp>,
540    },
541    /// `Sublist` variant.
542    Sublist {
543        sublist_type: u32,
544        negated: bool,
545        ops: Vec<DecodedOp>,
546    },
547    /// `Pipe` variant.
548    Pipe { lineno: u32, ops: Vec<DecodedOp> },
549    /// `Redir` variant.
550    Redir {
551        redir_type: u32,
552        fd: i32,
553        target: String,
554        varid: Option<String>,
555    },
556    /// `Assign` variant.
557    Assign { name: String, value: String },
558    /// `AssignArray` variant.
559    AssignArray { name: String, values: Vec<String> },
560    /// `Simple` variant.
561    Simple { args: Vec<String> },
562    /// `Typeset` variant.
563    Typeset {
564        args: Vec<String>,
565        assigns: Vec<DecodedOp>,
566    },
567    /// `Subsh` variant.
568    Subsh { ops: Vec<DecodedOp> },
569    /// `Cursh` variant.
570    Cursh { ops: Vec<DecodedOp> },
571    /// `Timed` variant.
572    Timed { cmd: Option<Box<DecodedOp>> },
573    /// `FuncDef` variant.
574    FuncDef { name: String, body: Vec<DecodedOp> },
575    /// `For` variant.
576    For {
577        var: String,
578        list: Vec<String>,
579        body: Vec<DecodedOp>,
580    },
581    /// `ForCond` variant.
582    ForCond {
583        init: String,
584        cond: String,
585        step: String,
586        body: Vec<DecodedOp>,
587    },
588    /// `Select` variant.
589    Select {
590        var: String,
591        list: Vec<String>,
592        body: Vec<DecodedOp>,
593    },
594    /// `While` variant.
595    While {
596        cond: Vec<DecodedOp>,
597        body: Vec<DecodedOp>,
598        is_until: bool,
599    },
600    /// `Repeat` variant.
601    Repeat { count: String, body: Vec<DecodedOp> },
602    /// `Case` variant.
603    Case {
604        word: String,
605        cases: Vec<(String, Vec<DecodedOp>)>,
606    },
607    /// `CaseItem` variant.
608    CaseItem {
609        pattern: String,
610        terminator: u32,
611        body: Vec<DecodedOp>,
612    },
613    /// `If` variant.
614    If {
615        if_type: u32,
616        conditions: Vec<(Vec<DecodedOp>, Vec<DecodedOp>)>,
617        else_body: Option<Vec<DecodedOp>>,
618    },
619    /// `Cond` variant.
620    Cond { cond_type: u32, args: Vec<String> },
621    /// `Arith` variant.
622    Arith { expr: String },
623    /// `AutoFn` variant.
624    AutoFn,
625    /// `Try` variant.
626    Try {
627        try_body: Vec<DecodedOp>,
628        always_body: Vec<DecodedOp>,
629    },
630    /// `Unknown` variant.
631    Unknown { code: u32, data: u32 },
632}
633
634/// Wordcode-byte cursor for decoding `.zwc` blobs.
635/// Inverse of the C source's `bld_eprog()` (Src/parse.c:547) —
636/// walks the same WC_* dispatch tree as `gettext2()` (Src/text.c)
637/// but emits the typed `DecodedOp` AST instead of source text.
638pub struct WordcodeDecoder<'a> {
639    /// `code` field.
640    code: &'a [u32],
641    /// `strings` field.
642    strings: &'a [u8],
643    /// `strs_base` field.
644    strs_base: usize,
645    /// `pos` field.
646    pub pos: usize,
647}
648
649impl<'a> WordcodeDecoder<'a> {
650    /// `new` — see implementation.
651    pub fn new(code: &'a [u32], strings: &'a [u8], strs_base: usize) -> Self {
652        Self {
653            code,
654            strings,
655            strs_base,
656            pos: 0,
657        }
658    }
659    /// `at_end` — see implementation.
660    pub fn at_end(&self) -> bool {
661        self.pos >= self.code.len()
662    }
663    /// `peek` — see implementation.
664    pub fn peek(&self) -> Option<u32> {
665        self.code.get(self.pos).copied()
666    }
667    /// `next` — see implementation.
668    #[allow(clippy::should_implement_trait)]
669    pub fn next(&mut self) -> Option<u32> {
670        let val = self.code.get(self.pos).copied();
671        if val.is_some() {
672            self.pos += 1;
673        }
674        val
675    }
676    /// `read_string` — see implementation.
677    pub fn read_string(&mut self) -> String {
678        let wc = self.next().unwrap_or(0);
679        self.decode_string(wc)
680    }
681    /// `decode_string` — see implementation.
682    pub fn decode_string(&self, wc: u32) -> String {
683        // Zsh string encoding from ecrawstr():
684        // - c == 6 || c == 7 -> empty string
685        // - c & 2 (bit 1 set) -> short string, chars in bits 3-10, 11-18, 19-26
686        // - otherwise -> long string at strs + (c >> 2)
687
688        if wc == 6 || wc == 7 {
689            return String::new();
690        }
691
692        if (wc & 2) != 0 {
693            // Short string (1-3 chars packed in upper bits)
694            let mut s = String::new();
695            let c1 = ((wc >> 3) & 0xff) as u8;
696            let c2 = ((wc >> 11) & 0xff) as u8;
697            let c3 = ((wc >> 19) & 0xff) as u8;
698            if c1 != 0 {
699                s.push(c1 as char);
700            }
701            if c2 != 0 {
702                s.push(c2 as char);
703            }
704            if c3 != 0 {
705                s.push(c3 as char);
706            }
707            s
708        } else {
709            // Long string (offset into strs from strs_base)
710            let offset = (wc >> 2) as usize;
711            self.get_string_at(self.strs_base + offset)
712        }
713    }
714
715    fn get_string_at(&self, offset: usize) -> String {
716        if offset >= self.strings.len() {
717            return String::new();
718        }
719
720        let end = self.strings[offset..]
721            .iter()
722            .position(|&b| b == 0)
723            .map(|p| offset + p)
724            .unwrap_or(self.strings.len());
725
726        // Untokenize the zsh string - convert tokens back to shell syntax
727        let raw = &self.strings[offset..end];
728        untokenize(raw)
729    }
730
731    /// Decode the wordcode into a list of operations
732    pub fn decode(&self) -> Vec<DecodedOp> {
733        let mut decoder = WordcodeDecoder::new(self.code, self.strings, self.strs_base);
734        decoder.decode_program()
735    }
736
737    fn decode_program(&mut self) -> Vec<DecodedOp> {
738        let mut ops = Vec::new();
739
740        while let Some(wc) = self.peek() {
741            let code = wc_code(wc);
742
743            if code == WC_END {
744                self.next();
745                ops.push(DecodedOp::End);
746                break;
747            }
748
749            if let Some(op) = self.decode_next_op() {
750                ops.push(op);
751            } else {
752                break;
753            }
754        }
755
756        ops
757    }
758
759    fn decode_next_op(&mut self) -> Option<DecodedOp> {
760        let wc = self.next()?;
761        let code = wc_code(wc);
762        let data = wc_data(wc);
763
764        let op = match code {
765            WC_END => DecodedOp::End,
766            WC_LIST => self.decode_list(data),
767            WC_SUBLIST => self.decode_sublist(data),
768            WC_PIPE => self.decode_pipe(data),
769            WC_REDIR => self.decode_redir(data),
770            WC_ASSIGN => self.decode_assign(data),
771            WC_SIMPLE => self.decode_simple(data),
772            WC_TYPESET => self.decode_typeset(data),
773            WC_SUBSH => self.decode_subsh(data),
774            WC_CURSH => self.decode_cursh(data),
775            WC_TIMED => self.decode_timed(data),
776            WC_FUNCDEF => self.decode_funcdef(data),
777            WC_FOR => self.decode_for(data),
778            WC_SELECT => self.decode_select(data),
779            WC_WHILE => self.decode_while(data),
780            WC_REPEAT => self.decode_repeat(data),
781            WC_CASE => self.decode_case(data),
782            WC_IF => self.decode_if(data),
783            WC_COND => self.decode_cond(data),
784            WC_ARITH => self.decode_arith(),
785            WC_AUTOFN => DecodedOp::AutoFn,
786            WC_TRY => self.decode_try(data),
787            _ => DecodedOp::Unknown { code, data },
788        };
789
790        Some(op)
791    }
792
793    fn decode_list(&mut self, data: u32) -> DecodedOp {
794        let list_type = data & ((1 << WC_LIST_FREE) - 1);
795        let is_end = (list_type & Z_END) != 0;
796        let is_simple = (list_type & Z_SIMPLE) != 0;
797        let _skip = data >> WC_LIST_FREE;
798
799        let mut body = Vec::new();
800
801        if is_simple {
802            // Simple list just has a lineno, then the command
803            let lineno = self.next().unwrap_or(0);
804            body.push(DecodedOp::LineNo(lineno));
805        }
806
807        // Continue decoding the list contents
808        if !is_simple {
809            while let Some(wc) = self.peek() {
810                let c = wc_code(wc);
811                if c == WC_END || c == WC_LIST {
812                    break;
813                }
814                if let Some(op) = self.decode_next_op() {
815                    body.push(op);
816                } else {
817                    break;
818                }
819            }
820        }
821
822        DecodedOp::List {
823            list_type,
824            is_end,
825            ops: body,
826        }
827    }
828
829    fn decode_sublist(&mut self, data: u32) -> DecodedOp {
830        let sublist_type = data & 3;
831        let flags = data & 0x1c;
832        let negated = (flags & WC_SUBLIST_NOT) != 0;
833        let is_simple = (flags & WC_SUBLIST_SIMPLE) != 0;
834        let _skip = data >> WC_SUBLIST_FREE;
835
836        let mut body = Vec::new();
837
838        if is_simple {
839            // Simple sublist
840            let lineno = self.next().unwrap_or(0);
841            body.push(DecodedOp::LineNo(lineno));
842        }
843
844        DecodedOp::Sublist {
845            sublist_type,
846            negated,
847            ops: body,
848        }
849    }
850
851    fn decode_pipe(&mut self, data: u32) -> DecodedOp {
852        let pipe_type = data & 1;
853        let lineno = data >> 1;
854        let _is_end = pipe_type == WC_PIPE_END;
855
856        DecodedOp::Pipe {
857            lineno,
858            ops: vec![],
859        }
860    }
861
862    fn decode_redir(&mut self, data: u32) -> DecodedOp {
863        let redir_type = data & 0x1f; // REDIR_TYPE_MASK
864        let has_varid = (data & 0x20) != 0; // REDIR_VARID_MASK
865        let from_heredoc = (data & 0x40) != 0; // REDIR_FROM_HEREDOC_MASK
866
867        let fd = self.next().unwrap_or(0) as i32;
868        let target = self.read_string();
869
870        let varid = if has_varid {
871            Some(self.read_string())
872        } else {
873            None
874        };
875
876        if from_heredoc {
877            // Skip heredoc data (2 extra words)
878            self.next();
879            self.next();
880        }
881
882        DecodedOp::Redir {
883            redir_type,
884            fd,
885            target,
886            varid,
887        }
888    }
889
890    fn decode_assign(&mut self, data: u32) -> DecodedOp {
891        let is_array = (data & 1) != 0;
892        let num_elements = (data >> 2) as usize;
893
894        let name = self.read_string();
895
896        if is_array {
897            let mut values = Vec::with_capacity(num_elements);
898            for _ in 0..num_elements {
899                values.push(self.read_string());
900            }
901            DecodedOp::AssignArray { name, values }
902        } else {
903            let value = self.read_string();
904            DecodedOp::Assign { name, value }
905        }
906    }
907
908    fn decode_simple(&mut self, data: u32) -> DecodedOp {
909        let argc = data as usize;
910        let mut args = Vec::with_capacity(argc);
911        for _ in 0..argc {
912            args.push(self.read_string());
913        }
914        DecodedOp::Simple { args }
915    }
916
917    fn decode_typeset(&mut self, data: u32) -> DecodedOp {
918        let argc = data as usize;
919        let mut args = Vec::with_capacity(argc);
920        for _ in 0..argc {
921            args.push(self.read_string());
922        }
923
924        // Followed by number of assignments
925        let num_assigns = self.next().unwrap_or(0) as usize;
926        let mut assigns = Vec::with_capacity(num_assigns);
927
928        for _ in 0..num_assigns {
929            if let Some(op) = self.decode_next_op() {
930                assigns.push(op);
931            }
932        }
933
934        DecodedOp::Typeset { args, assigns }
935    }
936
937    fn decode_subsh(&mut self, data: u32) -> DecodedOp {
938        let skip = data as usize;
939        let end_pos = self.pos + skip;
940
941        let mut body = Vec::new();
942        while self.pos < end_pos && !self.at_end() {
943            if let Some(op) = self.decode_next_op() {
944                body.push(op);
945            } else {
946                break;
947            }
948        }
949
950        DecodedOp::Subsh { ops: body }
951    }
952
953    fn decode_cursh(&mut self, data: u32) -> DecodedOp {
954        let skip = data as usize;
955        let end_pos = self.pos + skip;
956
957        let mut body = Vec::new();
958        while self.pos < end_pos && !self.at_end() {
959            if let Some(op) = self.decode_next_op() {
960                body.push(op);
961            } else {
962                break;
963            }
964        }
965
966        DecodedOp::Cursh { ops: body }
967    }
968
969    fn decode_timed(&mut self, data: u32) -> DecodedOp {
970        let timed_type = data;
971        let has_pipe = timed_type == 1; // WC_TIMED_PIPE
972
973        if has_pipe {
974            // Followed by a pipe
975            if let Some(op) = self.decode_next_op() {
976                return DecodedOp::Timed {
977                    cmd: Some(Box::new(op)),
978                };
979            }
980        }
981
982        DecodedOp::Timed { cmd: None }
983    }
984
985    fn decode_funcdef(&mut self, data: u32) -> DecodedOp {
986        let skip = data as usize;
987
988        let num_names = self.next().unwrap_or(0) as usize;
989        let mut names = Vec::with_capacity(num_names);
990        for _ in 0..num_names {
991            names.push(self.read_string());
992        }
993
994        // Read function metadata
995        let _strs_offset = self.next();
996        let _strs_len = self.next();
997        let _npats = self.next();
998        let _tracing = self.next();
999
1000        // Skip the function body (we'd need a separate decoder for it)
1001        let _end_pos = self.pos + skip.saturating_sub(num_names + 5);
1002
1003        let name = names.first().cloned().unwrap_or_default();
1004
1005        DecodedOp::FuncDef { name, body: vec![] }
1006    }
1007
1008    fn decode_for(&mut self, data: u32) -> DecodedOp {
1009        let for_type = data & 3;
1010        let _skip = data >> 2;
1011
1012        match for_type {
1013            WC_FOR_COND => {
1014                let init = self.read_string();
1015                let cond = self.read_string();
1016                let step = self.read_string();
1017                DecodedOp::ForCond {
1018                    init,
1019                    cond,
1020                    step,
1021                    body: vec![],
1022                }
1023            }
1024            WC_FOR_LIST => {
1025                let var = self.read_string();
1026                let num_words = self.next().unwrap_or(0) as usize;
1027                let mut list = Vec::with_capacity(num_words);
1028                for _ in 0..num_words {
1029                    list.push(self.read_string());
1030                }
1031                DecodedOp::For {
1032                    var,
1033                    list,
1034                    body: vec![],
1035                }
1036            }
1037            _ => {
1038                // WC_FOR_PPARAM - uses positional params
1039                let var = self.read_string();
1040                DecodedOp::For {
1041                    var,
1042                    list: vec![],
1043                    body: vec![],
1044                }
1045            }
1046        }
1047    }
1048
1049    fn decode_select(&mut self, data: u32) -> DecodedOp {
1050        let select_type = data & 1;
1051        let _skip = data >> 1;
1052
1053        let var = self.read_string();
1054        let list = if select_type == 1 {
1055            // WC_SELECT_LIST
1056            let num_words = self.next().unwrap_or(0) as usize;
1057            let mut words = Vec::with_capacity(num_words);
1058            for _ in 0..num_words {
1059                words.push(self.read_string());
1060            }
1061            words
1062        } else {
1063            vec![]
1064        };
1065
1066        DecodedOp::Select {
1067            var,
1068            list,
1069            body: vec![],
1070        }
1071    }
1072
1073    fn decode_while(&mut self, data: u32) -> DecodedOp {
1074        let is_until = (data & 1) != 0;
1075        let _skip = data >> 1;
1076        DecodedOp::While {
1077            cond: vec![],
1078            body: vec![],
1079            is_until,
1080        }
1081    }
1082
1083    fn decode_repeat(&mut self, data: u32) -> DecodedOp {
1084        let _skip = data;
1085        let count = self.read_string();
1086        DecodedOp::Repeat {
1087            count,
1088            body: vec![],
1089        }
1090    }
1091
1092    fn decode_case(&mut self, data: u32) -> DecodedOp {
1093        let case_type = data & 7;
1094        let _skip = data >> WC_CASE_FREE;
1095
1096        if case_type == WC_CASE_HEAD {
1097            let word = self.read_string();
1098            DecodedOp::Case {
1099                word,
1100                cases: vec![],
1101            }
1102        } else {
1103            // Individual case patterns
1104            let pattern = self.read_string();
1105            let _npats = self.next();
1106            DecodedOp::CaseItem {
1107                pattern,
1108                terminator: case_type,
1109                body: vec![],
1110            }
1111        }
1112    }
1113
1114    fn decode_if(&mut self, data: u32) -> DecodedOp {
1115        let if_type = data & 3;
1116        let _skip = data >> 2;
1117
1118        DecodedOp::If {
1119            if_type,
1120            conditions: vec![],
1121            else_body: None,
1122        }
1123    }
1124
1125    fn decode_cond(&mut self, data: u32) -> DecodedOp {
1126        let cond_type = data & 127;
1127        let _skip = data >> 7;
1128
1129        // Decode based on condition type
1130        let args = match cond_type {
1131            // COND_NOT = 1
1132            1 => vec![],
1133            // COND_AND = 2, COND_OR = 3
1134            2 | 3 => vec![],
1135            // Binary operators have 2 args
1136            _ if cond_type >= 7 => {
1137                vec![self.read_string(), self.read_string()]
1138            }
1139            // Unary operators have 1 arg
1140            _ => {
1141                vec![self.read_string()]
1142            }
1143        };
1144
1145        DecodedOp::Cond { cond_type, args }
1146    }
1147
1148    fn decode_arith(&mut self) -> DecodedOp {
1149        let expr = self.read_string();
1150        DecodedOp::Arith { expr }
1151    }
1152
1153    fn decode_try(&mut self, data: u32) -> DecodedOp {
1154        let _skip = data;
1155        DecodedOp::Try {
1156            try_body: vec![],
1157            always_body: vec![],
1158        }
1159    }
1160}
1161
1162/// Dump the function table + header info from a `.zwc` file.
1163/// zshrs-original tooling — C zsh has no `zcompile -t` for this;
1164/// `zsh -c '. file.zwc'` is the only consumer. The `zshrs zwc`
1165/// dump path was added so test scaffolding can inspect zwc layout.
1166pub fn dump_zwc_info<P: AsRef<Path>>(path: P) -> io::Result<()> {
1167    let zwc = ZwcFile::load(&path)?;
1168
1169    println!("ZWC file: {:?}", path.as_ref());
1170    println!(
1171        "  Magic: 0x{:08x} ({})",
1172        zwc.header.magic,
1173        if zwc.header.magic == FD_MAGIC {
1174            "native"
1175        } else {
1176            "swapped"
1177        }
1178    );
1179    println!("  Version: zsh-{}", zwc.header.version);
1180    println!("  Header length: {} words", zwc.header.header_len);
1181    println!("  Wordcode size: {} words", zwc.wordcode.len());
1182    println!("  Functions: {}", zwc.functions.len());
1183
1184    for func in &zwc.functions {
1185        println!(
1186            "    {} (offset={}, len={}, npats={})",
1187            func.name, func.start, func.len, func.npats
1188        );
1189    }
1190
1191    Ok(())
1192}
1193
1194/// Dump one named function's decoded body from a `.zwc` file.
1195/// zshrs-original tooling.
1196pub fn dump_zwc_function<P: AsRef<Path>>(path: P, func_name: &str) -> io::Result<()> {
1197    let zwc = ZwcFile::load(&path)?;
1198
1199    let func = zwc.get_function(func_name).ok_or_else(|| {
1200        io::Error::new(
1201            io::ErrorKind::NotFound,
1202            format!("Function '{}' not found", func_name),
1203        )
1204    })?;
1205
1206    println!("Function: {}", func.name);
1207    println!("  Offset: {} words", func.start);
1208    println!("  Length: {} words", func.len);
1209    println!("  Patterns: {}", func.npats);
1210    println!("  Strings offset: {}", func.strs_offset);
1211
1212    // Show raw wordcode
1213    let header_words = zwc.header.header_len as usize;
1214    let start_idx = (func.start as usize).saturating_sub(header_words);
1215    let end_idx = start_idx + func.len as usize;
1216
1217    if start_idx < zwc.wordcode.len() {
1218        println!("\n  Wordcode:");
1219        let end = end_idx.min(zwc.wordcode.len());
1220        for (i, &wc) in zwc.wordcode[start_idx..end].iter().enumerate().take(50) {
1221            let code = wc_code(wc);
1222            let data = wc_data(wc);
1223            let code_name = match code {
1224                WC_END => "END",
1225                WC_LIST => "LIST",
1226                WC_SUBLIST => "SUBLIST",
1227                WC_PIPE => "PIPE",
1228                WC_REDIR => "REDIR",
1229                WC_ASSIGN => "ASSIGN",
1230                WC_SIMPLE => "SIMPLE",
1231                WC_TYPESET => "TYPESET",
1232                WC_SUBSH => "SUBSH",
1233                WC_CURSH => "CURSH",
1234                WC_TIMED => "TIMED",
1235                WC_FUNCDEF => "FUNCDEF",
1236                WC_FOR => "FOR",
1237                WC_SELECT => "SELECT",
1238                WC_WHILE => "WHILE",
1239                WC_REPEAT => "REPEAT",
1240                WC_CASE => "CASE",
1241                WC_IF => "IF",
1242                WC_COND => "COND",
1243                WC_ARITH => "ARITH",
1244                WC_AUTOFN => "AUTOFN",
1245                WC_TRY => "TRY",
1246                _ => "???",
1247            };
1248            println!("    [{:3}] 0x{:08x} = {} (data={})", i, wc, code_name, data);
1249        }
1250        if end - start_idx > 50 {
1251            println!("    ... ({} more words)", end - start_idx - 50);
1252        }
1253    }
1254
1255    // Try to decode
1256    if let Some(decoded) = zwc.decode_function(func) {
1257        println!("\n  Decoded ops:");
1258        for (i, op) in decoded.body.iter().enumerate().take(20) {
1259            println!("    [{:2}] {:?}", i, op);
1260        }
1261        if decoded.body.len() > 20 {
1262            println!("    ... ({} more ops)", decoded.body.len() - 20);
1263        }
1264    }
1265
1266    Ok(())
1267}
1268
1269/// Convert decoded ZWC ops to our shell AST for execution
1270impl DecodedOp {
1271    /// `to_shell_command` — see implementation.
1272    pub fn to_shell_command(&self) -> Option<ShellCommand> {
1273        match self {
1274            DecodedOp::Simple { args } => {
1275                if args.is_empty() {
1276                    return None;
1277                }
1278                Some(ShellCommand::Simple(SimpleCommand {
1279                    assignments: vec![],
1280                    words: args.iter().map(|s| ShellWord::Literal(s.clone())).collect(),
1281                    redirects: vec![],
1282                }))
1283            }
1284
1285            DecodedOp::Assign { name, value } => Some(ShellCommand::Simple(SimpleCommand {
1286                assignments: vec![(name.clone(), ShellWord::Literal(value.clone()), false)],
1287                words: vec![],
1288                redirects: vec![],
1289            })),
1290
1291            DecodedOp::AssignArray { name, values } => {
1292                let array_word = ShellWord::Concat(
1293                    values
1294                        .iter()
1295                        .map(|s| ShellWord::Literal(s.clone()))
1296                        .collect(),
1297                );
1298                Some(ShellCommand::Simple(SimpleCommand {
1299                    assignments: vec![(name.clone(), array_word, false)],
1300                    words: vec![],
1301                    redirects: vec![],
1302                }))
1303            }
1304
1305            DecodedOp::List { ops, .. } => {
1306                let commands: Vec<(ShellCommand, ListOp)> = ops
1307                    .iter()
1308                    .filter_map(|op| op.to_shell_command())
1309                    .map(|cmd| (cmd, ListOp::Semi))
1310                    .collect();
1311
1312                if commands.is_empty() {
1313                    None
1314                } else if commands.len() == 1 {
1315                    Some(commands.into_iter().next().unwrap().0)
1316                } else {
1317                    Some(ShellCommand::List(commands))
1318                }
1319            }
1320
1321            DecodedOp::Sublist { ops, negated, .. } => {
1322                let commands: Vec<ShellCommand> =
1323                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1324
1325                if commands.is_empty() {
1326                    None
1327                } else {
1328                    Some(ShellCommand::Pipeline(commands, *negated))
1329                }
1330            }
1331
1332            DecodedOp::Pipe { ops, .. } => {
1333                let commands: Vec<ShellCommand> =
1334                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1335
1336                if commands.is_empty() {
1337                    None
1338                } else if commands.len() == 1 {
1339                    Some(commands.into_iter().next().unwrap())
1340                } else {
1341                    Some(ShellCommand::Pipeline(commands, false))
1342                }
1343            }
1344
1345            DecodedOp::Typeset { args, assigns } => {
1346                // Typeset is like a simple command with the typeset builtin
1347                let mut words: Vec<ShellWord> =
1348                    args.iter().map(|s| ShellWord::Literal(s.clone())).collect();
1349
1350                // Add any assignments as words
1351                for assign in assigns {
1352                    if let DecodedOp::Assign { name, value } = assign {
1353                        words.push(ShellWord::Literal(format!("{}={}", name, value)));
1354                    }
1355                }
1356
1357                Some(ShellCommand::Simple(SimpleCommand {
1358                    assignments: vec![],
1359                    words,
1360                    redirects: vec![],
1361                }))
1362            }
1363
1364            DecodedOp::Subsh { ops } => {
1365                let commands: Vec<ShellCommand> =
1366                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1367                Some(ShellCommand::Compound(CompoundCommand::Subshell(commands)))
1368            }
1369
1370            DecodedOp::Cursh { ops } => {
1371                let commands: Vec<ShellCommand> =
1372                    ops.iter().filter_map(|op| op.to_shell_command()).collect();
1373                Some(ShellCommand::Compound(CompoundCommand::BraceGroup(
1374                    commands,
1375                )))
1376            }
1377
1378            DecodedOp::For { var, list, body } => {
1379                let words = if list.is_empty() {
1380                    None
1381                } else {
1382                    Some(list.iter().map(|s| ShellWord::Literal(s.clone())).collect())
1383                };
1384                let body_cmds: Vec<ShellCommand> =
1385                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1386                Some(ShellCommand::Compound(CompoundCommand::For {
1387                    var: var.clone(),
1388                    words,
1389                    body: body_cmds,
1390                }))
1391            }
1392
1393            DecodedOp::ForCond {
1394                init,
1395                cond,
1396                step,
1397                body,
1398            } => {
1399                let body_cmds: Vec<ShellCommand> =
1400                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1401                Some(ShellCommand::Compound(CompoundCommand::ForArith {
1402                    init: init.clone(),
1403                    cond: cond.clone(),
1404                    step: step.clone(),
1405                    body: body_cmds,
1406                }))
1407            }
1408
1409            DecodedOp::While {
1410                cond,
1411                body,
1412                is_until,
1413            } => {
1414                let cond_cmds: Vec<ShellCommand> =
1415                    cond.iter().filter_map(|op| op.to_shell_command()).collect();
1416                let body_cmds: Vec<ShellCommand> =
1417                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1418
1419                if *is_until {
1420                    Some(ShellCommand::Compound(CompoundCommand::Until {
1421                        condition: cond_cmds,
1422                        body: body_cmds,
1423                    }))
1424                } else {
1425                    Some(ShellCommand::Compound(CompoundCommand::While {
1426                        condition: cond_cmds,
1427                        body: body_cmds,
1428                    }))
1429                }
1430            }
1431
1432            DecodedOp::FuncDef { name, body } => {
1433                let body_cmds: Vec<ShellCommand> =
1434                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1435
1436                let func_body = if body_cmds.is_empty() {
1437                    // Empty function body - create a no-op
1438                    ShellCommand::Simple(SimpleCommand {
1439                        assignments: vec![],
1440                        words: vec![ShellWord::Literal(":".to_string())],
1441                        redirects: vec![],
1442                    })
1443                } else if body_cmds.len() == 1 {
1444                    body_cmds.into_iter().next().unwrap()
1445                } else {
1446                    ShellCommand::List(body_cmds.into_iter().map(|c| (c, ListOp::Semi)).collect())
1447                };
1448
1449                Some(ShellCommand::FunctionDef(name.clone(), Box::new(func_body)))
1450            }
1451
1452            DecodedOp::Arith { expr } => {
1453                Some(ShellCommand::Compound(CompoundCommand::Arith(expr.clone())))
1454            }
1455
1456            // Ops that don't directly translate
1457            DecodedOp::End | DecodedOp::LineNo(_) | DecodedOp::AutoFn => None,
1458
1459            DecodedOp::Redir { .. } => {
1460                // Redirections are attached to commands, not standalone
1461                None
1462            }
1463
1464            DecodedOp::If {
1465                conditions,
1466                else_body,
1467                ..
1468            } => {
1469                let cond_pairs: Vec<(Vec<ShellCommand>, Vec<ShellCommand>)> = conditions
1470                    .iter()
1471                    .map(|(c, b)| {
1472                        (
1473                            c.iter().filter_map(|op| op.to_shell_command()).collect(),
1474                            b.iter().filter_map(|op| op.to_shell_command()).collect(),
1475                        )
1476                    })
1477                    .collect();
1478                let else_part: Option<Vec<ShellCommand>> = else_body
1479                    .as_ref()
1480                    .map(|body| body.iter().filter_map(|op| op.to_shell_command()).collect());
1481                Some(ShellCommand::Compound(CompoundCommand::If {
1482                    conditions: cond_pairs,
1483                    else_part,
1484                }))
1485            }
1486
1487            DecodedOp::Case { word, cases } => {
1488                let mapped: Vec<(Vec<ShellWord>, Vec<ShellCommand>, CaseTerminator)> = cases
1489                    .iter()
1490                    .map(|(pat, body)| {
1491                        (
1492                            vec![ShellWord::Literal(pat.clone())],
1493                            body.iter().filter_map(|op| op.to_shell_command()).collect(),
1494                            CaseTerminator::Break,
1495                        )
1496                    })
1497                    .collect();
1498                Some(ShellCommand::Compound(CompoundCommand::Case {
1499                    word: ShellWord::Literal(word.clone()),
1500                    cases: mapped,
1501                }))
1502            }
1503
1504            DecodedOp::CaseItem { .. } => {
1505                // CaseItem is only meaningful as a child of Case; the Case
1506                // branch above flattens directly from the (pattern, body)
1507                // pairs the decoder builds, so a stray CaseItem at the top
1508                // level has no executable form.
1509                None
1510            }
1511
1512            DecodedOp::Repeat { count, body } => {
1513                let body_cmds: Vec<ShellCommand> =
1514                    body.iter().filter_map(|op| op.to_shell_command()).collect();
1515                Some(ShellCommand::Compound(CompoundCommand::Repeat {
1516                    count: count.clone(),
1517                    body: body_cmds,
1518                }))
1519            }
1520
1521            DecodedOp::Try {
1522                try_body,
1523                always_body,
1524            } => {
1525                let try_cmds: Vec<ShellCommand> = try_body
1526                    .iter()
1527                    .filter_map(|op| op.to_shell_command())
1528                    .collect();
1529                let always_cmds: Vec<ShellCommand> = always_body
1530                    .iter()
1531                    .filter_map(|op| op.to_shell_command())
1532                    .collect();
1533                Some(ShellCommand::Compound(CompoundCommand::Try {
1534                    try_body: try_cmds,
1535                    always_body: always_cmds,
1536                }))
1537            }
1538
1539            DecodedOp::Select { .. } => {
1540                // CompoundCommand::Select needs a var and word list; the
1541                // current DecodedOp::Select carries fields the decoder
1542                // hasn't surfaced yet (see zwc.rs:1054-1086 for the parts
1543                // we do decode). Leave unmapped until the decoder grows
1544                // those fields rather than guess at them here.
1545                None
1546            }
1547
1548            DecodedOp::Cond { .. } => {
1549                // [[ ... ]] conditional. CompoundCommand has no Cond variant —
1550                // the parser-level ZshCond shape lives only in the parse
1551                // crate. Bridging that requires a converter on the parse
1552                // side; deferred to a follow-up port.
1553                None
1554            }
1555
1556            DecodedOp::Timed { .. } => {
1557                // `time cmd` — needs ZshCommand::Time-style wrapping which
1558                // CompoundCommand doesn't model. Deferred.
1559                None
1560            }
1561
1562            DecodedOp::Unknown { .. } => None,
1563        }
1564    }
1565}
1566
1567/// Helper to convert redir type to our RedirectOp
1568#[allow(dead_code)]
1569fn redir_type_to_op(redir_type: u32) -> Option<RedirectOp> {
1570    // Zsh redirect types from zsh.h
1571    const REDIR_WRITE: u32 = 0;
1572    const REDIR_WRITENOW: u32 = 1;
1573    const REDIR_APP: u32 = 2;
1574    const REDIR_APPNOW: u32 = 3;
1575    const REDIR_ERRWRITE: u32 = 4;
1576    const REDIR_ERRWRITENOW: u32 = 5;
1577    const REDIR_ERRAPP: u32 = 6;
1578    const REDIR_ERRAPPNOW: u32 = 7;
1579    const REDIR_READWRITE: u32 = 8;
1580    const REDIR_READ: u32 = 9;
1581    const REDIR_HEREDOC: u32 = 10;
1582    const REDIR_HEREDOCDASH: u32 = 11;
1583    const REDIR_HERESTR: u32 = 12;
1584    const REDIR_MERGEIN: u32 = 13;
1585    const REDIR_MERGEOUT: u32 = 14;
1586    const REDIR_CLOSE: u32 = 15;
1587    const REDIR_INPIPE: u32 = 16;
1588    const REDIR_OUTPIPE: u32 = 17;
1589
1590    match redir_type {
1591        REDIR_WRITE | REDIR_WRITENOW => Some(RedirectOp::Write),
1592        REDIR_APP | REDIR_APPNOW => Some(RedirectOp::Append),
1593        REDIR_ERRWRITE | REDIR_ERRWRITENOW => Some(RedirectOp::WriteBoth),
1594        REDIR_ERRAPP | REDIR_ERRAPPNOW => Some(RedirectOp::AppendBoth),
1595        REDIR_READWRITE => Some(RedirectOp::ReadWrite),
1596        REDIR_READ => Some(RedirectOp::Read),
1597        REDIR_HEREDOC | REDIR_HEREDOCDASH => Some(RedirectOp::HereDoc),
1598        REDIR_HERESTR => Some(RedirectOp::HereString),
1599        REDIR_MERGEIN => Some(RedirectOp::DupRead),
1600        REDIR_MERGEOUT => Some(RedirectOp::DupWrite),
1601        REDIR_CLOSE | REDIR_INPIPE | REDIR_OUTPIPE => None, // Not directly supported
1602        _ => None,
1603    }
1604}
1605
1606impl DecodedFunction {
1607    /// Convert the decoded function to a shell function definition
1608    pub fn to_shell_function(&self) -> Option<ShellCommand> {
1609        let body_cmds: Vec<ShellCommand> = self
1610            .body
1611            .iter()
1612            .filter_map(|op| op.to_shell_command())
1613            .collect();
1614
1615        let func_body = if body_cmds.is_empty() {
1616            ShellCommand::Simple(SimpleCommand {
1617                assignments: vec![],
1618                words: vec![ShellWord::Literal(":".to_string())],
1619                redirects: vec![],
1620            })
1621        } else if body_cmds.len() == 1 {
1622            body_cmds.into_iter().next().unwrap()
1623        } else {
1624            ShellCommand::List(body_cmds.into_iter().map(|c| (c, ListOp::Semi)).collect())
1625        };
1626
1627        // Extract just the function name without the path prefix
1628        let name = self
1629            .name
1630            .rsplit('/')
1631            .next()
1632            .unwrap_or(&self.name)
1633            .to_string();
1634
1635        Some(ShellCommand::FunctionDef(name, Box::new(func_body)))
1636    }
1637}
1638
1639#[cfg(test)]
1640mod tests {
1641    use super::*;
1642
1643    #[test]
1644    fn test_wc_code() {
1645        let _g = crate::test_util::global_state_lock();
1646        assert_eq!(wc_code(WC_LIST), WC_LIST);
1647        assert_eq!(wc_code(WC_SIMPLE | (5 << WC_CODEBITS)), WC_SIMPLE);
1648    }
1649
1650    #[test]
1651    fn test_wc_data() {
1652        let _g = crate::test_util::global_state_lock();
1653        let wc = WC_SIMPLE | (42 << WC_CODEBITS);
1654        assert_eq!(wc_data(wc), 42);
1655    }
1656
1657    #[test]
1658    fn test_load_src_zwc() {
1659        let _g = crate::test_util::global_state_lock();
1660        let path = "/Users/wizard/.zinit/plugins/MenkeTechnologies---zsh-more-completions/src.zwc";
1661        if !std::path::Path::new(path).exists() {
1662            eprintln!("Skipping test - {} not found", path);
1663            return;
1664        }
1665
1666        let zwc = ZwcFile::load(path).expect("Failed to load src.zwc");
1667        println!("Loaded {} functions from src.zwc", zwc.function_count());
1668
1669        // Should have thousands of completion functions
1670        assert!(
1671            zwc.function_count() > 1000,
1672            "Expected > 1000 functions, got {}",
1673            zwc.function_count()
1674        );
1675
1676        // Check some known functions exist
1677        let funcs = zwc.list_functions();
1678        println!("First 10 functions: {:?}", &funcs[..10.min(funcs.len())]);
1679
1680        // Try to decode _ls
1681        if let Some(func) = zwc.get_function("_ls") {
1682            println!("Found _ls function");
1683            if let Some(decoded) = zwc.decode_function(func) {
1684                println!("Decoded _ls: {} ops", decoded.body.len());
1685            }
1686        }
1687    }
1688
1689    #[test]
1690    fn test_load_zshrc_zwc() {
1691        let _g = crate::test_util::global_state_lock();
1692        let home = std::env::var("HOME").unwrap_or_default();
1693        let path = format!("{}/.zshrc.zwc", home);
1694        if !std::path::Path::new(&path).exists() {
1695            eprintln!("Skipping test - {} not found", path);
1696            return;
1697        }
1698
1699        let zwc = ZwcFile::load(&path).expect("Failed to load .zshrc.zwc");
1700        println!("Loaded {} functions from .zshrc.zwc", zwc.function_count());
1701
1702        for name in zwc.list_functions() {
1703            println!("  Function: {}", name);
1704            if let Some(func) = zwc.get_function(name) {
1705                if let Some(decoded) = zwc.decode_function(func) {
1706                    println!("    Decoded: {} ops", decoded.body.len());
1707                    for (i, op) in decoded.body.iter().take(3).enumerate() {
1708                        if let Some(cmd) = op.to_shell_command() {
1709                            println!("      [{}] -> ShellCommand OK", i);
1710                        } else {
1711                            println!("      [{}] {:?}", i, op);
1712                        }
1713                    }
1714                }
1715            }
1716        }
1717    }
1718
1719    #[test]
1720    fn decoded_op_if_converts_to_compound_if() {
1721        let _g = crate::test_util::global_state_lock();
1722        let cmd_a = DecodedOp::Simple {
1723            args: vec!["true".into()],
1724        };
1725        let cmd_b = DecodedOp::Simple {
1726            args: vec!["false".into()],
1727        };
1728        let op = DecodedOp::If {
1729            if_type: 0,
1730            conditions: vec![(vec![cmd_a], vec![cmd_b])],
1731            else_body: None,
1732        };
1733        let result = op.to_shell_command();
1734        match result {
1735            Some(ShellCommand::Compound(CompoundCommand::If {
1736                conditions,
1737                else_part,
1738            })) => {
1739                assert_eq!(conditions.len(), 1);
1740                assert!(else_part.is_none());
1741                assert_eq!(conditions[0].0.len(), 1);
1742                assert_eq!(conditions[0].1.len(), 1);
1743            }
1744            other => panic!("expected If, got {:?}", other),
1745        }
1746    }
1747
1748    #[test]
1749    fn decoded_op_repeat_converts_with_count_and_body() {
1750        let _g = crate::test_util::global_state_lock();
1751        let body = DecodedOp::Simple {
1752            args: vec!["echo".into(), "hi".into()],
1753        };
1754        let op = DecodedOp::Repeat {
1755            count: "3".into(),
1756            body: vec![body],
1757        };
1758        match op.to_shell_command() {
1759            Some(ShellCommand::Compound(CompoundCommand::Repeat { count, body })) => {
1760                assert_eq!(count, "3");
1761                assert_eq!(body.len(), 1);
1762            }
1763            other => panic!("expected Repeat, got {:?}", other),
1764        }
1765    }
1766
1767    #[test]
1768    fn decoded_op_case_converts_each_pattern_branch() {
1769        let _g = crate::test_util::global_state_lock();
1770        let body_one = DecodedOp::Simple {
1771            args: vec!["echo".into(), "one".into()],
1772        };
1773        let body_two = DecodedOp::Simple {
1774            args: vec!["echo".into(), "two".into()],
1775        };
1776        let op = DecodedOp::Case {
1777            word: "$x".into(),
1778            cases: vec![("a*".into(), vec![body_one]), ("b*".into(), vec![body_two])],
1779        };
1780        match op.to_shell_command() {
1781            Some(ShellCommand::Compound(CompoundCommand::Case { cases, .. })) => {
1782                assert_eq!(cases.len(), 2);
1783                assert_eq!(cases[0].0.len(), 1);
1784                assert_eq!(cases[1].0.len(), 1);
1785            }
1786            other => panic!("expected Case, got {:?}", other),
1787        }
1788    }
1789
1790    #[test]
1791    fn decoded_op_try_converts_both_arms() {
1792        let _g = crate::test_util::global_state_lock();
1793        let try_arm = DecodedOp::Simple {
1794            args: vec!["false".into()],
1795        };
1796        let always_arm = DecodedOp::Simple {
1797            args: vec!["echo".into(), "done".into()],
1798        };
1799        let op = DecodedOp::Try {
1800            try_body: vec![try_arm],
1801            always_body: vec![always_arm],
1802        };
1803        match op.to_shell_command() {
1804            Some(ShellCommand::Compound(CompoundCommand::Try {
1805                try_body,
1806                always_body,
1807            })) => {
1808                assert_eq!(try_body.len(), 1);
1809                assert_eq!(always_body.len(), 1);
1810            }
1811            other => panic!("expected Try, got {:?}", other),
1812        }
1813    }
1814
1815    #[test]
1816    fn test_load_zshenv_zwc() {
1817        let _g = crate::test_util::global_state_lock();
1818        let home = std::env::var("HOME").unwrap_or_default();
1819        let path = format!("{}/.zshenv.zwc", home);
1820        if !std::path::Path::new(&path).exists() {
1821            eprintln!("Skipping test - {} not found", path);
1822            return;
1823        }
1824
1825        let zwc = ZwcFile::load(&path).expect("Failed to load .zshenv.zwc");
1826        println!("Loaded {} functions from .zshenv.zwc", zwc.function_count());
1827
1828        for name in zwc.list_functions() {
1829            println!("  Function: {}", name);
1830            if let Some(func) = zwc.get_function(name) {
1831                if let Some(decoded) = zwc.decode_function(func) {
1832                    println!("    Decoded: {} ops", decoded.body.len());
1833                }
1834            }
1835        }
1836    }
1837}