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