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