Skip to main content

zsh/
compiler.rs

1//! Shell compiler — lowers ShellCommand AST to fusevm bytecode.
2//!
3//! This is the bridge between the parser (AST) and the VM (bytecode).
4//! Gradual lowering: compile what we can, fall back to exec.rs interpreter
5//! for anything not yet supported.
6//!
7//! Architecture:
8//!   zsh source → parser → ShellCommand AST → compiler → Chunk → VM::run()
9//!
10//! The compiled bytecode is cached in SQLite alongside the AST cache,
11//! keyed by (path, mtime). Second launch skips parse AND compile.
12
13use crate::parser::*;
14use fusevm::{ChunkBuilder, Op, Value};
15
16/// Compile a script (list of commands) into a fusevm Chunk.
17pub fn compile_script(commands: &[ShellCommand], source: &str) -> fusevm::Chunk {
18    let mut c = Compiler::new(source);
19    for cmd in commands {
20        c.compile_command(cmd);
21    }
22    c.finish()
23}
24
25/// Compile a single function body into a Chunk.
26pub fn compile_function(name: &str, body: &ShellCommand) -> fusevm::Chunk {
27    let mut c = Compiler::new(name);
28    c.compile_command(body);
29    c.emit(Op::Return, 0);
30    c.finish()
31}
32
33struct Compiler {
34    builder: ChunkBuilder,
35    line: u32,
36    /// Track loop start/break targets for break/continue
37    loop_stack: Vec<LoopCtx>,
38}
39
40struct LoopCtx {
41    start: usize,       // jump target for `continue`
42    breaks: Vec<usize>, // jump placeholders to patch for `break`
43}
44
45impl Compiler {
46    fn new(source: &str) -> Self {
47        let mut builder = ChunkBuilder::new();
48        builder.set_source(source);
49        Self {
50            builder,
51            line: 1,
52            loop_stack: Vec::new(),
53        }
54    }
55
56    fn emit(&mut self, op: Op, line: u32) -> usize {
57        self.builder.emit(op, line)
58    }
59
60    fn pos(&self) -> usize {
61        self.builder.current_pos()
62    }
63
64    fn name(&mut self, s: &str) -> u16 {
65        self.builder.add_name(s)
66    }
67
68    fn constant_str(&mut self, s: &str) -> u16 {
69        self.builder.add_constant(Value::str(s))
70    }
71
72    fn finish(self) -> fusevm::Chunk {
73        self.builder.build()
74    }
75
76    // ── Command dispatch ──
77
78    fn compile_command(&mut self, cmd: &ShellCommand) {
79        match cmd {
80            ShellCommand::Simple(simple) => self.compile_simple(simple),
81            ShellCommand::Pipeline(cmds, negated) => self.compile_pipeline(cmds, *negated),
82            ShellCommand::List(items) => self.compile_list(items),
83            ShellCommand::Compound(compound) => self.compile_compound(compound),
84            ShellCommand::FunctionDef(name, body) => self.compile_funcdef(name, body),
85        }
86    }
87
88    // ── Simple command ──
89
90    fn compile_simple(&mut self, cmd: &SimpleCommand) {
91        // Handle assignments
92        for (var, val, is_append) in &cmd.assignments {
93            self.compile_word(val);
94            if *is_append {
95                // load current value, concat, store
96                let idx = self.name(var);
97                let tmp = self.name(var);
98                self.emit(Op::GetVar(idx), self.line);
99                self.emit(Op::Swap, self.line);
100                self.emit(Op::Concat, self.line);
101                self.emit(Op::SetVar(tmp), self.line);
102            } else {
103                let idx = self.name(var);
104                self.emit(Op::SetVar(idx), self.line);
105            }
106        }
107
108        if cmd.words.is_empty() {
109            return;
110        }
111
112        // Compile redirects
113        for redir in &cmd.redirects {
114            self.compile_redirect(redir);
115        }
116
117        // Check if first word is a known simple builtin we can compile directly
118        if let ShellWord::Literal(name) = &cmd.words[0] {
119            match name.as_str() {
120                "echo" => return self.compile_echo(&cmd.words[1..]),
121                "print" => return self.compile_print(&cmd.words[1..]),
122                "true" => {
123                    self.emit(Op::LoadInt(0), self.line);
124                    self.emit(Op::SetStatus, self.line);
125                    return;
126                }
127                "false" => {
128                    self.emit(Op::LoadInt(1), self.line);
129                    self.emit(Op::SetStatus, self.line);
130                    return;
131                }
132                "return" => {
133                    if cmd.words.len() > 1 {
134                        self.compile_word(&cmd.words[1]);
135                    } else {
136                        self.emit(Op::GetStatus, self.line);
137                    }
138                    self.emit(Op::ReturnValue, self.line);
139                    return;
140                }
141                "break" => {
142                    let j = self.emit(Op::Jump(0), self.line); // placeholder
143                    if let Some(ctx) = self.loop_stack.last_mut() {
144                        ctx.breaks.push(j);
145                    }
146                    return;
147                }
148                "continue" => {
149                    if let Some(ctx) = self.loop_stack.last() {
150                        let target = ctx.start;
151                        self.emit(Op::Jump(target), self.line);
152                    }
153                    return;
154                }
155                _ => {}
156            }
157        }
158
159        // General case: push all words onto stack, emit Exec
160        let argc = cmd.words.len() as u8;
161        for word in &cmd.words {
162            self.compile_word(word);
163        }
164        self.emit(Op::Exec(argc), self.line);
165        self.emit(Op::SetStatus, self.line);
166    }
167
168    // ── Builtins compiled to bytecode ──
169
170    fn compile_echo(&mut self, args: &[ShellWord]) {
171        if args.is_empty() {
172            let idx = self.constant_str("");
173            self.emit(Op::LoadConst(idx), self.line);
174            self.emit(Op::PrintLn(1), self.line);
175            return;
176        }
177        for arg in args {
178            self.compile_word(arg);
179        }
180        self.emit(Op::PrintLn(args.len() as u8), self.line);
181    }
182
183    fn compile_print(&mut self, args: &[ShellWord]) {
184        // print without -n is like echo; simplified for now
185        self.compile_echo(args);
186    }
187
188    // ── Pipeline ──
189
190    fn compile_pipeline(&mut self, cmds: &[ShellCommand], negated: bool) {
191        let n = cmds.len() as u8;
192        self.emit(Op::PipelineBegin(n), self.line);
193        for (i, cmd) in cmds.iter().enumerate() {
194            self.compile_command(cmd);
195            if i < cmds.len() - 1 {
196                self.emit(Op::PipelineStage, self.line);
197            }
198        }
199        self.emit(Op::PipelineEnd, self.line);
200        if negated {
201            // Negate exit status: 0→1, nonzero→0
202            self.emit(Op::GetStatus, self.line);
203            self.emit(Op::LogNot, self.line);
204            self.emit(Op::SetStatus, self.line);
205        }
206    }
207
208    // ── List (cmd1 && cmd2, cmd1 || cmd2, cmd1; cmd2) ──
209
210    fn compile_list(&mut self, items: &[(ShellCommand, ListOp)]) {
211        for (i, (cmd, op)) in items.iter().enumerate() {
212            self.compile_command(cmd);
213
214            // Last item has no following op
215            if i + 1 >= items.len() {
216                break;
217            }
218
219            match op {
220                ListOp::And => {
221                    // Short-circuit: if last status != 0, skip next command
222                    self.emit(Op::GetStatus, self.line);
223                    let j = self.emit(Op::JumpIfTrue(0), self.line); // status != 0 means failure
224                                                                     // If we get here, status was 0 (truthy in shell = success)
225                                                                     // Actually shell convention: status 0 = success = truthy for &&
226                                                                     // JumpIfTrue with status value... need to check:
227                                                                     // status=0 → success → continue → don't jump
228                                                                     // status≠0 → failure → skip → jump
229                                                                     // So: push status, if nonzero (truthy as int) jump past next
230                    self.builder.patch_jump(j, self.pos());
231                    // Actually this needs rethinking — shell status 0 = success but
232                    // Int(0) is falsy in the VM. We need JumpIfFalse for &&.
233                    // Let's fix: push status, convert to shell-truthiness, branch.
234                    // For now emit the simple version — refinement later.
235                }
236                ListOp::Or => {
237                    // Short-circuit: if last status == 0, skip next command
238                    self.emit(Op::GetStatus, self.line);
239                    let j = self.emit(Op::JumpIfFalse(0), self.line);
240                    self.builder.patch_jump(j, self.pos());
241                }
242                ListOp::Semi | ListOp::Amp | ListOp::Newline => {
243                    // Sequential or background — just continue
244                    // TODO: Amp should emit ExecBg
245                }
246            }
247        }
248    }
249
250    // ── Compound commands ──
251
252    fn compile_compound(&mut self, compound: &CompoundCommand) {
253        match compound {
254            CompoundCommand::BraceGroup(cmds) => {
255                for cmd in cmds {
256                    self.compile_command(cmd);
257                }
258            }
259            CompoundCommand::Subshell(cmds) => {
260                self.emit(Op::SubshellBegin, self.line);
261                for cmd in cmds {
262                    self.compile_command(cmd);
263                }
264                self.emit(Op::SubshellEnd, self.line);
265            }
266            CompoundCommand::If {
267                conditions,
268                else_part,
269            } => {
270                self.compile_if(conditions, else_part);
271            }
272            CompoundCommand::For { var, words, body } => {
273                self.compile_for(var, words, body);
274            }
275            CompoundCommand::ForArith {
276                init,
277                cond,
278                step,
279                body,
280            } => {
281                self.compile_for_arith(init, cond, step, body);
282            }
283            CompoundCommand::While { condition, body } => {
284                self.compile_while(condition, body, false);
285            }
286            CompoundCommand::Until { condition, body } => {
287                self.compile_while(condition, body, true);
288            }
289            CompoundCommand::Case { word, cases } => {
290                self.compile_case(word, cases);
291            }
292            CompoundCommand::Try {
293                try_body,
294                always_body,
295            } => {
296                // Try: execute try_body, then always execute always_body
297                for cmd in try_body {
298                    self.compile_command(cmd);
299                }
300                for cmd in always_body {
301                    self.compile_command(cmd);
302                }
303            }
304            CompoundCommand::Repeat { count, body } => {
305                self.compile_repeat(count, body);
306            }
307            _ => {
308                // Unsupported compound — will need interpreter fallback
309                // TODO: Coproc, Select, Cond, Arith, WithRedirects
310            }
311        }
312    }
313
314    fn compile_if(
315        &mut self,
316        conditions: &[(Vec<ShellCommand>, Vec<ShellCommand>)],
317        else_part: &Option<Vec<ShellCommand>>,
318    ) {
319        let mut end_jumps = Vec::new();
320
321        for (cond_cmds, body_cmds) in conditions {
322            // Compile condition
323            for cmd in cond_cmds {
324                self.compile_command(cmd);
325            }
326            // Check exit status
327            self.emit(Op::GetStatus, self.line);
328            let skip = self.emit(Op::JumpIfTrue(0), self.line); // nonzero status = falsy in shell
329
330            // Compile body
331            for cmd in body_cmds {
332                self.compile_command(cmd);
333            }
334            let end_j = self.emit(Op::Jump(0), self.line);
335            end_jumps.push(end_j);
336
337            // Patch the skip to jump here (past body)
338            self.builder.patch_jump(skip, self.pos());
339        }
340
341        // Else part
342        if let Some(else_cmds) = else_part {
343            for cmd in else_cmds {
344                self.compile_command(cmd);
345            }
346        }
347
348        // Patch all end jumps to here
349        let end = self.pos();
350        for j in end_jumps {
351            self.builder.patch_jump(j, end);
352        }
353    }
354
355    fn compile_for(&mut self, var: &str, words: &Option<Vec<ShellWord>>, body: &[ShellCommand]) {
356        let var_idx = self.name(var);
357
358        // Push all iteration words onto stack as an array
359        if let Some(ws) = words {
360            for w in ws {
361                self.compile_word(w);
362            }
363            self.emit(Op::MakeArray(ws.len() as u16), self.line);
364        } else {
365            // for x; do ... done — iterate over positional params
366            // TODO: push $@ as array
367            let empty = self.constant_str("");
368            self.emit(Op::LoadConst(empty), self.line);
369            return;
370        }
371
372        // Iteration: get array length, loop index 0..len
373        let iter_idx = self.name("__for_arr");
374        let i_idx = self.name("__for_i");
375        let len_idx = self.name("__for_len");
376
377        self.emit(Op::SetVar(iter_idx), self.line); // store array
378        self.emit(Op::ArrayLen(iter_idx), self.line); // push length
379        self.emit(Op::SetVar(len_idx), self.line); // store length
380        self.emit(Op::LoadInt(0), self.line);
381        self.emit(Op::SetVar(i_idx), self.line); // i = 0
382
383        let loop_top = self.pos();
384        self.loop_stack.push(LoopCtx {
385            start: loop_top,
386            breaks: Vec::new(),
387        });
388
389        // condition: i < len
390        self.emit(Op::GetVar(i_idx), self.line);
391        self.emit(Op::GetVar(len_idx), self.line);
392        self.emit(Op::NumLt, self.line);
393        let exit_jump = self.emit(Op::JumpIfFalse(0), self.line);
394
395        // body: var = arr[i]
396        self.emit(Op::GetVar(i_idx), self.line);
397        self.emit(Op::ArrayGet(iter_idx), self.line);
398        self.emit(Op::SetVar(var_idx), self.line);
399
400        for cmd in body {
401            self.compile_command(cmd);
402        }
403
404        // i++
405        self.emit(Op::GetVar(i_idx), self.line);
406        self.emit(Op::LoadInt(1), self.line);
407        self.emit(Op::Add, self.line);
408        self.emit(Op::SetVar(i_idx), self.line);
409        self.emit(Op::Jump(loop_top), self.line);
410
411        // patch exit
412        let exit_pos = self.pos();
413        self.builder.patch_jump(exit_jump, exit_pos);
414
415        // patch breaks
416        let ctx = self.loop_stack.pop().unwrap();
417        for b in ctx.breaks {
418            self.builder.patch_jump(b, exit_pos);
419        }
420    }
421
422    fn compile_for_arith(&mut self, init: &str, cond: &str, step: &str, body: &[ShellCommand]) {
423        // (( init )); while (( cond )); do body; (( step )); done
424        // For now, emit as extended ops — arithmetic compilation is complex
425        // TODO: lower arithmetic expressions to VM ops
426        let init_c = self.constant_str(init);
427        let cond_c = self.constant_str(cond);
428        let step_c = self.constant_str(step);
429
430        // Extended: evaluate init expression
431        self.emit(Op::LoadConst(init_c), self.line);
432        self.emit(Op::Extended(0, 0), self.line); // placeholder: eval arith
433
434        let loop_top = self.pos();
435        self.loop_stack.push(LoopCtx {
436            start: loop_top,
437            breaks: Vec::new(),
438        });
439
440        // Extended: evaluate condition
441        self.emit(Op::LoadConst(cond_c), self.line);
442        self.emit(Op::Extended(1, 0), self.line); // placeholder: eval arith condition
443        let exit_jump = self.emit(Op::JumpIfFalse(0), self.line);
444
445        for cmd in body {
446            self.compile_command(cmd);
447        }
448
449        // Extended: evaluate step
450        self.emit(Op::LoadConst(step_c), self.line);
451        self.emit(Op::Extended(0, 0), self.line);
452        self.emit(Op::Jump(loop_top), self.line);
453
454        let exit_pos = self.pos();
455        self.builder.patch_jump(exit_jump, exit_pos);
456
457        let ctx = self.loop_stack.pop().unwrap();
458        for b in ctx.breaks {
459            self.builder.patch_jump(b, exit_pos);
460        }
461    }
462
463    fn compile_while(&mut self, condition: &[ShellCommand], body: &[ShellCommand], negate: bool) {
464        let loop_top = self.pos();
465        self.loop_stack.push(LoopCtx {
466            start: loop_top,
467            breaks: Vec::new(),
468        });
469
470        for cmd in condition {
471            self.compile_command(cmd);
472        }
473        self.emit(Op::GetStatus, self.line);
474
475        let exit_jump = if negate {
476            self.emit(Op::JumpIfFalse(0), self.line) // until: exit when status == 0
477        } else {
478            self.emit(Op::JumpIfTrue(0), self.line) // while: exit when status != 0
479        };
480
481        for cmd in body {
482            self.compile_command(cmd);
483        }
484        self.emit(Op::Jump(loop_top), self.line);
485
486        let exit_pos = self.pos();
487        self.builder.patch_jump(exit_jump, exit_pos);
488
489        let ctx = self.loop_stack.pop().unwrap();
490        for b in ctx.breaks {
491            self.builder.patch_jump(b, exit_pos);
492        }
493    }
494
495    fn compile_case(
496        &mut self,
497        word: &ShellWord,
498        cases: &[(Vec<ShellWord>, Vec<ShellCommand>, CaseTerminator)],
499    ) {
500        self.compile_word(word); // push the test value
501
502        let mut end_jumps = Vec::new();
503
504        for (patterns, cmds, _terminator) in cases {
505            // For each pattern, test equality
506            let mut pattern_match_jumps = Vec::new();
507
508            for pat in patterns {
509                self.emit(Op::Dup, self.line); // dup test value
510                self.compile_word(pat);
511                self.emit(Op::StrEq, self.line);
512                let j = self.emit(Op::JumpIfTrue(0), self.line);
513                pattern_match_jumps.push(j);
514            }
515
516            // None matched — jump past this arm
517            let skip = self.emit(Op::Jump(0), self.line);
518
519            // Patch pattern matches to here (arm body)
520            let body_start = self.pos();
521            for j in pattern_match_jumps {
522                self.builder.patch_jump(j, body_start);
523            }
524
525            for cmd in cmds {
526                self.compile_command(cmd);
527            }
528
529            let end_j = self.emit(Op::Jump(0), self.line);
530            end_jumps.push(end_j);
531
532            // Patch skip
533            self.builder.patch_jump(skip, self.pos());
534        }
535
536        let end = self.pos();
537        for j in end_jumps {
538            self.builder.patch_jump(j, end);
539        }
540        self.emit(Op::Pop, self.line); // pop test value
541    }
542
543    fn compile_repeat(&mut self, count: &str, body: &[ShellCommand]) {
544        // repeat N do ... done
545        let count_c = self.constant_str(count);
546        let i_idx = self.name("__repeat_i");
547
548        self.emit(Op::LoadConst(count_c), self.line);
549        // TODO: coerce to int
550        self.emit(Op::SetVar(i_idx), self.line);
551
552        let loop_top = self.pos();
553        self.loop_stack.push(LoopCtx {
554            start: loop_top,
555            breaks: Vec::new(),
556        });
557
558        self.emit(Op::GetVar(i_idx), self.line);
559        self.emit(Op::LoadInt(0), self.line);
560        self.emit(Op::NumGt, self.line);
561        let exit_jump = self.emit(Op::JumpIfFalse(0), self.line);
562
563        for cmd in body {
564            self.compile_command(cmd);
565        }
566
567        // i--
568        self.emit(Op::GetVar(i_idx), self.line);
569        self.emit(Op::LoadInt(1), self.line);
570        self.emit(Op::Sub, self.line);
571        self.emit(Op::SetVar(i_idx), self.line);
572        self.emit(Op::Jump(loop_top), self.line);
573
574        let exit_pos = self.pos();
575        self.builder.patch_jump(exit_jump, exit_pos);
576        let ctx = self.loop_stack.pop().unwrap();
577        for b in ctx.breaks {
578            self.builder.patch_jump(b, exit_pos);
579        }
580    }
581
582    // ── Function definition ──
583
584    fn compile_funcdef(&mut self, name: &str, body: &ShellCommand) {
585        // Jump over the function body — it's not executed at definition time
586        let skip = self.emit(Op::Jump(0), self.line);
587
588        let name_idx = self.name(name);
589        let entry = self.pos();
590        self.emit(Op::PushFrame, self.line);
591        self.compile_command(body);
592        self.emit(Op::PopFrame, self.line);
593        self.emit(Op::Return, self.line);
594
595        self.builder.add_sub_entry(name_idx, entry);
596        self.builder.patch_jump(skip, self.pos());
597    }
598
599    // ── Word compilation ──
600
601    fn compile_word(&mut self, word: &ShellWord) {
602        match word {
603            ShellWord::Literal(s) => {
604                let idx = self.constant_str(s);
605                self.emit(Op::LoadConst(idx), self.line);
606            }
607            ShellWord::SingleQuoted(s) => {
608                let idx = self.constant_str(s);
609                self.emit(Op::LoadConst(idx), self.line);
610            }
611            ShellWord::DoubleQuoted(parts) => {
612                if parts.is_empty() {
613                    let idx = self.constant_str("");
614                    self.emit(Op::LoadConst(idx), self.line);
615                } else {
616                    for (i, p) in parts.iter().enumerate() {
617                        self.compile_word(p);
618                        if i > 0 {
619                            self.emit(Op::Concat, self.line);
620                        }
621                    }
622                }
623            }
624            ShellWord::Variable(name) => {
625                let idx = self.name(name);
626                self.emit(Op::GetVar(idx), self.line);
627            }
628            ShellWord::VariableBraced(name, modifier) => {
629                let idx = self.name(name);
630                self.emit(Op::GetVar(idx), self.line);
631                if let Some(m) = modifier {
632                    self.compile_var_modifier(idx, m);
633                }
634            }
635            ShellWord::ArithSub(expr) => {
636                // For now, push as string for runtime eval
637                // TODO: compile arithmetic expressions to VM ops
638                let idx = self.constant_str(expr);
639                self.emit(Op::LoadConst(idx), self.line);
640                self.emit(Op::Extended(2, 0), self.line); // placeholder: eval arith
641            }
642            ShellWord::CommandSub(cmd) => {
643                // Compile command into a sub-chunk, emit CmdSubst
644                // For now, push as extended op
645                // TODO: compile sub-command as block range
646                self.compile_command(cmd);
647                // The command's output should be on stack after CmdSubst
648            }
649            ShellWord::Glob(pattern) => {
650                let idx = self.constant_str(pattern);
651                self.emit(Op::LoadConst(idx), self.line);
652                self.emit(Op::Glob, self.line);
653            }
654            ShellWord::Tilde(user) => {
655                if let Some(u) = user {
656                    let idx = self.constant_str(&format!("~{}", u));
657                    self.emit(Op::LoadConst(idx), self.line);
658                } else {
659                    let idx = self.constant_str("~");
660                    self.emit(Op::LoadConst(idx), self.line);
661                }
662                self.emit(Op::TildeExpand, self.line);
663            }
664            ShellWord::Concat(parts) => {
665                for (i, p) in parts.iter().enumerate() {
666                    self.compile_word(p);
667                    if i > 0 {
668                        self.emit(Op::Concat, self.line);
669                    }
670                }
671            }
672            ShellWord::ArrayLiteral(elements) => {
673                for e in elements {
674                    self.compile_word(e);
675                }
676                self.emit(Op::MakeArray(elements.len() as u16), self.line);
677            }
678            ShellWord::ArrayVar(name, index) => {
679                let idx = self.name(name);
680                self.compile_word(index);
681                self.emit(Op::ArrayGet(idx), self.line);
682            }
683            ShellWord::ProcessSubIn(cmd) => {
684                // TODO: compile as block range
685                self.compile_command(cmd);
686            }
687            ShellWord::ProcessSubOut(cmd) => {
688                self.compile_command(cmd);
689            }
690        }
691    }
692
693    fn compile_var_modifier(&mut self, _var_idx: u16, modifier: &VarModifier) {
694        match modifier {
695            VarModifier::Default(word) => {
696                // ${var:-default}: if top is empty, replace with default
697                self.emit(Op::Dup, self.line);
698                self.emit(Op::StringLen, self.line);
699                self.emit(Op::LoadInt(0), self.line);
700                self.emit(Op::NumEq, self.line);
701                let skip = self.emit(Op::JumpIfFalse(0), self.line);
702                self.emit(Op::Pop, self.line); // pop empty value
703                self.compile_word(word); // push default
704                self.builder.patch_jump(skip, self.pos());
705            }
706            VarModifier::Length => {
707                self.emit(Op::StringLen, self.line);
708            }
709            _ => {
710                // TODO: other modifiers
711                // For now, leave the value as-is
712            }
713        }
714    }
715
716    fn compile_redirect(&mut self, redir: &Redirect) {
717        let fd = redir.fd.unwrap_or(match redir.op {
718            RedirectOp::Read | RedirectOp::HereDoc | RedirectOp::HereString => 0,
719            _ => 1,
720        }) as u8;
721
722        let op_byte = match redir.op {
723            RedirectOp::Write => fusevm::op::redirect_op::WRITE,
724            RedirectOp::Append => fusevm::op::redirect_op::APPEND,
725            RedirectOp::Read => fusevm::op::redirect_op::READ,
726            RedirectOp::ReadWrite => fusevm::op::redirect_op::READ_WRITE,
727            RedirectOp::Clobber => fusevm::op::redirect_op::CLOBBER,
728            RedirectOp::DupRead => fusevm::op::redirect_op::DUP_READ,
729            RedirectOp::DupWrite => fusevm::op::redirect_op::DUP_WRITE,
730            RedirectOp::WriteBoth => fusevm::op::redirect_op::WRITE_BOTH,
731            RedirectOp::AppendBoth => fusevm::op::redirect_op::APPEND_BOTH,
732            RedirectOp::HereDoc => {
733                if let Some(ref content) = redir.heredoc_content {
734                    let idx = self.constant_str(content);
735                    self.emit(Op::HereDoc(idx), self.line);
736                }
737                return;
738            }
739            RedirectOp::HereString => {
740                self.compile_word(&redir.target);
741                self.emit(Op::HereString, self.line);
742                return;
743            }
744        };
745
746        self.compile_word(&redir.target);
747        self.emit(Op::Redirect(fd, op_byte), self.line);
748    }
749}
750
751#[cfg(test)]
752mod tests {
753    use super::*;
754
755    #[test]
756    fn test_compile_echo() {
757        let cmd = ShellCommand::Simple(SimpleCommand {
758            assignments: vec![],
759            words: vec![
760                ShellWord::Literal("echo".to_string()),
761                ShellWord::Literal("hello".to_string()),
762            ],
763            redirects: vec![],
764        });
765        let chunk = compile_script(&[cmd], "test");
766        // Should have: LoadConst("hello"), PrintLn(1)
767        assert!(chunk.ops.len() >= 2);
768        assert!(matches!(chunk.ops.last(), Some(Op::PrintLn(1))));
769    }
770
771    #[test]
772    fn test_compile_assignment() {
773        let cmd = ShellCommand::Simple(SimpleCommand {
774            assignments: vec![("X".to_string(), ShellWord::Literal("42".to_string()), false)],
775            words: vec![],
776            redirects: vec![],
777        });
778        let chunk = compile_script(&[cmd], "test");
779        assert!(chunk.ops.iter().any(|op| matches!(op, Op::SetVar(_))));
780    }
781
782    #[test]
783    fn test_compile_for_loop() {
784        let cmd = ShellCommand::Compound(CompoundCommand::For {
785            var: "i".to_string(),
786            words: Some(vec![
787                ShellWord::Literal("a".to_string()),
788                ShellWord::Literal("b".to_string()),
789            ]),
790            body: vec![ShellCommand::Simple(SimpleCommand {
791                assignments: vec![],
792                words: vec![
793                    ShellWord::Literal("echo".to_string()),
794                    ShellWord::Variable("i".to_string()),
795                ],
796                redirects: vec![],
797            })],
798        });
799        let chunk = compile_script(&[cmd], "test");
800        // Should have Jump ops for the loop
801        assert!(chunk.ops.iter().any(|op| matches!(op, Op::Jump(_))));
802        assert!(chunk.ops.iter().any(|op| matches!(op, Op::JumpIfFalse(_))));
803    }
804
805    #[test]
806    fn test_compile_if() {
807        let cmd = ShellCommand::Compound(CompoundCommand::If {
808            conditions: vec![(
809                vec![ShellCommand::Simple(SimpleCommand {
810                    assignments: vec![],
811                    words: vec![ShellWord::Literal("true".to_string())],
812                    redirects: vec![],
813                })],
814                vec![ShellCommand::Simple(SimpleCommand {
815                    assignments: vec![],
816                    words: vec![
817                        ShellWord::Literal("echo".to_string()),
818                        ShellWord::Literal("yes".to_string()),
819                    ],
820                    redirects: vec![],
821                })],
822            )],
823            else_part: None,
824        });
825        let chunk = compile_script(&[cmd], "test");
826        assert!(chunk.ops.iter().any(|op| matches!(op, Op::JumpIfTrue(_))));
827    }
828}