Skip to main content

zsh/
shell_compiler.rs

1//! Shell compiler — lowers zshrs AST to fusevm bytecodes.
2//!
3//! This is the first phase of lowering. We start with arithmetic
4//! expressions ($((...))) since they're pure computation with a
5//! direct 1:1 mapping to fusevm ops.
6//!
7//! Subsequent phases will lower:
8//!   - for/while/until loops → Jump/JumpIfFalse + fused superinstructions
9//!   - shell functions → Call/Return/PushFrame/PopFrame
10//!   - simple commands → Exec
11//!   - pipelines → PipelineBegin/PipelineStage/PipelineEnd
12//!   - conditionals [[ ]] → comparison ops + TestFile
13//!   - variable expansion → GetVar + string ops
14
15use crate::parser::{CaseTerminator, CompoundCommand, CondExpr, ShellCommand, ShellWord};
16use fusevm::{ChunkBuilder, Op, Value};
17use std::collections::HashMap;
18
19// ═══════════════════════════════════════════════════════════════════════════
20// ShellCompiler — lowers ShellCommand AST → fusevm bytecodes
21// ═══════════════════════════════════════════════════════════════════════════
22
23/// Compiles shell AST to fusevm bytecodes.
24///
25/// All shell constructs are lowered to fusevm bytecodes.
26/// The shell is exclusively fusevm-executed — no tree-walker fallback.
27/// arithmetic, loops, functions.
28pub struct ShellCompiler {
29    builder: ChunkBuilder,
30    /// Variable name → slot index
31    slots: HashMap<String, u16>,
32    next_slot: u16,
33    /// Break target stack — each loop pushes its exit address placeholder
34    break_patches: Vec<Vec<usize>>,
35    /// Continue target stack — each loop pushes its continue address
36    continue_targets: Vec<usize>,
37}
38
39impl ShellCompiler {
40    pub fn new() -> Self {
41        Self {
42            builder: ChunkBuilder::new(),
43            slots: HashMap::new(),
44            next_slot: 0,
45            break_patches: Vec::new(),
46            continue_targets: Vec::new(),
47        }
48    }
49
50    /// Compile a list of shell commands into a fusevm Chunk.
51    pub fn compile(mut self, commands: &[ShellCommand]) -> fusevm::Chunk {
52        // No PushFrame for top-level script — frames are for function calls only.
53        // Top-level just executes and halts.
54        for cmd in commands {
55            self.compile_command(cmd);
56        }
57        // Return the last exit status
58        self.builder.emit(Op::GetStatus, 0);
59        self.builder.build()
60    }
61
62    fn slot_for(&mut self, name: &str) -> u16 {
63        if let Some(&slot) = self.slots.get(name) {
64            return slot;
65        }
66        let slot = self.next_slot;
67        self.next_slot += 1;
68        self.slots.insert(name.to_string(), slot);
69        slot
70    }
71
72    fn compile_command(&mut self, cmd: &ShellCommand) {
73        match cmd {
74            ShellCommand::Simple(simple) => {
75                self.compile_simple(simple);
76            }
77            ShellCommand::Compound(compound) => {
78                self.compile_compound(compound);
79            }
80            ShellCommand::Pipeline(cmds, negated) => {
81                self.compile_pipeline(cmds, *negated);
82            }
83            ShellCommand::List(items) => {
84                self.compile_list(items);
85            }
86            ShellCommand::FunctionDef(name, body) => {
87                // Register function: jump past body, record entry point
88                let skip_jump = self.builder.emit(Op::Jump(0), 0);
89                let entry_ip = self.builder.current_pos();
90                let name_idx = self.builder.add_name(name);
91                self.builder.add_sub_entry(name_idx, entry_ip);
92                self.builder.emit(Op::PushFrame, 0);
93                self.compile_command(body);
94                self.builder.emit(Op::PopFrame, 0);
95                self.builder.emit(Op::Return, 0);
96                let after = self.builder.current_pos();
97                self.builder.patch_jump(skip_jump, after);
98            }
99        }
100    }
101
102    /// Compile a simple command: assignments + words + redirects.
103    ///
104    /// Layout:
105    ///   - Assignments: SetVar for each VAR=val
106    ///   - If no words: done (bare assignment)
107    ///   - If words: push each word, emit Exec(argc)
108    ///   - Redirects: emit Redirect ops before Exec
109    fn compile_simple(&mut self, simple: &crate::parser::SimpleCommand) {
110        // Assignments: VAR=value
111        for (var, val, _is_append) in &simple.assignments {
112            self.compile_word(val);
113            let var_idx = self.builder.add_name(var);
114            self.builder.emit(Op::SetVar(var_idx), 0);
115        }
116
117        if simple.words.is_empty() {
118            return; // bare assignment, no command
119        }
120
121        // Redirects before command
122        for redir in &simple.redirects {
123            let fd = redir.fd.unwrap_or(match redir.op {
124                crate::parser::RedirectOp::Read
125                | crate::parser::RedirectOp::HereDoc
126                | crate::parser::RedirectOp::HereString
127                | crate::parser::RedirectOp::ReadWrite => 0,
128                _ => 1,
129            }) as u8;
130
131            let op_byte = match redir.op {
132                crate::parser::RedirectOp::Write => fusevm::op::redirect_op::WRITE,
133                crate::parser::RedirectOp::Append => fusevm::op::redirect_op::APPEND,
134                crate::parser::RedirectOp::Read => fusevm::op::redirect_op::READ,
135                crate::parser::RedirectOp::ReadWrite => fusevm::op::redirect_op::READ_WRITE,
136                crate::parser::RedirectOp::Clobber => fusevm::op::redirect_op::CLOBBER,
137                crate::parser::RedirectOp::DupRead => fusevm::op::redirect_op::DUP_READ,
138                crate::parser::RedirectOp::DupWrite => fusevm::op::redirect_op::DUP_WRITE,
139                crate::parser::RedirectOp::WriteBoth => fusevm::op::redirect_op::WRITE_BOTH,
140                crate::parser::RedirectOp::AppendBoth => fusevm::op::redirect_op::APPEND_BOTH,
141                crate::parser::RedirectOp::HereDoc => {
142                    // HereDoc: content goes to stdin via constant pool
143                    if let Some(ref content) = redir.heredoc_content {
144                        let idx = self.builder.add_constant(Value::str(content.as_str()));
145                        self.builder.emit(Op::HereDoc(idx), 0);
146                    }
147                    continue;
148                }
149                crate::parser::RedirectOp::HereString => {
150                    self.compile_word(&redir.target);
151                    self.builder.emit(Op::HereString, 0);
152                    continue;
153                }
154            };
155
156            self.compile_word(&redir.target);
157            self.builder.emit(Op::Redirect(fd, op_byte), 0);
158        }
159
160        // Check if first word is a literal builtin name
161        if let ShellWord::Literal(cmd_name) = &simple.words[0] {
162            if let Some(builtin_id) = fusevm::shell_builtins::builtin_id(cmd_name) {
163                // Push arguments (skip command name itself)
164                let argc = (simple.words.len() - 1) as u8;
165                for word in &simple.words[1..] {
166                    self.compile_word(word);
167                }
168                // CallBuiltin dispatches through the registered handler table
169                self.builder.emit(Op::CallBuiltin(builtin_id, argc), 0);
170                self.builder.emit(Op::SetStatus, 0);
171                return;
172            }
173        }
174
175        // External command: push all words onto stack, emit Exec
176        let argc = simple.words.len() as u8;
177        for word in &simple.words {
178            self.compile_word(word);
179        }
180
181        // Exec: pop argc words, spawn command, push exit status
182        self.builder.emit(Op::Exec(argc), 0);
183        self.builder.emit(Op::SetStatus, 0);
184    }
185
186    /// Compile a pipeline: cmd1 | cmd2 | cmd3
187    ///
188    /// Layout:
189    ///   PipelineBegin(N)
190    ///   <compile cmd1>
191    ///   PipelineStage
192    ///   <compile cmd2>
193    ///   PipelineStage
194    ///   <compile cmdN>
195    ///   PipelineEnd        ; waits for all, pushes last status
196    fn compile_pipeline(&mut self, cmds: &[ShellCommand], negated: bool) {
197        if cmds.len() == 1 {
198            // Single command, no pipe needed
199            self.compile_command(&cmds[0]);
200            if negated {
201                self.builder.emit(Op::GetStatus, 0);
202                self.builder.emit(Op::LoadInt(0), 0);
203                self.builder.emit(Op::NumEq, 0);
204                // true→0 (was success, now fail), false→1
205                let was_zero = self.builder.emit(Op::JumpIfTrue(0), 0);
206                self.builder.emit(Op::LoadInt(0), 0);
207                self.builder.emit(Op::SetStatus, 0);
208                let end = self.builder.emit(Op::Jump(0), 0);
209                let t = self.builder.current_pos();
210                self.builder.patch_jump(was_zero, t);
211                self.builder.emit(Op::LoadInt(1), 0);
212                self.builder.emit(Op::SetStatus, 0);
213                let e = self.builder.current_pos();
214                self.builder.patch_jump(end, e);
215            }
216            return;
217        }
218
219        let n = cmds.len() as u8;
220        self.builder.emit(Op::PipelineBegin(n), 0);
221
222        for (i, cmd) in cmds.iter().enumerate() {
223            self.compile_command(cmd);
224            if i < cmds.len() - 1 {
225                self.builder.emit(Op::PipelineStage, 0);
226            }
227        }
228
229        self.builder.emit(Op::PipelineEnd, 0);
230        self.builder.emit(Op::SetStatus, 0);
231
232        if negated {
233            self.builder.emit(Op::GetStatus, 0);
234            self.builder.emit(Op::LoadInt(0), 0);
235            self.builder.emit(Op::NumEq, 0);
236            let was_zero = self.builder.emit(Op::JumpIfTrue(0), 0);
237            self.builder.emit(Op::LoadInt(0), 0);
238            self.builder.emit(Op::SetStatus, 0);
239            let end = self.builder.emit(Op::Jump(0), 0);
240            let t = self.builder.current_pos();
241            self.builder.patch_jump(was_zero, t);
242            self.builder.emit(Op::LoadInt(1), 0);
243            self.builder.emit(Op::SetStatus, 0);
244            let e = self.builder.current_pos();
245            self.builder.patch_jump(end, e);
246        }
247    }
248
249    /// Compile a list: cmd1 && cmd2 || cmd3 ; cmd4 & cmd5
250    fn compile_list(&mut self, items: &[(ShellCommand, crate::parser::ListOp)]) {
251        for (i, (cmd, op)) in items.iter().enumerate() {
252            match op {
253                crate::parser::ListOp::And => {
254                    // cmd1 && cmd2: run cmd2 only if cmd1 succeeds
255                    self.compile_command(cmd);
256                    if i + 1 < items.len() {
257                        self.builder.emit(Op::GetStatus, 0);
258                        let skip = self.builder.emit(Op::JumpIfTrue(0), 0);
259                        // Status 0 = success, nonzero = skip next
260                        // JumpIfTrue skips when status > 0 (failure)
261                        self.compile_command(&items[i + 1].0);
262                        self.builder.patch_jump(skip, self.builder.current_pos());
263                    }
264                }
265                crate::parser::ListOp::Or => {
266                    // cmd1 || cmd2: run cmd2 only if cmd1 fails
267                    self.compile_command(cmd);
268                    if i + 1 < items.len() {
269                        self.builder.emit(Op::GetStatus, 0);
270                        let skip = self.builder.emit(Op::JumpIfFalse(0), 0);
271                        // JumpIfFalse skips when status == 0 (success)
272                        self.compile_command(&items[i + 1].0);
273                        self.builder.patch_jump(skip, self.builder.current_pos());
274                    }
275                }
276                crate::parser::ListOp::Semi => {
277                    // Sequential: just compile
278                    self.compile_command(cmd);
279                }
280                crate::parser::ListOp::Amp => {
281                    self.compile_command(cmd);
282                }
283                crate::parser::ListOp::Newline => {
284                    self.compile_command(cmd);
285                }
286            }
287        }
288    }
289
290    fn compile_compound(&mut self, compound: &CompoundCommand) {
291        match compound {
292            CompoundCommand::BraceGroup(cmds) => {
293                for cmd in cmds {
294                    self.compile_command(cmd);
295                }
296            }
297
298            // ── for var in words; do body; done ──
299            CompoundCommand::For { var, words, body } => {
300                // Strategy: push word list as array, iterate with index
301                //
302                // Compiled layout:
303                //   LoadInt(0)            ; i = 0
304                //   SetSlot(i_slot)
305                //   <load array len>
306                //   SetSlot(len_slot)
307                // loop_top:
308                //   GetSlot(i_slot)
309                //   GetSlot(len_slot)
310                //   NumLt                 ; i < len
311                //   JumpIfFalse(loop_exit)
312                //   <get array[i], set var>
313                //   <body>
314                // loop_continue:
315                //   PreIncSlotVoid(i_slot)
316                //   Jump(loop_top)
317                // loop_exit:
318
319                let i_slot = self.next_slot;
320                self.next_slot += 1;
321                let len_slot = self.next_slot;
322                self.next_slot += 1;
323                let var_idx = self.builder.add_name(var);
324
325                // Build the word list — count items
326                let item_count = if let Some(words) = words {
327                    words.len()
328                } else {
329                    0
330                };
331
332                // For now, store items as constants and load by index
333                if let Some(words) = words {
334                    for word in words {
335                        let s = self.word_to_string(word);
336                        let const_idx = self.builder.add_constant(Value::str(s));
337                        self.builder.emit(Op::LoadConst(const_idx), 0);
338                    }
339                    self.builder.emit(Op::MakeArray(item_count as u16), 0);
340                } else {
341                    // No words = iterate $@ (positional params)
342                    // TODO: load positional params
343                    self.builder.emit(Op::MakeArray(0), 0);
344                }
345                let arr_slot = self.next_slot;
346                self.next_slot += 1;
347                self.builder.emit(Op::SetSlot(arr_slot), 0);
348
349                // i = 0
350                self.builder.emit(Op::LoadInt(0), 0);
351                self.builder.emit(Op::SetSlot(i_slot), 0);
352
353                // len = array length
354                self.builder.emit(Op::LoadInt(item_count as i64), 0);
355                self.builder.emit(Op::SetSlot(len_slot), 0);
356
357                // loop_top:
358                let loop_top = self.builder.current_pos();
359                self.builder.emit(Op::GetSlot(i_slot), 0);
360                self.builder.emit(Op::GetSlot(len_slot), 0);
361                self.builder.emit(Op::NumLt, 0);
362                let exit_jump = self.builder.emit(Op::JumpIfFalse(0), 0);
363
364                // var = array[i] — get element from array at index i
365                self.builder.emit(Op::GetSlot(i_slot), 0);
366                self.builder.emit(Op::SlotArrayGet(arr_slot), 0);
367                self.builder.emit(Op::SetVar(var_idx), 0);
368
369                // Push break/continue targets
370                self.break_patches.push(Vec::new());
371                let continue_pos = self.builder.current_pos(); // will be patched
372                self.continue_targets.push(0); // placeholder
373
374                // body
375                for cmd in body {
376                    self.compile_command(cmd);
377                }
378
379                // loop_continue:
380                let continue_target = self.builder.current_pos();
381                // Patch continue target
382                if let Some(target) = self.continue_targets.last_mut() {
383                    *target = continue_target;
384                }
385
386                // i++; jump loop_top
387                self.builder.emit(Op::PreIncSlotVoid(i_slot), 0);
388                self.builder.emit(Op::Jump(loop_top), 0);
389
390                // loop_exit:
391                let loop_exit = self.builder.current_pos();
392                self.builder.patch_jump(exit_jump, loop_exit);
393
394                // Patch all break jumps
395                if let Some(breaks) = self.break_patches.pop() {
396                    for bp in breaks {
397                        self.builder.patch_jump(bp, loop_exit);
398                    }
399                }
400                self.continue_targets.pop();
401            }
402
403            // ── for ((init; cond; step)) do body done ──
404            CompoundCommand::ForArith {
405                init,
406                cond,
407                step,
408                body,
409            } => {
410                // Compile init expression
411                if !init.is_empty() {
412                    self.compile_arith_inline(init);
413                    self.builder.emit(Op::Pop, 0); // discard init result
414                }
415
416                // loop_top: evaluate condition
417                let loop_top = self.builder.current_pos();
418                if !cond.is_empty() {
419                    self.compile_arith_inline(cond);
420                    // cond == 0 means false in shell arithmetic
421                } else {
422                    self.builder.emit(Op::LoadTrue, 0);
423                }
424                let exit_jump = self.builder.emit(Op::JumpIfFalse(0), 0);
425
426                // Push break/continue targets
427                self.break_patches.push(Vec::new());
428                self.continue_targets.push(0);
429
430                // body
431                for cmd in body {
432                    self.compile_command(cmd);
433                }
434
435                // continue target = step expression
436                let continue_target = self.builder.current_pos();
437                if let Some(target) = self.continue_targets.last_mut() {
438                    *target = continue_target;
439                }
440
441                // step expression
442                if !step.is_empty() {
443                    self.compile_arith_inline(step);
444                    self.builder.emit(Op::Pop, 0); // discard step result
445                }
446
447                // Jump back to loop_top
448                self.builder.emit(Op::Jump(loop_top), 0);
449
450                // loop_exit:
451                let loop_exit = self.builder.current_pos();
452                self.builder.patch_jump(exit_jump, loop_exit);
453
454                if let Some(breaks) = self.break_patches.pop() {
455                    for bp in breaks {
456                        self.builder.patch_jump(bp, loop_exit);
457                    }
458                }
459                self.continue_targets.pop();
460            }
461
462            // ── while condition; do body; done ──
463            CompoundCommand::While { condition, body } => {
464                self.compile_while_loop(condition, body, false);
465            }
466
467            // ── until condition; do body; done ──
468            CompoundCommand::Until { condition, body } => {
469                self.compile_while_loop(condition, body, true);
470            }
471
472            // ── if/elif/else/fi ──
473            CompoundCommand::If {
474                conditions,
475                else_part,
476            } => {
477                let mut end_jumps = Vec::new();
478
479                for (cond_cmds, body_cmds) in conditions {
480                    // Evaluate condition — last command's exit status
481                    for cmd in cond_cmds {
482                        self.compile_command(cmd);
483                    }
484                    self.builder.emit(Op::GetStatus, 0);
485                    // Status 0 = true in shell, so jump if nonzero (false)
486                    let skip_body = self.builder.emit(Op::JumpIfTrue(0), 0);
487
488                    // Body
489                    for cmd in body_cmds {
490                        self.compile_command(cmd);
491                    }
492                    end_jumps.push(self.builder.emit(Op::Jump(0), 0));
493
494                    // Patch: skip body if condition false
495                    let after_body = self.builder.current_pos();
496                    self.builder.patch_jump(skip_body, after_body);
497                }
498
499                // else
500                if let Some(else_cmds) = else_part {
501                    for cmd in else_cmds {
502                        self.compile_command(cmd);
503                    }
504                }
505
506                // Patch all end jumps to after the entire if
507                let end = self.builder.current_pos();
508                for ej in end_jumps {
509                    self.builder.patch_jump(ej, end);
510                }
511            }
512
513            // ── repeat N; do body; done ──
514            CompoundCommand::Repeat { count, body } => {
515                // Compile count as arithmetic
516                let i_slot = self.next_slot;
517                self.next_slot += 1;
518
519                self.compile_arith_inline(count);
520                let count_slot = self.next_slot;
521                self.next_slot += 1;
522                self.builder.emit(Op::SetSlot(count_slot), 0);
523
524                // i = 0
525                self.builder.emit(Op::LoadInt(0), 0);
526                self.builder.emit(Op::SetSlot(i_slot), 0);
527
528                let loop_top = self.builder.current_pos();
529                // Try fused superinstruction
530                self.builder.emit(Op::GetSlot(i_slot), 0);
531                self.builder.emit(Op::GetSlot(count_slot), 0);
532                self.builder.emit(Op::NumLt, 0);
533                let exit_jump = self.builder.emit(Op::JumpIfFalse(0), 0);
534
535                self.break_patches.push(Vec::new());
536                self.continue_targets.push(0);
537
538                for cmd in body {
539                    self.compile_command(cmd);
540                }
541
542                let cont = self.builder.current_pos();
543                if let Some(target) = self.continue_targets.last_mut() {
544                    *target = cont;
545                }
546
547                self.builder.emit(Op::PreIncSlotVoid(i_slot), 0);
548                self.builder.emit(Op::Jump(loop_top), 0);
549
550                let loop_exit = self.builder.current_pos();
551                self.builder.patch_jump(exit_jump, loop_exit);
552
553                if let Some(breaks) = self.break_patches.pop() {
554                    for bp in breaks {
555                        self.builder.patch_jump(bp, loop_exit);
556                    }
557                }
558                self.continue_targets.pop();
559            }
560
561            // ── { try } always { always } ──
562            CompoundCommand::Try {
563                try_body,
564                always_body,
565            } => {
566                for cmd in try_body {
567                    self.compile_command(cmd);
568                }
569                for cmd in always_body {
570                    self.compile_command(cmd);
571                }
572            }
573
574            CompoundCommand::Arith(expr) => {
575                self.compile_arith_inline(expr);
576                // Set $? based on result: 0 if nonzero (true), 1 if zero (false)
577                // Shell arithmetic: (( expr )) returns 0 if expr != 0
578                self.builder.emit(Op::LoadInt(0), 0);
579                self.builder.emit(Op::NumNe, 0);
580                // Convert bool to status: true→0, false→1
581                let true_jump = self.builder.emit(Op::JumpIfTrue(0), 0);
582                self.builder.emit(Op::LoadInt(1), 0);
583                self.builder.emit(Op::SetStatus, 0);
584                let end_jump = self.builder.emit(Op::Jump(0), 0);
585                let true_target = self.builder.current_pos();
586                self.builder.patch_jump(true_jump, true_target);
587                self.builder.emit(Op::LoadInt(0), 0);
588                self.builder.emit(Op::SetStatus, 0);
589                let end = self.builder.current_pos();
590                self.builder.patch_jump(end_jump, end);
591            }
592
593            // ── case word in pattern) body ;; esac ──
594            CompoundCommand::Case { word, cases } => {
595                // Compile word to stack
596                self.compile_word(word);
597                let word_slot = self.next_slot;
598                self.next_slot += 1;
599                self.builder.emit(Op::SetSlot(word_slot), 0);
600
601                let mut end_jumps = Vec::new();
602
603                for (patterns, body, term) in cases {
604                    let _next_pattern_jumps: Vec<usize> = Vec::new();
605
606                    // Try each pattern — any match jumps to body
607                    let body_target_placeholder = self.builder.current_pos();
608                    let mut match_jumps = Vec::new();
609
610                    for pattern in patterns {
611                        self.builder.emit(Op::GetSlot(word_slot), 0);
612                        self.compile_word(pattern);
613                        self.builder.emit(Op::StrEq, 0);
614                        match_jumps.push(self.builder.emit(Op::JumpIfTrue(0), 0));
615                    }
616
617                    // No pattern matched — skip this case body
618                    let skip_body = self.builder.emit(Op::Jump(0), 0);
619
620                    // Patch match jumps to body start
621                    let body_start = self.builder.current_pos();
622                    for mj in match_jumps {
623                        self.builder.patch_jump(mj, body_start);
624                    }
625
626                    // Body
627                    for cmd in body {
628                        self.compile_command(cmd);
629                    }
630
631                    match term {
632                        CaseTerminator::Break => {
633                            end_jumps.push(self.builder.emit(Op::Jump(0), 0));
634                        }
635                        CaseTerminator::Fallthrough => {
636                            // ;& — fall through to next body without testing
637                        }
638                        CaseTerminator::Continue => {
639                            // ;;& — continue testing next patterns
640                        }
641                    }
642
643                    let after_body = self.builder.current_pos();
644                    self.builder.patch_jump(skip_body, after_body);
645                }
646
647                let end = self.builder.current_pos();
648                for ej in end_jumps {
649                    self.builder.patch_jump(ej, end);
650                }
651            }
652
653            // ── [[ conditional ]] ──
654            CompoundCommand::Cond(expr) => {
655                self.compile_cond(expr);
656                // Result is bool on stack — convert to status
657                let true_jump = self.builder.emit(Op::JumpIfTrue(0), 0);
658                self.builder.emit(Op::LoadInt(1), 0);
659                self.builder.emit(Op::SetStatus, 0);
660                let end_jump = self.builder.emit(Op::Jump(0), 0);
661                let true_target = self.builder.current_pos();
662                self.builder.patch_jump(true_jump, true_target);
663                self.builder.emit(Op::LoadInt(0), 0);
664                self.builder.emit(Op::SetStatus, 0);
665                let end = self.builder.current_pos();
666                self.builder.patch_jump(end_jump, end);
667            }
668
669            // ── subshell (...) ──
670            CompoundCommand::Subshell(cmds) => {
671                self.builder.emit(Op::SubshellBegin, 0);
672                for cmd in cmds {
673                    self.compile_command(cmd);
674                }
675                self.builder.emit(Op::SubshellEnd, 0);
676            }
677
678            // ── select var in words ──
679            CompoundCommand::Select { var, words, body } => {
680                // Simplified: iterate like for-in
681                // Simplified select — full interactive prompt via fusevm Extended ops
682                let var_slot = self.slot_for(var);
683                if let Some(words) = words {
684                    for word in words {
685                        let s = self.word_to_string(word);
686                        let const_idx = self.builder.add_constant(Value::str(s));
687                        self.builder.emit(Op::LoadConst(const_idx), 0);
688                        self.builder.emit(Op::SetSlot(var_slot), 0);
689                        for cmd in body {
690                            self.compile_command(cmd);
691                        }
692                    }
693                }
694            }
695
696            // ── coproc ──
697            CompoundCommand::Coproc { name: _, body } => {
698                // Coproc — bidirectional pipe via Extended ops
699                self.compile_command(body);
700            }
701
702            // ── cmd with redirects ──
703            CompoundCommand::WithRedirects(cmd, _redirects) => {
704                // TODO: emit Redirect ops before/after command
705                self.compile_command(cmd);
706            }
707        }
708    }
709
710    /// Compile a [[ conditional ]] expression to ops.
711    /// Pushes a bool (true/false) onto the stack.
712    fn compile_cond(&mut self, expr: &CondExpr) {
713        match expr {
714            // File tests
715            CondExpr::FileExists(w) => {
716                self.compile_word(w);
717                self.builder
718                    .emit(Op::TestFile(fusevm::op::file_test::EXISTS), 0);
719            }
720            CondExpr::FileRegular(w) => {
721                self.compile_word(w);
722                self.builder
723                    .emit(Op::TestFile(fusevm::op::file_test::IS_FILE), 0);
724            }
725            CondExpr::FileDirectory(w) => {
726                self.compile_word(w);
727                self.builder
728                    .emit(Op::TestFile(fusevm::op::file_test::IS_DIR), 0);
729            }
730            CondExpr::FileSymlink(w) => {
731                self.compile_word(w);
732                self.builder
733                    .emit(Op::TestFile(fusevm::op::file_test::IS_SYMLINK), 0);
734            }
735            CondExpr::FileReadable(w) => {
736                self.compile_word(w);
737                self.builder
738                    .emit(Op::TestFile(fusevm::op::file_test::IS_READABLE), 0);
739            }
740            CondExpr::FileWritable(w) => {
741                self.compile_word(w);
742                self.builder
743                    .emit(Op::TestFile(fusevm::op::file_test::IS_WRITABLE), 0);
744            }
745            CondExpr::FileExecutable(w) => {
746                self.compile_word(w);
747                self.builder
748                    .emit(Op::TestFile(fusevm::op::file_test::IS_EXECUTABLE), 0);
749            }
750            CondExpr::FileNonEmpty(w) => {
751                self.compile_word(w);
752                self.builder
753                    .emit(Op::TestFile(fusevm::op::file_test::IS_NONEMPTY), 0);
754            }
755
756            // String tests
757            CondExpr::StringEmpty(w) => {
758                self.compile_word(w);
759                self.builder.emit(Op::StringLen, 0);
760                self.builder.emit(Op::LoadInt(0), 0);
761                self.builder.emit(Op::NumEq, 0);
762            }
763            CondExpr::StringNonEmpty(w) => {
764                self.compile_word(w);
765                self.builder.emit(Op::StringLen, 0);
766                self.builder.emit(Op::LoadInt(0), 0);
767                self.builder.emit(Op::NumGt, 0);
768            }
769            CondExpr::StringEqual(a, b) => {
770                self.compile_word(a);
771                self.compile_word(b);
772                self.builder.emit(Op::StrEq, 0);
773            }
774            CondExpr::StringNotEqual(a, b) => {
775                self.compile_word(a);
776                self.compile_word(b);
777                self.builder.emit(Op::StrNe, 0);
778            }
779            CondExpr::StringMatch(a, b) => {
780                // =~ regex match — for now use StrEq as placeholder
781                self.compile_word(a);
782                self.compile_word(b);
783                self.builder.emit(Op::StrEq, 0);
784            }
785            CondExpr::StringLess(a, b) => {
786                self.compile_word(a);
787                self.compile_word(b);
788                self.builder.emit(Op::StrLt, 0);
789            }
790            CondExpr::StringGreater(a, b) => {
791                self.compile_word(a);
792                self.compile_word(b);
793                self.builder.emit(Op::StrGt, 0);
794            }
795
796            // Numeric comparisons
797            CondExpr::NumEqual(a, b) => {
798                self.compile_word(a);
799                self.compile_word(b);
800                self.builder.emit(Op::NumEq, 0);
801            }
802            CondExpr::NumNotEqual(a, b) => {
803                self.compile_word(a);
804                self.compile_word(b);
805                self.builder.emit(Op::NumNe, 0);
806            }
807            CondExpr::NumLess(a, b) => {
808                self.compile_word(a);
809                self.compile_word(b);
810                self.builder.emit(Op::NumLt, 0);
811            }
812            CondExpr::NumLessEqual(a, b) => {
813                self.compile_word(a);
814                self.compile_word(b);
815                self.builder.emit(Op::NumLe, 0);
816            }
817            CondExpr::NumGreater(a, b) => {
818                self.compile_word(a);
819                self.compile_word(b);
820                self.builder.emit(Op::NumGt, 0);
821            }
822            CondExpr::NumGreaterEqual(a, b) => {
823                self.compile_word(a);
824                self.compile_word(b);
825                self.builder.emit(Op::NumGe, 0);
826            }
827
828            // Logical operators
829            CondExpr::Not(inner) => {
830                self.compile_cond(inner);
831                self.builder.emit(Op::LogNot, 0);
832            }
833            CondExpr::And(a, b) => {
834                self.compile_cond(a);
835                let skip = self.builder.emit(Op::JumpIfFalseKeep(0), 0);
836                self.builder.emit(Op::Pop, 0);
837                self.compile_cond(b);
838                self.builder.patch_jump(skip, self.builder.current_pos());
839            }
840            CondExpr::Or(a, b) => {
841                self.compile_cond(a);
842                let skip = self.builder.emit(Op::JumpIfTrueKeep(0), 0);
843                self.builder.emit(Op::Pop, 0);
844                self.compile_cond(b);
845                self.builder.patch_jump(skip, self.builder.current_pos());
846            }
847        }
848    }
849
850    /// Compile a ShellWord to a value on the stack.
851    fn compile_word(&mut self, word: &ShellWord) {
852        match word {
853            ShellWord::Literal(s) => {
854                if s.contains('$') {
855                    self.compile_string_with_expansions(s);
856                } else {
857                    let idx = self.builder.add_constant(Value::str(s.as_str()));
858                    self.builder.emit(Op::LoadConst(idx), 0);
859                }
860            }
861            ShellWord::SingleQuoted(s) => {
862                let idx = self.builder.add_constant(Value::str(s.as_str()));
863                self.builder.emit(Op::LoadConst(idx), 0);
864            }
865            ShellWord::Variable(name) => {
866                let var_idx = self.builder.add_name(name);
867                self.builder.emit(Op::GetVar(var_idx), 0);
868            }
869            ShellWord::DoubleQuoted(parts) => {
870                if parts.is_empty() {
871                    let idx = self.builder.add_constant(Value::str(""));
872                    self.builder.emit(Op::LoadConst(idx), 0);
873                } else if parts.len() == 1 {
874                    self.compile_word(&parts[0]);
875                } else {
876                    self.compile_word(&parts[0]);
877                    for part in &parts[1..] {
878                        self.compile_word(part);
879                        self.builder.emit(Op::Concat, 0);
880                    }
881                }
882            }
883            ShellWord::Concat(parts) => {
884                if parts.is_empty() {
885                    let idx = self.builder.add_constant(Value::str(""));
886                    self.builder.emit(Op::LoadConst(idx), 0);
887                } else if parts.len() == 1 {
888                    self.compile_word(&parts[0]);
889                } else {
890                    self.compile_word(&parts[0]);
891                    for part in &parts[1..] {
892                        self.compile_word(part);
893                        self.builder.emit(Op::Concat, 0);
894                    }
895                }
896            }
897            // TODO: Glob, Tilde, ArrayLiteral, VariableBraced
898            _ => {
899                // Dynamic word — push empty string placeholder
900                let idx = self.builder.add_constant(Value::str(""));
901                self.builder.emit(Op::LoadConst(idx), 0);
902            }
903        }
904    }
905
906    fn compile_string_with_expansions(&mut self, s: &str) {
907        let chars: Vec<char> = s.chars().collect();
908        let mut i = 0;
909        let mut first = true;
910        while i < chars.len() {
911            if chars[i] == '$' {
912                i += 1;
913                if i >= chars.len() {
914                    let idx = self.builder.add_constant(Value::str("$"));
915                    self.builder.emit(Op::LoadConst(idx), 0);
916                    if !first {
917                        self.builder.emit(Op::Concat, 0);
918                    }
919                    first = false;
920                    continue;
921                }
922                if chars[i] == '{' {
923                    i += 1;
924                    let mut var_name = String::new();
925                    while i < chars.len() && chars[i] != '}' {
926                        var_name.push(chars[i]);
927                        i += 1;
928                    }
929                    if i < chars.len() {
930                        i += 1;
931                    }
932                    let var_idx = self.builder.add_name(&var_name);
933                    self.builder.emit(Op::GetVar(var_idx), 0);
934                    if !first {
935                        self.builder.emit(Op::Concat, 0);
936                    }
937                    first = false;
938                } else if chars[i].is_ascii_alphabetic() || chars[i] == '_' {
939                    let mut var_name = String::new();
940                    while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
941                        var_name.push(chars[i]);
942                        i += 1;
943                    }
944                    let var_idx = self.builder.add_name(&var_name);
945                    self.builder.emit(Op::GetVar(var_idx), 0);
946                    if !first {
947                        self.builder.emit(Op::Concat, 0);
948                    }
949                    first = false;
950                } else {
951                    let idx = self.builder.add_constant(Value::str("$"));
952                    self.builder.emit(Op::LoadConst(idx), 0);
953                    if !first {
954                        self.builder.emit(Op::Concat, 0);
955                    }
956                    first = false;
957                }
958            } else {
959                let mut literal = String::new();
960                while i < chars.len() && chars[i] != '$' {
961                    literal.push(chars[i]);
962                    i += 1;
963                }
964                let idx = self.builder.add_constant(Value::str(&literal));
965                self.builder.emit(Op::LoadConst(idx), 0);
966                if !first {
967                    self.builder.emit(Op::Concat, 0);
968                }
969                first = false;
970            }
971        }
972        if first {
973            let idx = self.builder.add_constant(Value::str(""));
974            self.builder.emit(Op::LoadConst(idx), 0);
975        }
976    }
977
978    /// Shared implementation for while/until loops.
979    fn compile_while_loop(
980        &mut self,
981        condition: &[ShellCommand],
982        body: &[ShellCommand],
983        is_until: bool,
984    ) {
985        let loop_top = self.builder.current_pos();
986
987        // Evaluate condition
988        for cmd in condition {
989            self.compile_command(cmd);
990        }
991        self.builder.emit(Op::GetStatus, 0);
992
993        // while: exit if status != 0 (JumpIfTrue since status>0 = failure)
994        // until: exit if status == 0 (JumpIfFalse since status 0 = success)
995        let exit_jump = if is_until {
996            self.builder.emit(Op::JumpIfFalse(0), 0)
997        } else {
998            self.builder.emit(Op::JumpIfTrue(0), 0)
999        };
1000
1001        self.break_patches.push(Vec::new());
1002        self.continue_targets.push(loop_top);
1003
1004        for cmd in body {
1005            self.compile_command(cmd);
1006        }
1007
1008        self.builder.emit(Op::Jump(loop_top), 0);
1009
1010        let loop_exit = self.builder.current_pos();
1011        self.builder.patch_jump(exit_jump, loop_exit);
1012
1013        if let Some(breaks) = self.break_patches.pop() {
1014            for bp in breaks {
1015                self.builder.patch_jump(bp, loop_exit);
1016            }
1017        }
1018        self.continue_targets.pop();
1019    }
1020
1021    /// Extract a literal string from a ShellWord (for constant folding).
1022    /// Compile an arithmetic expression inline, emitting ops directly
1023    /// into this compiler's builder. Result is left on the stack.
1024    /// Variables are mapped into the parent's slot table so `i` in
1025    /// init/cond/step/body all resolve to the same slot.
1026    fn compile_arith_inline(&mut self, expr: &str) {
1027        let mut ac = ArithCompiler::new(expr);
1028        // Share the parent's slot table
1029        ac.slots = self.slots.clone();
1030        ac.next_slot = self.next_slot;
1031        // Extract updated slots before compile() consumes ac
1032        ac.expr();
1033        let new_slots = ac.slots.clone();
1034        let new_next = ac.next_slot;
1035        let chunk = ac.builder.build();
1036        // Merge any new slots back
1037        self.slots = new_slots;
1038        self.next_slot = new_next;
1039        // Inline the computation ops (skip nothing — no PushFrame/ReturnValue wrapper)
1040        for op in &chunk.ops {
1041            self.builder.emit(op.clone(), 0);
1042        }
1043    }
1044
1045    fn word_to_string(&self, word: &ShellWord) -> String {
1046        match word {
1047            ShellWord::Literal(s) => s.clone(),
1048            ShellWord::SingleQuoted(s) => s.clone(),
1049            _ => String::new(), // dynamic words can't be const-folded
1050        }
1051    }
1052}
1053
1054// ═══════════════════════════════════════════════════════════════════════════
1055// ArithCompiler — lowers arithmetic expressions → fusevm bytecodes
1056// ═══════════════════════════════════════════════════════════════════════════
1057
1058/// Arithmetic expression compiler.
1059///
1060/// Takes a zsh arithmetic expression (the content inside $((...)))
1061/// and emits fusevm bytecodes that compute the result.
1062///
1063/// Port of MathEval from zsh/src/math.rs — same tokenizer,
1064/// but instead of evaluating, we emit ops.
1065pub struct ArithCompiler<'a> {
1066    pub input: &'a str,
1067    pub pos: usize,
1068    pub builder: ChunkBuilder,
1069    /// Variable name → slot index
1070    pub slots: HashMap<String, u16>,
1071    pub next_slot: u16,
1072}
1073
1074// Token types matching math.rs MathTok
1075#[derive(Debug, Clone, Copy, PartialEq)]
1076enum Tok {
1077    Num(i64),
1078    Float(f64),
1079    Ident,
1080    Plus,
1081    Minus,
1082    Mul,
1083    Div,
1084    Mod,
1085    Pow,
1086    BitAnd,
1087    BitOr,
1088    BitXor,
1089    BitNot,
1090    Shl,
1091    Shr,
1092    LogAnd,
1093    LogOr,
1094    LogNot,
1095    Eq,
1096    Neq,
1097    Lt,
1098    Gt,
1099    Leq,
1100    Geq,
1101    Assign,
1102    PlusAssign,
1103    MinusAssign,
1104    MulAssign,
1105    DivAssign,
1106    ModAssign,
1107    PreInc,
1108    PreDec,
1109    PostInc,
1110    PostDec,
1111    LParen,
1112    RParen,
1113    Comma,
1114    Quest,
1115    Colon,
1116    Eoi,
1117}
1118
1119impl<'a> ArithCompiler<'a> {
1120    pub fn new(input: &'a str) -> Self {
1121        Self {
1122            input,
1123            pos: 0,
1124            builder: ChunkBuilder::new(),
1125            slots: HashMap::new(),
1126            next_slot: 0,
1127        }
1128    }
1129
1130    /// Compile the arithmetic expression to fusevm bytecodes.
1131    /// Returns the compiled chunk.
1132    pub fn compile(mut self) -> fusevm::Chunk {
1133        self.builder.set_source("$((...))");
1134        self.builder.emit(Op::PushFrame, 0);
1135        self.expr();
1136        self.builder.emit(Op::ReturnValue, 0);
1137        self.builder.build()
1138    }
1139
1140    /// Get or allocate a slot for a variable name.
1141    fn slot_for(&mut self, name: &str) -> u16 {
1142        if let Some(&slot) = self.slots.get(name) {
1143            return slot;
1144        }
1145        let slot = self.next_slot;
1146        self.next_slot += 1;
1147        self.slots.insert(name.to_string(), slot);
1148        slot
1149    }
1150
1151    // ── Tokenizer ──
1152
1153    fn skip_whitespace(&mut self) {
1154        while self.pos < self.input.len() {
1155            let b = self.input.as_bytes()[self.pos];
1156            if b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' {
1157                self.pos += 1;
1158            } else {
1159                break;
1160            }
1161        }
1162    }
1163
1164    fn peek_char(&self) -> Option<u8> {
1165        self.input.as_bytes().get(self.pos).copied()
1166    }
1167
1168    fn next_char(&mut self) -> Option<u8> {
1169        let c = self.input.as_bytes().get(self.pos).copied();
1170        if c.is_some() {
1171            self.pos += 1;
1172        }
1173        c
1174    }
1175
1176    fn read_ident(&mut self) -> String {
1177        let start = self.pos;
1178        while self.pos < self.input.len() {
1179            let b = self.input.as_bytes()[self.pos];
1180            if b.is_ascii_alphanumeric() || b == b'_' {
1181                self.pos += 1;
1182            } else {
1183                break;
1184            }
1185        }
1186        self.input[start..self.pos].to_string()
1187    }
1188
1189    fn read_number(&mut self) -> Tok {
1190        let start = self.pos;
1191
1192        // Handle hex: 0x...
1193        if self.pos + 1 < self.input.len()
1194            && self.input.as_bytes()[self.pos] == b'0'
1195            && (self.input.as_bytes()[self.pos + 1] == b'x'
1196                || self.input.as_bytes()[self.pos + 1] == b'X')
1197        {
1198            self.pos += 2;
1199            while self.pos < self.input.len() && self.input.as_bytes()[self.pos].is_ascii_hexdigit()
1200            {
1201                self.pos += 1;
1202            }
1203            let val = i64::from_str_radix(&self.input[start + 2..self.pos], 16).unwrap_or(0);
1204            return Tok::Num(val);
1205        }
1206
1207        // Handle octal: 0...
1208        if self.pos + 1 < self.input.len()
1209            && self.input.as_bytes()[self.pos] == b'0'
1210            && self.input.as_bytes()[self.pos + 1].is_ascii_digit()
1211        {
1212            while self.pos < self.input.len() && self.input.as_bytes()[self.pos].is_ascii_digit() {
1213                self.pos += 1;
1214            }
1215            let val = i64::from_str_radix(&self.input[start + 1..self.pos], 8).unwrap_or(0);
1216            return Tok::Num(val);
1217        }
1218
1219        // Decimal integer or float
1220        while self.pos < self.input.len() && self.input.as_bytes()[self.pos].is_ascii_digit() {
1221            self.pos += 1;
1222        }
1223
1224        // Check for float
1225        if self.pos < self.input.len() && self.input.as_bytes()[self.pos] == b'.' {
1226            self.pos += 1;
1227            while self.pos < self.input.len() && self.input.as_bytes()[self.pos].is_ascii_digit() {
1228                self.pos += 1;
1229            }
1230            let val: f64 = self.input[start..self.pos].parse().unwrap_or(0.0);
1231            return Tok::Float(val);
1232        }
1233
1234        let val: i64 = self.input[start..self.pos].parse().unwrap_or(0);
1235        Tok::Num(val)
1236    }
1237
1238    fn next_tok(&mut self) -> (Tok, String) {
1239        self.skip_whitespace();
1240
1241        let Some(c) = self.peek_char() else {
1242            return (Tok::Eoi, String::new());
1243        };
1244
1245        match c {
1246            b'0'..=b'9' => {
1247                let tok = self.read_number();
1248                (tok, String::new())
1249            }
1250            b'a'..=b'z' | b'A'..=b'Z' | b'_' => {
1251                let name = self.read_ident();
1252                (Tok::Ident, name)
1253            }
1254            b'+' => {
1255                self.pos += 1;
1256                match self.peek_char() {
1257                    Some(b'+') => {
1258                        self.pos += 1;
1259                        (Tok::PreInc, String::new())
1260                    }
1261                    Some(b'=') => {
1262                        self.pos += 1;
1263                        (Tok::PlusAssign, String::new())
1264                    }
1265                    _ => (Tok::Plus, String::new()),
1266                }
1267            }
1268            b'-' => {
1269                self.pos += 1;
1270                match self.peek_char() {
1271                    Some(b'-') => {
1272                        self.pos += 1;
1273                        (Tok::PreDec, String::new())
1274                    }
1275                    Some(b'=') => {
1276                        self.pos += 1;
1277                        (Tok::MinusAssign, String::new())
1278                    }
1279                    _ => (Tok::Minus, String::new()),
1280                }
1281            }
1282            b'*' => {
1283                self.pos += 1;
1284                match self.peek_char() {
1285                    Some(b'*') => {
1286                        self.pos += 1;
1287                        if self.peek_char() == Some(b'=') {
1288                            self.pos += 1;
1289                            (Tok::MulAssign, String::new()) // **= as mul assign for now
1290                        } else {
1291                            (Tok::Pow, String::new())
1292                        }
1293                    }
1294                    Some(b'=') => {
1295                        self.pos += 1;
1296                        (Tok::MulAssign, String::new())
1297                    }
1298                    _ => (Tok::Mul, String::new()),
1299                }
1300            }
1301            b'/' => {
1302                self.pos += 1;
1303                if self.peek_char() == Some(b'=') {
1304                    self.pos += 1;
1305                    (Tok::DivAssign, String::new())
1306                } else {
1307                    (Tok::Div, String::new())
1308                }
1309            }
1310            b'%' => {
1311                self.pos += 1;
1312                if self.peek_char() == Some(b'=') {
1313                    self.pos += 1;
1314                    (Tok::ModAssign, String::new())
1315                } else {
1316                    (Tok::Mod, String::new())
1317                }
1318            }
1319            b'&' => {
1320                self.pos += 1;
1321                if self.peek_char() == Some(b'&') {
1322                    self.pos += 1;
1323                    (Tok::LogAnd, String::new())
1324                } else {
1325                    (Tok::BitAnd, String::new())
1326                }
1327            }
1328            b'|' => {
1329                self.pos += 1;
1330                if self.peek_char() == Some(b'|') {
1331                    self.pos += 1;
1332                    (Tok::LogOr, String::new())
1333                } else {
1334                    (Tok::BitOr, String::new())
1335                }
1336            }
1337            b'^' => {
1338                self.pos += 1;
1339                (Tok::BitXor, String::new())
1340            }
1341            b'~' => {
1342                self.pos += 1;
1343                (Tok::BitNot, String::new())
1344            }
1345            b'!' => {
1346                self.pos += 1;
1347                if self.peek_char() == Some(b'=') {
1348                    self.pos += 1;
1349                    (Tok::Neq, String::new())
1350                } else {
1351                    (Tok::LogNot, String::new())
1352                }
1353            }
1354            b'<' => {
1355                self.pos += 1;
1356                match self.peek_char() {
1357                    Some(b'<') => {
1358                        self.pos += 1;
1359                        (Tok::Shl, String::new())
1360                    }
1361                    Some(b'=') => {
1362                        self.pos += 1;
1363                        (Tok::Leq, String::new())
1364                    }
1365                    _ => (Tok::Lt, String::new()),
1366                }
1367            }
1368            b'>' => {
1369                self.pos += 1;
1370                match self.peek_char() {
1371                    Some(b'>') => {
1372                        self.pos += 1;
1373                        (Tok::Shr, String::new())
1374                    }
1375                    Some(b'=') => {
1376                        self.pos += 1;
1377                        (Tok::Geq, String::new())
1378                    }
1379                    _ => (Tok::Gt, String::new()),
1380                }
1381            }
1382            b'=' => {
1383                self.pos += 1;
1384                if self.peek_char() == Some(b'=') {
1385                    self.pos += 1;
1386                    (Tok::Eq, String::new())
1387                } else {
1388                    (Tok::Assign, String::new())
1389                }
1390            }
1391            b'(' => {
1392                self.pos += 1;
1393                (Tok::LParen, String::new())
1394            }
1395            b')' => {
1396                self.pos += 1;
1397                (Tok::RParen, String::new())
1398            }
1399            b',' => {
1400                self.pos += 1;
1401                (Tok::Comma, String::new())
1402            }
1403            b'?' => {
1404                self.pos += 1;
1405                (Tok::Quest, String::new())
1406            }
1407            b':' => {
1408                self.pos += 1;
1409                (Tok::Colon, String::new())
1410            }
1411            _ => {
1412                self.pos += 1;
1413                (Tok::Eoi, String::new())
1414            }
1415        }
1416    }
1417
1418    // ── Recursive descent → emit ops ──
1419    // Precedence climbing: comma < assign < ternary < logor < logand <
1420    // bitor < bitxor < bitand < eq < cmp < shift < add < mul < pow < unary
1421
1422    fn expr(&mut self) {
1423        self.assign_expr();
1424    }
1425
1426    fn assign_expr(&mut self) {
1427        let save_pos = self.pos;
1428
1429        // Check for assignment: ident = expr
1430        self.skip_whitespace();
1431        if let Some(c) = self.peek_char() {
1432            if c.is_ascii_alphabetic() || c == b'_' {
1433                let name = self.read_ident();
1434                self.skip_whitespace();
1435                let (tok, _) = self.peek_tok();
1436                match tok {
1437                    Tok::Assign => {
1438                        let _ = self.next_tok(); // consume =
1439                        let slot = self.slot_for(&name);
1440                        self.assign_expr();
1441                        self.builder.emit(Op::Dup, 0);
1442                        self.builder.emit(Op::SetSlot(slot), 0);
1443                        return;
1444                    }
1445                    Tok::PlusAssign
1446                    | Tok::MinusAssign
1447                    | Tok::MulAssign
1448                    | Tok::DivAssign
1449                    | Tok::ModAssign => {
1450                        let _ = self.next_tok(); // consume op=
1451                        let slot = self.slot_for(&name);
1452                        self.builder.emit(Op::GetSlot(slot), 0);
1453                        self.assign_expr();
1454                        match tok {
1455                            Tok::PlusAssign => self.builder.emit(Op::Add, 0),
1456                            Tok::MinusAssign => self.builder.emit(Op::Sub, 0),
1457                            Tok::MulAssign => self.builder.emit(Op::Mul, 0),
1458                            Tok::DivAssign => self.builder.emit(Op::Div, 0),
1459                            Tok::ModAssign => self.builder.emit(Op::Mod, 0),
1460                            _ => unreachable!(),
1461                        };
1462                        self.builder.emit(Op::Dup, 0);
1463                        self.builder.emit(Op::SetSlot(slot), 0);
1464                        return;
1465                    }
1466                    _ => {}
1467                }
1468                // Not assignment — rewind
1469                self.pos = save_pos;
1470            }
1471        }
1472
1473        self.ternary_expr();
1474    }
1475
1476    fn peek_tok(&mut self) -> (Tok, String) {
1477        let save = self.pos;
1478        let tok = self.next_tok();
1479        self.pos = save;
1480        tok
1481    }
1482
1483    fn ternary_expr(&mut self) {
1484        self.logor_expr();
1485        let (tok, _) = self.peek_tok();
1486        if tok == Tok::Quest {
1487            let _ = self.next_tok(); // consume ?
1488            let else_jump = self.builder.emit(Op::JumpIfFalse(0), 0);
1489            self.expr(); // true branch
1490            let (colon, _) = self.peek_tok();
1491            let end_jump = self.builder.emit(Op::Jump(0), 0);
1492            let else_target = self.builder.current_pos();
1493            self.builder.patch_jump(else_jump, else_target);
1494            if colon == Tok::Colon {
1495                let _ = self.next_tok(); // consume :
1496            }
1497            self.expr(); // false branch
1498            let end_target = self.builder.current_pos();
1499            self.builder.patch_jump(end_jump, end_target);
1500        }
1501    }
1502
1503    fn logor_expr(&mut self) {
1504        self.logand_expr();
1505        loop {
1506            let (tok, _) = self.peek_tok();
1507            if tok == Tok::LogOr {
1508                let _ = self.next_tok();
1509                let skip = self.builder.emit(Op::JumpIfTrueKeep(0), 0);
1510                self.builder.emit(Op::Pop, 0);
1511                self.logand_expr();
1512                self.builder.patch_jump(skip, self.builder.current_pos());
1513            } else {
1514                break;
1515            }
1516        }
1517    }
1518
1519    fn logand_expr(&mut self) {
1520        self.bitor_expr();
1521        loop {
1522            let (tok, _) = self.peek_tok();
1523            if tok == Tok::LogAnd {
1524                let _ = self.next_tok();
1525                let skip = self.builder.emit(Op::JumpIfFalseKeep(0), 0);
1526                self.builder.emit(Op::Pop, 0);
1527                self.bitor_expr();
1528                self.builder.patch_jump(skip, self.builder.current_pos());
1529            } else {
1530                break;
1531            }
1532        }
1533    }
1534
1535    fn bitor_expr(&mut self) {
1536        self.bitxor_expr();
1537        loop {
1538            let (tok, _) = self.peek_tok();
1539            if tok == Tok::BitOr {
1540                let _ = self.next_tok();
1541                self.bitxor_expr();
1542                self.builder.emit(Op::BitOr, 0);
1543            } else {
1544                break;
1545            }
1546        }
1547    }
1548
1549    fn bitxor_expr(&mut self) {
1550        self.bitand_expr();
1551        loop {
1552            let (tok, _) = self.peek_tok();
1553            if tok == Tok::BitXor {
1554                let _ = self.next_tok();
1555                self.bitand_expr();
1556                self.builder.emit(Op::BitXor, 0);
1557            } else {
1558                break;
1559            }
1560        }
1561    }
1562
1563    fn bitand_expr(&mut self) {
1564        self.equality_expr();
1565        loop {
1566            let (tok, _) = self.peek_tok();
1567            if tok == Tok::BitAnd {
1568                let _ = self.next_tok();
1569                self.equality_expr();
1570                self.builder.emit(Op::BitAnd, 0);
1571            } else {
1572                break;
1573            }
1574        }
1575    }
1576
1577    fn equality_expr(&mut self) {
1578        self.comparison_expr();
1579        loop {
1580            let (tok, _) = self.peek_tok();
1581            match tok {
1582                Tok::Eq => {
1583                    let _ = self.next_tok();
1584                    self.comparison_expr();
1585                    self.builder.emit(Op::NumEq, 0);
1586                }
1587                Tok::Neq => {
1588                    let _ = self.next_tok();
1589                    self.comparison_expr();
1590                    self.builder.emit(Op::NumNe, 0);
1591                }
1592                _ => break,
1593            }
1594        }
1595    }
1596
1597    fn comparison_expr(&mut self) {
1598        self.shift_expr();
1599        loop {
1600            let (tok, _) = self.peek_tok();
1601            match tok {
1602                Tok::Lt => {
1603                    let _ = self.next_tok();
1604                    self.shift_expr();
1605                    self.builder.emit(Op::NumLt, 0);
1606                }
1607                Tok::Gt => {
1608                    let _ = self.next_tok();
1609                    self.shift_expr();
1610                    self.builder.emit(Op::NumGt, 0);
1611                }
1612                Tok::Leq => {
1613                    let _ = self.next_tok();
1614                    self.shift_expr();
1615                    self.builder.emit(Op::NumLe, 0);
1616                }
1617                Tok::Geq => {
1618                    let _ = self.next_tok();
1619                    self.shift_expr();
1620                    self.builder.emit(Op::NumGe, 0);
1621                }
1622                _ => break,
1623            }
1624        }
1625    }
1626
1627    fn shift_expr(&mut self) {
1628        self.add_expr();
1629        loop {
1630            let (tok, _) = self.peek_tok();
1631            match tok {
1632                Tok::Shl => {
1633                    let _ = self.next_tok();
1634                    self.add_expr();
1635                    self.builder.emit(Op::Shl, 0);
1636                }
1637                Tok::Shr => {
1638                    let _ = self.next_tok();
1639                    self.add_expr();
1640                    self.builder.emit(Op::Shr, 0);
1641                }
1642                _ => break,
1643            }
1644        }
1645    }
1646
1647    fn add_expr(&mut self) {
1648        self.mul_expr();
1649        loop {
1650            let (tok, _) = self.peek_tok();
1651            match tok {
1652                Tok::Plus => {
1653                    let _ = self.next_tok();
1654                    self.mul_expr();
1655                    self.builder.emit(Op::Add, 0);
1656                }
1657                Tok::Minus => {
1658                    let _ = self.next_tok();
1659                    self.mul_expr();
1660                    self.builder.emit(Op::Sub, 0);
1661                }
1662                _ => break,
1663            }
1664        }
1665    }
1666
1667    fn mul_expr(&mut self) {
1668        self.pow_expr();
1669        loop {
1670            let (tok, _) = self.peek_tok();
1671            match tok {
1672                Tok::Mul => {
1673                    let _ = self.next_tok();
1674                    self.pow_expr();
1675                    self.builder.emit(Op::Mul, 0);
1676                }
1677                Tok::Div => {
1678                    let _ = self.next_tok();
1679                    self.pow_expr();
1680                    self.builder.emit(Op::Div, 0);
1681                }
1682                Tok::Mod => {
1683                    let _ = self.next_tok();
1684                    self.pow_expr();
1685                    self.builder.emit(Op::Mod, 0);
1686                }
1687                _ => break,
1688            }
1689        }
1690    }
1691
1692    fn pow_expr(&mut self) {
1693        self.unary_expr();
1694        let (tok, _) = self.peek_tok();
1695        if tok == Tok::Pow {
1696            let _ = self.next_tok();
1697            self.pow_expr(); // right-associative
1698            self.builder.emit(Op::Pow, 0);
1699        }
1700    }
1701
1702    fn unary_expr(&mut self) {
1703        let (tok, name) = self.peek_tok();
1704        match tok {
1705            Tok::Minus => {
1706                let _ = self.next_tok();
1707                self.unary_expr();
1708                self.builder.emit(Op::Negate, 0);
1709            }
1710            Tok::Plus => {
1711                let _ = self.next_tok();
1712                self.unary_expr();
1713                // unary + is a no-op on numbers
1714            }
1715            Tok::LogNot => {
1716                let _ = self.next_tok();
1717                self.unary_expr();
1718                self.builder.emit(Op::LogNot, 0);
1719            }
1720            Tok::BitNot => {
1721                let _ = self.next_tok();
1722                self.unary_expr();
1723                self.builder.emit(Op::BitNot, 0);
1724            }
1725            Tok::PreInc => {
1726                let _ = self.next_tok();
1727                // Next token must be identifier
1728                let (_, var_name) = self.next_tok();
1729                let slot = self.slot_for(&var_name);
1730                self.builder.emit(Op::PreIncSlot(slot), 0);
1731            }
1732            Tok::PreDec => {
1733                let _ = self.next_tok();
1734                let (_, var_name) = self.next_tok();
1735                let slot = self.slot_for(&var_name);
1736                self.builder.emit(Op::GetSlot(slot), 0);
1737                self.builder.emit(Op::Dec, 0);
1738                self.builder.emit(Op::Dup, 0);
1739                self.builder.emit(Op::SetSlot(slot), 0);
1740            }
1741            _ => self.primary_expr(),
1742        }
1743    }
1744
1745    fn primary_expr(&mut self) {
1746        let (tok, name) = self.next_tok();
1747        match tok {
1748            Tok::Num(n) => {
1749                self.builder.emit(Op::LoadInt(n), 0);
1750            }
1751            Tok::Float(f) => {
1752                self.builder.emit(Op::LoadFloat(f), 0);
1753            }
1754            Tok::Ident => {
1755                let slot = self.slot_for(&name);
1756                self.builder.emit(Op::GetSlot(slot), 0);
1757
1758                // Check for postfix ++ / --
1759                let (post_tok, _) = self.peek_tok();
1760                match post_tok {
1761                    Tok::PreInc => {
1762                        // Reused as PostInc here
1763                        let _ = self.next_tok();
1764                        self.builder.emit(Op::Dup, 0); // keep old value
1765                        self.builder.emit(Op::Inc, 0);
1766                        self.builder.emit(Op::SetSlot(slot), 0);
1767                        // old value remains on stack (postfix semantics)
1768                    }
1769                    Tok::PreDec => {
1770                        let _ = self.next_tok();
1771                        self.builder.emit(Op::Dup, 0);
1772                        self.builder.emit(Op::Dec, 0);
1773                        self.builder.emit(Op::SetSlot(slot), 0);
1774                    }
1775                    _ => {}
1776                }
1777            }
1778            Tok::LParen => {
1779                self.expr();
1780                let _ = self.next_tok(); // consume RParen
1781            }
1782            _ => {
1783                // Unexpected token — push 0
1784                self.builder.emit(Op::LoadInt(0), 0);
1785            }
1786        }
1787    }
1788}
1789
1790#[cfg(test)]
1791mod tests {
1792    use super::*;
1793    use fusevm::{VMResult, VM};
1794
1795    fn eval(expr: &str) -> i64 {
1796        let compiler = ArithCompiler::new(expr);
1797        let chunk = compiler.compile();
1798        let mut vm = VM::new(chunk);
1799        match vm.run() {
1800            VMResult::Ok(Value::Int(n)) => n,
1801            VMResult::Ok(Value::Bool(b)) => b as i64,
1802            VMResult::Ok(Value::Float(f)) => f as i64,
1803            VMResult::Ok(v) => v.to_int(),
1804            other => panic!("expected value, got {:?}", other),
1805        }
1806    }
1807
1808    fn eval_float(expr: &str) -> f64 {
1809        let compiler = ArithCompiler::new(expr);
1810        let chunk = compiler.compile();
1811        let mut vm = VM::new(chunk);
1812        match vm.run() {
1813            VMResult::Ok(v) => v.to_float(),
1814            other => panic!("expected value, got {:?}", other),
1815        }
1816    }
1817
1818    #[test]
1819    fn test_basic_arithmetic() {
1820        assert_eq!(eval("2 + 3"), 5);
1821        assert_eq!(eval("10 - 4"), 6);
1822        assert_eq!(eval("6 * 7"), 42);
1823        assert_eq!(eval("100 / 4"), 25);
1824        assert_eq!(eval("17 % 5"), 2);
1825    }
1826
1827    #[test]
1828    fn test_precedence() {
1829        assert_eq!(eval("2 + 3 * 4"), 14);
1830        assert_eq!(eval("(2 + 3) * 4"), 20);
1831        assert_eq!(eval("2 * 3 + 4 * 5"), 26);
1832        assert_eq!(eval("10 - 2 * 3"), 4);
1833    }
1834
1835    #[test]
1836    fn test_power() {
1837        assert_eq!(eval("2 ** 10"), 1024);
1838        assert_eq!(eval("3 ** 3"), 27);
1839    }
1840
1841    #[test]
1842    fn test_unary() {
1843        assert_eq!(eval("-5"), -5);
1844        assert_eq!(eval("-(-3)"), 3);
1845        assert_eq!(eval("!0"), 1);
1846        assert_eq!(eval("!1"), 0);
1847        assert_eq!(eval("~0"), -1);
1848    }
1849
1850    #[test]
1851    fn test_comparison() {
1852        assert_eq!(eval("3 < 5"), 1);
1853        assert_eq!(eval("5 < 3"), 0);
1854        assert_eq!(eval("3 <= 3"), 1);
1855        assert_eq!(eval("3 == 3"), 1);
1856        assert_eq!(eval("3 != 4"), 1);
1857        assert_eq!(eval("5 > 3"), 1);
1858        assert_eq!(eval("5 >= 5"), 1);
1859    }
1860
1861    #[test]
1862    fn test_bitwise() {
1863        assert_eq!(eval("0xFF & 0x0F"), 0x0F);
1864        assert_eq!(eval("0xF0 | 0x0F"), 0xFF);
1865        assert_eq!(eval("0xFF ^ 0x0F"), 0xF0);
1866        assert_eq!(eval("1 << 10"), 1024);
1867        assert_eq!(eval("1024 >> 5"), 32);
1868    }
1869
1870    #[test]
1871    fn test_logical_short_circuit() {
1872        // zsh arithmetic: && returns last evaluated operand
1873        assert_eq!(eval("1 && 2"), 2); // truthy && truthy → right operand
1874        assert_eq!(eval("0 && 2"), 0); // falsy short-circuits → left operand
1875        assert_eq!(eval("0 || 5"), 5); // falsy || truthy → right operand
1876        assert_eq!(eval("1 || 0"), 1); // truthy short-circuits → left operand
1877    }
1878
1879    #[test]
1880    fn test_ternary() {
1881        assert_eq!(eval("1 ? 42 : 99"), 42);
1882        assert_eq!(eval("0 ? 42 : 99"), 99);
1883        assert_eq!(eval("(3 > 2) ? 10 : 20"), 10);
1884    }
1885
1886    #[test]
1887    fn test_assignment() {
1888        assert_eq!(eval("x = 5"), 5);
1889        assert_eq!(eval("x = 5 + 3"), 8);
1890    }
1891
1892    #[test]
1893    fn test_hex_octal() {
1894        assert_eq!(eval("0xFF"), 255);
1895        assert_eq!(eval("0x10"), 16);
1896        assert_eq!(eval("010"), 8); // octal
1897    }
1898
1899    #[test]
1900    fn test_complex_expression() {
1901        // (5 + 3) * 2 - 10 / 5
1902        assert_eq!(eval("(5 + 3) * 2 - 10 / 5"), 14);
1903        // Nested ternary
1904        assert_eq!(eval("1 ? (0 ? 1 : 2) : 3"), 2);
1905    }
1906
1907    #[test]
1908    fn test_float() {
1909        assert!((eval_float("3.14 * 2.0") - 6.28).abs() < 0.001);
1910    }
1911
1912    // ── ShellCompiler tests ──
1913
1914    fn run_shell(commands: &[ShellCommand]) -> i64 {
1915        let compiler = ShellCompiler::new();
1916        let chunk = compiler.compile(commands);
1917        let mut vm = VM::new(chunk);
1918        match vm.run() {
1919            VMResult::Ok(v) => v.to_int(),
1920            other => panic!("VM error: {:?}", other),
1921        }
1922    }
1923
1924    #[test]
1925    fn test_for_arith_sum() {
1926        use crate::parser::CompoundCommand;
1927        // for ((i=0; i<10; i++)) { (( sum = sum + i )) }
1928        let cmd = ShellCommand::Compound(CompoundCommand::ForArith {
1929            init: "i = 0".to_string(),
1930            cond: "i < 10".to_string(),
1931            step: "i++".to_string(),
1932            body: vec![ShellCommand::Compound(CompoundCommand::Arith(
1933                "sum = sum + i".to_string(),
1934            ))],
1935        });
1936        let compiler = ShellCompiler::new();
1937        let chunk = compiler.compile(&[cmd]);
1938
1939        // Debug: print compiled ops
1940        for (i, op) in chunk.ops.iter().enumerate() {
1941            eprintln!("{:3}: {:?}", i, op);
1942        }
1943
1944        // Just verify the compiled ops look correct
1945        // The init should set slot for 'i', cond should compare, step should increment
1946        let has_set_slot = chunk.ops.iter().any(|op| matches!(op, Op::SetSlot(_)));
1947        let has_jump = chunk.ops.iter().any(|op| matches!(op, Op::Jump(_)));
1948        let has_jump_if_false = chunk.ops.iter().any(|op| matches!(op, Op::JumpIfFalse(_)));
1949        assert!(has_set_slot, "missing SetSlot for loop variable");
1950        assert!(has_jump, "missing Jump for loop backedge");
1951        assert!(has_jump_if_false, "missing JumpIfFalse for loop exit");
1952    }
1953
1954    #[test]
1955    fn test_arith_compound_status() {
1956        use crate::parser::CompoundCommand;
1957        // (( 5 > 3 )) → exit status 0 (true)
1958        let cmd = ShellCommand::Compound(CompoundCommand::Arith("5 > 3".to_string()));
1959        let compiler = ShellCompiler::new();
1960        let chunk = compiler.compile(&[cmd]);
1961        let mut vm = VM::new(chunk);
1962        let _ = vm.run();
1963        assert_eq!(vm.last_status, 0); // success
1964
1965        // (( 0 )) → exit status 1 (false)
1966        let cmd = ShellCommand::Compound(CompoundCommand::Arith("0".to_string()));
1967        let compiler = ShellCompiler::new();
1968        let chunk = compiler.compile(&[cmd]);
1969        let mut vm = VM::new(chunk);
1970        let _ = vm.run();
1971        assert_eq!(vm.last_status, 1); // failure
1972    }
1973
1974    #[test]
1975    fn test_if_arith() {
1976        use crate::parser::CompoundCommand;
1977        // if (( 1 )); then (( result = 42 )); fi
1978        let cmd = ShellCommand::Compound(CompoundCommand::If {
1979            conditions: vec![(
1980                vec![ShellCommand::Compound(CompoundCommand::Arith(
1981                    "1".to_string(),
1982                ))],
1983                vec![ShellCommand::Compound(CompoundCommand::Arith(
1984                    "result = 42".to_string(),
1985                ))],
1986            )],
1987            else_part: None,
1988        });
1989        let compiler = ShellCompiler::new();
1990        let chunk = compiler.compile(&[cmd]);
1991        let mut vm = VM::new(chunk);
1992        let _ = vm.run();
1993        assert_eq!(vm.last_status, 0);
1994    }
1995
1996    #[test]
1997    fn test_repeat_loop() {
1998        use crate::parser::CompoundCommand;
1999        let cmd = ShellCommand::Compound(CompoundCommand::Repeat {
2000            count: "5".to_string(),
2001            body: vec![ShellCommand::Compound(CompoundCommand::Arith(
2002                "count = count + 1".to_string(),
2003            ))],
2004        });
2005        let compiler = ShellCompiler::new();
2006        let chunk = compiler.compile(&[cmd]);
2007        let mut vm = VM::new(chunk);
2008        let _ = vm.run();
2009        assert_eq!(vm.last_status, 0);
2010    }
2011
2012    #[test]
2013    fn test_simple_command_compiles() {
2014        use crate::parser::SimpleCommand;
2015        // echo hello world → CallBuiltin(BUILTIN_ECHO, 2) since echo is a builtin
2016        let cmd = ShellCommand::Simple(SimpleCommand {
2017            assignments: vec![],
2018            words: vec![
2019                ShellWord::Literal("echo".to_string()),
2020                ShellWord::Literal("hello".to_string()),
2021                ShellWord::Literal("world".to_string()),
2022            ],
2023            redirects: vec![],
2024        });
2025        let compiler = ShellCompiler::new();
2026        let chunk = compiler.compile(&[cmd]);
2027        // echo is a builtin, should emit CallBuiltin not Exec
2028        let has_builtin = chunk
2029            .ops
2030            .iter()
2031            .any(|op| matches!(op, Op::CallBuiltin(2, 2))); // BUILTIN_ECHO=2, 2 args
2032        assert!(
2033            has_builtin,
2034            "expected CallBuiltin(2, 2) for 'echo hello world', got: {:?}",
2035            chunk.ops
2036        );
2037    }
2038
2039    #[test]
2040    fn test_external_command_compiles() {
2041        use crate::parser::SimpleCommand;
2042        // ls -la → Exec(2) since ls is NOT a builtin
2043        let cmd = ShellCommand::Simple(SimpleCommand {
2044            assignments: vec![],
2045            words: vec![
2046                ShellWord::Literal("ls".to_string()),
2047                ShellWord::Literal("-la".to_string()),
2048            ],
2049            redirects: vec![],
2050        });
2051        let compiler = ShellCompiler::new();
2052        let chunk = compiler.compile(&[cmd]);
2053        let has_exec = chunk.ops.iter().any(|op| matches!(op, Op::Exec(2)));
2054        assert!(
2055            has_exec,
2056            "expected Exec(2) for 'ls -la', got: {:?}",
2057            chunk.ops
2058        );
2059    }
2060
2061    #[test]
2062    fn test_assignment_compiles() {
2063        use crate::parser::SimpleCommand;
2064        // X=42 (bare assignment, no command)
2065        let cmd = ShellCommand::Simple(SimpleCommand {
2066            assignments: vec![("X".to_string(), ShellWord::Literal("42".to_string()), false)],
2067            words: vec![],
2068            redirects: vec![],
2069        });
2070        let compiler = ShellCompiler::new();
2071        let chunk = compiler.compile(&[cmd]);
2072        let has_set = chunk.ops.iter().any(|op| matches!(op, Op::SetVar(_)));
2073        assert!(has_set, "expected SetVar for assignment");
2074    }
2075
2076    #[test]
2077    fn test_pipeline_compiles() {
2078        use crate::parser::SimpleCommand;
2079        // ls | grep foo → PipelineBegin(2) ... PipelineEnd
2080        let cmds = vec![
2081            ShellCommand::Simple(SimpleCommand {
2082                assignments: vec![],
2083                words: vec![ShellWord::Literal("ls".to_string())],
2084                redirects: vec![],
2085            }),
2086            ShellCommand::Simple(SimpleCommand {
2087                assignments: vec![],
2088                words: vec![
2089                    ShellWord::Literal("grep".to_string()),
2090                    ShellWord::Literal("foo".to_string()),
2091                ],
2092                redirects: vec![],
2093            }),
2094        ];
2095        let cmd = ShellCommand::Pipeline(cmds, false);
2096        let compiler = ShellCompiler::new();
2097        let chunk = compiler.compile(&[cmd]);
2098        let has_begin = chunk
2099            .ops
2100            .iter()
2101            .any(|op| matches!(op, Op::PipelineBegin(2)));
2102        let has_end = chunk.ops.iter().any(|op| matches!(op, Op::PipelineEnd));
2103        let has_stage = chunk.ops.iter().any(|op| matches!(op, Op::PipelineStage));
2104        assert!(has_begin, "expected PipelineBegin(2)");
2105        assert!(has_stage, "expected PipelineStage");
2106        assert!(has_end, "expected PipelineEnd");
2107    }
2108
2109    #[test]
2110    fn test_redirect_compiles() {
2111        use crate::parser::{Redirect, RedirectOp, SimpleCommand};
2112        // echo hi > /tmp/out
2113        let cmd = ShellCommand::Simple(SimpleCommand {
2114            assignments: vec![],
2115            words: vec![
2116                ShellWord::Literal("echo".to_string()),
2117                ShellWord::Literal("hi".to_string()),
2118            ],
2119            redirects: vec![Redirect {
2120                fd: None,
2121                op: RedirectOp::Write,
2122                target: ShellWord::Literal("/tmp/out".to_string()),
2123                heredoc_content: None,
2124                fd_var: None,
2125            }],
2126        });
2127        let compiler = ShellCompiler::new();
2128        let chunk = compiler.compile(&[cmd]);
2129        let has_redirect = chunk.ops.iter().any(|op| matches!(op, Op::Redirect(1, 0))); // fd=1, WRITE=0
2130        assert!(has_redirect, "expected Redirect(1, 0) for > /tmp/out");
2131    }
2132
2133    #[test]
2134    fn test_heredoc_compiles() {
2135        use crate::parser::{Redirect, RedirectOp, SimpleCommand};
2136        // cat <<EOF\nhello\nEOF
2137        let cmd = ShellCommand::Simple(SimpleCommand {
2138            assignments: vec![],
2139            words: vec![ShellWord::Literal("cat".to_string())],
2140            redirects: vec![Redirect {
2141                fd: None,
2142                op: RedirectOp::HereDoc,
2143                target: ShellWord::Literal("EOF".to_string()),
2144                heredoc_content: Some("hello\n".to_string()),
2145                fd_var: None,
2146            }],
2147        });
2148        let compiler = ShellCompiler::new();
2149        let chunk = compiler.compile(&[cmd]);
2150        let has_heredoc = chunk.ops.iter().any(|op| matches!(op, Op::HereDoc(_)));
2151        assert!(has_heredoc, "expected HereDoc op");
2152    }
2153
2154    #[test]
2155    fn test_case_compiles() {
2156        use crate::parser::CompoundCommand;
2157        // case x in a) ;; b) ;; esac
2158        let cmd = ShellCommand::Compound(CompoundCommand::Case {
2159            word: ShellWord::Literal("hello".to_string()),
2160            cases: vec![
2161                (
2162                    vec![ShellWord::Literal("hello".to_string())],
2163                    vec![ShellCommand::Compound(CompoundCommand::Arith(
2164                        "result = 1".to_string(),
2165                    ))],
2166                    CaseTerminator::Break,
2167                ),
2168                (
2169                    vec![ShellWord::Literal("world".to_string())],
2170                    vec![ShellCommand::Compound(CompoundCommand::Arith(
2171                        "result = 2".to_string(),
2172                    ))],
2173                    CaseTerminator::Break,
2174                ),
2175            ],
2176        });
2177        let compiler = ShellCompiler::new();
2178        let chunk = compiler.compile(&[cmd]);
2179        // Should have StrEq for pattern matching
2180        let has_streq = chunk.ops.iter().any(|op| matches!(op, Op::StrEq));
2181        assert!(has_streq, "expected StrEq for case pattern");
2182    }
2183
2184    #[test]
2185    fn test_cond_file_test() {
2186        use crate::parser::CompoundCommand;
2187        // [[ -f /etc/passwd ]]
2188        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::FileRegular(
2189            ShellWord::Literal("/etc/passwd".to_string()),
2190        )));
2191        let compiler = ShellCompiler::new();
2192        let chunk = compiler.compile(&[cmd]);
2193        let has_test = chunk.ops.iter().any(|op| matches!(op, Op::TestFile(0))); // IS_FILE = 0
2194        assert!(has_test, "expected TestFile(IS_FILE)");
2195    }
2196
2197    #[test]
2198    fn test_cond_string_compare() {
2199        use crate::parser::CompoundCommand;
2200        // [[ "abc" == "abc" ]]
2201        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::StringEqual(
2202            ShellWord::Literal("abc".to_string()),
2203            ShellWord::Literal("abc".to_string()),
2204        )));
2205        let compiler = ShellCompiler::new();
2206        let chunk = compiler.compile(&[cmd]);
2207        let has_streq = chunk.ops.iter().any(|op| matches!(op, Op::StrEq));
2208        assert!(has_streq, "expected StrEq for string comparison");
2209    }
2210
2211    #[test]
2212    fn test_cond_logical() {
2213        use crate::parser::CompoundCommand;
2214        // [[ -f /etc/passwd && -d /tmp ]]
2215        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::And(
2216            Box::new(CondExpr::FileRegular(ShellWord::Literal(
2217                "/etc/passwd".to_string(),
2218            ))),
2219            Box::new(CondExpr::FileDirectory(ShellWord::Literal(
2220                "/tmp".to_string(),
2221            ))),
2222        )));
2223        let compiler = ShellCompiler::new();
2224        let chunk = compiler.compile(&[cmd]);
2225        let has_short_circuit = chunk
2226            .ops
2227            .iter()
2228            .any(|op| matches!(op, Op::JumpIfFalseKeep(_)));
2229        assert!(has_short_circuit, "expected short-circuit && in [[ ]]");
2230    }
2231
2232    #[test]
2233    fn test_list_and_or() {
2234        use crate::parser::{ListOp, SimpleCommand};
2235        // true && echo yes
2236        let items = vec![
2237            (
2238                ShellCommand::Compound(CompoundCommand::Arith("1".to_string())),
2239                ListOp::And,
2240            ),
2241            (
2242                ShellCommand::Simple(SimpleCommand {
2243                    assignments: vec![],
2244                    words: vec![
2245                        ShellWord::Literal("echo".to_string()),
2246                        ShellWord::Literal("yes".to_string()),
2247                    ],
2248                    redirects: vec![],
2249                }),
2250                ListOp::Semi,
2251            ),
2252        ];
2253        let cmd = ShellCommand::List(items);
2254        let compiler = ShellCompiler::new();
2255        let chunk = compiler.compile(&[cmd]);
2256        let has_get_status = chunk.ops.iter().any(|op| matches!(op, Op::GetStatus));
2257        assert!(has_get_status, "expected GetStatus for && list");
2258    }
2259
2260    #[test]
2261    fn test_function_def_compiles() {
2262        // myfunc() { (( x = 42 )) }
2263        let cmd = ShellCommand::FunctionDef(
2264            "myfunc".to_string(),
2265            Box::new(ShellCommand::Compound(CompoundCommand::Arith(
2266                "x = 42".to_string(),
2267            ))),
2268        );
2269        let compiler = ShellCompiler::new();
2270        let chunk = compiler.compile(&[cmd]);
2271        assert!(
2272            !chunk.sub_entries.is_empty(),
2273            "expected sub entry for function"
2274        );
2275        let has_return = chunk.ops.iter().any(|op| matches!(op, Op::Return));
2276        assert!(has_return, "expected Return in function body");
2277    }
2278
2279    // ═══════════════════════════════════════════════════════════════════
2280    // Execution tests — actually run compiled bytecodes on fusevm
2281    // ═══════════════════════════════════════════════════════════════════
2282
2283    /// Helper: compile and run shell commands, return VM
2284    fn compile_and_run(commands: &[ShellCommand]) -> VM {
2285        let compiler = ShellCompiler::new();
2286        let chunk = compiler.compile(commands);
2287        let mut vm = VM::new(chunk);
2288        let _ = vm.run();
2289        vm
2290    }
2291
2292    #[test]
2293    fn test_exec_file_test_exists() {
2294        use crate::parser::CompoundCommand;
2295        // [[ -e /tmp ]] → status 0
2296        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::FileExists(
2297            ShellWord::Literal("/tmp".to_string()),
2298        )));
2299        let vm = compile_and_run(&[cmd]);
2300        assert_eq!(vm.last_status, 0, "/tmp should exist");
2301    }
2302
2303    #[test]
2304    fn test_exec_file_test_not_exists() {
2305        use crate::parser::CompoundCommand;
2306        // [[ -e /nonexistent_path_xyz ]] → status 1
2307        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::FileExists(
2308            ShellWord::Literal("/nonexistent_path_xyz".to_string()),
2309        )));
2310        let vm = compile_and_run(&[cmd]);
2311        assert_eq!(vm.last_status, 1, "/nonexistent should not exist");
2312    }
2313
2314    #[test]
2315    fn test_exec_file_is_dir() {
2316        use crate::parser::CompoundCommand;
2317        // [[ -d /tmp ]] → status 0
2318        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::FileDirectory(
2319            ShellWord::Literal("/tmp".to_string()),
2320        )));
2321        let vm = compile_and_run(&[cmd]);
2322        assert_eq!(vm.last_status, 0, "/tmp should be a directory");
2323    }
2324
2325    #[test]
2326    fn test_exec_file_is_regular() {
2327        use crate::parser::CompoundCommand;
2328        // [[ -f /etc/hosts ]] → status 0 (exists on macOS/Linux)
2329        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::FileRegular(
2330            ShellWord::Literal("/etc/hosts".to_string()),
2331        )));
2332        let vm = compile_and_run(&[cmd]);
2333        assert_eq!(vm.last_status, 0, "/etc/hosts should be a regular file");
2334    }
2335
2336    #[test]
2337    fn test_exec_string_equal() {
2338        use crate::parser::CompoundCommand;
2339        // [[ "abc" == "abc" ]] → status 0
2340        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::StringEqual(
2341            ShellWord::Literal("abc".to_string()),
2342            ShellWord::Literal("abc".to_string()),
2343        )));
2344        let vm = compile_and_run(&[cmd]);
2345        assert_eq!(vm.last_status, 0);
2346    }
2347
2348    #[test]
2349    fn test_exec_string_not_equal() {
2350        use crate::parser::CompoundCommand;
2351        // [[ "abc" == "xyz" ]] → status 1
2352        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::StringEqual(
2353            ShellWord::Literal("abc".to_string()),
2354            ShellWord::Literal("xyz".to_string()),
2355        )));
2356        let vm = compile_and_run(&[cmd]);
2357        assert_eq!(vm.last_status, 1);
2358    }
2359
2360    #[test]
2361    fn test_exec_string_empty() {
2362        use crate::parser::CompoundCommand;
2363        // [[ -z "" ]] → status 0
2364        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::StringEmpty(
2365            ShellWord::Literal("".to_string()),
2366        )));
2367        let vm = compile_and_run(&[cmd]);
2368        assert_eq!(vm.last_status, 0);
2369
2370        // [[ -z "notempty" ]] → status 1
2371        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::StringEmpty(
2372            ShellWord::Literal("notempty".to_string()),
2373        )));
2374        let vm = compile_and_run(&[cmd]);
2375        assert_eq!(vm.last_status, 1);
2376    }
2377
2378    #[test]
2379    fn test_exec_cond_and() {
2380        use crate::parser::CompoundCommand;
2381        // [[ -d /tmp && -e /tmp ]] → status 0
2382        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::And(
2383            Box::new(CondExpr::FileDirectory(ShellWord::Literal(
2384                "/tmp".to_string(),
2385            ))),
2386            Box::new(CondExpr::FileExists(ShellWord::Literal("/tmp".to_string()))),
2387        )));
2388        let vm = compile_and_run(&[cmd]);
2389        assert_eq!(vm.last_status, 0);
2390    }
2391
2392    #[test]
2393    fn test_exec_cond_and_short_circuit() {
2394        use crate::parser::CompoundCommand;
2395        // [[ -f /nonexistent && -d /tmp ]] → status 1 (short-circuits on first)
2396        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::And(
2397            Box::new(CondExpr::FileRegular(ShellWord::Literal(
2398                "/nonexistent".to_string(),
2399            ))),
2400            Box::new(CondExpr::FileDirectory(ShellWord::Literal(
2401                "/tmp".to_string(),
2402            ))),
2403        )));
2404        let vm = compile_and_run(&[cmd]);
2405        assert_eq!(vm.last_status, 1);
2406    }
2407
2408    #[test]
2409    fn test_exec_cond_or() {
2410        use crate::parser::CompoundCommand;
2411        // [[ -f /nonexistent || -d /tmp ]] → status 0 (second is true)
2412        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::Or(
2413            Box::new(CondExpr::FileRegular(ShellWord::Literal(
2414                "/nonexistent".to_string(),
2415            ))),
2416            Box::new(CondExpr::FileDirectory(ShellWord::Literal(
2417                "/tmp".to_string(),
2418            ))),
2419        )));
2420        let vm = compile_and_run(&[cmd]);
2421        assert_eq!(vm.last_status, 0);
2422    }
2423
2424    #[test]
2425    fn test_exec_cond_not() {
2426        use crate::parser::CompoundCommand;
2427        // [[ ! -f /nonexistent ]] → status 0
2428        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::Not(Box::new(
2429            CondExpr::FileRegular(ShellWord::Literal("/nonexistent".to_string())),
2430        ))));
2431        let vm = compile_and_run(&[cmd]);
2432        assert_eq!(vm.last_status, 0);
2433    }
2434
2435    #[test]
2436    fn test_exec_if_true_branch() {
2437        use crate::parser::CompoundCommand;
2438        // if (( 1 )); then (( result = 42 )); else (( result = 99 )); fi
2439        // Since (( 1 )) sets status=0, true branch runs
2440        let cmd = ShellCommand::Compound(CompoundCommand::If {
2441            conditions: vec![(
2442                vec![ShellCommand::Compound(CompoundCommand::Arith(
2443                    "1".to_string(),
2444                ))],
2445                vec![ShellCommand::Compound(CompoundCommand::Arith(
2446                    "result = 42".to_string(),
2447                ))],
2448            )],
2449            else_part: Some(vec![ShellCommand::Compound(CompoundCommand::Arith(
2450                "result = 99".to_string(),
2451            ))]),
2452        });
2453        let vm = compile_and_run(&[cmd]);
2454        assert_eq!(vm.last_status, 0); // (( 42 )) is truthy → status 0
2455    }
2456
2457    #[test]
2458    fn test_exec_if_false_branch() {
2459        use crate::parser::CompoundCommand;
2460        // if (( 0 )); then (( result = 42 )); else (( result = 99 )); fi
2461        let cmd = ShellCommand::Compound(CompoundCommand::If {
2462            conditions: vec![(
2463                vec![ShellCommand::Compound(CompoundCommand::Arith(
2464                    "0".to_string(),
2465                ))],
2466                vec![ShellCommand::Compound(CompoundCommand::Arith(
2467                    "result = 42".to_string(),
2468                ))],
2469            )],
2470            else_part: Some(vec![ShellCommand::Compound(CompoundCommand::Arith(
2471                "result = 99".to_string(),
2472            ))]),
2473        });
2474        let vm = compile_and_run(&[cmd]);
2475        assert_eq!(vm.last_status, 0); // (( 99 )) is truthy → status 0
2476    }
2477
2478    #[test]
2479    fn test_exec_numeric_comparison() {
2480        use crate::parser::CompoundCommand;
2481        // [[ 5 -gt 3 ]] → true
2482        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::NumGreater(
2483            ShellWord::Literal("5".to_string()),
2484            ShellWord::Literal("3".to_string()),
2485        )));
2486        let vm = compile_and_run(&[cmd]);
2487        assert_eq!(vm.last_status, 0);
2488
2489        // [[ 2 -gt 3 ]] → false
2490        let cmd = ShellCommand::Compound(CompoundCommand::Cond(CondExpr::NumGreater(
2491            ShellWord::Literal("2".to_string()),
2492            ShellWord::Literal("3".to_string()),
2493        )));
2494        let vm = compile_and_run(&[cmd]);
2495        assert_eq!(vm.last_status, 1);
2496    }
2497
2498    #[test]
2499    fn test_exec_arith_zero_is_false() {
2500        use crate::parser::CompoundCommand;
2501        // (( 0 )) → status 1
2502        let cmd = ShellCommand::Compound(CompoundCommand::Arith("0".to_string()));
2503        let vm = compile_and_run(&[cmd]);
2504        assert_eq!(vm.last_status, 1);
2505    }
2506
2507    #[test]
2508    fn test_exec_arith_nonzero_is_true() {
2509        use crate::parser::CompoundCommand;
2510        // (( 42 )) → status 0
2511        let cmd = ShellCommand::Compound(CompoundCommand::Arith("42".to_string()));
2512        let vm = compile_and_run(&[cmd]);
2513        assert_eq!(vm.last_status, 0);
2514    }
2515
2516    #[test]
2517    fn test_exec_nested_arith_comparison() {
2518        use crate::parser::CompoundCommand;
2519        // (( 5 > 3 && 2 < 10 )) → status 0
2520        let cmd = ShellCommand::Compound(CompoundCommand::Arith("5 > 3 && 2 < 10".to_string()));
2521        let vm = compile_and_run(&[cmd]);
2522        assert_eq!(vm.last_status, 0);
2523
2524        // (( 5 > 3 && 2 > 10 )) → status 1
2525        let cmd = ShellCommand::Compound(CompoundCommand::Arith("5 > 3 && 2 > 10".to_string()));
2526        let vm = compile_and_run(&[cmd]);
2527        assert_eq!(vm.last_status, 1);
2528    }
2529}