Skip to main content

fusevm/
vm.rs

1//! The fusevm execution engine — stack-based bytecode dispatch loop.
2//!
3//! This is the hot path. Every cycle counts. The dispatch loop uses
4//! a flat `match` on `Op` variants — Rust compiles this to a jump table.
5//!
6//! Frontends register extension handlers via `ExtensionHandler` for
7//! language-specific opcodes (`Op::Extended`, `Op::ExtendedWide`).
8//!
9//! ## Optimizations
10//!
11//! - **Type-specialized integer fast paths**: Add, Sub, Mul, Mod, comparisons
12//!   check for `Int×Int` first and skip `to_float()` coercion entirely.
13//! - **Zero-clone dispatch**: ops are borrowed from the chunk, not cloned per cycle.
14//!   `LoadConst` copies scalars (Int/Float/Bool) without touching Arc refcounts.
15//! - **In-place container mutation**: array/hash ops (Push, Pop, Shift, Set,
16//!   HashSet, HashDelete) mutate globals directly — no clone-modify-writeback.
17//! - **`Cow<str>` string coercion**: `as_str_cow()` borrows `Str` variants without
18//!   allocation. Used in string comparisons, Concat, Print, hash key lookup.
19//! - **Inline builtin cache**: `CallBuiltin` dispatches through a pre-registered
20//!   function pointer table — no name lookup at runtime.
21//! - **Fused superinstructions**: hot loop patterns run as single ops
22//!   (AccumSumLoop, SlotIncLtIntJumpBack, etc.)
23//! - **Pre-allocated collections**: Range, MakeHash, HashKeys/Values use exact
24//!   or estimated capacity. ConcatConstLoop pre-sizes the string buffer.
25
26use crate::awk_host::AwkHost;
27use crate::chunk::Chunk;
28use crate::host::ShellHost;
29#[cfg(feature = "jit")]
30use crate::jit::{DeoptInfo, JitCompiler, SlotKind, TraceLookup};
31use crate::op::Op;
32use crate::value::Value;
33
34// Tracing-JIT thresholds previously sourced from `vm.rs` constants are
35// now read through `JitCompiler::get_config()` so callers can override
36// per-thread via `JitCompiler::set_config(...)`. The defaults match the
37// historical phase-by-phase constants:
38//   trace_threshold      = 50  (backedges before recording arms)
39//   max_side_exits       = 50  (side-exits before main-trace blacklist)
40//   max_inline_recursion = 4   (self-recursive call depth cap)
41//   max_trace_chain      = 4   (chained side-trace dispatch depth cap)
42//   max_trace_len        = 256 (recorded ops cap)
43
44/// In-progress trace recording state.
45///
46/// The recorder is armed when `trace_lookup` returns `StartRecording`.
47/// While armed, every dispatched op is appended to `ops` before the op's
48/// effect is applied. The recording closes when the interpreter takes a
49/// backward jump that lands at `close_anchor_ip`.
50///
51/// Phase 9 split: `record_anchor_ip` is where the recording STARTED (the
52/// trace cache key); `close_anchor_ip` is where the recording is expected
53/// to LAND on close. For main traces the two are identical (a loop header
54/// is both the recording start and the closing-branch target). For side
55/// traces (recordings armed at a hot side-exit), `record_anchor_ip` is the
56/// side-exit IP the recorder started from, while `close_anchor_ip` remains
57/// the enclosing loop's header — so the trace closes correctly when the
58/// loop's backward branch fires.
59///
60/// `entered_ips` simulates the inlined frame stack so the recorder can
61/// (a) bound self-recursion to `MAX_INLINE_RECURSION` levels, and
62/// (b) reject unbalanced Returns. The values pushed are bytecode entry IPs
63/// resolved from `Op::Call(name_idx, _)`.
64#[cfg(feature = "jit")]
65struct TraceRecorder {
66    /// Phase 9: IP recording started from. Used as the trace cache key
67    /// `(chunk.op_hash, record_anchor_ip)`.
68    record_anchor_ip: usize,
69    /// Phase 9: IP the closing backward branch is expected to land at.
70    /// For main traces this equals `record_anchor_ip`; for side traces
71    /// it's the enclosing loop's header.
72    close_anchor_ip: usize,
73    /// IP just past the closing branch (where the interpreter resumes on
74    /// normal loop exit).
75    fallthrough_ip: usize,
76    /// Recorded op sequence (body + closing branch as final op).
77    ops: Vec<Op>,
78    /// Original bytecode IP each recorded op was dispatched from. Parallel to
79    /// `ops`. Used at compile time to infer direction taken at conditional
80    /// branches: for op at index `i`, if `recorded_ips[i+1]` equals the op's
81    /// jump target, the jump was taken; otherwise the fallthrough was.
82    recorded_ips: Vec<usize>,
83    /// Slot type snapshot at recording start; installed as the entry guard.
84    slot_kinds_at_anchor: Vec<SlotKind>,
85    /// Stack of bytecode entry IPs for currently inlined callees. Empty in
86    /// the caller frame; pushed on Op::Call, popped on Op::Return /
87    /// Op::ReturnValue. Used for recursion detection.
88    entered_ips: Vec<usize>,
89    /// True if any condition aborted the recording. Causes cleanup-only on
90    /// next jump dispatch.
91    aborted: bool,
92}
93
94/// Call frame on the frame stack.
95#[derive(Debug, Clone)]
96pub struct Frame {
97    /// Return address (ip to resume after call)
98    pub return_ip: usize,
99    /// Base pointer into the value stack (locals start here)
100    pub stack_base: usize,
101    /// Local variable slots (indexed by `GetSlot`/`SetSlot`)
102    pub slots: Vec<Value>,
103}
104
105/// Extension handler for language-specific opcodes.
106/// Frontends register this at VM init.
107pub type ExtensionHandler = Box<dyn FnMut(&mut VM, u16, u8) + Send>;
108/// Wide extension handler (usize payload).
109pub type ExtensionWideHandler = Box<dyn FnMut(&mut VM, u16, usize) + Send>;
110/// Builtin function handler: (vm, argc) → Value
111pub type BuiltinHandler = fn(&mut VM, u8) -> Value;
112
113/// The virtual machine.
114pub struct VM {
115    /// Value stack
116    pub stack: Vec<Value>,
117    /// Call frame stack
118    pub frames: Vec<Frame>,
119    /// Global variables (name pool index → value)
120    pub globals: Vec<Value>,
121    /// Instruction pointer
122    pub ip: usize,
123    /// Current chunk being executed
124    pub chunk: Chunk,
125    /// Last exit status ($?)
126    pub last_status: i32,
127    /// Extension handler for `Op::Extended`
128    ext_handler: Option<ExtensionHandler>,
129    /// Extension handler for `Op::ExtendedWide`
130    ext_wide_handler: Option<ExtensionWideHandler>,
131    /// Inline builtin cache: builtin_id → function pointer (no lookup at dispatch)
132    builtin_table: Vec<Option<BuiltinHandler>>,
133    /// Frontend-supplied shell host (glob/expand/redirect/pipeline/etc).
134    /// When `None`, shell ops fall back to minimal stub behavior.
135    pub host: Option<Box<dyn ShellHost>>,
136    /// Frontend-supplied AWK host (fields/record/print/getline/string builtins).
137    /// The VM routes the reserved AWK op range (`Op::ExtendedWide` with
138    /// `id >= awk_builtins::AWK_OP_BASE`) here. When `None`, AWK ops are inert
139    /// stubs and the universal ops still execute normally.
140    pub awk_host: Option<Box<dyn AwkHost>>,
141    /// AWK PRNG seed for native `rand`/`srand` (glibc LCG, gawk-compatible).
142    /// Execution-intrinsic VM state (like a register), not part of AWK's data
143    /// model, so it lives here and is handled VM-side in both dispatch paths.
144    /// Initialized to 1 to match awkrs's default seed sequence.
145    awk_rand_seed: u64,
146    /// AWK control-flow signal raised by `Op::AwkSignal(code)` (the chunk halts
147    /// and the frontend driver reads this after `run()`): `next`/`nextfile`/
148    /// `exit` and range-pattern flow that has no `fusevm::Value` representation.
149    /// `None` unless an awk frontend emitted `Op::AwkSignal`; zshrs/stryke never
150    /// do, so for them this stays `None` and `Halted` behaves exactly as before.
151    awk_signal: Option<u8>,
152    /// Halted flag
153    halted: bool,
154    /// Tracing JIT enabled. When true, backward branches consult the trace
155    /// cache and may invoke compiled traces or arm the recorder.
156    #[cfg(feature = "jit")]
157    tracing_jit: bool,
158    /// JIT compiler instance — stateless wrapper over the thread-local cache.
159    #[cfg(feature = "jit")]
160    jit: JitCompiler,
161    /// Active trace recording, if any.
162    #[cfg(feature = "jit")]
163    recorder: Option<TraceRecorder>,
164    /// Reusable scratch i64 buffer of slot values, passed to compiled traces.
165    #[cfg(feature = "jit")]
166    slot_buf: Vec<i64>,
167    /// Reusable scratch slot-kind snapshot for the trace entry guard.
168    #[cfg(feature = "jit")]
169    slot_kinds_buf: Vec<SlotKind>,
170    /// Reusable scratch buffer the trace fn populates on every invocation
171    /// with the resume IP and (on callee-frame side-exits) inlined-frame
172    /// materialization records the VM uses to reshape `vm.frames`.
173    /// Stored inline (~888 bytes) to avoid heap indirection on the hot
174    /// trace path; the size cost is paid once per VM and the access
175    /// savings hit every invocation.
176    #[cfg(feature = "jit")]
177    deopt_info: DeoptInfo,
178    /// Cached block-JIT eligibility for `self.chunk`. `None` until first
179    /// `VM::run` call evaluates it; reused across subsequent runs since
180    /// `Chunk` is immutable for the VM's lifetime. Saves the TLS HashMap
181    /// lookup that `JitCompiler::is_block_eligible` would otherwise
182    /// perform on every run.
183    #[cfg(feature = "jit")]
184    block_eligible_cached: Option<bool>,
185}
186
187/// Result of VM execution
188#[derive(Debug)]
189pub enum VMResult {
190    Ok(Value),
191    /// Halted (no more instructions)
192    Halted,
193    /// Runtime error
194    Error(String),
195}
196
197impl VM {
198    /// Construct a fresh VM bound to the given chunk. Allocates the
199    /// per-name slot vector, seeds the call-frame stack with a root
200    /// frame, and zeros every per-thread counter (cycle / deopt /
201    /// trace stats). The chunk's `op_hash` is preserved verbatim so
202    /// subsequent JIT-cache lookups can short-circuit recompilation.
203    pub fn new(chunk: Chunk) -> Self {
204        let num_names = chunk.names.len();
205        let mut frames = Vec::with_capacity(32);
206        frames.push(Frame {
207            return_ip: 0,
208            stack_base: 0,
209            slots: Vec::with_capacity(16),
210        });
211        Self {
212            stack: Vec::with_capacity(256),
213            frames,
214            globals: vec![Value::Undef; num_names],
215            ip: 0,
216            chunk,
217            last_status: 0,
218            ext_handler: None,
219            ext_wide_handler: None,
220            builtin_table: Vec::new(),
221            host: None,
222            awk_host: None,
223            awk_rand_seed: 1,
224            awk_signal: None,
225            halted: false,
226            #[cfg(feature = "jit")]
227            tracing_jit: false,
228            #[cfg(feature = "jit")]
229            jit: JitCompiler::new(),
230            #[cfg(feature = "jit")]
231            recorder: None,
232            #[cfg(feature = "jit")]
233            slot_buf: Vec::new(),
234            #[cfg(feature = "jit")]
235            slot_kinds_buf: Vec::new(),
236            #[cfg(feature = "jit")]
237            deopt_info: DeoptInfo::zeroed(),
238            #[cfg(feature = "jit")]
239            block_eligible_cached: None,
240        }
241    }
242
243    /// Enable the tracing JIT for this VM. After this call, hot loops
244    /// (loops crossing the backedge threshold) will be recorded and JIT-
245    /// compiled at runtime; subsequent iterations dispatch through the
246    /// compiled trace.
247    ///
248    /// Phase 1 limits: only int-slot loops with a single backward branch and
249    /// no internal jumps are traceable. Loops outside that envelope continue
250    /// to run in the interpreter.
251    #[cfg(feature = "jit")]
252    pub fn enable_tracing_jit(&mut self) {
253        self.tracing_jit = true;
254    }
255
256    /// Disable the tracing JIT. Existing compiled traces remain in the
257    /// thread-local cache but are no longer consulted from this VM.
258    #[cfg(feature = "jit")]
259    pub fn disable_tracing_jit(&mut self) {
260        self.tracing_jit = false;
261        self.recorder = None;
262    }
263
264    /// Reset the VM for re-use with a new chunk, preserving internal
265    /// `Vec` allocations to avoid the construction cost of `VM::new`.
266    ///
267    /// State that's cleared:
268    /// - Value stack (truncated, capacity preserved)
269    /// - Frame stack (rebuilt with one entry pointing at the new chunk)
270    /// - Globals (resized to match the new chunk's name pool)
271    /// - Instruction pointer, halted flag, exit status
272    /// - Tracing JIT recorder / slot buffers / deopt info
273    /// - Cached block-JIT eligibility (the new chunk has a different hash)
274    ///
275    /// State that's preserved:
276    /// - Tracing JIT enabled flag
277    /// - Extension handlers (`ext_handler`, `ext_wide_handler`)
278    /// - Builtin table
279    /// - Shell host
280    ///
281    /// This pairs with [`VMPool`] for hot-path callers that run many
282    /// chunks back-to-back and want to skip the per-call allocation cost
283    /// of `VM::new`.
284    pub fn reset(&mut self, chunk: Chunk) {
285        self.stack.clear();
286        self.frames.clear();
287        let num_names = chunk.names.len();
288        self.globals.clear();
289        self.globals.resize(num_names, Value::Undef);
290        self.frames.push(Frame {
291            return_ip: 0,
292            stack_base: 0,
293            slots: Vec::with_capacity(16),
294        });
295        self.ip = 0;
296        self.last_status = 0;
297        self.halted = false;
298        self.awk_rand_seed = 1;
299        self.chunk = chunk;
300        #[cfg(feature = "jit")]
301        {
302            self.recorder = None;
303            self.slot_buf.clear();
304            self.slot_kinds_buf.clear();
305            self.deopt_info = DeoptInfo::zeroed();
306            self.block_eligible_cached = None;
307        }
308    }
309
310    /// Register the frontend shell host. Replaces any prior host.
311    pub fn set_shell_host(&mut self, host: Box<dyn ShellHost>) {
312        self.host = Some(host);
313    }
314
315    /// Register the frontend AWK host. Replaces any prior host. The VM then
316    /// routes the reserved AWK op range to it (see [`crate::awk_host::AwkHost`]).
317    pub fn set_awk_host(&mut self, host: Box<dyn AwkHost>) {
318        self.awk_host = Some(host);
319    }
320
321    /// AWK control-flow signal raised by the most recent `run()`, if any. An
322    /// awk frontend reads this after `run()` returns to map `Op::AwkSignal`
323    /// codes (`awk_builtins::signal::{NEXT,NEXTFILE,EXIT}`) onto its own
324    /// record/file/exit control flow. `None` when no signal was raised (always
325    /// the case for zshrs/stryke, which never emit `Op::AwkSignal`).
326    pub fn awk_signal(&self) -> Option<u8> {
327        self.awk_signal
328    }
329
330    /// Register a handler for `Op::Extended(id, arg)` opcodes.
331    pub fn set_extension_handler(&mut self, handler: ExtensionHandler) {
332        self.ext_handler = Some(handler);
333    }
334
335    /// Register a handler for `Op::ExtendedWide(id, payload)` opcodes.
336    pub fn set_extension_wide_handler(&mut self, handler: ExtensionWideHandler) {
337        self.ext_wide_handler = Some(handler);
338    }
339
340    /// Register a builtin function by ID. `CallBuiltin(id, argc)` dispatches
341    /// directly through the function pointer — no name lookup at runtime.
342    pub fn register_builtin(&mut self, id: u16, handler: BuiltinHandler) {
343        let idx = id as usize;
344        if idx >= self.builtin_table.len() {
345            self.builtin_table.resize(idx + 1, None);
346        }
347        self.builtin_table[idx] = Some(handler);
348    }
349
350    /// Externally request the VM to halt after the current op finishes.
351    /// Used by host-side shell semantics like `set -e` post-command checks
352    /// and `exit` from inside builtins to stop dispatch at a safe point.
353    pub fn request_halt(&mut self) {
354        self.halted = true;
355    }
356
357    // ── Tracing JIT integration helpers ──
358
359    /// Snapshot the current frame's slots into the i64 + slot-kind buffers.
360    ///
361    /// Slots that don't fit cleanly into i64 (Array/Hash/String/etc) are
362    /// reported as `SlotKind::Int` with i64 value 0 — they will fail the
363    /// trace's entry guard if the recorded trace expected Int there, which
364    /// is the desired behavior (skip the trace, fall back to interpreter).
365    ///
366    /// Specialized fast paths for 0-slot and 1-slot frames (the common
367    /// case for tight inner loops) — these skip Vec resize bookkeeping
368    /// and the iterator loop, saving ~20-50 ns per `vm.run()` invocation.
369    #[cfg(feature = "jit")]
370    #[inline]
371    fn refresh_slot_buffers(&mut self) {
372        let frame = match self.frames.last() {
373            Some(f) => f,
374            None => return,
375        };
376        let n = frame.slots.len();
377        match n {
378            0 => {
379                self.slot_buf.clear();
380                self.slot_kinds_buf.clear();
381            }
382            1 => {
383                let (i, kind) = match &frame.slots[0] {
384                    Value::Int(v) => (*v, SlotKind::Int),
385                    Value::Float(f) => (f.to_bits() as i64, SlotKind::Float),
386                    Value::Bool(b) => (*b as i64, SlotKind::Int),
387                    _ => (0, SlotKind::Int),
388                };
389                if self.slot_buf.is_empty() {
390                    self.slot_buf.push(i);
391                    self.slot_kinds_buf.push(kind);
392                } else {
393                    self.slot_buf.truncate(1);
394                    self.slot_buf[0] = i;
395                    self.slot_kinds_buf.truncate(1);
396                    self.slot_kinds_buf[0] = kind;
397                }
398            }
399            _ => {
400                self.slot_buf.clear();
401                self.slot_kinds_buf.clear();
402                self.slot_buf.reserve(n);
403                self.slot_kinds_buf.reserve(n);
404                for v in &frame.slots {
405                    let (i, kind) = match v {
406                        Value::Int(n) => (*n, SlotKind::Int),
407                        Value::Float(f) => (f.to_bits() as i64, SlotKind::Float),
408                        Value::Bool(b) => (*b as i64, SlotKind::Int),
409                        _ => (0, SlotKind::Int),
410                    };
411                    self.slot_buf.push(i);
412                    self.slot_kinds_buf.push(kind);
413                }
414            }
415        }
416    }
417
418    /// Copy the i64 slot buffer back into the current frame's slots,
419    /// materializing Int and Float kinds. Float slots are stored as i64
420    /// bit patterns in the buffer; recover via `f64::from_bits`. Slots of
421    /// other kinds (Array, Hash, etc.) are left untouched — those slots
422    /// would have prevented trace install if referenced.
423    ///
424    /// Specialized for 0/1-slot frames (common case).
425    #[cfg(feature = "jit")]
426    #[inline]
427    fn write_slots_back(&mut self) {
428        let frame = match self.frames.last_mut() {
429            Some(f) => f,
430            None => return,
431        };
432        let n = frame.slots.len().min(self.slot_buf.len());
433        match n {
434            0 => {}
435            1 => match self.slot_kinds_buf.first() {
436                Some(SlotKind::Int) => frame.slots[0] = Value::Int(self.slot_buf[0]),
437                Some(SlotKind::Float) => {
438                    frame.slots[0] = Value::Float(f64::from_bits(self.slot_buf[0] as u64));
439                }
440                None => {}
441            },
442            _ => {
443                for i in 0..n {
444                    match self.slot_kinds_buf.get(i) {
445                        Some(SlotKind::Int) => {
446                            frame.slots[i] = Value::Int(self.slot_buf[i]);
447                        }
448                        Some(SlotKind::Float) => {
449                            frame.slots[i] = Value::Float(f64::from_bits(self.slot_buf[i] as u64));
450                        }
451                        None => {}
452                    }
453                }
454            }
455        }
456    }
457
458    /// Consult the trace cache at a backward-branch site and return the IP
459    /// the interpreter should resume at. If a compiled trace runs, slot state
460    /// is copied back from the trace's i64 buffer into the frame, and any
461    /// inlined callee frames the trace recorded at a side-exit are
462    /// materialized as synthetic `Frame`s on `self.frames` so the
463    /// interpreter can resume mid-callee with a correctly shaped call stack.
464    #[cfg(feature = "jit")]
465    fn lookup_trace_for_backward(&mut self, anchor_ip: usize, fallthrough_ip: usize) -> usize {
466        self.refresh_slot_buffers();
467        let lookup = self.jit.trace_lookup(
468            &self.chunk,
469            anchor_ip,
470            &mut self.slot_buf,
471            &self.slot_kinds_buf,
472            &mut self.deopt_info,
473        );
474        match lookup {
475            TraceLookup::Ran { resume_ip } => {
476                self.write_slots_back();
477                self.materialize_deopt_frames();
478                // Phase 9: if the trace deopted (returned non-fallthrough),
479                // try to chain into a side trace at the resume IP.
480                self.chain_side_traces(anchor_ip, fallthrough_ip, resume_ip)
481            }
482            TraceLookup::StartRecording => {
483                // Main-trace path: record_anchor_ip == close_anchor_ip
484                // (the loop header). Side-trace recording is armed via the
485                // chained-dispatch path below.
486                self.recorder = Some(TraceRecorder {
487                    record_anchor_ip: anchor_ip,
488                    close_anchor_ip: anchor_ip,
489                    fallthrough_ip,
490                    ops: Vec::new(),
491                    recorded_ips: Vec::new(),
492                    slot_kinds_at_anchor: self.slot_kinds_buf.clone(),
493                    entered_ips: Vec::new(),
494                    aborted: false,
495                });
496                anchor_ip
497            }
498            TraceLookup::NotHot | TraceLookup::GuardMismatch | TraceLookup::Skip => anchor_ip,
499        }
500    }
501
502    /// Phase 9: chained dispatch through linked traces.
503    ///
504    /// When the main trace's `compiled.invoke` returns a non-fallthrough
505    /// resume IP (a brif guard fired and we side-exited), this method
506    /// attempts to dispatch a side trace registered at that resume IP.
507    /// Iterates up to `MAX_TRACE_CHAIN` times so a sequence of linked
508    /// side-exits can resolve through their respective side traces.
509    ///
510    /// Side-trace recording is armed when a side-exit IP becomes hot (the
511    /// `StartRecording` branch). The recorder is set up with
512    /// `close_anchor_ip = main_anchor`, so the side trace closes correctly
513    /// when the enclosing loop's backward branch fires.
514    ///
515    /// The main trace's `side_exit_count` is incremented only when the
516    /// chain bottoms out without finding a side trace — exits that are
517    /// being absorbed productively shouldn't push the main trace toward
518    /// blacklisting.
519    #[cfg(feature = "jit")]
520    fn chain_side_traces(
521        &mut self,
522        main_anchor: usize,
523        main_fallthrough: usize,
524        first_resume: usize,
525    ) -> usize {
526        let mut current = first_resume;
527        if current == main_fallthrough {
528            return current;
529        }
530        let max_chain = self.jit.get_config().max_trace_chain;
531        for _ in 0..max_chain {
532            // The chained trace at `current` may itself have a different
533            // fallthrough; we re-fetch on each iteration.
534            let chained_fallthrough = self
535                .jit
536                .trace_loop_anchors(&self.chunk, current)
537                .map(|(_, fallthrough)| fallthrough);
538
539            self.refresh_slot_buffers();
540            let lookup = self.jit.trace_lookup(
541                &self.chunk,
542                current,
543                &mut self.slot_buf,
544                &self.slot_kinds_buf,
545                &mut self.deopt_info,
546            );
547            match lookup {
548                TraceLookup::Ran { resume_ip } => {
549                    self.write_slots_back();
550                    self.materialize_deopt_frames();
551                    current = resume_ip;
552                    if Some(current) == chained_fallthrough {
553                        return current;
554                    }
555                }
556                TraceLookup::StartRecording => {
557                    // Arm side-trace recording. The side trace's close
558                    // anchor is the main loop's header; its fallthrough is
559                    // the main loop's post-loop IP. Slot-kind snapshot is
560                    // taken at THIS moment (post-deopt state).
561                    self.recorder = Some(TraceRecorder {
562                        record_anchor_ip: current,
563                        close_anchor_ip: main_anchor,
564                        fallthrough_ip: main_fallthrough,
565                        ops: Vec::new(),
566                        recorded_ips: Vec::new(),
567                        slot_kinds_at_anchor: self.slot_kinds_buf.clone(),
568                        entered_ips: Vec::new(),
569                        aborted: false,
570                    });
571                    return current;
572                }
573                _ => {
574                    // No side trace available; count toward main trace's
575                    // blacklist budget.
576                    self.jit.trace_bump_side_exit(&self.chunk, main_anchor);
577                    return current;
578                }
579            }
580        }
581        current
582    }
583
584    /// Push synthetic `Frame`s onto `self.frames` for each inlined callee
585    /// frame the trace recorded at side-exit, then push any remaining
586    /// abstract-stack values from the trace onto `self.stack` as
587    /// `Value::Int`. Order matters: stack values are pushed BEFORE the
588    /// frames are pushed, because each frame's `stack_base` snapshots
589    /// `self.stack.len()` AFTER the stack values are placed — that way
590    /// when the synthetic frame eventually returns and truncates to
591    /// `stack_base`, those values are correctly retained. Phase 5+.
592    #[cfg(feature = "jit")]
593    fn materialize_deopt_frames(&mut self) {
594        // 1. Push the abstract stack (in trace order; entry [0] ends up at
595        //    the bottom, [N-1] at the top). Float entries are bit-cast back
596        //    via `f64::from_bits`; everything else is treated as `i64`.
597        let stack_count = self.deopt_info.stack_count;
598        for i in 0..stack_count {
599            let raw = self.deopt_info.stack_buf[i];
600            let v = match self.deopt_info.stack_kinds[i] {
601                crate::jit::STACK_KIND_FLOAT => Value::Float(f64::from_bits(raw as u64)),
602                _ => Value::Int(raw),
603            };
604            self.stack.push(v);
605        }
606        // 2. Materialize inlined frames.
607        let count = self.deopt_info.frame_count;
608        if count == 0 {
609            return;
610        }
611        for i in 0..count {
612            let df = &self.deopt_info.frames[i];
613            let mut slots: Vec<Value> = Vec::with_capacity(df.slot_count);
614            for j in 0..df.slot_count {
615                slots.push(Value::Int(df.slots[j]));
616            }
617            self.frames.push(Frame {
618                return_ip: df.return_ip,
619                stack_base: self.stack.len(),
620                slots,
621            });
622        }
623    }
624
625    /// Finalize the active recording: either close (install) when the just-
626    /// dispatched jump landed back at the anchor and the trace is eligible,
627    /// or abort and discard. Safe to call only when `self.recorder.is_some()`.
628    #[cfg(feature = "jit")]
629    fn finalize_recorder(&mut self) {
630        let Some(rec) = self.recorder.as_ref() else {
631            return;
632        };
633        let aborted = rec.aborted;
634        let close_anchor = rec.close_anchor_ip;
635        let record_anchor = rec.record_anchor_ip;
636        // Trace closes when the just-dispatched jump lands at the recorded
637        // close anchor. For main traces this is the loop header; for side
638        // traces it's the enclosing loop's header (the side trace
639        // started at a side-exit IP but still closes when the loop iterates).
640        if !aborted && self.ip == close_anchor {
641            let r = self.recorder.take().unwrap();
642            if self.jit.is_trace_eligible(&r.ops, r.close_anchor_ip) {
643                // Phase 9: when record != close (side trace), install via
644                // the kinded variant so the IR's "continuation" branch
645                // exits rather than looping back.
646                let installed = self.jit.trace_install_with_kind(
647                    &self.chunk,
648                    r.record_anchor_ip,
649                    r.close_anchor_ip,
650                    r.fallthrough_ip,
651                    &r.ops,
652                    &r.recorded_ips,
653                    &r.slot_kinds_at_anchor,
654                );
655                if !installed {
656                    self.jit.trace_abort(&self.chunk, r.record_anchor_ip);
657                }
658            } else {
659                self.jit.trace_abort(&self.chunk, r.record_anchor_ip);
660            }
661        } else {
662            // Trace dispatch landed somewhere unexpected — abort. The
663            // record_anchor_ip captured before take() is the cache key.
664            let _ = self.recorder.take();
665            self.jit.trace_abort(&self.chunk, record_anchor);
666        }
667    }
668
669    // ── Stack operations ──
670
671    /// Push `val` onto the value stack. Inlined for hot-path callers
672    /// (extension handlers, builtin shims) that bypass the dispatch
673    /// loop's own push.
674    #[inline(always)]
675    pub fn push(&mut self, val: Value) {
676        self.stack.push(val);
677    }
678
679    /// Pop the top of the value stack, returning `Value::Undef` if the
680    /// stack is empty. Returning Undef rather than panicking matches
681    /// Perl's "underflow is undef" semantic and lets extension/builtin
682    /// handlers stay panic-free under malformed bytecode.
683    #[inline(always)]
684    pub fn pop(&mut self) -> Value {
685        self.stack.pop().unwrap_or(Value::Undef)
686    }
687
688    /// Borrow the top of the value stack without popping. Returns a
689    /// reference to `Value::Undef` when the stack is empty.
690    #[inline(always)]
691    pub fn peek(&self) -> &Value {
692        self.stack.last().unwrap_or(&Value::Undef)
693    }
694
695    // ── Type-specialized helpers (avoid to_float coercion on hot paths) ──
696
697    /// Pop two values; if both Int, apply int_op. Otherwise promote to float.
698    #[inline(always)]
699    fn arith_int_fast(&mut self, int_op: fn(i64, i64) -> i64, float_op: fn(f64, f64) -> f64) {
700        let len = self.stack.len();
701        if len >= 2 {
702            // Borrow both slots without popping (avoid branch + unwrap_or)
703            let b = &self.stack[len - 1];
704            let a = &self.stack[len - 2];
705            let result = match (a, b) {
706                (Value::Int(x), Value::Int(y)) => Value::Int(int_op(*x, *y)),
707                _ => Value::Float(float_op(a.to_float(), b.to_float())),
708            };
709            self.stack.truncate(len - 2);
710            self.stack.push(result);
711        }
712    }
713
714    /// Pop two values; compare as int if both Int, otherwise float.
715    /// Push Bool(true/false).
716    #[inline(always)]
717    fn cmp_int_fast(&mut self, int_cmp: fn(i64, i64) -> bool, float_cmp: fn(f64, f64) -> bool) {
718        let len = self.stack.len();
719        if len >= 2 {
720            let b = &self.stack[len - 1];
721            let a = &self.stack[len - 2];
722            let result = match (a, b) {
723                (Value::Int(x), Value::Int(y)) => int_cmp(*x, *y),
724                _ => float_cmp(a.to_float(), b.to_float()),
725            };
726            self.stack.truncate(len - 2);
727            self.stack.push(Value::Bool(result));
728        }
729    }
730
731    // ── Main dispatch loop ──
732
733    /// Execute the loaded chunk until completion or error.
734    ///
735    /// Phase 10: tiered auto-dispatch. When `tracing_jit` is enabled the
736    /// VM consults all three Cranelift tiers in priority order:
737    ///
738    /// 1. **Block JIT** — if the entire chunk is block-eligible, the
739    ///    block-JIT cache returns `Some(result)` after its own warmup
740    ///    threshold and the whole chunk runs in native code with zero
741    ///    interpreter dispatch.
742    /// 2. **Tracing JIT** — when block JIT doesn't apply, the dispatch
743    ///    loop runs with the recorder armed at backward branches; hot
744    ///    loops compile to traces that take over subsequent iterations.
745    /// 3. **Interpreter** — fallback for cold code and chunks neither
746    ///    tier handles.
747    ///
748    /// Block JIT is tried first because, when it applies, it has zero
749    /// VM-side overhead (direct fn-ptr through the slot pointer). For
750    /// chunks block JIT can't take, control falls through to the
751    /// interpreter with tracing JIT integrated. The two tiers don't
752    /// compete on the same chunk: block-eligible chunks short-circuit
753    /// before tracing JIT records anything.
754    pub fn run(&mut self) -> VMResult {
755        use crate::awk_builtins as ab;
756        // A fresh execution: clear any AWK signal raised by a prior run on a
757        // reused VM. (zshrs/stryke never emit `Op::AwkSignal`, so this stays
758        // `None` for them.)
759        self.awk_signal = None;
760        // Phase 10: try block JIT first for fully-eligible chunks. The
761        // block-JIT cache has its own threshold (10 invocations); the
762        // call returns None until it warms up, at which point the whole
763        // chunk runs in native code.
764        //
765        // VM-side eligibility cache (`block_eligible_cached`) saves the
766        // TLS HashMap lookup `JitCompiler::is_block_eligible` would
767        // otherwise do on every run. The slot buffer must be valid on
768        // every invocation because `try_run_block` may compile + invoke
769        // the same call (on threshold cross), so we can't skip the
770        // refresh based on warmup state.
771        #[cfg(feature = "jit")]
772        if self.tracing_jit && self.frames.len() == 1 && self.ip == 0 {
773            let eligible = match self.block_eligible_cached {
774                Some(v) => v,
775                None => {
776                    let v = self.jit.is_block_eligible(&self.chunk);
777                    self.block_eligible_cached = Some(v);
778                    v
779                }
780            };
781            if eligible {
782                self.refresh_slot_buffers();
783                if let Some(result_i64) = self.jit.try_run_block_kinded(
784                    &self.chunk,
785                    &mut self.slot_buf,
786                    &self.slot_kinds_buf,
787                ) {
788                    // A JIT-compiled AwkDivJit/AwkModJit may have hit a zero
789                    // divisor and set the thread-local trap code before
790                    // returning. Honor it as the awk fatal, discarding the
791                    // (garbage) block result and slot writeback.
792                    match crate::jit::take_awk_div_trap() {
793                        1 => return VMResult::Error("division by zero attempted".to_string()),
794                        2 => {
795                            return VMResult::Error("division by zero attempted in `%'".to_string())
796                        }
797                        3 => {
798                            return VMResult::Error(
799                                "lshift: negative values are not allowed".to_string(),
800                            )
801                        }
802                        4 => {
803                            return VMResult::Error(
804                                "rshift: negative values are not allowed".to_string(),
805                            )
806                        }
807                        5 => {
808                            return VMResult::Error(
809                                "compl: negative value is not allowed".to_string(),
810                            )
811                        }
812                        _ => {}
813                    }
814                    self.write_slots_back();
815                    self.halted = true;
816                    return VMResult::Ok(Value::Int(result_i64));
817                }
818            }
819        }
820
821        let ops = &self.chunk.ops as *const Vec<Op>;
822        // SAFETY: self.chunk.ops is not mutated during execution.
823        // We take a pointer to avoid borrow checker issues with &self.chunk.ops
824        // while mutating self.stack/frames/globals.
825        let ops = unsafe { &*ops };
826
827        while self.ip < ops.len() && !self.halted {
828            // Zero-clone: borrow the op instead of cloning
829            let ip = self.ip;
830            self.ip += 1;
831
832            // Tracing JIT: capture this op into the active recording, if any.
833            // Push happens before dispatch so the closing branch is included.
834            // Track whether the recorder was armed BEFORE this dispatch step —
835            // a recorder armed inside this step (via `StartRecording`) must not
836            // finalize until the NEXT iteration, otherwise the very dispatch
837            // step that armed it would also close it with an empty op list.
838            #[cfg(feature = "jit")]
839            let recorder_was_armed = self.recorder.is_some();
840            #[cfg(feature = "jit")]
841            if self.recorder.is_some() {
842                let cfg = self.jit.get_config();
843                let max_len = cfg.max_trace_len;
844                let max_inline_recursion = cfg.max_inline_recursion;
845                let cur_op = ops[ip].clone();
846                // Resolve sub-entry up front (immutable chunk borrow) so the
847                // mutable recorder borrow below doesn't collide.
848                let resolved_entry = if let Op::Call(name_idx, _) = &cur_op {
849                    self.chunk.find_sub(*name_idx)
850                } else {
851                    None
852                };
853                let rec = self.recorder.as_mut().unwrap();
854                if !rec.aborted {
855                    rec.ops.push(cur_op.clone());
856                    rec.recorded_ips.push(ip);
857                    if rec.ops.len() > max_len {
858                        rec.aborted = true;
859                    } else {
860                        // Maintain inlined-frame stack and abort on patterns
861                        // the trace JIT can't represent in phase 2.
862                        match cur_op {
863                            Op::Call(_, _) => match resolved_entry {
864                                Some(entry_ip) => {
865                                    // Phase 8: bounded recursion. Allow a
866                                    // self-call to be inlined up to
867                                    // `MAX_INLINE_RECURSION` levels deep
868                                    // before aborting. Each push is one
869                                    // level — the depth limit naturally
870                                    // bounds tail-recursive helpers without
871                                    // explicit base-case detection.
872                                    let occurrences = rec
873                                        .entered_ips
874                                        .iter()
875                                        .filter(|&&ip| ip == entry_ip)
876                                        .count();
877                                    if occurrences >= max_inline_recursion {
878                                        rec.aborted = true;
879                                    } else {
880                                        rec.entered_ips.push(entry_ip);
881                                    }
882                                }
883                                None => rec.aborted = true, // unknown sub
884                            },
885                            Op::Return | Op::ReturnValue => {
886                                if rec.entered_ips.is_empty() {
887                                    rec.aborted = true; // unbalanced — would
888                                                        // pop the caller frame
889                                } else {
890                                    rec.entered_ips.pop();
891                                }
892                            }
893                            Op::CallBuiltin(_, _) => rec.aborted = true,
894                            // Most AWK ops are host calls (like CallBuiltin) and
895                            // can't appear in a compiled trace — abort. Pure ops
896                            // with native CLIF codegen in `emit_data_op`
897                            // (e.g. AwkInt → trunc) are omitted here so they can
898                            // be recorded and compiled.
899                            Op::AwkFieldGet
900                            | Op::AwkFieldSet
901                            | Op::AwkNf
902                            | Op::AwkSetRecord
903                            | Op::AwkSpecialGet(_)
904                            | Op::AwkSpecialSet(_)
905                            | Op::AwkPrint(_)
906                            | Op::AwkPrintf(_)
907                            | Op::AwkSprintf(_)
908                            | Op::AwkGetline(_)
909                            | Op::AwkLength(_)
910                            | Op::AwkSubstr(_)
911                            | Op::AwkIndex
912                            | Op::AwkSplit(_)
913                            | Op::AwkSub(_)
914                            | Op::AwkGsub(_)
915                            | Op::AwkGensub(_)
916                            | Op::AwkMatch
917                            | Op::AwkToLower
918                            | Op::AwkToUpper
919                            | Op::AwkSqrt
920                            | Op::AwkSin
921                            | Op::AwkCos
922                            | Op::AwkExp
923                            | Op::AwkLog
924                            | Op::AwkAtan2
925                            | Op::AwkDiv
926                            | Op::AwkMod
927                            | Op::AwkDivJit
928                            | Op::AwkModJit
929                            | Op::AwkSqrtJit
930                            | Op::AwkLogJit
931                            | Op::AwkLshiftJit
932                            | Op::AwkRshiftJit
933                            | Op::AwkComplJit
934                            | Op::AwkAnd(_)
935                            | Op::AwkOr(_)
936                            | Op::AwkXor(_)
937                            | Op::AwkCompl
938                            | Op::AwkLshift
939                            | Op::AwkRshift
940                            | Op::AwkStrtonum
941                            | Op::AwkSystime
942                            | Op::AwkRand
943                            | Op::AwkSrand(_)
944                            | Op::AwkStrftime(_)
945                            | Op::AwkMktime(_)
946                            | Op::AwkOrd
947                            | Op::AwkChr
948                            | Op::AwkMkbool
949                            | Op::AwkIntdiv
950                            | Op::AwkIntdiv0
951                            | Op::AwkArrayGet(_)
952                            | Op::AwkArraySet(_)
953                            | Op::AwkArrayExists(_)
954                            | Op::AwkArrayDelete(_)
955                            | Op::AwkArrayClear(_)
956                            | Op::AwkArrayLen(_) => rec.aborted = true,
957                            Op::AwkSignal(_) => rec.aborted = true,
958                            _ => {}
959                        }
960                    }
961                }
962            }
963
964            match &ops[ip] {
965                Op::Nop => {}
966
967                // ── Constants ──
968                Op::LoadInt(n) => self.push(Value::Int(*n)),
969                Op::LoadFloat(f) => self.push(Value::Float(*f)),
970                Op::LoadConst(idx) => {
971                    let val = match self.chunk.constants.get(*idx as usize) {
972                        Some(Value::Int(n)) => Value::Int(*n),
973                        Some(Value::Float(f)) => Value::Float(*f),
974                        Some(Value::Bool(b)) => Value::Bool(*b),
975                        Some(other) => other.clone(),
976                        None => Value::Undef,
977                    };
978                    self.push(val);
979                }
980                Op::LoadTrue => self.push(Value::Bool(true)),
981                Op::LoadFalse => self.push(Value::Bool(false)),
982                Op::LoadUndef => self.push(Value::Undef),
983
984                // ── Stack ──
985                Op::Pop => {
986                    self.pop();
987                }
988                Op::Dup => {
989                    let val = self.peek().clone();
990                    self.push(val);
991                }
992                Op::Dup2 => {
993                    let len = self.stack.len();
994                    if len >= 2 {
995                        let a = self.stack[len - 2].clone();
996                        let b = self.stack[len - 1].clone();
997                        self.push(a);
998                        self.push(b);
999                    }
1000                }
1001                Op::Swap => {
1002                    let len = self.stack.len();
1003                    if len >= 2 {
1004                        self.stack.swap(len - 1, len - 2);
1005                    }
1006                }
1007                Op::Rot => {
1008                    let len = self.stack.len();
1009                    if len >= 3 {
1010                        // [a, b, c] → [b, c, a] via two swaps instead of O(n) remove
1011                        self.stack.swap(len - 3, len - 2);
1012                        self.stack.swap(len - 2, len - 1);
1013                    }
1014                }
1015
1016                // ── Variables ──
1017                Op::GetVar(idx) => {
1018                    let val = self.get_var(*idx);
1019                    self.push(val);
1020                }
1021                Op::SetVar(idx) => {
1022                    let val = self.pop();
1023                    self.set_var(*idx, val);
1024                }
1025                Op::DeclareVar(idx) => {
1026                    let val = self.pop();
1027                    self.set_var(*idx, val);
1028                }
1029                Op::GetSlot(slot) => {
1030                    let val = self.get_slot(*slot);
1031                    self.push(val);
1032                }
1033                Op::SetSlot(slot) => {
1034                    let val = self.pop();
1035                    self.set_slot(*slot, val);
1036                }
1037                Op::SlotArrayGet(slot) => {
1038                    let index = self.pop().to_int() as usize;
1039                    let val = self.get_slot(*slot);
1040                    let result = if let Value::Array(ref arr) = val {
1041                        arr.get(index).cloned().unwrap_or(Value::Undef)
1042                    } else {
1043                        Value::Undef
1044                    };
1045                    self.push(result);
1046                }
1047                Op::SlotArraySet(slot) => {
1048                    let index = self.pop().to_int() as usize;
1049                    let val = self.pop();
1050                    let arr_val = self.get_slot(*slot);
1051                    if let Value::Array(mut arr) = arr_val {
1052                        if index >= arr.len() {
1053                            arr.resize(index + 1, Value::Undef);
1054                        }
1055                        arr[index] = val;
1056                        self.set_slot(*slot, Value::Array(arr));
1057                    }
1058                }
1059
1060                // ── Arithmetic (type-specialized: Int×Int avoids to_float) ──
1061                Op::Add => self.arith_int_fast(i64::wrapping_add, |a, b| a + b),
1062                Op::Sub => self.arith_int_fast(i64::wrapping_sub, |a, b| a - b),
1063                Op::Mul => self.arith_int_fast(i64::wrapping_mul, |a, b| a * b),
1064                Op::Div => {
1065                    let b = self.pop();
1066                    let a = self.pop();
1067                    let divisor = b.to_float();
1068                    self.push(if divisor == 0.0 {
1069                        Value::Undef
1070                    } else {
1071                        Value::Float(a.to_float() / divisor)
1072                    });
1073                }
1074                Op::Mod => self.arith_int_fast(|x, y| if y != 0 { x % y } else { 0 }, |a, b| a % b),
1075                Op::Pow => {
1076                    let b = self.pop();
1077                    let a = self.pop();
1078                    self.push(Value::Float(a.to_float().powf(b.to_float())));
1079                }
1080                Op::Negate => {
1081                    let val = self.pop();
1082                    self.push(match val {
1083                        Value::Int(n) => Value::Int(n.wrapping_neg()),
1084                        _ => Value::Float(-val.to_float()),
1085                    });
1086                }
1087                Op::Inc => {
1088                    let val = self.pop();
1089                    self.push(match val {
1090                        Value::Int(n) => Value::Int(n.wrapping_add(1)),
1091                        _ => Value::Int(val.to_int().wrapping_add(1)),
1092                    });
1093                }
1094                Op::Dec => {
1095                    let val = self.pop();
1096                    self.push(match val {
1097                        Value::Int(n) => Value::Int(n.wrapping_sub(1)),
1098                        _ => Value::Int(val.to_int().wrapping_sub(1)),
1099                    });
1100                }
1101
1102                // ── String ──
1103                Op::Concat => {
1104                    let b = self.pop();
1105                    let a = self.pop();
1106                    let a_s = a.as_str_cow();
1107                    let b_s = b.as_str_cow();
1108                    let mut s = String::with_capacity(a_s.len() + b_s.len());
1109                    s.push_str(&a_s);
1110                    s.push_str(&b_s);
1111                    self.push(Value::str(s));
1112                }
1113                Op::StringRepeat => {
1114                    let count = self.pop().to_int();
1115                    let s = self.pop().to_str();
1116                    self.push(Value::str(s.repeat(count.max(0) as usize)));
1117                }
1118                Op::StringLen => {
1119                    let s = self.pop();
1120                    self.push(Value::Int(s.len() as i64));
1121                }
1122
1123                // ── Comparison (type-specialized: Int×Int avoids to_float) ──
1124                Op::NumEq => self.cmp_int_fast(|x, y| x == y, |a, b| a == b),
1125                Op::NumNe => self.cmp_int_fast(|x, y| x != y, |a, b| a != b),
1126                Op::NumLt => self.cmp_int_fast(|x, y| x < y, |a, b| a < b),
1127                Op::NumGt => self.cmp_int_fast(|x, y| x > y, |a, b| a > b),
1128                Op::NumLe => self.cmp_int_fast(|x, y| x <= y, |a, b| a <= b),
1129                Op::NumGe => self.cmp_int_fast(|x, y| x >= y, |a, b| a >= b),
1130                Op::Spaceship => {
1131                    let len = self.stack.len();
1132                    if len >= 2 {
1133                        let b = &self.stack[len - 1];
1134                        let a = &self.stack[len - 2];
1135                        let result = match (a, b) {
1136                            (Value::Int(x), Value::Int(y)) => x.cmp(y) as i64,
1137                            _ => {
1138                                let af = a.to_float();
1139                                let bf = b.to_float();
1140                                if af < bf {
1141                                    -1
1142                                } else if af > bf {
1143                                    1
1144                                } else {
1145                                    0
1146                                }
1147                            }
1148                        };
1149                        self.stack.truncate(len - 2);
1150                        self.stack.push(Value::Int(result));
1151                    }
1152                }
1153
1154                // ── Comparison (string — borrow via Cow to avoid allocation) ──
1155                Op::StrEq => {
1156                    let b = self.pop();
1157                    let a = self.pop();
1158                    self.push(Value::Bool(a.as_str_cow() == b.as_str_cow()));
1159                }
1160                Op::StrNe => {
1161                    let b = self.pop();
1162                    let a = self.pop();
1163                    self.push(Value::Bool(a.as_str_cow() != b.as_str_cow()));
1164                }
1165                Op::StrLt => {
1166                    let b = self.pop();
1167                    let a = self.pop();
1168                    self.push(Value::Bool(a.as_str_cow() < b.as_str_cow()));
1169                }
1170                Op::StrGt => {
1171                    let b = self.pop();
1172                    let a = self.pop();
1173                    self.push(Value::Bool(a.as_str_cow() > b.as_str_cow()));
1174                }
1175                Op::StrLe => {
1176                    let b = self.pop();
1177                    let a = self.pop();
1178                    self.push(Value::Bool(a.as_str_cow() <= b.as_str_cow()));
1179                }
1180                Op::StrGe => {
1181                    let b = self.pop();
1182                    let a = self.pop();
1183                    self.push(Value::Bool(a.as_str_cow() >= b.as_str_cow()));
1184                }
1185                Op::StrCmp => {
1186                    let b = self.pop();
1187                    let a = self.pop();
1188                    self.push(Value::Int(match a.as_str_cow().cmp(&b.as_str_cow()) {
1189                        std::cmp::Ordering::Less => -1,
1190                        std::cmp::Ordering::Equal => 0,
1191                        std::cmp::Ordering::Greater => 1,
1192                    }));
1193                }
1194
1195                // ── Logical / Bitwise ──
1196                Op::LogNot => {
1197                    let val = self.pop();
1198                    self.push(Value::Bool(!val.is_truthy()));
1199                }
1200                Op::LogAnd => {
1201                    let b = self.pop();
1202                    let a = self.pop();
1203                    self.push(Value::Bool(a.is_truthy() && b.is_truthy()));
1204                }
1205                Op::LogOr => {
1206                    let b = self.pop();
1207                    let a = self.pop();
1208                    self.push(Value::Bool(a.is_truthy() || b.is_truthy()));
1209                }
1210                Op::BitAnd => {
1211                    let b = self.pop();
1212                    let a = self.pop();
1213                    self.push(Value::Int(a.to_int() & b.to_int()));
1214                }
1215                Op::BitOr => {
1216                    let b = self.pop();
1217                    let a = self.pop();
1218                    self.push(Value::Int(a.to_int() | b.to_int()));
1219                }
1220                Op::BitXor => {
1221                    let b = self.pop();
1222                    let a = self.pop();
1223                    self.push(Value::Int(a.to_int() ^ b.to_int()));
1224                }
1225                Op::BitNot => {
1226                    let val = self.pop();
1227                    self.push(Value::Int(!val.to_int()));
1228                }
1229                Op::Shl => {
1230                    let b = self.pop();
1231                    let a = self.pop();
1232                    self.push(Value::Int(a.to_int() << (b.to_int() as u32 & 63)));
1233                }
1234                Op::Shr => {
1235                    let b = self.pop();
1236                    let a = self.pop();
1237                    self.push(Value::Int(a.to_int() >> (b.to_int() as u32 & 63)));
1238                }
1239
1240                // ── Control flow ──
1241                Op::Jump(target) => {
1242                    let target = *target;
1243                    #[cfg(feature = "jit")]
1244                    if self.tracing_jit && self.recorder.is_none() && target <= ip {
1245                        self.ip = self.lookup_trace_for_backward(target, ip + 1);
1246                    } else {
1247                        self.ip = target;
1248                    }
1249                    #[cfg(not(feature = "jit"))]
1250                    {
1251                        self.ip = target;
1252                    }
1253                }
1254                Op::JumpIfTrue(target) => {
1255                    let target = *target;
1256                    if self.pop().is_truthy() {
1257                        #[cfg(feature = "jit")]
1258                        if self.tracing_jit && self.recorder.is_none() && target <= ip {
1259                            self.ip = self.lookup_trace_for_backward(target, ip + 1);
1260                        } else {
1261                            self.ip = target;
1262                        }
1263                        #[cfg(not(feature = "jit"))]
1264                        {
1265                            self.ip = target;
1266                        }
1267                    }
1268                }
1269                Op::JumpIfFalse(target) => {
1270                    let target = *target;
1271                    if !self.pop().is_truthy() {
1272                        #[cfg(feature = "jit")]
1273                        if self.tracing_jit && self.recorder.is_none() && target <= ip {
1274                            self.ip = self.lookup_trace_for_backward(target, ip + 1);
1275                        } else {
1276                            self.ip = target;
1277                        }
1278                        #[cfg(not(feature = "jit"))]
1279                        {
1280                            self.ip = target;
1281                        }
1282                    }
1283                }
1284                Op::JumpIfTrueKeep(target) => {
1285                    let target = *target;
1286                    if self.peek().is_truthy() {
1287                        #[cfg(feature = "jit")]
1288                        if self.tracing_jit && self.recorder.is_none() && target <= ip {
1289                            self.ip = self.lookup_trace_for_backward(target, ip + 1);
1290                        } else {
1291                            self.ip = target;
1292                        }
1293                        #[cfg(not(feature = "jit"))]
1294                        {
1295                            self.ip = target;
1296                        }
1297                    }
1298                }
1299                Op::JumpIfFalseKeep(target) => {
1300                    let target = *target;
1301                    if !self.peek().is_truthy() {
1302                        #[cfg(feature = "jit")]
1303                        if self.tracing_jit && self.recorder.is_none() && target <= ip {
1304                            self.ip = self.lookup_trace_for_backward(target, ip + 1);
1305                        } else {
1306                            self.ip = target;
1307                        }
1308                        #[cfg(not(feature = "jit"))]
1309                        {
1310                            self.ip = target;
1311                        }
1312                    }
1313                }
1314
1315                // ── Functions ──
1316                Op::Call(name_idx, argc) => {
1317                    if let Some(entry_ip) = self.chunk.find_sub(*name_idx) {
1318                        self.frames.push(Frame {
1319                            return_ip: self.ip,
1320                            stack_base: self.stack.len() - *argc as usize,
1321                            slots: Vec::new(),
1322                        });
1323                        self.ip = entry_ip;
1324                    } else {
1325                        return VMResult::Error(format!(
1326                            "undefined function: {}",
1327                            self.chunk
1328                                .names
1329                                .get(*name_idx as usize)
1330                                .map(|s| s.as_str())
1331                                .unwrap_or("?")
1332                        ));
1333                    }
1334                }
1335                Op::Return => {
1336                    if let Some(frame) = self.frames.pop() {
1337                        self.stack.truncate(frame.stack_base);
1338                        self.ip = frame.return_ip;
1339                    } else {
1340                        self.halted = true;
1341                    }
1342                }
1343                Op::ReturnValue => {
1344                    let val = self.pop();
1345                    if let Some(frame) = self.frames.pop() {
1346                        self.stack.truncate(frame.stack_base);
1347                        self.ip = frame.return_ip;
1348                        self.push(val);
1349                    } else {
1350                        self.halted = true;
1351                        return VMResult::Ok(val);
1352                    }
1353                }
1354
1355                // ── Scope ──
1356                Op::PushFrame => {
1357                    self.frames.push(Frame {
1358                        return_ip: self.ip,
1359                        stack_base: self.stack.len(),
1360                        slots: Vec::new(),
1361                    });
1362                }
1363                Op::PopFrame => {
1364                    if let Some(frame) = self.frames.pop() {
1365                        self.stack.truncate(frame.stack_base);
1366                    }
1367                }
1368
1369                // ── I/O (write directly, no intermediate Vec) ──
1370                Op::Print(n) => {
1371                    let n = *n;
1372                    let start = self.stack.len().saturating_sub(n as usize);
1373                    use std::io::Write;
1374                    let stdout = std::io::stdout();
1375                    let mut lock = stdout.lock();
1376                    for v in &self.stack[start..] {
1377                        let _ = write!(lock, "{}", v.as_str_cow());
1378                    }
1379                    self.stack.truncate(start);
1380                }
1381                Op::PrintLn(n) => {
1382                    let n = *n;
1383                    let start = self.stack.len().saturating_sub(n as usize);
1384                    use std::io::Write;
1385                    let stdout = std::io::stdout();
1386                    let mut lock = stdout.lock();
1387                    for v in &self.stack[start..] {
1388                        let _ = write!(lock, "{}", v.as_str_cow());
1389                    }
1390                    let _ = writeln!(lock);
1391                    self.stack.truncate(start);
1392                }
1393                Op::ReadLine => {
1394                    let mut line = String::new();
1395                    let _ = std::io::stdin().read_line(&mut line);
1396                    self.push(Value::str(line.trim_end_matches('\n')));
1397                }
1398
1399                // ── Fused superinstructions ──
1400                Op::PreIncSlot(slot) => {
1401                    let val = self.get_slot(*slot).to_int() + 1;
1402                    self.set_slot(*slot, Value::Int(val));
1403                    self.push(Value::Int(val));
1404                }
1405                Op::PreIncSlotVoid(slot) => {
1406                    let val = self.get_slot(*slot).to_int() + 1;
1407                    self.set_slot(*slot, Value::Int(val));
1408                }
1409                Op::SlotLtIntJumpIfFalse(slot, limit, target) => {
1410                    if self.get_slot(*slot).to_int() >= *limit as i64 {
1411                        self.ip = *target;
1412                    }
1413                }
1414                Op::SlotIncLtIntJumpBack(slot, limit, target) => {
1415                    let val = self.get_slot(*slot).to_int() + 1;
1416                    self.set_slot(*slot, Value::Int(val));
1417                    if val < *limit as i64 {
1418                        self.ip = *target;
1419                    }
1420                }
1421                Op::AccumSumLoop(sum_slot, i_slot, limit) => {
1422                    let mut sum = self.get_slot(*sum_slot).to_int();
1423                    let mut i = self.get_slot(*i_slot).to_int();
1424                    let lim = *limit as i64;
1425                    while i < lim {
1426                        sum += i;
1427                        i += 1;
1428                    }
1429                    self.set_slot(*sum_slot, Value::Int(sum));
1430                    self.set_slot(*i_slot, Value::Int(i));
1431                }
1432                Op::AddAssignSlotVoid(a, b) => {
1433                    let sum = self.get_slot(*a).to_int() + self.get_slot(*b).to_int();
1434                    self.set_slot(*a, Value::Int(sum));
1435                }
1436                Op::PreDecSlot(slot) => {
1437                    let val = self.get_slot(*slot).to_int() - 1;
1438                    self.set_slot(*slot, Value::Int(val));
1439                    self.push(Value::Int(val));
1440                }
1441                Op::PostIncSlot(slot) => {
1442                    let old = self.get_slot(*slot).to_int();
1443                    self.set_slot(*slot, Value::Int(old + 1));
1444                    self.push(Value::Int(old));
1445                }
1446                Op::PostDecSlot(slot) => {
1447                    let old = self.get_slot(*slot).to_int();
1448                    self.set_slot(*slot, Value::Int(old - 1));
1449                    self.push(Value::Int(old));
1450                }
1451
1452                // ── Status ──
1453                Op::SetStatus => {
1454                    self.last_status = self.pop().to_int() as i32;
1455                }
1456                Op::GetStatus => {
1457                    self.push(Value::Status(self.last_status));
1458                }
1459
1460                // ── Extension dispatch ──
1461                Op::Extended(id, arg) => {
1462                    let (id, arg) = (*id, *arg);
1463                    if let Some(mut handler) = self.ext_handler.take() {
1464                        handler(self, id, arg);
1465                        self.ext_handler = Some(handler);
1466                    }
1467                }
1468                Op::ExtendedWide(id, payload) => {
1469                    let (id, payload) = (*id, *payload);
1470                    if crate::awk_builtins::is_awk_op(id) {
1471                        self.dispatch_awk(id, payload);
1472                    } else if let Some(mut handler) = self.ext_wide_handler.take() {
1473                        handler(self, id, payload);
1474                        self.ext_wide_handler = Some(handler);
1475                    }
1476                }
1477
1478                // ── Arrays ──
1479                Op::GetArray(idx) => {
1480                    let val = self.get_var(*idx);
1481                    self.push(val);
1482                }
1483                Op::SetArray(idx) => {
1484                    let val = self.pop();
1485                    self.set_var(*idx, val);
1486                }
1487                Op::DeclareArray(idx) => {
1488                    self.set_var(*idx, Value::Array(Vec::new()));
1489                }
1490                Op::ArrayGet(arr_idx) => {
1491                    let index = self.pop().to_int() as usize;
1492                    let idx = *arr_idx as usize;
1493                    let val = if idx < self.globals.len() {
1494                        if let Value::Array(ref arr) = self.globals[idx] {
1495                            arr.get(index).cloned().unwrap_or(Value::Undef)
1496                        } else {
1497                            Value::Undef
1498                        }
1499                    } else {
1500                        Value::Undef
1501                    };
1502                    self.push(val);
1503                }
1504                Op::ArraySet(arr_idx) => {
1505                    let index = self.pop().to_int() as usize;
1506                    let val = self.pop();
1507                    let idx = *arr_idx as usize;
1508                    if idx >= self.globals.len() {
1509                        self.globals.resize(idx + 1, Value::Undef);
1510                    }
1511                    if let Value::Array(ref mut vec) = self.globals[idx] {
1512                        if index >= vec.len() {
1513                            vec.resize(index + 1, Value::Undef);
1514                        }
1515                        vec[index] = val;
1516                    }
1517                }
1518                Op::ArrayPush(arr_idx) => {
1519                    let val = self.pop();
1520                    let idx = *arr_idx as usize;
1521                    if idx >= self.globals.len() {
1522                        self.globals.resize(idx + 1, Value::Undef);
1523                    }
1524                    if let Value::Array(ref mut vec) = self.globals[idx] {
1525                        vec.push(val);
1526                    }
1527                }
1528                Op::ArrayPop(arr_idx) => {
1529                    let idx = *arr_idx as usize;
1530                    let val = if idx < self.globals.len() {
1531                        if let Value::Array(ref mut vec) = self.globals[idx] {
1532                            vec.pop().unwrap_or(Value::Undef)
1533                        } else {
1534                            Value::Undef
1535                        }
1536                    } else {
1537                        Value::Undef
1538                    };
1539                    self.push(val);
1540                }
1541                Op::ArrayShift(arr_idx) => {
1542                    let idx = *arr_idx as usize;
1543                    let val = if idx < self.globals.len() {
1544                        if let Value::Array(ref mut vec) = self.globals[idx] {
1545                            if vec.is_empty() {
1546                                Value::Undef
1547                            } else {
1548                                vec.remove(0)
1549                            }
1550                        } else {
1551                            Value::Undef
1552                        }
1553                    } else {
1554                        Value::Undef
1555                    };
1556                    self.push(val);
1557                }
1558                Op::ArrayLen(arr_idx) => {
1559                    let idx = *arr_idx as usize;
1560                    let len = if idx < self.globals.len() {
1561                        if let Value::Array(ref vec) = self.globals[idx] {
1562                            vec.len() as i64
1563                        } else {
1564                            0
1565                        }
1566                    } else {
1567                        0
1568                    };
1569                    self.push(Value::Int(len));
1570                }
1571                Op::MakeArray(n) => {
1572                    let n = *n;
1573                    let start = self.stack.len().saturating_sub(n as usize);
1574                    let elements: Vec<Value> = self.stack.drain(start..).collect();
1575                    self.push(Value::Array(elements));
1576                }
1577
1578                // ── Hashes ──
1579                Op::GetHash(idx) => {
1580                    let val = self.get_var(*idx);
1581                    self.push(val);
1582                }
1583                Op::SetHash(idx) => {
1584                    let val = self.pop();
1585                    self.set_var(*idx, val);
1586                }
1587                Op::DeclareHash(idx) => {
1588                    self.set_var(*idx, Value::Hash(std::collections::HashMap::new()));
1589                }
1590                Op::HashGet(hash_idx) => {
1591                    let key_val = self.pop();
1592                    let key = key_val.as_str_cow();
1593                    let idx = *hash_idx as usize;
1594                    let val = if idx < self.globals.len() {
1595                        if let Value::Hash(ref map) = self.globals[idx] {
1596                            map.get(key.as_ref()).cloned().unwrap_or(Value::Undef)
1597                        } else {
1598                            Value::Undef
1599                        }
1600                    } else {
1601                        Value::Undef
1602                    };
1603                    self.push(val);
1604                }
1605                Op::HashSet(hash_idx) => {
1606                    let key = self.pop().to_str();
1607                    let val = self.pop();
1608                    let idx = *hash_idx as usize;
1609                    if idx >= self.globals.len() {
1610                        self.globals.resize(idx + 1, Value::Undef);
1611                    }
1612                    if let Value::Hash(ref mut map) = self.globals[idx] {
1613                        map.insert(key, val);
1614                    }
1615                }
1616                Op::HashDelete(hash_idx) => {
1617                    let key_val = self.pop();
1618                    let key = key_val.as_str_cow();
1619                    let idx = *hash_idx as usize;
1620                    let val = if idx < self.globals.len() {
1621                        if let Value::Hash(ref mut map) = self.globals[idx] {
1622                            map.remove(key.as_ref()).unwrap_or(Value::Undef)
1623                        } else {
1624                            Value::Undef
1625                        }
1626                    } else {
1627                        Value::Undef
1628                    };
1629                    self.push(val);
1630                }
1631                Op::HashExists(hash_idx) => {
1632                    let key_val = self.pop();
1633                    let key = key_val.as_str_cow();
1634                    let idx = *hash_idx as usize;
1635                    let val = if idx < self.globals.len() {
1636                        if let Value::Hash(ref map) = self.globals[idx] {
1637                            map.contains_key(key.as_ref())
1638                        } else {
1639                            false
1640                        }
1641                    } else {
1642                        false
1643                    };
1644                    self.push(Value::Bool(val));
1645                }
1646                Op::HashKeys(hash_idx) => {
1647                    let idx = *hash_idx as usize;
1648                    let arr = if idx < self.globals.len() {
1649                        if let Value::Hash(ref map) = self.globals[idx] {
1650                            let mut keys = Vec::with_capacity(map.len());
1651                            keys.extend(map.keys().map(|k| Value::str(k.as_str())));
1652                            keys
1653                        } else {
1654                            Vec::new()
1655                        }
1656                    } else {
1657                        Vec::new()
1658                    };
1659                    self.push(Value::Array(arr));
1660                }
1661                Op::HashValues(hash_idx) => {
1662                    let idx = *hash_idx as usize;
1663                    let arr = if idx < self.globals.len() {
1664                        if let Value::Hash(ref map) = self.globals[idx] {
1665                            let mut vals = Vec::with_capacity(map.len());
1666                            vals.extend(map.values().cloned());
1667                            vals
1668                        } else {
1669                            Vec::new()
1670                        }
1671                    } else {
1672                        Vec::new()
1673                    };
1674                    self.push(Value::Array(arr));
1675                }
1676                Op::MakeHash(n) => {
1677                    let n = *n;
1678                    let start = self.stack.len().saturating_sub(n as usize);
1679                    let pairs: Vec<Value> = self.stack.drain(start..).collect();
1680                    let mut map = std::collections::HashMap::with_capacity(pairs.len() / 2);
1681                    let mut iter = pairs.into_iter();
1682                    while let Some(key) = iter.next() {
1683                        if let Some(val) = iter.next() {
1684                            map.insert(key.to_str(), val);
1685                        }
1686                    }
1687                    self.push(Value::Hash(map));
1688                }
1689
1690                // ── Range ──
1691                Op::Range => {
1692                    let to = self.pop().to_int();
1693                    let from = self.pop().to_int();
1694                    let cap = (to - from + 1).max(0) as usize;
1695                    let mut arr = Vec::with_capacity(cap);
1696                    arr.extend((from..=to).map(Value::Int));
1697                    self.push(Value::Array(arr));
1698                }
1699                Op::RangeStep => {
1700                    let step = self.pop().to_int();
1701                    let to = self.pop().to_int();
1702                    let from = self.pop().to_int();
1703                    let cap = if step > 0 {
1704                        ((to - from) / step + 1).max(0) as usize
1705                    } else if step < 0 {
1706                        ((from - to) / (-step) + 1).max(0) as usize
1707                    } else {
1708                        0
1709                    };
1710                    let mut arr = Vec::with_capacity(cap);
1711                    if step > 0 {
1712                        let mut i = from;
1713                        while i <= to {
1714                            arr.push(Value::Int(i));
1715                            i += step;
1716                        }
1717                    } else if step < 0 {
1718                        let mut i = from;
1719                        while i >= to {
1720                            arr.push(Value::Int(i));
1721                            i += step;
1722                        }
1723                    }
1724                    self.push(Value::Array(arr));
1725                }
1726
1727                // ── Shell ops ──
1728                Op::TestFile(test_type) => {
1729                    let test_type = *test_type;
1730                    let path = self.pop().to_str();
1731                    let result = match test_type {
1732                        crate::op::file_test::EXISTS => std::path::Path::new(&path).exists(),
1733                        crate::op::file_test::IS_FILE => std::path::Path::new(&path).is_file(),
1734                        crate::op::file_test::IS_DIR => std::path::Path::new(&path).is_dir(),
1735                        crate::op::file_test::IS_SYMLINK => {
1736                            std::path::Path::new(&path).is_symlink()
1737                        }
1738                        crate::op::file_test::IS_READABLE | crate::op::file_test::IS_WRITABLE => {
1739                            std::path::Path::new(&path).exists()
1740                        }
1741                        crate::op::file_test::IS_EXECUTABLE => {
1742                            #[cfg(unix)]
1743                            {
1744                                use std::os::unix::fs::PermissionsExt;
1745                                std::fs::metadata(&path)
1746                                    .map(|m| m.permissions().mode() & 0o111 != 0)
1747                                    .unwrap_or(false)
1748                            }
1749                            #[cfg(not(unix))]
1750                            {
1751                                std::path::Path::new(&path).exists()
1752                            }
1753                        }
1754                        crate::op::file_test::IS_NONEMPTY => std::fs::metadata(&path)
1755                            .map(|m| m.len() > 0)
1756                            .unwrap_or(false),
1757                        crate::op::file_test::IS_SOCKET => {
1758                            #[cfg(unix)]
1759                            {
1760                                use std::os::unix::fs::FileTypeExt;
1761                                std::fs::symlink_metadata(&path)
1762                                    .map(|m| m.file_type().is_socket())
1763                                    .unwrap_or(false)
1764                            }
1765                            #[cfg(not(unix))]
1766                            {
1767                                false
1768                            }
1769                        }
1770                        crate::op::file_test::IS_FIFO => {
1771                            #[cfg(unix)]
1772                            {
1773                                use std::os::unix::fs::FileTypeExt;
1774                                std::fs::symlink_metadata(&path)
1775                                    .map(|m| m.file_type().is_fifo())
1776                                    .unwrap_or(false)
1777                            }
1778                            #[cfg(not(unix))]
1779                            {
1780                                false
1781                            }
1782                        }
1783                        crate::op::file_test::IS_BLOCK_DEV => {
1784                            #[cfg(unix)]
1785                            {
1786                                use std::os::unix::fs::FileTypeExt;
1787                                std::fs::symlink_metadata(&path)
1788                                    .map(|m| m.file_type().is_block_device())
1789                                    .unwrap_or(false)
1790                            }
1791                            #[cfg(not(unix))]
1792                            {
1793                                false
1794                            }
1795                        }
1796                        crate::op::file_test::IS_CHAR_DEV => {
1797                            #[cfg(unix)]
1798                            {
1799                                use std::os::unix::fs::FileTypeExt;
1800                                std::fs::symlink_metadata(&path)
1801                                    .map(|m| m.file_type().is_char_device())
1802                                    .unwrap_or(false)
1803                            }
1804                            #[cfg(not(unix))]
1805                            {
1806                                false
1807                            }
1808                        }
1809                        _ => false,
1810                    };
1811                    self.push(Value::Bool(result));
1812                }
1813
1814                Op::Exec(argc) => {
1815                    let argc = *argc;
1816                    let start = self.stack.len().saturating_sub(argc as usize);
1817                    // Flatten Value::Array entries into argv. Shell array splice
1818                    // (`${arr[@]}`) pushes a single Array value at compile-time
1819                    // even though it expands to N argv slots at runtime. Without
1820                    // this flat_map, `cmd ${arr[@]}` would pass the whole array
1821                    // as one space-joined arg instead of N separate args.
1822                    let args: Vec<String> = self
1823                        .stack
1824                        .drain(start..)
1825                        .flat_map(|v| match v {
1826                            Value::Array(items) => {
1827                                items.into_iter().map(|i| i.to_str()).collect::<Vec<_>>()
1828                            }
1829                            other => vec![other.to_str()],
1830                        })
1831                        .collect();
1832                    if let Some(cmd) = args.first() {
1833                        // Check if it's a shell function
1834                        let name_idx = self.chunk.names.iter().position(|n| n == cmd);
1835                        if let Some(name_idx) = name_idx {
1836                            if let Some(entry_ip) = self.chunk.find_sub(name_idx as u16) {
1837                                // Push arguments for the function (skip command name)
1838                                for arg in &args[1..] {
1839                                    self.push(Value::str(arg));
1840                                }
1841                                // Push frame and call
1842                                self.frames.push(Frame {
1843                                    return_ip: self.ip,
1844                                    stack_base: self.stack.len() - (args.len() - 1),
1845                                    slots: Vec::with_capacity(8),
1846                                });
1847                                self.ip = entry_ip;
1848                                continue;
1849                            }
1850                        }
1851
1852                        match cmd.as_str() {
1853                            "true" => self.push(Value::Status(0)),
1854                            "false" => self.push(Value::Status(1)),
1855                            "echo" => {
1856                                println!("{}", args[1..].join(" "));
1857                                self.push(Value::Status(0));
1858                            }
1859                            "test" | "[" => {
1860                                self.push(Value::Status(0));
1861                            }
1862                            _ => {
1863                                // Route through the host's `exec` so frontends
1864                                // (zshrs) can apply intercepts/AOP advice/job
1865                                // tracking on dynamic command names like
1866                                // `cmd=ls; $cmd`. The default ShellHost::exec
1867                                // implementation falls back to Command::new,
1868                                // so behavior is identical when no host is
1869                                // wired. Without host, we keep the inline
1870                                // Command::new path so the VM still runs in
1871                                // host-less embeddings (tests, REPL stubs).
1872                                let status = if let Some(h) = self.host.as_mut() {
1873                                    h.exec(args.clone())
1874                                } else {
1875                                    use std::process::{Command, Stdio};
1876                                    Command::new(cmd)
1877                                        .args(&args[1..])
1878                                        .stdout(Stdio::inherit())
1879                                        .stderr(Stdio::inherit())
1880                                        .status()
1881                                        .map(|s| s.code().unwrap_or(1))
1882                                        .unwrap_or(127)
1883                                };
1884                                self.push(Value::Status(status));
1885                            }
1886                        }
1887                    } else {
1888                        self.push(Value::Status(0));
1889                    }
1890                }
1891                Op::ExecBg(argc) => {
1892                    let argc = *argc;
1893                    let start = self.stack.len().saturating_sub(argc as usize);
1894                    // Same Array-flattening as Op::Exec — see comment there.
1895                    let args: Vec<String> = self
1896                        .stack
1897                        .drain(start..)
1898                        .flat_map(|v| match v {
1899                            Value::Array(items) => {
1900                                items.into_iter().map(|i| i.to_str()).collect::<Vec<_>>()
1901                            }
1902                            other => vec![other.to_str()],
1903                        })
1904                        .collect();
1905                    if let Some(cmd) = args.first() {
1906                        // Route bg exec through the host. Frontends override
1907                        // to register the spawned pid in their job table; the
1908                        // default impl spawns and detaches. We DON'T wait on
1909                        // the bg child here — that's the host's responsibility
1910                        // (zshrs uses BUILTIN_RUN_BG which forks before
1911                        // emitting Op::ExecBg, so this path is rare for
1912                        // shell-level bg). Without host, fall back to inline
1913                        // Command::new spawn for host-less embeddings.
1914                        if let Some(h) = self.host.as_mut() {
1915                            let _ = h.exec_bg(args.clone());
1916                        } else {
1917                            use std::process::{Command, Stdio};
1918                            let _ = Command::new(cmd)
1919                                .args(&args[1..])
1920                                .stdout(Stdio::null())
1921                                .stderr(Stdio::null())
1922                                .spawn();
1923                        }
1924                    }
1925                    self.push(Value::Status(0));
1926                }
1927
1928                // ── Shell ops ── (route through host when set, fall back to stubs)
1929                Op::PipelineBegin(n) => {
1930                    let n = *n;
1931                    if let Some(h) = self.host.as_mut() {
1932                        h.pipeline_begin(n);
1933                    }
1934                }
1935                Op::PipelineStage => {
1936                    if let Some(h) = self.host.as_mut() {
1937                        h.pipeline_stage();
1938                    }
1939                }
1940                Op::PipelineEnd => {
1941                    let status = if let Some(h) = self.host.as_mut() {
1942                        h.pipeline_end()
1943                    } else {
1944                        self.last_status
1945                    };
1946                    self.last_status = status;
1947                    self.push(Value::Status(status));
1948                }
1949                Op::SubshellBegin => {
1950                    if let Some(h) = self.host.as_mut() {
1951                        h.subshell_begin();
1952                    }
1953                }
1954                Op::SubshellEnd => {
1955                    if let Some(h) = self.host.as_mut() {
1956                        if let Some(status) = h.subshell_end() {
1957                            self.last_status = status;
1958                        }
1959                    }
1960                }
1961                Op::Redirect(fd, op) => {
1962                    let fd = *fd;
1963                    let op = *op;
1964                    let target = self.pop().to_str();
1965                    if let Some(h) = self.host.as_mut() {
1966                        h.redirect(fd, op, &target);
1967                    }
1968                }
1969                Op::HereDoc(idx) => {
1970                    let content = self
1971                        .chunk
1972                        .constants
1973                        .get(*idx as usize)
1974                        .map(|v| v.to_str())
1975                        .unwrap_or_default();
1976                    if let Some(h) = self.host.as_mut() {
1977                        h.heredoc(&content);
1978                    }
1979                }
1980                Op::HereString => {
1981                    let s = self.pop().to_str();
1982                    if let Some(h) = self.host.as_mut() {
1983                        h.herestring(&s);
1984                    }
1985                }
1986                Op::CmdSubst(idx) => {
1987                    let result = match self.chunk.sub_chunks.get(*idx as usize) {
1988                        Some(sub) => {
1989                            // Split borrow: self.host and self.chunk are disjoint fields
1990                            let sub_ref: *const Chunk = sub;
1991                            // SAFETY: sub_chunks is not mutated during op dispatch
1992                            let sub_ref = unsafe { &*sub_ref };
1993                            if let Some(h) = self.host.as_mut() {
1994                                h.cmd_subst(sub_ref)
1995                            } else {
1996                                String::new()
1997                            }
1998                        }
1999                        None => String::new(),
2000                    };
2001                    self.push(Value::str(result));
2002                }
2003                Op::ProcessSubIn(idx) => {
2004                    let result = match self.chunk.sub_chunks.get(*idx as usize) {
2005                        Some(sub) => {
2006                            let sub_ref: *const Chunk = sub;
2007                            let sub_ref = unsafe { &*sub_ref };
2008                            if let Some(h) = self.host.as_mut() {
2009                                h.process_sub_in(sub_ref)
2010                            } else {
2011                                String::new()
2012                            }
2013                        }
2014                        None => String::new(),
2015                    };
2016                    self.push(Value::str(result));
2017                }
2018                Op::ProcessSubOut(idx) => {
2019                    let result = match self.chunk.sub_chunks.get(*idx as usize) {
2020                        Some(sub) => {
2021                            let sub_ref: *const Chunk = sub;
2022                            let sub_ref = unsafe { &*sub_ref };
2023                            if let Some(h) = self.host.as_mut() {
2024                                h.process_sub_out(sub_ref)
2025                            } else {
2026                                String::new()
2027                            }
2028                        }
2029                        None => String::new(),
2030                    };
2031                    self.push(Value::str(result));
2032                }
2033                Op::Glob | Op::GlobRecursive => {
2034                    let recursive = matches!(&ops[ip], Op::GlobRecursive);
2035                    let pat_val = self.pop();
2036                    let pattern = pat_val.to_str();
2037                    let matches: Vec<String> = if let Some(h) = self.host.as_mut() {
2038                        h.glob(&pattern, recursive)
2039                    } else {
2040                        glob::glob(&pattern)
2041                            .into_iter()
2042                            .flat_map(|paths| paths.filter_map(|p| p.ok()))
2043                            .map(|p| p.to_string_lossy().into_owned())
2044                            .collect()
2045                    };
2046                    let arr: Vec<Value> = matches.into_iter().map(Value::str).collect();
2047                    self.push(Value::Array(arr));
2048                }
2049                Op::TrapSet(idx) => {
2050                    // stack: [signal_name]
2051                    let sig = self.pop().to_str();
2052                    if let Some(sub) = self.chunk.sub_chunks.get(*idx as usize) {
2053                        let sub_ref: *const Chunk = sub;
2054                        let sub_ref = unsafe { &*sub_ref };
2055                        if let Some(h) = self.host.as_mut() {
2056                            h.trap_set(&sig, sub_ref);
2057                        }
2058                    }
2059                }
2060                Op::TrapCheck => {
2061                    if let Some(h) = self.host.as_mut() {
2062                        h.trap_check();
2063                    }
2064                }
2065                Op::TildeExpand => {
2066                    let s = self.pop().to_str();
2067                    let result = if let Some(h) = self.host.as_mut() {
2068                        h.tilde_expand(&s)
2069                    } else {
2070                        s
2071                    };
2072                    self.push(Value::str(result));
2073                }
2074                Op::BraceExpand => {
2075                    let s = self.pop().to_str();
2076                    let result = if let Some(h) = self.host.as_mut() {
2077                        h.brace_expand(&s)
2078                    } else {
2079                        vec![s]
2080                    };
2081                    let arr: Vec<Value> = result.into_iter().map(Value::str).collect();
2082                    self.push(Value::Array(arr));
2083                }
2084                Op::WordSplit => {
2085                    let s = self.pop().to_str();
2086                    let result = if let Some(h) = self.host.as_mut() {
2087                        h.word_split(&s)
2088                    } else {
2089                        s.split_whitespace().map(|w| w.to_string()).collect()
2090                    };
2091                    let arr: Vec<Value> = result.into_iter().map(Value::str).collect();
2092                    self.push(Value::Array(arr));
2093                }
2094                Op::ExpandParam(modifier) => {
2095                    // Stack layout per modifier:
2096                    //   DEFAULT/ASSIGN/ERROR/ALTERNATE/STRIP*/RSTRIP*: [name, arg]
2097                    //   SUBST_FIRST/SUBST_ALL: [name, pat, rep]
2098                    //   SLICE: [name, off, len]
2099                    //   LENGTH/UPPER/LOWER/UPPER_FIRST/LOWER_FIRST/INDIRECT/KEYS: [name]
2100                    let m = *modifier;
2101                    let argc = match m {
2102                        crate::op::param_mod::DEFAULT
2103                        | crate::op::param_mod::ASSIGN
2104                        | crate::op::param_mod::ERROR
2105                        | crate::op::param_mod::ALTERNATE
2106                        | crate::op::param_mod::STRIP_SHORT
2107                        | crate::op::param_mod::STRIP_LONG
2108                        | crate::op::param_mod::RSTRIP_SHORT
2109                        | crate::op::param_mod::RSTRIP_LONG => 1,
2110                        crate::op::param_mod::SUBST_FIRST
2111                        | crate::op::param_mod::SUBST_ALL
2112                        | crate::op::param_mod::SLICE => 2,
2113                        _ => 0,
2114                    };
2115                    let mut args: Vec<Value> = Vec::with_capacity(argc);
2116                    for _ in 0..argc {
2117                        args.push(self.pop());
2118                    }
2119                    args.reverse();
2120                    let name = self.pop().to_str();
2121                    let result = if let Some(h) = self.host.as_mut() {
2122                        h.expand_param(&name, m, &args)
2123                    } else {
2124                        Value::str("")
2125                    };
2126                    self.push(result);
2127                }
2128                Op::StrMatch => {
2129                    let pat = self.pop().to_str();
2130                    let s = self.pop().to_str();
2131                    let result = if let Some(h) = self.host.as_mut() {
2132                        h.str_match(&s, &pat)
2133                    } else {
2134                        s == pat
2135                    };
2136                    self.push(Value::Bool(result));
2137                }
2138                Op::RegexMatch => {
2139                    let re = self.pop().to_str();
2140                    let s = self.pop().to_str();
2141                    let result = if let Some(h) = self.host.as_mut() {
2142                        h.regex_match(&s, &re)
2143                    } else {
2144                        false
2145                    };
2146                    self.push(Value::Bool(result));
2147                }
2148                Op::WithRedirectsBegin(n) => {
2149                    let n = *n;
2150                    if let Some(h) = self.host.as_mut() {
2151                        h.with_redirects_begin(n);
2152                    }
2153                }
2154                Op::WithRedirectsEnd => {
2155                    if let Some(h) = self.host.as_mut() {
2156                        h.with_redirects_end();
2157                    }
2158                }
2159                Op::CallFunction(name_idx, argc) => {
2160                    let name = self
2161                        .chunk
2162                        .names
2163                        .get(*name_idx as usize)
2164                        .cloned()
2165                        .unwrap_or_default();
2166                    let argc = *argc as usize;
2167                    let start = self.stack.len().saturating_sub(argc);
2168                    // Flatten arrays (see Op::Exec for rationale).
2169                    let args: Vec<String> = self
2170                        .stack
2171                        .drain(start..)
2172                        .flat_map(|v| match v {
2173                            Value::Array(items) => {
2174                                items.into_iter().map(|i| i.to_str()).collect::<Vec<_>>()
2175                            }
2176                            other => vec![other.to_str()],
2177                        })
2178                        .collect();
2179                    let status = if let Some(h) = self.host.as_mut() {
2180                        match h.call_function(&name, args.clone()) {
2181                            Some(s) => s,
2182                            None => {
2183                                let mut full = Vec::with_capacity(args.len() + 1);
2184                                full.push(name.clone());
2185                                full.extend(args);
2186                                h.exec(full)
2187                            }
2188                        }
2189                    } else {
2190                        // No host — fall back to in-chunk function lookup, then external exec
2191                        let nidx = *name_idx;
2192                        if let Some(entry_ip) = self.chunk.find_sub(nidx) {
2193                            for arg in &args {
2194                                self.push(Value::str(arg));
2195                            }
2196                            self.frames.push(Frame {
2197                                return_ip: self.ip,
2198                                stack_base: self.stack.len() - args.len(),
2199                                slots: Vec::with_capacity(8),
2200                            });
2201                            self.ip = entry_ip;
2202                            continue;
2203                        }
2204                        let mut full = Vec::with_capacity(args.len() + 1);
2205                        full.push(name);
2206                        full.extend(args);
2207                        use std::process::Command;
2208                        Command::new(&full[0])
2209                            .args(&full[1..])
2210                            .status()
2211                            .map(|s| s.code().unwrap_or(1))
2212                            .unwrap_or(127)
2213                    };
2214                    self.last_status = status;
2215                    self.push(Value::Status(status));
2216                }
2217
2218                // ── Remaining fused ops ──
2219                Op::ConcatConstLoop(const_idx, s_slot, i_slot, limit) => {
2220                    let c_str = self
2221                        .chunk
2222                        .constants
2223                        .get(*const_idx as usize)
2224                        .map(|v| v.as_str_cow())
2225                        .unwrap_or(std::borrow::Cow::Borrowed(""));
2226                    let mut s = self.get_slot(*s_slot).to_str();
2227                    let mut i = self.get_slot(*i_slot).to_int();
2228                    let lim = *limit as i64;
2229                    let iters = (lim - i).max(0) as usize;
2230                    s.reserve(c_str.len() * iters);
2231                    while i < lim {
2232                        s.push_str(&c_str);
2233                        i += 1;
2234                    }
2235                    self.set_slot(*s_slot, Value::str(s));
2236                    self.set_slot(*i_slot, Value::Int(i));
2237                }
2238                Op::PushIntRangeLoop(arr_idx, i_slot, limit) => {
2239                    let mut i = self.get_slot(*i_slot).to_int();
2240                    let lim = *limit as i64;
2241                    let arr = self.get_var(*arr_idx);
2242                    let mut vec = if let Value::Array(v) = arr {
2243                        v
2244                    } else {
2245                        Vec::new()
2246                    };
2247                    vec.reserve((lim - i).max(0) as usize);
2248                    while i < lim {
2249                        vec.push(Value::Int(i));
2250                        i += 1;
2251                    }
2252                    self.set_var(*arr_idx, Value::Array(vec));
2253                    self.set_slot(*i_slot, Value::Int(i));
2254                }
2255
2256                // ── Higher-order (stubs) ──
2257                Op::MapBlock(_)
2258                | Op::GrepBlock(_)
2259                | Op::SortBlock(_)
2260                | Op::SortDefault
2261                | Op::ForEachBlock(_) => {}
2262
2263                // ── Builtins (inline cache) ──
2264                Op::CallBuiltin(id, argc) => {
2265                    let (id, argc) = (*id, *argc);
2266                    if let Some(Some(handler)) = self.builtin_table.get(id as usize) {
2267                        let result = handler(self, argc);
2268                        self.push(result);
2269                    }
2270                }
2271
2272                // ── AWK ops (first-class; dispatched to the AwkHost, same path
2273                //    as the reserved ExtendedWide AWK range) ──
2274                Op::AwkFieldGet => self.dispatch_awk(ab::AWK_FIELD_GET, 0),
2275                Op::AwkFieldSet => self.dispatch_awk(ab::AWK_FIELD_SET, 0),
2276                Op::AwkNf => self.dispatch_awk(ab::AWK_NF, 0),
2277                Op::AwkSetRecord => self.dispatch_awk(ab::AWK_SET_RECORD, 0),
2278                Op::AwkSpecialGet(n) => self.dispatch_awk(ab::AWK_SPECIAL_GET, *n as usize),
2279                Op::AwkSpecialSet(n) => self.dispatch_awk(ab::AWK_SPECIAL_SET, *n as usize),
2280                Op::AwkPrint(argc) => self.dispatch_awk(ab::AWK_PRINT, *argc as usize),
2281                Op::AwkPrintf(argc) => self.dispatch_awk(ab::AWK_PRINTF, *argc as usize),
2282                Op::AwkSprintf(argc) => self.dispatch_awk(ab::AWK_SPRINTF, *argc as usize),
2283                Op::AwkGetline(src) => self.dispatch_awk(ab::AWK_GETLINE, *src as usize),
2284                Op::AwkLength(argc) => self.dispatch_awk(ab::AWK_LENGTH, *argc as usize),
2285                Op::AwkSubstr(argc) => self.dispatch_awk(ab::AWK_SUBSTR, *argc as usize),
2286                Op::AwkIndex => self.dispatch_awk(ab::AWK_INDEX, 0),
2287                Op::AwkSplit(argc) => self.dispatch_awk(ab::AWK_SPLIT, *argc as usize),
2288                Op::AwkSub(argc) => self.dispatch_awk(ab::AWK_SUB, *argc as usize),
2289                Op::AwkGsub(argc) => self.dispatch_awk(ab::AWK_GSUB, *argc as usize),
2290                Op::AwkMatch => self.dispatch_awk(ab::AWK_MATCH, 0),
2291                Op::AwkToLower => self.dispatch_awk(ab::AWK_TOLOWER, 0),
2292                Op::AwkToUpper => self.dispatch_awk(ab::AWK_TOUPPER, 0),
2293                Op::AwkInt => self.dispatch_awk(ab::AWK_INT, 0),
2294                Op::AwkSqrt => self.dispatch_awk(ab::AWK_SQRT, 0),
2295                Op::AwkSin => self.dispatch_awk(ab::AWK_SIN, 0),
2296                Op::AwkCos => self.dispatch_awk(ab::AWK_COS, 0),
2297                Op::AwkExp => self.dispatch_awk(ab::AWK_EXP, 0),
2298                Op::AwkLog => self.dispatch_awk(ab::AWK_LOG, 0),
2299                Op::AwkAtan2 => self.dispatch_awk(ab::AWK_ATAN2, 0),
2300                // awk `a / b` and `a % b`: pop b then a (same order as Op::Div),
2301                // raise the POSIX fatal error on a zero divisor instead of
2302                // yielding Undef. Distinct from the shared shell-arithmetic ops.
2303                Op::AwkDiv => {
2304                    let b = self.pop();
2305                    let a = self.pop();
2306                    let divisor = b.to_float();
2307                    if divisor == 0.0 {
2308                        return VMResult::Error("division by zero attempted".to_string());
2309                    }
2310                    self.push(Value::Float(a.to_float() / divisor));
2311                }
2312                Op::AwkMod => {
2313                    let b = self.pop();
2314                    let a = self.pop();
2315                    let divisor = b.to_float();
2316                    if divisor == 0.0 {
2317                        return VMResult::Error("division by zero attempted in `%'".to_string());
2318                    }
2319                    self.push(Value::Float(a.to_float() % divisor));
2320                }
2321                // Block-JIT-eligible div/mod (see `Op::AwkDivJit`). The
2322                // interpreter behavior is byte-identical to AwkDiv/AwkMod; the
2323                // distinct opcode only changes JIT eligibility.
2324                Op::AwkDivJit => {
2325                    let b = self.pop();
2326                    let a = self.pop();
2327                    let divisor = b.to_float();
2328                    if divisor == 0.0 {
2329                        return VMResult::Error("division by zero attempted".to_string());
2330                    }
2331                    self.push(Value::Float(a.to_float() / divisor));
2332                }
2333                Op::AwkModJit => {
2334                    let b = self.pop();
2335                    let a = self.pop();
2336                    let divisor = b.to_float();
2337                    if divisor == 0.0 {
2338                        return VMResult::Error("division by zero attempted in `%'".to_string());
2339                    }
2340                    self.push(Value::Float(a.to_float() % divisor));
2341                }
2342                // awk sqrt(x) — interpreter path. On negative input, emit the
2343                // generic "awk: warning: sqrt: received negative argument <x>"
2344                // warning to stderr (the JIT-trapped path uses the same generic
2345                // format via the warn libcall, so the two tiers agree).
2346                Op::AwkSqrtJit => {
2347                    let a = self.pop().to_float();
2348                    if a < 0.0 {
2349                        eprintln!("awk: warning: sqrt: received negative argument {a}");
2350                        self.push(Value::Float(f64::NAN));
2351                    } else {
2352                        self.push(Value::Float(a.sqrt()));
2353                    }
2354                }
2355                // awk log(x) — interpreter path. Negative emits the generic warn
2356                // and pushes NaN; zero returns -inf naturally (no lint warn in
2357                // this tier — host frontends that want LINT=1 behavior must use
2358                // the existing `Op::AwkLog` host-dispatched variant).
2359                Op::AwkLogJit => {
2360                    let a = self.pop().to_float();
2361                    if a < 0.0 {
2362                        eprintln!("awk: warning: log: received negative argument {a}");
2363                        self.push(Value::Float(f64::NAN));
2364                    } else {
2365                        self.push(Value::Float(a.ln()));
2366                    }
2367                }
2368                // awk lshift(a, n) — fatal on negative operands. Stack [a, n]:
2369                // pop n then a (matches the awk evaluation order pushed by
2370                // frontends).
2371                Op::AwkLshiftJit => {
2372                    let n = self.pop().to_float();
2373                    let a = self.pop().to_float();
2374                    if a < 0.0 || n < 0.0 {
2375                        return VMResult::Error(
2376                            "lshift: negative values are not allowed".to_string(),
2377                        );
2378                    }
2379                    let shifted = (a as i64).wrapping_shl((n as u32) & 0x3f);
2380                    self.push(Value::Float(shifted as f64));
2381                }
2382                // awk rshift(a, n) — same guard as lshift but logical right.
2383                Op::AwkRshiftJit => {
2384                    let n = self.pop().to_float();
2385                    let a = self.pop().to_float();
2386                    if a < 0.0 || n < 0.0 {
2387                        return VMResult::Error(
2388                            "rshift: negative values are not allowed".to_string(),
2389                        );
2390                    }
2391                    let shifted = ((a as i64) as u64).wrapping_shr((n as u32) & 0x3f);
2392                    self.push(Value::Float(shifted as f64));
2393                }
2394                // awk compl(a) — fatal on negative. `!a` in u64 space then back
2395                // to f64 (the high bits saturate the f64 mantissa, matching
2396                // awkrs's `num_to_u64` semantics).
2397                Op::AwkComplJit => {
2398                    let a = self.pop().to_float();
2399                    if a < 0.0 {
2400                        return VMResult::Error("compl: negative value is not allowed".to_string());
2401                    }
2402                    let v = !(a as i64);
2403                    self.push(Value::Float(v as f64));
2404                }
2405                // awk `$N` numeric read — interpreter path. Calls the same
2406                // host-installed hook as the JIT-compiled variant so behavior
2407                // matches across tiers. Returns 0.0 when no hook is set, which
2408                // matches awk's "missing field" coercion.
2409                Op::AwkGetFieldNum(field_idx) => {
2410                    let v = crate::jit::fusevm_jit_awk_get_field_num(*field_idx as i64);
2411                    self.push(Value::Float(v));
2412                }
2413                Op::PowFloat => {
2414                    let b = self.pop();
2415                    let a = self.pop();
2416                    self.push(Value::Float(a.to_float().powf(b.to_float())));
2417                }
2418                Op::SqrtFloat => {
2419                    let a = self.pop();
2420                    self.push(Value::Float(a.to_float().sqrt()));
2421                }
2422                Op::SinFloat => {
2423                    let a = self.pop();
2424                    self.push(Value::Float(a.to_float().sin()));
2425                }
2426                Op::CosFloat => {
2427                    let a = self.pop();
2428                    self.push(Value::Float(a.to_float().cos()));
2429                }
2430                Op::ExpFloat => {
2431                    let a = self.pop();
2432                    self.push(Value::Float(a.to_float().exp()));
2433                }
2434                Op::Atan2Float => {
2435                    let x = self.pop();
2436                    let y = self.pop();
2437                    self.push(Value::Float(y.to_float().atan2(x.to_float())));
2438                }
2439                Op::LogFloat => {
2440                    let a = self.pop();
2441                    self.push(Value::Float(a.to_float().ln()));
2442                }
2443                Op::AbsFloat => {
2444                    let a = self.pop();
2445                    self.push(Value::Float(a.to_float().abs()));
2446                }
2447                Op::TruncInt => {
2448                    let a = self.pop();
2449                    self.push(Value::Int(a.to_int()));
2450                }
2451                Op::CeilFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().ceil())); }
2452                Op::FloorFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().floor())); }
2453                Op::TruncFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().trunc())); }
2454                Op::RoundFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().round_ties_even())); }
2455                Op::TanFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().tan())); }
2456                Op::AsinFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().asin())); }
2457                Op::AcosFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().acos())); }
2458                Op::AtanFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().atan())); }
2459                Op::SinhFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().sinh())); }
2460                Op::CoshFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().cosh())); }
2461                Op::TanhFloat => { let a = self.pop(); self.push(Value::Float(a.to_float().tanh())); }
2462                Op::Log2Float => { let a = self.pop(); self.push(Value::Float(a.to_float().log2())); }
2463                Op::Log10Float => { let a = self.pop(); self.push(Value::Float(a.to_float().log10())); }
2464                Op::AbsInt => { let a = self.pop(); self.push(Value::Int(a.to_int().wrapping_abs())); }
2465                Op::GcdInt => {
2466                    let b = self.pop().to_int().unsigned_abs();
2467                    let a = self.pop().to_int().unsigned_abs();
2468                    let mut x = a; let mut y = b;
2469                    while y != 0 { let t = x % y; x = y; y = t; }
2470                    self.push(Value::Int(x as i64));
2471                }
2472                Op::LcmInt => {
2473                    let b = self.pop().to_int().unsigned_abs();
2474                    let a = self.pop().to_int().unsigned_abs();
2475                    if a == 0 || b == 0 { self.push(Value::Int(0)); } else {
2476                        let mut x = a; let mut y = b;
2477                        while y != 0 { let t = x % y; x = y; y = t; }
2478                        let prod = (a / x).saturating_mul(b);
2479                        self.push(Value::Int(prod.min(i64::MAX as u64) as i64));
2480                    }
2481                }
2482                Op::TimeInt => {
2483                    let secs = std::time::SystemTime::now()
2484                        .duration_since(std::time::UNIX_EPOCH)
2485                        .map(|d| d.as_secs() as i64)
2486                        .unwrap_or(0);
2487                    self.push(Value::Int(secs));
2488                }
2489                Op::AwkArrayGet(n) => self.dispatch_awk(ab::AWK_ARRAY_GET, *n as usize),
2490                Op::AwkArraySet(n) => self.dispatch_awk(ab::AWK_ARRAY_SET, *n as usize),
2491                Op::AwkArrayExists(n) => self.dispatch_awk(ab::AWK_ARRAY_EXISTS, *n as usize),
2492                Op::AwkArrayDelete(n) => self.dispatch_awk(ab::AWK_ARRAY_DELETE, *n as usize),
2493                Op::AwkArrayClear(n) => self.dispatch_awk(ab::AWK_ARRAY_CLEAR, *n as usize),
2494                Op::AwkArrayLen(n) => self.dispatch_awk(ab::AWK_ARRAY_LEN, *n as usize),
2495                Op::AwkAnd(argc) => self.dispatch_awk(ab::AWK_AND, *argc as usize),
2496                Op::AwkOr(argc) => self.dispatch_awk(ab::AWK_OR, *argc as usize),
2497                Op::AwkXor(argc) => self.dispatch_awk(ab::AWK_XOR, *argc as usize),
2498                Op::AwkCompl => self.dispatch_awk(ab::AWK_COMPL, 0),
2499                Op::AwkLshift => self.dispatch_awk(ab::AWK_LSHIFT, 0),
2500                Op::AwkRshift => self.dispatch_awk(ab::AWK_RSHIFT, 0),
2501                Op::AwkStrtonum => self.dispatch_awk(ab::AWK_STRTONUM, 0),
2502                Op::AwkSystime => self.dispatch_awk(ab::AWK_SYSTIME, 0),
2503                Op::AwkRand => self.dispatch_awk(ab::AWK_RAND, 0),
2504                Op::AwkSrand(argc) => self.dispatch_awk(ab::AWK_SRAND, *argc as usize),
2505                Op::AwkStrftime(argc) => self.dispatch_awk(ab::AWK_STRFTIME, *argc as usize),
2506                Op::AwkMktime(argc) => self.dispatch_awk(ab::AWK_MKTIME, *argc as usize),
2507                Op::AwkOrd => self.dispatch_awk(ab::AWK_ORD, 1),
2508                Op::AwkChr => self.dispatch_awk(ab::AWK_CHR, 1),
2509                Op::AwkMkbool => self.dispatch_awk(ab::AWK_MKBOOL, 1),
2510                Op::AwkIntdiv => self.dispatch_awk(ab::AWK_INTDIV, 2),
2511                Op::AwkIntdiv0 => self.dispatch_awk(ab::AWK_INTDIV0, 2),
2512                Op::AwkGensub(argc) => self.dispatch_awk(ab::AWK_GENSUB, *argc as usize),
2513                Op::AwkSignal(code) => {
2514                    // Raise the AWK control-flow signal and halt this chunk; the
2515                    // frontend driver reads `self.awk_signal()` after `run()`.
2516                    self.awk_signal = Some(*code);
2517                    self.halted = true;
2518                }
2519            }
2520
2521            // Tracing JIT: finalize an active recording on either:
2522            //   (a) the recorder was marked aborted earlier (e.g. trace
2523            //       exceeded MAX_TRACE_LEN, observed CallBuiltin, etc.) —
2524            //       discard and clean up the cache entry, OR
2525            //   (b) the just-dispatched jump landed at the anchor IP —
2526            //       this is the loop-closing backward branch.
2527            // Internal mid-trace branches that DON'T land at the anchor
2528            // continue recording; their direction is captured in
2529            // `recorded_ips` for later compile-time guard emission.
2530            // Only run finalize if the recorder was armed *before* this step;
2531            // a recorder freshly armed inside this step starts recording on
2532            // the next iteration.
2533            #[cfg(feature = "jit")]
2534            if recorder_was_armed && self.recorder.is_some() {
2535                let aborted = self.recorder.as_ref().map_or(false, |r| r.aborted);
2536                // Phase 9: close on the recorded `close_anchor_ip` rather
2537                // than `record_anchor_ip` — for side traces these differ.
2538                let close_ip = self
2539                    .recorder
2540                    .as_ref()
2541                    .map(|r| r.close_anchor_ip)
2542                    .unwrap_or(0);
2543                let was_jump = matches!(
2544                    &ops[ip],
2545                    Op::Jump(_)
2546                        | Op::JumpIfTrue(_)
2547                        | Op::JumpIfFalse(_)
2548                        | Op::JumpIfTrueKeep(_)
2549                        | Op::JumpIfFalseKeep(_)
2550                );
2551                let landed_at_anchor = self.ip == close_ip;
2552                if aborted || (was_jump && landed_at_anchor) {
2553                    self.finalize_recorder();
2554                }
2555            }
2556        }
2557
2558        if let Some(val) = self.stack.pop() {
2559            VMResult::Ok(val)
2560        } else {
2561            VMResult::Halted
2562        }
2563    }
2564
2565    // ── Helpers ──
2566
2567    /// Dispatch one AWK op (`Op::ExtendedWide(id, payload)` with `id` in the
2568    /// reserved AWK range) through the registered [`AwkHost`]. Value operands
2569    /// come from the stack (pushed in source order); `payload` carries the
2570    /// inline integer operand (field index / argument count / name-pool index).
2571    ///
2572    /// When no AWK host is registered, AWK ops are inert: ops that yield a value
2573    /// push a neutral default so the stack stays balanced, statement-form ops
2574    /// (`print`/`delete`/field-set) simply drop their operands.
2575    ///
2576    /// [`AwkHost`]: crate::awk_host::AwkHost
2577    fn dispatch_awk(&mut self, id: u16, payload: usize) {
2578        use crate::awk_builtins as ab;
2579        use crate::awk_host::AwkLvalue;
2580
2581        // Take the host out to satisfy the borrow checker (handlers reach back
2582        // into `self` via the stack); restore it afterwards. Mirrors the
2583        // `ext_handler` take/restore pattern used for `Op::Extended`.
2584        let mut host = match self.awk_host.take() {
2585            Some(h) => h,
2586            None => {
2587                self.dispatch_awk_stub(id, payload);
2588                return;
2589            }
2590        };
2591
2592        // Pop `n` value operands, returned in source (pushed) order.
2593        macro_rules! pop_n {
2594            ($n:expr) => {{
2595                let n = $n;
2596                let mut v: Vec<Value> = (0..n).map(|_| self.pop()).collect();
2597                v.reverse();
2598                v
2599            }};
2600        }
2601        let name_at = |vm: &Self, idx: usize| -> String {
2602            vm.chunk.names.get(idx).cloned().unwrap_or_default()
2603        };
2604
2605        match id {
2606            ab::AWK_FIELD_GET => {
2607                let i = self.pop().to_int();
2608                let v = host.field_get(i);
2609                self.push(v);
2610            }
2611            ab::AWK_FIELD_SET => {
2612                let i = self.pop().to_int();
2613                let v = self.pop();
2614                host.field_set(i, v);
2615            }
2616            ab::AWK_NF => {
2617                let n = host.nf();
2618                self.push(Value::Int(n));
2619            }
2620            ab::AWK_SET_RECORD => {
2621                let v = self.pop();
2622                host.set_record(v);
2623            }
2624            ab::AWK_SPECIAL_GET => {
2625                let name = name_at(self, payload);
2626                let v = host.special_get(&name);
2627                self.push(v);
2628            }
2629            ab::AWK_SPECIAL_SET => {
2630                let name = name_at(self, payload);
2631                let v = self.pop();
2632                host.special_set(&name, v);
2633            }
2634            ab::AWK_PRINT => {
2635                let args = pop_n!(payload);
2636                host.print(&args);
2637            }
2638            ab::AWK_PRINTF => {
2639                let mut args = pop_n!(payload);
2640                let fmt = if args.is_empty() {
2641                    String::new()
2642                } else {
2643                    args.remove(0).to_str()
2644                };
2645                host.printf(&fmt, &args);
2646            }
2647            ab::AWK_SPRINTF => {
2648                let mut args = pop_n!(payload);
2649                let fmt = if args.is_empty() {
2650                    String::new()
2651                } else {
2652                    args.remove(0).to_str()
2653                };
2654                let v = host.sprintf(&fmt, &args);
2655                self.push(v);
2656            }
2657            ab::AWK_GETLINE => {
2658                // For file/command sources the operand string is on the stack.
2659                let operand = match payload {
2660                    ab::getline_source::FILE
2661                    | ab::getline_source::FILE_VAR
2662                    | ab::getline_source::CMD
2663                    | ab::getline_source::CMD_VAR => Some(self.pop().to_str()),
2664                    _ => None,
2665                };
2666                let status = host.getline(payload, operand.as_deref(), None);
2667                self.push(Value::Int(status));
2668            }
2669            ab::AWK_LENGTH => {
2670                let arg = if payload == 0 { None } else { Some(self.pop()) };
2671                let n = host.length(arg.as_ref());
2672                self.push(Value::Int(n));
2673            }
2674            ab::AWK_SUBSTR => {
2675                let args = pop_n!(payload);
2676                let s = args.first().cloned().unwrap_or(Value::str(""));
2677                let m = args.get(1).map(|v| v.to_int()).unwrap_or(1);
2678                let n = args.get(2).map(|v| v.to_int());
2679                let v = host.substr(&s, m, n);
2680                self.push(v);
2681            }
2682            ab::AWK_INDEX => {
2683                let t = self.pop();
2684                let s = self.pop();
2685                let r = host.index(&s, &t);
2686                self.push(Value::Int(r));
2687            }
2688            ab::AWK_SPLIT => {
2689                let args = pop_n!(payload);
2690                let s = args.first().cloned().unwrap_or(Value::str(""));
2691                let arr = args.get(1).map(|v| v.to_str()).unwrap_or_default();
2692                let fs = args.get(2);
2693                let n = host.split(&s, &arr, fs);
2694                self.push(Value::Int(n));
2695            }
2696            ab::AWK_SUB | ab::AWK_GSUB => {
2697                // Stack: [re, repl, target_name]. The target name string lets
2698                // the host write back to the right lvalue (a var here; field /
2699                // array targets use their own dedicated emission).
2700                let args = pop_n!(payload);
2701                let re = args.first().cloned().unwrap_or(Value::str(""));
2702                let repl = args.get(1).cloned().unwrap_or(Value::str(""));
2703                let target = args
2704                    .get(2)
2705                    .map(|v| AwkLvalue::Var(v.to_str()))
2706                    .unwrap_or(AwkLvalue::Field(0));
2707                let n = if id == ab::AWK_SUB {
2708                    host.sub(&re, &repl, &target)
2709                } else {
2710                    host.gsub(&re, &repl, &target)
2711                };
2712                self.push(Value::Int(n));
2713            }
2714            ab::AWK_MATCH => {
2715                let re = self.pop();
2716                let s = self.pop();
2717                let r = host.match_re(&s, &re);
2718                self.push(Value::Int(r));
2719            }
2720            ab::AWK_GENSUB => {
2721                // Stack: [re, repl, how, target?] (payload = argc, 3..=4).
2722                let args = pop_n!(payload);
2723                let re = args.first().cloned().unwrap_or(Value::str(""));
2724                let repl = args.get(1).cloned().unwrap_or(Value::str(""));
2725                let how = args.get(2).cloned().unwrap_or(Value::str("g"));
2726                let target = args.get(3);
2727                let v = host.gensub(&re, &repl, &how, target);
2728                self.push(v);
2729            }
2730            ab::AWK_TOLOWER => {
2731                let s = self.pop();
2732                let v = host.tolower(&s);
2733                self.push(v);
2734            }
2735            ab::AWK_TOUPPER => {
2736                let s = self.pop();
2737                let v = host.toupper(&s);
2738                self.push(v);
2739            }
2740            ab::AWK_INT => {
2741                let x = self.pop();
2742                let v = host.int(&x);
2743                self.push(v);
2744            }
2745            ab::AWK_SQRT => {
2746                let x = self.pop();
2747                let v = host.sqrt(&x);
2748                self.push(v);
2749            }
2750            ab::AWK_SIN => {
2751                let x = self.pop();
2752                let v = host.sin(&x);
2753                self.push(v);
2754            }
2755            ab::AWK_COS => {
2756                let x = self.pop();
2757                let v = host.cos(&x);
2758                self.push(v);
2759            }
2760            ab::AWK_EXP => {
2761                let x = self.pop();
2762                let v = host.exp(&x);
2763                self.push(v);
2764            }
2765            ab::AWK_LOG => {
2766                let x = self.pop();
2767                let v = host.log(&x);
2768                self.push(v);
2769            }
2770            ab::AWK_ATAN2 => {
2771                let x = self.pop();
2772                let y = self.pop();
2773                let v = host.atan2(&y, &x);
2774                self.push(v);
2775            }
2776            ab::AWK_AND => {
2777                let args = pop_n!(payload);
2778                let v = host.and(&args);
2779                self.push(v);
2780            }
2781            ab::AWK_OR => {
2782                let args = pop_n!(payload);
2783                let v = host.or(&args);
2784                self.push(v);
2785            }
2786            ab::AWK_XOR => {
2787                let args = pop_n!(payload);
2788                let v = host.xor(&args);
2789                self.push(v);
2790            }
2791            ab::AWK_COMPL => {
2792                let v = self.pop();
2793                let r = host.compl(&v);
2794                self.push(r);
2795            }
2796            ab::AWK_LSHIFT => {
2797                let n = self.pop();
2798                let v = self.pop();
2799                let r = host.lshift(&v, &n);
2800                self.push(r);
2801            }
2802            ab::AWK_RSHIFT => {
2803                let n = self.pop();
2804                let v = self.pop();
2805                let r = host.rshift(&v, &n);
2806                self.push(r);
2807            }
2808            ab::AWK_STRTONUM => {
2809                let s = self.pop();
2810                let r = host.strtonum(&s);
2811                self.push(r);
2812            }
2813            ab::AWK_SYSTIME => {
2814                let r = host.systime();
2815                self.push(r);
2816            }
2817            ab::AWK_RAND => {
2818                let r = crate::awk_host::awk_rand(&mut self.awk_rand_seed);
2819                self.push(Value::Float(r));
2820            }
2821            ab::AWK_SRAND => {
2822                let n = if payload >= 1 {
2823                    Some(self.pop().to_float() as u32 as u64)
2824                } else {
2825                    None
2826                };
2827                let r = crate::awk_host::awk_srand(&mut self.awk_rand_seed, n);
2828                self.push(Value::Float(r));
2829            }
2830            ab::AWK_STRFTIME => {
2831                let args = pop_n!(payload);
2832                let r = host.strftime(&args);
2833                self.push(r);
2834            }
2835            ab::AWK_MKTIME => {
2836                let args = pop_n!(payload);
2837                let r = host.mktime(&args);
2838                self.push(r);
2839            }
2840            ab::AWK_ORD => {
2841                let a = self.pop();
2842                let r = host.ord(&a);
2843                self.push(r);
2844            }
2845            ab::AWK_CHR => {
2846                let a = self.pop();
2847                let r = host.chr(&a);
2848                self.push(r);
2849            }
2850            ab::AWK_MKBOOL => {
2851                let a = self.pop();
2852                let r = host.mkbool(&a);
2853                self.push(r);
2854            }
2855            ab::AWK_INTDIV => {
2856                let b = self.pop();
2857                let a = self.pop();
2858                let r = host.intdiv(&a, &b);
2859                self.push(r);
2860            }
2861            ab::AWK_INTDIV0 => {
2862                let b = self.pop();
2863                let a = self.pop();
2864                let r = host.intdiv0(&a, &b);
2865                self.push(r);
2866            }
2867            ab::AWK_ARRAY_GET => {
2868                let name = name_at(self, payload);
2869                let key = self.pop();
2870                let v = host.array_get(&name, &key);
2871                self.push(v);
2872            }
2873            ab::AWK_ARRAY_SET => {
2874                let name = name_at(self, payload);
2875                let key = self.pop();
2876                let v = self.pop();
2877                host.array_set(&name, &key, v);
2878            }
2879            ab::AWK_ARRAY_EXISTS => {
2880                let name = name_at(self, payload);
2881                let key = self.pop();
2882                let b = host.array_exists(&name, &key);
2883                self.push(Value::Bool(b));
2884            }
2885            ab::AWK_ARRAY_DELETE => {
2886                let name = name_at(self, payload);
2887                let key = self.pop();
2888                host.array_delete(&name, &key);
2889            }
2890            ab::AWK_ARRAY_CLEAR => {
2891                let name = name_at(self, payload);
2892                host.array_clear(&name);
2893            }
2894            ab::AWK_ARRAY_LEN => {
2895                let name = name_at(self, payload);
2896                let n = host.array_len(&name);
2897                self.push(Value::Int(n));
2898            }
2899            // Unknown AWK op id: drop nothing, push Undef to keep callers that
2900            // expect a value from glitching. (Reserved range, forward-compat.)
2901            _ => self.push(Value::Undef),
2902        }
2903
2904        self.awk_host = Some(host);
2905    }
2906
2907    /// Inert fallback for AWK ops when no [`AwkHost`] is registered. Keeps the
2908    /// stack balanced: value-producing ops push a neutral default; statement
2909    /// ops drop their operands.
2910    fn dispatch_awk_stub(&mut self, id: u16, payload: usize) {
2911        use crate::awk_builtins as ab;
2912        use crate::awk_host::{
2913            awk_canon_nan, awk_chr, awk_compl, awk_fold_and, awk_fold_or, awk_fold_xor, awk_index,
2914            awk_int, awk_intdiv, awk_intdiv0, awk_length, awk_lshift, awk_mkbool, awk_mktime,
2915            awk_ord, awk_rand, awk_rshift, awk_srand, awk_strftime, awk_strtonum, awk_substr,
2916            awk_systime, awk_tolower, awk_toupper,
2917        };
2918        match id {
2919            // value-producing: pop declared operands, push neutral default
2920            ab::AWK_FIELD_GET => {
2921                self.pop();
2922                self.push(Value::str(""));
2923            }
2924            // `length(s)` (scalar form, payload>0) is host-independent — compute
2925            // it natively. `length($0)` (payload==0) and `length(arr)` need the
2926            // host, so they still yield 0 here.
2927            ab::AWK_LENGTH if payload > 0 => {
2928                let s = self.pop();
2929                self.push(Value::Int(awk_length(Some(&s))));
2930            }
2931            ab::AWK_NF | ab::AWK_LENGTH | ab::AWK_ARRAY_LEN => {
2932                self.push(Value::Int(0));
2933            }
2934            ab::AWK_SPECIAL_GET => self.push(Value::Undef),
2935            ab::AWK_SPRINTF => {
2936                for _ in 0..payload {
2937                    self.pop();
2938                }
2939                self.push(Value::str(""));
2940            }
2941            // Host-independent string builtins: compute the real result so these
2942            // AWK ops execute natively even with no registered host. Operand pop
2943            // order mirrors the host path in `dispatch_awk`.
2944            ab::AWK_SUBSTR => {
2945                let mut args: Vec<Value> = (0..payload).map(|_| self.pop()).collect();
2946                args.reverse();
2947                let s = args.first().cloned().unwrap_or(Value::str(""));
2948                let m = args.get(1).map(|v| v.to_int()).unwrap_or(1);
2949                let n = args.get(2).map(|v| v.to_int());
2950                self.push(awk_substr(&s, m, n));
2951            }
2952            ab::AWK_TOLOWER => {
2953                let s = self.pop();
2954                self.push(awk_tolower(&s));
2955            }
2956            ab::AWK_TOUPPER => {
2957                let s = self.pop();
2958                self.push(awk_toupper(&s));
2959            }
2960            // Host-independent numeric builtins: pure f64 math, computed
2961            // natively even with no registered host.
2962            ab::AWK_INT => {
2963                let x = self.pop();
2964                self.push(awk_int(&x));
2965            }
2966            ab::AWK_SQRT => {
2967                let x = self.pop();
2968                self.push(Value::Float(x.to_float().sqrt()));
2969            }
2970            ab::AWK_SIN => {
2971                let x = self.pop();
2972                self.push(Value::Float(awk_canon_nan(x.to_float().sin())));
2973            }
2974            ab::AWK_COS => {
2975                let x = self.pop();
2976                self.push(Value::Float(awk_canon_nan(x.to_float().cos())));
2977            }
2978            ab::AWK_EXP => {
2979                let x = self.pop();
2980                self.push(Value::Float(awk_canon_nan(x.to_float().exp())));
2981            }
2982            ab::AWK_LOG => {
2983                let x = self.pop();
2984                self.push(Value::Float(x.to_float().ln()));
2985            }
2986            ab::AWK_ATAN2 => {
2987                let x = self.pop();
2988                let y = self.pop();
2989                self.push(Value::Float(awk_canon_nan(
2990                    y.to_float().atan2(x.to_float()),
2991                )));
2992            }
2993            // Host-independent bitwise builtins (gawk): pure integer math.
2994            ab::AWK_AND => {
2995                let args: Vec<Value> = {
2996                    let mut v: Vec<Value> = (0..payload).map(|_| self.pop()).collect();
2997                    v.reverse();
2998                    v
2999                };
3000                self.push(Value::Int(awk_fold_and(&args)));
3001            }
3002            ab::AWK_OR => {
3003                let args: Vec<Value> = {
3004                    let mut v: Vec<Value> = (0..payload).map(|_| self.pop()).collect();
3005                    v.reverse();
3006                    v
3007                };
3008                self.push(Value::Int(awk_fold_or(&args)));
3009            }
3010            ab::AWK_XOR => {
3011                let args: Vec<Value> = {
3012                    let mut v: Vec<Value> = (0..payload).map(|_| self.pop()).collect();
3013                    v.reverse();
3014                    v
3015                };
3016                self.push(Value::Int(awk_fold_xor(&args)));
3017            }
3018            ab::AWK_COMPL => {
3019                let v = self.pop();
3020                self.push(Value::Int(awk_compl(&v)));
3021            }
3022            ab::AWK_LSHIFT => {
3023                let n = self.pop();
3024                let v = self.pop();
3025                self.push(Value::Int(awk_lshift(&v, &n)));
3026            }
3027            ab::AWK_RSHIFT => {
3028                let n = self.pop();
3029                let v = self.pop();
3030                self.push(Value::Int(awk_rshift(&v, &n)));
3031            }
3032            ab::AWK_STRTONUM => {
3033                let s = self.pop();
3034                self.push(Value::Float(awk_strtonum(&s.to_str())));
3035            }
3036            ab::AWK_SYSTIME => {
3037                self.push(Value::Float(awk_systime()));
3038            }
3039            ab::AWK_RAND => {
3040                let r = awk_rand(&mut self.awk_rand_seed);
3041                self.push(Value::Float(r));
3042            }
3043            ab::AWK_SRAND => {
3044                let n = if payload >= 1 {
3045                    Some(self.pop().to_float() as u32 as u64)
3046                } else {
3047                    None
3048                };
3049                let r = awk_srand(&mut self.awk_rand_seed, n);
3050                self.push(Value::Float(r));
3051            }
3052            ab::AWK_STRFTIME => {
3053                let mut args: Vec<Value> = (0..payload).map(|_| self.pop()).collect();
3054                args.reverse();
3055                self.push(awk_strftime(&args));
3056            }
3057            ab::AWK_MKTIME => {
3058                let mut args: Vec<Value> = (0..payload).map(|_| self.pop()).collect();
3059                args.reverse();
3060                self.push(awk_mktime(&args));
3061            }
3062            ab::AWK_ORD => {
3063                let a = self.pop();
3064                self.push(awk_ord(&a));
3065            }
3066            ab::AWK_CHR => {
3067                let a = self.pop();
3068                self.push(awk_chr(&a));
3069            }
3070            ab::AWK_MKBOOL => {
3071                let a = self.pop();
3072                self.push(awk_mkbool(&a));
3073            }
3074            ab::AWK_INTDIV => {
3075                let b = self.pop();
3076                let a = self.pop();
3077                self.push(awk_intdiv(&a, &b));
3078            }
3079            ab::AWK_INTDIV0 => {
3080                let b = self.pop();
3081                let a = self.pop();
3082                self.push(awk_intdiv0(&a, &b));
3083            }
3084            ab::AWK_INDEX => {
3085                let t = self.pop();
3086                let s = self.pop();
3087                self.push(Value::Int(awk_index(&s, &t)));
3088            }
3089            ab::AWK_MATCH => {
3090                self.pop();
3091                self.pop();
3092                self.push(Value::Int(0));
3093            }
3094            ab::AWK_SPLIT | ab::AWK_SUB | ab::AWK_GSUB => {
3095                for _ in 0..payload {
3096                    self.pop();
3097                }
3098                self.push(Value::Int(0));
3099            }
3100            ab::AWK_GENSUB => {
3101                for _ in 0..payload {
3102                    self.pop();
3103                }
3104                self.push(Value::str(""));
3105            }
3106            ab::AWK_GETLINE => {
3107                if matches!(
3108                    payload,
3109                    ab::getline_source::FILE
3110                        | ab::getline_source::FILE_VAR
3111                        | ab::getline_source::CMD
3112                        | ab::getline_source::CMD_VAR
3113                ) {
3114                    self.pop();
3115                }
3116                self.push(Value::Int(0));
3117            }
3118            ab::AWK_ARRAY_GET => {
3119                self.pop();
3120                self.push(Value::str(""));
3121            }
3122            ab::AWK_ARRAY_EXISTS => {
3123                self.pop();
3124                self.push(Value::Bool(false));
3125            }
3126            // statement-form ops: drop operands, push nothing
3127            ab::AWK_FIELD_SET | ab::AWK_ARRAY_SET => {
3128                self.pop();
3129                self.pop();
3130            }
3131            ab::AWK_SET_RECORD | ab::AWK_SPECIAL_SET | ab::AWK_ARRAY_DELETE => {
3132                self.pop();
3133            }
3134            ab::AWK_PRINT | ab::AWK_PRINTF => {
3135                for _ in 0..payload {
3136                    self.pop();
3137                }
3138            }
3139            ab::AWK_ARRAY_CLEAR => {}
3140            _ => {}
3141        }
3142    }
3143
3144    fn get_var(&self, idx: u16) -> Value {
3145        self.globals
3146            .get(idx as usize)
3147            .cloned()
3148            .unwrap_or(Value::Undef)
3149    }
3150
3151    fn set_var(&mut self, idx: u16, val: Value) {
3152        let idx = idx as usize;
3153        if idx >= self.globals.len() {
3154            self.globals.resize(idx + 1, Value::Undef);
3155        }
3156        self.globals[idx] = val;
3157    }
3158
3159    /// Read a slot from the current (top) call frame.
3160    ///
3161    /// Returns `Value::Undef` when there is no active frame or the slot
3162    /// index is out of range. Public so frontend extension handlers
3163    /// (`set_extension_handler`) can read slot operands without reaching
3164    /// into `frames` directly.
3165    pub fn get_slot(&self, slot: u16) -> Value {
3166        self.frames
3167            .last()
3168            .and_then(|f| f.slots.get(slot as usize))
3169            .cloned()
3170            .unwrap_or(Value::Undef)
3171    }
3172
3173    /// Write a slot in the current (top) call frame, growing the frame's
3174    /// slot vector as needed. No-op when there is no active frame.
3175    ///
3176    /// Public so frontend extension handlers can write slot results back
3177    /// without reaching into `frames` directly.
3178    pub fn set_slot(&mut self, slot: u16, val: Value) {
3179        if let Some(frame) = self.frames.last_mut() {
3180            let idx = slot as usize;
3181            if idx >= frame.slots.len() {
3182                frame.slots.resize(idx + 1, Value::Undef);
3183            }
3184            frame.slots[idx] = val;
3185        }
3186    }
3187}
3188
3189/// Pool of reusable `VM` instances.
3190///
3191/// `VM::new` does ~3 `Vec` allocations (stack, frames, globals) at
3192/// construction. Callers that run many small chunks back-to-back —
3193/// REPL-style invocation, batch script execution, eval loops — pay
3194/// that cost on every call. `VMPool` recycles the allocations: the
3195/// first `acquire` allocates, subsequent acquires pop a previously-
3196/// released VM and reset it via `VM::reset`.
3197///
3198/// # Example
3199///
3200/// ```
3201/// use fusevm::{ChunkBuilder, Op, VMPool, VMResult, Value};
3202///
3203/// let mut pool = VMPool::new();
3204///
3205/// for _ in 0..1000 {
3206///     let mut b = ChunkBuilder::new();
3207///     b.emit(Op::LoadInt(40), 1);
3208///     b.emit(Op::LoadInt(2), 1);
3209///     b.emit(Op::Add, 1);
3210///
3211///     let mut vm = pool.acquire(b.build());
3212///     let result = vm.run();
3213///     assert!(matches!(result, VMResult::Ok(Value::Int(42))));
3214///     pool.release(vm);
3215/// }
3216/// ```
3217pub struct VMPool {
3218    pool: Vec<VM>,
3219}
3220
3221impl VMPool {
3222    /// Construct an empty pool.
3223    pub fn new() -> Self {
3224        Self { pool: Vec::new() }
3225    }
3226
3227    /// Construct with a pre-allocated capacity.
3228    pub fn with_capacity(cap: usize) -> Self {
3229        Self {
3230            pool: Vec::with_capacity(cap),
3231        }
3232    }
3233
3234    /// Acquire a VM ready to run `chunk`. Pops a recycled VM if
3235    /// available; otherwise constructs a fresh one. The returned VM
3236    /// inherits the pool's previously-released VMs' allocations
3237    /// (Vec capacities preserved).
3238    pub fn acquire(&mut self, chunk: Chunk) -> VM {
3239        if let Some(mut vm) = self.pool.pop() {
3240            vm.reset(chunk);
3241            vm
3242        } else {
3243            VM::new(chunk)
3244        }
3245    }
3246
3247    /// Return a VM to the pool for later reuse. The VM's allocations
3248    /// are kept; only state is cleared on the next `acquire`.
3249    pub fn release(&mut self, vm: VM) {
3250        self.pool.push(vm);
3251    }
3252
3253    /// Run a closure against an acquired VM, returning it to the pool
3254    /// after the closure finishes (RAII-style scope).
3255    pub fn with<F, T>(&mut self, chunk: Chunk, f: F) -> T
3256    where
3257        F: FnOnce(&mut VM) -> T,
3258    {
3259        let mut vm = self.acquire(chunk);
3260        let r = f(&mut vm);
3261        self.release(vm);
3262        r
3263    }
3264
3265    /// Number of VMs currently held in the pool (released, ready for
3266    /// reuse). Doesn't count VMs currently checked out via `acquire`.
3267    pub fn len(&self) -> usize {
3268        self.pool.len()
3269    }
3270
3271    /// Whether the pool is empty.
3272    pub fn is_empty(&self) -> bool {
3273        self.pool.is_empty()
3274    }
3275}
3276
3277impl Default for VMPool {
3278    fn default() -> Self {
3279        Self::new()
3280    }
3281}
3282
3283#[cfg(test)]
3284mod tests {
3285    use super::*;
3286    use crate::chunk::ChunkBuilder;
3287
3288    #[test]
3289    fn test_arithmetic() {
3290        let mut b = ChunkBuilder::new();
3291        b.emit(Op::LoadInt(10), 1);
3292        b.emit(Op::LoadInt(32), 1);
3293        b.emit(Op::Add, 1);
3294        let mut vm = VM::new(b.build());
3295        match vm.run() {
3296            VMResult::Ok(Value::Int(42)) => {}
3297            other => panic!("expected Int(42), got {:?}", other),
3298        }
3299    }
3300
3301    #[test]
3302    fn test_jump() {
3303        let mut b = ChunkBuilder::new();
3304        b.emit(Op::LoadInt(1), 1);
3305        b.emit(Op::Jump(3), 1);
3306        b.emit(Op::LoadInt(999), 1); // skipped
3307                                     // ip 3:
3308        b.emit(Op::LoadInt(2), 1);
3309        b.emit(Op::Add, 1);
3310        let mut vm = VM::new(b.build());
3311        match vm.run() {
3312            VMResult::Ok(Value::Int(3)) => {}
3313            other => panic!("expected Int(3), got {:?}", other),
3314        }
3315    }
3316
3317    #[test]
3318    fn test_fused_sum_loop() {
3319        // sum = 0; for i in 0..100 { sum += i }
3320        let mut b = ChunkBuilder::new();
3321        b.emit(Op::PushFrame, 1);
3322        b.emit(Op::LoadInt(0), 1);
3323        b.emit(Op::SetSlot(0), 1); // sum = 0
3324        b.emit(Op::LoadInt(0), 1);
3325        b.emit(Op::SetSlot(1), 1); // i = 0
3326        b.emit(Op::AccumSumLoop(0, 1, 100), 1);
3327        b.emit(Op::GetSlot(0), 1);
3328
3329        let mut vm = VM::new(b.build());
3330        match vm.run() {
3331            VMResult::Ok(Value::Int(4950)) => {}
3332            other => panic!("expected Int(4950), got {:?}", other),
3333        }
3334    }
3335
3336    #[test]
3337    fn test_function_call() {
3338        let mut b = ChunkBuilder::new();
3339        let double_name = b.add_name("double");
3340
3341        // main: push 21, call double, result on stack
3342        b.emit(Op::LoadInt(21), 1);
3343        b.emit(Op::Call(double_name, 1), 1);
3344        let end_jump = b.emit(Op::Jump(0), 1); // jump past function body
3345
3346        // double: arg * 2
3347        let double_ip = b.current_pos();
3348        b.add_sub_entry(double_name, double_ip);
3349        b.emit(Op::LoadInt(2), 2);
3350        b.emit(Op::Mul, 2);
3351        b.emit(Op::ReturnValue, 2);
3352
3353        b.patch_jump(end_jump, b.current_pos());
3354
3355        let mut vm = VM::new(b.build());
3356        match vm.run() {
3357            VMResult::Ok(Value::Int(42)) => {}
3358            other => panic!("expected Int(42), got {:?}", other),
3359        }
3360    }
3361
3362    #[test]
3363    fn test_builtin_cache() {
3364        let mut b = ChunkBuilder::new();
3365        b.emit(Op::LoadInt(10), 1);
3366        b.emit(Op::CallBuiltin(0, 1), 1);
3367        let mut vm = VM::new(b.build());
3368        vm.register_builtin(0, |vm, _argc| {
3369            let val = vm.pop();
3370            Value::Int(val.to_int() * 2)
3371        });
3372        match vm.run() {
3373            VMResult::Ok(Value::Int(20)) => {}
3374            other => panic!("expected Int(20), got {:?}", other),
3375        }
3376    }
3377
3378    // ── helpers ──
3379
3380    fn run_one(ops: Vec<Op>) -> VMResult {
3381        let mut b = ChunkBuilder::new();
3382        for op in ops {
3383            b.emit(op, 1);
3384        }
3385        VM::new(b.build()).run()
3386    }
3387
3388    fn expect_int(ops: Vec<Op>, want: i64) {
3389        match run_one(ops) {
3390            VMResult::Ok(Value::Int(n)) => assert_eq!(n, want),
3391            other => panic!("expected Int({}), got {:?}", want, other),
3392        }
3393    }
3394
3395    fn expect_bool(ops: Vec<Op>, want: bool) {
3396        match run_one(ops) {
3397            VMResult::Ok(Value::Bool(b)) => assert_eq!(b, want),
3398            other => panic!("expected Bool({}), got {:?}", want, other),
3399        }
3400    }
3401
3402    // ── Arithmetic ──
3403
3404    #[test]
3405    fn arithmetic_sub_mul_div_mod() {
3406        expect_int(vec![Op::LoadInt(20), Op::LoadInt(8), Op::Sub], 12);
3407        expect_int(vec![Op::LoadInt(6), Op::LoadInt(7), Op::Mul], 42);
3408        expect_int(vec![Op::LoadInt(20), Op::LoadInt(3), Op::Mod], 2);
3409        // Div returns Float for int operands (no truncating int division).
3410        match run_one(vec![Op::LoadInt(20), Op::LoadInt(5), Op::Div]) {
3411            VMResult::Ok(Value::Float(f)) => assert!((f - 4.0).abs() < 1e-9),
3412            VMResult::Ok(Value::Int(4)) => {} // tolerate either impl
3413            other => panic!("got {:?}", other),
3414        }
3415    }
3416
3417    #[test]
3418    fn arithmetic_negate_and_inc_dec() {
3419        expect_int(vec![Op::LoadInt(5), Op::Negate], -5);
3420        expect_int(vec![Op::LoadInt(5), Op::Inc], 6);
3421        expect_int(vec![Op::LoadInt(5), Op::Dec], 4);
3422    }
3423
3424    #[test]
3425    fn arithmetic_pow_returns_float() {
3426        match run_one(vec![Op::LoadInt(3), Op::LoadInt(4), Op::Pow]) {
3427            VMResult::Ok(Value::Float(f)) => assert!((f - 81.0).abs() < 1e-9),
3428            VMResult::Ok(Value::Int(81)) => {} // tolerate either impl
3429            other => panic!("got {:?}", other),
3430        }
3431    }
3432
3433    // ── Comparison ──
3434
3435    #[test]
3436    fn num_comparisons_produce_booleans() {
3437        expect_bool(vec![Op::LoadInt(1), Op::LoadInt(1), Op::NumEq], true);
3438        expect_bool(vec![Op::LoadInt(1), Op::LoadInt(2), Op::NumEq], false);
3439        expect_bool(vec![Op::LoadInt(1), Op::LoadInt(2), Op::NumLt], true);
3440        expect_bool(vec![Op::LoadInt(1), Op::LoadInt(2), Op::NumGt], false);
3441        expect_bool(vec![Op::LoadInt(2), Op::LoadInt(2), Op::NumLe], true);
3442        expect_bool(vec![Op::LoadInt(2), Op::LoadInt(2), Op::NumGe], true);
3443        expect_bool(vec![Op::LoadInt(2), Op::LoadInt(2), Op::NumNe], false);
3444    }
3445
3446    #[test]
3447    fn spaceship_returns_neg_zero_pos() {
3448        expect_int(vec![Op::LoadInt(1), Op::LoadInt(2), Op::Spaceship], -1);
3449        expect_int(vec![Op::LoadInt(2), Op::LoadInt(2), Op::Spaceship], 0);
3450        expect_int(vec![Op::LoadInt(3), Op::LoadInt(2), Op::Spaceship], 1);
3451    }
3452
3453    #[test]
3454    fn string_comparisons() {
3455        let mut b = ChunkBuilder::new();
3456        let a = b.add_constant(Value::str("alpha"));
3457        let z = b.add_constant(Value::str("beta"));
3458        b.emit(Op::LoadConst(a), 1);
3459        b.emit(Op::LoadConst(z), 1);
3460        b.emit(Op::StrLt, 1);
3461        let mut vm = VM::new(b.build());
3462        assert!(matches!(vm.run(), VMResult::Ok(Value::Bool(true))));
3463    }
3464
3465    #[test]
3466    fn string_eq_and_ne() {
3467        let mut b = ChunkBuilder::new();
3468        let s1 = b.add_constant(Value::str("hi"));
3469        let s2 = b.add_constant(Value::str("hi"));
3470        b.emit(Op::LoadConst(s1), 1);
3471        b.emit(Op::LoadConst(s2), 1);
3472        b.emit(Op::StrEq, 1);
3473        assert!(matches!(
3474            VM::new(b.build()).run(),
3475            VMResult::Ok(Value::Bool(true))
3476        ));
3477    }
3478
3479    // ── Stack manipulation ──
3480
3481    #[test]
3482    fn pop_discards_top() {
3483        // push 1, push 2, pop, → result 1
3484        expect_int(vec![Op::LoadInt(1), Op::LoadInt(2), Op::Pop], 1);
3485    }
3486
3487    #[test]
3488    fn dup_duplicates_top() {
3489        // 5, dup, add → 10
3490        expect_int(vec![Op::LoadInt(5), Op::Dup, Op::Add], 10);
3491    }
3492
3493    #[test]
3494    fn swap_exchanges_top_two() {
3495        // 10, 3, swap, sub → 10 - 3 = 7 (after swap top is 10, next is 3 → 3 - 10 = -7?)
3496        // Sub semantics: pops b then a, returns a - b. After swap, top=10, below=3.
3497        // Pop b=10, a=3 → 3 - 10 = -7.
3498        expect_int(vec![Op::LoadInt(10), Op::LoadInt(3), Op::Swap, Op::Sub], -7);
3499    }
3500
3501    #[test]
3502    fn dup2_duplicates_top_two_values() {
3503        // Dup2 on [3,4] yields [3,4,3,4]. Two Adds collapse to 11 on top.
3504        expect_int(
3505            vec![Op::LoadInt(3), Op::LoadInt(4), Op::Dup2, Op::Add, Op::Add],
3506            11,
3507        );
3508    }
3509
3510    // ── Logical / Bitwise ──
3511
3512    #[test]
3513    fn log_not_inverts_truthiness() {
3514        expect_bool(vec![Op::LoadInt(0), Op::LogNot], true);
3515        expect_bool(vec![Op::LoadInt(1), Op::LogNot], false);
3516        expect_bool(vec![Op::LoadTrue, Op::LogNot], false);
3517        expect_bool(vec![Op::LoadFalse, Op::LogNot], true);
3518    }
3519
3520    #[test]
3521    fn bitwise_ops() {
3522        expect_int(
3523            vec![Op::LoadInt(0b1100), Op::LoadInt(0b1010), Op::BitAnd],
3524            0b1000,
3525        );
3526        expect_int(
3527            vec![Op::LoadInt(0b1100), Op::LoadInt(0b1010), Op::BitOr],
3528            0b1110,
3529        );
3530        expect_int(
3531            vec![Op::LoadInt(0b1100), Op::LoadInt(0b1010), Op::BitXor],
3532            0b0110,
3533        );
3534        expect_int(vec![Op::LoadInt(1), Op::LoadInt(4), Op::Shl], 16);
3535        expect_int(vec![Op::LoadInt(64), Op::LoadInt(2), Op::Shr], 16);
3536    }
3537
3538    #[test]
3539    fn bit_not_inverts_bits() {
3540        expect_int(vec![Op::LoadInt(0), Op::BitNot], -1);
3541    }
3542
3543    // ── Strings ──
3544
3545    #[test]
3546    fn concat_joins_strings() {
3547        let mut b = ChunkBuilder::new();
3548        let h = b.add_constant(Value::str("hello "));
3549        let w = b.add_constant(Value::str("world"));
3550        b.emit(Op::LoadConst(h), 1);
3551        b.emit(Op::LoadConst(w), 1);
3552        b.emit(Op::Concat, 1);
3553        match VM::new(b.build()).run() {
3554            VMResult::Ok(v) => assert_eq!(v.to_str(), "hello world"),
3555            other => panic!("got {:?}", other),
3556        }
3557    }
3558
3559    #[test]
3560    fn string_repeat_op() {
3561        let mut b = ChunkBuilder::new();
3562        let s = b.add_constant(Value::str("ab"));
3563        b.emit(Op::LoadConst(s), 1);
3564        b.emit(Op::LoadInt(3), 1);
3565        b.emit(Op::StringRepeat, 1);
3566        match VM::new(b.build()).run() {
3567            VMResult::Ok(v) => assert_eq!(v.to_str(), "ababab"),
3568            other => panic!("got {:?}", other),
3569        }
3570    }
3571
3572    #[test]
3573    fn string_len_returns_int() {
3574        let mut b = ChunkBuilder::new();
3575        let s = b.add_constant(Value::str("abcd"));
3576        b.emit(Op::LoadConst(s), 1);
3577        b.emit(Op::StringLen, 1);
3578        match VM::new(b.build()).run() {
3579            VMResult::Ok(Value::Int(4)) => {}
3580            other => panic!("got {:?}", other),
3581        }
3582    }
3583
3584    // ── Constants & literals ──
3585
3586    #[test]
3587    fn load_true_false_undef() {
3588        assert!(matches!(
3589            run_one(vec![Op::LoadTrue]),
3590            VMResult::Ok(Value::Bool(true))
3591        ));
3592        assert!(matches!(
3593            run_one(vec![Op::LoadFalse]),
3594            VMResult::Ok(Value::Bool(false))
3595        ));
3596        assert!(matches!(
3597            run_one(vec![Op::LoadUndef]),
3598            VMResult::Ok(Value::Undef)
3599        ));
3600    }
3601
3602    #[test]
3603    fn load_const_string() {
3604        let mut b = ChunkBuilder::new();
3605        let c = b.add_constant(Value::str("xyz"));
3606        b.emit(Op::LoadConst(c), 1);
3607        match VM::new(b.build()).run() {
3608            VMResult::Ok(v) => assert_eq!(v.to_str(), "xyz"),
3609            other => panic!("got {:?}", other),
3610        }
3611    }
3612
3613    // ── Control flow ──
3614
3615    #[test]
3616    fn jump_if_true_taken() {
3617        // load true; JumpIfTrue past "load 0"; load 1 → 1
3618        let mut b = ChunkBuilder::new();
3619        b.emit(Op::LoadTrue, 1);
3620        let j = b.emit(Op::JumpIfTrue(0), 1);
3621        b.emit(Op::LoadInt(0), 1);
3622        b.patch_jump(j, b.current_pos());
3623        b.emit(Op::LoadInt(1), 1);
3624        assert!(matches!(
3625            VM::new(b.build()).run(),
3626            VMResult::Ok(Value::Int(1))
3627        ));
3628    }
3629
3630    #[test]
3631    fn jump_if_false_not_taken_for_true() {
3632        let mut b = ChunkBuilder::new();
3633        b.emit(Op::LoadTrue, 1);
3634        let j = b.emit(Op::JumpIfFalse(0), 1);
3635        b.emit(Op::LoadInt(7), 1); // executed
3636        b.patch_jump(j, b.current_pos());
3637        match VM::new(b.build()).run() {
3638            VMResult::Ok(Value::Int(7)) => {}
3639            other => panic!("got {:?}", other),
3640        }
3641    }
3642
3643    // ── Frame / scope ──
3644
3645    #[test]
3646    fn push_pop_frame_with_slots() {
3647        // PushFrame creates a new frame with its own slot table; GetSlot reads
3648        // back what SetSlot wrote. We omit PopFrame to keep the result on the
3649        // stack at end-of-chunk.
3650        let mut b = ChunkBuilder::new();
3651        b.emit(Op::PushFrame, 1);
3652        b.emit(Op::LoadInt(99), 1);
3653        b.emit(Op::SetSlot(0), 1);
3654        b.emit(Op::GetSlot(0), 1);
3655        match VM::new(b.build()).run() {
3656            VMResult::Ok(Value::Int(99)) => {}
3657            other => panic!("got {:?}", other),
3658        }
3659    }
3660
3661    // ── Public VM helpers: push/pop/peek ──
3662
3663    #[test]
3664    fn vm_push_pop_peek_round_trip() {
3665        let chunk = ChunkBuilder::new().build();
3666        let mut vm = VM::new(chunk);
3667        vm.push(Value::Int(1));
3668        vm.push(Value::Int(2));
3669        assert_eq!(*vm.peek(), Value::Int(2));
3670        assert_eq!(vm.pop(), Value::Int(2));
3671        assert_eq!(vm.pop(), Value::Int(1));
3672    }
3673
3674    // ── register_builtin: overwrite + grow ──
3675
3676    #[test]
3677    fn register_builtin_overwrites_existing_handler() {
3678        let mut b = ChunkBuilder::new();
3679        b.emit(Op::LoadInt(1), 1);
3680        b.emit(Op::CallBuiltin(0, 1), 1);
3681        let mut vm = VM::new(b.build());
3682        vm.register_builtin(0, |vm, _| {
3683            vm.pop();
3684            Value::Int(111)
3685        });
3686        // Overwrite with a different handler before run.
3687        vm.register_builtin(0, |vm, _| {
3688            vm.pop();
3689            Value::Int(222)
3690        });
3691        assert!(matches!(vm.run(), VMResult::Ok(Value::Int(222))));
3692    }
3693
3694    #[test]
3695    fn register_builtin_grows_table_to_high_id() {
3696        // High id should expand the builtin_table to accommodate.
3697        let chunk = ChunkBuilder::new().build();
3698        let mut vm = VM::new(chunk);
3699        vm.register_builtin(500, |_, _| Value::Int(0));
3700        // Indirect proof: re-registering at lower id still works (no panic).
3701        vm.register_builtin(1, |_, _| Value::Int(0));
3702    }
3703
3704    // ── reset() ──
3705
3706    #[test]
3707    fn reset_clears_state_and_runs_new_chunk() {
3708        let mut b1 = ChunkBuilder::new();
3709        b1.emit(Op::LoadInt(1), 1);
3710        let mut vm = VM::new(b1.build());
3711        assert!(matches!(vm.run(), VMResult::Ok(Value::Int(1))));
3712
3713        let mut b2 = ChunkBuilder::new();
3714        b2.emit(Op::LoadInt(2), 1);
3715        b2.emit(Op::LoadInt(3), 1);
3716        b2.emit(Op::Add, 1);
3717        vm.reset(b2.build());
3718        assert!(matches!(vm.run(), VMResult::Ok(Value::Int(5))));
3719    }
3720
3721    // ── Extension handler ──
3722
3723    #[test]
3724    fn extension_handler_invoked_with_payload() {
3725        use std::sync::{Arc, Mutex};
3726        let captured: Arc<Mutex<Option<(u16, u8)>>> = Arc::new(Mutex::new(None));
3727        let captured_cl = Arc::clone(&captured);
3728
3729        let mut b = ChunkBuilder::new();
3730        b.emit(Op::Extended(7, 42), 1);
3731        let mut vm = VM::new(b.build());
3732        vm.set_extension_handler(Box::new(move |vm, id, arg| {
3733            *captured_cl.lock().unwrap() = Some((id, arg));
3734            vm.push(Value::Int(123));
3735        }));
3736        match vm.run() {
3737            VMResult::Ok(Value::Int(123)) => {}
3738            other => panic!("got {:?}", other),
3739        }
3740        assert_eq!(*captured.lock().unwrap(), Some((7, 42)));
3741    }
3742
3743    #[test]
3744    fn extension_wide_handler_invoked_with_payload() {
3745        use std::sync::{Arc, Mutex};
3746        let captured: Arc<Mutex<Option<(u16, usize)>>> = Arc::new(Mutex::new(None));
3747        let captured_cl = Arc::clone(&captured);
3748        let mut b = ChunkBuilder::new();
3749        b.emit(Op::ExtendedWide(9, 9999), 1);
3750        let mut vm = VM::new(b.build());
3751        vm.set_extension_wide_handler(Box::new(move |vm, id, payload| {
3752            *captured_cl.lock().unwrap() = Some((id, payload));
3753            vm.push(Value::Int(0));
3754        }));
3755        let _ = vm.run();
3756        assert_eq!(*captured.lock().unwrap(), Some((9, 9999)));
3757    }
3758
3759    // ── VMPool ──
3760
3761    #[test]
3762    fn vmpool_new_default_and_with_capacity_start_empty() {
3763        let p = VMPool::new();
3764        assert!(p.is_empty());
3765        assert_eq!(p.len(), 0);
3766        let p = VMPool::with_capacity(8);
3767        assert!(p.is_empty());
3768        let p: VMPool = Default::default();
3769        assert!(p.is_empty());
3770    }
3771
3772    #[test]
3773    fn vmpool_release_then_acquire_reuses_vm() {
3774        let mut pool = VMPool::new();
3775        let chunk1 = {
3776            let mut b = ChunkBuilder::new();
3777            b.emit(Op::LoadInt(1), 1);
3778            b.build()
3779        };
3780        let vm = pool.acquire(chunk1);
3781        assert_eq!(pool.len(), 0);
3782        pool.release(vm);
3783        assert_eq!(pool.len(), 1);
3784
3785        let chunk2 = {
3786            let mut b = ChunkBuilder::new();
3787            b.emit(Op::LoadInt(2), 1);
3788            b.build()
3789        };
3790        let mut vm = pool.acquire(chunk2);
3791        assert_eq!(pool.len(), 0);
3792        assert!(matches!(vm.run(), VMResult::Ok(Value::Int(2))));
3793    }
3794
3795    #[test]
3796    fn vmpool_with_returns_value_and_recycles_vm() {
3797        let mut pool = VMPool::new();
3798        let chunk = {
3799            let mut b = ChunkBuilder::new();
3800            b.emit(Op::LoadInt(10), 1);
3801            b.emit(Op::LoadInt(5), 1);
3802            b.emit(Op::Add, 1);
3803            b.build()
3804        };
3805        let result = pool.with(chunk, |vm| match vm.run() {
3806            VMResult::Ok(Value::Int(n)) => n,
3807            other => panic!("got {:?}", other),
3808        });
3809        assert_eq!(result, 15);
3810        assert_eq!(pool.len(), 1, "VM should be returned to pool after with()");
3811    }
3812
3813    // ── AWK host dispatch ──────────────────────────────────────────────────
3814
3815    /// Recording AWK host: captures every routed call so tests can assert the
3816    /// VM popped/pushed the right operands and dispatched to the right method.
3817    #[derive(Default)]
3818    struct RecordingAwkHost {
3819        record: String,
3820        fields: Vec<String>,
3821        printed: Vec<Vec<String>>,
3822        field_sets: Vec<(i64, String)>,
3823        special_sets: Vec<(String, String)>,
3824        array: std::collections::HashMap<String, String>,
3825    }
3826
3827    impl crate::awk_host::AwkHost for RecordingAwkHost {
3828        fn field_get(&mut self, i: i64) -> Value {
3829            Value::str(self.fields.get(i as usize).cloned().unwrap_or_default())
3830        }
3831        fn field_set(&mut self, i: i64, v: Value) {
3832            self.field_sets.push((i, v.to_str()));
3833        }
3834        fn nf(&mut self) -> i64 {
3835            self.fields.len() as i64
3836        }
3837        fn set_record(&mut self, v: Value) {
3838            self.record = v.to_str();
3839            self.fields = self.record.split(' ').map(|s| s.to_string()).collect();
3840        }
3841        fn special_get(&mut self, name: &str) -> Value {
3842            match name {
3843                "NR" => Value::Int(7),
3844                _ => Value::str(""),
3845            }
3846        }
3847        fn special_set(&mut self, name: &str, v: Value) {
3848            self.special_sets.push((name.to_string(), v.to_str()));
3849        }
3850        fn print(&mut self, args: &[Value]) {
3851            self.printed.push(args.iter().map(|v| v.to_str()).collect());
3852        }
3853        fn array_get(&mut self, _arr: &str, key: &Value) -> Value {
3854            Value::str(self.array.get(&key.to_str()).cloned().unwrap_or_default())
3855        }
3856        fn array_set(&mut self, _arr: &str, key: &Value, v: Value) {
3857            self.array.insert(key.to_str(), v.to_str());
3858        }
3859    }
3860
3861    fn awk_op(b: &mut ChunkBuilder, id: u16, payload: usize) {
3862        b.emit(Op::ExtendedWide(id, payload), 1);
3863    }
3864
3865    #[test]
3866    fn awk_field_get_routes_to_host() {
3867        use crate::awk_builtins::*;
3868        let chunk = {
3869            let mut b = ChunkBuilder::new();
3870            b.emit(Op::LoadInt(2), 1); // $2
3871            awk_op(&mut b, AWK_FIELD_GET, 0);
3872            b.build()
3873        };
3874        let mut vm = VM::new(chunk);
3875        let host = RecordingAwkHost {
3876            fields: vec!["a".into(), "b".into(), "c".into()],
3877            ..Default::default()
3878        };
3879        vm.set_awk_host(Box::new(host));
3880        match vm.run() {
3881            VMResult::Ok(v) => assert_eq!(v.to_str(), "c"),
3882            other => panic!("got {:?}", other),
3883        }
3884    }
3885
3886    #[test]
3887    fn awk_print_pops_args_in_source_order() {
3888        use crate::awk_builtins::*;
3889        let chunk = {
3890            let mut b = ChunkBuilder::new();
3891            let x = b.add_constant(Value::str("x"));
3892            let y = b.add_constant(Value::str("y"));
3893            b.emit(Op::LoadConst(x), 1);
3894            b.emit(Op::LoadConst(y), 1);
3895            awk_op(&mut b, AWK_PRINT, 2);
3896            b.build()
3897        };
3898        let mut vm = VM::new(chunk);
3899        // Use a shared host we can inspect after the run.
3900        struct H(std::sync::Arc<std::sync::Mutex<Vec<Vec<String>>>>);
3901        impl crate::awk_host::AwkHost for H {
3902            fn print(&mut self, args: &[Value]) {
3903                self.0
3904                    .lock()
3905                    .unwrap()
3906                    .push(args.iter().map(|v| v.to_str()).collect());
3907            }
3908        }
3909        let sink = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3910        vm.set_awk_host(Box::new(H(sink.clone())));
3911        let _ = vm.run();
3912        assert_eq!(
3913            sink.lock().unwrap().as_slice(),
3914            &[vec!["x".to_string(), "y".to_string()]]
3915        );
3916    }
3917
3918    #[test]
3919    fn awk_field_set_pops_value_and_index() {
3920        use crate::awk_builtins::*;
3921        let chunk = {
3922            let mut b = ChunkBuilder::new();
3923            let v = b.add_constant(Value::str("Z"));
3924            b.emit(Op::LoadConst(v), 1); // value
3925            b.emit(Op::LoadInt(3), 1); // index
3926            awk_op(&mut b, AWK_FIELD_SET, 0);
3927            b.build()
3928        };
3929        struct H(std::sync::Arc<std::sync::Mutex<Vec<(i64, String)>>>);
3930        impl crate::awk_host::AwkHost for H {
3931            fn field_set(&mut self, i: i64, v: Value) {
3932                self.0.lock().unwrap().push((i, v.to_str()));
3933            }
3934        }
3935        let sink = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
3936        let mut vm = VM::new(chunk);
3937        vm.set_awk_host(Box::new(H(sink.clone())));
3938        let _ = vm.run();
3939        assert_eq!(sink.lock().unwrap().as_slice(), &[(3i64, "Z".to_string())]);
3940    }
3941
3942    #[test]
3943    fn awk_special_get_and_array_roundtrip() {
3944        use crate::awk_builtins::*;
3945        let chunk = {
3946            let mut b = ChunkBuilder::new();
3947            let arr = b.add_name("counts");
3948            let k = b.add_constant(Value::str("k"));
3949            let val = b.add_constant(Value::str("42"));
3950            // counts["k"] = "42"
3951            b.emit(Op::LoadConst(val), 1);
3952            b.emit(Op::LoadConst(k), 1);
3953            awk_op(&mut b, AWK_ARRAY_SET, arr as usize);
3954            // push counts["k"] then NR
3955            b.emit(Op::LoadConst(k), 1);
3956            awk_op(&mut b, AWK_ARRAY_GET, arr as usize);
3957            b.build()
3958        };
3959        let mut vm = VM::new(chunk);
3960        vm.set_awk_host(Box::new(RecordingAwkHost::default()));
3961        match vm.run() {
3962            VMResult::Ok(v) => assert_eq!(v.to_str(), "42"),
3963            other => panic!("got {:?}", other),
3964        }
3965    }
3966
3967    #[test]
3968    fn awk_ops_are_inert_without_host_but_keep_stack_balanced() {
3969        use crate::awk_builtins::*;
3970        // $1 with no host → pushes "" (stack stays balanced, run returns it).
3971        let chunk = {
3972            let mut b = ChunkBuilder::new();
3973            b.emit(Op::LoadInt(1), 1);
3974            awk_op(&mut b, AWK_FIELD_GET, 0);
3975            b.build()
3976        };
3977        let mut vm = VM::new(chunk);
3978        match vm.run() {
3979            VMResult::Ok(v) => assert_eq!(v.to_str(), ""),
3980            other => panic!("got {:?}", other),
3981        }
3982    }
3983
3984    // ── First-class AWK ops (Op::Awk*) ──
3985    // These mirror the shell-ops design: named Op variants dispatched to the
3986    // same AwkHost path as the reserved ExtendedWide AWK range. The tests below
3987    // prove each first-class variant routes to the host identically to its
3988    // `ExtendedWide(AWK_*)` form.
3989
3990    #[test]
3991    fn first_class_awk_field_get_routes_to_host() {
3992        let chunk = {
3993            let mut b = ChunkBuilder::new();
3994            b.emit(Op::LoadInt(2), 1); // $2
3995            b.emit(Op::AwkFieldGet, 1);
3996            b.build()
3997        };
3998        let mut vm = VM::new(chunk);
3999        let host = RecordingAwkHost {
4000            fields: vec!["a".into(), "b".into(), "c".into()],
4001            ..Default::default()
4002        };
4003        vm.set_awk_host(Box::new(host));
4004        match vm.run() {
4005            VMResult::Ok(v) => assert_eq!(v.to_str(), "c"),
4006            other => panic!("got {:?}", other),
4007        }
4008    }
4009
4010    #[test]
4011    fn first_class_awk_print_pops_args_in_source_order() {
4012        struct H(std::sync::Arc<std::sync::Mutex<Vec<Vec<String>>>>);
4013        impl crate::awk_host::AwkHost for H {
4014            fn print(&mut self, args: &[Value]) {
4015                self.0
4016                    .lock()
4017                    .unwrap()
4018                    .push(args.iter().map(|v| v.to_str()).collect());
4019            }
4020        }
4021        let chunk = {
4022            let mut b = ChunkBuilder::new();
4023            let x = b.add_constant(Value::str("x"));
4024            let y = b.add_constant(Value::str("y"));
4025            b.emit(Op::LoadConst(x), 1);
4026            b.emit(Op::LoadConst(y), 1);
4027            b.emit(Op::AwkPrint(2), 1);
4028            b.build()
4029        };
4030        let mut vm = VM::new(chunk);
4031        let sink = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
4032        vm.set_awk_host(Box::new(H(sink.clone())));
4033        let _ = vm.run();
4034        assert_eq!(
4035            sink.lock().unwrap().as_slice(),
4036            &[vec!["x".to_string(), "y".to_string()]]
4037        );
4038    }
4039
4040    #[test]
4041    fn first_class_awk_array_roundtrip_matches_extendedwide() {
4042        // counts["k"]="42"; counts["k"] — once via Op::AwkArray*, once via the
4043        // ExtendedWide form; both must yield the same value.
4044        fn run_variant(first_class: bool) -> String {
4045            use crate::awk_builtins::*;
4046            let mut b = ChunkBuilder::new();
4047            let arr = b.add_name("counts");
4048            let k = b.add_constant(Value::str("k"));
4049            let val = b.add_constant(Value::str("42"));
4050            b.emit(Op::LoadConst(val), 1);
4051            b.emit(Op::LoadConst(k), 1);
4052            if first_class {
4053                b.emit(Op::AwkArraySet(arr), 1);
4054            } else {
4055                b.emit(Op::ExtendedWide(AWK_ARRAY_SET, arr as usize), 1);
4056            }
4057            b.emit(Op::LoadConst(k), 1);
4058            if first_class {
4059                b.emit(Op::AwkArrayGet(arr), 1);
4060            } else {
4061                b.emit(Op::ExtendedWide(AWK_ARRAY_GET, arr as usize), 1);
4062            }
4063            let mut vm = VM::new(b.build());
4064            vm.set_awk_host(Box::new(RecordingAwkHost::default()));
4065            match vm.run() {
4066                VMResult::Ok(v) => v.to_str(),
4067                other => panic!("got {:?}", other),
4068            }
4069        }
4070        assert_eq!(run_variant(true), "42");
4071        assert_eq!(run_variant(true), run_variant(false));
4072    }
4073
4074    #[test]
4075    fn first_class_awk_ops_inert_without_host() {
4076        // $1 (Op::AwkFieldGet) with no host → pushes "" and stays balanced.
4077        let chunk = {
4078            let mut b = ChunkBuilder::new();
4079            b.emit(Op::LoadInt(1), 1);
4080            b.emit(Op::AwkFieldGet, 1);
4081            b.build()
4082        };
4083        let mut vm = VM::new(chunk);
4084        match vm.run() {
4085            VMResult::Ok(v) => assert_eq!(v.to_str(), ""),
4086            other => panic!("got {:?}", other),
4087        }
4088    }
4089
4090    // ── Host-independent AWK string builtins execute natively (no host) ──
4091    // `substr`/`tolower`/`toupper`/`index`/`length(s)` need none of AWK's
4092    // host-side runtime state, so they compute real results even when no
4093    // `AwkHost` is registered (unlike field/array/print ops, which stay inert).
4094
4095    fn run_native(chunk: crate::chunk::Chunk) -> Value {
4096        let mut vm = VM::new(chunk);
4097        match vm.run() {
4098            VMResult::Ok(v) => v,
4099            other => panic!("got {:?}", other),
4100        }
4101    }
4102
4103    #[test]
4104    fn awk_substr_executes_natively_without_host() {
4105        // substr("hello", 2, 3) → "ell"
4106        let chunk = {
4107            let mut b = ChunkBuilder::new();
4108            let s = b.add_constant(Value::str("hello"));
4109            b.emit(Op::LoadConst(s), 1);
4110            b.emit(Op::LoadInt(2), 1);
4111            b.emit(Op::LoadInt(3), 1);
4112            b.emit(Op::AwkSubstr(3), 1);
4113            b.build()
4114        };
4115        assert_eq!(run_native(chunk).to_str(), "ell");
4116    }
4117
4118    #[test]
4119    fn awk_substr_two_arg_to_end_without_host() {
4120        // substr("hello", 2) → "ello"
4121        let chunk = {
4122            let mut b = ChunkBuilder::new();
4123            let s = b.add_constant(Value::str("hello"));
4124            b.emit(Op::LoadConst(s), 1);
4125            b.emit(Op::LoadInt(2), 1);
4126            b.emit(Op::AwkSubstr(2), 1);
4127            b.build()
4128        };
4129        assert_eq!(run_native(chunk).to_str(), "ello");
4130    }
4131
4132    #[test]
4133    fn awk_tolower_toupper_execute_natively_without_host() {
4134        let lower = {
4135            let mut b = ChunkBuilder::new();
4136            let s = b.add_constant(Value::str("MiXeD"));
4137            b.emit(Op::LoadConst(s), 1);
4138            b.emit(Op::AwkToLower, 1);
4139            b.build()
4140        };
4141        assert_eq!(run_native(lower).to_str(), "mixed");
4142        let upper = {
4143            let mut b = ChunkBuilder::new();
4144            let s = b.add_constant(Value::str("MiXeD"));
4145            b.emit(Op::LoadConst(s), 1);
4146            b.emit(Op::AwkToUpper, 1);
4147            b.build()
4148        };
4149        assert_eq!(run_native(upper).to_str(), "MIXED");
4150    }
4151
4152    #[test]
4153    fn awk_index_executes_natively_without_host() {
4154        // index("hello", "ll") → 3
4155        let chunk = {
4156            let mut b = ChunkBuilder::new();
4157            let s = b.add_constant(Value::str("hello"));
4158            let t = b.add_constant(Value::str("ll"));
4159            b.emit(Op::LoadConst(s), 1);
4160            b.emit(Op::LoadConst(t), 1);
4161            b.emit(Op::AwkIndex, 1);
4162            b.build()
4163        };
4164        assert_eq!(run_native(chunk).to_int(), 3);
4165    }
4166
4167    #[test]
4168    fn awk_length_scalar_executes_natively_without_host() {
4169        // length("héllo") → 5 chars (not bytes)
4170        let chunk = {
4171            let mut b = ChunkBuilder::new();
4172            let s = b.add_constant(Value::str("héllo"));
4173            b.emit(Op::LoadConst(s), 1);
4174            b.emit(Op::AwkLength(1), 1);
4175            b.build()
4176        };
4177        assert_eq!(run_native(chunk).to_int(), 5);
4178    }
4179
4180    #[test]
4181    fn awk_native_string_ops_match_host_path() {
4182        // The no-host native result must equal the DefaultAwkHost result.
4183        use crate::awk_host::{awk_index, awk_substr, awk_tolower};
4184        assert_eq!(awk_substr(&Value::str("hello"), 2, Some(3)).to_str(), "ell");
4185        assert_eq!(awk_index(&Value::str("hello"), &Value::str("z")), 0);
4186        assert_eq!(awk_tolower(&Value::str("ABC")).to_str(), "abc");
4187    }
4188
4189    // ── Host-independent AWK numeric builtins execute natively (no host) ──
4190    // int/sqrt/sin/cos/exp/log/atan2 are pure f64 math with no AWK runtime
4191    // state, so they compute real results with no `AwkHost` registered.
4192
4193    #[test]
4194    fn awk_int_truncates_toward_zero_without_host() {
4195        for (input, want) in [(3.7_f64, 3_i64), (-3.7, -3), (0.0, 0)] {
4196            let chunk = {
4197                let mut b = ChunkBuilder::new();
4198                let x = b.add_constant(Value::Float(input));
4199                b.emit(Op::LoadConst(x), 1);
4200                b.emit(Op::AwkInt, 1);
4201                b.build()
4202            };
4203            assert_eq!(run_native(chunk).to_int(), want, "int({input})");
4204        }
4205    }
4206
4207    #[test]
4208    fn awk_sqrt_exp_log_execute_natively_without_host() {
4209        let sqrt = {
4210            let mut b = ChunkBuilder::new();
4211            let x = b.add_constant(Value::Float(16.0));
4212            b.emit(Op::LoadConst(x), 1);
4213            b.emit(Op::AwkSqrt, 1);
4214            b.build()
4215        };
4216        assert_eq!(run_native(sqrt).to_float(), 4.0);
4217        let log = {
4218            let mut b = ChunkBuilder::new();
4219            let x = b.add_constant(Value::Float(std::f64::consts::E));
4220            b.emit(Op::LoadConst(x), 1);
4221            b.emit(Op::AwkLog, 1);
4222            b.build()
4223        };
4224        assert!((run_native(log).to_float() - 1.0).abs() < 1e-12);
4225        let exp = {
4226            let mut b = ChunkBuilder::new();
4227            let x = b.add_constant(Value::Float(0.0));
4228            b.emit(Op::LoadConst(x), 1);
4229            b.emit(Op::AwkExp, 1);
4230            b.build()
4231        };
4232        assert_eq!(run_native(exp).to_float(), 1.0);
4233    }
4234
4235    #[test]
4236    fn awk_sin_cos_execute_natively_without_host() {
4237        let sin = {
4238            let mut b = ChunkBuilder::new();
4239            let x = b.add_constant(Value::Float(0.0));
4240            b.emit(Op::LoadConst(x), 1);
4241            b.emit(Op::AwkSin, 1);
4242            b.build()
4243        };
4244        assert_eq!(run_native(sin).to_float(), 0.0);
4245        let cos = {
4246            let mut b = ChunkBuilder::new();
4247            let x = b.add_constant(Value::Float(0.0));
4248            b.emit(Op::LoadConst(x), 1);
4249            b.emit(Op::AwkCos, 1);
4250            b.build()
4251        };
4252        assert_eq!(run_native(cos).to_float(), 1.0);
4253    }
4254
4255    #[test]
4256    fn awk_atan2_pops_y_then_x_without_host() {
4257        // atan2(1, 1) == π/4. Stack order is [y, x] (y pushed first).
4258        let chunk = {
4259            let mut b = ChunkBuilder::new();
4260            let y = b.add_constant(Value::Float(1.0));
4261            let x = b.add_constant(Value::Float(1.0));
4262            b.emit(Op::LoadConst(y), 1);
4263            b.emit(Op::LoadConst(x), 1);
4264            b.emit(Op::AwkAtan2, 1);
4265            b.build()
4266        };
4267        assert!((run_native(chunk).to_float() - std::f64::consts::FRAC_PI_4).abs() < 1e-12);
4268    }
4269
4270    // ── Host-independent AWK bitwise builtins execute natively (no host) ──
4271    // gawk and/or/xor/compl/lshift/rshift are pure integer math (operands
4272    // truncated to u64), ported faithfully from awkrs's f64 path.
4273
4274    fn run_native_int(chunk: crate::chunk::Chunk) -> i64 {
4275        run_native(chunk).to_int()
4276    }
4277
4278    #[test]
4279    fn awk_and_or_xor_execute_natively_without_host() {
4280        // and(12,10)=8, or(12,10)=14, xor(12,10)=6
4281        let mk = |op: Op| {
4282            let mut b = ChunkBuilder::new();
4283            b.emit(Op::LoadInt(12), 1);
4284            b.emit(Op::LoadInt(10), 1);
4285            b.emit(op, 1);
4286            b.build()
4287        };
4288        assert_eq!(run_native_int(mk(Op::AwkAnd(2))), 8);
4289        assert_eq!(run_native_int(mk(Op::AwkOr(2))), 14);
4290        assert_eq!(run_native_int(mk(Op::AwkXor(2))), 6);
4291    }
4292
4293    #[test]
4294    fn awk_and_is_variadic_without_host() {
4295        // and(15, 9, 5) → 15&9=9, 9&5=1
4296        let chunk = {
4297            let mut b = ChunkBuilder::new();
4298            b.emit(Op::LoadInt(15), 1);
4299            b.emit(Op::LoadInt(9), 1);
4300            b.emit(Op::LoadInt(5), 1);
4301            b.emit(Op::AwkAnd(3), 1);
4302            b.build()
4303        };
4304        assert_eq!(run_native_int(chunk), 1);
4305    }
4306
4307    #[test]
4308    fn awk_compl_matches_awkrs_i64_wrap_without_host() {
4309        // awkrs f64 path: compl(0) = (!0u64) as i64 = -1.
4310        let chunk = {
4311            let mut b = ChunkBuilder::new();
4312            b.emit(Op::LoadInt(0), 1);
4313            b.emit(Op::AwkCompl, 1);
4314            b.build()
4315        };
4316        assert_eq!(run_native_int(chunk), -1);
4317    }
4318
4319    #[test]
4320    fn awk_lshift_rshift_execute_natively_without_host() {
4321        // lshift(1,4)=16; rshift(256,4)=16. Stack order is [v, n].
4322        let lshift = {
4323            let mut b = ChunkBuilder::new();
4324            b.emit(Op::LoadInt(1), 1);
4325            b.emit(Op::LoadInt(4), 1);
4326            b.emit(Op::AwkLshift, 1);
4327            b.build()
4328        };
4329        assert_eq!(run_native_int(lshift), 16);
4330        let rshift = {
4331            let mut b = ChunkBuilder::new();
4332            b.emit(Op::LoadInt(256), 1);
4333            b.emit(Op::LoadInt(4), 1);
4334            b.emit(Op::AwkRshift, 1);
4335            b.build()
4336        };
4337        assert_eq!(run_native_int(rshift), 16);
4338    }
4339
4340    #[test]
4341    fn awk_bitwise_free_fns_match_gawk_semantics() {
4342        use crate::awk_host::{awk_compl, awk_fold_and, awk_fold_or, awk_fold_xor, awk_lshift};
4343        assert_eq!(awk_fold_and(&[Value::Int(12), Value::Int(10)]), 8);
4344        assert_eq!(awk_fold_or(&[Value::Int(12), Value::Int(10)]), 14);
4345        assert_eq!(awk_fold_xor(&[Value::Int(12), Value::Int(10)]), 6);
4346        assert_eq!(awk_compl(&Value::Int(0)), -1);
4347        // shift count is masked to low 6 bits (n & 0x3f)
4348        assert_eq!(awk_lshift(&Value::Int(1), &Value::Int(64)), 1);
4349    }
4350
4351    #[test]
4352    fn awk_mktime_utc_executes_natively_without_host() {
4353        // mktime("2020 01 01 00 00 00", 1) in UTC = 1577836800 epoch seconds.
4354        let chunk = {
4355            let mut b = ChunkBuilder::new();
4356            let s = b.add_constant(Value::str("2020 01 01 00 00 00"));
4357            b.emit(Op::LoadConst(s), 1);
4358            b.emit(Op::LoadInt(1), 1); // utc = true
4359            b.emit(Op::AwkMktime(2), 1);
4360            b.build()
4361        };
4362        assert_eq!(run_native(chunk).to_float(), 1_577_836_800.0);
4363    }
4364
4365    #[test]
4366    fn awk_mktime_bad_datespec_returns_minus_one_without_host() {
4367        // Fewer than 6 fields → -1.
4368        let chunk = {
4369            let mut b = ChunkBuilder::new();
4370            let s = b.add_constant(Value::str("2020 01 01"));
4371            b.emit(Op::LoadConst(s), 1);
4372            b.emit(Op::AwkMktime(1), 1);
4373            b.build()
4374        };
4375        assert_eq!(run_native(chunk).to_float(), -1.0);
4376    }
4377
4378    #[test]
4379    fn awk_strftime_utc_executes_natively_without_host() {
4380        // strftime("%Y-%m-%d", 0, 1) in UTC = "1970-01-01".
4381        let chunk = {
4382            let mut b = ChunkBuilder::new();
4383            let fmt = b.add_constant(Value::str("%Y-%m-%d"));
4384            b.emit(Op::LoadConst(fmt), 1);
4385            b.emit(Op::LoadInt(0), 1); // ts = epoch
4386            b.emit(Op::LoadInt(1), 1); // utc = true
4387            b.emit(Op::AwkStrftime(3), 1);
4388            b.build()
4389        };
4390        assert_eq!(run_native(chunk).to_str(), "1970-01-01");
4391    }
4392
4393    #[test]
4394    fn awk_strftime_mktime_free_fns_match_awkrs() {
4395        use crate::awk_host::{awk_mktime, awk_strftime};
4396        // UTC round trip and -1 sentinel.
4397        assert_eq!(
4398            awk_mktime(&[Value::str("2020 01 01 00 00 00"), Value::Int(1)]).to_float(),
4399            1_577_836_800.0
4400        );
4401        assert_eq!(awk_mktime(&[Value::str("garbage")]).to_float(), -1.0);
4402        assert_eq!(
4403            awk_strftime(&[Value::str("%H:%M:%S"), Value::Int(0), Value::Int(1)]).to_str(),
4404            "00:00:00"
4405        );
4406    }
4407
4408    #[test]
4409    fn awk_ord_executes_natively_without_host() {
4410        // ord("A") = 65; ord("") = 0.
4411        let chunk = {
4412            let mut b = ChunkBuilder::new();
4413            let s = b.add_constant(Value::str("ABC"));
4414            b.emit(Op::LoadConst(s), 1);
4415            b.emit(Op::AwkOrd, 1);
4416            b.build()
4417        };
4418        assert_eq!(run_native(chunk).to_float(), 65.0);
4419
4420        let empty = {
4421            let mut b = ChunkBuilder::new();
4422            let s = b.add_constant(Value::str(""));
4423            b.emit(Op::LoadConst(s), 1);
4424            b.emit(Op::AwkOrd, 1);
4425            b.build()
4426        };
4427        assert_eq!(run_native(empty).to_float(), 0.0);
4428    }
4429
4430    #[test]
4431    fn awk_chr_executes_natively_without_host() {
4432        // chr(65) = "A"; chr of an invalid scalar (surrogate) = "".
4433        let chunk = {
4434            let mut b = ChunkBuilder::new();
4435            b.emit(Op::LoadInt(65), 1);
4436            b.emit(Op::AwkChr, 1);
4437            b.build()
4438        };
4439        assert_eq!(run_native(chunk).to_str(), "A");
4440
4441        let bad = {
4442            let mut b = ChunkBuilder::new();
4443            b.emit(Op::LoadInt(0xD800), 1); // lone surrogate → invalid
4444            b.emit(Op::AwkChr, 1);
4445            b.build()
4446        };
4447        assert_eq!(run_native(bad).to_str(), "");
4448    }
4449
4450    #[test]
4451    fn awk_mkbool_executes_natively_without_host() {
4452        // mkbool(7) = 1; mkbool(0) = 0; mkbool("") = 0.
4453        let truthy = {
4454            let mut b = ChunkBuilder::new();
4455            b.emit(Op::LoadInt(7), 1);
4456            b.emit(Op::AwkMkbool, 1);
4457            b.build()
4458        };
4459        assert_eq!(run_native(truthy).to_float(), 1.0);
4460
4461        let zero = {
4462            let mut b = ChunkBuilder::new();
4463            b.emit(Op::LoadInt(0), 1);
4464            b.emit(Op::AwkMkbool, 1);
4465            b.build()
4466        };
4467        assert_eq!(run_native(zero).to_float(), 0.0);
4468    }
4469
4470    #[test]
4471    fn awk_intdiv_executes_natively_without_host() {
4472        // intdiv(17, 5) = 3 (truncating); intdiv(x, 0) = Undef.
4473        let chunk = {
4474            let mut b = ChunkBuilder::new();
4475            b.emit(Op::LoadInt(17), 1);
4476            b.emit(Op::LoadInt(5), 1);
4477            b.emit(Op::AwkIntdiv, 1);
4478            b.build()
4479        };
4480        assert_eq!(run_native(chunk).to_float(), 3.0);
4481
4482        let div0 = {
4483            let mut b = ChunkBuilder::new();
4484            b.emit(Op::LoadInt(17), 1);
4485            b.emit(Op::LoadInt(0), 1);
4486            b.emit(Op::AwkIntdiv, 1);
4487            b.build()
4488        };
4489        assert!(matches!(run_native(div0), Value::Undef));
4490    }
4491
4492    #[test]
4493    fn awk_intdiv0_executes_natively_without_host() {
4494        // intdiv0(17, 5) = 3; intdiv0(x, 0) = 0 (safe variant, never errors).
4495        let chunk = {
4496            let mut b = ChunkBuilder::new();
4497            b.emit(Op::LoadInt(17), 1);
4498            b.emit(Op::LoadInt(5), 1);
4499            b.emit(Op::AwkIntdiv0, 1);
4500            b.build()
4501        };
4502        assert_eq!(run_native(chunk).to_float(), 3.0);
4503
4504        let div0 = {
4505            let mut b = ChunkBuilder::new();
4506            b.emit(Op::LoadInt(17), 1);
4507            b.emit(Op::LoadInt(0), 1);
4508            b.emit(Op::AwkIntdiv0, 1);
4509            b.build()
4510        };
4511        assert_eq!(run_native(div0).to_float(), 0.0);
4512    }
4513
4514    #[test]
4515    fn awk_div_mod_compute_and_trap_on_zero() {
4516        // awk `a / b` and `a % b` compute the float result for a nonzero
4517        // divisor and raise the POSIX fatal runtime error on a zero divisor
4518        // (distinct from the shell-arithmetic Op::Div/Op::Mod which yield
4519        // Undef / 0). Pop order mirrors Op::Div: b is on top, a beneath.
4520        let div = {
4521            let mut b = ChunkBuilder::new();
4522            b.emit(Op::LoadFloat(7.0), 1);
4523            b.emit(Op::LoadFloat(2.0), 1);
4524            b.emit(Op::AwkDiv, 1);
4525            b.build()
4526        };
4527        assert_eq!(run_native(div).to_float(), 3.5);
4528
4529        let md = {
4530            let mut b = ChunkBuilder::new();
4531            b.emit(Op::LoadFloat(7.0), 1);
4532            b.emit(Op::LoadFloat(3.0), 1);
4533            b.emit(Op::AwkMod, 1);
4534            b.build()
4535        };
4536        assert_eq!(run_native(md).to_float(), 1.0);
4537
4538        let div0 = {
4539            let mut b = ChunkBuilder::new();
4540            b.emit(Op::LoadFloat(1.0), 1);
4541            b.emit(Op::LoadFloat(0.0), 1);
4542            b.emit(Op::AwkDiv, 1);
4543            b.build()
4544        };
4545        match VM::new(div0).run() {
4546            VMResult::Error(m) => assert_eq!(m, "division by zero attempted"),
4547            other => panic!("expected div-by-zero trap, got {:?}", other),
4548        }
4549
4550        let mod0 = {
4551            let mut b = ChunkBuilder::new();
4552            b.emit(Op::LoadFloat(1.0), 1);
4553            b.emit(Op::LoadFloat(0.0), 1);
4554            b.emit(Op::AwkMod, 1);
4555            b.build()
4556        };
4557        match VM::new(mod0).run() {
4558            VMResult::Error(m) => assert_eq!(m, "division by zero attempted in `%'"),
4559            other => panic!("expected mod-by-zero trap, got {:?}", other),
4560        }
4561    }
4562
4563    #[test]
4564    fn awk_signal_halts_chunk_and_records_code() {
4565        use crate::awk_builtins::signal;
4566        // `Op::AwkSignal(code)` halts the chunk immediately and stashes `code`
4567        // in the VM for the frontend driver to read; ops after it do not run.
4568        let chunk = {
4569            let mut b = ChunkBuilder::new();
4570            b.emit(Op::LoadInt(1), 1);
4571            b.emit(Op::AwkSignal(signal::NEXTFILE), 1);
4572            // Unreachable once the signal halts the chunk.
4573            b.emit(Op::LoadInt(99), 1);
4574            b.build()
4575        };
4576        let mut vm = VM::new(chunk);
4577        let r = vm.run();
4578        assert_eq!(vm.awk_signal(), Some(signal::NEXTFILE));
4579        // The pre-signal value remains on the stack; the post-signal LoadInt(99)
4580        // never executed (would have been the Ok value otherwise).
4581        match r {
4582            VMResult::Ok(v) => assert_eq!(v.to_float(), 1.0),
4583            other => panic!("expected Ok(1), got {:?}", other),
4584        }
4585
4586        // A signal-free chunk leaves awk_signal None (zshrs/stryke behavior).
4587        let plain = {
4588            let mut b = ChunkBuilder::new();
4589            b.emit(Op::LoadInt(5), 1);
4590            b.build()
4591        };
4592        let mut vm2 = VM::new(plain);
4593        let _ = vm2.run();
4594        assert_eq!(vm2.awk_signal(), None);
4595    }
4596
4597    #[test]
4598    fn awk_gensub_stub_is_stack_balanced_without_host() {
4599        // Host-bound op: with no AwkHost the stub pops all argc operands and
4600        // pushes a neutral empty string, keeping the stack balanced.
4601        let chunk = {
4602            let mut b = ChunkBuilder::new();
4603            let re = b.add_constant(Value::str("x"));
4604            let repl = b.add_constant(Value::str("y"));
4605            let how = b.add_constant(Value::str("g"));
4606            let target = b.add_constant(Value::str("xax"));
4607            b.emit(Op::LoadConst(re), 1);
4608            b.emit(Op::LoadConst(repl), 1);
4609            b.emit(Op::LoadConst(how), 1);
4610            b.emit(Op::LoadConst(target), 1);
4611            b.emit(Op::AwkGensub(4), 1);
4612            b.build()
4613        };
4614        assert_eq!(run_native(chunk).to_str(), "");
4615    }
4616
4617    #[test]
4618    fn awk_char_scalar_free_fns_match_awkrs() {
4619        use crate::awk_host::{awk_chr, awk_intdiv, awk_intdiv0, awk_mkbool, awk_ord};
4620        assert_eq!(awk_ord(&Value::str("z")).to_float(), 122.0);
4621        assert_eq!(awk_chr(&Value::Int(0x1F600)).to_str(), "😀");
4622        assert_eq!(awk_mkbool(&Value::str("0")).to_float(), 0.0);
4623        assert_eq!(awk_mkbool(&Value::str("x")).to_float(), 1.0);
4624        assert_eq!(awk_intdiv(&Value::Int(-7), &Value::Int(2)).to_float(), -3.0);
4625        assert!(matches!(
4626            awk_intdiv(&Value::Int(1), &Value::Int(0)),
4627            Value::Undef
4628        ));
4629        assert_eq!(
4630            awk_intdiv0(&Value::Int(-7), &Value::Int(2)).to_float(),
4631            -3.0
4632        );
4633        assert_eq!(awk_intdiv0(&Value::Int(1), &Value::Int(0)).to_float(), 0.0);
4634    }
4635
4636    #[test]
4637    fn awk_rand_executes_natively_without_host() {
4638        // Fresh VM seeds rand_seed=1; first rand() is deterministic.
4639        let chunk = {
4640            let mut b = ChunkBuilder::new();
4641            b.emit(Op::AwkRand, 1);
4642            b.build()
4643        };
4644        let v = run_native(chunk).to_float();
4645        assert!((0.0..1.0).contains(&v), "rand() = {v} out of [0,1)");
4646        assert_eq!(
4647            v, 0.51385498046875,
4648            "first rand() from seed=1 must be stable"
4649        );
4650    }
4651
4652    #[test]
4653    fn awk_srand_reseeds_and_returns_prev_seed_without_host() {
4654        // srand(42) on a fresh VM (seed=1) returns previous seed 1.0, then the
4655        // next rand() follows the seed-42 sequence.
4656        let chunk = {
4657            let mut b = ChunkBuilder::new();
4658            b.emit(Op::LoadInt(42), 1);
4659            b.emit(Op::AwkSrand(1), 1); // pushes prev seed (1.0)
4660            b.emit(Op::Pop, 1);
4661            b.emit(Op::AwkRand, 1);
4662            b.build()
4663        };
4664        assert_eq!(run_native(chunk).to_float(), 0.582305908203125);
4665    }
4666
4667    #[test]
4668    fn awk_srand_no_arg_returns_prev_seed_without_host() {
4669        // srand() with no arg reseeds from the clock but still returns prev seed
4670        // (1.0 on a fresh VM).
4671        let chunk = {
4672            let mut b = ChunkBuilder::new();
4673            b.emit(Op::AwkSrand(0), 1);
4674            b.build()
4675        };
4676        assert_eq!(run_native(chunk).to_float(), 1.0);
4677    }
4678
4679    #[test]
4680    fn awk_rand_srand_free_fns_match_awkrs_lcg() {
4681        use crate::awk_host::{awk_rand, awk_srand};
4682        let mut seed: u64 = 1;
4683        assert_eq!(awk_rand(&mut seed), 0.51385498046875);
4684        // srand(Some(42)) returns prev seed low-32 bits and reseeds to 42
4685        let prev = awk_srand(&mut seed, Some(42));
4686        assert_eq!(prev, 1103527590.0);
4687        assert_eq!(awk_rand(&mut seed), 0.582305908203125);
4688    }
4689
4690    #[test]
4691    fn awk_systime_executes_natively_without_host() {
4692        // systime() pushes seconds since the Unix epoch; must be a large positive
4693        // number (well past 2020-01-01 = 1577836800) with no host registered.
4694        let chunk = {
4695            let mut b = ChunkBuilder::new();
4696            b.emit(Op::AwkSystime, 1);
4697            b.build()
4698        };
4699        let v = run_native(chunk).to_float();
4700        assert!(v > 1_577_836_800.0, "systime() = {v} should be past 2020");
4701    }
4702
4703    #[test]
4704    fn awk_systime_free_fn_is_positive() {
4705        use crate::awk_host::awk_systime;
4706        assert!(awk_systime() > 1_577_836_800.0);
4707    }
4708
4709    #[test]
4710    fn awk_strtonum_executes_natively_without_host() {
4711        // strtonum("0x10") → 16 (hex), no host registered.
4712        let chunk = {
4713            let mut b = ChunkBuilder::new();
4714            let s = b.add_constant(Value::str("0x10"));
4715            b.emit(Op::LoadConst(s), 1);
4716            b.emit(Op::AwkStrtonum, 1);
4717            b.build()
4718        };
4719        assert_eq!(run_native(chunk).to_float(), 16.0);
4720    }
4721
4722    #[test]
4723    fn awk_strtonum_octal_and_decimal_prefix_without_host() {
4724        // strtonum("010") → 8 (octal); strtonum("42abc") → 42 (longest prefix).
4725        let octal = {
4726            let mut b = ChunkBuilder::new();
4727            let s = b.add_constant(Value::str("010"));
4728            b.emit(Op::LoadConst(s), 1);
4729            b.emit(Op::AwkStrtonum, 1);
4730            b.build()
4731        };
4732        assert_eq!(run_native(octal).to_float(), 8.0);
4733        let prefix = {
4734            let mut b = ChunkBuilder::new();
4735            let s = b.add_constant(Value::str("42abc"));
4736            b.emit(Op::LoadConst(s), 1);
4737            b.emit(Op::AwkStrtonum, 1);
4738            b.build()
4739        };
4740        assert_eq!(run_native(prefix).to_float(), 42.0);
4741    }
4742
4743    #[test]
4744    fn awk_strtonum_free_fn_matches_awkrs_semantics() {
4745        use crate::awk_host::awk_strtonum;
4746        assert_eq!(awk_strtonum(""), 0.0);
4747        assert_eq!(awk_strtonum("   "), 0.0);
4748        assert_eq!(awk_strtonum("0x10"), 16.0);
4749        assert_eq!(awk_strtonum("0Xff"), 255.0);
4750        assert_eq!(awk_strtonum("010"), 8.0);
4751        assert_eq!(awk_strtonum("3.5"), 3.5);
4752        // invalid hex → 0
4753        assert_eq!(awk_strtonum("0x"), 0.0);
4754        assert_eq!(awk_strtonum("0xzz"), 0.0);
4755        // signed disqualifies hex/octal form: "+0x10" parses leading "+0" → 0
4756        assert_eq!(awk_strtonum("+0x10"), 0.0);
4757        // bare nan/inf without sign → 0 (gawk number scan rejects)
4758        assert_eq!(awk_strtonum("nan"), 0.0);
4759        assert_eq!(awk_strtonum("inf"), 0.0);
4760    }
4761
4762    #[test]
4763    fn awk_builtin_substr_default_impl_is_posix() {
4764        use crate::awk_host::{AwkHost, DefaultAwkHost};
4765        let mut h = DefaultAwkHost;
4766        assert_eq!(h.substr(&Value::str("hello"), 2, Some(3)).to_str(), "ell");
4767        assert_eq!(h.substr(&Value::str("hello"), 2, None).to_str(), "ello");
4768        assert_eq!(h.substr(&Value::str("hello"), 0, Some(3)).to_str(), "he");
4769        assert_eq!(h.index(&Value::str("hello"), &Value::str("ll")), 3);
4770        assert_eq!(h.index(&Value::str("hello"), &Value::str("z")), 0);
4771    }
4772
4773    #[test]
4774    fn awk_op_range_is_disjoint_from_generic_extended_wide() {
4775        use crate::awk_builtins::*;
4776        assert!(!is_awk_op(0));
4777        assert!(!is_awk_op(AWK_OP_BASE - 1));
4778        assert!(is_awk_op(AWK_FIELD_GET));
4779        assert!(is_awk_op(AWK_ARRAY_LEN));
4780        assert!(!is_awk_op(AWK_OP_END));
4781    }
4782
4783    // ── Block-JIT-eligible builtins with awk negative-arg semantics ──
4784    // Interpreter-tier coverage for AwkSqrtJit / AwkLogJit (warn-then-NaN) and
4785    // AwkLshiftJit / AwkRshiftJit / AwkComplJit (fatal trap). Block-JIT codegen
4786    // is a separate follow-up — these stay block-JIT-ineligible for now and
4787    // run on the fusevm interpreter through the chunk dispatch.
4788
4789    fn build_unary(op: Op, x: f64) -> crate::Chunk {
4790        let mut b = crate::ChunkBuilder::new();
4791        b.emit(Op::PushFrame, 1);
4792        b.emit(Op::LoadFloat(x), 1);
4793        b.emit(op, 1);
4794        b.build()
4795    }
4796
4797    fn build_binary(op: Op, a: f64, n: f64) -> crate::Chunk {
4798        let mut b = crate::ChunkBuilder::new();
4799        b.emit(Op::PushFrame, 1);
4800        b.emit(Op::LoadFloat(a), 1);
4801        b.emit(Op::LoadFloat(n), 1);
4802        b.emit(op, 1);
4803        b.build()
4804    }
4805
4806    #[test]
4807    fn awk_sqrt_jit_negative_warns_returns_nan() {
4808        let chunk = build_unary(Op::AwkSqrtJit, -4.0);
4809        let mut vm = VM::new(chunk);
4810        match vm.run() {
4811            VMResult::Ok(v) => assert!(v.to_float().is_nan(), "expected NaN, got {v:?}"),
4812            other => panic!("expected Ok(NaN), got {other:?}"),
4813        }
4814    }
4815
4816    #[test]
4817    fn awk_sqrt_jit_positive_returns_sqrt() {
4818        let chunk = build_unary(Op::AwkSqrtJit, 16.0);
4819        let mut vm = VM::new(chunk);
4820        match vm.run() {
4821            VMResult::Ok(v) => assert_eq!(v.to_float(), 4.0),
4822            other => panic!("expected Ok(4.0), got {other:?}"),
4823        }
4824    }
4825
4826    #[test]
4827    fn awk_log_jit_positive_returns_ln() {
4828        let chunk = build_unary(Op::AwkLogJit, std::f64::consts::E);
4829        let mut vm = VM::new(chunk);
4830        match vm.run() {
4831            VMResult::Ok(v) => assert!((v.to_float() - 1.0).abs() < 1e-10),
4832            other => panic!("expected Ok(~1.0), got {other:?}"),
4833        }
4834    }
4835
4836    #[test]
4837    fn awk_log_jit_negative_warns_returns_nan() {
4838        let chunk = build_unary(Op::AwkLogJit, -1.0);
4839        let mut vm = VM::new(chunk);
4840        match vm.run() {
4841            VMResult::Ok(v) => assert!(v.to_float().is_nan()),
4842            other => panic!("expected Ok(NaN), got {other:?}"),
4843        }
4844    }
4845
4846    #[test]
4847    fn awk_lshift_jit_computes_left_shift() {
4848        let chunk = build_binary(Op::AwkLshiftJit, 1.0, 4.0);
4849        let mut vm = VM::new(chunk);
4850        match vm.run() {
4851            VMResult::Ok(v) => assert_eq!(v.to_float(), 16.0, "1 << 4 == 16"),
4852            other => panic!("expected Ok(16.0), got {other:?}"),
4853        }
4854    }
4855
4856    #[test]
4857    fn awk_lshift_jit_negative_amount_errors() {
4858        let chunk = build_binary(Op::AwkLshiftJit, 1.0, -1.0);
4859        let mut vm = VM::new(chunk);
4860        match vm.run() {
4861            VMResult::Error(msg) => assert!(msg.contains("lshift"), "msg = {msg:?}"),
4862            other => panic!("expected Error, got {other:?}"),
4863        }
4864    }
4865
4866    #[test]
4867    fn awk_rshift_jit_computes_right_shift() {
4868        let chunk = build_binary(Op::AwkRshiftJit, 16.0, 2.0);
4869        let mut vm = VM::new(chunk);
4870        match vm.run() {
4871            VMResult::Ok(v) => assert_eq!(v.to_float(), 4.0, "16 >> 2 == 4"),
4872            other => panic!("expected Ok(4.0), got {other:?}"),
4873        }
4874    }
4875
4876    #[test]
4877    fn awk_compl_jit_negates_bits() {
4878        // compl(15) ≈ !15 in i64 ≈ -16 ≈ as f64 ≈ -16.0
4879        let chunk = build_unary(Op::AwkComplJit, 15.0);
4880        let mut vm = VM::new(chunk);
4881        match vm.run() {
4882            VMResult::Ok(v) => assert_eq!(v.to_float(), -16.0, "compl(15) == -16"),
4883            other => panic!("expected Ok(-16.0), got {other:?}"),
4884        }
4885    }
4886
4887    #[test]
4888    fn awk_compl_jit_negative_errors() {
4889        let chunk = build_unary(Op::AwkComplJit, -1.0);
4890        let mut vm = VM::new(chunk);
4891        match vm.run() {
4892            VMResult::Error(msg) => assert!(msg.contains("compl"), "msg = {msg:?}"),
4893            other => panic!("expected Error, got {other:?}"),
4894        }
4895    }
4896}