Skip to main content

stryke/
vm_helper.rs

1use std::cell::Cell;
2use std::cmp::Ordering;
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::fs::File;
5use std::io::{self, BufRead, BufReader, Cursor, Read, Write as IoWrite};
6#[cfg(unix)]
7use std::os::unix::process::ExitStatusExt;
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::atomic::AtomicUsize;
11use std::sync::Arc;
12use std::sync::{Barrier, OnceLock};
13use std::time::{Duration, Instant};
14
15use indexmap::IndexMap;
16use parking_lot::{Mutex, RwLock};
17use rand::rngs::StdRng;
18use rand::{Rng, SeedableRng};
19use rayon::prelude::*;
20
21use caseless::default_case_fold_str;
22
23use crate::ast::*;
24use crate::builtins::StrykeSocket;
25use crate::crypt_util::perl_crypt;
26use crate::error::{ErrorKind, StrykeError, StrykeResult};
27use crate::mro::linearize_c3;
28use crate::perl_decode::decode_utf8_or_latin1;
29use crate::perl_fs::read_file_text_perl_compat;
30use crate::perl_regex::{perl_quotemeta, PerlCaptures, PerlCompiledRegex};
31use crate::pmap_progress::{FanProgress, PmapProgress};
32use crate::profiler::Profiler;
33use crate::scope::Scope;
34use crate::sort_fast::{detect_sort_block_fast, sort_magic_cmp};
35use crate::value::{
36    perl_list_range_expand, perl_shl_i64, perl_shr_i64, CaptureResult, PerlBarrier, PerlDataFrame,
37    PerlGenerator, PerlHeap, PerlPpool, PipelineInner, PipelineOp, RemoteCluster, StrykeAsyncTask,
38    StrykeSub, StrykeValue,
39};
40
41/// Merge two counting-hash accumulators (parallel `preduce_init` partials).
42/// Returns a hashref so arrow deref (`$acc->{k}`) stays valid after parallel merge.
43pub(crate) fn preduce_init_merge_maps(
44    mut acc: IndexMap<String, StrykeValue>,
45    b: IndexMap<String, StrykeValue>,
46) -> StrykeValue {
47    for (k, v2) in b {
48        acc.entry(k)
49            .and_modify(|v1| *v1 = StrykeValue::float(v1.to_number() + v2.to_number()))
50            .or_insert(v2);
51    }
52    StrykeValue::hash_ref(Arc::new(RwLock::new(acc)))
53}
54
55/// `(off, end)` for `splice` / `arr.drain(off..end)` — Perl negative OFFSET/LENGTH; clamps offset to array length.
56#[inline]
57fn splice_compute_range(
58    arr_len: usize,
59    offset_val: &StrykeValue,
60    length_val: &StrykeValue,
61) -> (usize, usize) {
62    let off_i = offset_val.to_int();
63    let off = if off_i < 0 {
64        arr_len.saturating_sub((-off_i) as usize)
65    } else {
66        (off_i as usize).min(arr_len)
67    };
68    let rest = arr_len.saturating_sub(off);
69    let take = if length_val.is_undef() {
70        rest
71    } else {
72        let l = length_val.to_int();
73        if l < 0 {
74            rest.saturating_sub((-l) as usize)
75        } else {
76            (l as usize).min(rest)
77        }
78    };
79    let end = (off + take).min(arr_len);
80    (off, end)
81}
82
83/// Combine two partial results from `preduce_init`: hash/hashref maps add per-key counts; otherwise
84/// the fold block is invoked with `$a` / `$b` as the two partial accumulators (associative combine).
85pub(crate) fn merge_preduce_init_partials(
86    a: StrykeValue,
87    b: StrykeValue,
88    block: &Block,
89    subs: &HashMap<String, Arc<StrykeSub>>,
90    scope_capture: &[(String, StrykeValue)],
91) -> StrykeValue {
92    if let (Some(m1), Some(m2)) = (a.as_hash_map(), b.as_hash_map()) {
93        return preduce_init_merge_maps(m1, m2);
94    }
95    if let (Some(r1), Some(r2)) = (a.as_hash_ref(), b.as_hash_ref()) {
96        let m1 = r1.read().clone();
97        let m2 = r2.read().clone();
98        return preduce_init_merge_maps(m1, m2);
99    }
100    if let Some(m1) = a.as_hash_map() {
101        if let Some(r2) = b.as_hash_ref() {
102            let m2 = r2.read().clone();
103            return preduce_init_merge_maps(m1, m2);
104        }
105    }
106    if let Some(r1) = a.as_hash_ref() {
107        if let Some(m2) = b.as_hash_map() {
108            let m1 = r1.read().clone();
109            return preduce_init_merge_maps(m1, m2);
110        }
111    }
112    let mut local_interp = VMHelper::new();
113    local_interp.subs = subs.clone();
114    local_interp.scope.restore_capture(scope_capture);
115    local_interp.enable_parallel_guard();
116    local_interp
117        .scope
118        .declare_array("_", vec![a.clone(), b.clone()]);
119    local_interp.scope.set_sort_pair(a, b);
120    match local_interp.exec_block(block) {
121        Ok(val) => val,
122        Err(_) => StrykeValue::UNDEF,
123    }
124}
125
126/// Seed each parallel chunk from `init` without sharing mutable hashref storage (plain `clone` on
127/// `HashRef` reuses the same `Arc<RwLock<…>>`).
128pub(crate) fn preduce_init_fold_identity(init: &StrykeValue) -> StrykeValue {
129    if let Some(m) = init.as_hash_map() {
130        return StrykeValue::hash(m.clone());
131    }
132    if let Some(r) = init.as_hash_ref() {
133        return StrykeValue::hash_ref(Arc::new(RwLock::new(r.read().clone())));
134    }
135    init.clone()
136}
137
138pub(crate) fn fold_preduce_init_step(
139    subs: &HashMap<String, Arc<StrykeSub>>,
140    scope_capture: &[(String, StrykeValue)],
141    block: &Block,
142    acc: StrykeValue,
143    item: StrykeValue,
144) -> StrykeValue {
145    let mut local_interp = VMHelper::new();
146    local_interp.subs = subs.clone();
147    local_interp.scope.restore_capture(scope_capture);
148    local_interp.enable_parallel_guard();
149    local_interp
150        .scope
151        .declare_array("_", vec![acc.clone(), item.clone()]);
152    local_interp.scope.set_sort_pair(acc, item);
153    match local_interp.exec_block(block) {
154        Ok(val) => val,
155        Err(_) => StrykeValue::UNDEF,
156    }
157}
158
159/// `use feature 'say'`
160pub const FEAT_SAY: u64 = 1 << 0;
161/// `use feature 'state'`
162pub const FEAT_STATE: u64 = 1 << 1;
163/// `use feature 'switch'` (given/when when fully wired)
164pub const FEAT_SWITCH: u64 = 1 << 2;
165/// `use feature 'unicode_strings'`
166pub const FEAT_UNICODE_STRINGS: u64 = 1 << 3;
167
168/// Flow control signals propagated via Result.
169#[derive(Debug)]
170pub(crate) enum Flow {
171    Return(StrykeValue),
172    Last(Option<String>),
173    Next(Option<String>),
174    Redo(Option<String>),
175    Yield(StrykeValue),
176    /// `goto &sub` — tail-call: replace current sub with the named one, keeping @_.
177    GotoSub(String),
178}
179
180pub(crate) type ExecResult = Result<StrykeValue, FlowOrError>;
181
182#[derive(Debug)]
183pub(crate) enum FlowOrError {
184    Flow(Flow),
185    Error(StrykeError),
186}
187
188impl From<StrykeError> for FlowOrError {
189    fn from(e: StrykeError) -> Self {
190        FlowOrError::Error(e)
191    }
192}
193
194impl From<Flow> for FlowOrError {
195    fn from(f: Flow) -> Self {
196        FlowOrError::Flow(f)
197    }
198}
199
200/// Bindings introduced by a successful algebraic [`MatchPattern`] (scalar vs array).
201enum PatternBinding {
202    Scalar(String, StrykeValue),
203    Array(String, Vec<StrykeValue>),
204}
205
206/// Perl `$]` — numeric language level (`5 + minor/1000 + patch/1_000_000`).
207/// Emulated Perl 5.x level (not the `stryke` crate semver).
208pub fn perl_bracket_version() -> f64 {
209    const PERL_EMUL_MINOR: u32 = 38;
210    const PERL_EMUL_PATCH: u32 = 0;
211    5.0 + (PERL_EMUL_MINOR as f64) / 1000.0 + (PERL_EMUL_PATCH as f64) / 1_000_000.0
212}
213
214/// Cheap seed for [`StdRng`] at startup (avoids `getentropy` / blocking sources).
215#[inline]
216fn fast_rng_seed() -> u64 {
217    let local: u8 = 0;
218    let addr = &local as *const u8 as u64;
219    (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ addr
220}
221
222/// `$^X` — cache `current_exe()` once per process (tiny win on repeated `Interpreter::new`).
223fn cached_executable_path() -> String {
224    static CACHED: OnceLock<String> = OnceLock::new();
225    CACHED
226        .get_or_init(|| {
227            std::env::current_exe()
228                .map(|p| p.to_string_lossy().into_owned())
229                .unwrap_or_else(|_| "stryke".to_string())
230        })
231        .clone()
232}
233
234fn build_term_hash() -> IndexMap<String, StrykeValue> {
235    let mut m = IndexMap::new();
236    m.insert(
237        "TERM".into(),
238        StrykeValue::string(std::env::var("TERM").unwrap_or_default()),
239    );
240    m.insert(
241        "COLORTERM".into(),
242        StrykeValue::string(std::env::var("COLORTERM").unwrap_or_default()),
243    );
244
245    let (rows, cols) = term_size();
246    m.insert("rows".into(), StrykeValue::integer(rows));
247    m.insert("cols".into(), StrykeValue::integer(cols));
248
249    #[cfg(unix)]
250    let is_tty = unsafe { libc::isatty(1) != 0 };
251    #[cfg(not(unix))]
252    let is_tty = false;
253    m.insert(
254        "is_tty".into(),
255        StrykeValue::integer(if is_tty { 1 } else { 0 }),
256    );
257
258    m
259}
260
261fn term_size() -> (i64, i64) {
262    #[cfg(unix)]
263    {
264        unsafe {
265            let mut ws: libc::winsize = std::mem::zeroed();
266            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 {
267                return (ws.ws_row as i64, ws.ws_col as i64);
268            }
269        }
270    }
271    let rows = std::env::var("LINES")
272        .ok()
273        .and_then(|s| s.parse().ok())
274        .unwrap_or(24);
275    let cols = std::env::var("COLUMNS")
276        .ok()
277        .and_then(|s| s.parse().ok())
278        .unwrap_or(80);
279    (rows, cols)
280}
281
282#[cfg(unix)]
283fn build_uname_hash() -> IndexMap<String, StrykeValue> {
284    fn uts_field(slice: &[libc::c_char]) -> String {
285        let n = slice.iter().take_while(|&&c| c != 0).count();
286        let bytes: Vec<u8> = slice[..n].iter().map(|&c| c as u8).collect();
287        String::from_utf8_lossy(&bytes).into_owned()
288    }
289    let mut m = IndexMap::new();
290    let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
291    if unsafe { libc::uname(&mut uts) } == 0 {
292        m.insert(
293            "sysname".into(),
294            StrykeValue::string(uts_field(uts.sysname.as_slice())),
295        );
296        m.insert(
297            "nodename".into(),
298            StrykeValue::string(uts_field(uts.nodename.as_slice())),
299        );
300        m.insert(
301            "release".into(),
302            StrykeValue::string(uts_field(uts.release.as_slice())),
303        );
304        m.insert(
305            "version".into(),
306            StrykeValue::string(uts_field(uts.version.as_slice())),
307        );
308        m.insert(
309            "machine".into(),
310            StrykeValue::string(uts_field(uts.machine.as_slice())),
311        );
312    }
313    m
314}
315
316#[cfg(unix)]
317fn build_limits_hash() -> IndexMap<String, StrykeValue> {
318    use libc::{getrlimit, rlimit, RLIM_INFINITY};
319    // `__rlimit_resource_t` is a glibc-only enum; musl libc uses
320    // `c_int` for `getrlimit`'s resource parameter. Branch on `target_env`
321    // so musl builds (release CI's x86_64-unknown-linux-musl target)
322    // pick the c_int alias instead of failing to find the glibc type.
323    #[cfg(all(target_os = "linux", target_env = "gnu"))]
324    type RlimitResource = libc::__rlimit_resource_t;
325    #[cfg(all(target_os = "linux", not(target_env = "gnu")))]
326    type RlimitResource = libc::c_int;
327    #[cfg(not(target_os = "linux"))]
328    type RlimitResource = libc::c_int;
329    fn get_limit(resource: RlimitResource) -> (i64, i64) {
330        let mut rlim = rlimit {
331            rlim_cur: 0,
332            rlim_max: 0,
333        };
334        if unsafe { getrlimit(resource, &mut rlim) } == 0 {
335            let cur = if rlim.rlim_cur == RLIM_INFINITY {
336                -1
337            } else {
338                rlim.rlim_cur as i64
339            };
340            let max = if rlim.rlim_max == RLIM_INFINITY {
341                -1
342            } else {
343                rlim.rlim_max as i64
344            };
345            (cur, max)
346        } else {
347            (-1, -1)
348        }
349    }
350    let mut m = IndexMap::new();
351    let (cur, max) = get_limit(libc::RLIMIT_NOFILE);
352    m.insert("nofile".into(), StrykeValue::integer(cur));
353    m.insert("nofile_max".into(), StrykeValue::integer(max));
354    let (cur, max) = get_limit(libc::RLIMIT_STACK);
355    m.insert("stack".into(), StrykeValue::integer(cur));
356    m.insert("stack_max".into(), StrykeValue::integer(max));
357    let (cur, max) = get_limit(libc::RLIMIT_AS);
358    m.insert("as".into(), StrykeValue::integer(cur));
359    m.insert("as_max".into(), StrykeValue::integer(max));
360    let (cur, max) = get_limit(libc::RLIMIT_DATA);
361    m.insert("data".into(), StrykeValue::integer(cur));
362    m.insert("data_max".into(), StrykeValue::integer(max));
363    let (cur, max) = get_limit(libc::RLIMIT_FSIZE);
364    m.insert("fsize".into(), StrykeValue::integer(cur));
365    m.insert("fsize_max".into(), StrykeValue::integer(max));
366    let (cur, max) = get_limit(libc::RLIMIT_CORE);
367    m.insert("core".into(), StrykeValue::integer(cur));
368    m.insert("core_max".into(), StrykeValue::integer(max));
369    let (cur, max) = get_limit(libc::RLIMIT_CPU);
370    m.insert("cpu".into(), StrykeValue::integer(cur));
371    m.insert("cpu_max".into(), StrykeValue::integer(max));
372    let (cur, max) = get_limit(libc::RLIMIT_NPROC);
373    m.insert("nproc".into(), StrykeValue::integer(cur));
374    m.insert("nproc_max".into(), StrykeValue::integer(max));
375    #[cfg(target_os = "linux")]
376    {
377        let (cur, max) = get_limit(libc::RLIMIT_MEMLOCK);
378        m.insert("memlock".into(), StrykeValue::integer(cur));
379        m.insert("memlock_max".into(), StrykeValue::integer(max));
380    }
381    m
382}
383
384/// Context of the **current** subroutine call (`wantarray`).
385#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
386pub(crate) enum WantarrayCtx {
387    #[default]
388    Scalar,
389    List,
390    Void,
391}
392
393impl WantarrayCtx {
394    #[inline]
395    pub(crate) fn from_byte(b: u8) -> Self {
396        match b {
397            1 => Self::List,
398            2 => Self::Void,
399            _ => Self::Scalar,
400        }
401    }
402
403    #[inline]
404    pub(crate) fn as_byte(self) -> u8 {
405        match self {
406            Self::Scalar => 0,
407            Self::List => 1,
408            Self::Void => 2,
409        }
410    }
411}
412
413/// Minimum log level filter for `log_*` / `log_json` (trace = most verbose).
414#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
415pub(crate) enum LogLevelFilter {
416    Trace,
417    Debug,
418    Info,
419    Warn,
420    Error,
421}
422
423impl LogLevelFilter {
424    pub(crate) fn parse(s: &str) -> Option<Self> {
425        match s.trim().to_ascii_lowercase().as_str() {
426            "trace" => Some(Self::Trace),
427            "debug" => Some(Self::Debug),
428            "info" => Some(Self::Info),
429            "warn" | "warning" => Some(Self::Warn),
430            "error" => Some(Self::Error),
431            _ => None,
432        }
433    }
434
435    pub(crate) fn as_str(self) -> &'static str {
436        match self {
437            Self::Trace => "trace",
438            Self::Debug => "debug",
439            Self::Info => "info",
440            Self::Warn => "warn",
441            Self::Error => "error",
442        }
443    }
444}
445
446/// True when `@$aref->[IX]` / `IX` needs **list** context on the RHS of `=` (multi-slot slice).
447fn arrow_deref_array_assign_rhs_list_ctx(index: &Expr) -> bool {
448    match &index.kind {
449        ExprKind::Range { .. } | ExprKind::SliceRange { .. } => true,
450        ExprKind::QW(ws) => ws.len() > 1,
451        ExprKind::List(el) => {
452            if el.len() > 1 {
453                true
454            } else if el.len() == 1 {
455                arrow_deref_array_assign_rhs_list_ctx(&el[0])
456            } else {
457                false
458            }
459        }
460        _ => false,
461    }
462}
463
464/// Wantarray for the RHS of a plain `=` assignment — must match [`crate::compiler::Compiler`] lowering
465/// so `<>` / `readline` list-slurp matches Perl for `@a = <>` (not only `my`/`our`/`local` initializers).
466pub(crate) fn assign_rhs_wantarray(target: &Expr) -> WantarrayCtx {
467    match &target.kind {
468        ExprKind::ArrayVar(_) | ExprKind::HashVar(_) => WantarrayCtx::List,
469        ExprKind::ScalarVar(_) | ExprKind::ArrayElement { .. } | ExprKind::HashElement { .. } => {
470            WantarrayCtx::Scalar
471        }
472        ExprKind::Deref { kind, .. } => match kind {
473            Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
474            Sigil::Array | Sigil::Hash => WantarrayCtx::List,
475        },
476        ExprKind::ArrowDeref {
477            index,
478            kind: DerefKind::Array,
479            ..
480        } => {
481            if arrow_deref_array_assign_rhs_list_ctx(index) {
482                WantarrayCtx::List
483            } else {
484                WantarrayCtx::Scalar
485            }
486        }
487        ExprKind::ArrowDeref {
488            kind: DerefKind::Hash,
489            ..
490        }
491        | ExprKind::ArrowDeref {
492            kind: DerefKind::Call,
493            ..
494        } => WantarrayCtx::Scalar,
495        ExprKind::HashSliceDeref { .. }
496        | ExprKind::HashSlice { .. }
497        | ExprKind::HashKvSlice { .. } => WantarrayCtx::List,
498        ExprKind::ArraySlice { indices, .. } => {
499            if indices.len() > 1 {
500                WantarrayCtx::List
501            } else if indices.len() == 1 {
502                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
503                    WantarrayCtx::List
504                } else {
505                    WantarrayCtx::Scalar
506                }
507            } else {
508                WantarrayCtx::Scalar
509            }
510        }
511        ExprKind::AnonymousListSlice { indices, .. } => {
512            if indices.len() > 1 {
513                WantarrayCtx::List
514            } else if indices.len() == 1 {
515                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
516                    WantarrayCtx::List
517                } else {
518                    WantarrayCtx::Scalar
519                }
520            } else {
521                WantarrayCtx::Scalar
522            }
523        }
524        ExprKind::Typeglob(_) | ExprKind::TypeglobExpr(_) => WantarrayCtx::Scalar,
525        ExprKind::List(_) => WantarrayCtx::List,
526        _ => WantarrayCtx::Scalar,
527    }
528}
529
530/// Memoized inputs + result for a non-`g` `regex_match_execute` call. Populated on every
531/// successful match and consulted at the top of the next call; on exact-match (same pattern,
532/// flags, multiline, and haystack content) we skip regex execution + capture-var scope population
533/// entirely, replaying the stored `StrykeValue` result. See [`VMHelper::regex_match_memo`].
534#[derive(Clone)]
535pub(crate) struct RegexMatchMemo {
536    pub pattern: String,
537    pub flags: String,
538    pub multiline: bool,
539    pub haystack: String,
540    pub result: StrykeValue,
541}
542
543/// State for scalar `..` / `...` (key: `Expr` address).
544#[derive(Clone, Copy, Default)]
545struct FlipFlopTreeState {
546    active: bool,
547    /// Exclusive `...`: `$.` line where the left bound matched — right is only tested when `$.` is
548    /// strictly greater (Perl: do not test the right operand until the next evaluation; for numeric
549    /// `$.` that defers past the left-match line, including multiple evals on that line).
550    exclusive_left_line: Option<i64>,
551}
552
553/// `BufReader` / `print` / `sysread` / `tell` on the same handle share this [`File`] cursor.
554#[derive(Clone)]
555pub(crate) struct IoSharedFile(pub Arc<Mutex<File>>);
556
557impl Read for IoSharedFile {
558    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
559        self.0.lock().read(buf)
560    }
561}
562
563pub(crate) struct IoSharedFileWrite(pub Arc<Mutex<File>>);
564
565impl IoWrite for IoSharedFileWrite {
566    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
567        self.0.lock().write(buf)
568    }
569
570    fn flush(&mut self) -> io::Result<()> {
571        self.0.lock().flush()
572    }
573}
574
575/// There is no Tree walking Interpreter, this is Just a Virtual Machine helper struct
576pub struct VMHelper {
577    pub scope: Scope,
578    pub(crate) subs: HashMap<String, Arc<StrykeSub>>,
579    /// AOP advice registry — populated by `Op::RegisterAdvice` from `before|after|around` decls.
580    pub(crate) intercepts: Vec<crate::aop::Intercept>,
581    /// Auto-incremented for the next registered intercept id (1-based; matches zshrs).
582    pub(crate) next_intercept_id: u32,
583    /// Stack of active around-advice contexts; `proceed()` reads the top frame.
584    pub(crate) intercept_ctx_stack: Vec<crate::aop::InterceptCtx>,
585    /// Re-entrancy guard: while running advice for a name, calling that same name from inside
586    /// the body skips advice and runs the original directly. Prevents infinite recursion when
587    /// an advice body uses the same sub it advises.
588    pub(crate) intercept_active_names: Vec<String>,
589    pub(crate) file: String,
590    /// File handles: name → writer
591    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
592    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
593    /// Output separator ($,)
594    pub ofs: String,
595    /// Output record separator ($\)
596    pub ors: String,
597    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
598    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
599    pub irs: Option<String>,
600    /// $! — last OS error
601    pub errno: String,
602    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
603    pub errno_code: i32,
604    /// $@ — last eval error (string)
605    pub eval_error: String,
606    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
607    pub eval_error_code: i32,
608    /// When `die` is called with a ref argument, the ref value is preserved here.
609    pub eval_error_value: Option<StrykeValue>,
610    /// @ARGV
611    pub argv: Vec<String>,
612    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
613    pub env: IndexMap<String, StrykeValue>,
614    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
615    pub env_materialized: bool,
616    /// $0
617    pub program_name: String,
618    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle)
619    pub line_number: i64,
620    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
621    pub last_readline_handle: String,
622    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
623    pub(crate) last_stdin_die_bracket: String,
624    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
625    pub handle_line_numbers: HashMap<String, i64>,
626    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
627    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
628    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
629    pub(crate) flip_flop_active: Vec<bool>,
630    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
631    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
632    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
633    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
634    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
635    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
636    pub(crate) flip_flop_sequence: Vec<i64>,
637    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
638    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
639    /// range on one line return the same sequence number).
640    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
641    /// Scalar `..` / `...` flip-flop state (key: `Expr` address).
642    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
643    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
644    pub sigint_pending_caret: Cell<bool>,
645    /// Auto-split mode (-a)
646    pub auto_split: bool,
647    /// Field separator for -F
648    pub field_separator: Option<String>,
649    /// BEGIN blocks
650    begin_blocks: Vec<Block>,
651    /// `UNITCHECK` blocks (LIFO at run)
652    unit_check_blocks: Vec<Block>,
653    /// `CHECK` blocks (LIFO at run)
654    check_blocks: Vec<Block>,
655    /// `INIT` blocks (FIFO at run)
656    init_blocks: Vec<Block>,
657    /// END blocks
658    end_blocks: Vec<Block>,
659    /// -w warnings / `use warnings` / `$^W`
660    pub warnings: bool,
661    /// Output autoflush (`$|`).
662    pub output_autoflush: bool,
663    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
664    pub default_print_handle: String,
665    /// Suppress stdout output (fan workers with progress bars).
666    pub suppress_stdout: bool,
667    /// Per-instance test counters for `assert_*` / `test_run` / `test_skip` (stryke
668    /// `.stk` test framework). Atomics so the immutable-ref builtin signature
669    /// (`fn(&VMHelper, ...)`) can mutate without changing every call site. Replaces
670    /// the previous `AtomicUsize` process-globals which leaked counts across runs
671    /// in a single process — embedders running multiple `.stk` programs in one
672    /// `VMHelper` would see the previous run's counts contaminate the next.
673    pub test_pass_count: std::sync::atomic::AtomicUsize,
674    pub test_fail_count: std::sync::atomic::AtomicUsize,
675    pub test_skip_count: std::sync::atomic::AtomicUsize,
676    /// Cumulative-across-the-whole-run counters. The `test_pass_count` /
677    /// `test_fail_count` / `test_skip_count` triplet above is reset by
678    /// `test_run` after it prints its per-block summary, so external
679    /// embedders (the worker-pool test runner reading counts after
680    /// `execute()` returns) see 0 if any test in the file already
681    /// finished a `test_run` block. The `_total` counters are summed-in
682    /// by `test_run` *before* the reset, never themselves reset, and
683    /// give the harness an honest "how many assertions did this run
684    /// actually do" number.
685    pub test_pass_total: std::sync::atomic::AtomicUsize,
686    pub test_fail_total: std::sync::atomic::AtomicUsize,
687    pub test_skip_total: std::sync::atomic::AtomicUsize,
688    /// Set to `true` by `test_run` when any assertion failed during the run.
689    /// CLI driver (`main.rs`) reads this after `execute` returns and exits with
690    /// code 1. Replaces the previous in-VM `std::process::exit(1)` which made
691    /// embedding (running a `.stk` program from a Rust harness) impossible —
692    /// any failing test would kill the host process.
693    pub test_run_failed: std::sync::atomic::AtomicBool,
694    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
695    pub child_exit_status: i64,
696    /// Last successful match (`$&`, `${^MATCH}`).
697    pub last_match: String,
698    /// Before match (`` $` ``, `${^PREMATCH}`).
699    pub prematch: String,
700    /// After match (`$'`, `${^POSTMATCH}`).
701    pub postmatch: String,
702    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
703    pub last_paren_match: String,
704    /// List separator for array stringification in concatenation / interpolation (`$"`).
705    pub list_separator: String,
706    /// Script start time (`$^T`) — seconds since Unix epoch.
707    pub script_start_time: i64,
708    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
709    pub compile_hints: i64,
710    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
711    pub warning_bits: i64,
712    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
713    pub global_phase: String,
714    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
715    pub subscript_sep: String,
716    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
717    /// The `stryke` driver sets this from `-i` / `-i.ext`.
718    pub inplace_edit: String,
719    /// `$^D` — debugging flags (integer; mostly ignored).
720    pub debug_flags: i64,
721    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
722    pub perl_debug_flags: i64,
723    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
724    pub eval_nesting: u32,
725    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
726    pub argv_current_file: String,
727    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
728    pub(crate) diamond_next_idx: usize,
729    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
730    pub(crate) diamond_reader: Option<BufReader<File>>,
731    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
732    pub strict_refs: bool,
733    pub strict_subs: bool,
734    pub strict_vars: bool,
735    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
736    pub utf8_pragma: bool,
737    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
738    pub open_pragma_utf8: bool,
739    /// `use feature` — bit flags (`FEAT_*`).
740    pub feature_bits: u64,
741    /// Number of parallel threads
742    pub num_threads: usize,
743    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
744    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
745    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
746    /// Third flag: `$*` multiline (prepends `(?s)` when true).
747    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
748    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
749    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
750    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
751    /// scope population entirely on cache hit.
752    ///
753    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
754    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
755    /// capture-var side-effect replay is forced on the next hit.
756    regex_match_memo: Option<RegexMatchMemo>,
757    /// False when the user (or some non-regex code path) has written to one of the capture
758    /// variables since the last `apply_regex_captures` call. The memoized match result is still
759    /// valid, but the scope side effects need to be reapplied on the next hit.
760    regex_capture_scope_fresh: bool,
761    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
762    pub(crate) regex_pos: HashMap<String, Option<usize>>,
763    /// Persistent storage for `state` variables, keyed by "line:name".
764    pub(crate) state_vars: HashMap<String, StrykeValue>,
765    /// Per-frame tracking of state variable bindings: (var_name, state_key).
766    pub(crate) state_bindings_stack: Vec<Vec<(String, String)>>,
767    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
768    pub(crate) rand_rng: StdRng,
769    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
770    pub(crate) dir_handles: HashMap<String, DirHandleState>,
771    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
772    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
773    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
774    pub(crate) pipe_children: HashMap<String, Child>,
775    /// Sockets from `socket` / `accept` / `connect`.
776    pub(crate) socket_handles: HashMap<String, StrykeSocket>,
777    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
778    pub(crate) wantarray_kind: WantarrayCtx,
779    /// `struct Name { ... }` definitions (merged from VM chunks).
780    pub struct_defs: HashMap<String, Arc<StructDef>>,
781    /// `enum Name { ... }` definitions (merged from VM chunks).
782    pub enum_defs: HashMap<String, Arc<EnumDef>>,
783    /// `class Name extends ... impl ... { ... }` definitions.
784    pub class_defs: HashMap<String, Arc<ClassDef>>,
785    /// `trait Name { ... }` definitions.
786    pub trait_defs: HashMap<String, Arc<TraitDef>>,
787    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
788    /// call/return (JIT disabled); per-statement lines and subs.
789    pub profiler: Option<Profiler>,
790    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
791    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
792    /// Virtual modules: path → source (for AOT bundles). Checked before filesystem in `require`.
793    pub(crate) virtual_modules: HashMap<String, String>,
794    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
795    pub(crate) tied_hashes: HashMap<String, StrykeValue>,
796    /// `tie $name` — TIESCALAR object for FETCH/STORE.
797    pub(crate) tied_scalars: HashMap<String, StrykeValue>,
798    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
799    pub(crate) tied_arrays: HashMap<String, StrykeValue>,
800    /// `use overload` — class → Perl overload key → short method name in that package.
801    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
802    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
803    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
804    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
805    pub(crate) special_caret_scalars: HashMap<String, StrykeValue>,
806    /// `$%` — format output page number.
807    pub format_page_number: i64,
808    /// `$=` — format lines per page.
809    pub format_lines_per_page: i64,
810    /// `$-` — lines remaining on format page.
811    pub format_lines_left: i64,
812    /// `$:` — characters to break format lines (Perl default `\n`).
813    pub format_line_break_chars: String,
814    /// `$^` — top-of-form format name.
815    pub format_top_name: String,
816    /// `$^A` — format write accumulator.
817    pub accumulator_format: String,
818    /// `$^F` — max system file descriptor (Perl default 2).
819    pub max_system_fd: i64,
820    /// `$^M` — emergency memory buffer (no-op pool in stryke).
821    pub emergency_memory: String,
822    /// `$^N` — last opened named regexp capture name.
823    pub last_subpattern_name: String,
824    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
825    pub inc_hook_index: i64,
826    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
827    pub multiline_match: bool,
828    /// `$^X` — path to this executable (cached).
829    pub executable_path: String,
830    /// `$^L` — formfeed string for formats (Perl default `\f`).
831    pub formfeed_string: String,
832    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
833    pub(crate) glob_handle_alias: HashMap<String, String>,
834    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
835    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
836    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
837    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
838    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
839    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
840    pub(crate) special_var_restore_frames: Vec<Vec<(String, StrykeValue)>>,
841    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
842    /// Lazy-init flag: reflection hashes (`%b`, `%stryke::builtins`, etc.)
843    /// are only built on first access to avoid startup cost.
844    pub(crate) reflection_hashes_ready: bool,
845    pub(crate) english_enabled: bool,
846    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
847    pub(crate) english_no_match_vars: bool,
848    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
849    /// available for the rest of the program — Perl exports them into the caller's namespace
850    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
851    pub(crate) english_match_vars_ever_enabled: bool,
852    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
853    english_lexical_scalars: Vec<HashSet<String>>,
854    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
855    our_lexical_scalars: Vec<HashSet<String>>,
856    /// Bare names from `our @arr` per frame — drives package qualification in
857    /// [`Self::tree_array_storage_name`] so `our @x` reads route through the
858    /// package stash while `my @x` stays lexical.
859    our_lexical_arrays: Vec<HashSet<String>>,
860    /// Bare names from `our %h` per frame — companion to
861    /// [`Self::our_lexical_arrays`].
862    our_lexical_hashes: Vec<HashSet<String>>,
863    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
864    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
865    pub vm_jit_enabled: bool,
866    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
867    pub disasm_bytecode: bool,
868    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from a bytecode cache hit. When
869    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
870    /// (`.take()`) on first read so re-entry compiles normally.
871    pub cached_chunk: Option<crate::bytecode::Chunk>,
872    /// Sideband: script path for bytecode cache save after compilation (mtime-based).
873    pub cache_script_path: Option<std::path::PathBuf>,
874    /// Interpreter base for relative filesystem paths (`cd` updates this; OS `chdir` does not).
875    pub(crate) stryke_pwd: PathBuf,
876    /// Set while stepping a `gen { }` body (`yield`).
877    pub(crate) in_generator: bool,
878    /// `-n`/`-p` driver: prelude only; body runs per line in [`Self::process_line_vm`].
879    pub line_mode_skip_main: bool,
880    /// Pre-compiled chunk for `-n`/`-p` line mode. Stored after the prelude `execute()` call
881    /// so `process_line_vm` can re-execute the body portion per input line.
882    pub line_mode_chunk: Option<crate::bytecode::Chunk>,
883    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
884    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
885    /// matches Perl (true on the last line of that source).
886    pub(crate) line_mode_eof_pending: bool,
887    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
888    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
889    pub line_mode_stdin_pending: VecDeque<String>,
890    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
891    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
892    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
893    pub(crate) log_level_override: Option<LogLevelFilter>,
894    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
895    /// Pushed on `call_sub` entry, popped on exit.
896    pub(crate) current_sub_stack: Vec<Arc<StrykeSub>>,
897    /// Interactive debugger state (`-d` flag).
898    pub debugger: Option<crate::debugger::Debugger>,
899    /// Call stack for debugger: (sub_name, call_line).
900    pub(crate) debug_call_stack: Vec<(String, usize)>,
901}
902
903/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
904#[derive(Debug, Clone, Default)]
905pub struct ReplCompletionSnapshot {
906    pub subs: Vec<String>,
907    pub blessed_scalars: HashMap<String, String>,
908    pub isa_for_class: HashMap<String, Vec<String>>,
909}
910
911impl ReplCompletionSnapshot {
912    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
913    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
914        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
915        let mro = linearize_c3(class, &parents, 0);
916        let mut names = HashSet::new();
917        for pkg in &mro {
918            if pkg == "UNIVERSAL" {
919                continue;
920            }
921            let prefix = format!("{}::", pkg);
922            for k in &self.subs {
923                if k.starts_with(&prefix) {
924                    let rest = &k[prefix.len()..];
925                    if !rest.contains("::") {
926                        names.insert(rest.to_string());
927                    }
928                }
929            }
930        }
931        for k in &self.subs {
932            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
933                if !rest.contains("::") {
934                    names.insert(rest.to_string());
935                }
936            }
937        }
938        let mut v: Vec<String> = names.into_iter().collect();
939        v.sort();
940        v
941    }
942}
943
944fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
945    let left = left.trim_end();
946    if left.is_empty() {
947        return None;
948    }
949    if let Some(i) = left.rfind('$') {
950        let name = left[i + 1..].trim();
951        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
952            return state.blessed_scalars.get(name).cloned();
953        }
954    }
955    let tok = left.split_whitespace().last()?;
956    if tok.contains("::") {
957        return Some(tok.to_string());
958    }
959    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
960        return Some(tok.to_string());
961    }
962    None
963}
964
965/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
966pub fn repl_arrow_method_completions(
967    state: &ReplCompletionSnapshot,
968    line: &str,
969    pos: usize,
970) -> Option<(usize, Vec<String>)> {
971    let pos = pos.min(line.len());
972    let before = &line[..pos];
973    let arrow_idx = before.rfind("->")?;
974    let after_arrow = &before[arrow_idx + 2..];
975    let rest = after_arrow.trim_start();
976    let ws_len = after_arrow.len() - rest.len();
977    let method_start = arrow_idx + 2 + ws_len;
978    let method_prefix = &line[method_start..pos];
979    if !method_prefix
980        .chars()
981        .all(|c| c.is_alphanumeric() || c == '_')
982    {
983        return None;
984    }
985    let left = line[..arrow_idx].trim_end();
986    let class = repl_resolve_class_for_arrow(state, left)?;
987    let mut methods = state.methods_for_class(&class);
988    methods.retain(|m| m.starts_with(method_prefix));
989    Some((method_start, methods))
990}
991
992/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
993#[derive(Debug, Clone, Default)]
994pub(crate) struct ModuleExportLists {
995    /// Default imports for `use Module` with no list.
996    pub export: Vec<String>,
997    /// Extra symbols allowed in `use Module qw(name)`.
998    pub export_ok: Vec<String>,
999}
1000
1001/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
1002fn piped_shell_command(cmd: &str) -> Command {
1003    if cfg!(windows) {
1004        let mut c = Command::new("cmd");
1005        c.arg("/C").arg(cmd);
1006        c
1007    } else {
1008        let mut c = Command::new("sh");
1009        c.arg("-c").arg(cmd);
1010        c
1011    }
1012}
1013
1014/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
1015/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
1016/// so the Rust `regex` crate can match them.
1017/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
1018/// so the Rust regex crate can match NUL and other octal-specified bytes.
1019/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
1020fn expand_perl_regex_octal_escapes(pat: &str) -> String {
1021    let mut out = String::with_capacity(pat.len());
1022    let mut it = pat.chars().peekable();
1023    while let Some(c) = it.next() {
1024        if c == '\\' {
1025            if let Some(&'0') = it.peek() {
1026                // Collect up to 3 octal digits starting with '0'
1027                let mut oct = String::new();
1028                while oct.len() < 3 {
1029                    if let Some(&d) = it.peek() {
1030                        if ('0'..='7').contains(&d) {
1031                            oct.push(d);
1032                            it.next();
1033                        } else {
1034                            break;
1035                        }
1036                    } else {
1037                        break;
1038                    }
1039                }
1040                if let Ok(val) = u8::from_str_radix(&oct, 8) {
1041                    out.push_str(&format!("\\x{:02x}", val));
1042                } else {
1043                    out.push('\\');
1044                    out.push_str(&oct);
1045                }
1046                continue;
1047            }
1048        }
1049        out.push(c);
1050    }
1051    out
1052}
1053
1054fn expand_perl_regex_quotemeta(pat: &str) -> String {
1055    let mut out = String::with_capacity(pat.len().saturating_mul(2));
1056    let mut it = pat.chars().peekable();
1057    let mut in_q = false;
1058    while let Some(c) = it.next() {
1059        if in_q {
1060            if c == '\\' && it.peek() == Some(&'E') {
1061                it.next();
1062                in_q = false;
1063                continue;
1064            }
1065            out.push_str(&perl_quotemeta(&c.to_string()));
1066            continue;
1067        }
1068        if c == '\\' && it.peek() == Some(&'Q') {
1069            it.next();
1070            in_q = true;
1071            continue;
1072        }
1073        out.push(c);
1074    }
1075    out
1076}
1077
1078/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
1079///
1080/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
1081/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
1082///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
1083pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
1084    let mut out = String::with_capacity(replacement.len() + 8);
1085    let mut it = replacement.chars().peekable();
1086    while let Some(c) = it.next() {
1087        if c == '\\' {
1088            match it.peek() {
1089                Some(&d) if d.is_ascii_digit() => {
1090                    it.next();
1091                    out.push_str("${");
1092                    out.push(d);
1093                    while let Some(&d2) = it.peek() {
1094                        if !d2.is_ascii_digit() {
1095                            break;
1096                        }
1097                        it.next();
1098                        out.push(d2);
1099                    }
1100                    out.push('}');
1101                }
1102                Some(&'\\') => {
1103                    it.next();
1104                    out.push('\\');
1105                }
1106                _ => out.push('\\'),
1107            }
1108        } else if c == '$' {
1109            match it.peek() {
1110                Some(&d) if d.is_ascii_digit() => {
1111                    it.next();
1112                    out.push_str("${");
1113                    out.push(d);
1114                    while let Some(&d2) = it.peek() {
1115                        if !d2.is_ascii_digit() {
1116                            break;
1117                        }
1118                        it.next();
1119                        out.push(d2);
1120                    }
1121                    out.push('}');
1122                }
1123                Some(&'{') => {
1124                    // already braced — pass through as-is
1125                    out.push('$');
1126                }
1127                _ => out.push('$'),
1128            }
1129        } else {
1130            out.push(c);
1131        }
1132    }
1133    out
1134}
1135
1136/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
1137/// past the closing `]`.
1138fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
1139    debug_assert_eq!(chars.get(i), Some(&'['));
1140    out.push('[');
1141    i += 1;
1142    if i < chars.len() && chars[i] == '^' {
1143        out.push('^');
1144        i += 1;
1145    }
1146    if i >= chars.len() {
1147        return i;
1148    }
1149    // `]` as the first class character is literal iff another unescaped `]` closes the class
1150    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
1151    // this `]`.
1152    if chars[i] == ']' {
1153        if i + 1 < chars.len() && chars[i + 1] == ']' {
1154            // `[]]` / `[^]]`: literal `]` then the closing `]`.
1155            out.push(']');
1156            i += 1;
1157        } else {
1158            let mut scan = i + 1;
1159            let mut found_closing = false;
1160            while scan < chars.len() {
1161                if chars[scan] == '\\' && scan + 1 < chars.len() {
1162                    scan += 2;
1163                    continue;
1164                }
1165                if chars[scan] == ']' {
1166                    found_closing = true;
1167                    break;
1168                }
1169                scan += 1;
1170            }
1171            if found_closing {
1172                out.push(']');
1173                i += 1;
1174            } else {
1175                out.push(']');
1176                return i + 1;
1177            }
1178        }
1179    }
1180    while i < chars.len() && chars[i] != ']' {
1181        if chars[i] == '\\' && i + 1 < chars.len() {
1182            out.push(chars[i]);
1183            out.push(chars[i + 1]);
1184            i += 2;
1185            continue;
1186        }
1187        out.push(chars[i]);
1188        i += 1;
1189    }
1190    if i < chars.len() {
1191        out.push(']');
1192        i += 1;
1193    }
1194    i
1195}
1196
1197/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
1198/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
1199/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1200/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1201fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1202    if multiline_flag {
1203        return pat.to_string();
1204    }
1205    let chars: Vec<char> = pat.chars().collect();
1206    let mut out = String::with_capacity(pat.len().saturating_add(16));
1207    let mut i = 0usize;
1208    while i < chars.len() {
1209        let c = chars[i];
1210        if c == '\\' && i + 1 < chars.len() {
1211            out.push(c);
1212            out.push(chars[i + 1]);
1213            i += 2;
1214            continue;
1215        }
1216        if c == '[' {
1217            i = copy_regex_char_class(&chars, i, &mut out);
1218            continue;
1219        }
1220        if c == '$' {
1221            if let Some(&next) = chars.get(i + 1) {
1222                if next.is_ascii_digit() {
1223                    out.push(c);
1224                    i += 1;
1225                    continue;
1226                }
1227                if next == '{' {
1228                    out.push(c);
1229                    i += 1;
1230                    continue;
1231                }
1232                if next.is_ascii_alphanumeric() || next == '_' {
1233                    out.push(c);
1234                    i += 1;
1235                    continue;
1236                }
1237            }
1238            out.push_str("(?=\\n?\\z)");
1239            i += 1;
1240            continue;
1241        }
1242        out.push(c);
1243        i += 1;
1244    }
1245    out
1246}
1247
1248/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1249#[derive(Debug, Clone)]
1250pub(crate) struct DirHandleState {
1251    pub entries: Vec<String>,
1252    pub pos: usize,
1253}
1254
1255/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1256pub(crate) fn perl_osname() -> String {
1257    match std::env::consts::OS {
1258        "linux" => "linux".to_string(),
1259        "macos" => "darwin".to_string(),
1260        "windows" => "MSWin32".to_string(),
1261        other => other.to_string(),
1262    }
1263}
1264
1265fn perl_version_v_string() -> String {
1266    format!("v{}", env!("CARGO_PKG_VERSION"))
1267}
1268
1269fn extended_os_error_string() -> String {
1270    std::io::Error::last_os_error().to_string()
1271}
1272
1273#[cfg(unix)]
1274fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1275    unsafe {
1276        (
1277            libc::getuid() as i64,
1278            libc::geteuid() as i64,
1279            libc::getgid() as i64,
1280            libc::getegid() as i64,
1281        )
1282    }
1283}
1284
1285#[cfg(not(unix))]
1286fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1287    (0, 0, 0, 0)
1288}
1289
1290fn unix_id_for_special(name: &str) -> i64 {
1291    let (r, e, _, _) = unix_real_effective_ids();
1292    match name {
1293        "<" => r,
1294        ">" => e,
1295        _ => 0,
1296    }
1297}
1298
1299#[cfg(unix)]
1300fn unix_group_list_string(primary: libc::gid_t) -> String {
1301    let mut buf = vec![0 as libc::gid_t; 256];
1302    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1303    if n <= 0 {
1304        return format!("{}", primary);
1305    }
1306    let mut parts = vec![format!("{}", primary)];
1307    for g in buf.iter().take(n as usize) {
1308        parts.push(format!("{}", g));
1309    }
1310    parts.join(" ")
1311}
1312
1313/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1314#[cfg(unix)]
1315fn unix_group_list_for_special(name: &str) -> String {
1316    let (_, _, gid, egid) = unix_real_effective_ids();
1317    match name {
1318        "(" => unix_group_list_string(gid as libc::gid_t),
1319        ")" => unix_group_list_string(egid as libc::gid_t),
1320        _ => String::new(),
1321    }
1322}
1323
1324#[cfg(not(unix))]
1325fn unix_group_list_for_special(_name: &str) -> String {
1326    String::new()
1327}
1328
1329/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1330/// `~/.ssh/config` and keys).
1331#[cfg(unix)]
1332fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1333    use libc::{getpwuid_r, getuid};
1334    use std::ffi::CStr;
1335    use std::os::unix::ffi::OsStringExt;
1336    let uid = unsafe { getuid() };
1337    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1338    let mut result: *mut libc::passwd = std::ptr::null_mut();
1339    let mut buf = vec![0u8; 16_384];
1340    let rc = unsafe {
1341        getpwuid_r(
1342            uid,
1343            &mut pw,
1344            buf.as_mut_ptr().cast::<libc::c_char>(),
1345            buf.len(),
1346            &mut result,
1347        )
1348    };
1349    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1350        return None;
1351    }
1352    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1353    if bytes.is_empty() {
1354        return None;
1355    }
1356    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1357}
1358
1359/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1360#[cfg(unix)]
1361fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1362    use libc::getpwnam_r;
1363    use std::ffi::{CStr, CString};
1364    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1365    let bytes = login.as_bytes();
1366    if bytes.is_empty() || bytes.contains(&0) {
1367        return None;
1368    }
1369    let cname = CString::new(bytes).ok()?;
1370    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1371    let mut result: *mut libc::passwd = std::ptr::null_mut();
1372    let mut buf = vec![0u8; 16_384];
1373    let rc = unsafe {
1374        getpwnam_r(
1375            cname.as_ptr(),
1376            &mut pw,
1377            buf.as_mut_ptr().cast::<libc::c_char>(),
1378            buf.len(),
1379            &mut result,
1380        )
1381    };
1382    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1383        return None;
1384    }
1385    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1386    if dir_bytes.is_empty() {
1387        return None;
1388    }
1389    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1390}
1391
1392impl Default for VMHelper {
1393    fn default() -> Self {
1394        Self::new()
1395    }
1396}
1397
1398/// How [`VMHelper::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1399#[derive(Clone, Copy)]
1400pub(crate) enum CaptureAllMode {
1401    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1402    Empty,
1403    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1404    Append,
1405    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1406    Skip,
1407}
1408
1409impl VMHelper {
1410    pub fn new() -> Self {
1411        let mut scope = Scope::new();
1412        scope.declare_array("INC", vec![StrykeValue::string(".".to_string())]);
1413        scope.declare_hash("INC", IndexMap::new());
1414        scope.declare_array("ARGV", vec![]);
1415        scope.declare_array("_", vec![]);
1416
1417        // @path / @p — $PATH split by OS path separator, frozen (immutable)
1418        let path_vec: Vec<StrykeValue> = std::env::var("PATH")
1419            .unwrap_or_default()
1420            .split(if cfg!(windows) { ';' } else { ':' })
1421            .filter(|s| !s.is_empty())
1422            .map(|p| StrykeValue::string(p.to_string()))
1423            .collect();
1424        scope.declare_array_frozen("path", path_vec.clone(), true);
1425        scope.declare_array_frozen("p", path_vec, true);
1426
1427        // @fpath / @f — $FPATH (zsh function path) split by ':', frozen
1428        let fpath_vec: Vec<StrykeValue> = std::env::var("FPATH")
1429            .unwrap_or_default()
1430            .split(':')
1431            .filter(|s| !s.is_empty())
1432            .map(|p| StrykeValue::string(p.to_string()))
1433            .collect();
1434        scope.declare_array_frozen("fpath", fpath_vec.clone(), true);
1435        scope.declare_array_frozen("f", fpath_vec, true);
1436        scope.declare_hash("ENV", IndexMap::new());
1437        scope.declare_hash("SIG", IndexMap::new());
1438
1439        // %term — terminal info (frozen)
1440        let term_map = build_term_hash();
1441        scope.declare_hash_global_frozen("term", term_map);
1442
1443        // %uname — system identification (frozen, Unix only)
1444        #[cfg(unix)]
1445        {
1446            let uname_map = build_uname_hash();
1447            scope.declare_hash_global_frozen("uname", uname_map);
1448        }
1449        #[cfg(not(unix))]
1450        {
1451            scope.declare_hash_global_frozen("uname", IndexMap::new());
1452        }
1453
1454        // %limits — resource limits (frozen, Unix only)
1455        #[cfg(unix)]
1456        {
1457            let limits_map = build_limits_hash();
1458            scope.declare_hash_global_frozen("limits", limits_map);
1459        }
1460        #[cfg(not(unix))]
1461        {
1462            scope.declare_hash_global_frozen("limits", IndexMap::new());
1463        }
1464
1465        // Reflection hashes — populated from `build.rs`-generated tables so
1466        // they track the real parser/dispatcher/LSP without hand-maintenance.
1467        // Eleven hashes; all lookups are O(1). Forward maps:
1468        //   %b   / %stryke::builtins      — callable name → category ("parallel", "string", …)
1469        //   %k   / %stryke::keywords      — language keyword → category ("control", "decl", …)
1470        //   %o   / %stryke::operators     — symbol operator → category ("arith", "pipeline", …)
1471        //   %v   / %stryke::special_vars  — special var spelling (sigil included) → category
1472        //   %pc  / %stryke::perl_compats  — subset: Perl 5 core only
1473        //   %e   / %stryke::extensions    — subset: stryke-only
1474        //   %a   / %stryke::aliases       — alias → primary
1475        //   %d   / %stryke::descriptions  — name → LSP one-liner (sparse)
1476        //   %all / %stryke::all           — primaries + aliases + keywords (union)
1477        // Inverted indexes for constant-time reverse queries:
1478        //   %c   / %stryke::categories    — category → arrayref of names
1479        //   %p   / %stryke::primaries     — primary → arrayref of aliases
1480        //
1481        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1482        // together they cover `keys %builtins`. Short aliases use the
1483        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1484        // Reflection hashes are lazily initialized on first access
1485        // (see `ensure_reflection_hashes`). Only declare the version scalar
1486        // eagerly since it's trivial.
1487        scope.declare_scalar(
1488            "stryke::VERSION",
1489            StrykeValue::string(env!("CARGO_PKG_VERSION").to_string()),
1490        );
1491        scope.declare_array("-", vec![]);
1492        scope.declare_array("+", vec![]);
1493        scope.declare_array("^CAPTURE", vec![]);
1494        scope.declare_array("^CAPTURE_ALL", vec![]);
1495        scope.declare_hash("^HOOK", IndexMap::new());
1496        scope.declare_scalar("~", StrykeValue::string("STDOUT".to_string()));
1497
1498        let script_start_time = std::time::SystemTime::now()
1499            .duration_since(std::time::UNIX_EPOCH)
1500            .map(|d| d.as_secs() as i64)
1501            .unwrap_or(0);
1502
1503        let executable_path = cached_executable_path();
1504
1505        let stryke_pwd_init = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1506        let stryke_pwd = std::fs::canonicalize(&stryke_pwd_init).unwrap_or(stryke_pwd_init);
1507
1508        let mut special_caret_scalars: HashMap<String, StrykeValue> = HashMap::new();
1509        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1510            special_caret_scalars.insert(format!("^{}", name), StrykeValue::UNDEF);
1511        }
1512
1513        let mut s = Self {
1514            scope,
1515            subs: HashMap::new(),
1516            intercepts: Vec::new(),
1517            next_intercept_id: 1,
1518            intercept_ctx_stack: Vec::new(),
1519            intercept_active_names: Vec::new(),
1520            struct_defs: HashMap::new(),
1521            enum_defs: HashMap::new(),
1522            class_defs: HashMap::new(),
1523            trait_defs: HashMap::new(),
1524            file: "-e".to_string(),
1525            output_handles: HashMap::new(),
1526            input_handles: HashMap::new(),
1527            ofs: String::new(),
1528            ors: String::new(),
1529            irs: Some("\n".to_string()),
1530            errno: String::new(),
1531            errno_code: 0,
1532            eval_error: String::new(),
1533            eval_error_code: 0,
1534            eval_error_value: None,
1535            argv: Vec::new(),
1536            env: IndexMap::new(),
1537            env_materialized: false,
1538            program_name: "stryke".to_string(),
1539            line_number: 0,
1540            last_readline_handle: String::new(),
1541            last_stdin_die_bracket: "<STDIN>".to_string(),
1542            handle_line_numbers: HashMap::new(),
1543            flip_flop_active: Vec::new(),
1544            flip_flop_exclusive_left_line: Vec::new(),
1545            flip_flop_sequence: Vec::new(),
1546            flip_flop_last_dot: Vec::new(),
1547            flip_flop_tree: HashMap::new(),
1548            sigint_pending_caret: Cell::new(false),
1549            auto_split: false,
1550            field_separator: None,
1551            begin_blocks: Vec::new(),
1552            unit_check_blocks: Vec::new(),
1553            check_blocks: Vec::new(),
1554            init_blocks: Vec::new(),
1555            end_blocks: Vec::new(),
1556            warnings: false,
1557            output_autoflush: false,
1558            default_print_handle: "STDOUT".to_string(),
1559            suppress_stdout: false,
1560            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1561            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1562            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1563            test_pass_total: std::sync::atomic::AtomicUsize::new(0),
1564            test_fail_total: std::sync::atomic::AtomicUsize::new(0),
1565            test_skip_total: std::sync::atomic::AtomicUsize::new(0),
1566            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1567            child_exit_status: 0,
1568            last_match: String::new(),
1569            prematch: String::new(),
1570            postmatch: String::new(),
1571            last_paren_match: String::new(),
1572            list_separator: " ".to_string(),
1573            script_start_time,
1574            compile_hints: 0,
1575            warning_bits: 0,
1576            global_phase: "RUN".to_string(),
1577            subscript_sep: "\x1c".to_string(),
1578            inplace_edit: String::new(),
1579            debug_flags: 0,
1580            perl_debug_flags: 0,
1581            eval_nesting: 0,
1582            argv_current_file: String::new(),
1583            diamond_next_idx: 0,
1584            diamond_reader: None,
1585            strict_refs: false,
1586            strict_subs: false,
1587            strict_vars: false,
1588            utf8_pragma: false,
1589            open_pragma_utf8: false,
1590            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1591            feature_bits: FEAT_SAY,
1592            num_threads: 0, // lazily read from rayon on first parallel op
1593            regex_cache: HashMap::new(),
1594            regex_last: None,
1595            regex_match_memo: None,
1596            regex_capture_scope_fresh: false,
1597            regex_pos: HashMap::new(),
1598            state_vars: HashMap::new(),
1599            state_bindings_stack: Vec::new(),
1600            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1601            dir_handles: HashMap::new(),
1602            io_file_slots: HashMap::new(),
1603            pipe_children: HashMap::new(),
1604            socket_handles: HashMap::new(),
1605            wantarray_kind: WantarrayCtx::Scalar,
1606            profiler: None,
1607            module_export_lists: HashMap::new(),
1608            virtual_modules: HashMap::new(),
1609            tied_hashes: HashMap::new(),
1610            tied_scalars: HashMap::new(),
1611            tied_arrays: HashMap::new(),
1612            overload_table: HashMap::new(),
1613            format_templates: HashMap::new(),
1614            special_caret_scalars,
1615            format_page_number: 0,
1616            format_lines_per_page: 60,
1617            format_lines_left: 0,
1618            format_line_break_chars: "\n".to_string(),
1619            format_top_name: String::new(),
1620            accumulator_format: String::new(),
1621            max_system_fd: 2,
1622            emergency_memory: String::new(),
1623            last_subpattern_name: String::new(),
1624            inc_hook_index: 0,
1625            multiline_match: false,
1626            executable_path,
1627            formfeed_string: "\x0c".to_string(),
1628            glob_handle_alias: HashMap::new(),
1629            glob_restore_frames: vec![Vec::new()],
1630            special_var_restore_frames: vec![Vec::new()],
1631            reflection_hashes_ready: false,
1632            english_enabled: false,
1633            english_no_match_vars: false,
1634            english_match_vars_ever_enabled: false,
1635            english_lexical_scalars: vec![HashSet::new()],
1636            our_lexical_scalars: vec![HashSet::new()],
1637            our_lexical_arrays: vec![HashSet::new()],
1638            our_lexical_hashes: vec![HashSet::new()],
1639            vm_jit_enabled: !matches!(
1640                std::env::var("STRYKE_NO_JIT"),
1641                Ok(v)
1642                    if v == "1"
1643                        || v.eq_ignore_ascii_case("true")
1644                        || v.eq_ignore_ascii_case("yes")
1645            ),
1646            disasm_bytecode: false,
1647            cached_chunk: None,
1648            cache_script_path: None,
1649            stryke_pwd,
1650            in_generator: false,
1651            line_mode_skip_main: false,
1652            line_mode_chunk: None,
1653            line_mode_eof_pending: false,
1654            line_mode_stdin_pending: VecDeque::new(),
1655            rate_limit_slots: Vec::new(),
1656            log_level_override: None,
1657            current_sub_stack: Vec::new(),
1658            debugger: None,
1659            debug_call_stack: Vec::new(),
1660        };
1661        s.install_overload_pragma_stubs();
1662        s
1663    }
1664
1665    /// Lazily populate the reflection hashes (`%b`, `%stryke::builtins`, etc.)
1666    /// on first access. This avoids building ~12k hash entries on startup for
1667    /// one-liners that never touch introspection.
1668    pub(crate) fn ensure_reflection_hashes(&mut self) {
1669        if self.reflection_hashes_ready {
1670            return;
1671        }
1672        self.reflection_hashes_ready = true;
1673        // Package stashes (`%main::` / `%Pkg::`) are Perl-spec, install in
1674        // every mode — `--compat` does not turn off the symbol table.
1675        self.refresh_package_stashes();
1676        // Everything below is stryke-only. `--compat` skips the entire block
1677        // so a Perl 5 script sees no extension hashes and can use `%all` /
1678        // `%b` / `%parameters` / `%stryke::*` etc. as ordinary user hashes.
1679        if crate::compat_mode() {
1680            return;
1681        }
1682        let builtins_map = crate::builtins::builtins_hash_map();
1683        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1684        let extensions_map = crate::builtins::extensions_hash_map();
1685        let aliases_map = crate::builtins::aliases_hash_map();
1686        let descriptions_map = crate::builtins::descriptions_hash_map();
1687        let categories_map = crate::builtins::categories_hash_map();
1688        let primaries_map = crate::builtins::primaries_hash_map();
1689        let keywords_map = crate::builtins::keywords_hash_map();
1690        let operators_map = crate::builtins::operators_hash_map();
1691        let special_vars_map = crate::builtins::special_vars_hash_map();
1692        let all_map = crate::builtins::all_hash_map();
1693        self.scope
1694            .declare_hash_global_frozen("stryke::builtins", builtins_map.clone());
1695        self.scope
1696            .declare_hash_global_frozen("stryke::perl_compats", perl_compats_map.clone());
1697        self.scope
1698            .declare_hash_global_frozen("stryke::extensions", extensions_map.clone());
1699        self.scope
1700            .declare_hash_global_frozen("stryke::aliases", aliases_map.clone());
1701        self.scope
1702            .declare_hash_global_frozen("stryke::descriptions", descriptions_map.clone());
1703        self.scope
1704            .declare_hash_global_frozen("stryke::categories", categories_map.clone());
1705        self.scope
1706            .declare_hash_global_frozen("stryke::primaries", primaries_map.clone());
1707        self.scope
1708            .declare_hash_global_frozen("stryke::keywords", keywords_map.clone());
1709        self.scope
1710            .declare_hash_global_frozen("stryke::operators", operators_map.clone());
1711        self.scope
1712            .declare_hash_global_frozen("stryke::special_vars", special_vars_map.clone());
1713        self.scope
1714            .declare_hash_global_frozen("stryke::all", all_map.clone());
1715        // Short aliases: only declare if no user-declared hash with that name
1716        // exists, to avoid overwriting `my %e` etc.
1717        for (name, val) in [
1718            ("b", builtins_map),
1719            ("pc", perl_compats_map),
1720            ("e", extensions_map),
1721            ("a", aliases_map),
1722            ("d", descriptions_map),
1723            ("c", categories_map),
1724            ("p", primaries_map),
1725            ("k", keywords_map),
1726            ("o", operators_map),
1727            ("v", special_vars_map),
1728            ("all", all_map),
1729        ] {
1730            if !self.scope.any_frame_has_hash(name) {
1731                self.scope.declare_hash_global_frozen(name, val);
1732            }
1733        }
1734        // Initial install of `%parameters` (zsh-`$parameters` analogue).
1735        // Refreshed automatically on every read via `touch_env_hash`.
1736        if !self.scope.has_lexical_hash("parameters") {
1737            self.refresh_parameters_hash();
1738        }
1739    }
1740
1741    /// Rebuild `%parameters` (zsh-`$parameters` analogue) from the current
1742    /// scope. Maps every live sigil-prefixed name (`$x`, `@a`, `%h`, …) to its
1743    /// kind string (`"scalar"`, `"array"`, `"hash"`, `"atomic_array"`,
1744    /// `"atomic_hash"`, `"shared_array"`, `"shared_hash"`). Installed as a
1745    /// frozen global hash so user code can read it but not assign into it
1746    /// (parallel to `%all` / `%b` / `%stryke::*`). Refreshed automatically on
1747    /// every `%parameters` read via the `touch_env_hash` hook, so the snapshot
1748    /// is always current — the user never needs to call this directly.
1749    pub fn refresh_parameters_hash(&mut self) {
1750        let pairs = self.scope.parameters_pairs();
1751        let mut h: indexmap::IndexMap<String, StrykeValue> =
1752            indexmap::IndexMap::with_capacity(pairs.len());
1753        for (name, kind) in pairs {
1754            h.insert(name, StrykeValue::string(kind.to_string()));
1755        }
1756        // declare_hash_global_frozen overwrites unconditionally, so each
1757        // refresh replaces the prior snapshot.
1758        self.scope.declare_hash_global_frozen("parameters", h);
1759    }
1760
1761    /// Populate `%main::` / `%Foo::` package stashes with current symbol-table
1762    /// state so `keys %main::` and `keys %Foo::` enumerate live names. Maps
1763    /// each unqualified name → its kind string (`"scalar"`, `"array"`,
1764    /// `"hash"`, `"sub"`). Stryke has no real Perl typeglob layer; the kind
1765    /// string is the most useful per-symbol value we can offer.
1766    ///
1767    /// Callable repeatedly — overwrites prior stashes — so the REPL refreshes
1768    /// after every line and scripts can call it explicitly via the
1769    /// `refresh_stashes()` builtin if they want post-eval visibility.
1770    pub fn refresh_package_stashes(&mut self) {
1771        use indexmap::IndexMap;
1772
1773        let mut by_pkg: std::collections::HashMap<String, IndexMap<String, StrykeValue>> =
1774            std::collections::HashMap::new();
1775
1776        let record = |pkg: &str,
1777                      sym: &str,
1778                      kind: &str,
1779                      map: &mut std::collections::HashMap<
1780            String,
1781            IndexMap<String, StrykeValue>,
1782        >| {
1783            map.entry(pkg.to_string())
1784                .or_default()
1785                .insert(sym.to_string(), StrykeValue::string(kind.to_string()));
1786        };
1787
1788        // Subs: keys like "main::foo" / "Foo::Bar::baz".
1789        for key in self.subs.keys() {
1790            if let Some(idx) = key.rfind("::") {
1791                let (pkg, rest) = key.split_at(idx);
1792                let sym = &rest[2..];
1793                if pkg.is_empty() || sym.is_empty() {
1794                    continue;
1795                }
1796                record(pkg, sym, "sub", &mut by_pkg);
1797            } else {
1798                // Bare-name sub (no package qualifier) lives in main::.
1799                record("main", key, "sub", &mut by_pkg);
1800            }
1801        }
1802
1803        // Package-qualified scalars / arrays / hashes from every frame.
1804        for frame in self.scope.frames_for_introspection() {
1805            let (scalars, arrays, hashes) = frame;
1806            for name in scalars {
1807                if let Some(idx) = name.rfind("::") {
1808                    let (pkg, rest) = name.split_at(idx);
1809                    let sym = &rest[2..];
1810                    if !pkg.is_empty() && !sym.is_empty() {
1811                        record(pkg, sym, "scalar", &mut by_pkg);
1812                    }
1813                }
1814            }
1815            for name in arrays {
1816                if let Some(idx) = name.rfind("::") {
1817                    let (pkg, rest) = name.split_at(idx);
1818                    let sym = &rest[2..];
1819                    if !pkg.is_empty() && !sym.is_empty() {
1820                        record(pkg, sym, "array", &mut by_pkg);
1821                    }
1822                }
1823            }
1824            for name in hashes {
1825                if let Some(idx) = name.rfind("::") {
1826                    let (pkg, rest) = name.split_at(idx);
1827                    let sym = &rest[2..];
1828                    if !pkg.is_empty() && !sym.is_empty() {
1829                        record(pkg, sym, "hash", &mut by_pkg);
1830                    }
1831                }
1832            }
1833        }
1834
1835        // Install each `%Pkg::` in the global frame. Lexer emits the trailing
1836        // `::` as part of the name, so the stash hash lives under that exact
1837        // key. `declare_hash_global_frozen` overwrites any prior copy.
1838        for (pkg, mut entries) in by_pkg {
1839            entries.sort_keys();
1840            let key = format!("{}::", pkg);
1841            self.scope.declare_hash_global_frozen(&key, entries);
1842        }
1843    }
1844
1845    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1846    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1847    /// strict subs and to satisfy `use overload ();` call sites.
1848    fn install_overload_pragma_stubs(&mut self) {
1849        let empty: Block = vec![];
1850        for key in ["overload::import", "overload::unimport"] {
1851            let name = key.to_string();
1852            self.subs.insert(
1853                name.clone(),
1854                Arc::new(StrykeSub {
1855                    name,
1856                    params: vec![],
1857                    body: empty.clone(),
1858                    prototype: None,
1859                    closure_env: None,
1860                    fib_like: None,
1861                }),
1862            );
1863        }
1864    }
1865
1866    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1867    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1868    pub fn line_mode_worker_clone(&self) -> VMHelper {
1869        VMHelper {
1870            scope: self.scope.clone(),
1871            subs: self.subs.clone(),
1872            intercepts: self.intercepts.clone(),
1873            next_intercept_id: self.next_intercept_id,
1874            intercept_ctx_stack: self.intercept_ctx_stack.clone(),
1875            intercept_active_names: self.intercept_active_names.clone(),
1876            struct_defs: self.struct_defs.clone(),
1877            enum_defs: self.enum_defs.clone(),
1878            class_defs: self.class_defs.clone(),
1879            trait_defs: self.trait_defs.clone(),
1880            file: self.file.clone(),
1881            output_handles: HashMap::new(),
1882            input_handles: HashMap::new(),
1883            ofs: self.ofs.clone(),
1884            ors: self.ors.clone(),
1885            irs: self.irs.clone(),
1886            errno: self.errno.clone(),
1887            errno_code: self.errno_code,
1888            eval_error: self.eval_error.clone(),
1889            eval_error_code: self.eval_error_code,
1890            eval_error_value: self.eval_error_value.clone(),
1891            argv: self.argv.clone(),
1892            env: self.env.clone(),
1893            env_materialized: self.env_materialized,
1894            program_name: self.program_name.clone(),
1895            line_number: 0,
1896            last_readline_handle: String::new(),
1897            last_stdin_die_bracket: "<STDIN>".to_string(),
1898            handle_line_numbers: HashMap::new(),
1899            flip_flop_active: Vec::new(),
1900            flip_flop_exclusive_left_line: Vec::new(),
1901            flip_flop_sequence: Vec::new(),
1902            flip_flop_last_dot: Vec::new(),
1903            flip_flop_tree: HashMap::new(),
1904            sigint_pending_caret: Cell::new(false),
1905            auto_split: self.auto_split,
1906            field_separator: self.field_separator.clone(),
1907            begin_blocks: self.begin_blocks.clone(),
1908            unit_check_blocks: self.unit_check_blocks.clone(),
1909            check_blocks: self.check_blocks.clone(),
1910            init_blocks: self.init_blocks.clone(),
1911            end_blocks: self.end_blocks.clone(),
1912            warnings: self.warnings,
1913            output_autoflush: self.output_autoflush,
1914            default_print_handle: self.default_print_handle.clone(),
1915            suppress_stdout: self.suppress_stdout,
1916            // Workers start with fresh test counters — they don't share with the
1917            // parent. The parent is responsible for aggregating across workers if
1918            // it cares (none of the current parallel callers do).
1919            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1920            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1921            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1922            test_pass_total: std::sync::atomic::AtomicUsize::new(0),
1923            test_fail_total: std::sync::atomic::AtomicUsize::new(0),
1924            test_skip_total: std::sync::atomic::AtomicUsize::new(0),
1925            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1926            child_exit_status: self.child_exit_status,
1927            last_match: self.last_match.clone(),
1928            prematch: self.prematch.clone(),
1929            postmatch: self.postmatch.clone(),
1930            last_paren_match: self.last_paren_match.clone(),
1931            list_separator: self.list_separator.clone(),
1932            script_start_time: self.script_start_time,
1933            compile_hints: self.compile_hints,
1934            warning_bits: self.warning_bits,
1935            global_phase: self.global_phase.clone(),
1936            subscript_sep: self.subscript_sep.clone(),
1937            inplace_edit: self.inplace_edit.clone(),
1938            debug_flags: self.debug_flags,
1939            perl_debug_flags: self.perl_debug_flags,
1940            eval_nesting: self.eval_nesting,
1941            argv_current_file: String::new(),
1942            diamond_next_idx: 0,
1943            diamond_reader: None,
1944            strict_refs: self.strict_refs,
1945            strict_subs: self.strict_subs,
1946            strict_vars: self.strict_vars,
1947            utf8_pragma: self.utf8_pragma,
1948            open_pragma_utf8: self.open_pragma_utf8,
1949            feature_bits: self.feature_bits,
1950            num_threads: 0,
1951            regex_cache: self.regex_cache.clone(),
1952            regex_last: self.regex_last.clone(),
1953            regex_match_memo: self.regex_match_memo.clone(),
1954            regex_capture_scope_fresh: false,
1955            regex_pos: self.regex_pos.clone(),
1956            state_vars: self.state_vars.clone(),
1957            state_bindings_stack: Vec::new(),
1958            rand_rng: self.rand_rng.clone(),
1959            dir_handles: HashMap::new(),
1960            io_file_slots: HashMap::new(),
1961            pipe_children: HashMap::new(),
1962            socket_handles: HashMap::new(),
1963            wantarray_kind: self.wantarray_kind,
1964            profiler: None,
1965            module_export_lists: self.module_export_lists.clone(),
1966            virtual_modules: self.virtual_modules.clone(),
1967            tied_hashes: self.tied_hashes.clone(),
1968            tied_scalars: self.tied_scalars.clone(),
1969            tied_arrays: self.tied_arrays.clone(),
1970            overload_table: self.overload_table.clone(),
1971            format_templates: self.format_templates.clone(),
1972            special_caret_scalars: self.special_caret_scalars.clone(),
1973            format_page_number: self.format_page_number,
1974            format_lines_per_page: self.format_lines_per_page,
1975            format_lines_left: self.format_lines_left,
1976            format_line_break_chars: self.format_line_break_chars.clone(),
1977            format_top_name: self.format_top_name.clone(),
1978            accumulator_format: self.accumulator_format.clone(),
1979            max_system_fd: self.max_system_fd,
1980            emergency_memory: self.emergency_memory.clone(),
1981            last_subpattern_name: self.last_subpattern_name.clone(),
1982            inc_hook_index: self.inc_hook_index,
1983            multiline_match: self.multiline_match,
1984            executable_path: self.executable_path.clone(),
1985            formfeed_string: self.formfeed_string.clone(),
1986            glob_handle_alias: self.glob_handle_alias.clone(),
1987            glob_restore_frames: self.glob_restore_frames.clone(),
1988            special_var_restore_frames: self.special_var_restore_frames.clone(),
1989            reflection_hashes_ready: self.reflection_hashes_ready,
1990            english_enabled: self.english_enabled,
1991            english_no_match_vars: self.english_no_match_vars,
1992            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
1993            english_lexical_scalars: self.english_lexical_scalars.clone(),
1994            our_lexical_scalars: self.our_lexical_scalars.clone(),
1995            our_lexical_arrays: self.our_lexical_arrays.clone(),
1996            our_lexical_hashes: self.our_lexical_hashes.clone(),
1997            vm_jit_enabled: self.vm_jit_enabled,
1998            disasm_bytecode: self.disasm_bytecode,
1999            // Sideband cache fields belong to the top-level driver, not line-mode workers.
2000            cached_chunk: None,
2001            cache_script_path: None,
2002            stryke_pwd: self.stryke_pwd.clone(),
2003            in_generator: false,
2004            line_mode_skip_main: false,
2005            line_mode_chunk: self.line_mode_chunk.clone(),
2006            line_mode_eof_pending: false,
2007            line_mode_stdin_pending: VecDeque::new(),
2008            rate_limit_slots: Vec::new(),
2009            log_level_override: self.log_level_override,
2010            current_sub_stack: Vec::new(),
2011            debugger: None,
2012            debug_call_stack: Vec::new(),
2013        }
2014    }
2015
2016    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
2017    pub(crate) fn parallel_thread_count(&mut self) -> usize {
2018        if self.num_threads == 0 {
2019            self.num_threads = rayon::current_num_threads();
2020        }
2021        self.num_threads
2022    }
2023
2024    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
2025    pub(crate) fn eval_par_list_call(
2026        &mut self,
2027        name: &str,
2028        args: &[StrykeValue],
2029        ctx: WantarrayCtx,
2030        line: usize,
2031    ) -> StrykeResult<StrykeValue> {
2032        match name {
2033            "puniq" => {
2034                let (list_src, show_prog) = match args.len() {
2035                    0 => return Err(StrykeError::runtime("puniq: expected LIST", line)),
2036                    1 => (&args[0], false),
2037                    2 => (&args[0], args[1].is_true()),
2038                    _ => {
2039                        return Err(StrykeError::runtime(
2040                            "puniq: expected LIST [, progress => EXPR]",
2041                            line,
2042                        ));
2043                    }
2044                };
2045                let list = list_src.to_list();
2046                let n_threads = self.parallel_thread_count();
2047                let pmap_progress = PmapProgress::new(show_prog, list.len());
2048                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
2049                pmap_progress.finish();
2050                if ctx == WantarrayCtx::List {
2051                    Ok(StrykeValue::array(out))
2052                } else {
2053                    Ok(StrykeValue::integer(out.len() as i64))
2054                }
2055            }
2056            "pfirst" => {
2057                let (code_val, list_src, show_prog) = match args.len() {
2058                    2 => (&args[0], &args[1], false),
2059                    3 => (&args[0], &args[1], args[2].is_true()),
2060                    _ => {
2061                        return Err(StrykeError::runtime(
2062                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
2063                            line,
2064                        ));
2065                    }
2066                };
2067                let Some(sub) = code_val.as_code_ref() else {
2068                    return Err(StrykeError::runtime(
2069                        "pfirst: first argument must be a code reference",
2070                        line,
2071                    ));
2072                };
2073                let sub = sub.clone();
2074                let list = list_src.to_list();
2075                if list.is_empty() {
2076                    return Ok(StrykeValue::UNDEF);
2077                }
2078                let pmap_progress = PmapProgress::new(show_prog, list.len());
2079                let subs = self.subs.clone();
2080                let (scope_capture, atomic_arrays, atomic_hashes) =
2081                    self.scope.capture_with_atomics();
2082                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
2083                    let mut local_interp = VMHelper::new();
2084                    local_interp.subs = subs.clone();
2085                    local_interp.scope.restore_capture(&scope_capture);
2086                    local_interp
2087                        .scope
2088                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2089                    local_interp.enable_parallel_guard();
2090                    local_interp.scope.set_topic(item);
2091                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2092                        Ok(v) => v.is_true(),
2093                        Err(_) => false,
2094                    }
2095                });
2096                pmap_progress.finish();
2097                Ok(out.unwrap_or(StrykeValue::UNDEF))
2098            }
2099            "pany" => {
2100                let (code_val, list_src, show_prog) = match args.len() {
2101                    2 => (&args[0], &args[1], false),
2102                    3 => (&args[0], &args[1], args[2].is_true()),
2103                    _ => {
2104                        return Err(StrykeError::runtime(
2105                            "pany: expected BLOCK, LIST [, progress => EXPR]",
2106                            line,
2107                        ));
2108                    }
2109                };
2110                let Some(sub) = code_val.as_code_ref() else {
2111                    return Err(StrykeError::runtime(
2112                        "pany: first argument must be a code reference",
2113                        line,
2114                    ));
2115                };
2116                let sub = sub.clone();
2117                let list = list_src.to_list();
2118                let pmap_progress = PmapProgress::new(show_prog, list.len());
2119                let subs = self.subs.clone();
2120                let (scope_capture, atomic_arrays, atomic_hashes) =
2121                    self.scope.capture_with_atomics();
2122                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
2123                    let mut local_interp = VMHelper::new();
2124                    local_interp.subs = subs.clone();
2125                    local_interp.scope.restore_capture(&scope_capture);
2126                    local_interp
2127                        .scope
2128                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2129                    local_interp.enable_parallel_guard();
2130                    local_interp.scope.set_topic(item);
2131                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2132                        Ok(v) => v.is_true(),
2133                        Err(_) => false,
2134                    }
2135                });
2136                pmap_progress.finish();
2137                Ok(StrykeValue::integer(if b { 1 } else { 0 }))
2138            }
2139            _ => Err(StrykeError::runtime(
2140                format!("internal: unknown par_list builtin {name}"),
2141                line,
2142            )),
2143        }
2144    }
2145
2146    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
2147        #[cfg(unix)]
2148        if let Some(sig) = s.signal() {
2149            return sig as i64 & 0x7f;
2150        }
2151        let code = s.code().unwrap_or(0) as i64;
2152        code << 8
2153    }
2154
2155    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
2156        self.child_exit_status = self.encode_exit_status(s);
2157    }
2158
2159    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
2160    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
2161        // Perl's $! is the bare description ("No such file or directory"),
2162        // not Rust's "<desc> (os error N)" form. Strip the trailing parenthetical.
2163        let s = e.to_string();
2164        let stripped = s
2165            .rfind(" (os error ")
2166            .map(|i| s[..i].to_string())
2167            .unwrap_or(s);
2168        self.errno = stripped;
2169        self.errno_code = e.raw_os_error().unwrap_or(0);
2170    }
2171
2172    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
2173    ///
2174    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
2175    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
2176    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
2177    ///
2178    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
2179    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
2180    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
2181    /// `~/.ssh/config` and keys apply.
2182    pub(crate) fn ssh_builtin_execute(
2183        &mut self,
2184        args: &[StrykeValue],
2185    ) -> StrykeResult<StrykeValue> {
2186        use std::process::Command;
2187        let mut cmd = Command::new("ssh");
2188        #[cfg(unix)]
2189        {
2190            use libc::geteuid;
2191            let home_for_ssh = if unsafe { geteuid() } == 0 {
2192                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
2193            } else {
2194                None
2195            };
2196            if let Some(h) = home_for_ssh {
2197                cmd.env("HOME", h);
2198            } else if std::env::var_os("HOME").is_none() {
2199                if let Some(h) = pw_home_dir_for_current_uid() {
2200                    cmd.env("HOME", h);
2201                }
2202            }
2203        }
2204        for a in args {
2205            cmd.arg(a.to_string());
2206        }
2207        match cmd.status() {
2208            Ok(s) => {
2209                self.record_child_exit_status(s);
2210                Ok(StrykeValue::integer(s.code().unwrap_or(-1) as i64))
2211            }
2212            Err(e) => {
2213                self.apply_io_error_to_errno(&e);
2214                Ok(StrykeValue::integer(-1))
2215            }
2216        }
2217    }
2218
2219    /// Set `$@` message; numeric side is `0` if empty, else `1`.
2220    pub(crate) fn set_eval_error(&mut self, msg: String) {
2221        self.eval_error = msg;
2222        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2223        self.eval_error_value = None;
2224    }
2225
2226    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &StrykeError) {
2227        self.eval_error = e.to_string();
2228        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2229        self.eval_error_value = e.die_value.clone();
2230    }
2231
2232    pub(crate) fn clear_eval_error(&mut self) {
2233        self.eval_error = String::new();
2234        self.eval_error_code = 0;
2235        self.eval_error_value = None;
2236    }
2237
2238    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
2239    fn bump_line_for_handle(&mut self, handle_key: &str) {
2240        self.last_readline_handle = handle_key.to_string();
2241        *self
2242            .handle_line_numbers
2243            .entry(handle_key.to_string())
2244            .or_insert(0) += 1;
2245    }
2246
2247    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
2248    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
2249        if name.starts_with('^') {
2250            return name.to_string();
2251        }
2252        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
2253            let pkg = self.current_package();
2254            if !pkg.is_empty() && pkg != "main" {
2255                return format!("{}::{}", pkg, name);
2256            }
2257        }
2258        name.to_string()
2259    }
2260
2261    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
2262    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
2263        if name.contains("::") {
2264            return name.to_string();
2265        }
2266        let pkg = self.current_package();
2267        if pkg.is_empty() || pkg == "main" {
2268            format!("main::{}", name)
2269        } else {
2270            format!("{}::{}", pkg, name)
2271        }
2272    }
2273
2274    /// Bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
2275    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
2276        if name.contains("::") {
2277            return name.to_string();
2278        }
2279        for (lex, our) in self
2280            .english_lexical_scalars
2281            .iter()
2282            .zip(self.our_lexical_scalars.iter())
2283            .rev()
2284        {
2285            if lex.contains(name) {
2286                if our.contains(name) {
2287                    return self.stash_scalar_name_for_package(name);
2288                }
2289                return name.to_string();
2290            }
2291        }
2292        name.to_string()
2293    }
2294
2295    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
2296    pub(crate) fn tie_execute(
2297        &mut self,
2298        target_kind: u8,
2299        target_name: &str,
2300        class_and_args: Vec<StrykeValue>,
2301        line: usize,
2302    ) -> StrykeResult<StrykeValue> {
2303        let mut it = class_and_args.into_iter();
2304        let class = it.next().unwrap_or(StrykeValue::UNDEF);
2305        let pkg = class.to_string();
2306        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
2307        let tie_ctor = match target_kind {
2308            0 => "TIESCALAR",
2309            1 => "TIEARRAY",
2310            2 => "TIEHASH",
2311            _ => return Err(StrykeError::runtime("tie: invalid target kind", line)),
2312        };
2313        let tie_fn = format!("{}::{}", pkg, tie_ctor);
2314        let sub =
2315            self.subs.get(&tie_fn).cloned().ok_or_else(|| {
2316                StrykeError::runtime(format!("tie: cannot find &{}", tie_fn), line)
2317            })?;
2318        let mut call_args = vec![StrykeValue::string(pkg.clone())];
2319        call_args.extend(it);
2320        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
2321            Ok(v) => v,
2322            Err(FlowOrError::Flow(_)) => StrykeValue::UNDEF,
2323            Err(FlowOrError::Error(e)) => return Err(e),
2324        };
2325        match target_kind {
2326            0 => {
2327                self.tied_scalars.insert(target_name.to_string(), obj);
2328            }
2329            1 => {
2330                let key = self.stash_array_name_for_package(target_name);
2331                self.tied_arrays.insert(key, obj);
2332            }
2333            2 => {
2334                self.tied_hashes.insert(target_name.to_string(), obj);
2335            }
2336            _ => return Err(StrykeError::runtime("tie: invalid target kind", line)),
2337        }
2338        Ok(StrykeValue::UNDEF)
2339    }
2340
2341    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
2342    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
2343        let key = format!("{}::ISA", class);
2344        self.scope
2345            .get_array(&key)
2346            .into_iter()
2347            .map(|v| v.to_string())
2348            .collect()
2349    }
2350
2351    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
2352        let p = |c: &str| self.parents_of_class(c);
2353        linearize_c3(class, &p, 0)
2354    }
2355
2356    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
2357    pub(crate) fn resolve_method_full_name(
2358        &self,
2359        invocant_class: &str,
2360        method: &str,
2361        super_mode: bool,
2362    ) -> Option<String> {
2363        let mro = self.mro_linearize(invocant_class);
2364        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
2365        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
2366        // even when running `C::meth`.
2367        let start = if super_mode {
2368            mro.iter()
2369                .position(|p| p == invocant_class)
2370                .map(|i| i + 1)
2371                // If the class string does not appear in MRO (should be rare), skip the first
2372                // entry so we still search parents before giving up.
2373                .unwrap_or(1)
2374        } else {
2375            0
2376        };
2377        for pkg in mro.iter().skip(start) {
2378            if pkg == "UNIVERSAL" {
2379                continue;
2380            }
2381            let fq = format!("{}::{}", pkg, method);
2382            if self.subs.contains_key(&fq) {
2383                return Some(fq);
2384            }
2385        }
2386        mro.iter()
2387            .skip(start)
2388            .find(|p| *p != "UNIVERSAL")
2389            .map(|pkg| format!("{}::{}", pkg, method))
2390    }
2391
2392    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
2393        if let Some(alias) = self.glob_handle_alias.get(name) {
2394            return alias.clone();
2395        }
2396        // `print $fh …` stores the handle as "$varname"; resolve it by
2397        // reading the scalar variable which holds the IO handle name.
2398        if let Some(var_name) = name.strip_prefix('$') {
2399            let val = self.scope.get_scalar(var_name);
2400            let s = val.to_string();
2401            if !s.is_empty() {
2402                return self.resolve_io_handle_name(&s);
2403            }
2404        }
2405        name.to_string()
2406    }
2407
2408    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
2409    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
2410        if name.contains("::") {
2411            name.to_string()
2412        } else {
2413            self.qualify_sub_key(name)
2414        }
2415    }
2416
2417    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
2418    pub(crate) fn copy_typeglob_slots(
2419        &mut self,
2420        lhs: &str,
2421        rhs: &str,
2422        line: usize,
2423    ) -> StrykeResult<()> {
2424        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
2425        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
2426        match self.subs.get(&rhs_sub).cloned() {
2427            Some(s) => {
2428                self.subs.insert(lhs_sub, s);
2429            }
2430            None => {
2431                self.subs.remove(&lhs_sub);
2432            }
2433        }
2434        let sv = self.scope.get_scalar(rhs);
2435        self.scope
2436            .set_scalar(lhs, sv.clone())
2437            .map_err(|e| e.at_line(line))?;
2438        let lhs_an = self.stash_array_name_for_package(lhs);
2439        let rhs_an = self.stash_array_name_for_package(rhs);
2440        let av = self.scope.get_array(&rhs_an);
2441        self.scope
2442            .set_array(&lhs_an, av.clone())
2443            .map_err(|e| e.at_line(line))?;
2444        let hv = self.scope.get_hash(rhs);
2445        self.scope
2446            .set_hash(lhs, hv.clone())
2447            .map_err(|e| e.at_line(line))?;
2448        match self.glob_handle_alias.get(rhs).cloned() {
2449            Some(t) => {
2450                self.glob_handle_alias.insert(lhs.to_string(), t);
2451            }
2452            None => {
2453                self.glob_handle_alias.remove(lhs);
2454            }
2455        }
2456        Ok(())
2457    }
2458
2459    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2460    pub(crate) fn install_format_decl(
2461        &mut self,
2462        basename: &str,
2463        lines: &[String],
2464        line: usize,
2465    ) -> StrykeResult<()> {
2466        let pkg = self.current_package();
2467        let key = format!("{}::{}", pkg, basename);
2468        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2469        self.format_templates.insert(key, Arc::new(tmpl));
2470        Ok(())
2471    }
2472
2473    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2474    /// Anonymous overload handlers are emitted by the parser as a synthetic
2475    /// `__overload_anon_N` SubDecl at the top of the program (registered under
2476    /// `main::`); re-bind a clone under the current package so the dispatch
2477    /// `Pkg::__overload_anon_N` lookup at runtime resolves. (PARITY-012)
2478    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2479        let pkg = self.current_package();
2480        for (_, v) in pairs {
2481            if v.starts_with("__overload_anon_") {
2482                // Synthetic anon-overload subs are emitted at the top of the
2483                // program, before any user `package N` statement, so they're
2484                // registered under the bare name (qualify_sub_key returns the
2485                // unqualified form for the `main` package). Re-bind a clone
2486                // under `Pkg::name` so the dispatch lookup `Pkg::sub_short`
2487                // resolves.
2488                let pkg_key = format!("{}::{}", pkg, v);
2489                if !self.subs.contains_key(&pkg_key) {
2490                    let src = if let Some(s) = self.subs.get(v) {
2491                        Some(s.clone())
2492                    } else {
2493                        self.subs.get(&format!("main::{}", v)).cloned()
2494                    };
2495                    if let Some(sub) = src {
2496                        self.subs.insert(pkg_key, sub);
2497                    }
2498                }
2499            }
2500        }
2501        let ent = self.overload_table.entry(pkg).or_default();
2502        for (k, v) in pairs {
2503            ent.insert(k.clone(), v.clone());
2504        }
2505    }
2506
2507    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2508    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2509    pub(crate) fn local_declare_typeglob(
2510        &mut self,
2511        lhs: &str,
2512        rhs: Option<&str>,
2513        line: usize,
2514    ) -> StrykeResult<()> {
2515        let old = self.glob_handle_alias.remove(lhs);
2516        let Some(frame) = self.glob_restore_frames.last_mut() else {
2517            return Err(StrykeError::runtime(
2518                "internal: no glob restore frame for local *GLOB",
2519                line,
2520            ));
2521        };
2522        frame.push((lhs.to_string(), old));
2523        if let Some(r) = rhs {
2524            self.glob_handle_alias
2525                .insert(lhs.to_string(), r.to_string());
2526        }
2527        Ok(())
2528    }
2529
2530    pub(crate) fn scope_push_hook(&mut self) {
2531        self.scope.push_frame();
2532        self.glob_restore_frames.push(Vec::new());
2533        self.special_var_restore_frames.push(Vec::new());
2534        self.english_lexical_scalars.push(HashSet::new());
2535        self.our_lexical_scalars.push(HashSet::new());
2536        self.our_lexical_arrays.push(HashSet::new());
2537        self.our_lexical_hashes.push(HashSet::new());
2538        self.state_bindings_stack.push(Vec::new());
2539    }
2540
2541    #[inline]
2542    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2543        if let Some(s) = self.english_lexical_scalars.last_mut() {
2544            s.insert(name.to_string());
2545        }
2546    }
2547
2548    /// Snapshot the `english_lexical_scalars` stack for parallel worker spawn (rayon
2549    /// closures need owned `Vec<HashSet<String>>` they can `clone()` per-worker).
2550    #[inline]
2551    pub(crate) fn english_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2552        self.english_lexical_scalars.clone()
2553    }
2554
2555    /// Snapshot the `our_lexical_scalars` stack — companion to
2556    /// [`Self::english_lexical_scalars_clone`].
2557    #[inline]
2558    pub(crate) fn our_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2559        self.our_lexical_scalars.clone()
2560    }
2561
2562    /// Replace `english_lexical_scalars` wholesale (parallel-worker setup).
2563    #[inline]
2564    pub(crate) fn set_english_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2565        self.english_lexical_scalars = v;
2566    }
2567
2568    /// Replace `our_lexical_scalars` wholesale (parallel-worker setup).
2569    #[inline]
2570    pub(crate) fn set_our_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2571        self.our_lexical_scalars = v;
2572    }
2573
2574    #[inline]
2575    fn note_our_scalar(&mut self, bare_name: &str) {
2576        if let Some(s) = self.our_lexical_scalars.last_mut() {
2577            s.insert(bare_name.to_string());
2578        }
2579    }
2580
2581    #[inline]
2582    fn note_our_array(&mut self, bare_name: &str) {
2583        if let Some(s) = self.our_lexical_arrays.last_mut() {
2584            s.insert(bare_name.to_string());
2585        }
2586    }
2587
2588    #[inline]
2589    fn note_our_hash(&mut self, bare_name: &str) {
2590        if let Some(s) = self.our_lexical_hashes.last_mut() {
2591            s.insert(bare_name.to_string());
2592        }
2593    }
2594
2595    /// Package stash key for `our @arr` — mirrors [`Self::stash_scalar_name_for_package`].
2596    /// Used at the `our`-decl site to choose the storage key; bare-name reads go
2597    /// through [`Self::tree_array_storage_name`] which only qualifies when the
2598    /// surrounding scope actually has a matching `our` binding.
2599    pub(crate) fn stash_array_full_name_for_package(&self, name: &str) -> String {
2600        if name.contains("::") {
2601            return name.to_string();
2602        }
2603        let pkg = self.current_package();
2604        if pkg.is_empty() || pkg == "main" {
2605            format!("main::{}", name)
2606        } else {
2607            format!("{}::{}", pkg, name)
2608        }
2609    }
2610
2611    /// Package stash key for `our %h` — companion to
2612    /// [`Self::stash_array_full_name_for_package`].
2613    pub(crate) fn stash_hash_full_name_for_package(&self, name: &str) -> String {
2614        if name.contains("::") {
2615            return name.to_string();
2616        }
2617        let pkg = self.current_package();
2618        if pkg.is_empty() || pkg == "main" {
2619            format!("main::{}", name)
2620        } else {
2621            format!("{}::{}", pkg, name)
2622        }
2623    }
2624
2625    /// Bare `@x` after `our @x` reads the package stash array. Walk the
2626    /// lexical chain inside-out and qualify only when an enclosing scope
2627    /// marked the name as `our`.
2628    pub(crate) fn tree_array_storage_name(&self, name: &str) -> String {
2629        if name.contains("::") {
2630            return name.to_string();
2631        }
2632        for ours in self.our_lexical_arrays.iter().rev() {
2633            if ours.contains(name) {
2634                return self.stash_array_full_name_for_package(name);
2635            }
2636        }
2637        name.to_string()
2638    }
2639
2640    /// Bare `%h` after `our %h` reads the package stash hash. See
2641    /// [`Self::tree_array_storage_name`] for the resolution policy.
2642    pub(crate) fn tree_hash_storage_name(&self, name: &str) -> String {
2643        if name.contains("::") {
2644            return name.to_string();
2645        }
2646        for ours in self.our_lexical_hashes.iter().rev() {
2647            if ours.contains(name) {
2648                return self.stash_hash_full_name_for_package(name);
2649            }
2650        }
2651        name.to_string()
2652    }
2653
2654    /// Public wrapper for [`Self::english_note_lexical_scalar`] — used by bytecode
2655    /// `Op::DeclareOurSync*` to register bare names so worker `tree_scalar_storage_name`
2656    /// reads rewrite to `Pkg::x`.
2657    #[inline]
2658    pub(crate) fn english_note_lexical_scalar_pub(&mut self, name: &str) {
2659        self.english_note_lexical_scalar(name);
2660    }
2661
2662    /// Public wrapper for [`Self::note_our_scalar`] — see [`Self::english_note_lexical_scalar_pub`].
2663    #[inline]
2664    pub(crate) fn note_our_scalar_pub(&mut self, bare_name: &str) {
2665        self.note_our_scalar(bare_name);
2666    }
2667
2668    pub(crate) fn scope_pop_hook(&mut self) {
2669        if !self.scope.can_pop_frame() {
2670            return;
2671        }
2672        // Execute deferred blocks in LIFO order before popping the frame.
2673        // Important: defer blocks run in the CURRENT scope (not a new frame),
2674        // so they can modify variables in the enclosing scope.
2675        let defers = self.scope.take_defers();
2676        for coderef in defers {
2677            if let Some(sub) = coderef.as_code_ref() {
2678                // Execute the defer block body directly in the current scope,
2679                // without creating a new frame or restoring closure captures.
2680                // This allows defer { $x = 100 } to modify the outer $x.
2681                let saved_wa = self.wantarray_kind;
2682                self.wantarray_kind = WantarrayCtx::Void;
2683                let _ = self.exec_block_no_scope(&sub.body);
2684                self.wantarray_kind = saved_wa;
2685            }
2686        }
2687        // Save state variable values back before popping the frame
2688        if let Some(bindings) = self.state_bindings_stack.pop() {
2689            for (var_name, state_key) in &bindings {
2690                let val = self.scope.get_scalar(var_name).clone();
2691                self.state_vars.insert(state_key.clone(), val);
2692            }
2693        }
2694        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2695        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2696        if let Some(entries) = self.special_var_restore_frames.pop() {
2697            for (name, old) in entries.into_iter().rev() {
2698                let _ = self.set_special_var(&name, &old);
2699            }
2700        }
2701        if let Some(entries) = self.glob_restore_frames.pop() {
2702            for (name, old) in entries.into_iter().rev() {
2703                match old {
2704                    Some(s) => {
2705                        self.glob_handle_alias.insert(name, s);
2706                    }
2707                    None => {
2708                        self.glob_handle_alias.remove(&name);
2709                    }
2710                }
2711            }
2712        }
2713        self.scope.pop_frame();
2714        let _ = self.english_lexical_scalars.pop();
2715        let _ = self.our_lexical_scalars.pop();
2716        let _ = self.our_lexical_arrays.pop();
2717        let _ = self.our_lexical_hashes.pop();
2718    }
2719
2720    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2721    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2722    #[inline]
2723    pub(crate) fn enable_parallel_guard(&mut self) {
2724        self.scope.set_parallel_guard(true);
2725    }
2726
2727    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues after VM compilation.
2728    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2729        self.begin_blocks.clear();
2730        self.unit_check_blocks.clear();
2731        self.check_blocks.clear();
2732        self.init_blocks.clear();
2733        self.end_blocks.clear();
2734    }
2735
2736    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2737    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2738    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2739    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2740    /// there because it only calls [`Scope::pop_frame`].
2741    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2742        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2743            self.scope_pop_hook();
2744        }
2745    }
2746
2747    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2748    ///
2749    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2750    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2751    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2752    /// workers that call `perl_signal::poll`).
2753    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> StrykeResult<()> {
2754        self.touch_env_hash("SIG");
2755        let v = self.scope.get_hash_element("SIG", sig);
2756        if v.is_undef() {
2757            return Self::default_sig_action(sig);
2758        }
2759        if let Some(s) = v.as_str() {
2760            if s == "IGNORE" {
2761                return Ok(());
2762            }
2763            if s == "DEFAULT" {
2764                return Self::default_sig_action(sig);
2765            }
2766        }
2767        if let Some(sub) = v.as_code_ref() {
2768            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2769                Ok(_) => Ok(()),
2770                Err(FlowOrError::Flow(_)) => Ok(()),
2771                Err(FlowOrError::Error(e)) => Err(e),
2772            }
2773        } else {
2774            Self::default_sig_action(sig)
2775        }
2776    }
2777
2778    /// Dispatch `$SIG{__WARN__}` if a coderef is installed; fall back to stderr.
2779    /// Recursion is guarded by temporarily clearing the slot during dispatch so
2780    /// a `__WARN__` handler that itself calls `warn` does not loop.
2781    pub(crate) fn fire_pseudosig_warn(&mut self, msg: &str, line: usize) -> StrykeResult<()> {
2782        self.touch_env_hash("SIG");
2783        let slot = self.scope.get_hash_element("SIG", "__WARN__");
2784        if let Some(sub) = slot.as_code_ref() {
2785            let prev = slot;
2786            let _ = self
2787                .scope
2788                .set_hash_element("SIG", "__WARN__", StrykeValue::UNDEF);
2789            let arg = StrykeValue::string(msg.to_string());
2790            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2791            let _ = self.scope.set_hash_element("SIG", "__WARN__", prev);
2792            return match r {
2793                Ok(_) => Ok(()),
2794                Err(FlowOrError::Flow(_)) => Ok(()),
2795                Err(FlowOrError::Error(e)) => Err(e),
2796            };
2797        }
2798        eprint!("{}", msg);
2799        Ok(())
2800    }
2801
2802    /// Dispatch `$SIG{__DIE__}` if a coderef is installed. Perl semantics:
2803    /// the handler runs even when the die is going to be caught by an `eval`,
2804    /// and the die still propagates afterwards. If the handler itself dies,
2805    /// that error replaces the original. Recursion is guarded by temporarily
2806    /// clearing the slot during dispatch.
2807    pub(crate) fn fire_pseudosig_die(&mut self, msg: &str, line: usize) -> StrykeResult<()> {
2808        self.touch_env_hash("SIG");
2809        let slot = self.scope.get_hash_element("SIG", "__DIE__");
2810        if let Some(sub) = slot.as_code_ref() {
2811            let prev = slot;
2812            let _ = self
2813                .scope
2814                .set_hash_element("SIG", "__DIE__", StrykeValue::UNDEF);
2815            let arg = StrykeValue::string(msg.to_string());
2816            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2817            let _ = self.scope.set_hash_element("SIG", "__DIE__", prev);
2818            return match r {
2819                Ok(_) => Ok(()),
2820                Err(FlowOrError::Flow(_)) => Ok(()),
2821                Err(FlowOrError::Error(e)) => Err(e),
2822            };
2823        }
2824        Ok(())
2825    }
2826
2827    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2828    #[inline]
2829    fn default_sig_action(sig: &str) -> StrykeResult<()> {
2830        match sig {
2831            // 128 + signal number (common shell convention)
2832            "INT" => std::process::exit(130),
2833            "TERM" => std::process::exit(143),
2834            "ALRM" => std::process::exit(142),
2835            // Default for SIGCHLD is ignore
2836            "CHLD" => Ok(()),
2837            _ => Ok(()),
2838        }
2839    }
2840
2841    /// Notify the debugger that a user sub call is about to happen. No-op
2842    /// when no debugger is attached. Paired with [`Self::debugger_leave_sub`]
2843    /// after the call returns — the depth counter is what makes step-over
2844    /// skip past UDFs instead of stepping into them.
2845    #[inline]
2846    pub fn debugger_enter_sub(&mut self, name: &str) {
2847        if let Some(dbg) = &mut self.debugger {
2848            dbg.enter_sub(name);
2849        }
2850    }
2851
2852    /// Pair to [`Self::debugger_enter_sub`].
2853    #[inline]
2854    pub fn debugger_leave_sub(&mut self) {
2855        if let Some(dbg) = &mut self.debugger {
2856            dbg.leave_sub();
2857        }
2858    }
2859
2860    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2861    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2862    pub fn materialize_env_if_needed(&mut self) {
2863        if self.env_materialized {
2864            return;
2865        }
2866        self.env = std::env::vars()
2867            .map(|(k, v)| (k, StrykeValue::string(v)))
2868            .collect();
2869        self.scope
2870            .set_hash("ENV", self.env.clone())
2871            .expect("set %ENV");
2872        self.env_materialized = true;
2873    }
2874
2875    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2876    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2877        self.materialize_env_if_needed();
2878        if let Some(x) = self.log_level_override {
2879            return x;
2880        }
2881        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2882        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2883    }
2884
2885    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2886    pub(crate) fn no_color_effective(&mut self) -> bool {
2887        self.materialize_env_if_needed();
2888        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2889        if v.is_undef() {
2890            return false;
2891        }
2892        !v.to_string().is_empty()
2893    }
2894
2895    #[inline]
2896    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2897        // `%main::ENV` ≡ `%ENV`, `%main::parameters` ≡ `%parameters`,
2898        // `%main::a` ≡ `%a`, etc. Strip the `main::` qualifier so the
2899        // lazy-materialize / reflection-hash branches fire on the
2900        // canonical bare name. Without this, `exists $main::ENV{PATH}`
2901        // returns 0 on a fresh interpreter because ENV never gets
2902        // materialized.
2903        let hash_name: &str = crate::scope::strip_main_prefix(hash_name).unwrap_or(hash_name);
2904        if hash_name == "ENV" {
2905            self.materialize_env_if_needed();
2906        } else if hash_name == "parameters"
2907            && !crate::compat_mode()
2908            && !self.scope.has_lexical_hash("parameters")
2909        {
2910            // `%parameters` (zsh `$parameters` analogue) — rebuild on every
2911            // read so it always reflects current scope state. Frozen install,
2912            // so user code can read but not assign into it. Stryke-only;
2913            // `--compat` skips the auto-refresh so Perl 5 scripts that use
2914            // `%parameters` for their own purposes are unaffected.
2915            self.ensure_reflection_hashes();
2916            self.refresh_parameters_hash();
2917        } else if hash_name.ends_with("::") && hash_name.len() > 2 {
2918            // `%main::` / `%Foo::` — repopulate from current symbol table on
2919            // every read so newly-defined subs / `our` vars become visible
2920            // without an explicit `refresh_stashes()` call. Cheap: walks
2921            // `subs` keys + frame name lists, no value cloning.
2922            self.refresh_package_stashes();
2923        } else if !self.reflection_hashes_ready && !self.scope.has_lexical_hash(hash_name) {
2924            match hash_name {
2925                "b"
2926                | "pc"
2927                | "e"
2928                | "a"
2929                | "d"
2930                | "c"
2931                | "p"
2932                | "k"
2933                | "o"
2934                | "v"
2935                | "all"
2936                | "stryke::builtins"
2937                | "stryke::perl_compats"
2938                | "stryke::extensions"
2939                | "stryke::aliases"
2940                | "stryke::descriptions"
2941                | "stryke::categories"
2942                | "stryke::primaries"
2943                | "stryke::keywords"
2944                | "stryke::operators"
2945                | "stryke::special_vars"
2946                | "stryke::all" => {
2947                    self.ensure_reflection_hashes();
2948                }
2949                _ => {}
2950            }
2951        }
2952    }
2953
2954    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2955    pub(crate) fn exists_arrow_hash_element(
2956        &self,
2957        container: StrykeValue,
2958        key: &str,
2959        line: usize,
2960    ) -> StrykeResult<bool> {
2961        if let Some(r) = container.as_hash_ref() {
2962            return Ok(r.read().contains_key(key));
2963        }
2964        if let Some(b) = container.as_blessed_ref() {
2965            let data = b.data.read();
2966            if let Some(r) = data.as_hash_ref() {
2967                return Ok(r.read().contains_key(key));
2968            }
2969            if let Some(hm) = data.as_hash_map() {
2970                return Ok(hm.contains_key(key));
2971            }
2972            return Err(StrykeError::runtime(
2973                "exists argument is not a HASH reference",
2974                line,
2975            ));
2976        }
2977        // `exists $h{x}{y}` when `$h{x}` is undef OR a non-hash scalar: Perl
2978        // returns false for the deepest test without erroring. Stryke
2979        // previously errored on the intermediate. Match Perl. (BUG-009)
2980        let _ = line;
2981        Ok(false)
2982    }
2983
2984    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
2985    pub(crate) fn delete_arrow_hash_element(
2986        &self,
2987        container: StrykeValue,
2988        key: &str,
2989        line: usize,
2990    ) -> StrykeResult<StrykeValue> {
2991        if let Some(r) = container.as_hash_ref() {
2992            return Ok(r.write().shift_remove(key).unwrap_or(StrykeValue::UNDEF));
2993        }
2994        if let Some(b) = container.as_blessed_ref() {
2995            let mut data = b.data.write();
2996            if let Some(r) = data.as_hash_ref() {
2997                return Ok(r.write().shift_remove(key).unwrap_or(StrykeValue::UNDEF));
2998            }
2999            if let Some(mut map) = data.as_hash_map() {
3000                let v = map.shift_remove(key).unwrap_or(StrykeValue::UNDEF);
3001                *data = StrykeValue::hash(map);
3002                return Ok(v);
3003            }
3004            return Err(StrykeError::runtime(
3005                "delete argument is not a HASH reference",
3006                line,
3007            ));
3008        }
3009        Err(StrykeError::runtime(
3010            "delete argument is not a HASH reference",
3011            line,
3012        ))
3013    }
3014
3015    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
3016    pub(crate) fn exists_arrow_array_element(
3017        &self,
3018        container: StrykeValue,
3019        idx: i64,
3020        line: usize,
3021    ) -> StrykeResult<bool> {
3022        if let Some(a) = container.as_array_ref() {
3023            let arr = a.read();
3024            let i = if idx < 0 {
3025                (arr.len() as i64 + idx) as usize
3026            } else {
3027                idx as usize
3028            };
3029            return Ok(i < arr.len());
3030        }
3031        // `exists $a[5][0]` when `$a[5]` is missing OR a non-array scalar:
3032        // Perl returns false at the deepest test without erroring. (BUG-009)
3033        let _ = line;
3034        Ok(false)
3035    }
3036
3037    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
3038    pub(crate) fn delete_arrow_array_element(
3039        &self,
3040        container: StrykeValue,
3041        idx: i64,
3042        line: usize,
3043    ) -> StrykeResult<StrykeValue> {
3044        if let Some(a) = container.as_array_ref() {
3045            let mut arr = a.write();
3046            let i = if idx < 0 {
3047                (arr.len() as i64 + idx) as usize
3048            } else {
3049                idx as usize
3050            };
3051            if i >= arr.len() {
3052                return Ok(StrykeValue::UNDEF);
3053            }
3054            let old = arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
3055            arr[i] = StrykeValue::UNDEF;
3056            return Ok(old);
3057        }
3058        Err(StrykeError::runtime(
3059            "delete argument is not an ARRAY reference",
3060            line,
3061        ))
3062    }
3063
3064    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
3065    pub(crate) fn inc_directories(&self) -> Vec<String> {
3066        let mut v: Vec<String> = self
3067            .scope
3068            .get_array("INC")
3069            .into_iter()
3070            .map(|x| x.to_string())
3071            .filter(|s| !s.is_empty())
3072            .collect();
3073        if v.is_empty() {
3074            v.push(".".to_string());
3075        }
3076        v
3077    }
3078
3079    #[inline]
3080    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
3081        matches!(
3082            name,
3083            "_" | "0"
3084                | "!"
3085                | "@"
3086                | "/"
3087                | "\\"
3088                | ","
3089                | "."
3090                | "__PACKAGE__"
3091                | "$$"
3092                | "|"
3093                | "?"
3094                | "\""
3095                | "&"
3096                | "`"
3097                | "'"
3098                | "+"
3099                | "<"
3100                | ">"
3101                | "("
3102                | ")"
3103                | "]"
3104                | ";"
3105                | "ARGV"
3106                | "%"
3107                | "="
3108                | "-"
3109                | ":"
3110                | "*"
3111                | "INC"
3112                // sort/reduce comparator slots — predefined package globals
3113                // ($main::a, $main::b). Perl exempts them globally, not just
3114                // inside sort blocks, so any reference compiles cleanly.
3115                | "a"
3116                | "b"
3117        ) || name.chars().all(|c| c.is_ascii_digit())
3118            || name.starts_with('^')
3119            || (name.starts_with('#') && name.len() > 1)
3120            // Stryke implicit closure-param slots (`$_0`, `$_1`, …, `$_99`).
3121            // These are auto-bound inside any block that takes positional
3122            // arguments (sort comparators, reduce blocks, sub bodies, map/
3123            // grep blocks). Treat them like the digit-only match groups —
3124            // exempt globally so a strict-vars check inside a `preduce {
3125            // $_0 + $_1 }` block doesn't reject them as undeclared.
3126            || (name.starts_with('_')
3127                && name.len() > 1
3128                && name[1..].chars().all(|c| c.is_ascii_digit()))
3129    }
3130
3131    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3132        if !self.strict_vars
3133            || Self::strict_scalar_exempt(name)
3134            || name.contains("::")
3135            || self.scope.scalar_binding_exists(name)
3136        {
3137            return Ok(());
3138        }
3139        Err(StrykeError::runtime(
3140            format!(
3141                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
3142                name, name
3143            ),
3144            line,
3145        )
3146        .into())
3147    }
3148
3149    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3150        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
3151            return Ok(());
3152        }
3153        Err(StrykeError::runtime(
3154            format!(
3155                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
3156                name, name
3157            ),
3158            line,
3159        )
3160        .into())
3161    }
3162
3163    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3164        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
3165        if !self.strict_vars
3166            || name.contains("::")
3167            || self.scope.hash_binding_exists(name)
3168            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
3169        {
3170            return Ok(());
3171        }
3172        Err(StrykeError::runtime(
3173            format!(
3174                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
3175                name, name
3176            ),
3177            line,
3178        )
3179        .into())
3180    }
3181
3182    fn looks_like_version_only(spec: &str) -> bool {
3183        let t = spec.trim();
3184        !t.is_empty()
3185            && !t.contains('/')
3186            && !t.contains('\\')
3187            && !t.contains("::")
3188            && t.chars()
3189                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
3190            && t.chars().any(|c| c.is_ascii_digit())
3191    }
3192
3193    fn module_spec_to_relpath(spec: &str) -> String {
3194        let t = spec.trim();
3195        if t.contains("::") {
3196            format!("{}.pm", t.replace("::", "/"))
3197        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
3198            t.replace('\\', "/")
3199        } else {
3200            format!("{}.pm", t)
3201        }
3202    }
3203
3204    /// Lockfile-driven module resolution (RFC §"Module Resolution"). Walks up from
3205    /// `cwd` for `stryke.toml`, then asks [`crate::pkg::commands::resolve_module`]
3206    /// to find the module either in `lib/` or in the lockfile-pinned store. The
3207    /// `relpath` arg is the `@INC`-style path (`Foo/Bar.pm`) used elsewhere in
3208    /// `require`; it is converted to a logical name (`Foo::Bar`) for the resolver.
3209    /// Both `.pm` and `.stk` variants are tried — stryke source uses `.stk`.
3210    fn try_resolve_via_lockfile(relpath: &str) -> Option<std::path::PathBuf> {
3211        let cwd = std::env::current_dir().ok()?;
3212        let project_root = crate::pkg::commands::find_project_root(&cwd)?;
3213
3214        // Convert "Foo/Bar.pm" → "Foo::Bar". Drop the trailing extension so
3215        // `resolve_module` (which appends `.stk`) builds the right path.
3216        let stem = relpath
3217            .strip_suffix(".pm")
3218            .or_else(|| relpath.strip_suffix(".pl"))
3219            .or_else(|| relpath.strip_suffix(".stk"))
3220            .unwrap_or(relpath);
3221        let logical = stem.replace('/', "::");
3222
3223        crate::pkg::commands::resolve_module(&project_root, &logical).unwrap_or_default()
3224    }
3225
3226    /// `sub name` in `package P` → stash key `P::name`. `sub Q::name { }` is already fully
3227    /// qualified — do not prepend the current package. Unqualified names in `main` are stored
3228    /// **bare** (`name`), matching the compiler's `Op::Call` interning so the VM's
3229    /// `sub_for_closure_restore` lookup hits in one step.
3230    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
3231        if name.contains("::") {
3232            return name.to_string();
3233        }
3234        let pkg = self.current_package();
3235        if pkg.is_empty() || pkg == "main" {
3236            name.to_string()
3237        } else {
3238            format!("{}::{}", pkg, name)
3239        }
3240    }
3241
3242    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
3243    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
3244        let mut msg = format!("Undefined subroutine &{}", name);
3245        if self.strict_subs {
3246            msg.push_str(
3247                " (strict subs: declare the sub or use a fully qualified name before calling)",
3248            );
3249        }
3250        msg
3251    }
3252
3253    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
3254    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
3255        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
3256        if self.strict_subs {
3257            msg.push_str(
3258                " (strict subs: declare the sub or use a fully qualified name before calling)",
3259            );
3260        }
3261        msg
3262    }
3263
3264    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
3265    fn import_alias_key(&self, short: &str) -> String {
3266        self.qualify_sub_key(short)
3267    }
3268
3269    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
3270    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
3271        if imports.len() == 1 {
3272            match &imports[0].kind {
3273                ExprKind::QW(ws) => return ws.is_empty(),
3274                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
3275                ExprKind::List(xs) => return xs.is_empty(),
3276                _ => {}
3277            }
3278        }
3279        false
3280    }
3281
3282    /// After `require`, copy `Module::export` → caller stash per `use` list.
3283    fn apply_module_import(
3284        &mut self,
3285        module: &str,
3286        imports: &[Expr],
3287        line: usize,
3288    ) -> StrykeResult<()> {
3289        if imports.is_empty() {
3290            return self.import_all_from_module(module, line);
3291        }
3292        if Self::is_explicit_empty_import_list(imports) {
3293            return Ok(());
3294        }
3295        let names = Self::pragma_import_strings(imports, line)?;
3296        if names.is_empty() {
3297            return Ok(());
3298        }
3299        for name in names {
3300            self.import_one_symbol(module, &name, line)?;
3301        }
3302        Ok(())
3303    }
3304
3305    fn import_all_from_module(&mut self, module: &str, line: usize) -> StrykeResult<()> {
3306        if let Some(lists) = self.module_export_lists.get(module) {
3307            let export: Vec<String> = lists.export.clone();
3308            for short in export {
3309                self.import_named_sub(module, &short, line)?;
3310            }
3311            return Ok(());
3312        }
3313        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
3314        let prefix = format!("{}::", module);
3315        let keys: Vec<String> = self
3316            .subs
3317            .keys()
3318            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
3319            .cloned()
3320            .collect();
3321        for k in keys {
3322            let short = k[prefix.len()..].to_string();
3323            if let Some(sub) = self.subs.get(&k).cloned() {
3324                let alias = self.import_alias_key(&short);
3325                self.subs.insert(alias, sub);
3326            }
3327        }
3328        Ok(())
3329    }
3330
3331    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
3332    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> StrykeResult<()> {
3333        let qual = format!("{}::{}", module, short);
3334        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
3335            StrykeError::runtime(
3336                format!(
3337                    "`{}` is not defined in module `{}` (expected `{}`)",
3338                    short, module, qual
3339                ),
3340                line,
3341            )
3342        })?;
3343        let alias = self.import_alias_key(short);
3344        self.subs.insert(alias, sub);
3345        Ok(())
3346    }
3347
3348    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> StrykeResult<()> {
3349        if let Some(lists) = self.module_export_lists.get(module) {
3350            let allowed: HashSet<&str> = lists
3351                .export
3352                .iter()
3353                .map(|s| s.as_str())
3354                .chain(lists.export_ok.iter().map(|s| s.as_str()))
3355                .collect();
3356            if !allowed.contains(export) {
3357                return Err(StrykeError::runtime(
3358                    format!(
3359                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
3360                        export, module
3361                    ),
3362                    line,
3363                ));
3364            }
3365        }
3366        self.import_named_sub(module, export, line)
3367    }
3368
3369    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
3370    fn record_exporter_our_array_name(&mut self, name: &str, items: &[StrykeValue]) {
3371        if name != "EXPORT" && name != "EXPORT_OK" {
3372            return;
3373        }
3374        let pkg = self.current_package();
3375        if pkg.is_empty() || pkg == "main" {
3376            return;
3377        }
3378        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
3379        let ent = self.module_export_lists.entry(pkg).or_default();
3380        if name == "EXPORT" {
3381            ent.export = names;
3382        } else {
3383            ent.export_ok = names;
3384        }
3385    }
3386
3387    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
3388    /// Refresh [`StrykeSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
3389    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
3390    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
3391        let key = self.qualify_sub_key(name);
3392        let Some(sub) = self.subs.get(&key).cloned() else {
3393            return;
3394        };
3395        let captured = self.scope.capture();
3396        let closure_env = if captured.is_empty() {
3397            None
3398        } else {
3399            Some(captured)
3400        };
3401        let mut new_sub = (*sub).clone();
3402        new_sub.closure_env = closure_env;
3403        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
3404        self.subs.insert(key, Arc::new(new_sub));
3405    }
3406
3407    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<StrykeSub>> {
3408        if let Some(s) = self.subs.get(name) {
3409            return Some(s.clone());
3410        }
3411        if !name.contains("::") {
3412            // Non-`main` packages store subs at `Pkg::name`; resolve bare callers there.
3413            let pkg = self.current_package();
3414            if !pkg.is_empty() && pkg != "main" {
3415                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
3416                q.push_str(&pkg);
3417                q.push_str("::");
3418                q.push_str(name);
3419                return self.subs.get(&q).cloned();
3420            }
3421            return None;
3422        }
3423        // `\&main::greet` / `defined &main::greet`: subs in `main` are stored bare so the
3424        // compiler's `Op::Call("greet", ...)` and the runtime stash lookup share a key.
3425        // Strip the `main::` qualifier and try the bare form so explicit qualified callers
3426        // still resolve to the same sub.
3427        if let Some(rest) = name.strip_prefix("main::") {
3428            if !rest.contains("::") {
3429                return self.subs.get(rest).cloned();
3430            }
3431        }
3432        None
3433    }
3434
3435    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
3436    /// before calling `import`).
3437    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
3438        if let Some(first) = imports.first() {
3439            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
3440                return &imports[1..];
3441            }
3442        }
3443        imports
3444    }
3445
3446    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
3447    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> StrykeResult<Vec<String>> {
3448        let mut out = Vec::new();
3449        for e in imports {
3450            match &e.kind {
3451                ExprKind::String(s) => out.push(s.clone()),
3452                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
3453                ExprKind::Integer(n) => out.push(n.to_string()),
3454                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
3455                // a single interpolated variable.  Reconstruct the sigil+name form.
3456                ExprKind::InterpolatedString(parts) => {
3457                    let mut s = String::new();
3458                    for p in parts {
3459                        match p {
3460                            StringPart::Literal(l) => s.push_str(l),
3461                            StringPart::ScalarVar(v) => {
3462                                s.push('$');
3463                                s.push_str(v);
3464                            }
3465                            StringPart::ArrayVar(v) => {
3466                                s.push('@');
3467                                s.push_str(v);
3468                            }
3469                            _ => {
3470                                return Err(StrykeError::runtime(
3471                                    "pragma import must be a compile-time string, qw(), or integer",
3472                                    e.line.max(default_line),
3473                                ));
3474                            }
3475                        }
3476                    }
3477                    out.push(s);
3478                }
3479                _ => {
3480                    return Err(StrykeError::runtime(
3481                        "pragma import must be a compile-time string, qw(), or integer",
3482                        e.line.max(default_line),
3483                    ));
3484                }
3485            }
3486        }
3487        Ok(out)
3488    }
3489
3490    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3491        if imports.is_empty() {
3492            self.strict_refs = true;
3493            self.strict_subs = true;
3494            self.strict_vars = true;
3495            return Ok(());
3496        }
3497        let names = Self::pragma_import_strings(imports, line)?;
3498        for name in names {
3499            match name.as_str() {
3500                "refs" => self.strict_refs = true,
3501                "subs" => self.strict_subs = true,
3502                "vars" => self.strict_vars = true,
3503                _ => {
3504                    return Err(StrykeError::runtime(
3505                        format!("Unknown strict mode `{}`", name),
3506                        line,
3507                    ));
3508                }
3509            }
3510        }
3511        Ok(())
3512    }
3513
3514    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3515        if imports.is_empty() {
3516            self.strict_refs = false;
3517            self.strict_subs = false;
3518            self.strict_vars = false;
3519            return Ok(());
3520        }
3521        let names = Self::pragma_import_strings(imports, line)?;
3522        for name in names {
3523            match name.as_str() {
3524                "refs" => self.strict_refs = false,
3525                "subs" => self.strict_subs = false,
3526                "vars" => self.strict_vars = false,
3527                _ => {
3528                    return Err(StrykeError::runtime(
3529                        format!("Unknown strict mode `{}`", name),
3530                        line,
3531                    ));
3532                }
3533            }
3534        }
3535        Ok(())
3536    }
3537
3538    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3539        let items = Self::pragma_import_strings(imports, line)?;
3540        if items.is_empty() {
3541            return Err(StrykeError::runtime(
3542                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
3543                line,
3544            ));
3545        }
3546        for item in items {
3547            let s = item.trim();
3548            if let Some(rest) = s.strip_prefix(':') {
3549                self.apply_feature_bundle(rest, line)?;
3550            } else {
3551                self.apply_feature_name(s, true, line)?;
3552            }
3553        }
3554        Ok(())
3555    }
3556
3557    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3558        if imports.is_empty() {
3559            self.feature_bits = 0;
3560            return Ok(());
3561        }
3562        let items = Self::pragma_import_strings(imports, line)?;
3563        for item in items {
3564            let s = item.trim();
3565            if let Some(rest) = s.strip_prefix(':') {
3566                self.clear_feature_bundle(rest);
3567            } else {
3568                self.apply_feature_name(s, false, line)?;
3569            }
3570        }
3571        Ok(())
3572    }
3573
3574    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> StrykeResult<()> {
3575        let key = v.trim();
3576        match key {
3577            "5.10" | "5.010" | "5.10.0" => {
3578                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3579            }
3580            "5.12" | "5.012" | "5.12.0" => {
3581                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3582            }
3583            _ => {
3584                return Err(StrykeError::runtime(
3585                    format!("unsupported feature bundle :{}", key),
3586                    line,
3587                ));
3588            }
3589        }
3590        Ok(())
3591    }
3592
3593    fn clear_feature_bundle(&mut self, v: &str) {
3594        let key = v.trim();
3595        if matches!(
3596            key,
3597            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
3598        ) {
3599            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
3600        }
3601    }
3602
3603    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> StrykeResult<()> {
3604        let bit = match name {
3605            "say" => FEAT_SAY,
3606            "state" => FEAT_STATE,
3607            "switch" => FEAT_SWITCH,
3608            "unicode_strings" => FEAT_UNICODE_STRINGS,
3609            // Features that stryke accepts as known but tracks no separate bit for —
3610            // either always-on, always-off, or syntactic sugar already enabled.
3611            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
3612            "postderef"
3613            | "postderef_qq"
3614            | "evalbytes"
3615            | "current_sub"
3616            | "fc"
3617            | "lexical_subs"
3618            | "signatures"
3619            | "refaliasing"
3620            | "bitwise"
3621            | "isa"
3622            | "indirect"
3623            | "multidimensional"
3624            | "bareword_filehandles"
3625            | "try"
3626            | "defer"
3627            | "extra_paired_delimiters"
3628            | "module_true"
3629            | "class"
3630            | "array_base" => return Ok(()),
3631            _ => {
3632                return Err(StrykeError::runtime(
3633                    format!("unknown feature `{}`", name),
3634                    line,
3635                ));
3636            }
3637        };
3638        if enable {
3639            self.feature_bits |= bit;
3640        } else {
3641            self.feature_bits &= !bit;
3642        }
3643        Ok(())
3644    }
3645
3646    /// `require EXPR` — load once, record `%INC`, return `1` on success.
3647    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> StrykeResult<StrykeValue> {
3648        let t = spec.trim();
3649        if t.is_empty() {
3650            return Err(StrykeError::runtime("require: empty argument", line));
3651        }
3652        match t {
3653            "strict" => {
3654                self.apply_use_strict(&[], line)?;
3655                return Ok(StrykeValue::integer(1));
3656            }
3657            "utf8" => {
3658                self.utf8_pragma = true;
3659                return Ok(StrykeValue::integer(1));
3660            }
3661            "feature" | "v5" => {
3662                return Ok(StrykeValue::integer(1));
3663            }
3664            "warnings" => {
3665                self.warnings = true;
3666                return Ok(StrykeValue::integer(1));
3667            }
3668            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
3669                return Ok(StrykeValue::integer(1));
3670            }
3671            _ => {}
3672        }
3673        let p = Path::new(t);
3674        if p.is_absolute() {
3675            return self.require_absolute_path(p, line);
3676        }
3677        if t.starts_with("./") || t.starts_with("../") {
3678            return self.require_relative_path(p, line);
3679        }
3680        if Self::looks_like_version_only(t) {
3681            return Ok(StrykeValue::integer(1));
3682        }
3683        let relpath = Self::module_spec_to_relpath(t);
3684        self.require_from_inc(&relpath, line)
3685    }
3686
3687    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
3688    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> StrykeResult<()> {
3689        let v = self.scope.get_hash_element("^HOOK", key);
3690        if v.is_undef() {
3691            return Ok(());
3692        }
3693        let Some(sub) = v.as_code_ref() else {
3694            return Ok(());
3695        };
3696        let r = self.call_sub(
3697            sub.as_ref(),
3698            vec![StrykeValue::string(path.to_string())],
3699            WantarrayCtx::Scalar,
3700            line,
3701        );
3702        match r {
3703            Ok(_) => Ok(()),
3704            Err(FlowOrError::Error(e)) => Err(e),
3705            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
3706            Err(FlowOrError::Flow(other)) => Err(StrykeError::runtime(
3707                format!(
3708                    "require hook {:?} returned unexpected control flow: {:?}",
3709                    key, other
3710                ),
3711                line,
3712            )),
3713        }
3714    }
3715
3716    fn require_absolute_path(&mut self, path: &Path, line: usize) -> StrykeResult<StrykeValue> {
3717        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
3718        let key = canon.to_string_lossy().into_owned();
3719        if self.scope.exists_hash_element("INC", &key) {
3720            return Ok(StrykeValue::integer(1));
3721        }
3722        self.invoke_require_hook("require__before", &key, line)?;
3723        let code = read_file_text_perl_compat(&canon).map_err(|e| {
3724            StrykeError::runtime(
3725                format!("Can't open {} for reading: {}", canon.display(), e),
3726                line,
3727            )
3728        })?;
3729        let code = crate::data_section::strip_perl_end_marker(&code);
3730        self.scope
3731            .set_hash_element("INC", &key, StrykeValue::string(key.clone()))?;
3732        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3733        let r = crate::parse_and_run_module_in_file(code, self, &key);
3734        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3735        r?;
3736        self.invoke_require_hook("require__after", &key, line)?;
3737        Ok(StrykeValue::integer(1))
3738    }
3739
3740    fn require_relative_path(&mut self, path: &Path, line: usize) -> StrykeResult<StrykeValue> {
3741        // First try the path verbatim (cwd-relative — backward-compat
3742        // for `stryke ./Foo.stk` where the user has chdir'd into the
3743        // exercise dir).
3744        if path.exists() {
3745            return self.require_absolute_path(path, line);
3746        }
3747        // Fall back to resolving against the directory of the calling
3748        // script (`self.file`). This makes `require "./Foo.stk"` work
3749        // when the test is invoked via absolute path:
3750        //   `stryke /abs/path/to/proj/t/test_foo.stk` ← cwd unrelated
3751        //   `require "./Foo.stk"` resolves to `/abs/path/to/proj/Foo.stk`
3752        //
3753        // Walks up from `self.file`'s parent dir trying each ancestor —
3754        // covers both same-dir requires AND the common `t/` test-subdir
3755        // pattern where the test file is in `t/` and the source is one
3756        // level up.
3757        if !self.file.is_empty() {
3758            let caller = Path::new(&self.file);
3759            let mut anchor = caller.parent();
3760            while let Some(dir) = anchor {
3761                let candidate = dir.join(path);
3762                if candidate.exists() {
3763                    return self.require_absolute_path(&candidate, line);
3764                }
3765                anchor = dir.parent();
3766                // Stop once we hit the filesystem root to avoid
3767                // unbounded ascent. `parent()` on `/` returns None.
3768                if anchor.map(|p| p.as_os_str().is_empty()).unwrap_or(false) {
3769                    break;
3770                }
3771            }
3772        }
3773        Err(StrykeError::runtime(
3774            format!(
3775                "Can't locate {} (relative path does not exist)",
3776                path.display()
3777            ),
3778            line,
3779        ))
3780    }
3781
3782    fn require_from_inc(&mut self, relpath: &str, line: usize) -> StrykeResult<StrykeValue> {
3783        if self.scope.exists_hash_element("INC", relpath) {
3784            return Ok(StrykeValue::integer(1));
3785        }
3786        self.invoke_require_hook("require__before", relpath, line)?;
3787
3788        // Lockfile-driven module resolution. When the cwd is inside a stryke
3789        // project (`stryke.toml` reachable), `use Foo::Bar` first looks at
3790        // `lib/Foo/Bar.stk` and then at lockfile-pinned store entries before
3791        // falling through to `@INC`. See docs/PACKAGE_REGISTRY.md §"Module
3792        // Resolution".
3793        if let Some(found) = Self::try_resolve_via_lockfile(relpath) {
3794            let code = read_file_text_perl_compat(&found).map_err(|e| {
3795                StrykeError::runtime(
3796                    format!("Can't open {} for reading: {}", found.display(), e),
3797                    line,
3798                )
3799            })?;
3800            let code = crate::data_section::strip_perl_end_marker(&code);
3801            let abs = found.canonicalize().unwrap_or(found);
3802            let abs_s = abs.to_string_lossy().into_owned();
3803            self.scope
3804                .set_hash_element("INC", relpath, StrykeValue::string(abs_s.clone()))?;
3805            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3806            let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3807            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3808            r?;
3809            self.invoke_require_hook("require__after", relpath, line)?;
3810            return Ok(StrykeValue::integer(1));
3811        }
3812
3813        // Check virtual modules first (AOT bundles).
3814        if let Some(code) = self.virtual_modules.get(relpath).cloned() {
3815            let code = crate::data_section::strip_perl_end_marker(&code);
3816            self.scope.set_hash_element(
3817                "INC",
3818                relpath,
3819                StrykeValue::string(format!("(virtual)/{}", relpath)),
3820            )?;
3821            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3822            let r = crate::parse_and_run_module_in_file(code, self, relpath);
3823            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3824            r?;
3825            self.invoke_require_hook("require__after", relpath, line)?;
3826            return Ok(StrykeValue::integer(1));
3827        }
3828
3829        for dir in self.inc_directories() {
3830            let full = Path::new(&dir).join(relpath);
3831            if full.is_file() {
3832                let code = read_file_text_perl_compat(&full).map_err(|e| {
3833                    StrykeError::runtime(
3834                        format!("Can't open {} for reading: {}", full.display(), e),
3835                        line,
3836                    )
3837                })?;
3838                let code = crate::data_section::strip_perl_end_marker(&code);
3839                let abs = full.canonicalize().unwrap_or(full);
3840                let abs_s = abs.to_string_lossy().into_owned();
3841                self.scope
3842                    .set_hash_element("INC", relpath, StrykeValue::string(abs_s.clone()))?;
3843                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3844                let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3845                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3846                r?;
3847                self.invoke_require_hook("require__after", relpath, line)?;
3848                return Ok(StrykeValue::integer(1));
3849            }
3850        }
3851        Err(StrykeError::runtime(
3852            format!(
3853                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
3854                relpath
3855            ),
3856            line,
3857        ))
3858    }
3859
3860    /// Register a virtual module (for AOT bundles). Path should be relative like "lib/foo.stk".
3861    pub fn register_virtual_module(&mut self, path: String, source: String) {
3862        self.virtual_modules.insert(path, source);
3863    }
3864
3865    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
3866    pub(crate) fn exec_use_stmt(
3867        &mut self,
3868        module: &str,
3869        imports: &[Expr],
3870        line: usize,
3871    ) -> StrykeResult<()> {
3872        match module {
3873            "strict" => self.apply_use_strict(imports, line),
3874            "utf8" => {
3875                if !imports.is_empty() {
3876                    return Err(StrykeError::runtime("use utf8 takes no arguments", line));
3877                }
3878                self.utf8_pragma = true;
3879                Ok(())
3880            }
3881            "feature" => self.apply_use_feature(imports, line),
3882            "v5" => Ok(()),
3883            "warnings" => {
3884                self.warnings = true;
3885                Ok(())
3886            }
3887            "English" => {
3888                self.english_enabled = true;
3889                let args = Self::pragma_import_strings(imports, line)?;
3890                let no_match = args.iter().any(|a| a == "-no_match_vars");
3891                // Once match vars are exported (use English without -no_match_vars),
3892                // they stay available for the rest of the program — Perl exports them
3893                // into the caller's namespace and later pragmas cannot un-export them.
3894                if !no_match {
3895                    self.english_match_vars_ever_enabled = true;
3896                }
3897                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
3898                Ok(())
3899            }
3900            "Env" => self.apply_use_env(imports, line),
3901            "open" => self.apply_use_open(imports, line),
3902            "constant" => self.apply_use_constant(imports, line),
3903            "bigint" | "bignum" | "bigrat" => {
3904                // Activate BigInt promotion for `**` (and any other op
3905                // that consults the bigint pragma). `bignum` and
3906                // `bigrat` are routed here too — stryke doesn't yet
3907                // distinguish them from `bigint` for arithmetic, but
3908                // accepting them prevents the default-load path from
3909                // searching @INC for a CPAN module that won't parse.
3910                crate::set_bigint_pragma(true);
3911                Ok(())
3912            }
3913            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3914            _ => {
3915                self.require_execute(module, line)?;
3916                let imports = Self::imports_after_leading_use_version(imports);
3917                self.apply_module_import(module, imports, line)?;
3918                Ok(())
3919            }
3920        }
3921    }
3922
3923    /// `no strict 'refs'`, `no warnings`, `no feature`, …
3924    pub(crate) fn exec_no_stmt(
3925        &mut self,
3926        module: &str,
3927        imports: &[Expr],
3928        line: usize,
3929    ) -> StrykeResult<()> {
3930        match module {
3931            "strict" => self.apply_no_strict(imports, line),
3932            "utf8" => {
3933                if !imports.is_empty() {
3934                    return Err(StrykeError::runtime("no utf8 takes no arguments", line));
3935                }
3936                self.utf8_pragma = false;
3937                Ok(())
3938            }
3939            "feature" => self.apply_no_feature(imports, line),
3940            "v5" => Ok(()),
3941            "warnings" => {
3942                self.warnings = false;
3943                Ok(())
3944            }
3945            "English" => {
3946                self.english_enabled = false;
3947                // Don't reset no_match_vars here — if match vars were ever enabled,
3948                // they persist (Perl's export cannot be un-exported).
3949                if !self.english_match_vars_ever_enabled {
3950                    self.english_no_match_vars = false;
3951                }
3952                Ok(())
3953            }
3954            "open" => {
3955                self.open_pragma_utf8 = false;
3956                Ok(())
3957            }
3958            "bigint" | "bignum" | "bigrat" => {
3959                crate::set_bigint_pragma(false);
3960                Ok(())
3961            }
3962            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3963            _ => Ok(()),
3964        }
3965    }
3966
3967    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
3968    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3969        let names = Self::pragma_import_strings(imports, line)?;
3970        for n in names {
3971            let key = n.trim_start_matches('@');
3972            if key.eq_ignore_ascii_case("PATH") {
3973                let path_env = std::env::var("PATH").unwrap_or_default();
3974                let path_vec: Vec<StrykeValue> = std::env::split_paths(&path_env)
3975                    .map(|p| StrykeValue::string(p.to_string_lossy().into_owned()))
3976                    .collect();
3977                let aname = self.stash_array_name_for_package("PATH");
3978                self.scope.declare_array(&aname, path_vec);
3979            }
3980        }
3981        Ok(())
3982    }
3983
3984    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
3985    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3986        let items = Self::pragma_import_strings(imports, line)?;
3987        for item in items {
3988            let s = item.trim();
3989            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
3990                self.open_pragma_utf8 = true;
3991                continue;
3992            }
3993            if let Some(rest) = s.strip_prefix(":encoding(") {
3994                if let Some(inner) = rest.strip_suffix(')') {
3995                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
3996                        self.open_pragma_utf8 = true;
3997                    }
3998                }
3999            }
4000        }
4001        Ok(())
4002    }
4003
4004    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
4005    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
4006        if imports.is_empty() {
4007            return Ok(());
4008        }
4009        // `use constant 1.03;` — version check only (ignored here).
4010        if imports.len() == 1 {
4011            match &imports[0].kind {
4012                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
4013                _ => {}
4014            }
4015        }
4016        for imp in imports {
4017            match &imp.kind {
4018                ExprKind::List(items) => {
4019                    if items.len() % 2 != 0 {
4020                        return Err(StrykeError::runtime(
4021                            format!(
4022                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
4023                                items.len()
4024                            ),
4025                            line,
4026                        ));
4027                    }
4028                    let mut i = 0;
4029                    while i < items.len() {
4030                        let name = self.use_constant_name_from_expr(&items[i], line)?;
4031                        self.install_constant_from_expr(&name, &items[i + 1], line)?;
4032                        i += 2;
4033                    }
4034                }
4035                // `use constant { A => 1, B => 2 }` — hashref-block form
4036                // (BUG-086). Each `(key_expr, value_expr)` pair installs a
4037                // constant subroutine whose body returns the value expression
4038                // verbatim, so list-valued constants like
4039                // `MULTI => [1, 2, 3]` retain their original shape.
4040                ExprKind::HashRef(pairs) => {
4041                    for (key_expr, value_expr) in pairs {
4042                        let name = self.use_constant_name_from_expr(key_expr, line)?;
4043                        self.install_constant_from_expr(&name, value_expr, line)?;
4044                    }
4045                }
4046                _ => {
4047                    return Err(StrykeError::runtime(
4048                        "use constant: expected list of NAME => VALUE pairs",
4049                        line,
4050                    ));
4051                }
4052            }
4053        }
4054        Ok(())
4055    }
4056
4057    /// Pull the constant's name out of the `NAME =>` slot. The parser
4058    /// usually delivers it as `String(...)` via fat-arrow auto-quoting,
4059    /// but bare-identifier keys in a hashref-block come through as
4060    /// `Bareword(...)` too.
4061    fn use_constant_name_from_expr(&self, e: &Expr, line: usize) -> StrykeResult<String> {
4062        match &e.kind {
4063            ExprKind::String(s) => Ok(s.clone()),
4064            ExprKind::Bareword(s) => Ok(s.clone()),
4065            _ => Err(StrykeError::runtime(
4066                "use constant: constant name must be a string literal",
4067                line,
4068            )),
4069        }
4070    }
4071
4072    /// Install a constant subroutine from the raw value expression.
4073    ///
4074    /// For multi-value parenthesized lists (`(1, 2, 3)`) the body keeps the
4075    /// `List` shape so callers see the full list — fixing BUG-086 where
4076    /// the value was eagerly evaluated and collapsed to its last comma
4077    /// operand via scalar coercion. For every other shape we keep the
4078    /// original eval-once-and-freeze semantics so non-pure initializers
4079    /// like `use constant START_TIME => time()` only run once.
4080    fn install_constant_from_expr(
4081        &mut self,
4082        name: &str,
4083        value: &Expr,
4084        line: usize,
4085    ) -> StrykeResult<()> {
4086        // Multi-value list form: install the `List` AST directly so the
4087        // constant returns the full list (not just the last comma operand).
4088        // The body is a bare `Expression(...)` statement — implicit return
4089        // hands the value back in the caller's wantarray context, the same
4090        // way `fn arr = (1, 2, 3)` does. Wrapping in a `Return(...)`
4091        // statement instead would collapse to scalar context for
4092        // bareword-call sites like `my @a = ARR`.
4093        if matches!(value.kind, ExprKind::List(_)) {
4094            let key = self.qualify_sub_key(name);
4095            let body = vec![Statement {
4096                label: None,
4097                kind: StmtKind::Expression(value.clone()),
4098                line,
4099            }];
4100            self.subs.insert(
4101                key.clone(),
4102                Arc::new(StrykeSub {
4103                    name: key,
4104                    params: vec![],
4105                    body,
4106                    prototype: None,
4107                    closure_env: None,
4108                    fib_like: None,
4109                }),
4110            );
4111            return Ok(());
4112        }
4113        // Scalar / arrayref / hashref / etc.: eval once and freeze, the
4114        // same path single-pair `use constant` has used since day one.
4115        let val = match self.eval_expr(value) {
4116            Ok(v) => v,
4117            Err(FlowOrError::Error(e)) => return Err(e),
4118            Err(FlowOrError::Flow(_)) => {
4119                return Err(StrykeError::runtime(
4120                    "use constant: unexpected control flow in initializer",
4121                    line,
4122                ));
4123            }
4124        };
4125        self.install_constant_sub(name, &val, line)
4126    }
4127
4128    fn install_constant_sub(
4129        &mut self,
4130        name: &str,
4131        val: &StrykeValue,
4132        line: usize,
4133    ) -> StrykeResult<()> {
4134        let key = self.qualify_sub_key(name);
4135        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
4136        let body = vec![Statement {
4137            label: None,
4138            kind: StmtKind::Return(Some(ret_expr)),
4139            line,
4140        }];
4141        self.subs.insert(
4142            key.clone(),
4143            Arc::new(StrykeSub {
4144                name: key,
4145                params: vec![],
4146                body,
4147                prototype: None,
4148                closure_env: None,
4149                fib_like: None,
4150            }),
4151        );
4152        Ok(())
4153    }
4154
4155    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
4156    fn perl_value_to_const_literal_expr(&self, v: &StrykeValue, line: usize) -> StrykeResult<Expr> {
4157        if v.is_undef() {
4158            return Ok(Expr {
4159                kind: ExprKind::Undef,
4160                line,
4161            });
4162        }
4163        if let Some(n) = v.as_integer() {
4164            return Ok(Expr {
4165                kind: ExprKind::Integer(n),
4166                line,
4167            });
4168        }
4169        if let Some(f) = v.as_float() {
4170            return Ok(Expr {
4171                kind: ExprKind::Float(f),
4172                line,
4173            });
4174        }
4175        if let Some(s) = v.as_str() {
4176            return Ok(Expr {
4177                kind: ExprKind::String(s),
4178                line,
4179            });
4180        }
4181        if let Some(arr) = v.as_array_vec() {
4182            let mut elems = Vec::with_capacity(arr.len());
4183            for e in &arr {
4184                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
4185            }
4186            return Ok(Expr {
4187                kind: ExprKind::ArrayRef(elems),
4188                line,
4189            });
4190        }
4191        if let Some(h) = v.as_hash_map() {
4192            let mut pairs = Vec::with_capacity(h.len());
4193            for (k, vv) in h.iter() {
4194                pairs.push((
4195                    Expr {
4196                        kind: ExprKind::String(k.clone()),
4197                        line,
4198                    },
4199                    self.perl_value_to_const_literal_expr(vv, line)?,
4200                ));
4201            }
4202            return Ok(Expr {
4203                kind: ExprKind::HashRef(pairs),
4204                line,
4205            });
4206        }
4207        if let Some(aref) = v.as_array_ref() {
4208            let arr = aref.read();
4209            let mut elems = Vec::with_capacity(arr.len());
4210            for e in arr.iter() {
4211                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
4212            }
4213            return Ok(Expr {
4214                kind: ExprKind::ArrayRef(elems),
4215                line,
4216            });
4217        }
4218        if let Some(href) = v.as_hash_ref() {
4219            let h = href.read();
4220            let mut pairs = Vec::with_capacity(h.len());
4221            for (k, vv) in h.iter() {
4222                pairs.push((
4223                    Expr {
4224                        kind: ExprKind::String(k.clone()),
4225                        line,
4226                    },
4227                    self.perl_value_to_const_literal_expr(vv, line)?,
4228                ));
4229            }
4230            return Ok(Expr {
4231                kind: ExprKind::HashRef(pairs),
4232                line,
4233            });
4234        }
4235        Err(StrykeError::runtime(
4236            format!("use constant: unsupported value type ({v:?})"),
4237            line,
4238        ))
4239    }
4240
4241    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
4242    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> StrykeResult<()> {
4243        // Reset per-interpreter pragma flags. Each new program scan starts
4244        // clean; pragmas activate only when the program contains `use utf8;`
4245        // / `use bigint;` etc. (Globals like `BIGINT_PRAGMA` stay sticky
4246        // across runs in the same process — bigint tests that need
4247        // isolation use subprocess invocation.)
4248        self.utf8_pragma = false;
4249        for stmt in &program.statements {
4250            match &stmt.kind {
4251                StmtKind::Package { name } => {
4252                    let _ = self
4253                        .scope
4254                        .set_scalar("__PACKAGE__", StrykeValue::string(name.clone()));
4255                }
4256                StmtKind::SubDecl {
4257                    name,
4258                    params,
4259                    body,
4260                    prototype,
4261                } => {
4262                    let key = self.qualify_sub_key(name);
4263                    let mut sub = StrykeSub {
4264                        name: name.clone(),
4265                        params: params.clone(),
4266                        body: body.clone(),
4267                        closure_env: None,
4268                        prototype: prototype.clone(),
4269                        fib_like: None,
4270                    };
4271                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
4272                    self.subs.insert(key, Arc::new(sub));
4273                }
4274                StmtKind::UsePerlVersion { .. } => {}
4275                StmtKind::Use { module, imports } => {
4276                    self.exec_use_stmt(module, imports, stmt.line)?;
4277                }
4278                StmtKind::UseOverload { pairs } => {
4279                    self.install_use_overload_pairs(pairs);
4280                }
4281                StmtKind::FormatDecl { name, lines } => {
4282                    self.install_format_decl(name, lines, stmt.line)?;
4283                }
4284                StmtKind::No { module, imports } => {
4285                    self.exec_no_stmt(module, imports, stmt.line)?;
4286                }
4287                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
4288                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
4289                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
4290                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
4291                StmtKind::End(block) => self.end_blocks.push(block.clone()),
4292                _ => {}
4293            }
4294        }
4295        Ok(())
4296    }
4297
4298    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
4299    pub fn install_data_handle(&mut self, data: Vec<u8>) {
4300        self.input_handles.insert(
4301            "DATA".to_string(),
4302            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
4303        );
4304    }
4305
4306    /// Resolve `path` against [`Self::stryke_pwd`] when relative; absolute paths unchanged.
4307    #[inline]
4308    pub(crate) fn resolve_stryke_path(&self, path: &str) -> PathBuf {
4309        if path.is_empty() {
4310            return self.stryke_pwd.clone();
4311        }
4312        let p = Path::new(path);
4313        if p.is_absolute() {
4314            PathBuf::from(path)
4315        } else {
4316            self.stryke_pwd.join(path)
4317        }
4318    }
4319
4320    pub(crate) fn resolve_stryke_path_string(&self, path: &str) -> String {
4321        self.resolve_stryke_path(path)
4322            .to_string_lossy()
4323            .into_owned()
4324    }
4325
4326    /// `cd DIR` / `cd()` — set the interpreter working directory for relative path builtins
4327    /// (does not call OS `chdir`; use `chdir` for that). Returns `1` on success, `0` on failure
4328    /// and sets `$!` / errno. With no arguments, changes to `$HOME` / `%USERPROFILE%` when set.
4329    pub(crate) fn builtin_cd_execute(
4330        &mut self,
4331        args: &[StrykeValue],
4332        _line: usize,
4333    ) -> StrykeResult<StrykeValue> {
4334        let dest: PathBuf = if args.is_empty() {
4335            let home = std::env::var_os("HOME")
4336                .or_else(|| std::env::var_os("USERPROFILE"))
4337                .map(PathBuf::from);
4338            let Some(h) = home else {
4339                return Ok(StrykeValue::integer(0));
4340            };
4341            h
4342        } else {
4343            let raw = args[0].to_string();
4344            if raw.is_empty() {
4345                return Ok(StrykeValue::integer(0));
4346            }
4347            self.resolve_stryke_path(&raw)
4348        };
4349        match std::fs::metadata(&dest) {
4350            Ok(m) if m.is_dir() => {
4351                self.stryke_pwd = std::fs::canonicalize(&dest).unwrap_or(dest);
4352                Ok(StrykeValue::integer(1))
4353            }
4354            Ok(_) => Ok(StrykeValue::integer(0)),
4355            Err(e) => {
4356                self.apply_io_error_to_errno(&e);
4357                Ok(StrykeValue::integer(0))
4358            }
4359        }
4360    }
4361
4362    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
4363    ///
4364    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
4365    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
4366    /// [`piped_shell_command`]).
4367    pub(crate) fn open_builtin_execute(
4368        &mut self,
4369        handle_name: String,
4370        mode_s: String,
4371        file_opt: Option<String>,
4372        line: usize,
4373    ) -> StrykeResult<StrykeValue> {
4374        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
4375        // - leading `|`  → pipe to command (write to child's stdin)
4376        // - trailing `|` → pipe from command (read child's stdout)
4377        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
4378        let (actual_mode, path) = if let Some(f) = file_opt {
4379            (mode_s, f)
4380        } else {
4381            let trimmed = mode_s.trim();
4382            if let Some(rest) = trimmed.strip_prefix('|') {
4383                ("|-".to_string(), rest.trim_start().to_string())
4384            } else if trimmed.ends_with('|') {
4385                let mut cmd = trimmed.to_string();
4386                cmd.pop(); // trailing `|` that selects pipe-from-command
4387                ("-|".to_string(), cmd.trim_end().to_string())
4388            } else if let Some(rest) = trimmed.strip_prefix(">>") {
4389                (">>".to_string(), rest.trim().to_string())
4390            } else if let Some(rest) = trimmed.strip_prefix('>') {
4391                (">".to_string(), rest.trim().to_string())
4392            } else if let Some(rest) = trimmed.strip_prefix('<') {
4393                ("<".to_string(), rest.trim().to_string())
4394            } else {
4395                ("<".to_string(), trimmed.to_string())
4396            }
4397        };
4398        let handle_return = handle_name.clone();
4399        let file_path = match actual_mode.as_str() {
4400            "<" | ">" | ">>" => self.resolve_stryke_path_string(&path),
4401            _ => path.clone(),
4402        };
4403        match actual_mode.as_str() {
4404            "-|" => {
4405                let mut cmd = piped_shell_command(&path);
4406                cmd.stdout(Stdio::piped());
4407                let mut child = cmd.spawn().map_err(|e| {
4408                    self.apply_io_error_to_errno(&e);
4409                    StrykeError::runtime(format!("Can't open pipe from command: {}", e), line)
4410                })?;
4411                let stdout = child
4412                    .stdout
4413                    .take()
4414                    .ok_or_else(|| StrykeError::runtime("pipe: child has no stdout", line))?;
4415                self.input_handles
4416                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
4417                self.pipe_children.insert(handle_name, child);
4418            }
4419            "|-" => {
4420                let mut cmd = piped_shell_command(&path);
4421                cmd.stdin(Stdio::piped());
4422                let mut child = cmd.spawn().map_err(|e| {
4423                    self.apply_io_error_to_errno(&e);
4424                    StrykeError::runtime(format!("Can't open pipe to command: {}", e), line)
4425                })?;
4426                let stdin = child
4427                    .stdin
4428                    .take()
4429                    .ok_or_else(|| StrykeError::runtime("pipe: child has no stdin", line))?;
4430                self.output_handles
4431                    .insert(handle_name.clone(), Box::new(stdin));
4432                self.pipe_children.insert(handle_name, child);
4433            }
4434            "<" => {
4435                let file = match std::fs::File::open(&file_path) {
4436                    Ok(f) => f,
4437                    Err(e) => {
4438                        self.apply_io_error_to_errno(&e);
4439                        return Ok(StrykeValue::integer(0));
4440                    }
4441                };
4442                let shared = Arc::new(Mutex::new(file));
4443                self.io_file_slots
4444                    .insert(handle_name.clone(), Arc::clone(&shared));
4445                self.input_handles.insert(
4446                    handle_name.clone(),
4447                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
4448                );
4449            }
4450            ">" => {
4451                let file = match std::fs::File::create(&file_path) {
4452                    Ok(f) => f,
4453                    Err(e) => {
4454                        self.apply_io_error_to_errno(&e);
4455                        return Ok(StrykeValue::integer(0));
4456                    }
4457                };
4458                let shared = Arc::new(Mutex::new(file));
4459                self.io_file_slots
4460                    .insert(handle_name.clone(), Arc::clone(&shared));
4461                self.output_handles.insert(
4462                    handle_name.clone(),
4463                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4464                );
4465            }
4466            ">>" => {
4467                let file = match std::fs::OpenOptions::new()
4468                    .append(true)
4469                    .create(true)
4470                    .open(&file_path)
4471                {
4472                    Ok(f) => f,
4473                    Err(e) => {
4474                        self.apply_io_error_to_errno(&e);
4475                        return Ok(StrykeValue::integer(0));
4476                    }
4477                };
4478                let shared = Arc::new(Mutex::new(file));
4479                self.io_file_slots
4480                    .insert(handle_name.clone(), Arc::clone(&shared));
4481                self.output_handles.insert(
4482                    handle_name.clone(),
4483                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4484                );
4485            }
4486            _ => {
4487                return Err(StrykeError::runtime(
4488                    format!("Unknown open mode '{}'", actual_mode),
4489                    line,
4490                ));
4491            }
4492        }
4493        Ok(StrykeValue::io_handle(handle_return))
4494    }
4495
4496    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
4497    /// matches the previous key under [`StrykeValue::str_eq`]. Returns a list of arrayrefs
4498    /// (same outer shape as `chunked`).
4499    pub(crate) fn eval_chunk_by_builtin(
4500        &mut self,
4501        key_spec: &Expr,
4502        list_expr: &Expr,
4503        ctx: WantarrayCtx,
4504        line: usize,
4505    ) -> ExecResult {
4506        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
4507        let chunks = match &key_spec.kind {
4508            ExprKind::CodeRef { .. } => {
4509                let cr = self.eval_expr(key_spec)?;
4510                let Some(sub) = cr.as_code_ref() else {
4511                    return Err(StrykeError::runtime(
4512                        "group_by/chunk_by: first argument must be { BLOCK }",
4513                        line,
4514                    )
4515                    .into());
4516                };
4517                let sub = sub.clone();
4518                let mut chunks: Vec<StrykeValue> = Vec::new();
4519                let mut run: Vec<StrykeValue> = Vec::new();
4520                let mut prev_key: Option<StrykeValue> = None;
4521                for item in list {
4522                    self.scope.set_topic(item.clone());
4523                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
4524                        Ok(k) => k,
4525                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
4526                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
4527                        Err(_) => StrykeValue::UNDEF,
4528                    };
4529                    match &prev_key {
4530                        None => {
4531                            run.push(item);
4532                            prev_key = Some(key);
4533                        }
4534                        Some(pk) => {
4535                            if key.str_eq(pk) {
4536                                run.push(item);
4537                            } else {
4538                                chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
4539                                    std::mem::take(&mut run),
4540                                ))));
4541                                run.push(item);
4542                                prev_key = Some(key);
4543                            }
4544                        }
4545                    }
4546                }
4547                if !run.is_empty() {
4548                    chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
4549                }
4550                chunks
4551            }
4552            _ => {
4553                let mut chunks: Vec<StrykeValue> = Vec::new();
4554                let mut run: Vec<StrykeValue> = Vec::new();
4555                let mut prev_key: Option<StrykeValue> = None;
4556                for item in list {
4557                    self.scope.set_topic(item.clone());
4558                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
4559                    match &prev_key {
4560                        None => {
4561                            run.push(item);
4562                            prev_key = Some(key);
4563                        }
4564                        Some(pk) => {
4565                            if key.str_eq(pk) {
4566                                run.push(item);
4567                            } else {
4568                                chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
4569                                    std::mem::take(&mut run),
4570                                ))));
4571                                run.push(item);
4572                                prev_key = Some(key);
4573                            }
4574                        }
4575                    }
4576                }
4577                if !run.is_empty() {
4578                    chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
4579                }
4580                chunks
4581            }
4582        };
4583        Ok(match ctx {
4584            WantarrayCtx::List => StrykeValue::array(chunks),
4585            WantarrayCtx::Scalar => StrykeValue::integer(chunks.len() as i64),
4586            WantarrayCtx::Void => StrykeValue::UNDEF,
4587        })
4588    }
4589
4590    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
4591    pub(crate) fn list_higher_order_block_builtin(
4592        &mut self,
4593        name: &str,
4594        args: &[StrykeValue],
4595        line: usize,
4596    ) -> StrykeResult<StrykeValue> {
4597        match self.list_higher_order_block_builtin_exec(name, args, line) {
4598            Ok(v) => Ok(v),
4599            Err(FlowOrError::Error(e)) => Err(e),
4600            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
4601            Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
4602                format!("{name}: unsupported control flow in block"),
4603                line,
4604            )),
4605        }
4606    }
4607
4608    fn list_higher_order_block_builtin_exec(
4609        &mut self,
4610        name: &str,
4611        args: &[StrykeValue],
4612        line: usize,
4613    ) -> ExecResult {
4614        if args.is_empty() {
4615            return Err(
4616                StrykeError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
4617            );
4618        }
4619        let Some(sub) = args[0].as_code_ref() else {
4620            return Err(StrykeError::runtime(
4621                format!("{name}: first argument must be {{ BLOCK }}"),
4622                line,
4623            )
4624            .into());
4625        };
4626        let sub = sub.clone();
4627        let items: Vec<StrykeValue> = args[1..].to_vec();
4628        if matches!(name, "tap" | "peek") && items.len() == 1 {
4629            if let Some(p) = items[0].as_pipeline() {
4630                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
4631                return Ok(StrykeValue::pipeline(Arc::clone(&p)));
4632            }
4633            let v = &items[0];
4634            if v.is_iterator() || v.as_array_vec().is_some() {
4635                let source = crate::map_stream::into_pull_iter(v.clone());
4636                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
4637                return Ok(StrykeValue::iterator(Arc::new(
4638                    crate::map_stream::TapIterator::new(
4639                        source,
4640                        sub,
4641                        self.subs.clone(),
4642                        capture,
4643                        atomic_arrays,
4644                        atomic_hashes,
4645                    ),
4646                )));
4647            }
4648        }
4649        // Streaming optimization disabled for these functions because the pre-captured
4650        // coderef from args[0] has its closure_env populated at parse time, which causes
4651        // $_ to get stale values on subsequent calls. These functions work correctly in
4652        // the non-streaming eager path below.
4653        let wa = self.wantarray_kind;
4654        match name {
4655            "take_while" => {
4656                let mut out = Vec::new();
4657                for item in items {
4658                    // `call_sub` binds the item to @_/positional params (so
4659                    // stryke lambdas work) and to `$_` via `set_closure_args`
4660                    // (so `_`-using blocks work). Replaces the old
4661                    // `exec_block(&sub.body)` path which only set the topic.
4662                    self.scope.set_topic(item.clone());
4663                    let pred =
4664                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4665                    if !pred.is_true() {
4666                        break;
4667                    }
4668                    out.push(item);
4669                }
4670                Ok(match wa {
4671                    WantarrayCtx::List => StrykeValue::array(out),
4672                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4673                    WantarrayCtx::Void => StrykeValue::UNDEF,
4674                })
4675            }
4676            "drop_while" | "skip_while" => {
4677                let mut i = 0usize;
4678                while i < items.len() {
4679                    let it = items[i].clone();
4680                    self.scope.set_topic(it.clone());
4681                    let pred = self.call_sub(&sub, vec![it], WantarrayCtx::Scalar, line)?;
4682                    if !pred.is_true() {
4683                        break;
4684                    }
4685                    i += 1;
4686                }
4687                let rest = items[i..].to_vec();
4688                Ok(match wa {
4689                    WantarrayCtx::List => StrykeValue::array(rest),
4690                    WantarrayCtx::Scalar => StrykeValue::integer(rest.len() as i64),
4691                    WantarrayCtx::Void => StrykeValue::UNDEF,
4692                })
4693            }
4694            "reject" | "grepv" => {
4695                let mut out = Vec::new();
4696                for item in items {
4697                    self.scope.set_topic(item.clone());
4698                    let pred =
4699                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4700                    if !pred.is_true() {
4701                        out.push(item);
4702                    }
4703                }
4704                Ok(match wa {
4705                    WantarrayCtx::List => StrykeValue::array(out),
4706                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4707                    WantarrayCtx::Void => StrykeValue::UNDEF,
4708                })
4709            }
4710            "tap" | "peek" => {
4711                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
4712                Ok(match wa {
4713                    WantarrayCtx::List => StrykeValue::array(items),
4714                    WantarrayCtx::Scalar => StrykeValue::integer(items.len() as i64),
4715                    WantarrayCtx::Void => StrykeValue::UNDEF,
4716                })
4717            }
4718            "partition" => {
4719                let mut yes = Vec::new();
4720                let mut no = Vec::new();
4721                for item in items {
4722                    self.scope.set_topic(item.clone());
4723                    let pred =
4724                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4725                    if pred.is_true() {
4726                        yes.push(item);
4727                    } else {
4728                        no.push(item);
4729                    }
4730                }
4731                let yes_ref = StrykeValue::array_ref(Arc::new(RwLock::new(yes)));
4732                let no_ref = StrykeValue::array_ref(Arc::new(RwLock::new(no)));
4733                Ok(match wa {
4734                    WantarrayCtx::List => StrykeValue::array(vec![yes_ref, no_ref]),
4735                    WantarrayCtx::Scalar => StrykeValue::integer(2),
4736                    WantarrayCtx::Void => StrykeValue::UNDEF,
4737                })
4738            }
4739            "min_by" => {
4740                let mut best: Option<(StrykeValue, StrykeValue)> = None;
4741                for item in items {
4742                    self.scope.set_topic(item.clone());
4743                    let key =
4744                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4745                    best = Some(match best {
4746                        None => (item, key),
4747                        Some((bv, bk)) => {
4748                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
4749                                (item, key)
4750                            } else {
4751                                (bv, bk)
4752                            }
4753                        }
4754                    });
4755                }
4756                Ok(best.map(|(v, _)| v).unwrap_or(StrykeValue::UNDEF))
4757            }
4758            "max_by" => {
4759                let mut best: Option<(StrykeValue, StrykeValue)> = None;
4760                for item in items {
4761                    self.scope.set_topic(item.clone());
4762                    let key =
4763                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4764                    best = Some(match best {
4765                        None => (item, key),
4766                        Some((bv, bk)) => {
4767                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
4768                                (item, key)
4769                            } else {
4770                                (bv, bk)
4771                            }
4772                        }
4773                    });
4774                }
4775                Ok(best.map(|(v, _)| v).unwrap_or(StrykeValue::UNDEF))
4776            }
4777            "zip_with" => {
4778                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
4779                // Flatten items, then treat each array ref/binding as a separate list.
4780                let flat: Vec<StrykeValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
4781                let refs: Vec<Vec<StrykeValue>> = flat
4782                    .iter()
4783                    .map(|el| {
4784                        if let Some(ar) = el.as_array_ref() {
4785                            ar.read().clone()
4786                        } else if let Some(name) = el.as_array_binding_name() {
4787                            self.scope.get_array(&name)
4788                        } else {
4789                            vec![el.clone()]
4790                        }
4791                    })
4792                    .collect();
4793                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
4794                let mut out = Vec::with_capacity(max_len);
4795                for i in 0..max_len {
4796                    let pair: Vec<StrykeValue> = refs
4797                        .iter()
4798                        .map(|l| l.get(i).cloned().unwrap_or(StrykeValue::UNDEF))
4799                        .collect();
4800                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
4801                    out.push(result);
4802                }
4803                Ok(match wa {
4804                    WantarrayCtx::List => StrykeValue::array(out),
4805                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4806                    WantarrayCtx::Void => StrykeValue::UNDEF,
4807                })
4808            }
4809            "count_by" => {
4810                let mut counts = indexmap::IndexMap::new();
4811                for item in items {
4812                    self.scope.set_topic(item.clone());
4813                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4814                    let k = key.to_string();
4815                    let entry = counts.entry(k).or_insert(StrykeValue::integer(0));
4816                    *entry = StrykeValue::integer(entry.to_int() + 1);
4817                }
4818                Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(counts))))
4819            }
4820            _ => Err(StrykeError::runtime(
4821                format!("internal: unknown list block builtin `{name}`"),
4822                line,
4823            )
4824            .into()),
4825        }
4826    }
4827
4828    /// `rmdir LIST` — remove empty directories; returns count removed.
4829    pub(crate) fn builtin_rmdir_execute(
4830        &mut self,
4831        args: &[StrykeValue],
4832        _line: usize,
4833    ) -> StrykeResult<StrykeValue> {
4834        let mut count = 0i64;
4835        for a in args {
4836            let p = a.to_string();
4837            if p.is_empty() {
4838                continue;
4839            }
4840            let p = self.resolve_stryke_path_string(&p);
4841            if std::fs::remove_dir(&p).is_ok() {
4842                count += 1;
4843            }
4844        }
4845        Ok(StrykeValue::integer(count))
4846    }
4847
4848    /// `touch FILE, ...` — create if absent, update timestamps to now.
4849    pub(crate) fn builtin_touch_execute(
4850        &mut self,
4851        args: &[StrykeValue],
4852        _line: usize,
4853    ) -> StrykeResult<StrykeValue> {
4854        let paths: Vec<String> = args
4855            .iter()
4856            .map(|v| self.resolve_stryke_path_string(&v.to_string()))
4857            .collect();
4858        Ok(StrykeValue::integer(crate::perl_fs::touch_paths(&paths)))
4859    }
4860
4861    /// `utime ATIME, MTIME, LIST`
4862    pub(crate) fn builtin_utime_execute(
4863        &mut self,
4864        args: &[StrykeValue],
4865        line: usize,
4866    ) -> StrykeResult<StrykeValue> {
4867        if args.len() < 3 {
4868            return Err(StrykeError::runtime(
4869                "utime requires at least three arguments (atime, mtime, files...)",
4870                line,
4871            ));
4872        }
4873        let at = args[0].to_int();
4874        let mt = args[1].to_int();
4875        let paths: Vec<String> = args
4876            .iter()
4877            .skip(2)
4878            .map(|v| self.resolve_stryke_path_string(&v.to_string()))
4879            .collect();
4880        let n = crate::perl_fs::utime_paths(at, mt, &paths);
4881        #[cfg(not(unix))]
4882        if !paths.is_empty() && n == 0 {
4883            return Err(StrykeError::runtime(
4884                "utime is not supported on this platform",
4885                line,
4886            ));
4887        }
4888        Ok(StrykeValue::integer(n))
4889    }
4890
4891    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
4892    pub(crate) fn builtin_umask_execute(
4893        &mut self,
4894        args: &[StrykeValue],
4895        line: usize,
4896    ) -> StrykeResult<StrykeValue> {
4897        #[cfg(unix)]
4898        {
4899            let _ = line;
4900            if args.is_empty() {
4901                let cur = unsafe { libc::umask(0) };
4902                unsafe { libc::umask(cur) };
4903                return Ok(StrykeValue::integer(cur as i64));
4904            }
4905            let new_m = args[0].to_int() as libc::mode_t;
4906            let old = unsafe { libc::umask(new_m) };
4907            Ok(StrykeValue::integer(old as i64))
4908        }
4909        #[cfg(not(unix))]
4910        {
4911            let _ = args;
4912            Err(StrykeError::runtime(
4913                "umask is not supported on this platform",
4914                line,
4915            ))
4916        }
4917    }
4918
4919    /// `getcwd` — current directory or undef on failure.
4920    pub(crate) fn builtin_getcwd_execute(
4921        &mut self,
4922        args: &[StrykeValue],
4923        line: usize,
4924    ) -> StrykeResult<StrykeValue> {
4925        if !args.is_empty() {
4926            return Err(StrykeError::runtime("getcwd takes no arguments", line));
4927        }
4928        match std::env::current_dir() {
4929            Ok(p) => Ok(StrykeValue::string(p.to_string_lossy().into_owned())),
4930            Err(e) => {
4931                self.apply_io_error_to_errno(&e);
4932                Ok(StrykeValue::UNDEF)
4933            }
4934        }
4935    }
4936
4937    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
4938    pub(crate) fn builtin_realpath_execute(
4939        &mut self,
4940        args: &[StrykeValue],
4941        line: usize,
4942    ) -> StrykeResult<StrykeValue> {
4943        let path = args
4944            .first()
4945            .ok_or_else(|| StrykeError::runtime("realpath: need path", line))?
4946            .to_string();
4947        if path.is_empty() {
4948            return Err(StrykeError::runtime("realpath: need path", line));
4949        }
4950        let path = self.resolve_stryke_path_string(&path);
4951        match crate::perl_fs::realpath_resolved(&path) {
4952            Ok(s) => Ok(StrykeValue::string(s)),
4953            Err(e) => {
4954                self.apply_io_error_to_errno(&e);
4955                Ok(StrykeValue::UNDEF)
4956            }
4957        }
4958    }
4959
4960    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
4961    pub(crate) fn builtin_pipe_execute(
4962        &mut self,
4963        args: &[StrykeValue],
4964        line: usize,
4965    ) -> StrykeResult<StrykeValue> {
4966        if args.len() != 2 {
4967            return Err(StrykeError::runtime(
4968                "pipe requires exactly two arguments",
4969                line,
4970            ));
4971        }
4972        #[cfg(unix)]
4973        {
4974            use std::fs::File;
4975            use std::os::unix::io::FromRawFd;
4976
4977            let read_name = args[0].to_string();
4978            let write_name = args[1].to_string();
4979            if read_name.is_empty() || write_name.is_empty() {
4980                return Err(StrykeError::runtime("pipe: invalid handle name", line));
4981            }
4982            let mut fds = [0i32; 2];
4983            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
4984                let e = std::io::Error::last_os_error();
4985                self.apply_io_error_to_errno(&e);
4986                return Ok(StrykeValue::integer(0));
4987            }
4988            let read_file = unsafe { File::from_raw_fd(fds[0]) };
4989            let write_file = unsafe { File::from_raw_fd(fds[1]) };
4990
4991            let read_shared = Arc::new(Mutex::new(read_file));
4992            let write_shared = Arc::new(Mutex::new(write_file));
4993
4994            self.close_builtin_execute(read_name.clone()).ok();
4995            self.close_builtin_execute(write_name.clone()).ok();
4996
4997            self.io_file_slots
4998                .insert(read_name.clone(), Arc::clone(&read_shared));
4999            self.input_handles.insert(
5000                read_name,
5001                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
5002            );
5003
5004            self.io_file_slots
5005                .insert(write_name.clone(), Arc::clone(&write_shared));
5006            self.output_handles
5007                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
5008
5009            Ok(StrykeValue::integer(1))
5010        }
5011        #[cfg(not(unix))]
5012        {
5013            let _ = args;
5014            Err(StrykeError::runtime(
5015                "pipe is not supported on this platform",
5016                line,
5017            ))
5018        }
5019    }
5020
5021    pub(crate) fn close_builtin_execute(&mut self, name: String) -> StrykeResult<StrykeValue> {
5022        self.output_handles.remove(&name);
5023        self.input_handles.remove(&name);
5024        self.io_file_slots.remove(&name);
5025        if let Some(mut child) = self.pipe_children.remove(&name) {
5026            if let Ok(st) = child.wait() {
5027                self.record_child_exit_status(st);
5028            }
5029        }
5030        Ok(StrykeValue::integer(1))
5031    }
5032
5033    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
5034        self.input_handles.contains_key(name)
5035    }
5036
5037    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
5038    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
5039    /// readline-level EOF tracking exists.
5040    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
5041        self.line_mode_eof_pending
5042    }
5043
5044    /// `eof` / `eof()` / `eof FH` — shared by [`crate::vm::VM`] and
5045    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
5046    /// not [`ExprKind::Eof`]).
5047    pub(crate) fn eof_builtin_execute(
5048        &mut self,
5049        args: &[StrykeValue],
5050        line: usize,
5051    ) -> StrykeResult<StrykeValue> {
5052        match args.len() {
5053            0 => Ok(StrykeValue::integer(if self.eof_without_arg_is_true() {
5054                1
5055            } else {
5056                0
5057            })),
5058            1 => {
5059                let name = args[0].to_string();
5060                // `eof FH` is true when the handle is closed *or* the next
5061                // read would return no data. Peek the BufReader with
5062                // `fill_buf` so we don't consume the byte.
5063                use std::io::BufRead;
5064                let at_eof = match self.input_handles.get_mut(&name) {
5065                    None => true,
5066                    Some(reader) => match reader.fill_buf() {
5067                        Ok(buf) => buf.is_empty(),
5068                        Err(_) => true,
5069                    },
5070                };
5071                Ok(StrykeValue::integer(if at_eof { 1 } else { 0 }))
5072            }
5073            _ => Err(StrykeError::runtime("eof: too many arguments", line)),
5074        }
5075    }
5076
5077    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
5078    /// `0`, stringifies to `""`) for `""`.
5079    pub(crate) fn study_return_value(s: &str) -> StrykeValue {
5080        if s.is_empty() {
5081            StrykeValue::string(String::new())
5082        } else {
5083            StrykeValue::integer(1)
5084        }
5085    }
5086
5087    pub(crate) fn readline_builtin_execute(
5088        &mut self,
5089        handle: Option<&str>,
5090    ) -> StrykeResult<StrykeValue> {
5091        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
5092        if handle.is_none() {
5093            let argv = self.scope.get_array("ARGV");
5094            if !argv.is_empty() {
5095                loop {
5096                    if self.diamond_reader.is_none() {
5097                        while self.diamond_next_idx < argv.len() {
5098                            let path = self.resolve_stryke_path_string(
5099                                &argv[self.diamond_next_idx].to_string(),
5100                            );
5101                            self.diamond_next_idx += 1;
5102                            match File::open(&path) {
5103                                Ok(f) => {
5104                                    self.argv_current_file = path;
5105                                    self.diamond_reader = Some(BufReader::new(f));
5106                                    break;
5107                                }
5108                                Err(e) => {
5109                                    self.apply_io_error_to_errno(&e);
5110                                }
5111                            }
5112                        }
5113                        if self.diamond_reader.is_none() {
5114                            return Ok(StrykeValue::UNDEF);
5115                        }
5116                    }
5117                    let mut line_str = String::new();
5118                    let read_result: Result<usize, io::Error> =
5119                        if let Some(reader) = self.diamond_reader.as_mut() {
5120                            if self.open_pragma_utf8 {
5121                                let mut buf = Vec::new();
5122                                reader.read_until(b'\n', &mut buf).inspect(|n| {
5123                                    if *n > 0 {
5124                                        line_str = String::from_utf8_lossy(&buf).into_owned();
5125                                    }
5126                                })
5127                            } else {
5128                                let mut buf = Vec::new();
5129                                match reader.read_until(b'\n', &mut buf) {
5130                                    Ok(n) => {
5131                                        if n > 0 {
5132                                            line_str =
5133                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
5134                                                &buf,
5135                                            );
5136                                        }
5137                                        Ok(n)
5138                                    }
5139                                    Err(e) => Err(e),
5140                                }
5141                            }
5142                        } else {
5143                            unreachable!()
5144                        };
5145                    match read_result {
5146                        Ok(0) => {
5147                            self.diamond_reader = None;
5148                            continue;
5149                        }
5150                        Ok(_) => {
5151                            self.bump_line_for_handle(&self.argv_current_file.clone());
5152                            return Ok(StrykeValue::string(line_str));
5153                        }
5154                        Err(e) => {
5155                            self.apply_io_error_to_errno(&e);
5156                            self.diamond_reader = None;
5157                            continue;
5158                        }
5159                    }
5160                }
5161            } else {
5162                self.argv_current_file.clear();
5163            }
5164        }
5165
5166        let handle_name = handle.unwrap_or("STDIN");
5167        let mut line_str = String::new();
5168        if handle_name == "STDIN" {
5169            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
5170                self.last_stdin_die_bracket = if handle.is_none() {
5171                    "<>".to_string()
5172                } else {
5173                    "<STDIN>".to_string()
5174                };
5175                self.bump_line_for_handle("STDIN");
5176                return Ok(StrykeValue::string(queued));
5177            }
5178            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
5179                let mut buf = Vec::new();
5180                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
5181                    if *n > 0 {
5182                        line_str = String::from_utf8_lossy(&buf).into_owned();
5183                    }
5184                })
5185            } else {
5186                let mut buf = Vec::new();
5187                let mut lock = io::stdin().lock();
5188                match lock.read_until(b'\n', &mut buf) {
5189                    Ok(n) => {
5190                        if n > 0 {
5191                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
5192                        }
5193                        Ok(n)
5194                    }
5195                    Err(e) => Err(e),
5196                }
5197            };
5198            match r {
5199                Ok(0) => Ok(StrykeValue::UNDEF),
5200                Ok(_) => {
5201                    self.last_stdin_die_bracket = if handle.is_none() {
5202                        "<>".to_string()
5203                    } else {
5204                        "<STDIN>".to_string()
5205                    };
5206                    self.bump_line_for_handle("STDIN");
5207                    Ok(StrykeValue::string(line_str))
5208                }
5209                Err(e) => {
5210                    self.apply_io_error_to_errno(&e);
5211                    Ok(StrykeValue::UNDEF)
5212                }
5213            }
5214        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
5215            // Check $/ for slurp mode (None/undef = read entire file)
5216            let slurp_mode = self.irs.is_none();
5217            let r: Result<usize, io::Error> = if slurp_mode {
5218                // Slurp mode: read entire remaining content
5219                let mut buf = Vec::new();
5220                match reader.read_to_end(&mut buf) {
5221                    Ok(n) => {
5222                        if n > 0 {
5223                            line_str = if self.open_pragma_utf8 {
5224                                String::from_utf8_lossy(&buf).into_owned()
5225                            } else {
5226                                crate::perl_decode::decode_utf8_or_latin1_read_until(&buf)
5227                            };
5228                        }
5229                        Ok(n)
5230                    }
5231                    Err(e) => Err(e),
5232                }
5233            } else if self.open_pragma_utf8 {
5234                let mut buf = Vec::new();
5235                reader.read_until(b'\n', &mut buf).inspect(|n| {
5236                    if *n > 0 {
5237                        line_str = String::from_utf8_lossy(&buf).into_owned();
5238                    }
5239                })
5240            } else {
5241                let mut buf = Vec::new();
5242                match reader.read_until(b'\n', &mut buf) {
5243                    Ok(n) => {
5244                        if n > 0 {
5245                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
5246                        }
5247                        Ok(n)
5248                    }
5249                    Err(e) => Err(e),
5250                }
5251            };
5252            match r {
5253                Ok(0) => Ok(StrykeValue::UNDEF),
5254                Ok(_) => {
5255                    self.bump_line_for_handle(handle_name);
5256                    Ok(StrykeValue::string(line_str))
5257                }
5258                Err(e) => {
5259                    self.apply_io_error_to_errno(&e);
5260                    Ok(StrykeValue::UNDEF)
5261                }
5262            }
5263        } else {
5264            Ok(StrykeValue::UNDEF)
5265        }
5266    }
5267
5268    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
5269    pub(crate) fn readline_builtin_execute_list(
5270        &mut self,
5271        handle: Option<&str>,
5272    ) -> StrykeResult<StrykeValue> {
5273        let mut lines = Vec::new();
5274        loop {
5275            let v = self.readline_builtin_execute(handle)?;
5276            if v.is_undef() {
5277                break;
5278            }
5279            lines.push(v);
5280        }
5281        Ok(StrykeValue::array(lines))
5282    }
5283
5284    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> StrykeValue {
5285        let path = self.resolve_stryke_path_string(path);
5286        match std::fs::read_dir(&path) {
5287            Ok(rd) => {
5288                let entries: Vec<String> = rd
5289                    .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
5290                    .collect();
5291                self.dir_handles
5292                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
5293                StrykeValue::integer(1)
5294            }
5295            Err(e) => {
5296                self.apply_io_error_to_errno(&e);
5297                StrykeValue::integer(0)
5298            }
5299        }
5300    }
5301
5302    pub(crate) fn readdir_handle(&mut self, handle: &str) -> StrykeValue {
5303        if let Some(dh) = self.dir_handles.get_mut(handle) {
5304            if dh.pos < dh.entries.len() {
5305                let s = dh.entries[dh.pos].clone();
5306                dh.pos += 1;
5307                StrykeValue::string(s)
5308            } else {
5309                StrykeValue::UNDEF
5310            }
5311        } else {
5312            StrykeValue::UNDEF
5313        }
5314    }
5315
5316    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
5317    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> StrykeValue {
5318        if let Some(dh) = self.dir_handles.get_mut(handle) {
5319            let rest: Vec<StrykeValue> = dh.entries[dh.pos..]
5320                .iter()
5321                .cloned()
5322                .map(StrykeValue::string)
5323                .collect();
5324            dh.pos = dh.entries.len();
5325            StrykeValue::array(rest)
5326        } else {
5327            StrykeValue::array(Vec::new())
5328        }
5329    }
5330
5331    pub(crate) fn closedir_handle(&mut self, handle: &str) -> StrykeValue {
5332        StrykeValue::integer(if self.dir_handles.remove(handle).is_some() {
5333            1
5334        } else {
5335            0
5336        })
5337    }
5338
5339    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> StrykeValue {
5340        if let Some(dh) = self.dir_handles.get_mut(handle) {
5341            dh.pos = 0;
5342            StrykeValue::integer(1)
5343        } else {
5344            StrykeValue::integer(0)
5345        }
5346    }
5347
5348    pub(crate) fn telldir_handle(&mut self, handle: &str) -> StrykeValue {
5349        self.dir_handles
5350            .get(handle)
5351            .map(|dh| StrykeValue::integer(dh.pos as i64))
5352            .unwrap_or(StrykeValue::UNDEF)
5353    }
5354
5355    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> StrykeValue {
5356        if let Some(dh) = self.dir_handles.get_mut(handle) {
5357            dh.pos = pos.min(dh.entries.len());
5358            StrykeValue::integer(1)
5359        } else {
5360            StrykeValue::integer(0)
5361        }
5362    }
5363
5364    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
5365    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
5366    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
5367    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
5368    #[inline]
5369    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
5370        crate::special_vars::is_regex_match_scalar_name(name)
5371    }
5372
5373    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
5374    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
5375    /// `apply_regex_captures` instead of short-circuiting.
5376    #[inline]
5377    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
5378        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
5379            self.regex_capture_scope_fresh = false;
5380        }
5381    }
5382
5383    pub(crate) fn apply_regex_captures(
5384        &mut self,
5385        haystack: &str,
5386        offset: usize,
5387        re: &PerlCompiledRegex,
5388        caps: &PerlCaptures<'_>,
5389        capture_all: CaptureAllMode,
5390    ) -> Result<(), FlowOrError> {
5391        let m0 = caps.get(0).expect("regex capture 0");
5392        let s0 = offset + m0.start;
5393        let e0 = offset + m0.end;
5394        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
5395        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
5396        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
5397        let mut last_paren = String::new();
5398        for i in 1..caps.len() {
5399            if let Some(m) = caps.get(i) {
5400                last_paren = m.text.to_string();
5401            }
5402        }
5403        self.last_paren_match = last_paren;
5404        self.last_subpattern_name = String::new();
5405        for n in re.capture_names().flatten() {
5406            if caps.name(n).is_some() {
5407                self.last_subpattern_name = n.to_string();
5408            }
5409        }
5410        self.scope
5411            .set_scalar("&", StrykeValue::string(self.last_match.clone()))?;
5412        self.scope
5413            .set_scalar("`", StrykeValue::string(self.prematch.clone()))?;
5414        self.scope
5415            .set_scalar("'", StrykeValue::string(self.postmatch.clone()))?;
5416        self.scope
5417            .set_scalar("+", StrykeValue::string(self.last_paren_match.clone()))?;
5418        for i in 1..caps.len() {
5419            if let Some(m) = caps.get(i) {
5420                self.scope
5421                    .set_scalar(&i.to_string(), StrykeValue::string(m.text.to_string()))?;
5422            }
5423        }
5424        let mut start_arr = vec![StrykeValue::integer(s0 as i64)];
5425        let mut end_arr = vec![StrykeValue::integer(e0 as i64)];
5426        for i in 1..caps.len() {
5427            if let Some(m) = caps.get(i) {
5428                start_arr.push(StrykeValue::integer((offset + m.start) as i64));
5429                end_arr.push(StrykeValue::integer((offset + m.end) as i64));
5430            } else {
5431                start_arr.push(StrykeValue::integer(-1));
5432                end_arr.push(StrykeValue::integer(-1));
5433            }
5434        }
5435        self.scope.set_array("-", start_arr)?;
5436        self.scope.set_array("+", end_arr)?;
5437        let mut named = IndexMap::new();
5438        for name in re.capture_names().flatten() {
5439            if let Some(m) = caps.name(name) {
5440                named.insert(name.to_string(), StrykeValue::string(m.text.to_string()));
5441            }
5442        }
5443        self.scope.set_hash("+", named.clone())?;
5444        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
5445        let mut named_minus = IndexMap::new();
5446        for (name, val) in &named {
5447            named_minus.insert(
5448                name.clone(),
5449                StrykeValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
5450            );
5451        }
5452        self.scope.set_hash("-", named_minus)?;
5453        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
5454        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
5455        match capture_all {
5456            CaptureAllMode::Empty => {
5457                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5458            }
5459            CaptureAllMode::Append => {
5460                let mut rows = self.scope.get_array("^CAPTURE_ALL");
5461                rows.push(StrykeValue::array(cap_flat));
5462                self.scope.set_array("^CAPTURE_ALL", rows)?;
5463            }
5464            CaptureAllMode::Skip => {}
5465        }
5466        Ok(())
5467    }
5468
5469    pub(crate) fn clear_flip_flop_state(&mut self) {
5470        self.flip_flop_active.clear();
5471        self.flip_flop_exclusive_left_line.clear();
5472        self.flip_flop_sequence.clear();
5473        self.flip_flop_last_dot.clear();
5474        self.flip_flop_tree.clear();
5475    }
5476
5477    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
5478        self.flip_flop_active.resize(slots as usize, false);
5479        self.flip_flop_active.fill(false);
5480        self.flip_flop_exclusive_left_line
5481            .resize(slots as usize, None);
5482        self.flip_flop_exclusive_left_line.fill(None);
5483        self.flip_flop_sequence.resize(slots as usize, 0);
5484        self.flip_flop_sequence.fill(0);
5485        self.flip_flop_last_dot.resize(slots as usize, None);
5486        self.flip_flop_last_dot.fill(None);
5487    }
5488
5489    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
5490    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
5491    /// [`Self::handle_line_numbers`]).
5492    #[inline]
5493    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
5494        if self.last_readline_handle.is_empty() {
5495            self.line_number
5496        } else {
5497            *self
5498                .handle_line_numbers
5499                .get(&self.last_readline_handle)
5500                .unwrap_or(&0)
5501        }
5502    }
5503
5504    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
5505    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
5506    ///
5507    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
5508    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
5509    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
5510    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
5511    /// get `0` / `N` from [`StrykeValue::to_int`].
5512    pub(crate) fn scalar_flip_flop_eval(
5513        &mut self,
5514        left: i64,
5515        right: i64,
5516        slot: usize,
5517        exclusive: bool,
5518    ) -> StrykeResult<StrykeValue> {
5519        if self.flip_flop_active.len() <= slot {
5520            self.flip_flop_active.resize(slot + 1, false);
5521        }
5522        if self.flip_flop_exclusive_left_line.len() <= slot {
5523            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5524        }
5525        if self.flip_flop_sequence.len() <= slot {
5526            self.flip_flop_sequence.resize(slot + 1, 0);
5527        }
5528        if self.flip_flop_last_dot.len() <= slot {
5529            self.flip_flop_last_dot.resize(slot + 1, None);
5530        }
5531        let dot = self.scalar_flipflop_dot_line();
5532        let active = &mut self.flip_flop_active[slot];
5533        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5534        let seq = &mut self.flip_flop_sequence[slot];
5535        let last_dot = &mut self.flip_flop_last_dot[slot];
5536        if !*active {
5537            if dot == left {
5538                *active = true;
5539                *seq = 1;
5540                *last_dot = Some(dot);
5541                if exclusive {
5542                    *excl_left = Some(dot);
5543                } else {
5544                    *excl_left = None;
5545                    if dot == right {
5546                        *active = false;
5547                        return Ok(StrykeValue::string(format!("{}E0", *seq)));
5548                    }
5549                }
5550                return Ok(StrykeValue::string(seq.to_string()));
5551            }
5552            *last_dot = Some(dot);
5553            return Ok(StrykeValue::string(String::new()));
5554        }
5555        // Already active: increment the sequence once per new `$.`, so a second evaluation on
5556        // the same line reads the same number (matches Perl `pp_flop`).
5557        if *last_dot != Some(dot) {
5558            *seq += 1;
5559            *last_dot = Some(dot);
5560        }
5561        let cur_seq = *seq;
5562        if let Some(ll) = *excl_left {
5563            if dot == right && dot > ll {
5564                *active = false;
5565                *excl_left = None;
5566                *seq = 0;
5567                return Ok(StrykeValue::string(format!("{}E0", cur_seq)));
5568            }
5569        } else if dot == right {
5570            *active = false;
5571            *seq = 0;
5572            return Ok(StrykeValue::string(format!("{}E0", cur_seq)));
5573        }
5574        Ok(StrykeValue::string(cur_seq.to_string()))
5575    }
5576
5577    fn regex_flip_flop_transition(
5578        active: &mut bool,
5579        excl_left: &mut Option<i64>,
5580        exclusive: bool,
5581        dot: i64,
5582        left_m: bool,
5583        right_m: bool,
5584    ) -> i64 {
5585        if !*active {
5586            if left_m {
5587                *active = true;
5588                if exclusive {
5589                    *excl_left = Some(dot);
5590                } else {
5591                    *excl_left = None;
5592                    if right_m {
5593                        *active = false;
5594                    }
5595                }
5596                return 1;
5597            }
5598            return 0;
5599        }
5600        if let Some(ll) = *excl_left {
5601            if right_m && dot > ll {
5602                *active = false;
5603                *excl_left = None;
5604            }
5605        } else if right_m {
5606            *active = false;
5607        }
5608        1
5609    }
5610
5611    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
5612    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
5613    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
5614    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
5615    pub(crate) fn regex_flip_flop_eval(
5616        &mut self,
5617        left_pat: &str,
5618        left_flags: &str,
5619        right_pat: &str,
5620        right_flags: &str,
5621        slot: usize,
5622        exclusive: bool,
5623        line: usize,
5624    ) -> StrykeResult<StrykeValue> {
5625        let dot = self.scalar_flipflop_dot_line();
5626        let subject = self.scope.get_scalar("_").to_string();
5627        let left_re = self
5628            .compile_regex(left_pat, left_flags, line)
5629            .map_err(|e| match e {
5630                FlowOrError::Error(err) => err,
5631                FlowOrError::Flow(_) => {
5632                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5633                }
5634            })?;
5635        let right_re = self
5636            .compile_regex(right_pat, right_flags, line)
5637            .map_err(|e| match e {
5638                FlowOrError::Error(err) => err,
5639                FlowOrError::Flow(_) => {
5640                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5641                }
5642            })?;
5643        let left_m = left_re.is_match(&subject);
5644        let right_m = right_re.is_match(&subject);
5645        if self.flip_flop_active.len() <= slot {
5646            self.flip_flop_active.resize(slot + 1, false);
5647        }
5648        if self.flip_flop_exclusive_left_line.len() <= slot {
5649            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5650        }
5651        let active = &mut self.flip_flop_active[slot];
5652        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5653        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5654            active, excl_left, exclusive, dot, left_m, right_m,
5655        )))
5656    }
5657
5658    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
5659    pub(crate) fn regex_flip_flop_eval_dynamic_right(
5660        &mut self,
5661        left_pat: &str,
5662        left_flags: &str,
5663        slot: usize,
5664        exclusive: bool,
5665        line: usize,
5666        right_m: bool,
5667    ) -> StrykeResult<StrykeValue> {
5668        let dot = self.scalar_flipflop_dot_line();
5669        let subject = self.scope.get_scalar("_").to_string();
5670        let left_re = self
5671            .compile_regex(left_pat, left_flags, line)
5672            .map_err(|e| match e {
5673                FlowOrError::Error(err) => err,
5674                FlowOrError::Flow(_) => {
5675                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5676                }
5677            })?;
5678        let left_m = left_re.is_match(&subject);
5679        if self.flip_flop_active.len() <= slot {
5680            self.flip_flop_active.resize(slot + 1, false);
5681        }
5682        if self.flip_flop_exclusive_left_line.len() <= slot {
5683            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5684        }
5685        let active = &mut self.flip_flop_active[slot];
5686        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5687        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5688            active, excl_left, exclusive, dot, left_m, right_m,
5689        )))
5690    }
5691
5692    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
5693    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
5694        &mut self,
5695        left_pat: &str,
5696        left_flags: &str,
5697        slot: usize,
5698        exclusive: bool,
5699        line: usize,
5700        rhs_line: i64,
5701    ) -> StrykeResult<StrykeValue> {
5702        let dot = self.scalar_flipflop_dot_line();
5703        let subject = self.scope.get_scalar("_").to_string();
5704        let left_re = self
5705            .compile_regex(left_pat, left_flags, line)
5706            .map_err(|e| match e {
5707                FlowOrError::Error(err) => err,
5708                FlowOrError::Flow(_) => {
5709                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5710                }
5711            })?;
5712        let left_m = left_re.is_match(&subject);
5713        let right_m = dot == rhs_line;
5714        if self.flip_flop_active.len() <= slot {
5715            self.flip_flop_active.resize(slot + 1, false);
5716        }
5717        if self.flip_flop_exclusive_left_line.len() <= slot {
5718            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5719        }
5720        let active = &mut self.flip_flop_active[slot];
5721        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5722        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5723            active, excl_left, exclusive, dot, left_m, right_m,
5724        )))
5725    }
5726
5727    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
5728    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
5729    /// right test until `$.` is strictly past the line where the left regex matched (same as
5730    /// [`Self::regex_flip_flop_eval`]).
5731    pub(crate) fn regex_eof_flip_flop_eval(
5732        &mut self,
5733        left_pat: &str,
5734        left_flags: &str,
5735        slot: usize,
5736        exclusive: bool,
5737        line: usize,
5738    ) -> StrykeResult<StrykeValue> {
5739        let dot = self.scalar_flipflop_dot_line();
5740        let subject = self.scope.get_scalar("_").to_string();
5741        let left_re = self
5742            .compile_regex(left_pat, left_flags, line)
5743            .map_err(|e| match e {
5744                FlowOrError::Error(err) => err,
5745                FlowOrError::Flow(_) => {
5746                    StrykeError::runtime("unexpected flow in regex/eof flip-flop", line)
5747                }
5748            })?;
5749        let left_m = left_re.is_match(&subject);
5750        let right_m = self.eof_without_arg_is_true();
5751        if self.flip_flop_active.len() <= slot {
5752            self.flip_flop_active.resize(slot + 1, false);
5753        }
5754        if self.flip_flop_exclusive_left_line.len() <= slot {
5755            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5756        }
5757        let active = &mut self.flip_flop_active[slot];
5758        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5759        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5760            active, excl_left, exclusive, dot, left_m, right_m,
5761        )))
5762    }
5763
5764    /// Shared `chomp` implementation (mutates `target`).
5765    /// `read(FH, $buf, LEN)` — read from filehandle into named variable.
5766    /// Returns bytes read count (or error). Called from VM's ReadIntoVar op.
5767    pub(crate) fn builtin_read_into(
5768        &mut self,
5769        fh_val: StrykeValue,
5770        var_name: &str,
5771        length: usize,
5772        line: usize,
5773    ) -> ExecResult {
5774        use std::io::Read;
5775        let fh = fh_val
5776            .as_io_handle_name()
5777            .unwrap_or_else(|| fh_val.to_string());
5778        let mut buf = vec![0u8; length];
5779        let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
5780            slot.lock().read(&mut buf).unwrap_or(0)
5781        } else if fh == "STDIN" {
5782            std::io::stdin().read(&mut buf).unwrap_or(0)
5783        } else {
5784            return Err(StrykeError::runtime(format!("read: unopened handle {}", fh), line).into());
5785        };
5786        buf.truncate(n);
5787        let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
5788        let _ = self
5789            .scope
5790            .set_scalar(var_name, StrykeValue::string(read_str));
5791        Ok(StrykeValue::integer(n as i64))
5792    }
5793
5794    pub(crate) fn chomp_inplace_execute(&mut self, val: StrykeValue, target: &Expr) -> ExecResult {
5795        // Perl's `chomp` on `@arr` / `%hash` iterates and chomps every
5796        // element in place, returning the *total count* of newlines
5797        // removed. Pre-fix this collapsed the array/hash to its
5798        // stringified form, chomped that, and reassigned a scalar
5799        // back — silently destroying the container.
5800        match &target.kind {
5801            ExprKind::ArrayVar(name) => {
5802                let arr = self.scope.get_array(name);
5803                let mut total = 0i64;
5804                let mut new_arr = Vec::with_capacity(arr.len());
5805                for v in arr {
5806                    let mut s = v.to_string();
5807                    if s.ends_with('\n') {
5808                        s.pop();
5809                        total += 1;
5810                    }
5811                    new_arr.push(StrykeValue::string(s));
5812                }
5813                self.scope
5814                    .set_array(name, new_arr)
5815                    .map_err(FlowOrError::Error)?;
5816                return Ok(StrykeValue::integer(total));
5817            }
5818            ExprKind::HashVar(name) => {
5819                let h = self.scope.get_hash(name);
5820                let mut total = 0i64;
5821                let mut new_h: indexmap::IndexMap<String, StrykeValue> =
5822                    indexmap::IndexMap::with_capacity(h.len());
5823                for (k, v) in h {
5824                    let mut s = v.to_string();
5825                    if s.ends_with('\n') {
5826                        s.pop();
5827                        total += 1;
5828                    }
5829                    new_h.insert(k, StrykeValue::string(s));
5830                }
5831                self.scope
5832                    .set_hash(name, new_h)
5833                    .map_err(FlowOrError::Error)?;
5834                return Ok(StrykeValue::integer(total));
5835            }
5836            _ => {}
5837        }
5838        let mut s = val.to_string();
5839        let removed = if s.ends_with('\n') {
5840            s.pop();
5841            1i64
5842        } else {
5843            0i64
5844        };
5845        self.assign_value(target, StrykeValue::string(s))?;
5846        Ok(StrykeValue::integer(removed))
5847    }
5848
5849    /// Shared `chop` implementation (mutates `target`).
5850    pub(crate) fn chop_inplace_execute(&mut self, val: StrykeValue, target: &Expr) -> ExecResult {
5851        // Perl's `chop @arr` / `chop %hash` chops every element in
5852        // place and returns the *last character chopped*. Without
5853        // this branch the call stringified the whole container,
5854        // chopped one byte off the joined form, and reassigned a
5855        // scalar back — destroying the array.
5856        match &target.kind {
5857            ExprKind::ArrayVar(name) => {
5858                let arr = self.scope.get_array(name);
5859                let mut last = StrykeValue::UNDEF;
5860                let mut new_arr = Vec::with_capacity(arr.len());
5861                for v in arr {
5862                    let mut s = v.to_string();
5863                    if let Some(c) = s.pop() {
5864                        last = StrykeValue::string(c.to_string());
5865                    }
5866                    new_arr.push(StrykeValue::string(s));
5867                }
5868                self.scope
5869                    .set_array(name, new_arr)
5870                    .map_err(FlowOrError::Error)?;
5871                return Ok(last);
5872            }
5873            ExprKind::HashVar(name) => {
5874                let h = self.scope.get_hash(name);
5875                let mut last = StrykeValue::UNDEF;
5876                let mut new_h: indexmap::IndexMap<String, StrykeValue> =
5877                    indexmap::IndexMap::with_capacity(h.len());
5878                for (k, v) in h {
5879                    let mut s = v.to_string();
5880                    if let Some(c) = s.pop() {
5881                        last = StrykeValue::string(c.to_string());
5882                    }
5883                    new_h.insert(k, StrykeValue::string(s));
5884                }
5885                self.scope
5886                    .set_hash(name, new_h)
5887                    .map_err(FlowOrError::Error)?;
5888                return Ok(last);
5889            }
5890            _ => {}
5891        }
5892        let mut s = val.to_string();
5893        let chopped = s
5894            .pop()
5895            .map(|c| StrykeValue::string(c.to_string()))
5896            .unwrap_or(StrykeValue::UNDEF);
5897        self.assign_value(target, StrykeValue::string(s))?;
5898        Ok(chopped)
5899    }
5900
5901    /// Shared regex match implementation (`pos` is updated for scalar `/g`).
5902    pub(crate) fn regex_match_execute(
5903        &mut self,
5904        s: String,
5905        pattern: &str,
5906        flags: &str,
5907        scalar_g: bool,
5908        pos_key: &str,
5909        line: usize,
5910    ) -> ExecResult {
5911        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
5912        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
5913        // also keep per-pattern `pos()` state that the memo doesn't track.
5914        //
5915        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
5916        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
5917        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
5918        if !flags.contains('g') && !scalar_g {
5919            let memo_hit = {
5920                if let Some(ref mem) = self.regex_match_memo {
5921                    mem.pattern == pattern
5922                        && mem.flags == flags
5923                        && mem.multiline == self.multiline_match
5924                        && mem.haystack == s
5925                } else {
5926                    false
5927                }
5928            };
5929            if memo_hit {
5930                if self.regex_capture_scope_fresh {
5931                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
5932                }
5933                // Memo hit but scope side effects were invalidated. Re-apply captures
5934                // from the memoized haystack + a fresh compiled regex.
5935                let (memo_s, memo_result) = {
5936                    let mem = self.regex_match_memo.as_ref().expect("memo");
5937                    (mem.haystack.clone(), mem.result.clone())
5938                };
5939                let re = self.compile_regex(pattern, flags, line)?;
5940                if let Some(caps) = re.captures(&memo_s) {
5941                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
5942                }
5943                self.regex_capture_scope_fresh = true;
5944                return Ok(memo_result);
5945            }
5946        }
5947        let re = self.compile_regex(pattern, flags, line)?;
5948        if flags.contains('g') && scalar_g {
5949            let key = pos_key.to_string();
5950            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
5951            if start == 0 {
5952                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5953            }
5954            if start > s.len() {
5955                self.regex_pos.insert(key, None);
5956                return Ok(StrykeValue::integer(0));
5957            }
5958            let sub = s.get(start..).unwrap_or("");
5959            if let Some(caps) = re.captures(sub) {
5960                let overall = caps.get(0).expect("capture 0");
5961                let abs_end = start + overall.end;
5962                self.regex_pos.insert(key, Some(abs_end));
5963                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
5964                Ok(StrykeValue::integer(1))
5965            } else {
5966                self.regex_pos.insert(key, None);
5967                Ok(StrykeValue::integer(0))
5968            }
5969        } else if flags.contains('g') {
5970            let mut rows = Vec::new();
5971            let mut last_caps: Option<PerlCaptures<'_>> = None;
5972            let mut has_groups = false;
5973            // Flattened per-match captures so list-context `/g` returns
5974            // `($1_a, $2_a, $1_b, $2_b, …)` instead of the joined overall
5975            // match strings (Perl's documented behavior).
5976            let mut flat_captures: Vec<StrykeValue> = Vec::new();
5977            for caps in re.captures_iter(&s) {
5978                if caps.len() > 1 {
5979                    has_groups = true;
5980                }
5981                let cap_row = crate::perl_regex::numbered_capture_flat(&caps);
5982                flat_captures.extend(cap_row.iter().cloned());
5983                rows.push(StrykeValue::array(cap_row));
5984                last_caps = Some(caps);
5985            }
5986            self.scope.set_array("^CAPTURE_ALL", rows)?;
5987            if has_groups {
5988                if flat_captures.is_empty() {
5989                    return Ok(StrykeValue::integer(0));
5990                }
5991                if let Some(caps) = last_caps {
5992                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
5993                }
5994                return Ok(StrykeValue::array(flat_captures));
5995            }
5996            let matches: Vec<StrykeValue> = match &*re {
5997                PerlCompiledRegex::Rust(r) => r
5998                    .find_iter(&s)
5999                    .map(|m| StrykeValue::string(m.as_str().to_string()))
6000                    .collect(),
6001                PerlCompiledRegex::Fancy(r) => r
6002                    .find_iter(&s)
6003                    .filter_map(|m| m.ok())
6004                    .map(|m| StrykeValue::string(m.as_str().to_string()))
6005                    .collect(),
6006                PerlCompiledRegex::Pcre2(r) => r
6007                    .find_iter(s.as_bytes())
6008                    .filter_map(|m| m.ok())
6009                    .map(|m| {
6010                        let t = s.get(m.start()..m.end()).unwrap_or("");
6011                        StrykeValue::string(t.to_string())
6012                    })
6013                    .collect(),
6014            };
6015            if matches.is_empty() {
6016                Ok(StrykeValue::integer(0))
6017            } else {
6018                if let Some(caps) = last_caps {
6019                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
6020                }
6021                Ok(StrykeValue::array(matches))
6022            }
6023        } else if let Some(caps) = re.captures(&s) {
6024            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
6025            // Perl list-context `m//` returns the captures as a list when the
6026            // pattern has groups (so `my ($k, $v) = $s =~ /^(\w+)=(\d+)/` works).
6027            // When every capture is `undef` (e.g. only optional groups that
6028            // never fired, like `/^-?\d+(\.\d+)?$/` on "123") fall back to a
6029            // truthy `1`: returning `(undef,)` would silently flip every
6030            // Perl `if ($s =~ /…/)` idiom called through a fn whose body
6031            // happens to be in list context. Real captures still propagate.
6032            let list_context = self.wantarray_kind == WantarrayCtx::List;
6033            let has_groups = caps.len() > 1;
6034            let result = if list_context && has_groups {
6035                let cap_vec = crate::perl_regex::numbered_capture_flat(&caps);
6036                let any_defined = cap_vec.iter().any(|v| !v.is_undef());
6037                if any_defined {
6038                    StrykeValue::array(cap_vec)
6039                } else {
6040                    StrykeValue::integer(1)
6041                }
6042            } else {
6043                StrykeValue::integer(1)
6044            };
6045            // Only memoize when the result is the scalar form; otherwise the
6046            // memo can't know about wantarray.
6047            if matches!(result.as_integer(), Some(1)) {
6048                self.regex_match_memo = Some(RegexMatchMemo {
6049                    pattern: pattern.to_string(),
6050                    flags: flags.to_string(),
6051                    multiline: self.multiline_match,
6052                    haystack: s,
6053                    result: result.clone(),
6054                });
6055            }
6056            self.regex_capture_scope_fresh = true;
6057            Ok(result)
6058        } else {
6059            // No match: list context yields the empty list, scalar 0.
6060            let list_context = self.wantarray_kind == WantarrayCtx::List;
6061            let result = if list_context {
6062                StrykeValue::array(Vec::new())
6063            } else {
6064                StrykeValue::integer(0)
6065            };
6066            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
6067            if !list_context {
6068                self.regex_match_memo = Some(RegexMatchMemo {
6069                    pattern: pattern.to_string(),
6070                    flags: flags.to_string(),
6071                    multiline: self.multiline_match,
6072                    haystack: s,
6073                    result: result.clone(),
6074                });
6075            }
6076            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
6077            // the last successful match (if any) set them to. Don't flip the flag.
6078            Ok(result)
6079        }
6080    }
6081
6082    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
6083    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
6084    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
6085    pub(crate) fn expand_env_braces_in_subst(
6086        &mut self,
6087        raw: &str,
6088        line: usize,
6089    ) -> StrykeResult<String> {
6090        self.materialize_env_if_needed();
6091        let mut out = String::new();
6092        let mut rest = raw;
6093        while let Some(idx) = rest.find("$ENV{") {
6094            out.push_str(&rest[..idx]);
6095            let after = &rest[idx + 5..];
6096            let end = after
6097                .find('}')
6098                .ok_or_else(|| StrykeError::runtime("Unclosed $ENV{...} in s///", line))?;
6099            let key = &after[..end];
6100            let val = self.scope.get_hash_element("ENV", key);
6101            out.push_str(&val.to_string());
6102            rest = &after[end + 1..];
6103        }
6104        out.push_str(rest);
6105        Ok(out)
6106    }
6107
6108    /// Shared `s///` implementation.
6109    ///
6110    /// Perl replacement strings accept both `\1` and `$1` for back-references.
6111    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
6112    /// understand `$N`, so we normalise here.
6113    pub(crate) fn regex_subst_execute(
6114        &mut self,
6115        s: String,
6116        pattern: &str,
6117        replacement: &str,
6118        flags: &str,
6119        target: &Expr,
6120        line: usize,
6121    ) -> ExecResult {
6122        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
6123        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
6124        let re = self.compile_regex(&pattern, &re_flags, line)?;
6125        if flags.contains('e') {
6126            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
6127        }
6128        let replacement = self.expand_env_braces_in_subst(replacement, line)?;
6129        let replacement = self.interpolate_replacement_string(&replacement);
6130        let replacement = normalize_replacement_backrefs(&replacement);
6131        let last_caps = if flags.contains('g') {
6132            let mut rows = Vec::new();
6133            let mut last = None;
6134            for caps in re.captures_iter(&s) {
6135                rows.push(StrykeValue::array(
6136                    crate::perl_regex::numbered_capture_flat(&caps),
6137                ));
6138                last = Some(caps);
6139            }
6140            self.scope.set_array("^CAPTURE_ALL", rows)?;
6141            last
6142        } else {
6143            re.captures(&s)
6144        };
6145        if let Some(caps) = last_caps {
6146            let mode = if flags.contains('g') {
6147                CaptureAllMode::Skip
6148            } else {
6149                CaptureAllMode::Empty
6150            };
6151            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
6152        }
6153        let (new_s, count) = if flags.contains('g') {
6154            let count = re.find_iter_count(&s);
6155            (re.replace_all(&s, replacement.as_str()), count)
6156        } else {
6157            let count = if re.is_match(&s) { 1 } else { 0 };
6158            (re.replace(&s, replacement.as_str()), count)
6159        };
6160        if flags.contains('r') {
6161            // /r — non-destructive: return the modified string, leave target unchanged
6162            Ok(StrykeValue::string(new_s))
6163        } else {
6164            self.assign_value(target, StrykeValue::string(new_s))?;
6165            Ok(StrykeValue::integer(count as i64))
6166        }
6167    }
6168
6169    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
6170    /// and executes the string; the next round uses [`StrykeValue::to_string`] of the prior value).
6171    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
6172        let prep_source = |raw: &str| -> String {
6173            let mut code = raw.trim().to_string();
6174            if !code.ends_with(';') {
6175                code.push(';');
6176            }
6177            code
6178        };
6179        let mut cur = prep_source(replacement);
6180        let mut last = StrykeValue::UNDEF;
6181        for round in 0..e_count {
6182            last = crate::parse_and_run_string(&cur, self)?;
6183            if round + 1 < e_count {
6184                cur = prep_source(&last.to_string());
6185            }
6186        }
6187        Ok(last)
6188    }
6189
6190    fn regex_subst_execute_eval(
6191        &mut self,
6192        s: String,
6193        re: &PerlCompiledRegex,
6194        replacement: &str,
6195        flags: &str,
6196        target: &Expr,
6197        line: usize,
6198    ) -> ExecResult {
6199        let e_count = flags.chars().filter(|c| *c == 'e').count();
6200        if e_count == 0 {
6201            return Err(StrykeError::runtime("s///e: internal error (no e flag)", line).into());
6202        }
6203
6204        if flags.contains('g') {
6205            let mut rows = Vec::new();
6206            let mut out = String::new();
6207            let mut last = 0usize;
6208            let mut count = 0usize;
6209            for caps in re.captures_iter(&s) {
6210                let m0 = caps.get(0).expect("regex capture 0");
6211                out.push_str(&s[last..m0.start]);
6212                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
6213                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
6214                out.push_str(&repl_val.to_string());
6215                last = m0.end;
6216                count += 1;
6217                rows.push(StrykeValue::array(
6218                    crate::perl_regex::numbered_capture_flat(&caps),
6219                ));
6220            }
6221            self.scope.set_array("^CAPTURE_ALL", rows)?;
6222            out.push_str(&s[last..]);
6223            if flags.contains('r') {
6224                return Ok(StrykeValue::string(out));
6225            }
6226            self.assign_value(target, StrykeValue::string(out))?;
6227            return Ok(StrykeValue::integer(count as i64));
6228        }
6229        if let Some(caps) = re.captures(&s) {
6230            let m0 = caps.get(0).expect("regex capture 0");
6231            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
6232            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
6233            let mut out = String::new();
6234            out.push_str(&s[..m0.start]);
6235            out.push_str(&repl_val.to_string());
6236            out.push_str(&s[m0.end..]);
6237            if flags.contains('r') {
6238                return Ok(StrykeValue::string(out));
6239            }
6240            self.assign_value(target, StrykeValue::string(out))?;
6241            return Ok(StrykeValue::integer(1));
6242        }
6243        if flags.contains('r') {
6244            return Ok(StrykeValue::string(s));
6245        }
6246        self.assign_value(target, StrykeValue::string(s))?;
6247        Ok(StrykeValue::integer(0))
6248    }
6249
6250    /// Shared `tr///` implementation.
6251    pub(crate) fn regex_transliterate_execute(
6252        &mut self,
6253        s: String,
6254        from: &str,
6255        to: &str,
6256        flags: &str,
6257        target: &Expr,
6258        line: usize,
6259    ) -> ExecResult {
6260        let _ = line;
6261        let from_chars = Self::tr_expand_ranges(from);
6262        let to_chars = Self::tr_expand_ranges(to);
6263        let delete_mode = flags.contains('d');
6264        let complement = flags.contains('c');
6265        let squash = flags.contains('s');
6266
6267        let mut count = 0i64;
6268        let mut new_s = String::with_capacity(s.len());
6269        let mut last_out: Option<char> = None;
6270        for c in s.chars() {
6271            let in_from = from_chars.iter().position(|&fc| fc == c);
6272            let matched = if complement {
6273                in_from.is_none()
6274            } else {
6275                in_from.is_some()
6276            };
6277            if !matched {
6278                new_s.push(c);
6279                last_out = Some(c);
6280                continue;
6281            }
6282            count += 1;
6283            // Pick the replacement character.
6284            //   - complement: every matched char maps to the LAST char of `to`
6285            //     (Perl behavior); `tr/0-9//c` with empty `to` falls through to
6286            //     the keep/delete decision below.
6287            //   - direct: matched chars map by position, with the last `to`
6288            //     char duplicating if `from` is longer (unless `/d` is set).
6289            let out_c = if complement {
6290                to_chars.last().copied()
6291            } else if let Some(pos) = in_from {
6292                if pos < to_chars.len() {
6293                    Some(to_chars[pos])
6294                } else if delete_mode {
6295                    None
6296                } else {
6297                    to_chars.last().copied().or(Some(c))
6298                }
6299            } else {
6300                None
6301            };
6302            let out_c = match out_c {
6303                Some(c) => c,
6304                None => {
6305                    // No replacement available — either `/d` deletes, or there
6306                    // was no `to` char at all; keep the original character to
6307                    // match Perl's "tr/X//" identity behavior.
6308                    if delete_mode {
6309                        continue;
6310                    }
6311                    c
6312                }
6313            };
6314            if squash && last_out == Some(out_c) {
6315                continue;
6316            }
6317            new_s.push(out_c);
6318            last_out = Some(out_c);
6319        }
6320
6321        if flags.contains('r') {
6322            // /r — non-destructive: return the modified string, leave target unchanged
6323            Ok(StrykeValue::string(new_s))
6324        } else {
6325            self.assign_value(target, StrykeValue::string(new_s))?;
6326            Ok(StrykeValue::integer(count))
6327        }
6328    }
6329
6330    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
6331    /// A literal `-` at the start or end of the spec is kept as-is.
6332    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
6333        let raw: Vec<char> = spec.chars().collect();
6334        let mut out = Vec::with_capacity(raw.len());
6335        let mut i = 0;
6336        while i < raw.len() {
6337            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
6338                let start = raw[i] as u32;
6339                let end = raw[i + 2] as u32;
6340                for code in start..=end {
6341                    if let Some(c) = char::from_u32(code) {
6342                        out.push(c);
6343                    }
6344                }
6345                i += 3;
6346            } else {
6347                out.push(raw[i]);
6348                i += 1;
6349            }
6350        }
6351        out
6352    }
6353
6354    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
6355    pub(crate) fn splice_builtin_execute(
6356        &mut self,
6357        args: &[StrykeValue],
6358        line: usize,
6359    ) -> StrykeResult<StrykeValue> {
6360        if args.is_empty() {
6361            return Err(StrykeError::runtime("splice: missing array", line));
6362        }
6363        let arr_name = args[0].to_string();
6364        let arr_len = self.scope.array_len(&arr_name);
6365        let offset_val = args
6366            .get(1)
6367            .cloned()
6368            .unwrap_or_else(|| StrykeValue::integer(0));
6369        let length_val = match args.get(2) {
6370            None => StrykeValue::UNDEF,
6371            Some(v) => v.clone(),
6372        };
6373        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
6374        // Perl's `splice LIST` is list context — `@arr` and other list-valued
6375        // operands splat into the replacement, instead of being scalarized to
6376        // their element count. Mirrors `push`/`unshift` flattening.
6377        let mut rep_vals: Vec<StrykeValue> = Vec::new();
6378        for a in args.iter().skip(3) {
6379            if let Some(items) = a.as_array_vec() {
6380                rep_vals.extend(items);
6381            } else {
6382                rep_vals.push(a.clone());
6383            }
6384        }
6385        let removed = self.scope.splice_in_place(&arr_name, off, end, rep_vals)?;
6386        Ok(match self.wantarray_kind {
6387            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
6388            WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
6389        })
6390    }
6391
6392    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
6393    pub(crate) fn unshift_builtin_execute(
6394        &mut self,
6395        args: &[StrykeValue],
6396        line: usize,
6397    ) -> StrykeResult<StrykeValue> {
6398        if args.is_empty() {
6399            return Err(StrykeError::runtime("unshift: missing array", line));
6400        }
6401        let arr_name = args[0].to_string();
6402        let mut flat_vals: Vec<StrykeValue> = Vec::new();
6403        for a in args.iter().skip(1) {
6404            if let Some(items) = a.as_array_vec() {
6405                flat_vals.extend(items);
6406            } else {
6407                flat_vals.push(a.clone());
6408            }
6409        }
6410        let arr = self.scope.get_array_mut(&arr_name)?;
6411        for (i, v) in flat_vals.into_iter().enumerate() {
6412            arr.insert(i, v);
6413        }
6414        Ok(StrykeValue::integer(arr.len() as i64))
6415    }
6416
6417    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
6418    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
6419    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
6420        if upper == 0.0 {
6421            self.rand_rng.gen_range(0.0..1.0)
6422        } else if upper > 0.0 {
6423            self.rand_rng.gen_range(0.0..upper)
6424        } else {
6425            self.rand_rng.gen_range(upper..0.0)
6426        }
6427    }
6428
6429    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
6430    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
6431        let n = if let Some(s) = seed {
6432            s as i64
6433        } else {
6434            std::time::SystemTime::now()
6435                .duration_since(std::time::UNIX_EPOCH)
6436                .map(|d| d.as_secs() as i64)
6437                .unwrap_or(1)
6438        };
6439        let mag = n.unsigned_abs();
6440        self.rand_rng = StdRng::seed_from_u64(mag);
6441        n.abs()
6442    }
6443
6444    pub fn set_file(&mut self, file: &str) {
6445        self.file = file.to_string();
6446    }
6447
6448    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
6449    pub fn repl_completion_names(&self) -> Vec<String> {
6450        let mut v = self.scope.repl_binding_names();
6451        v.extend(self.subs.keys().cloned());
6452        v.sort();
6453        v.dedup();
6454        v
6455    }
6456
6457    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
6458    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
6459        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
6460        subs.sort();
6461        let mut classes: HashSet<String> = HashSet::new();
6462        for k in &subs {
6463            if let Some((pkg, rest)) = k.split_once("::") {
6464                if !rest.contains("::") {
6465                    classes.insert(pkg.to_string());
6466                }
6467            }
6468        }
6469        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
6470        for bn in self.scope.repl_binding_names() {
6471            if let Some(r) = bn.strip_prefix('$') {
6472                let v = self.scope.get_scalar(r);
6473                if let Some(b) = v.as_blessed_ref() {
6474                    blessed_scalars.insert(r.to_string(), b.class.clone());
6475                    classes.insert(b.class.clone());
6476                }
6477            }
6478        }
6479        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
6480        for c in classes {
6481            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
6482        }
6483        ReplCompletionSnapshot {
6484            subs,
6485            blessed_scalars,
6486            isa_for_class,
6487        }
6488    }
6489
6490    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
6491        if n == 0 {
6492            return Err(FlowOrError::Error(StrykeError::runtime(
6493                "bench: iteration count must be positive",
6494                line,
6495            )));
6496        }
6497        let mut samples = Vec::with_capacity(n);
6498        for _ in 0..n {
6499            let start = std::time::Instant::now();
6500            self.exec_block(body)?;
6501            samples.push(start.elapsed().as_secs_f64() * 1000.0);
6502        }
6503        let mut sorted = samples.clone();
6504        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6505        let min_ms = sorted[0];
6506        let mean = samples.iter().sum::<f64>() / n as f64;
6507        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
6508            .saturating_sub(1)
6509            .min(n - 1);
6510        let p99_ms = sorted[p99_idx];
6511        Ok(StrykeValue::string(format!(
6512            "bench: n={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
6513            n, min_ms, mean, p99_ms
6514        )))
6515    }
6516
6517    pub fn execute(&mut self, program: &Program) -> StrykeResult<StrykeValue> {
6518        // Snapshot the (possibly empty) class registry into the
6519        // thread-local that the free-function serializers consult, so
6520        // that `to_json($obj)` can resolve inheritance fields without
6521        // taking a `&VMHelper`. Done unconditionally — cheap clone of
6522        // an Arc<HashMap>-shaped structure.
6523        crate::serialize_normalize::install_class_defs(self.class_defs.clone());
6524        // `-n`/`-p`: compile and run only the prelude, store chunk for per-line re-execution.
6525        if self.line_mode_skip_main {
6526            crate::compile_and_run_prelude(program, self)?;
6527            return Ok(StrykeValue::UNDEF);
6528        }
6529        crate::try_vm_execute(program, self)
6530            .expect("VM compilation must succeed — all execution is VM-only")
6531    }
6532
6533    /// Run `END` blocks (after `-n`/`-p` line loop when prelude used [`Self::line_mode_skip_main`]).
6534    pub fn run_end_blocks(&mut self) -> StrykeResult<()> {
6535        self.global_phase = "END".to_string();
6536        let ends = std::mem::take(&mut self.end_blocks);
6537        for block in &ends {
6538            self.exec_block(block).map_err(|e| match e {
6539                FlowOrError::Error(e) => e,
6540                FlowOrError::Flow(_) => StrykeError::runtime("Unexpected flow control in END", 0),
6541            })?;
6542        }
6543        Ok(())
6544    }
6545
6546    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
6547    /// and drain remaining `DESTROY` callbacks.
6548    pub fn run_global_teardown(&mut self) -> StrykeResult<()> {
6549        self.global_phase = "DESTRUCT".to_string();
6550        self.drain_pending_destroys(0)
6551    }
6552
6553    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
6554    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> StrykeResult<()> {
6555        loop {
6556            let batch = crate::pending_destroy::take_queue();
6557            if batch.is_empty() {
6558                break;
6559            }
6560            for (class, payload) in batch {
6561                let fq = format!("{}::DESTROY", class);
6562                let Some(sub) = self.subs.get(&fq).cloned() else {
6563                    continue;
6564                };
6565                let inv = StrykeValue::blessed(Arc::new(
6566                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
6567                ));
6568                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
6569                    Ok(_) => {}
6570                    Err(FlowOrError::Error(e)) => return Err(e),
6571                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
6572                    Err(FlowOrError::Flow(other)) => {
6573                        return Err(StrykeError::runtime(
6574                            format!("DESTROY: unexpected control flow ({other:?})"),
6575                            line,
6576                        ));
6577                    }
6578                }
6579            }
6580        }
6581        Ok(())
6582    }
6583
6584    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
6585        self.exec_block_with_tail(block, WantarrayCtx::Void)
6586    }
6587
6588    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
6589    /// Non-final statements stay void context.
6590    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6591        let uses_goto = block
6592            .iter()
6593            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
6594        if uses_goto {
6595            self.scope_push_hook();
6596            let r = self.exec_block_with_goto_tail(block, tail);
6597            self.scope_pop_hook();
6598            r
6599        } else {
6600            self.scope_push_hook();
6601            let result = self.exec_block_no_scope_with_tail(block, tail);
6602            self.scope_pop_hook();
6603            result
6604        }
6605    }
6606
6607    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6608        let mut map: HashMap<String, usize> = HashMap::new();
6609        for (i, s) in block.iter().enumerate() {
6610            if let Some(l) = &s.label {
6611                map.insert(l.clone(), i);
6612            }
6613        }
6614        let mut pc = 0usize;
6615        let mut last = StrykeValue::UNDEF;
6616        let last_idx = block.len().saturating_sub(1);
6617        while pc < block.len() {
6618            if let StmtKind::Goto { target } = &block[pc].kind {
6619                let line = block[pc].line;
6620                let name = self.eval_expr(target)?.to_string();
6621                pc = *map.get(&name).ok_or_else(|| {
6622                    FlowOrError::Error(StrykeError::runtime(
6623                        format!("goto: unknown label {}", name),
6624                        line,
6625                    ))
6626                })?;
6627                continue;
6628            }
6629            let v = if pc == last_idx {
6630                match &block[pc].kind {
6631                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
6632                    _ => self.exec_statement(&block[pc])?,
6633                }
6634            } else {
6635                self.exec_statement(&block[pc])?
6636            };
6637            last = v;
6638            pc += 1;
6639        }
6640        Ok(last)
6641    }
6642
6643    /// Execute block statements without pushing/popping a scope frame.
6644    /// Used internally by loops and the VM for sub calls.
6645    #[inline]
6646    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
6647        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
6648    }
6649
6650    pub(crate) fn exec_block_no_scope_with_tail(
6651        &mut self,
6652        block: &Block,
6653        tail: WantarrayCtx,
6654    ) -> ExecResult {
6655        if block.is_empty() {
6656            return Ok(StrykeValue::UNDEF);
6657        }
6658        let last_i = block.len() - 1;
6659        for (i, stmt) in block.iter().enumerate() {
6660            if i < last_i {
6661                self.exec_statement(stmt)?;
6662            } else {
6663                return match &stmt.kind {
6664                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
6665                    _ => self.exec_statement(stmt),
6666                };
6667            }
6668        }
6669        Ok(StrykeValue::UNDEF)
6670    }
6671
6672    /// Spawn `block` on a worker thread; returns an [`StrykeValue::AsyncTask`] handle (`async { }` / `spawn { }`).
6673    pub(crate) fn spawn_async_block(&self, block: &Block) -> StrykeValue {
6674        use parking_lot::Mutex as ParkMutex;
6675
6676        let block = block.clone();
6677        let subs = self.subs.clone();
6678        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6679        let result = Arc::new(ParkMutex::new(None));
6680        let join = Arc::new(ParkMutex::new(None));
6681        let result2 = result.clone();
6682        let h = std::thread::spawn(move || {
6683            let mut interp = VMHelper::new();
6684            interp.subs = subs;
6685            interp.scope.restore_capture(&scalars);
6686            interp.scope.restore_atomics(&aar, &ahash);
6687            interp.enable_parallel_guard();
6688            let r = match interp.exec_block(&block) {
6689                Ok(v) => Ok(v),
6690                Err(FlowOrError::Error(e)) => Err(e),
6691                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6692                    Err(StrykeError::runtime("yield inside async/spawn block", 0))
6693                }
6694                Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
6695            };
6696            *result2.lock() = Some(r);
6697        });
6698        *join.lock() = Some(h);
6699        StrykeValue::async_task(Arc::new(StrykeAsyncTask { result, join }))
6700    }
6701
6702    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
6703    pub(crate) fn eval_timeout_block(
6704        &mut self,
6705        body: &Block,
6706        secs: f64,
6707        line: usize,
6708    ) -> ExecResult {
6709        use std::sync::mpsc::channel;
6710        use std::time::Duration;
6711
6712        let block = body.clone();
6713        let subs = self.subs.clone();
6714        let struct_defs = self.struct_defs.clone();
6715        let enum_defs = self.enum_defs.clone();
6716        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6717        self.materialize_env_if_needed();
6718        let env = self.env.clone();
6719        let argv = self.argv.clone();
6720        let inc = self.scope.get_array("INC");
6721        let (tx, rx) = channel::<StrykeResult<StrykeValue>>();
6722        let _handle = std::thread::spawn(move || {
6723            let mut interp = VMHelper::new();
6724            interp.subs = subs;
6725            interp.struct_defs = struct_defs;
6726            interp.enum_defs = enum_defs;
6727            interp.env = env.clone();
6728            interp.argv = argv.clone();
6729            interp.scope.declare_array(
6730                "ARGV",
6731                argv.iter()
6732                    .map(|s| StrykeValue::string(s.clone()))
6733                    .collect(),
6734            );
6735            for (k, v) in env {
6736                interp
6737                    .scope
6738                    .set_hash_element("ENV", &k, v)
6739                    .expect("set ENV in timeout thread");
6740            }
6741            interp.scope.declare_array("INC", inc);
6742            interp.scope.restore_capture(&scalars);
6743            interp.scope.restore_atomics(&aar, &ahash);
6744            interp.enable_parallel_guard();
6745            let out: StrykeResult<StrykeValue> = match interp.exec_block(&block) {
6746                Ok(v) => Ok(v),
6747                Err(FlowOrError::Error(e)) => Err(e),
6748                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6749                    Err(StrykeError::runtime("yield inside eval_timeout block", 0))
6750                }
6751                Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
6752            };
6753            let _ = tx.send(out);
6754        });
6755        let dur = Duration::from_secs_f64(secs.max(0.0));
6756        match rx.recv_timeout(dur) {
6757            Ok(Ok(v)) => Ok(v),
6758            Ok(Err(e)) => Err(FlowOrError::Error(e)),
6759            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(StrykeError::runtime(
6760                format!(
6761                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
6762                    secs
6763                ),
6764                line,
6765            )
6766            .into()),
6767            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(StrykeError::runtime(
6768                "eval_timeout: worker thread panicked or disconnected",
6769                line,
6770            )
6771            .into()),
6772        }
6773    }
6774
6775    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
6776        let mut last = StrykeValue::UNDEF;
6777        for stmt in body {
6778            match &stmt.kind {
6779                StmtKind::When { cond, body: wb } => {
6780                    if self.when_matches(cond)? {
6781                        return self.exec_block_smart(wb);
6782                    }
6783                }
6784                StmtKind::DefaultCase { body: db } => {
6785                    return self.exec_block_smart(db);
6786                }
6787                _ => {
6788                    last = self.exec_statement(stmt)?;
6789                }
6790            }
6791        }
6792        Ok(last)
6793    }
6794
6795    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
6796    pub(crate) fn exec_given_with_topic_value(
6797        &mut self,
6798        topic: StrykeValue,
6799        body: &Block,
6800    ) -> ExecResult {
6801        self.scope_push_hook();
6802        self.scope.declare_scalar("_", topic);
6803        self.english_note_lexical_scalar("_");
6804        let r = self.exec_given_body(body);
6805        self.scope_pop_hook();
6806        r
6807    }
6808
6809    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
6810        let t = self.eval_expr(topic)?;
6811        self.exec_given_with_topic_value(t, body)
6812    }
6813
6814    /// `when (COND)` — topic is `$_` (set by `given`).
6815    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6816        let topic = self.scope.get_scalar("_");
6817        let line = cond.line;
6818        match &cond.kind {
6819            ExprKind::Regex(pattern, flags) => {
6820                let re = self.compile_regex(pattern, flags, line)?;
6821                let s = topic.to_string();
6822                Ok(re.is_match(&s))
6823            }
6824            ExprKind::String(s) => Ok(topic.to_string() == *s),
6825            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
6826            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
6827            _ => {
6828                let c = self.eval_expr(cond)?;
6829                Ok(self.smartmatch_when(&topic, &c))
6830            }
6831        }
6832    }
6833
6834    fn smartmatch_when(&self, topic: &StrykeValue, c: &StrykeValue) -> bool {
6835        if let Some(re) = c.as_regex() {
6836            return re.is_match(&topic.to_string());
6837        }
6838        // ARRAY / array-ref RHS: smartmatch is "any element matches the topic"
6839        // (`$x ~~ @list` → `grep { $x ~~ $_ } @list` per `perlop`).
6840        // Without this branch, `when ([2, 3, 5, 7])` always falls through to
6841        // `default` because the array stringified ("23 5 7"-ish) won't equal
6842        // the scalar. Recurse so nested arrays / regexes inside the array
6843        // still work.
6844        if let Some(arr) = c.as_array_ref() {
6845            let arr = arr.read();
6846            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
6847        }
6848        if let Some(arr) = c.as_array_vec() {
6849            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
6850        }
6851        // HASH / hash-ref RHS: "topic is a key" (`$x ~~ %h` → `exists $h{$x}`).
6852        if let Some(href) = c.as_hash_ref() {
6853            return href.read().contains_key(&topic.to_string());
6854        }
6855        if let Some(h) = c.as_hash_map() {
6856            return h.contains_key(&topic.to_string());
6857        }
6858        // Coderef RHS: call it with the topic, treat truthy result as match.
6859        if let Some(sub) = c.as_code_ref() {
6860            // smartmatch_when is `&self`; we can't `call_sub` (needs `&mut`).
6861            // For now, fall through to string equality. Future: hoist
6862            // when_matches to use `&mut self` so coderef RHS can fire.
6863            let _ = sub;
6864        }
6865        // Numeric equality if both sides parse as numbers.
6866        if let (Some(a), Some(b)) = (topic.as_integer(), c.as_integer()) {
6867            return a == b;
6868        }
6869        topic.to_string() == c.to_string()
6870    }
6871
6872    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
6873    pub(crate) fn eval_boolean_rvalue_condition(
6874        &mut self,
6875        cond: &Expr,
6876    ) -> Result<bool, FlowOrError> {
6877        match &cond.kind {
6878            ExprKind::Regex(pattern, flags) => {
6879                let topic = self.scope.get_scalar("_");
6880                let line = cond.line;
6881                let s = topic.to_string();
6882                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
6883                Ok(v.is_true())
6884            }
6885            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
6886            ExprKind::ReadLine(_) => {
6887                let v = self.eval_expr(cond)?;
6888                self.scope.set_topic(v.clone());
6889                Ok(!v.is_undef())
6890            }
6891            _ => {
6892                let v = self.eval_expr(cond)?;
6893                Ok(v.is_true())
6894            }
6895        }
6896    }
6897
6898    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
6899    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6900        self.eval_boolean_rvalue_condition(cond)
6901    }
6902
6903    pub(crate) fn eval_algebraic_match(
6904        &mut self,
6905        subject: &Expr,
6906        arms: &[MatchArm],
6907        line: usize,
6908    ) -> ExecResult {
6909        let val = self.eval_algebraic_match_subject(subject, line)?;
6910        self.eval_algebraic_match_with_subject_value(val, arms, line)
6911    }
6912
6913    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
6914    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
6915        match &subject.kind {
6916            ExprKind::ArrayVar(name) => {
6917                self.check_strict_array_var(name, line)?;
6918                let aname = self.stash_array_name_for_package(name);
6919                Ok(StrykeValue::array_binding_ref(aname))
6920            }
6921            ExprKind::HashVar(name) => {
6922                self.check_strict_hash_var(name, line)?;
6923                self.touch_env_hash(name);
6924                Ok(StrykeValue::hash_binding_ref(name.clone()))
6925            }
6926            _ => self.eval_expr(subject),
6927        }
6928    }
6929
6930    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
6931    pub(crate) fn eval_algebraic_match_with_subject_value(
6932        &mut self,
6933        val: StrykeValue,
6934        arms: &[MatchArm],
6935        line: usize,
6936    ) -> ExecResult {
6937        // Exhaustive enum match: check variant coverage before matching
6938        if let Some(e) = val.as_enum_inst() {
6939            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
6940            if !has_catchall {
6941                let covered: Vec<String> = arms
6942                    .iter()
6943                    .filter_map(|a| {
6944                        if let MatchPattern::Value(expr) = &a.pattern {
6945                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
6946                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
6947                            }
6948                        }
6949                        None
6950                    })
6951                    .collect();
6952                let missing: Vec<&str> = e
6953                    .def
6954                    .variants
6955                    .iter()
6956                    .filter(|v| !covered.contains(&v.name))
6957                    .map(|v| v.name.as_str())
6958                    .collect();
6959                if !missing.is_empty() {
6960                    return Err(StrykeError::runtime(
6961                        format!(
6962                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
6963                            e.def.name,
6964                            missing.join(", ")
6965                        ),
6966                        line,
6967                    )
6968                    .into());
6969                }
6970            }
6971        }
6972        for arm in arms {
6973            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
6974                let re = self.compile_regex(pattern, flags, line)?;
6975                let s = val.to_string();
6976                if let Some(caps) = re.captures(&s) {
6977                    self.scope_push_hook();
6978                    self.scope.declare_scalar("_", val.clone());
6979                    self.english_note_lexical_scalar("_");
6980                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
6981                    let guard_ok = if let Some(g) = &arm.guard {
6982                        self.eval_expr(g)?.is_true()
6983                    } else {
6984                        true
6985                    };
6986                    if !guard_ok {
6987                        self.scope_pop_hook();
6988                        continue;
6989                    }
6990                    let out = self.eval_expr(&arm.body);
6991                    self.scope_pop_hook();
6992                    return out;
6993                }
6994                continue;
6995            }
6996            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
6997                self.scope_push_hook();
6998                self.scope.declare_scalar("_", val.clone());
6999                self.english_note_lexical_scalar("_");
7000                for b in bindings {
7001                    match b {
7002                        PatternBinding::Scalar(name, v) => {
7003                            self.scope.declare_scalar(&name, v);
7004                            self.english_note_lexical_scalar(&name);
7005                        }
7006                        PatternBinding::Array(name, elems) => {
7007                            self.scope.declare_array(&name, elems);
7008                        }
7009                    }
7010                }
7011                let guard_ok = if let Some(g) = &arm.guard {
7012                    self.eval_expr(g)?.is_true()
7013                } else {
7014                    true
7015                };
7016                if !guard_ok {
7017                    self.scope_pop_hook();
7018                    continue;
7019                }
7020                let out = self.eval_expr(&arm.body);
7021                self.scope_pop_hook();
7022                return out;
7023            }
7024        }
7025        Err(StrykeError::runtime(
7026            "match: no arm matched the value (add a `_` catch-all)",
7027            line,
7028        )
7029        .into())
7030    }
7031
7032    fn parse_duration_seconds(pv: &StrykeValue) -> Option<f64> {
7033        let s = pv.to_string();
7034        let s = s.trim();
7035        if let Some(rest) = s.strip_suffix("ms") {
7036            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
7037        }
7038        if let Some(rest) = s.strip_suffix('s') {
7039            return rest.trim().parse::<f64>().ok();
7040        }
7041        if let Some(rest) = s.strip_suffix('m') {
7042            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
7043        }
7044        s.parse::<f64>().ok()
7045    }
7046
7047    fn eval_retry_block(
7048        &mut self,
7049        body: &Block,
7050        times: &Expr,
7051        backoff: RetryBackoff,
7052        _line: usize,
7053    ) -> ExecResult {
7054        let max = self.eval_expr(times)?.to_int().max(1) as usize;
7055        let base_ms: u64 = 10;
7056        let mut attempt = 0usize;
7057        loop {
7058            attempt += 1;
7059            match self.exec_block(body) {
7060                Ok(v) => return Ok(v),
7061                Err(FlowOrError::Error(e)) => {
7062                    if attempt >= max {
7063                        return Err(FlowOrError::Error(e));
7064                    }
7065                    let delay_ms = match backoff {
7066                        RetryBackoff::None => 0,
7067                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
7068                        RetryBackoff::Exponential => {
7069                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
7070                        }
7071                    };
7072                    if delay_ms > 0 {
7073                        std::thread::sleep(Duration::from_millis(delay_ms));
7074                    }
7075                }
7076                Err(e) => return Err(e),
7077            }
7078        }
7079    }
7080
7081    fn eval_rate_limit_block(
7082        &mut self,
7083        slot: u32,
7084        max: &Expr,
7085        window: &Expr,
7086        body: &Block,
7087        _line: usize,
7088    ) -> ExecResult {
7089        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
7090        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
7091            .filter(|s| *s > 0.0)
7092            .unwrap_or(1.0);
7093        let window_d = Duration::from_secs_f64(window_sec);
7094        let slot = slot as usize;
7095        while self.rate_limit_slots.len() <= slot {
7096            self.rate_limit_slots.push(VecDeque::new());
7097        }
7098        {
7099            let dq = &mut self.rate_limit_slots[slot];
7100            loop {
7101                let now = Instant::now();
7102                while let Some(t0) = dq.front().copied() {
7103                    if now.duration_since(t0) >= window_d {
7104                        dq.pop_front();
7105                    } else {
7106                        break;
7107                    }
7108                }
7109                if dq.len() < max_n || max_n == 0 {
7110                    break;
7111                }
7112                let t0 = dq.front().copied().unwrap();
7113                let wait = window_d.saturating_sub(now.duration_since(t0));
7114                if wait.is_zero() {
7115                    dq.pop_front();
7116                    continue;
7117                }
7118                std::thread::sleep(wait);
7119            }
7120            dq.push_back(Instant::now());
7121        }
7122        self.exec_block(body)
7123    }
7124
7125    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
7126        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
7127            .filter(|s| *s > 0.0)
7128            .unwrap_or(1.0);
7129        loop {
7130            match self.exec_block(body) {
7131                Ok(_) => {}
7132                Err(e) => return Err(e),
7133            }
7134            std::thread::sleep(Duration::from_secs_f64(sec));
7135        }
7136    }
7137
7138    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
7139    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> StrykeResult<StrykeValue> {
7140        let pair = |value: StrykeValue, more: i64| {
7141            StrykeValue::array_ref(Arc::new(RwLock::new(vec![
7142                value,
7143                StrykeValue::integer(more),
7144            ])))
7145        };
7146        let mut exhausted = gen.exhausted.lock();
7147        if *exhausted {
7148            return Ok(pair(StrykeValue::UNDEF, 0));
7149        }
7150        let mut pc = gen.pc.lock();
7151        let mut scope_started = gen.scope_started.lock();
7152        if *pc >= gen.block.len() {
7153            if *scope_started {
7154                self.scope_pop_hook();
7155                *scope_started = false;
7156            }
7157            *exhausted = true;
7158            return Ok(pair(StrykeValue::UNDEF, 0));
7159        }
7160        if !*scope_started {
7161            self.scope_push_hook();
7162            *scope_started = true;
7163        }
7164        self.in_generator = true;
7165        while *pc < gen.block.len() {
7166            let stmt = &gen.block[*pc];
7167            match self.exec_statement(stmt) {
7168                Ok(_) => {
7169                    *pc += 1;
7170                }
7171                Err(FlowOrError::Flow(Flow::Yield(v))) => {
7172                    *pc += 1;
7173                    self.in_generator = false;
7174                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
7175                    // binds in the caller block, not inside a frame left across yield.
7176                    if *scope_started {
7177                        self.scope_pop_hook();
7178                        *scope_started = false;
7179                    }
7180                    return Ok(pair(v, 1));
7181                }
7182                Err(e) => {
7183                    self.in_generator = false;
7184                    if *scope_started {
7185                        self.scope_pop_hook();
7186                        *scope_started = false;
7187                    }
7188                    return Err(match e {
7189                        FlowOrError::Error(ee) => ee,
7190                        FlowOrError::Flow(Flow::Yield(_)) => {
7191                            unreachable!("yield handled above")
7192                        }
7193                        FlowOrError::Flow(flow) => StrykeError::runtime(
7194                            format!("unexpected control flow in generator: {:?}", flow),
7195                            0,
7196                        ),
7197                    });
7198                }
7199            }
7200        }
7201        self.in_generator = false;
7202        if *scope_started {
7203            self.scope_pop_hook();
7204            *scope_started = false;
7205        }
7206        *exhausted = true;
7207        Ok(pair(StrykeValue::UNDEF, 0))
7208    }
7209
7210    fn match_pattern_try(
7211        &mut self,
7212        subject: &StrykeValue,
7213        pattern: &MatchPattern,
7214        line: usize,
7215    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7216        match pattern {
7217            MatchPattern::Any => Ok(Some(vec![])),
7218            MatchPattern::Regex { .. } => {
7219                unreachable!("regex arms are handled in eval_algebraic_match")
7220            }
7221            MatchPattern::Value(expr) => {
7222                if self.match_pattern_value_alternation(subject, expr, line)? {
7223                    Ok(Some(vec![]))
7224                } else {
7225                    Ok(None)
7226                }
7227            }
7228            MatchPattern::Array(elems) => {
7229                let Some(arr) = self.match_subject_as_array(subject) else {
7230                    return Ok(None);
7231                };
7232                self.match_array_pattern_elems(&arr, elems, line)
7233            }
7234            MatchPattern::Hash(pairs) => {
7235                let Some(h) = self.match_subject_as_hash(subject) else {
7236                    return Ok(None);
7237                };
7238                self.match_hash_pattern_pairs(&h, pairs, line)
7239            }
7240            MatchPattern::OptionSome(name) => {
7241                let Some(arr) = self.match_subject_as_array(subject) else {
7242                    return Ok(None);
7243                };
7244                if arr.len() < 2 {
7245                    return Ok(None);
7246                }
7247                if !arr[1].is_true() {
7248                    return Ok(None);
7249                }
7250                Ok(Some(vec![PatternBinding::Scalar(
7251                    name.clone(),
7252                    arr[0].clone(),
7253                )]))
7254            }
7255        }
7256    }
7257
7258    /// Handle pattern alternation (e.g., `"foo" | "bar" | "baz"`) in match patterns.
7259    /// If the expression is a BitOr chain, recursively check if subject matches any alternative.
7260    fn match_pattern_value_alternation(
7261        &mut self,
7262        subject: &StrykeValue,
7263        expr: &Expr,
7264        _line: usize,
7265    ) -> Result<bool, FlowOrError> {
7266        if let ExprKind::BinOp {
7267            left,
7268            op: BinOp::BitOr,
7269            right,
7270        } = &expr.kind
7271        {
7272            if self.match_pattern_value_alternation(subject, left, _line)? {
7273                return Ok(true);
7274            }
7275            return self.match_pattern_value_alternation(subject, right, _line);
7276        }
7277        let pv = self.eval_expr(expr)?;
7278        Ok(self.smartmatch_when(subject, &pv))
7279    }
7280
7281    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
7282    fn match_subject_as_array(&self, v: &StrykeValue) -> Option<Vec<StrykeValue>> {
7283        if let Some(a) = v.as_array_vec() {
7284            return Some(a);
7285        }
7286        if let Some(r) = v.as_array_ref() {
7287            return Some(r.read().clone());
7288        }
7289        if let Some(name) = v.as_array_binding_name() {
7290            return Some(self.scope.get_array(&name));
7291        }
7292        None
7293    }
7294
7295    fn match_subject_as_hash(&mut self, v: &StrykeValue) -> Option<IndexMap<String, StrykeValue>> {
7296        if let Some(h) = v.as_hash_map() {
7297            return Some(h);
7298        }
7299        if let Some(r) = v.as_hash_ref() {
7300            return Some(r.read().clone());
7301        }
7302        if let Some(name) = v.as_hash_binding_name() {
7303            self.touch_env_hash(&name);
7304            return Some(self.scope.get_hash(&name));
7305        }
7306        None
7307    }
7308
7309    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
7310    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
7311    pub(crate) fn hash_slice_deref_values(
7312        &mut self,
7313        container: &StrykeValue,
7314        key_values: &[StrykeValue],
7315        line: usize,
7316    ) -> Result<StrykeValue, FlowOrError> {
7317        let h = if let Some(m) = self.match_subject_as_hash(container) {
7318            m
7319        } else {
7320            return Err(StrykeError::runtime(
7321                "Hash slice dereference needs a hash or hash reference value",
7322                line,
7323            )
7324            .into());
7325        };
7326        let mut result = Vec::new();
7327        for kv in key_values {
7328            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
7329                vv.iter().map(|x| x.to_string()).collect()
7330            } else {
7331                vec![kv.to_string()]
7332            };
7333            for k in key_strings {
7334                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
7335            }
7336        }
7337        Ok(StrykeValue::array(result))
7338    }
7339
7340    /// Single-key write for a hash slice container (hash ref or package hash name).
7341    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
7342    pub(crate) fn assign_hash_slice_one_key(
7343        &mut self,
7344        container: StrykeValue,
7345        key: &str,
7346        val: StrykeValue,
7347        line: usize,
7348    ) -> Result<StrykeValue, FlowOrError> {
7349        if let Some(r) = container.as_hash_ref() {
7350            r.write().insert(key.to_string(), val);
7351            return Ok(StrykeValue::UNDEF);
7352        }
7353        if let Some(name) = container.as_hash_binding_name() {
7354            self.touch_env_hash(&name);
7355            self.scope
7356                .set_hash_element(&name, key, val)
7357                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7358            return Ok(StrykeValue::UNDEF);
7359        }
7360        if let Some(s) = container.as_str() {
7361            self.touch_env_hash(&s);
7362            if self.strict_refs {
7363                return Err(StrykeError::runtime(
7364                    format!(
7365                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7366                        s
7367                    ),
7368                    line,
7369                )
7370                .into());
7371            }
7372            self.scope
7373                .set_hash_element(&s, key, val)
7374                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7375            return Ok(StrykeValue::UNDEF);
7376        }
7377        Err(StrykeError::runtime(
7378            "Hash slice assignment needs a hash or hash reference value",
7379            line,
7380        )
7381        .into())
7382    }
7383
7384    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
7385    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
7386    pub(crate) fn assign_named_hash_slice(
7387        &mut self,
7388        hash: &str,
7389        key_values: Vec<StrykeValue>,
7390        val: StrykeValue,
7391        line: usize,
7392    ) -> Result<StrykeValue, FlowOrError> {
7393        self.touch_env_hash(hash);
7394        let mut ks: Vec<String> = Vec::new();
7395        for kv in key_values {
7396            if let Some(vv) = kv.as_array_vec() {
7397                ks.extend(vv.iter().map(|x| x.to_string()));
7398            } else {
7399                ks.push(kv.to_string());
7400            }
7401        }
7402        if ks.is_empty() {
7403            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7404        }
7405        let items = val.to_list();
7406        for (i, k) in ks.iter().enumerate() {
7407            let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7408            self.scope
7409                .set_hash_element(hash, k, v)
7410                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7411        }
7412        Ok(StrykeValue::UNDEF)
7413    }
7414
7415    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
7416    pub(crate) fn assign_hash_slice_deref(
7417        &mut self,
7418        container: StrykeValue,
7419        key_values: Vec<StrykeValue>,
7420        val: StrykeValue,
7421        line: usize,
7422    ) -> Result<StrykeValue, FlowOrError> {
7423        let mut ks: Vec<String> = Vec::new();
7424        for kv in key_values {
7425            if let Some(vv) = kv.as_array_vec() {
7426                ks.extend(vv.iter().map(|x| x.to_string()));
7427            } else {
7428                ks.push(kv.to_string());
7429            }
7430        }
7431        if ks.is_empty() {
7432            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7433        }
7434        let items = val.to_list();
7435        if let Some(r) = container.as_hash_ref() {
7436            let mut h = r.write();
7437            for (i, k) in ks.iter().enumerate() {
7438                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7439                h.insert(k.clone(), v);
7440            }
7441            return Ok(StrykeValue::UNDEF);
7442        }
7443        if let Some(name) = container.as_hash_binding_name() {
7444            self.touch_env_hash(&name);
7445            for (i, k) in ks.iter().enumerate() {
7446                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7447                self.scope
7448                    .set_hash_element(&name, k, v)
7449                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7450            }
7451            return Ok(StrykeValue::UNDEF);
7452        }
7453        if let Some(s) = container.as_str() {
7454            if self.strict_refs {
7455                return Err(StrykeError::runtime(
7456                    format!(
7457                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7458                        s
7459                    ),
7460                    line,
7461                )
7462                .into());
7463            }
7464            self.touch_env_hash(&s);
7465            for (i, k) in ks.iter().enumerate() {
7466                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7467                self.scope
7468                    .set_hash_element(&s, k, v)
7469                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7470            }
7471            return Ok(StrykeValue::UNDEF);
7472        }
7473        Err(StrykeError::runtime(
7474            "Hash slice assignment needs a hash or hash reference value",
7475            line,
7476        )
7477        .into())
7478    }
7479
7480    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
7481    /// Perl 5 applies the compound op only to the **last** slice element.
7482    pub(crate) fn compound_assign_hash_slice_deref(
7483        &mut self,
7484        container: StrykeValue,
7485        key_values: Vec<StrykeValue>,
7486        op: BinOp,
7487        rhs: StrykeValue,
7488        line: usize,
7489    ) -> Result<StrykeValue, FlowOrError> {
7490        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7491        let last_old = old_list
7492            .to_list()
7493            .last()
7494            .cloned()
7495            .unwrap_or(StrykeValue::UNDEF);
7496        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7497        let mut ks: Vec<String> = Vec::new();
7498        for kv in &key_values {
7499            if let Some(vv) = kv.as_array_vec() {
7500                ks.extend(vv.iter().map(|x| x.to_string()));
7501            } else {
7502                ks.push(kv.to_string());
7503            }
7504        }
7505        if ks.is_empty() {
7506            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7507        }
7508        let last_key = ks.last().expect("non-empty ks");
7509        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7510        Ok(new_val)
7511    }
7512
7513    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
7514    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
7515    /// the **old** value of that last element.
7516    ///
7517    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
7518    pub(crate) fn hash_slice_deref_inc_dec(
7519        &mut self,
7520        container: StrykeValue,
7521        key_values: Vec<StrykeValue>,
7522        kind: u8,
7523        line: usize,
7524    ) -> Result<StrykeValue, FlowOrError> {
7525        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7526        let last_old = old_list
7527            .to_list()
7528            .last()
7529            .cloned()
7530            .unwrap_or(StrykeValue::UNDEF);
7531        let new_val = if kind & 1 == 0 {
7532            StrykeValue::integer(last_old.to_int() + 1)
7533        } else {
7534            StrykeValue::integer(last_old.to_int() - 1)
7535        };
7536        let mut ks: Vec<String> = Vec::new();
7537        for kv in &key_values {
7538            if let Some(vv) = kv.as_array_vec() {
7539                ks.extend(vv.iter().map(|x| x.to_string()));
7540            } else {
7541                ks.push(kv.to_string());
7542            }
7543        }
7544        let last_key = ks.last().ok_or_else(|| {
7545            StrykeError::runtime("Hash slice increment needs at least one key", line)
7546        })?;
7547        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7548        Ok(if kind < 2 { new_val } else { last_old })
7549    }
7550
7551    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[StrykeValue]) -> StrykeValue {
7552        self.touch_env_hash(hash);
7553        let h = self.scope.get_hash(hash);
7554        let mut result = Vec::new();
7555        for kv in key_values {
7556            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
7557                vv.iter().map(|x| x.to_string()).collect()
7558            } else {
7559                vec![kv.to_string()]
7560            };
7561            for k in key_strings {
7562                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
7563            }
7564        }
7565        StrykeValue::array(result)
7566    }
7567
7568    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
7569    pub(crate) fn compound_assign_named_hash_slice(
7570        &mut self,
7571        hash: &str,
7572        key_values: Vec<StrykeValue>,
7573        op: BinOp,
7574        rhs: StrykeValue,
7575        line: usize,
7576    ) -> Result<StrykeValue, FlowOrError> {
7577        let old_list = self.hash_slice_named_values(hash, &key_values);
7578        let last_old = old_list
7579            .to_list()
7580            .last()
7581            .cloned()
7582            .unwrap_or(StrykeValue::UNDEF);
7583        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7584        let mut ks: Vec<String> = Vec::new();
7585        for kv in &key_values {
7586            if let Some(vv) = kv.as_array_vec() {
7587                ks.extend(vv.iter().map(|x| x.to_string()));
7588            } else {
7589                ks.push(kv.to_string());
7590            }
7591        }
7592        if ks.is_empty() {
7593            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7594        }
7595        let last_key = ks.last().expect("non-empty ks");
7596        let container = StrykeValue::string(hash.to_string());
7597        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7598        Ok(new_val)
7599    }
7600
7601    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
7602    pub(crate) fn named_hash_slice_inc_dec(
7603        &mut self,
7604        hash: &str,
7605        key_values: Vec<StrykeValue>,
7606        kind: u8,
7607        line: usize,
7608    ) -> Result<StrykeValue, FlowOrError> {
7609        let old_list = self.hash_slice_named_values(hash, &key_values);
7610        let last_old = old_list
7611            .to_list()
7612            .last()
7613            .cloned()
7614            .unwrap_or(StrykeValue::UNDEF);
7615        let new_val = if kind & 1 == 0 {
7616            StrykeValue::integer(last_old.to_int() + 1)
7617        } else {
7618            StrykeValue::integer(last_old.to_int() - 1)
7619        };
7620        let mut ks: Vec<String> = Vec::new();
7621        for kv in &key_values {
7622            if let Some(vv) = kv.as_array_vec() {
7623                ks.extend(vv.iter().map(|x| x.to_string()));
7624            } else {
7625                ks.push(kv.to_string());
7626            }
7627        }
7628        let last_key = ks.last().ok_or_else(|| {
7629            StrykeError::runtime("Hash slice increment needs at least one key", line)
7630        })?;
7631        let container = StrykeValue::string(hash.to_string());
7632        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7633        Ok(if kind < 2 { new_val } else { last_old })
7634    }
7635
7636    fn match_array_pattern_elems(
7637        &mut self,
7638        arr: &[StrykeValue],
7639        elems: &[MatchArrayElem],
7640        line: usize,
7641    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7642        let has_rest = elems
7643            .iter()
7644            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
7645        let mut binds: Vec<PatternBinding> = Vec::new();
7646        let mut idx = 0usize;
7647        for (i, elem) in elems.iter().enumerate() {
7648            match elem {
7649                MatchArrayElem::Rest => {
7650                    if i != elems.len() - 1 {
7651                        return Err(StrykeError::runtime(
7652                            "internal: `*` must be last in array match pattern",
7653                            line,
7654                        )
7655                        .into());
7656                    }
7657                    return Ok(Some(binds));
7658                }
7659                MatchArrayElem::RestBind(name) => {
7660                    if i != elems.len() - 1 {
7661                        return Err(StrykeError::runtime(
7662                            "internal: `@name` rest bind must be last in array match pattern",
7663                            line,
7664                        )
7665                        .into());
7666                    }
7667                    let tail = arr[idx..].to_vec();
7668                    binds.push(PatternBinding::Array(name.clone(), tail));
7669                    return Ok(Some(binds));
7670                }
7671                MatchArrayElem::CaptureScalar(name) => {
7672                    if idx >= arr.len() {
7673                        return Ok(None);
7674                    }
7675                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
7676                    idx += 1;
7677                }
7678                MatchArrayElem::Expr(e) => {
7679                    if idx >= arr.len() {
7680                        return Ok(None);
7681                    }
7682                    let expected = self.eval_expr(e)?;
7683                    if !self.smartmatch_when(&arr[idx], &expected) {
7684                        return Ok(None);
7685                    }
7686                    idx += 1;
7687                }
7688            }
7689        }
7690        if !has_rest && idx != arr.len() {
7691            return Ok(None);
7692        }
7693        Ok(Some(binds))
7694    }
7695
7696    fn match_hash_pattern_pairs(
7697        &mut self,
7698        h: &IndexMap<String, StrykeValue>,
7699        pairs: &[MatchHashPair],
7700        _line: usize,
7701    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7702        let mut binds = Vec::new();
7703        for pair in pairs {
7704            match pair {
7705                MatchHashPair::KeyOnly { key } => {
7706                    let ks = self.eval_expr(key)?.to_string();
7707                    if !h.contains_key(&ks) {
7708                        return Ok(None);
7709                    }
7710                }
7711                MatchHashPair::Capture { key, name } => {
7712                    let ks = self.eval_expr(key)?.to_string();
7713                    let Some(v) = h.get(&ks) else {
7714                        return Ok(None);
7715                    };
7716                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
7717                }
7718            }
7719        }
7720        Ok(Some(binds))
7721    }
7722
7723    /// Check if a block declares variables (needs its own scope frame).
7724    #[inline]
7725    fn block_needs_scope(block: &Block) -> bool {
7726        block.iter().any(|s| match &s.kind {
7727            StmtKind::My(_)
7728            | StmtKind::Our(_)
7729            | StmtKind::Local(_)
7730            | StmtKind::State(_)
7731            | StmtKind::LocalExpr { .. } => true,
7732            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
7733            _ => false,
7734        })
7735    }
7736
7737    /// Execute block, only pushing a scope frame if needed.
7738    #[inline]
7739    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
7740        if Self::block_needs_scope(block) {
7741            self.exec_block(block)
7742        } else {
7743            self.exec_block_no_scope(block)
7744        }
7745    }
7746
7747    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
7748        let t0 = self.profiler.is_some().then(std::time::Instant::now);
7749        let r = self.exec_statement_inner(stmt);
7750        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
7751            prof.on_line(&self.file, stmt.line, t0.elapsed());
7752        }
7753        r
7754    }
7755
7756    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
7757        if let Err(e) = crate::perl_signal::poll(self) {
7758            return Err(FlowOrError::Error(e));
7759        }
7760        if let Err(e) = self.drain_pending_destroys(stmt.line) {
7761            return Err(FlowOrError::Error(e));
7762        }
7763        match &stmt.kind {
7764            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
7765            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
7766            StmtKind::If {
7767                condition,
7768                body,
7769                elsifs,
7770                else_block,
7771            } => {
7772                if self.eval_boolean_rvalue_condition(condition)? {
7773                    return self.exec_block(body);
7774                }
7775                for (c, b) in elsifs {
7776                    if self.eval_boolean_rvalue_condition(c)? {
7777                        return self.exec_block(b);
7778                    }
7779                }
7780                if let Some(eb) = else_block {
7781                    return self.exec_block(eb);
7782                }
7783                Ok(StrykeValue::UNDEF)
7784            }
7785            StmtKind::Unless {
7786                condition,
7787                body,
7788                else_block,
7789            } => {
7790                if !self.eval_boolean_rvalue_condition(condition)? {
7791                    return self.exec_block(body);
7792                }
7793                if let Some(eb) = else_block {
7794                    return self.exec_block(eb);
7795                }
7796                Ok(StrykeValue::UNDEF)
7797            }
7798            StmtKind::While {
7799                condition,
7800                body,
7801                label,
7802                continue_block,
7803            } => {
7804                'outer: loop {
7805                    if !self.eval_boolean_rvalue_condition(condition)? {
7806                        break;
7807                    }
7808                    'inner: loop {
7809                        match self.exec_block_smart(body) {
7810                            Ok(_) => break 'inner,
7811                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7812                                if l == label || l.is_none() =>
7813                            {
7814                                break 'outer;
7815                            }
7816                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7817                                if l == label || l.is_none() =>
7818                            {
7819                                if let Some(cb) = continue_block {
7820                                    let _ = self.exec_block_smart(cb);
7821                                }
7822                                continue 'outer;
7823                            }
7824                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7825                                if l == label || l.is_none() =>
7826                            {
7827                                continue 'inner;
7828                            }
7829                            Err(e) => return Err(e),
7830                        }
7831                    }
7832                    if let Some(cb) = continue_block {
7833                        let _ = self.exec_block_smart(cb);
7834                    }
7835                }
7836                Ok(StrykeValue::UNDEF)
7837            }
7838            StmtKind::Until {
7839                condition,
7840                body,
7841                label,
7842                continue_block,
7843            } => {
7844                'outer: loop {
7845                    if self.eval_boolean_rvalue_condition(condition)? {
7846                        break;
7847                    }
7848                    'inner: loop {
7849                        match self.exec_block(body) {
7850                            Ok(_) => break 'inner,
7851                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7852                                if l == label || l.is_none() =>
7853                            {
7854                                break 'outer;
7855                            }
7856                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7857                                if l == label || l.is_none() =>
7858                            {
7859                                if let Some(cb) = continue_block {
7860                                    let _ = self.exec_block_smart(cb);
7861                                }
7862                                continue 'outer;
7863                            }
7864                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7865                                if l == label || l.is_none() =>
7866                            {
7867                                continue 'inner;
7868                            }
7869                            Err(e) => return Err(e),
7870                        }
7871                    }
7872                    if let Some(cb) = continue_block {
7873                        let _ = self.exec_block_smart(cb);
7874                    }
7875                }
7876                Ok(StrykeValue::UNDEF)
7877            }
7878            StmtKind::DoWhile { body, condition } => {
7879                loop {
7880                    self.exec_block(body)?;
7881                    if !self.eval_boolean_rvalue_condition(condition)? {
7882                        break;
7883                    }
7884                }
7885                Ok(StrykeValue::UNDEF)
7886            }
7887            StmtKind::For {
7888                init,
7889                condition,
7890                step,
7891                body,
7892                label,
7893                continue_block,
7894            } => {
7895                self.scope_push_hook();
7896                if let Some(init) = init {
7897                    self.exec_statement(init)?;
7898                }
7899                'outer: loop {
7900                    if let Some(cond) = condition {
7901                        if !self.eval_boolean_rvalue_condition(cond)? {
7902                            break;
7903                        }
7904                    }
7905                    'inner: loop {
7906                        match self.exec_block_smart(body) {
7907                            Ok(_) => break 'inner,
7908                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7909                                if l == label || l.is_none() =>
7910                            {
7911                                break 'outer;
7912                            }
7913                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7914                                if l == label || l.is_none() =>
7915                            {
7916                                if let Some(cb) = continue_block {
7917                                    let _ = self.exec_block_smart(cb);
7918                                }
7919                                if let Some(step) = step {
7920                                    self.eval_expr(step)?;
7921                                }
7922                                continue 'outer;
7923                            }
7924                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7925                                if l == label || l.is_none() =>
7926                            {
7927                                continue 'inner;
7928                            }
7929                            Err(e) => {
7930                                self.scope_pop_hook();
7931                                return Err(e);
7932                            }
7933                        }
7934                    }
7935                    if let Some(cb) = continue_block {
7936                        let _ = self.exec_block_smart(cb);
7937                    }
7938                    if let Some(step) = step {
7939                        self.eval_expr(step)?;
7940                    }
7941                }
7942                self.scope_pop_hook();
7943                Ok(StrykeValue::UNDEF)
7944            }
7945            StmtKind::Foreach {
7946                var,
7947                list,
7948                body,
7949                label,
7950                continue_block,
7951            } => {
7952                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
7953                let items = list_val.to_list();
7954                self.scope_push_hook();
7955                self.scope.declare_scalar(var, StrykeValue::UNDEF);
7956                self.english_note_lexical_scalar(var);
7957                let mut i = 0usize;
7958                'outer: while i < items.len() {
7959                    // For the implicit topic loop (`for (@list) { ... }`,
7960                    // var=="_"), use `set_topic` so `$_`, `$_0`, `_`, `_0`
7961                    // all alias the iter value. Plain `set_scalar("_", ...)`
7962                    // only updates `$_` and leaves `$_0` undef, violating
7963                    // the four-way aliasing invariant. Explicit named loops
7964                    // (`for my $x (@list)`) keep the simple scalar binding.
7965                    if var == "_" {
7966                        self.scope.set_topic(items[i].clone());
7967                    } else {
7968                        self.scope
7969                            .set_scalar(var, items[i].clone())
7970                            .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
7971                    }
7972                    'inner: loop {
7973                        match self.exec_block_smart(body) {
7974                            Ok(_) => break 'inner,
7975                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7976                                if l == label || l.is_none() =>
7977                            {
7978                                break 'outer;
7979                            }
7980                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7981                                if l == label || l.is_none() =>
7982                            {
7983                                if let Some(cb) = continue_block {
7984                                    let _ = self.exec_block_smart(cb);
7985                                }
7986                                i += 1;
7987                                continue 'outer;
7988                            }
7989                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7990                                if l == label || l.is_none() =>
7991                            {
7992                                continue 'inner;
7993                            }
7994                            Err(e) => {
7995                                self.scope_pop_hook();
7996                                return Err(e);
7997                            }
7998                        }
7999                    }
8000                    if let Some(cb) = continue_block {
8001                        let _ = self.exec_block_smart(cb);
8002                    }
8003                    i += 1;
8004                }
8005                self.scope_pop_hook();
8006                Ok(StrykeValue::UNDEF)
8007            }
8008            StmtKind::SubDecl {
8009                name,
8010                params,
8011                body,
8012                prototype,
8013            } => {
8014                let key = self.qualify_sub_key(name);
8015                let captured = self.scope.capture();
8016                let closure_env = if captured.is_empty() {
8017                    None
8018                } else {
8019                    Some(captured)
8020                };
8021                let mut sub = StrykeSub {
8022                    name: name.clone(),
8023                    params: params.clone(),
8024                    body: body.clone(),
8025                    closure_env,
8026                    prototype: prototype.clone(),
8027                    fib_like: None,
8028                };
8029                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
8030                self.subs.insert(key, Arc::new(sub));
8031                Ok(StrykeValue::UNDEF)
8032            }
8033            StmtKind::StructDecl { def } => {
8034                if self.struct_defs.contains_key(&def.name) {
8035                    return Err(StrykeError::runtime(
8036                        format!("duplicate struct `{}`", def.name),
8037                        stmt.line,
8038                    )
8039                    .into());
8040                }
8041                self.struct_defs
8042                    .insert(def.name.clone(), Arc::new(def.clone()));
8043                Ok(StrykeValue::UNDEF)
8044            }
8045            StmtKind::EnumDecl { def } => {
8046                if self.enum_defs.contains_key(&def.name) {
8047                    return Err(StrykeError::runtime(
8048                        format!("duplicate enum `{}`", def.name),
8049                        stmt.line,
8050                    )
8051                    .into());
8052                }
8053                self.enum_defs
8054                    .insert(def.name.clone(), Arc::new(def.clone()));
8055                Ok(StrykeValue::UNDEF)
8056            }
8057            StmtKind::ClassDecl { def } => {
8058                if self.class_defs.contains_key(&def.name) {
8059                    return Err(StrykeError::runtime(
8060                        format!("duplicate class `{}`", def.name),
8061                        stmt.line,
8062                    )
8063                    .into());
8064                }
8065                // Final class enforcement: prevent subclassing
8066                for parent_name in &def.extends {
8067                    if let Some(parent_def) = self.class_defs.get(parent_name) {
8068                        if parent_def.is_final {
8069                            return Err(StrykeError::runtime(
8070                                format!("cannot extend final class `{}`", parent_name),
8071                                stmt.line,
8072                            )
8073                            .into());
8074                        }
8075                        // Final method enforcement: prevent overriding
8076                        for m in &def.methods {
8077                            if let Some(parent_method) = parent_def.method(&m.name) {
8078                                if parent_method.is_final {
8079                                    return Err(StrykeError::runtime(
8080                                        format!(
8081                                            "cannot override final method `{}` from class `{}`",
8082                                            m.name, parent_name
8083                                        ),
8084                                        stmt.line,
8085                                    )
8086                                    .into());
8087                                }
8088                            }
8089                        }
8090                    }
8091                }
8092                // Trait contract enforcement + default method inheritance
8093                let mut def = def.clone();
8094                for trait_name in &def.implements.clone() {
8095                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
8096                        for required in trait_def.required_methods() {
8097                            let has_method = def.methods.iter().any(|m| m.name == required.name);
8098                            if !has_method {
8099                                return Err(StrykeError::runtime(
8100                                    format!(
8101                                        "class `{}` implements trait `{}` but does not define required method `{}`",
8102                                        def.name, trait_name, required.name
8103                                    ),
8104                                    stmt.line,
8105                                )
8106                                .into());
8107                            }
8108                        }
8109                        // Inherit default methods from trait (methods with bodies)
8110                        for tm in &trait_def.methods {
8111                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
8112                                def.methods.push(tm.clone());
8113                            }
8114                        }
8115                    }
8116                }
8117                // Abstract method enforcement: concrete subclasses must implement
8118                // all abstract methods (body-less methods) from abstract parents
8119                if !def.is_abstract {
8120                    for parent_name in &def.extends.clone() {
8121                        if let Some(parent_def) = self.class_defs.get(parent_name) {
8122                            if parent_def.is_abstract {
8123                                for m in &parent_def.methods {
8124                                    if m.body.is_none()
8125                                        && !def.methods.iter().any(|dm| dm.name == m.name)
8126                                    {
8127                                        return Err(StrykeError::runtime(
8128                                            format!(
8129                                                "class `{}` must implement abstract method `{}` from `{}`",
8130                                                def.name, m.name, parent_name
8131                                            ),
8132                                            stmt.line,
8133                                        )
8134                                        .into());
8135                                    }
8136                                }
8137                            }
8138                        }
8139                    }
8140                }
8141                // Initialize static fields
8142                for sf in &def.static_fields {
8143                    let val = if let Some(ref expr) = sf.default {
8144                        self.eval_expr(expr)?
8145                    } else {
8146                        StrykeValue::UNDEF
8147                    };
8148                    let key = format!("{}::{}", def.name, sf.name);
8149                    self.scope.declare_scalar(&key, val);
8150                }
8151                // Register class methods into self.subs so method dispatch finds them.
8152                for m in &def.methods {
8153                    if let Some(ref body) = m.body {
8154                        let fq = format!("{}::{}", def.name, m.name);
8155                        let sub = Arc::new(StrykeSub {
8156                            name: fq.clone(),
8157                            params: m.params.clone(),
8158                            body: body.clone(),
8159                            closure_env: None,
8160                            prototype: None,
8161                            fib_like: None,
8162                        });
8163                        self.subs.insert(fq, sub);
8164                    }
8165                }
8166                // Set @ClassName::ISA so MRO/isa resolution works.
8167                if !def.extends.is_empty() {
8168                    let isa_key = format!("{}::ISA", def.name);
8169                    let parents: Vec<StrykeValue> = def
8170                        .extends
8171                        .iter()
8172                        .map(|p| StrykeValue::string(p.clone()))
8173                        .collect();
8174                    self.scope.declare_array(&isa_key, parents);
8175                }
8176                let arc_def = Arc::new(def);
8177                self.class_defs
8178                    .insert(arc_def.name.clone(), Arc::clone(&arc_def));
8179                // Mirror the new class into the serializer-visible
8180                // thread-local registry so `to_json($obj)` etc. can walk
8181                // its inheritance chain.
8182                crate::serialize_normalize::register_class_def(arc_def);
8183                Ok(StrykeValue::UNDEF)
8184            }
8185            StmtKind::TraitDecl { def } => {
8186                if self.trait_defs.contains_key(&def.name) {
8187                    return Err(StrykeError::runtime(
8188                        format!("duplicate trait `{}`", def.name),
8189                        stmt.line,
8190                    )
8191                    .into());
8192                }
8193                self.trait_defs
8194                    .insert(def.name.clone(), Arc::new(def.clone()));
8195                Ok(StrykeValue::UNDEF)
8196            }
8197            StmtKind::My(decls) | StmtKind::Our(decls) => {
8198                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
8199                // For list assignment my ($a, $b) = (10, 20), distribute elements.
8200                // All decls share the same initializer in the AST (parser clones it).
8201                if decls.len() > 1 && decls[0].initializer.is_some() {
8202                    let val = self.eval_expr_ctx(
8203                        decls[0].initializer.as_ref().unwrap(),
8204                        WantarrayCtx::List,
8205                    )?;
8206                    let items = val.to_list();
8207                    let mut idx = 0;
8208                    for decl in decls {
8209                        match decl.sigil {
8210                            Sigil::Scalar => {
8211                                let v = items.get(idx).cloned().unwrap_or(StrykeValue::UNDEF);
8212                                let skey = if is_our {
8213                                    self.stash_scalar_name_for_package(&decl.name)
8214                                } else {
8215                                    decl.name.clone()
8216                                };
8217                                self.scope.declare_scalar_frozen(
8218                                    &skey,
8219                                    v,
8220                                    decl.frozen,
8221                                    decl.type_annotation.clone(),
8222                                )?;
8223                                self.english_note_lexical_scalar(&decl.name);
8224                                if is_our {
8225                                    self.note_our_scalar(&decl.name);
8226                                }
8227                                idx += 1;
8228                            }
8229                            Sigil::Array => {
8230                                // Array slurps remaining elements
8231                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8232                                idx = items.len();
8233                                if is_our {
8234                                    self.record_exporter_our_array_name(&decl.name, &rest);
8235                                }
8236                                let aname = if is_our {
8237                                    self.stash_array_full_name_for_package(&decl.name)
8238                                } else {
8239                                    self.stash_array_name_for_package(&decl.name)
8240                                };
8241                                self.scope.declare_array(&aname, rest);
8242                                if is_our {
8243                                    self.note_our_array(&decl.name);
8244                                }
8245                            }
8246                            Sigil::Hash => {
8247                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8248                                idx = items.len();
8249                                let mut map = IndexMap::new();
8250                                let mut i = 0;
8251                                while i + 1 < rest.len() {
8252                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
8253                                    i += 2;
8254                                }
8255                                let hname = if is_our {
8256                                    self.stash_hash_full_name_for_package(&decl.name)
8257                                } else {
8258                                    decl.name.clone()
8259                                };
8260                                self.scope.declare_hash(&hname, map);
8261                                if is_our {
8262                                    self.note_our_hash(&decl.name);
8263                                }
8264                            }
8265                            Sigil::Typeglob => {
8266                                return Err(StrykeError::runtime(
8267                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
8268                                    stmt.line,
8269                                )
8270                                .into());
8271                            }
8272                        }
8273                    }
8274                } else {
8275                    // Single decl or no initializer
8276                    for decl in decls {
8277                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
8278                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
8279                        // compound op reads the lhs (see system Exporter.pm).
8280                        let compound_init = decl
8281                            .initializer
8282                            .as_ref()
8283                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
8284
8285                        if compound_init {
8286                            match decl.sigil {
8287                                Sigil::Typeglob => {
8288                                    return Err(StrykeError::runtime(
8289                                        "compound assignment on typeglob declaration is not supported",
8290                                        stmt.line,
8291                                    )
8292                                    .into());
8293                                }
8294                                Sigil::Scalar => {
8295                                    let skey = if is_our {
8296                                        self.stash_scalar_name_for_package(&decl.name)
8297                                    } else {
8298                                        decl.name.clone()
8299                                    };
8300                                    self.scope.declare_scalar_frozen(
8301                                        &skey,
8302                                        StrykeValue::UNDEF,
8303                                        decl.frozen,
8304                                        decl.type_annotation.clone(),
8305                                    )?;
8306                                    self.english_note_lexical_scalar(&decl.name);
8307                                    if is_our {
8308                                        self.note_our_scalar(&decl.name);
8309                                    }
8310                                    let init = decl.initializer.as_ref().unwrap();
8311                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8312                                }
8313                                Sigil::Array => {
8314                                    let aname = if is_our {
8315                                        self.stash_array_full_name_for_package(&decl.name)
8316                                    } else {
8317                                        self.stash_array_name_for_package(&decl.name)
8318                                    };
8319                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
8320                                    if is_our {
8321                                        self.note_our_array(&decl.name);
8322                                    }
8323                                    let init = decl.initializer.as_ref().unwrap();
8324                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8325                                    if is_our {
8326                                        let items = self.scope.get_array(&aname);
8327                                        self.record_exporter_our_array_name(&decl.name, &items);
8328                                    }
8329                                }
8330                                Sigil::Hash => {
8331                                    let hname = if is_our {
8332                                        self.stash_hash_full_name_for_package(&decl.name)
8333                                    } else {
8334                                        decl.name.clone()
8335                                    };
8336                                    self.scope.declare_hash_frozen(
8337                                        &hname,
8338                                        IndexMap::new(),
8339                                        decl.frozen,
8340                                    );
8341                                    if is_our {
8342                                        self.note_our_hash(&decl.name);
8343                                    }
8344                                    let init = decl.initializer.as_ref().unwrap();
8345                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8346                                }
8347                            }
8348                            continue;
8349                        }
8350
8351                        let val = if let Some(init) = &decl.initializer {
8352                            let ctx = match decl.sigil {
8353                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
8354                                Sigil::Scalar if decl.list_context => WantarrayCtx::List,
8355                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
8356                            };
8357                            let v = self.eval_expr_ctx(init, ctx)?;
8358                            // my ($x) = @arr → extract first element from list
8359                            if decl.sigil == Sigil::Scalar && decl.list_context {
8360                                v.to_list().first().cloned().unwrap_or(StrykeValue::UNDEF)
8361                            } else {
8362                                v
8363                            }
8364                        } else {
8365                            StrykeValue::UNDEF
8366                        };
8367                        match decl.sigil {
8368                            Sigil::Typeglob => {
8369                                return Err(StrykeError::runtime(
8370                                    "`my *FH` / typeglob declaration is not supported",
8371                                    stmt.line,
8372                                )
8373                                .into());
8374                            }
8375                            Sigil::Scalar => {
8376                                let skey = if is_our {
8377                                    self.stash_scalar_name_for_package(&decl.name)
8378                                } else {
8379                                    decl.name.clone()
8380                                };
8381                                self.scope.declare_scalar_frozen(
8382                                    &skey,
8383                                    val,
8384                                    decl.frozen,
8385                                    decl.type_annotation.clone(),
8386                                )?;
8387                                self.english_note_lexical_scalar(&decl.name);
8388                                if is_our {
8389                                    self.note_our_scalar(&decl.name);
8390                                }
8391                            }
8392                            Sigil::Array => {
8393                                let items = val.to_list();
8394                                if is_our {
8395                                    self.record_exporter_our_array_name(&decl.name, &items);
8396                                }
8397                                let aname = if is_our {
8398                                    self.stash_array_full_name_for_package(&decl.name)
8399                                } else {
8400                                    self.stash_array_name_for_package(&decl.name)
8401                                };
8402                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
8403                                if is_our {
8404                                    self.note_our_array(&decl.name);
8405                                }
8406                            }
8407                            Sigil::Hash => {
8408                                let items = val.to_list();
8409                                let mut map = IndexMap::new();
8410                                let mut i = 0;
8411                                while i + 1 < items.len() {
8412                                    let k = items[i].to_string();
8413                                    let v = items[i + 1].clone();
8414                                    map.insert(k, v);
8415                                    i += 2;
8416                                }
8417                                let hname = if is_our {
8418                                    self.stash_hash_full_name_for_package(&decl.name)
8419                                } else {
8420                                    decl.name.clone()
8421                                };
8422                                self.scope.declare_hash_frozen(&hname, map, decl.frozen);
8423                                if is_our {
8424                                    self.note_our_hash(&decl.name);
8425                                }
8426                            }
8427                        }
8428                    }
8429                }
8430                Ok(StrykeValue::UNDEF)
8431            }
8432            StmtKind::State(decls) => {
8433                // `state` variables persist across subroutine calls.
8434                // Key by source line + name for uniqueness.
8435                for decl in decls {
8436                    let state_key = format!("{}:{}", stmt.line, decl.name);
8437                    match decl.sigil {
8438                        Sigil::Scalar => {
8439                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
8440                                // Already initialized — declare with persisted value
8441                                self.scope.declare_scalar(&decl.name, prev);
8442                            } else {
8443                                // First encounter — evaluate initializer
8444                                let val = if let Some(init) = &decl.initializer {
8445                                    self.eval_expr(init)?
8446                                } else {
8447                                    StrykeValue::UNDEF
8448                                };
8449                                self.state_vars.insert(state_key.clone(), val.clone());
8450                                self.scope.declare_scalar(&decl.name, val);
8451                            }
8452                            // Register for save-back when scope pops
8453                            if let Some(frame) = self.state_bindings_stack.last_mut() {
8454                                frame.push((decl.name.clone(), state_key));
8455                            }
8456                        }
8457                        _ => {
8458                            // For arrays/hashes, fall back to simple my-like behavior
8459                            let val = if let Some(init) = &decl.initializer {
8460                                self.eval_expr(init)?
8461                            } else {
8462                                StrykeValue::UNDEF
8463                            };
8464                            match decl.sigil {
8465                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
8466                                Sigil::Hash => {
8467                                    let items = val.to_list();
8468                                    let mut map = IndexMap::new();
8469                                    let mut i = 0;
8470                                    while i + 1 < items.len() {
8471                                        map.insert(items[i].to_string(), items[i + 1].clone());
8472                                        i += 2;
8473                                    }
8474                                    self.scope.declare_hash(&decl.name, map);
8475                                }
8476                                _ => {}
8477                            }
8478                        }
8479                    }
8480                }
8481                Ok(StrykeValue::UNDEF)
8482            }
8483            StmtKind::Local(decls) => {
8484                if decls.len() > 1 && decls[0].initializer.is_some() {
8485                    let val = self.eval_expr_ctx(
8486                        decls[0].initializer.as_ref().unwrap(),
8487                        WantarrayCtx::List,
8488                    )?;
8489                    let items = val.to_list();
8490                    let mut idx = 0;
8491                    for decl in decls {
8492                        match decl.sigil {
8493                            Sigil::Scalar => {
8494                                let v = items.get(idx).cloned().unwrap_or(StrykeValue::UNDEF);
8495                                idx += 1;
8496                                self.scope.local_set_scalar(&decl.name, v)?;
8497                            }
8498                            Sigil::Array => {
8499                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8500                                idx = items.len();
8501                                self.scope.local_set_array(&decl.name, rest)?;
8502                            }
8503                            Sigil::Hash => {
8504                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8505                                idx = items.len();
8506                                if decl.name == "ENV" {
8507                                    self.materialize_env_if_needed();
8508                                }
8509                                let mut map = IndexMap::new();
8510                                let mut i = 0;
8511                                while i + 1 < rest.len() {
8512                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
8513                                    i += 2;
8514                                }
8515                                self.scope.local_set_hash(&decl.name, map)?;
8516                            }
8517                            Sigil::Typeglob => {
8518                                return Err(StrykeError::runtime(
8519                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
8520                                    stmt.line,
8521                                )
8522                                .into());
8523                            }
8524                        }
8525                    }
8526                    Ok(val)
8527                } else {
8528                    let mut last_val = StrykeValue::UNDEF;
8529                    for decl in decls {
8530                        let val = if let Some(init) = &decl.initializer {
8531                            let ctx = match decl.sigil {
8532                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
8533                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
8534                            };
8535                            self.eval_expr_ctx(init, ctx)?
8536                        } else {
8537                            StrykeValue::UNDEF
8538                        };
8539                        last_val = val.clone();
8540                        match decl.sigil {
8541                            Sigil::Typeglob => {
8542                                let old = self.glob_handle_alias.remove(&decl.name);
8543                                if let Some(frame) = self.glob_restore_frames.last_mut() {
8544                                    frame.push((decl.name.clone(), old));
8545                                }
8546                                if let Some(init) = &decl.initializer {
8547                                    if let ExprKind::Typeglob(rhs) = &init.kind {
8548                                        self.glob_handle_alias
8549                                            .insert(decl.name.clone(), rhs.clone());
8550                                    } else {
8551                                        return Err(StrykeError::runtime(
8552                                            "local *GLOB = *OTHER — right side must be a typeglob",
8553                                            stmt.line,
8554                                        )
8555                                        .into());
8556                                    }
8557                                }
8558                            }
8559                            Sigil::Scalar => {
8560                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
8561                                // must update the interpreter's backing field too — these are
8562                                // not stored in `Scope`. Save the prior value for restoration
8563                                // on `scope_pop_hook` so the block-exit restore is visible to
8564                                // print/I/O code.
8565                                if Self::is_special_scalar_name_for_set(&decl.name) {
8566                                    let old = self.get_special_var(&decl.name);
8567                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
8568                                    {
8569                                        frame.push((decl.name.clone(), old));
8570                                    }
8571                                    self.set_special_var(&decl.name, &val)
8572                                        .map_err(|e| e.at_line(stmt.line))?;
8573                                }
8574                                self.scope.local_set_scalar(&decl.name, val)?;
8575                            }
8576                            Sigil::Array => {
8577                                self.scope.local_set_array(&decl.name, val.to_list())?;
8578                            }
8579                            Sigil::Hash => {
8580                                if decl.name == "ENV" {
8581                                    self.materialize_env_if_needed();
8582                                }
8583                                let items = val.to_list();
8584                                let mut map = IndexMap::new();
8585                                let mut i = 0;
8586                                while i + 1 < items.len() {
8587                                    let k = items[i].to_string();
8588                                    let v = items[i + 1].clone();
8589                                    map.insert(k, v);
8590                                    i += 2;
8591                                }
8592                                self.scope.local_set_hash(&decl.name, map)?;
8593                            }
8594                        }
8595                    }
8596                    Ok(last_val)
8597                }
8598            }
8599            StmtKind::LocalExpr {
8600                target,
8601                initializer,
8602            } => {
8603                let rhs_name = |init: &Expr| -> StrykeResult<Option<String>> {
8604                    match &init.kind {
8605                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
8606                        _ => Err(StrykeError::runtime(
8607                            "local *GLOB = *OTHER — right side must be a typeglob",
8608                            stmt.line,
8609                        )),
8610                    }
8611                };
8612                match &target.kind {
8613                    ExprKind::Typeglob(name) => {
8614                        let rhs = if let Some(init) = initializer {
8615                            rhs_name(init)?
8616                        } else {
8617                            None
8618                        };
8619                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
8620                        return Ok(StrykeValue::UNDEF);
8621                    }
8622                    ExprKind::Deref {
8623                        expr,
8624                        kind: Sigil::Typeglob,
8625                    } => {
8626                        let lhs = self.eval_expr(expr)?.to_string();
8627                        let rhs = if let Some(init) = initializer {
8628                            rhs_name(init)?
8629                        } else {
8630                            None
8631                        };
8632                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8633                        return Ok(StrykeValue::UNDEF);
8634                    }
8635                    ExprKind::TypeglobExpr(e) => {
8636                        let lhs = self.eval_expr(e)?.to_string();
8637                        let rhs = if let Some(init) = initializer {
8638                            rhs_name(init)?
8639                        } else {
8640                            None
8641                        };
8642                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8643                        return Ok(StrykeValue::UNDEF);
8644                    }
8645                    _ => {}
8646                }
8647                let val = if let Some(init) = initializer {
8648                    let ctx = match &target.kind {
8649                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
8650                        _ => WantarrayCtx::Scalar,
8651                    };
8652                    self.eval_expr_ctx(init, ctx)?
8653                } else {
8654                    StrykeValue::UNDEF
8655                };
8656                match &target.kind {
8657                    ExprKind::ScalarVar(name) => {
8658                        // `local $X = …` on a special var — see twin block in
8659                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
8660                        if Self::is_special_scalar_name_for_set(name) {
8661                            let old = self.get_special_var(name);
8662                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
8663                                frame.push((name.clone(), old));
8664                            }
8665                            self.set_special_var(name, &val)
8666                                .map_err(|e| e.at_line(stmt.line))?;
8667                        }
8668                        self.scope.local_set_scalar(name, val.clone())?;
8669                    }
8670                    ExprKind::ArrayVar(name) => {
8671                        self.scope.local_set_array(name, val.to_list())?;
8672                    }
8673                    ExprKind::HashVar(name) => {
8674                        if name == "ENV" {
8675                            self.materialize_env_if_needed();
8676                        }
8677                        let items = val.to_list();
8678                        let mut map = IndexMap::new();
8679                        let mut i = 0;
8680                        while i + 1 < items.len() {
8681                            map.insert(items[i].to_string(), items[i + 1].clone());
8682                            i += 2;
8683                        }
8684                        self.scope.local_set_hash(name, map)?;
8685                    }
8686                    ExprKind::HashElement { hash, key } => {
8687                        let ks = self.eval_expr(key)?.to_string();
8688                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
8689                    }
8690                    ExprKind::ArrayElement { array, index } => {
8691                        self.check_strict_array_var(array, stmt.line)?;
8692                        let aname = self.stash_array_name_for_package(array);
8693                        let idx = self.eval_expr(index)?.to_int();
8694                        self.scope
8695                            .local_set_array_element(&aname, idx, val.clone())?;
8696                    }
8697                    _ => {
8698                        return Err(StrykeError::runtime(
8699                            format!(
8700                                "local on this lvalue is not supported yet ({:?})",
8701                                target.kind
8702                            ),
8703                            stmt.line,
8704                        )
8705                        .into());
8706                    }
8707                }
8708                Ok(val)
8709            }
8710            StmtKind::MySync(decls) => {
8711                for decl in decls {
8712                    let val = if let Some(init) = &decl.initializer {
8713                        self.eval_expr(init)?
8714                    } else {
8715                        StrykeValue::UNDEF
8716                    };
8717                    match decl.sigil {
8718                        Sigil::Typeglob => {
8719                            return Err(StrykeError::runtime(
8720                                "`mysync` does not support typeglob variables",
8721                                stmt.line,
8722                            )
8723                            .into());
8724                        }
8725                        Sigil::Scalar => {
8726                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
8727                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
8728                            let stored = if val.is_mysync_deque_or_heap() {
8729                                val
8730                            } else {
8731                                StrykeValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(
8732                                    val,
8733                                )))
8734                            };
8735                            self.scope.declare_scalar(&decl.name, stored);
8736                        }
8737                        Sigil::Array => {
8738                            self.scope.declare_atomic_array(&decl.name, val.to_list());
8739                        }
8740                        Sigil::Hash => {
8741                            let items = val.to_list();
8742                            let mut map = IndexMap::new();
8743                            let mut i = 0;
8744                            while i + 1 < items.len() {
8745                                map.insert(items[i].to_string(), items[i + 1].clone());
8746                                i += 2;
8747                            }
8748                            self.scope.declare_atomic_hash(&decl.name, map);
8749                        }
8750                    }
8751                }
8752                Ok(StrykeValue::UNDEF)
8753            }
8754            StmtKind::OurSync(decls) => {
8755                // The fan/pmap/pfor workers execute closure bodies via this tree-walker
8756                // (`exec_block_no_scope`), not the bytecode VM — so `oursync` MUST register
8757                // each declared name in `english_lexical_scalars` + `our_lexical_scalars`
8758                // for `tree_scalar_storage_name` to rewrite later `$x` reads to `Pkg::x`.
8759                // Without this, worker `$x` reads see UNDEF (the qualified key isn't
8760                // queried) even though capture/restore brought the cell across.
8761                for decl in decls {
8762                    let val = if let Some(init) = &decl.initializer {
8763                        self.eval_expr(init)?
8764                    } else {
8765                        StrykeValue::UNDEF
8766                    };
8767                    match decl.sigil {
8768                        Sigil::Typeglob => {
8769                            return Err(StrykeError::runtime(
8770                                "`oursync` does not support typeglob variables",
8771                                stmt.line,
8772                            )
8773                            .into());
8774                        }
8775                        Sigil::Scalar => {
8776                            let stash = self.stash_scalar_name_for_package(&decl.name);
8777                            let stored = if val.is_mysync_deque_or_heap() {
8778                                val
8779                            } else {
8780                                StrykeValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(
8781                                    val,
8782                                )))
8783                            };
8784                            self.scope.declare_scalar(&stash, stored);
8785                            self.english_note_lexical_scalar(&decl.name);
8786                            self.note_our_scalar(&decl.name);
8787                        }
8788                        Sigil::Array => {
8789                            let stash = self.stash_array_name_for_package(&decl.name);
8790                            self.scope.declare_atomic_array(&stash, val.to_list());
8791                            self.english_note_lexical_scalar(&decl.name);
8792                            self.note_our_scalar(&decl.name);
8793                        }
8794                        Sigil::Hash => {
8795                            let items = val.to_list();
8796                            let mut map = IndexMap::new();
8797                            let mut i = 0;
8798                            while i + 1 < items.len() {
8799                                map.insert(items[i].to_string(), items[i + 1].clone());
8800                                i += 2;
8801                            }
8802                            // Match `our %h` convention: bare hash name (existing
8803                            // cross-package quirk).
8804                            self.scope.declare_atomic_hash(&decl.name, map);
8805                            self.english_note_lexical_scalar(&decl.name);
8806                            self.note_our_scalar(&decl.name);
8807                        }
8808                    }
8809                }
8810                Ok(StrykeValue::UNDEF)
8811            }
8812            StmtKind::Package { name } => {
8813                // Minimal package support — just set a variable
8814                let _ = self
8815                    .scope
8816                    .set_scalar("__PACKAGE__", StrykeValue::string(name.clone()));
8817                Ok(StrykeValue::UNDEF)
8818            }
8819            StmtKind::UsePerlVersion { .. } => Ok(StrykeValue::UNDEF),
8820            StmtKind::Use { .. } => {
8821                // Handled in `prepare_program_top_level` before BEGIN / main.
8822                Ok(StrykeValue::UNDEF)
8823            }
8824            StmtKind::UseOverload { pairs } => {
8825                self.install_use_overload_pairs(pairs);
8826                Ok(StrykeValue::UNDEF)
8827            }
8828            StmtKind::No { .. } => {
8829                // Handled in `prepare_program_top_level` (same phase as `use`).
8830                Ok(StrykeValue::UNDEF)
8831            }
8832            StmtKind::Return(val) => {
8833                let v = if let Some(e) = val {
8834                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
8835                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
8836                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
8837                    self.eval_expr_ctx(e, self.wantarray_kind)?
8838                } else {
8839                    StrykeValue::UNDEF
8840                };
8841                Err(Flow::Return(v).into())
8842            }
8843            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
8844            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
8845            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
8846            StmtKind::Block(block) => self.exec_block(block),
8847            StmtKind::Begin(_)
8848            | StmtKind::UnitCheck(_)
8849            | StmtKind::Check(_)
8850            | StmtKind::Init(_)
8851            | StmtKind::End(_) => Ok(StrykeValue::UNDEF),
8852            StmtKind::Empty => Ok(StrykeValue::UNDEF),
8853            StmtKind::Goto { target } => {
8854                // goto &sub — tail call
8855                if let ExprKind::SubroutineRef(name) = &target.kind {
8856                    return Err(Flow::GotoSub(name.clone()).into());
8857                }
8858                Err(StrykeError::runtime("goto reached outside goto-aware block", stmt.line).into())
8859            }
8860            StmtKind::EvalTimeout { timeout, body } => {
8861                let secs = self.eval_expr(timeout)?.to_number();
8862                self.eval_timeout_block(body, secs, stmt.line)
8863            }
8864            StmtKind::Tie {
8865                target,
8866                class,
8867                args,
8868            } => {
8869                let kind = match &target {
8870                    TieTarget::Scalar(_) => 0u8,
8871                    TieTarget::Array(_) => 1u8,
8872                    TieTarget::Hash(_) => 2u8,
8873                };
8874                let name = match &target {
8875                    TieTarget::Scalar(s) => s.as_str(),
8876                    TieTarget::Array(a) => a.as_str(),
8877                    TieTarget::Hash(h) => h.as_str(),
8878                };
8879                let mut vals = vec![self.eval_expr(class)?];
8880                for a in args {
8881                    vals.push(self.eval_expr(a)?);
8882                }
8883                self.tie_execute(kind, name, vals, stmt.line)
8884                    .map_err(Into::into)
8885            }
8886            StmtKind::TryCatch {
8887                try_block,
8888                catch_var,
8889                catch_block,
8890                finally_block,
8891            } => match self.exec_block(try_block) {
8892                Ok(v) => {
8893                    if let Some(fb) = finally_block {
8894                        self.exec_block(fb)?;
8895                    }
8896                    Ok(v)
8897                }
8898                Err(FlowOrError::Error(e)) => {
8899                    if matches!(e.kind, ErrorKind::Exit(_)) {
8900                        return Err(FlowOrError::Error(e));
8901                    }
8902                    self.scope_push_hook();
8903                    self.scope
8904                        .declare_scalar(catch_var, StrykeValue::string(e.to_string()));
8905                    self.english_note_lexical_scalar(catch_var);
8906                    let r = self.exec_block(catch_block);
8907                    self.scope_pop_hook();
8908                    if let Some(fb) = finally_block {
8909                        self.exec_block(fb)?;
8910                    }
8911                    r
8912                }
8913                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
8914            },
8915            StmtKind::Given { topic, body } => self.exec_given(topic, body),
8916            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(StrykeError::runtime(
8917                "when/default may only appear inside a given block",
8918                stmt.line,
8919            )
8920            .into()),
8921            StmtKind::FormatDecl { .. } => {
8922                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
8923                Ok(StrykeValue::UNDEF)
8924            }
8925            StmtKind::AdviceDecl {
8926                kind,
8927                pattern,
8928                body,
8929            } => {
8930                // Tree-walker registration path: only reached if bytecode compilation
8931                // bailed out (extremely rare — production programs always run through
8932                // the VM). The body has no compiled bytecode region, so we tag it with
8933                // `u16::MAX` and `dispatch_with_advice` will refuse to fire it rather
8934                // than silently fall back to the AST tree-walker.
8935                let id = self.next_intercept_id;
8936                self.next_intercept_id = id.saturating_add(1);
8937                self.intercepts.push(crate::aop::Intercept {
8938                    id,
8939                    kind: *kind,
8940                    pattern: pattern.clone(),
8941                    body: body.clone(),
8942                    body_block_idx: u16::MAX,
8943                });
8944                Ok(StrykeValue::UNDEF)
8945            }
8946            StmtKind::Continue(block) => self.exec_block_smart(block),
8947        }
8948    }
8949
8950    #[inline]
8951    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
8952        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
8953    }
8954
8955    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
8956    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
8957    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
8958    pub(crate) fn scalar_compound_assign_scalar_target(
8959        &mut self,
8960        name: &str,
8961        op: BinOp,
8962        rhs: StrykeValue,
8963    ) -> Result<StrykeValue, StrykeError> {
8964        if op == BinOp::Concat {
8965            return self.scope.scalar_concat_inplace(name, &rhs);
8966        }
8967        self.scope
8968            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs))
8969    }
8970
8971    fn compound_scalar_binop(old: &StrykeValue, op: BinOp, rhs: &StrykeValue) -> StrykeValue {
8972        match op {
8973            BinOp::Add => {
8974                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8975                    StrykeValue::integer(a.wrapping_add(b))
8976                } else {
8977                    StrykeValue::float(old.to_number() + rhs.to_number())
8978                }
8979            }
8980            BinOp::Sub => {
8981                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8982                    StrykeValue::integer(a.wrapping_sub(b))
8983                } else {
8984                    StrykeValue::float(old.to_number() - rhs.to_number())
8985                }
8986            }
8987            BinOp::Mul => {
8988                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8989                    StrykeValue::integer(a.wrapping_mul(b))
8990                } else {
8991                    StrykeValue::float(old.to_number() * rhs.to_number())
8992                }
8993            }
8994            BinOp::BitAnd => {
8995                if let Some(s) = crate::value::set_intersection(old, rhs) {
8996                    s
8997                } else {
8998                    StrykeValue::integer(old.to_int() & rhs.to_int())
8999                }
9000            }
9001            BinOp::BitOr => {
9002                if let Some(s) = crate::value::set_union(old, rhs) {
9003                    s
9004                } else {
9005                    StrykeValue::integer(old.to_int() | rhs.to_int())
9006                }
9007            }
9008            BinOp::BitXor => StrykeValue::integer(old.to_int() ^ rhs.to_int()),
9009            BinOp::ShiftLeft => StrykeValue::integer(perl_shl_i64(old.to_int(), rhs.to_int())),
9010            BinOp::ShiftRight => StrykeValue::integer(perl_shr_i64(old.to_int(), rhs.to_int())),
9011            BinOp::Div => StrykeValue::float(old.to_number() / rhs.to_number()),
9012            BinOp::Mod => {
9013                // Return 0 on b==0 silently — this helper is the
9014                // `$x OP= rhs` atomic-mutate path which can't propagate
9015                // errors. The non-compound `%` path (eval_binop) raises
9016                // `ErrorKind::DivisionByZero`.
9017                let b = rhs.to_int();
9018                if b == 0 {
9019                    StrykeValue::integer(0)
9020                } else {
9021                    StrykeValue::integer(crate::value::perl_mod_i64(old.to_int(), b))
9022                }
9023            }
9024            BinOp::Pow => StrykeValue::float(old.to_number().powf(rhs.to_number())),
9025            BinOp::LogOr => {
9026                if old.is_true() {
9027                    old.clone()
9028                } else {
9029                    rhs.clone()
9030                }
9031            }
9032            BinOp::DefinedOr => {
9033                if !old.is_undef() {
9034                    old.clone()
9035                } else {
9036                    rhs.clone()
9037                }
9038            }
9039            BinOp::LogAnd => {
9040                if old.is_true() {
9041                    rhs.clone()
9042                } else {
9043                    old.clone()
9044                }
9045            }
9046            _ => StrykeValue::float(old.to_number() + rhs.to_number()),
9047        }
9048    }
9049
9050    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
9051    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
9052    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
9053    fn eval_hash_slice_key_components(
9054        &mut self,
9055        key_expr: &Expr,
9056    ) -> Result<Vec<String>, FlowOrError> {
9057        // Keys for `@h{LIST}` are always evaluated in Perl list context — an
9058        // `@ks` operand splats to its elements instead of scalarizing to its
9059        // count, and an `\@ref` operand unwraps after deref. Without this the
9060        // array-var form returned the empty list (BUG-028).
9061        let v = self.eval_expr_ctx(key_expr, WantarrayCtx::List)?;
9062        if let Some(vv) = v.as_array_vec() {
9063            return Ok(vv.iter().map(|x| x.to_string()).collect());
9064        }
9065        if let Some(r) = v.as_array_ref() {
9066            return Ok(r.read().iter().map(|x| x.to_string()).collect());
9067        }
9068        if v.is_iterator() {
9069            return Ok(v
9070                .into_iterator()
9071                .collect_all()
9072                .iter()
9073                .map(|x| x.to_string())
9074                .collect());
9075        }
9076        Ok(vec![v.to_string()])
9077    }
9078
9079    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
9080    pub(crate) fn symbolic_deref(
9081        &mut self,
9082        val: StrykeValue,
9083        kind: Sigil,
9084        line: usize,
9085    ) -> ExecResult {
9086        match kind {
9087            Sigil::Scalar => {
9088                if let Some(name) = val.as_scalar_binding_name() {
9089                    return Ok(self.get_special_var(&name));
9090                }
9091                if let Some(r) = val.as_scalar_ref() {
9092                    return Ok(r.read().clone());
9093                }
9094                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
9095                if let Some(r) = val.as_array_ref() {
9096                    return Ok(StrykeValue::array(r.read().clone()));
9097                }
9098                if let Some(name) = val.as_array_binding_name() {
9099                    return Ok(StrykeValue::array(self.scope.get_array(&name)));
9100                }
9101                if let Some(r) = val.as_hash_ref() {
9102                    return Ok(StrykeValue::hash(r.read().clone()));
9103                }
9104                if let Some(name) = val.as_hash_binding_name() {
9105                    self.touch_env_hash(&name);
9106                    return Ok(StrykeValue::hash(self.scope.get_hash(&name)));
9107                }
9108                if let Some(s) = val.as_str() {
9109                    if self.strict_refs {
9110                        return Err(StrykeError::runtime(
9111                            format!(
9112                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
9113                                s
9114                            ),
9115                            line,
9116                        )
9117                        .into());
9118                    }
9119                    return Ok(self.get_special_var(&s));
9120                }
9121                Err(StrykeError::runtime("Can't dereference non-reference as scalar", line).into())
9122            }
9123            Sigil::Array => {
9124                if let Some(r) = val.as_array_ref() {
9125                    return Ok(StrykeValue::array(r.read().clone()));
9126                }
9127                if let Some(name) = val.as_array_binding_name() {
9128                    return Ok(StrykeValue::array(self.scope.get_array(&name)));
9129                }
9130                if val.is_undef() {
9131                    if self.strict_refs {
9132                        return Err(StrykeError::runtime(
9133                            "Can't use an undefined value as an ARRAY reference",
9134                            line,
9135                        )
9136                        .into());
9137                    }
9138                    return Ok(StrykeValue::array(vec![]));
9139                }
9140                // Plain primitive scalar (int, float, string): under no-strict, perl
9141                // treats this as a symbolic ref `@{$val_as_string}` and silently
9142                // returns the (likely empty) named array. Under strict refs, error.
9143                // Heap objects (Pair, Generator, blessed-non-ref) fall through to
9144                // the dereference-error so we don't silently swallow real bugs.
9145                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
9146                    let s = val.to_string();
9147                    if self.strict_refs {
9148                        return Err(StrykeError::runtime(
9149                            format!(
9150                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
9151                                s
9152                            ),
9153                            line,
9154                        )
9155                        .into());
9156                    }
9157                    return Ok(StrykeValue::array(self.scope.get_array(&s)));
9158                }
9159                Err(StrykeError::runtime("Can't dereference non-reference as array", line).into())
9160            }
9161            Sigil::Hash => {
9162                if let Some(r) = val.as_hash_ref() {
9163                    return Ok(StrykeValue::hash(r.read().clone()));
9164                }
9165                if let Some(name) = val.as_hash_binding_name() {
9166                    self.touch_env_hash(&name);
9167                    return Ok(StrykeValue::hash(self.scope.get_hash(&name)));
9168                }
9169                // Stryke `class C { ... }` instances answer to `%$obj` by
9170                // flattening their field name/value pairs — the same shape
9171                // a Perl-style blessed hashref produces. This keeps the
9172                // canonical introspection idiom (`keys %$obj`, `values
9173                // %$obj`) working for stryke-native OO too. Order matches
9174                // the inheritance-collected field order from
9175                // `collect_class_fields_full`.
9176                if let Some(c) = val.as_class_inst() {
9177                    let all_fields = self.collect_class_fields_full(&c.def);
9178                    let values = c.get_values();
9179                    let mut map = IndexMap::new();
9180                    for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
9181                        if let Some(v) = values.get(i) {
9182                            map.insert(name.clone(), v.clone());
9183                        }
9184                    }
9185                    return Ok(StrykeValue::hash(map));
9186                }
9187                // Same for stryke `struct S { ... }` instances — keep them
9188                // introspectable through the Perl-style hash-deref idiom.
9189                if let Some(s) = val.as_struct_inst() {
9190                    let values = s.get_values();
9191                    let mut map = IndexMap::new();
9192                    for (i, field) in s.def.fields.iter().enumerate() {
9193                        if let Some(v) = values.get(i) {
9194                            map.insert(field.name.clone(), v.clone());
9195                        }
9196                    }
9197                    return Ok(StrykeValue::hash(map));
9198                }
9199                // Blessed-ref escape hatch: when the inner data is a hash,
9200                // unwrap and treat the deref as if it targeted the inner
9201                // hash. Old Perl OO code that wrote `%$self` on a blessed
9202                // hashref keeps working without an extra unbless step.
9203                if let Some(b) = val.as_blessed_ref() {
9204                    let inner = b.data.read().clone();
9205                    if let Some(r) = inner.as_hash_ref() {
9206                        return Ok(StrykeValue::hash(r.read().clone()));
9207                    }
9208                    if let Some(h) = inner.as_hash_map() {
9209                        return Ok(StrykeValue::hash(h));
9210                    }
9211                }
9212                if val.is_undef() {
9213                    if self.strict_refs {
9214                        return Err(StrykeError::runtime(
9215                            "Can't use an undefined value as a HASH reference",
9216                            line,
9217                        )
9218                        .into());
9219                    }
9220                    return Ok(StrykeValue::hash(IndexMap::new()));
9221                }
9222                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
9223                    let s = val.to_string();
9224                    if self.strict_refs {
9225                        return Err(StrykeError::runtime(
9226                            format!(
9227                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
9228                                s
9229                            ),
9230                            line,
9231                        )
9232                        .into());
9233                    }
9234                    self.touch_env_hash(&s);
9235                    return Ok(StrykeValue::hash(self.scope.get_hash(&s)));
9236                }
9237                Err(StrykeError::runtime("Can't dereference non-reference as hash", line).into())
9238            }
9239            Sigil::Typeglob => {
9240                if let Some(s) = val.as_str() {
9241                    return Ok(StrykeValue::string(self.resolve_io_handle_name(&s)));
9242                }
9243                Err(
9244                    StrykeError::runtime("Can't dereference non-reference as typeglob", line)
9245                        .into(),
9246                )
9247            }
9248        }
9249    }
9250
9251    /// `qq` list join expects a plain array; if a bare [`StrykeValue::array_ref`] reaches join, peel
9252    /// one level so elements stringify like Perl (`"@$r"`).
9253    #[inline]
9254    pub(crate) fn peel_array_ref_for_list_join(&self, v: StrykeValue) -> StrykeValue {
9255        if let Some(r) = v.as_array_ref() {
9256            return StrykeValue::array(r.read().clone());
9257        }
9258        v
9259    }
9260
9261    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
9262    pub(crate) fn make_array_ref_alias(&self, val: StrykeValue, line: usize) -> ExecResult {
9263        if let Some(a) = val.as_array_ref() {
9264            return Ok(StrykeValue::array_ref(Arc::clone(&a)));
9265        }
9266        if let Some(name) = val.as_array_binding_name() {
9267            return Ok(StrykeValue::array_binding_ref(name));
9268        }
9269        if let Some(s) = val.as_str() {
9270            if self.strict_refs {
9271                return Err(StrykeError::runtime(
9272                    format!(
9273                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
9274                        s
9275                    ),
9276                    line,
9277                )
9278                .into());
9279            }
9280            return Ok(StrykeValue::array_binding_ref(s.to_string()));
9281        }
9282        if let Some(r) = val.as_scalar_ref() {
9283            let inner = r.read().clone();
9284            return self.make_array_ref_alias(inner, line);
9285        }
9286        Err(StrykeError::runtime("Can't make array reference from value", line).into())
9287    }
9288
9289    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
9290    pub(crate) fn make_hash_ref_alias(&self, val: StrykeValue, line: usize) -> ExecResult {
9291        if let Some(h) = val.as_hash_ref() {
9292            return Ok(StrykeValue::hash_ref(Arc::clone(&h)));
9293        }
9294        if let Some(name) = val.as_hash_binding_name() {
9295            return Ok(StrykeValue::hash_binding_ref(name));
9296        }
9297        if let Some(s) = val.as_str() {
9298            if self.strict_refs {
9299                return Err(StrykeError::runtime(
9300                    format!(
9301                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
9302                        s
9303                    ),
9304                    line,
9305                )
9306                .into());
9307            }
9308            return Ok(StrykeValue::hash_binding_ref(s.to_string()));
9309        }
9310        if let Some(r) = val.as_scalar_ref() {
9311            let inner = r.read().clone();
9312            return self.make_hash_ref_alias(inner, line);
9313        }
9314        Err(StrykeError::runtime("Can't make hash reference from value", line).into())
9315    }
9316
9317    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
9318    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
9319    pub(crate) fn process_case_escapes(s: &str) -> String {
9320        // Quick check: if no backslash, nothing to do
9321        if !s.contains('\\') {
9322            return s.to_string();
9323        }
9324        let mut result = String::with_capacity(s.len());
9325        let mut chars = s.chars().peekable();
9326        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
9327        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
9328
9329        while let Some(c) = chars.next() {
9330            if c == '\\' {
9331                match chars.peek() {
9332                    Some(&'U') => {
9333                        chars.next();
9334                        mode = Some('U');
9335                        continue;
9336                    }
9337                    Some(&'L') => {
9338                        chars.next();
9339                        mode = Some('L');
9340                        continue;
9341                    }
9342                    Some(&'Q') => {
9343                        chars.next();
9344                        mode = Some('Q');
9345                        continue;
9346                    }
9347                    Some(&'E') => {
9348                        chars.next();
9349                        mode = None;
9350                        next_char_mod = None;
9351                        continue;
9352                    }
9353                    Some(&'u') => {
9354                        chars.next();
9355                        next_char_mod = Some('u');
9356                        continue;
9357                    }
9358                    Some(&'l') => {
9359                        chars.next();
9360                        next_char_mod = Some('l');
9361                        continue;
9362                    }
9363                    _ => {}
9364                }
9365            }
9366
9367            let ch = c;
9368
9369            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
9370            if let Some(m) = next_char_mod.take() {
9371                let transformed = match m {
9372                    'u' => ch.to_uppercase().next().unwrap_or(ch),
9373                    'l' => ch.to_lowercase().next().unwrap_or(ch),
9374                    _ => ch,
9375                };
9376                result.push(transformed);
9377            } else {
9378                // Apply ongoing mode
9379                match mode {
9380                    Some('U') => {
9381                        for uc in ch.to_uppercase() {
9382                            result.push(uc);
9383                        }
9384                    }
9385                    Some('L') => {
9386                        for lc in ch.to_lowercase() {
9387                            result.push(lc);
9388                        }
9389                    }
9390                    Some('Q') => {
9391                        if !ch.is_ascii_alphanumeric() && ch != '_' {
9392                            result.push('\\');
9393                        }
9394                        result.push(ch);
9395                    }
9396                    None | Some(_) => {
9397                        result.push(ch);
9398                    }
9399                }
9400            }
9401        }
9402        result
9403    }
9404
9405    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
9406        let line = expr.line;
9407        match &expr.kind {
9408            ExprKind::Integer(n) => Ok(StrykeValue::integer(*n)),
9409            ExprKind::Float(f) => Ok(StrykeValue::float(*f)),
9410            ExprKind::String(s) => {
9411                let processed = Self::process_case_escapes(s);
9412                Ok(StrykeValue::string(processed))
9413            }
9414            ExprKind::Bareword(s) => {
9415                if s == "__PACKAGE__" {
9416                    return Ok(StrykeValue::string(self.current_package()));
9417                }
9418                if let Some(sub) = self.resolve_sub_by_name(s) {
9419                    return self.call_sub(&sub, vec![], ctx, line);
9420                }
9421                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
9422                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
9423                    return r.map_err(Into::into);
9424                }
9425                Ok(StrykeValue::string(s.clone()))
9426            }
9427            ExprKind::Undef => Ok(StrykeValue::UNDEF),
9428            ExprKind::MagicConst(MagicConstKind::File) => {
9429                Ok(StrykeValue::string(self.file.clone()))
9430            }
9431            ExprKind::MagicConst(MagicConstKind::Line) => {
9432                Ok(StrykeValue::integer(expr.line as i64))
9433            }
9434            ExprKind::MagicConst(MagicConstKind::Sub) => {
9435                if let Some(sub) = self.current_sub_stack.last().cloned() {
9436                    Ok(StrykeValue::code_ref(sub))
9437                } else {
9438                    Ok(StrykeValue::UNDEF)
9439                }
9440            }
9441            ExprKind::Regex(pattern, flags) => {
9442                if ctx == WantarrayCtx::Void {
9443                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
9444                    let topic = self.scope.get_scalar("_");
9445                    let s = topic.to_string();
9446                    self.regex_match_execute(s, pattern, flags, false, "_", line)
9447                } else {
9448                    let re = self.compile_regex(pattern, flags, line)?;
9449                    Ok(StrykeValue::regex(re, pattern.clone(), flags.clone()))
9450                }
9451            }
9452            ExprKind::QW(words) => Ok(StrykeValue::array(
9453                words
9454                    .iter()
9455                    .map(|w| StrykeValue::string(w.clone()))
9456                    .collect(),
9457            )),
9458
9459            // Interpolated strings
9460            ExprKind::InterpolatedString(parts) => {
9461                let mut raw_result = String::new();
9462                for part in parts {
9463                    match part {
9464                        StringPart::Literal(s) => raw_result.push_str(s),
9465                        StringPart::ScalarVar(name) => {
9466                            self.check_strict_scalar_var(name, line)?;
9467                            let val = self.get_special_var(name);
9468                            let s = self.stringify_value(val, line)?;
9469                            raw_result.push_str(&s);
9470                        }
9471                        StringPart::ArrayVar(name) => {
9472                            self.check_strict_array_var(name, line)?;
9473                            let aname = self.stash_array_name_for_package(name);
9474                            let arr = self.scope.get_array(&aname);
9475                            let mut parts = Vec::with_capacity(arr.len());
9476                            for v in &arr {
9477                                parts.push(self.stringify_value(v.clone(), line)?);
9478                            }
9479                            let sep = self.list_separator.clone();
9480                            raw_result.push_str(&parts.join(&sep));
9481                        }
9482                        StringPart::Expr(e) => {
9483                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
9484                                self.check_strict_array_var(array, line)?;
9485                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9486                                let val = self.peel_array_ref_for_list_join(val);
9487                                let list = val.to_list();
9488                                let sep = self.list_separator.clone();
9489                                let mut parts = Vec::with_capacity(list.len());
9490                                for v in list {
9491                                    parts.push(self.stringify_value(v, line)?);
9492                                }
9493                                raw_result.push_str(&parts.join(&sep));
9494                            } else if let ExprKind::Deref {
9495                                kind: Sigil::Array, ..
9496                            } = &e.kind
9497                            {
9498                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9499                                let val = self.peel_array_ref_for_list_join(val);
9500                                let list = val.to_list();
9501                                let sep = self.list_separator.clone();
9502                                let mut parts = Vec::with_capacity(list.len());
9503                                for v in list {
9504                                    parts.push(self.stringify_value(v, line)?);
9505                                }
9506                                raw_result.push_str(&parts.join(&sep));
9507                            } else {
9508                                let val = self.eval_expr(e)?;
9509                                let s = self.stringify_value(val, line)?;
9510                                raw_result.push_str(&s);
9511                            }
9512                        }
9513                    }
9514                }
9515                let result = Self::process_case_escapes(&raw_result);
9516                Ok(StrykeValue::string(result))
9517            }
9518
9519            // Variables
9520            ExprKind::ScalarVar(name) => {
9521                self.check_strict_scalar_var(name, line)?;
9522                let stor = self.tree_scalar_storage_name(name);
9523                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
9524                    let class = obj
9525                        .as_blessed_ref()
9526                        .map(|b| b.class.clone())
9527                        .unwrap_or_default();
9528                    let full = format!("{}::FETCH", class);
9529                    if let Some(sub) = self.subs.get(&full).cloned() {
9530                        return self.call_sub(&sub, vec![obj], ctx, line);
9531                    }
9532                }
9533                Ok(self.get_special_var(&stor))
9534            }
9535            ExprKind::ArrayVar(name) => {
9536                self.check_strict_array_var(name, line)?;
9537                let qualified = self.tree_array_storage_name(name);
9538                let aname = self.stash_array_name_for_package(&qualified);
9539                let arr = self.scope.get_array(&aname);
9540                if ctx == WantarrayCtx::List {
9541                    Ok(StrykeValue::array(arr))
9542                } else {
9543                    Ok(StrykeValue::integer(arr.len() as i64))
9544                }
9545            }
9546            ExprKind::HashVar(name) => {
9547                self.check_strict_hash_var(name, line)?;
9548                self.touch_env_hash(name);
9549                let hname = self.tree_hash_storage_name(name);
9550                let h = self.scope.get_hash(&hname);
9551                let pv = StrykeValue::hash(h);
9552                if ctx == WantarrayCtx::List {
9553                    Ok(pv)
9554                } else {
9555                    Ok(pv.scalar_context())
9556                }
9557            }
9558            ExprKind::Typeglob(name) => {
9559                let n = self.resolve_io_handle_name(name);
9560                Ok(StrykeValue::string(n))
9561            }
9562            ExprKind::TypeglobExpr(e) => {
9563                let name = self.eval_expr(e)?.to_string();
9564                let n = self.resolve_io_handle_name(&name);
9565                Ok(StrykeValue::string(n))
9566            }
9567            ExprKind::ArrayElement { array, index } => {
9568                // Stryke string-index sugar: bareword `_[N]` parses to an
9569                // ArrayElement with a `__topicstr__N` synthetic name. Strip
9570                // the prefix and treat as substr-of-topic. Differs from
9571                // `$_[N]` (sigil form) which keeps Perl's @_-access.
9572                if let Some(real) = array.strip_prefix("__topicstr__") {
9573                    let s = self.scope.get_scalar(real).to_string();
9574                    if let ExprKind::Range {
9575                        from,
9576                        to,
9577                        exclusive,
9578                        step,
9579                    } = &index.kind
9580                    {
9581                        let n = s.chars().count() as i64;
9582                        let mut from_i = self.eval_expr(from)?.to_int();
9583                        let mut to_i = self.eval_expr(to)?.to_int();
9584                        let step_i = match step {
9585                            Some(e) => self.eval_expr(e)?.to_int(),
9586                            None => 1,
9587                        };
9588                        if from_i < 0 {
9589                            from_i += n
9590                        }
9591                        if to_i < 0 {
9592                            to_i += n
9593                        }
9594                        if *exclusive {
9595                            to_i -= 1
9596                        }
9597                        let chars: Vec<char> = s.chars().collect();
9598                        let mut out = String::new();
9599                        if step_i > 0 {
9600                            let mut i = from_i;
9601                            while i <= to_i && i < n {
9602                                if i >= 0 {
9603                                    out.push(chars[i as usize]);
9604                                }
9605                                i += step_i;
9606                            }
9607                        } else if step_i < 0 {
9608                            let mut i = from_i;
9609                            while i >= to_i && i >= 0 {
9610                                if i < n {
9611                                    out.push(chars[i as usize]);
9612                                }
9613                                i += step_i;
9614                            }
9615                        }
9616                        return Ok(StrykeValue::string(out));
9617                    }
9618                    let idx = self.eval_expr(index)?.to_int();
9619                    let n = s.chars().count() as i64;
9620                    let i = if idx < 0 { idx + n } else { idx };
9621                    return Ok(if i >= 0 && i < n {
9622                        s.chars()
9623                            .nth(i as usize)
9624                            .map(|c| StrykeValue::string(c.to_string()))
9625                            .unwrap_or(StrykeValue::UNDEF)
9626                    } else {
9627                        StrykeValue::UNDEF
9628                    });
9629                }
9630                self.check_strict_array_var(array, line)?;
9631                // Stryke (non-compat) string-slice sugar: when the index is
9632                // a `from:to[:step]` range AND the target is a string, return
9633                // a substring with optional step. Mirrors Python `s[1:10:2]`.
9634                // Detect this BEFORE collapsing the range to an int.
9635                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9636                    if let ExprKind::Range {
9637                        from,
9638                        to,
9639                        exclusive,
9640                        step,
9641                    } = &index.kind
9642                    {
9643                        let aname_check = self.stash_array_name_for_package(array);
9644                        let prefer_scalar =
9645                            array == "_" || self.scope.get_array(&aname_check).is_empty();
9646                        if prefer_scalar {
9647                            let s = self.scope.get_scalar(array).to_string();
9648                            if !s.is_empty() {
9649                                let n = s.chars().count() as i64;
9650                                let mut from_i = self.eval_expr(from)?.to_int();
9651                                let mut to_i = self.eval_expr(to)?.to_int();
9652                                let step_i = match step {
9653                                    Some(e) => self.eval_expr(e)?.to_int(),
9654                                    None => 1,
9655                                };
9656                                if from_i < 0 {
9657                                    from_i += n
9658                                }
9659                                if to_i < 0 {
9660                                    to_i += n
9661                                }
9662                                if *exclusive {
9663                                    to_i -= 1
9664                                }
9665                                let chars: Vec<char> = s.chars().collect();
9666                                let mut out = String::new();
9667                                if step_i > 0 {
9668                                    let mut i = from_i;
9669                                    while i <= to_i && i < n {
9670                                        if i >= 0 {
9671                                            out.push(chars[i as usize]);
9672                                        }
9673                                        i += step_i;
9674                                    }
9675                                } else if step_i < 0 {
9676                                    let mut i = from_i;
9677                                    while i >= to_i && i >= 0 {
9678                                        if i < n {
9679                                            out.push(chars[i as usize]);
9680                                        }
9681                                        i += step_i;
9682                                    }
9683                                }
9684                                return Ok(StrykeValue::string(out));
9685                            }
9686                        }
9687                    }
9688                }
9689                let idx = self.eval_expr(index)?.to_int();
9690                let aname = self.stash_array_name_for_package(array);
9691                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
9692                    let class = obj
9693                        .as_blessed_ref()
9694                        .map(|b| b.class.clone())
9695                        .unwrap_or_default();
9696                    let full = format!("{}::FETCH", class);
9697                    if let Some(sub) = self.subs.get(&full).cloned() {
9698                        let arg_vals = vec![obj, StrykeValue::integer(idx)];
9699                        return self.call_sub(&sub, arg_vals, ctx, line);
9700                    }
9701                }
9702                // Stryke (non-compat) sugar: `$name[i]` indexes by Unicode
9703                // char when `@name` is missing/empty but `$name` is a
9704                // non-empty string. So `$s[0]` is the first grapheme of
9705                // `$s`. NB: `$_[0]` keeps Perl's `@_`-access semantics
9706                // because `@_` is populated inside any sub call; the
9707                // bareword `_[0]` parses to the same AST node, so both
9708                // forms behave alike — use `substr(_, 0, 1)` for char-of-
9709                // topic when inside a sub. Compat mode = Perl semantics.
9710                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9711                    let prefer_scalar = self.scope.get_array(&aname).is_empty();
9712                    if prefer_scalar {
9713                        let s = self.scope.get_scalar(array).to_string();
9714                        if !s.is_empty() {
9715                            let n = s.chars().count() as i64;
9716                            let i = if idx < 0 { idx + n } else { idx };
9717                            if i >= 0 && i < n {
9718                                if let Some(c) = s.chars().nth(i as usize) {
9719                                    return Ok(StrykeValue::string(c.to_string()));
9720                                }
9721                            }
9722                            return Ok(StrykeValue::UNDEF);
9723                        }
9724                    }
9725                }
9726                Ok(self.scope.get_array_element(&aname, idx))
9727            }
9728            ExprKind::HashElement { hash, key } => {
9729                self.check_strict_hash_var(hash, line)?;
9730                let k = self.eval_expr(key)?.to_string();
9731                self.touch_env_hash(hash);
9732                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
9733                    let class = obj
9734                        .as_blessed_ref()
9735                        .map(|b| b.class.clone())
9736                        .unwrap_or_default();
9737                    let full = format!("{}::FETCH", class);
9738                    if let Some(sub) = self.subs.get(&full).cloned() {
9739                        let arg_vals = vec![obj, StrykeValue::string(k)];
9740                        return self.call_sub(&sub, arg_vals, ctx, line);
9741                    }
9742                }
9743                let hname = self.tree_hash_storage_name(hash);
9744                Ok(self.scope.get_hash_element(&hname, &k))
9745            }
9746            ExprKind::ArraySlice { array, indices } => {
9747                self.check_strict_array_var(array, line)?;
9748                let aname = self.stash_array_name_for_package(array);
9749                let flat = self.flatten_array_slice_index_specs(indices)?;
9750                let mut result = Vec::with_capacity(flat.len());
9751                for idx in flat {
9752                    result.push(self.scope.get_array_element(&aname, idx));
9753                }
9754                Ok(StrykeValue::array(result))
9755            }
9756            ExprKind::HashSlice { hash, keys } => {
9757                self.check_strict_hash_var(hash, line)?;
9758                self.touch_env_hash(hash);
9759                let mut result = Vec::new();
9760                for key_expr in keys {
9761                    for k in self.eval_hash_slice_key_components(key_expr)? {
9762                        result.push(self.scope.get_hash_element(hash, &k));
9763                    }
9764                }
9765                Ok(StrykeValue::array(result))
9766            }
9767            ExprKind::HashKvSlice { hash, keys } => {
9768                // `%h{KEYS}` — Perl 5.20+ key-value slice. Returns a flat
9769                // (key, value, key, value, ...) list. (BUG-008)
9770                self.check_strict_hash_var(hash, line)?;
9771                self.touch_env_hash(hash);
9772                let mut result = Vec::new();
9773                for key_expr in keys {
9774                    for k in self.eval_hash_slice_key_components(key_expr)? {
9775                        let v = self.scope.get_hash_element(hash, &k);
9776                        result.push(StrykeValue::string(k));
9777                        result.push(v);
9778                    }
9779                }
9780                Ok(StrykeValue::array(result))
9781            }
9782            ExprKind::HashSliceDeref { container, keys } => {
9783                let hv = self.eval_expr(container)?;
9784                let mut key_vals = Vec::with_capacity(keys.len());
9785                for key_expr in keys {
9786                    let v = if matches!(
9787                        key_expr.kind,
9788                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
9789                    ) {
9790                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
9791                    } else {
9792                        self.eval_expr(key_expr)?
9793                    };
9794                    key_vals.push(v);
9795                }
9796                self.hash_slice_deref_values(&hv, &key_vals, line)
9797            }
9798            ExprKind::AnonymousListSlice { source, indices } => {
9799                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
9800                let items = list_val.to_list();
9801                let flat = self.flatten_array_slice_index_specs(indices)?;
9802                let mut out = Vec::with_capacity(flat.len());
9803                for idx in flat {
9804                    let i = if idx < 0 {
9805                        (items.len() as i64 + idx) as usize
9806                    } else {
9807                        idx as usize
9808                    };
9809                    out.push(items.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
9810                }
9811                let arr = StrykeValue::array(out);
9812                if ctx != WantarrayCtx::List {
9813                    let v = arr.to_list();
9814                    Ok(v.last().cloned().unwrap_or(StrykeValue::UNDEF))
9815                } else {
9816                    Ok(arr)
9817                }
9818            }
9819
9820            // References
9821            ExprKind::ScalarRef(inner) => match &inner.kind {
9822                ExprKind::ScalarVar(name) => Ok(StrykeValue::scalar_binding_ref(name.clone())),
9823                ExprKind::ArrayVar(name) => {
9824                    self.check_strict_array_var(name, line)?;
9825                    let aname = self.stash_array_name_for_package(name);
9826                    // Promote the scope's array to shared Arc-backed storage.
9827                    // Both the scope and the returned ref share the same Arc.
9828                    let arc = self.scope.promote_array_to_shared(&aname);
9829                    Ok(StrykeValue::array_ref(arc))
9830                }
9831                ExprKind::HashVar(name) => {
9832                    self.check_strict_hash_var(name, line)?;
9833                    let arc = self.scope.promote_hash_to_shared(name);
9834                    Ok(StrykeValue::hash_ref(arc))
9835                }
9836                ExprKind::Deref {
9837                    expr: e,
9838                    kind: Sigil::Array,
9839                } => {
9840                    let v = self.eval_expr(e)?;
9841                    self.make_array_ref_alias(v, line)
9842                }
9843                ExprKind::Deref {
9844                    expr: e,
9845                    kind: Sigil::Hash,
9846                } => {
9847                    let v = self.eval_expr(e)?;
9848                    self.make_hash_ref_alias(v, line)
9849                }
9850                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
9851                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
9852                    Ok(StrykeValue::array_ref(Arc::new(RwLock::new(
9853                        list.to_list(),
9854                    ))))
9855                }
9856                ExprKind::HashSliceDeref { .. } => {
9857                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
9858                    Ok(StrykeValue::array_ref(Arc::new(RwLock::new(
9859                        list.to_list(),
9860                    ))))
9861                }
9862                _ => {
9863                    let val = self.eval_expr(inner)?;
9864                    Ok(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))))
9865                }
9866            },
9867            ExprKind::ArrayRef(elems) => {
9868                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
9869                // variables flatten into the ref rather than collapsing to a scalar count /
9870                // flip-flop value.
9871                let mut arr = Vec::with_capacity(elems.len());
9872                for e in elems {
9873                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9874                    let v = self.scope.resolve_container_binding_ref(v);
9875                    if let Some(vec) = v.as_array_vec() {
9876                        arr.extend(vec);
9877                    } else {
9878                        arr.push(v);
9879                    }
9880                }
9881                Ok(StrykeValue::array_ref(Arc::new(RwLock::new(arr))))
9882            }
9883            ExprKind::HashRef(pairs) => {
9884                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
9885                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
9886                let mut map = IndexMap::new();
9887                for (k, v) in pairs {
9888                    let key_str = self.eval_expr(k)?.to_string();
9889                    if key_str == "__HASH_SPREAD__" {
9890                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
9891                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
9892                        let items = spread.to_list();
9893                        let mut i = 0;
9894                        while i + 1 < items.len() {
9895                            map.insert(items[i].to_string(), items[i + 1].clone());
9896                            i += 2;
9897                        }
9898                    } else {
9899                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
9900                        map.insert(key_str, val);
9901                    }
9902                }
9903                Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map))))
9904            }
9905            ExprKind::CodeRef { params, body } => {
9906                let captured = self.scope.capture();
9907                Ok(StrykeValue::code_ref(Arc::new(StrykeSub {
9908                    name: "__ANON__".to_string(),
9909                    params: params.clone(),
9910                    body: body.clone(),
9911                    closure_env: Some(captured),
9912                    prototype: None,
9913                    fib_like: None,
9914                })))
9915            }
9916            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
9917            ExprKind::SubroutineCodeRef(name) => {
9918                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
9919                    StrykeError::runtime(self.undefined_subroutine_resolve_message(name), line)
9920                })?;
9921                Ok(StrykeValue::code_ref(sub))
9922            }
9923            ExprKind::DynamicSubCodeRef(expr) => {
9924                let name = self.eval_expr(expr)?.to_string();
9925                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
9926                    StrykeError::runtime(self.undefined_subroutine_resolve_message(&name), line)
9927                })?;
9928                Ok(StrykeValue::code_ref(sub))
9929            }
9930            ExprKind::Deref { expr, kind } => {
9931                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
9932                    let val = self.eval_expr(expr)?;
9933                    let n = self.array_deref_len(val, line)?;
9934                    return Ok(StrykeValue::integer(n));
9935                }
9936                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
9937                    let val = self.eval_expr(expr)?;
9938                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
9939                    return Ok(h.scalar_context());
9940                }
9941                let val = self.eval_expr(expr)?;
9942                self.symbolic_deref(val, *kind, line)
9943            }
9944            ExprKind::ArrowDeref { expr, index, kind } => {
9945                match kind {
9946                    DerefKind::Array => {
9947                        let container = self.eval_arrow_array_base(expr, line)?;
9948                        if let ExprKind::List(indices) = &index.kind {
9949                            let mut out = Vec::with_capacity(indices.len());
9950                            for ix in indices {
9951                                let idx = self.eval_expr(ix)?.to_int();
9952                                out.push(self.read_arrow_array_element(
9953                                    container.clone(),
9954                                    idx,
9955                                    line,
9956                                )?);
9957                            }
9958                            let arr = StrykeValue::array(out);
9959                            if ctx != WantarrayCtx::List {
9960                                let v = arr.to_list();
9961                                return Ok(v.last().cloned().unwrap_or(StrykeValue::UNDEF));
9962                            }
9963                            return Ok(arr);
9964                        }
9965                        let idx = self.eval_expr(index)?.to_int();
9966                        self.read_arrow_array_element(container, idx, line)
9967                    }
9968                    DerefKind::Hash => {
9969                        let val = self.eval_arrow_hash_base(expr, line)?;
9970                        let key = self.eval_expr(index)?.to_string();
9971                        self.read_arrow_hash_element(val, key.as_str(), line)
9972                    }
9973                    DerefKind::Call => {
9974                        // $coderef->(args). BUG-037: explicit `@array` / `%hash`
9975                        // arguments flatten into the call list (mirrors Perl's
9976                        // call list semantics so `$f->(@_)` does not pass
9977                        // `scalar(@_)`). Other arg shapes — `qw(...)`, list
9978                        // expressions, function calls — keep the original
9979                        // scalar-context evaluation so threading-style call
9980                        // sites (`~> qw(a b c d) fn { ... }`) pass the LHS as
9981                        // one threaded value rather than as flattened elements.
9982                        let val = self.eval_expr(expr)?;
9983                        if let ExprKind::List(ref arg_exprs) = index.kind {
9984                            let mut args = Vec::with_capacity(arg_exprs.len());
9985                            for a in arg_exprs {
9986                                if matches!(a.kind, ExprKind::ArrayVar(_) | ExprKind::HashVar(_)) {
9987                                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
9988                                    if let Some(items) = v.as_array_vec() {
9989                                        args.extend(items);
9990                                    } else {
9991                                        args.push(v);
9992                                    }
9993                                } else {
9994                                    args.push(self.eval_expr(a)?);
9995                                }
9996                            }
9997                            // Auto-deref ScalarRef for closure self-reference: $f->()
9998                            let callable = if let Some(inner) = val.as_scalar_ref() {
9999                                inner.read().clone()
10000                            } else {
10001                                val
10002                            };
10003                            if let Some(sub) = callable.as_code_ref() {
10004                                return self.call_sub(&sub, args, ctx, line);
10005                            }
10006                            Err(StrykeError::runtime("Not a code reference", line).into())
10007                        } else {
10008                            Err(StrykeError::runtime("Invalid call deref", line).into())
10009                        }
10010                    }
10011                }
10012            }
10013
10014            // Binary operators
10015            ExprKind::BinOp { left, op, right } => {
10016                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
10017                match op {
10018                    BinOp::BindMatch => {
10019                        let lv = self.eval_expr(left)?;
10020                        let rv = self.eval_expr(right)?;
10021                        let s = lv.to_string();
10022                        let pat = rv.to_string();
10023                        return self.regex_match_execute(s, &pat, "", false, "_", line);
10024                    }
10025                    BinOp::BindNotMatch => {
10026                        let lv = self.eval_expr(left)?;
10027                        let rv = self.eval_expr(right)?;
10028                        let s = lv.to_string();
10029                        let pat = rv.to_string();
10030                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
10031                        return Ok(StrykeValue::integer(if m.is_true() { 0 } else { 1 }));
10032                    }
10033                    BinOp::LogAnd | BinOp::LogAndWord => {
10034                        match &left.kind {
10035                            ExprKind::Regex(_, _) => {
10036                                if !self.eval_boolean_rvalue_condition(left)? {
10037                                    return Ok(StrykeValue::string(String::new()));
10038                                }
10039                            }
10040                            _ => {
10041                                let lv = self.eval_expr(left)?;
10042                                if !lv.is_true() {
10043                                    return Ok(lv);
10044                                }
10045                            }
10046                        }
10047                        return match &right.kind {
10048                            ExprKind::Regex(_, _) => Ok(StrykeValue::integer(
10049                                if self.eval_boolean_rvalue_condition(right)? {
10050                                    1
10051                                } else {
10052                                    0
10053                                },
10054                            )),
10055                            _ => self.eval_expr(right),
10056                        };
10057                    }
10058                    BinOp::LogOr | BinOp::LogOrWord => {
10059                        match &left.kind {
10060                            ExprKind::Regex(_, _) => {
10061                                if self.eval_boolean_rvalue_condition(left)? {
10062                                    return Ok(StrykeValue::integer(1));
10063                                }
10064                            }
10065                            _ => {
10066                                let lv = self.eval_expr(left)?;
10067                                if lv.is_true() {
10068                                    return Ok(lv);
10069                                }
10070                            }
10071                        }
10072                        return match &right.kind {
10073                            ExprKind::Regex(_, _) => Ok(StrykeValue::integer(
10074                                if self.eval_boolean_rvalue_condition(right)? {
10075                                    1
10076                                } else {
10077                                    0
10078                                },
10079                            )),
10080                            _ => self.eval_expr(right),
10081                        };
10082                    }
10083                    BinOp::DefinedOr => {
10084                        let lv = self.eval_expr(left)?;
10085                        if !lv.is_undef() {
10086                            return Ok(lv);
10087                        }
10088                        return self.eval_expr(right);
10089                    }
10090                    _ => {}
10091                }
10092                let lv = self.eval_expr(left)?;
10093                let rv = self.eval_expr(right)?;
10094                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
10095                    return r;
10096                }
10097                self.eval_binop(*op, &lv, &rv, line)
10098            }
10099
10100            // Unary
10101            ExprKind::UnaryOp { op, expr } => match op {
10102                UnaryOp::PreIncrement => {
10103                    if let ExprKind::ScalarVar(name) = &expr.kind {
10104                        self.check_strict_scalar_var(name, line)?;
10105                        let n = self.resolved_scalar_storage_name(name);
10106                        return Ok(self
10107                            .scope
10108                            .atomic_mutate(&n, perl_inc)
10109                            .map_err(|e| e.at_line(line))?);
10110                    }
10111                    if let ExprKind::Deref { kind, .. } = &expr.kind {
10112                        if matches!(kind, Sigil::Array | Sigil::Hash) {
10113                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
10114                                *kind, true, true, line,
10115                            ));
10116                        }
10117                    }
10118                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
10119                        let href = self.eval_expr(container)?;
10120                        let mut key_vals = Vec::with_capacity(keys.len());
10121                        for key_expr in keys {
10122                            key_vals.push(self.eval_expr(key_expr)?);
10123                        }
10124                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
10125                    }
10126                    if let ExprKind::ArrowDeref {
10127                        expr: arr_expr,
10128                        index,
10129                        kind: DerefKind::Array,
10130                    } = &expr.kind
10131                    {
10132                        if let ExprKind::List(indices) = &index.kind {
10133                            let container = self.eval_arrow_array_base(arr_expr, line)?;
10134                            let mut idxs = Vec::with_capacity(indices.len());
10135                            for ix in indices {
10136                                idxs.push(self.eval_expr(ix)?.to_int());
10137                            }
10138                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
10139                        }
10140                    }
10141                    let val = self.eval_expr(expr)?;
10142                    let new_val = perl_inc(&val);
10143                    self.assign_value(expr, new_val.clone())?;
10144                    Ok(new_val)
10145                }
10146                UnaryOp::PreDecrement => {
10147                    if let ExprKind::ScalarVar(name) = &expr.kind {
10148                        self.check_strict_scalar_var(name, line)?;
10149                        let n = self.resolved_scalar_storage_name(name);
10150                        return Ok(self
10151                            .scope
10152                            .atomic_mutate(&n, |v| StrykeValue::integer(v.to_int() - 1))
10153                            .map_err(|e| e.at_line(line))?);
10154                    }
10155                    if let ExprKind::Deref { kind, .. } = &expr.kind {
10156                        if matches!(kind, Sigil::Array | Sigil::Hash) {
10157                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
10158                                *kind, true, false, line,
10159                            ));
10160                        }
10161                    }
10162                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
10163                        let href = self.eval_expr(container)?;
10164                        let mut key_vals = Vec::with_capacity(keys.len());
10165                        for key_expr in keys {
10166                            key_vals.push(self.eval_expr(key_expr)?);
10167                        }
10168                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
10169                    }
10170                    if let ExprKind::ArrowDeref {
10171                        expr: arr_expr,
10172                        index,
10173                        kind: DerefKind::Array,
10174                    } = &expr.kind
10175                    {
10176                        if let ExprKind::List(indices) = &index.kind {
10177                            let container = self.eval_arrow_array_base(arr_expr, line)?;
10178                            let mut idxs = Vec::with_capacity(indices.len());
10179                            for ix in indices {
10180                                idxs.push(self.eval_expr(ix)?.to_int());
10181                            }
10182                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
10183                        }
10184                    }
10185                    let val = self.eval_expr(expr)?;
10186                    let new_val = StrykeValue::integer(val.to_int() - 1);
10187                    self.assign_value(expr, new_val.clone())?;
10188                    Ok(new_val)
10189                }
10190                _ => {
10191                    match op {
10192                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
10193                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
10194                                let topic = self.scope.get_scalar("_");
10195                                let rl = expr.line;
10196                                let s = topic.to_string();
10197                                let v =
10198                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
10199                                return Ok(StrykeValue::integer(if v.is_true() { 0 } else { 1 }));
10200                            }
10201                        }
10202                        _ => {}
10203                    }
10204                    let val = self.eval_expr(expr)?;
10205                    match op {
10206                        UnaryOp::Negate => {
10207                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
10208                                return r;
10209                            }
10210                            if let Some(n) = val.as_integer() {
10211                                Ok(StrykeValue::integer(-n))
10212                            } else {
10213                                Ok(StrykeValue::float(-val.to_number()))
10214                            }
10215                        }
10216                        UnaryOp::LogNot => {
10217                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
10218                                let pv = r?;
10219                                return Ok(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
10220                            }
10221                            Ok(StrykeValue::integer(if val.is_true() { 0 } else { 1 }))
10222                        }
10223                        UnaryOp::BitNot => Ok(StrykeValue::integer(!val.to_int())),
10224                        UnaryOp::LogNotWord => {
10225                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
10226                                let pv = r?;
10227                                return Ok(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
10228                            }
10229                            Ok(StrykeValue::integer(if val.is_true() { 0 } else { 1 }))
10230                        }
10231                        UnaryOp::Ref => {
10232                            if let ExprKind::ScalarVar(name) = &expr.kind {
10233                                return Ok(StrykeValue::scalar_binding_ref(name.clone()));
10234                            }
10235                            Ok(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))))
10236                        }
10237                        _ => unreachable!(),
10238                    }
10239                }
10240            },
10241
10242            ExprKind::PostfixOp { expr, op } => {
10243                // For scalar variables, use atomic_mutate_post to hold the lock
10244                // for the entire read-modify-write (critical for mysync).
10245                if let ExprKind::ScalarVar(name) = &expr.kind {
10246                    self.check_strict_scalar_var(name, line)?;
10247                    let n = self.resolved_scalar_storage_name(name);
10248                    let f: fn(&StrykeValue) -> StrykeValue = match op {
10249                        PostfixOp::Increment => |v| perl_inc(v),
10250                        PostfixOp::Decrement => |v| StrykeValue::integer(v.to_int() - 1),
10251                    };
10252                    return Ok(self
10253                        .scope
10254                        .atomic_mutate_post(&n, f)
10255                        .map_err(|e| e.at_line(line))?);
10256                }
10257                if let ExprKind::Deref { kind, .. } = &expr.kind {
10258                    if matches!(kind, Sigil::Array | Sigil::Hash) {
10259                        let is_inc = matches!(op, PostfixOp::Increment);
10260                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
10261                            *kind, false, is_inc, line,
10262                        ));
10263                    }
10264                }
10265                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
10266                    let href = self.eval_expr(container)?;
10267                    let mut key_vals = Vec::with_capacity(keys.len());
10268                    for key_expr in keys {
10269                        key_vals.push(self.eval_expr(key_expr)?);
10270                    }
10271                    let kind_byte = match op {
10272                        PostfixOp::Increment => 2u8,
10273                        PostfixOp::Decrement => 3u8,
10274                    };
10275                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
10276                }
10277                if let ExprKind::ArrowDeref {
10278                    expr: arr_expr,
10279                    index,
10280                    kind: DerefKind::Array,
10281                } = &expr.kind
10282                {
10283                    if let ExprKind::List(indices) = &index.kind {
10284                        let container = self.eval_arrow_array_base(arr_expr, line)?;
10285                        let mut idxs = Vec::with_capacity(indices.len());
10286                        for ix in indices {
10287                            idxs.push(self.eval_expr(ix)?.to_int());
10288                        }
10289                        let kind_byte = match op {
10290                            PostfixOp::Increment => 2u8,
10291                            PostfixOp::Decrement => 3u8,
10292                        };
10293                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
10294                    }
10295                }
10296                let val = self.eval_expr(expr)?;
10297                let old = val.clone();
10298                let new_val = match op {
10299                    PostfixOp::Increment => perl_inc(&val),
10300                    PostfixOp::Decrement => StrykeValue::integer(val.to_int() - 1),
10301                };
10302                self.assign_value(expr, new_val)?;
10303                Ok(old)
10304            }
10305
10306            // Assignment
10307            ExprKind::Assign { target, value } => {
10308                if let ExprKind::Typeglob(lhs) = &target.kind {
10309                    if let ExprKind::Typeglob(rhs) = &value.kind {
10310                        self.copy_typeglob_slots(lhs, rhs, line)?;
10311                        return self.eval_expr(value);
10312                    }
10313                }
10314                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
10315                self.assign_value(target, val.clone())?;
10316                Ok(val)
10317            }
10318            ExprKind::CompoundAssign { target, op, value } => {
10319                // For scalar targets, use atomic_mutate to hold the lock.
10320                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
10321                if let ExprKind::ScalarVar(name) = &target.kind {
10322                    self.check_strict_scalar_var(name, line)?;
10323                    let n = self.resolved_scalar_storage_name(name);
10324                    let op = *op;
10325                    let rhs = match op {
10326                        BinOp::LogOr => {
10327                            let old = self.scope.get_scalar(&n);
10328                            if old.is_true() {
10329                                return Ok(old);
10330                            }
10331                            self.eval_expr(value)?
10332                        }
10333                        BinOp::DefinedOr => {
10334                            let old = self.scope.get_scalar(&n);
10335                            if !old.is_undef() {
10336                                return Ok(old);
10337                            }
10338                            self.eval_expr(value)?
10339                        }
10340                        BinOp::LogAnd => {
10341                            let old = self.scope.get_scalar(&n);
10342                            if !old.is_true() {
10343                                return Ok(old);
10344                            }
10345                            self.eval_expr(value)?
10346                        }
10347                        _ => self.eval_expr(value)?,
10348                    };
10349                    return Ok(self.scalar_compound_assign_scalar_target(&n, op, rhs)?);
10350                }
10351                let rhs = self.eval_expr(value)?;
10352                // For hash element targets: $h{key} += 1
10353                if let ExprKind::HashElement { hash, key } = &target.kind {
10354                    self.check_strict_hash_var(hash, line)?;
10355                    let k = self.eval_expr(key)?.to_string();
10356                    let op = *op;
10357                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
10358                        BinOp::Add => {
10359                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10360                                StrykeValue::integer(a.wrapping_add(b))
10361                            } else {
10362                                StrykeValue::float(old.to_number() + rhs.to_number())
10363                            }
10364                        }
10365                        BinOp::Sub => {
10366                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10367                                StrykeValue::integer(a.wrapping_sub(b))
10368                            } else {
10369                                StrykeValue::float(old.to_number() - rhs.to_number())
10370                            }
10371                        }
10372                        BinOp::Concat => {
10373                            let mut s = old.to_string();
10374                            rhs.append_to(&mut s);
10375                            StrykeValue::string(s)
10376                        }
10377                        _ => StrykeValue::float(old.to_number() + rhs.to_number()),
10378                    })?);
10379                }
10380                // For array element targets: $a[i] += 1
10381                if let ExprKind::ArrayElement { array, index } = &target.kind {
10382                    self.check_strict_array_var(array, line)?;
10383                    let idx = self.eval_expr(index)?.to_int();
10384                    let op = *op;
10385                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
10386                        BinOp::Add => {
10387                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10388                                StrykeValue::integer(a.wrapping_add(b))
10389                            } else {
10390                                StrykeValue::float(old.to_number() + rhs.to_number())
10391                            }
10392                        }
10393                        BinOp::Sub => {
10394                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10395                                StrykeValue::integer(a.wrapping_sub(b))
10396                            } else {
10397                                StrykeValue::float(old.to_number() - rhs.to_number())
10398                            }
10399                        }
10400                        BinOp::Mul => {
10401                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10402                                StrykeValue::integer(a.wrapping_mul(b))
10403                            } else {
10404                                StrykeValue::float(old.to_number() * rhs.to_number())
10405                            }
10406                        }
10407                        BinOp::Div => StrykeValue::float(old.to_number() / rhs.to_number()),
10408                        BinOp::Mod => {
10409                            // Perl `%` is floored-division (sign-of-divisor),
10410                            // not Rust's `%` (sign-of-dividend) nor
10411                            // `rem_euclid` (always non-negative). Truncate
10412                            // float operands to int first, matching Perl 5.
10413                            let a = old.to_int();
10414                            let b = rhs.to_int();
10415                            if b == 0 {
10416                                StrykeValue::integer(0)
10417                            } else {
10418                                StrykeValue::integer(crate::value::perl_mod_i64(a, b))
10419                            }
10420                        }
10421                        BinOp::Concat => {
10422                            let mut s = old.to_string();
10423                            rhs.append_to(&mut s);
10424                            StrykeValue::string(s)
10425                        }
10426                        BinOp::Pow => StrykeValue::float(old.to_number().powf(rhs.to_number())),
10427                        BinOp::BitAnd => StrykeValue::integer(old.to_int() & rhs.to_int()),
10428                        BinOp::BitOr => StrykeValue::integer(old.to_int() | rhs.to_int()),
10429                        BinOp::BitXor => StrykeValue::integer(old.to_int() ^ rhs.to_int()),
10430                        BinOp::ShiftLeft => {
10431                            StrykeValue::integer(perl_shl_i64(old.to_int(), rhs.to_int()))
10432                        }
10433                        BinOp::ShiftRight => {
10434                            StrykeValue::integer(perl_shr_i64(old.to_int(), rhs.to_int()))
10435                        }
10436                        _ => StrykeValue::float(old.to_number() + rhs.to_number()),
10437                    })?);
10438                }
10439                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
10440                    let href = self.eval_expr(container)?;
10441                    let mut key_vals = Vec::with_capacity(keys.len());
10442                    for key_expr in keys {
10443                        key_vals.push(self.eval_expr(key_expr)?);
10444                    }
10445                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
10446                }
10447                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
10448                    if let ExprKind::Deref {
10449                        expr: inner,
10450                        kind: Sigil::Array,
10451                    } = &source.kind
10452                    {
10453                        let container = self.eval_arrow_array_base(inner, line)?;
10454                        let idxs = self.flatten_array_slice_index_specs(indices)?;
10455                        return self
10456                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
10457                    }
10458                }
10459                if let ExprKind::ArrowDeref {
10460                    expr: arr_expr,
10461                    index,
10462                    kind: DerefKind::Array,
10463                } = &target.kind
10464                {
10465                    if let ExprKind::List(indices) = &index.kind {
10466                        let container = self.eval_arrow_array_base(arr_expr, line)?;
10467                        let mut idxs = Vec::with_capacity(indices.len());
10468                        for ix in indices {
10469                            idxs.push(self.eval_expr(ix)?.to_int());
10470                        }
10471                        return self
10472                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
10473                    }
10474                }
10475                let old = self.eval_expr(target)?;
10476                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
10477                self.assign_value(target, new_val.clone())?;
10478                Ok(new_val)
10479            }
10480
10481            // Ternary — propagate wantarray context to both branches so
10482            // `($a, $b) = $c ? (1, 2) : (3, 4)` evaluates the chosen branch
10483            // in list context.
10484            ExprKind::Ternary {
10485                condition,
10486                then_expr,
10487                else_expr,
10488            } => {
10489                if self.eval_boolean_rvalue_condition(condition)? {
10490                    self.eval_expr_ctx(then_expr, ctx)
10491                } else {
10492                    self.eval_expr_ctx(else_expr, ctx)
10493                }
10494            }
10495
10496            // Range
10497            ExprKind::Range {
10498                from,
10499                to,
10500                exclusive,
10501                step,
10502            } => {
10503                if ctx == WantarrayCtx::List {
10504                    let f = self.eval_expr(from)?;
10505                    let t = self.eval_expr(to)?;
10506                    if let Some(s) = step {
10507                        let step_val = self.eval_expr(s)?.to_int();
10508                        let from_i = f.to_int();
10509                        let to_i = t.to_int();
10510                        let list = if step_val == 0 {
10511                            vec![]
10512                        } else if step_val > 0 {
10513                            (from_i..=to_i)
10514                                .step_by(step_val as usize)
10515                                .map(StrykeValue::integer)
10516                                .collect()
10517                        } else {
10518                            std::iter::successors(Some(from_i), |&x| {
10519                                let next = x - step_val.abs();
10520                                if next >= to_i {
10521                                    Some(next)
10522                                } else {
10523                                    None
10524                                }
10525                            })
10526                            .map(StrykeValue::integer)
10527                            .collect()
10528                        };
10529                        Ok(StrykeValue::array(list))
10530                    } else {
10531                        let list = perl_list_range_expand(f, t);
10532                        Ok(StrykeValue::array(list))
10533                    }
10534                } else {
10535                    let key = std::ptr::from_ref(expr) as usize;
10536                    match (&from.kind, &to.kind) {
10537                        (
10538                            ExprKind::Regex(left_pat, left_flags),
10539                            ExprKind::Regex(right_pat, right_flags),
10540                        ) => {
10541                            let dot = self.scalar_flipflop_dot_line();
10542                            let subject = self.scope.get_scalar("_").to_string();
10543                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10544                                |e| match e {
10545                                    FlowOrError::Error(err) => err,
10546                                    FlowOrError::Flow(_) => StrykeError::runtime(
10547                                        "unexpected flow in regex flip-flop",
10548                                        line,
10549                                    ),
10550                                },
10551                            )?;
10552                            let right_re = self
10553                                .compile_regex(right_pat, right_flags, line)
10554                                .map_err(|e| match e {
10555                                    FlowOrError::Error(err) => err,
10556                                    FlowOrError::Flow(_) => StrykeError::runtime(
10557                                        "unexpected flow in regex flip-flop",
10558                                        line,
10559                                    ),
10560                                })?;
10561                            let left_m = left_re.is_match(&subject);
10562                            let right_m = right_re.is_match(&subject);
10563                            let st = self.flip_flop_tree.entry(key).or_default();
10564                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10565                                &mut st.active,
10566                                &mut st.exclusive_left_line,
10567                                *exclusive,
10568                                dot,
10569                                left_m,
10570                                right_m,
10571                            )))
10572                        }
10573                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
10574                            let dot = self.scalar_flipflop_dot_line();
10575                            let subject = self.scope.get_scalar("_").to_string();
10576                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10577                                |e| match e {
10578                                    FlowOrError::Error(err) => err,
10579                                    FlowOrError::Flow(_) => StrykeError::runtime(
10580                                        "unexpected flow in regex/eof flip-flop",
10581                                        line,
10582                                    ),
10583                                },
10584                            )?;
10585                            let left_m = left_re.is_match(&subject);
10586                            let right_m = self.eof_without_arg_is_true();
10587                            let st = self.flip_flop_tree.entry(key).or_default();
10588                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10589                                &mut st.active,
10590                                &mut st.exclusive_left_line,
10591                                *exclusive,
10592                                dot,
10593                                left_m,
10594                                right_m,
10595                            )))
10596                        }
10597                        (
10598                            ExprKind::Regex(left_pat, left_flags),
10599                            ExprKind::Integer(_) | ExprKind::Float(_),
10600                        ) => {
10601                            let dot = self.scalar_flipflop_dot_line();
10602                            let right = self.eval_expr(to)?.to_int();
10603                            let subject = self.scope.get_scalar("_").to_string();
10604                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10605                                |e| match e {
10606                                    FlowOrError::Error(err) => err,
10607                                    FlowOrError::Flow(_) => StrykeError::runtime(
10608                                        "unexpected flow in regex flip-flop",
10609                                        line,
10610                                    ),
10611                                },
10612                            )?;
10613                            let left_m = left_re.is_match(&subject);
10614                            let right_m = dot == right;
10615                            let st = self.flip_flop_tree.entry(key).or_default();
10616                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10617                                &mut st.active,
10618                                &mut st.exclusive_left_line,
10619                                *exclusive,
10620                                dot,
10621                                left_m,
10622                                right_m,
10623                            )))
10624                        }
10625                        (ExprKind::Regex(left_pat, left_flags), _) => {
10626                            if let ExprKind::Eof(Some(_)) = &to.kind {
10627                                return Err(FlowOrError::Error(StrykeError::runtime(
10628                                    "regex flip-flop with eof(HANDLE) is not supported",
10629                                    line,
10630                                )));
10631                            }
10632                            let dot = self.scalar_flipflop_dot_line();
10633                            let subject = self.scope.get_scalar("_").to_string();
10634                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10635                                |e| match e {
10636                                    FlowOrError::Error(err) => err,
10637                                    FlowOrError::Flow(_) => StrykeError::runtime(
10638                                        "unexpected flow in regex flip-flop",
10639                                        line,
10640                                    ),
10641                                },
10642                            )?;
10643                            let left_m = left_re.is_match(&subject);
10644                            let right_m = self.eval_boolean_rvalue_condition(to)?;
10645                            let st = self.flip_flop_tree.entry(key).or_default();
10646                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10647                                &mut st.active,
10648                                &mut st.exclusive_left_line,
10649                                *exclusive,
10650                                dot,
10651                                left_m,
10652                                right_m,
10653                            )))
10654                        }
10655                        _ => {
10656                            let left = self.eval_expr(from)?.to_int();
10657                            let right = self.eval_expr(to)?.to_int();
10658                            let dot = self.scalar_flipflop_dot_line();
10659                            let st = self.flip_flop_tree.entry(key).or_default();
10660                            if !st.active {
10661                                if dot == left {
10662                                    st.active = true;
10663                                    if *exclusive {
10664                                        st.exclusive_left_line = Some(dot);
10665                                    } else {
10666                                        st.exclusive_left_line = None;
10667                                        if dot == right {
10668                                            st.active = false;
10669                                        }
10670                                    }
10671                                    return Ok(StrykeValue::integer(1));
10672                                }
10673                                return Ok(StrykeValue::integer(0));
10674                            }
10675                            if let Some(ll) = st.exclusive_left_line {
10676                                if dot == right && dot > ll {
10677                                    st.active = false;
10678                                    st.exclusive_left_line = None;
10679                                }
10680                            } else if dot == right {
10681                                st.active = false;
10682                            }
10683                            Ok(StrykeValue::integer(1))
10684                        }
10685                    }
10686                }
10687            }
10688
10689            // SliceRange — open-ended Python-style slice expansion. Reachable from the
10690            // tree-walker when slice subscripts are evaluated outside the VM (rare; VM is
10691            // the primary execution engine). Only closed forms (`from:to[:step]`) can be
10692            // expanded here without container length context; open ends require a slice
10693            // op (`Op::ArraySliceRange` / `Op::HashSliceRange`) which knows the container.
10694            ExprKind::SliceRange { from, to, step } => {
10695                let f = match from {
10696                    Some(e) => self.eval_expr(e)?,
10697                    None => {
10698                        return Err(StrykeError::runtime(
10699                            "open-ended slice range cannot be evaluated outside slice subscript",
10700                            line,
10701                        )
10702                        .into());
10703                    }
10704                };
10705                let t = match to {
10706                    Some(e) => self.eval_expr(e)?,
10707                    None => {
10708                        return Err(StrykeError::runtime(
10709                            "open-ended slice range cannot be evaluated outside slice subscript",
10710                            line,
10711                        )
10712                        .into());
10713                    }
10714                };
10715                let list = if let Some(s) = step {
10716                    let sv = self.eval_expr(s)?;
10717                    crate::value::perl_list_range_expand_stepped(f, t, sv)
10718                } else {
10719                    perl_list_range_expand(f, t)
10720                };
10721                Ok(StrykeValue::array(list))
10722            }
10723
10724            // Repeat — see `ast.rs` `ExprKind::Repeat` for the list/scalar split.
10725            ExprKind::Repeat {
10726                expr,
10727                count,
10728                list_repeat,
10729            } => {
10730                let n = self.eval_expr(count)?.to_int().max(0) as usize;
10731                if *list_repeat {
10732                    // `(LIST) x N` — evaluate the LHS in list context, replicate.
10733                    let saved = self.wantarray_kind;
10734                    self.wantarray_kind = WantarrayCtx::List;
10735                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10736                    self.wantarray_kind = saved;
10737                    let items: Vec<StrykeValue> = val.as_array_vec().unwrap_or_else(|| vec![val]);
10738                    let mut result = Vec::with_capacity(items.len() * n);
10739                    for _ in 0..n {
10740                        result.extend(items.iter().cloned());
10741                    }
10742                    Ok(StrykeValue::array(result))
10743                } else {
10744                    // `EXPR x N` — scalar string repetition.
10745                    let val = self.eval_expr(expr)?;
10746                    Ok(StrykeValue::string(val.to_string().repeat(n)))
10747                }
10748            }
10749
10750            // `my $x = …` / `our` / `state` / `local` used as an expression
10751            // (e.g. `if (my $line = readline)`).  Declare each variable in the
10752            // current scope, evaluate the initializer (if any), and return the
10753            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
10754            ExprKind::MyExpr { keyword, decls } => {
10755                // Build a temporary statement and dispatch to the canonical
10756                // statement handler so behavior matches `my $x = …;` exactly.
10757                let stmt_kind = match keyword.as_str() {
10758                    "my" => StmtKind::My(decls.clone()),
10759                    "our" => StmtKind::Our(decls.clone()),
10760                    "state" => StmtKind::State(decls.clone()),
10761                    "local" => StmtKind::Local(decls.clone()),
10762                    _ => StmtKind::My(decls.clone()),
10763                };
10764                let stmt = Statement {
10765                    label: None,
10766                    kind: stmt_kind,
10767                    line,
10768                };
10769                self.exec_statement(&stmt)?;
10770                // Return the value of the (first) declared variable so the
10771                // surrounding expression sees the assigned value, matching
10772                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
10773                let first = decls.first().ok_or_else(|| {
10774                    FlowOrError::Error(StrykeError::runtime("MyExpr: empty decl list", line))
10775                })?;
10776                Ok(match first.sigil {
10777                    Sigil::Scalar => self.scope.get_scalar(&first.name),
10778                    Sigil::Array => StrykeValue::array(self.scope.get_array(&first.name)),
10779                    Sigil::Hash => {
10780                        let h = self.scope.get_hash(&first.name);
10781                        let mut flat: Vec<StrykeValue> = Vec::with_capacity(h.len() * 2);
10782                        for (k, v) in h {
10783                            flat.push(StrykeValue::string(k));
10784                            flat.push(v);
10785                        }
10786                        StrykeValue::array(flat)
10787                    }
10788                    Sigil::Typeglob => StrykeValue::UNDEF,
10789                })
10790            }
10791
10792            // Function calls
10793            ExprKind::FuncCall { name, args } => {
10794                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
10795                // bare-name dispatch so the matches below stay flat.
10796                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
10797                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
10798                if matches!(dispatch_name, "read") && args.len() >= 3 {
10799                    let fh_val = self.eval_expr(&args[0])?;
10800                    let fh = fh_val
10801                        .as_io_handle_name()
10802                        .unwrap_or_else(|| fh_val.to_string());
10803                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
10804                    let offset = if args.len() > 3 {
10805                        self.eval_expr(&args[3])?.to_int().max(0) as usize
10806                    } else {
10807                        0
10808                    };
10809                    // Extract the variable name from the AST
10810                    let var_name = match &args[1].kind {
10811                        ExprKind::ScalarVar(n) => n.clone(),
10812                        _ => self.eval_expr(&args[1])?.to_string(),
10813                    };
10814                    let mut buf = vec![0u8; len];
10815                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
10816                        slot.lock().read(&mut buf).unwrap_or(0)
10817                    } else if fh == "STDIN" {
10818                        std::io::stdin().read(&mut buf).unwrap_or(0)
10819                    } else {
10820                        return Err(StrykeError::runtime(
10821                            format!("read: unopened handle {}", fh),
10822                            line,
10823                        )
10824                        .into());
10825                    };
10826                    buf.truncate(n);
10827                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
10828                    if offset > 0 {
10829                        let mut existing = self.scope.get_scalar(&var_name).to_string();
10830                        while existing.len() < offset {
10831                            existing.push('\0');
10832                        }
10833                        existing.push_str(&read_str);
10834                        let _ = self
10835                            .scope
10836                            .set_scalar(&var_name, StrykeValue::string(existing));
10837                    } else {
10838                        let _ = self
10839                            .scope
10840                            .set_scalar(&var_name, StrykeValue::string(read_str));
10841                    }
10842                    return Ok(StrykeValue::integer(n as i64));
10843                }
10844                if matches!(dispatch_name, "group_by" | "chunk_by") {
10845                    if args.len() != 2 {
10846                        return Err(StrykeError::runtime(
10847                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
10848                            line,
10849                        )
10850                        .into());
10851                    }
10852                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
10853                }
10854                if matches!(dispatch_name, "puniq" | "pfirst" | "pany") {
10855                    let mut arg_vals = Vec::with_capacity(args.len());
10856                    for a in args {
10857                        arg_vals.push(self.eval_expr(a)?);
10858                    }
10859                    let saved_wa = self.wantarray_kind;
10860                    self.wantarray_kind = ctx;
10861                    let r = self.eval_par_list_call(dispatch_name, &arg_vals, ctx, line);
10862                    self.wantarray_kind = saved_wa;
10863                    return r.map_err(Into::into);
10864                }
10865                let arg_vals = if matches!(dispatch_name, "any" | "all" | "none" | "first")
10866                    || matches!(
10867                        dispatch_name,
10868                        "take_while"
10869                            | "drop_while"
10870                            | "skip_while"
10871                            | "reject"
10872                            | "grepv"
10873                            | "tap"
10874                            | "peek"
10875                    )
10876                    || matches!(
10877                        dispatch_name,
10878                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
10879                    ) {
10880                    if args.len() != 2 {
10881                        return Err(StrykeError::runtime(
10882                            format!("{}: expected BLOCK, LIST", name),
10883                            line,
10884                        )
10885                        .into());
10886                    }
10887                    let cr = self.eval_expr(&args[0])?;
10888                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
10889                    let mut v = vec![cr];
10890                    v.extend(list_src.to_list());
10891                    v
10892                } else if matches!(
10893                    dispatch_name,
10894                    "zip"
10895                        | "zip_longest"
10896                        | "zip_shortest"
10897                        | "mesh"
10898                        | "mesh_longest"
10899                        | "mesh_shortest"
10900                ) {
10901                    let mut v = Vec::with_capacity(args.len());
10902                    for a in args {
10903                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
10904                    }
10905                    v
10906                } else if matches!(
10907                    dispatch_name,
10908                    "count" | "size" | "cnt" | "len" | "list_count" | "list_size"
10909                ) {
10910                    // Count-family: preserve the "user wrote 1 syntactic arg" signal.
10911                    // Flattening the lone operand here collapses `count(@empty)` to a
10912                    // zero-arg call, which would then fall back to `$_` topic — wrong.
10913                    // Pass the single evaluated value directly so the builtin's 1-arg
10914                    // path can dispatch on its type (string → chars, array/aref →
10915                    // element count via map_flatten_outputs, hash → key count, …).
10916                    let mut list_out = Vec::new();
10917                    if args.len() == 1 {
10918                        list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10919                    } else {
10920                        for a in args {
10921                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
10922                        }
10923                    }
10924                    list_out
10925                } else if matches!(
10926                    dispatch_name,
10927                    "uniq"
10928                        | "distinct"
10929                        | "uniqstr"
10930                        | "uniqint"
10931                        | "uniqnum"
10932                        | "flatten"
10933                        | "set"
10934                        | "with_index"
10935                        | "shuffle"
10936                        | "sum"
10937                        | "sum0"
10938                        | "product"
10939                        | "min"
10940                        | "max"
10941                        | "minstr"
10942                        | "maxstr"
10943                        | "mean"
10944                        | "median"
10945                        | "mode"
10946                        | "stddev"
10947                        | "variance"
10948                        | "pairs"
10949                        | "unpairs"
10950                        | "pairkeys"
10951                        | "pairvalues"
10952                ) {
10953                    // Slurpy list `(@)`: one list expr (`uniq @x`) or multiple actuals
10954                    // (`uniq(1, 1, 2)`). Each actual is evaluated in list context so
10955                    // `@a, @b` flattens.
10956                    let mut list_out = Vec::new();
10957                    if args.len() == 1 {
10958                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
10959                    } else {
10960                        for a in args {
10961                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
10962                        }
10963                    }
10964                    list_out
10965                } else if matches!(dispatch_name, "take" | "head" | "tail" | "drop") {
10966                    if args.is_empty() {
10967                        return Err(StrykeError::runtime(
10968                            "take/head/tail/drop: need LIST..., N or unary N",
10969                            line,
10970                        )
10971                        .into());
10972                    }
10973                    let mut arg_vals = Vec::with_capacity(args.len());
10974                    if args.len() == 1 {
10975                        // head @l == head @l, 1 — evaluate in list context
10976                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10977                    } else {
10978                        for a in &args[..args.len() - 1] {
10979                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
10980                        }
10981                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
10982                    }
10983                    arg_vals
10984                } else if matches!(dispatch_name, "chunked" | "windowed") {
10985                    let mut list_out = Vec::new();
10986                    match args.len() {
10987                        0 => {
10988                            return Err(StrykeError::runtime(
10989                                format!("{name}: expected (LIST, N) or unary N after |>"),
10990                                line,
10991                            )
10992                            .into());
10993                        }
10994                        1 => {
10995                            // chunked @l / windowed @l — evaluate in list context, default size
10996                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10997                        }
10998                        2 => {
10999                            list_out.extend(
11000                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
11001                            );
11002                            list_out.push(self.eval_expr(&args[1])?);
11003                        }
11004                        _ => {
11005                            return Err(StrykeError::runtime(
11006                                format!(
11007                                    "{name}: expected exactly (LIST, N); use one list expression then size"
11008                                ),
11009                                line,
11010                            )
11011                            .into());
11012                        }
11013                    }
11014                    list_out
11015                } else {
11016                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
11017                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
11018                    let mut arg_vals = Vec::with_capacity(args.len());
11019                    for a in args {
11020                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
11021                        if let Some(items) = v.as_array_vec() {
11022                            arg_vals.extend(items);
11023                        } else {
11024                            arg_vals.push(v);
11025                        }
11026                    }
11027                    arg_vals
11028                };
11029                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
11030                let saved_wa = self.wantarray_kind;
11031                self.wantarray_kind = ctx;
11032                // Builtins first — immune to monkey-patching (matches VM dispatch order).
11033                // In compat mode, user subs shadow builtins (Perl 5 semantics).
11034                if !crate::compat_mode() {
11035                    if matches!(
11036                        dispatch_name,
11037                        "take_while"
11038                            | "drop_while"
11039                            | "skip_while"
11040                            | "reject"
11041                            | "grepv"
11042                            | "tap"
11043                            | "peek"
11044                    ) {
11045                        let r =
11046                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
11047                        self.wantarray_kind = saved_wa;
11048                        return r.map_err(Into::into);
11049                    }
11050                    if let Some(r) =
11051                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
11052                    {
11053                        self.wantarray_kind = saved_wa;
11054                        return r.map_err(Into::into);
11055                    }
11056                }
11057                if let Some(sub) = self.resolve_sub_by_name(name) {
11058                    self.wantarray_kind = saved_wa;
11059                    let args = self.with_topic_default_args(arg_vals);
11060                    let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
11061                    return self.call_sub_with_package(&sub, args, ctx, line, pkg);
11062                }
11063                // Compat mode: check builtins after user subs (Perl 5 semantics).
11064                if crate::compat_mode() {
11065                    if matches!(
11066                        dispatch_name,
11067                        "take_while"
11068                            | "drop_while"
11069                            | "skip_while"
11070                            | "reject"
11071                            | "grepv"
11072                            | "tap"
11073                            | "peek"
11074                    ) {
11075                        let r =
11076                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
11077                        self.wantarray_kind = saved_wa;
11078                        return r.map_err(Into::into);
11079                    }
11080                    if let Some(r) =
11081                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
11082                    {
11083                        self.wantarray_kind = saved_wa;
11084                        return r.map_err(Into::into);
11085                    }
11086                }
11087                self.wantarray_kind = saved_wa;
11088                self.call_named_sub(name, arg_vals, line, ctx)
11089            }
11090            ExprKind::IndirectCall {
11091                target,
11092                args,
11093                ampersand: _,
11094                pass_caller_arglist,
11095            } => {
11096                let tval = self.eval_expr(target)?;
11097                let arg_vals = if *pass_caller_arglist {
11098                    self.scope.get_array("_")
11099                } else {
11100                    // BUG-037: explicit `@array` / `%hash` operands flatten
11101                    // into the call list. Other arg shapes (qw, list exprs,
11102                    // function calls) keep the scalar-context evaluation so
11103                    // threading-style sites (`~> EXPR fn { ... }` desugars to
11104                    // `IndirectCall { target: <fn>, args: [EXPR] }`) pass the
11105                    // LHS as one threaded value rather than as flattened
11106                    // elements.
11107                    let mut v = Vec::with_capacity(args.len());
11108                    for a in args {
11109                        if matches!(a.kind, ExprKind::ArrayVar(_) | ExprKind::HashVar(_)) {
11110                            let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
11111                            if let Some(items) = val.as_array_vec() {
11112                                v.extend(items);
11113                            } else {
11114                                v.push(val);
11115                            }
11116                        } else {
11117                            v.push(self.eval_expr(a)?);
11118                        }
11119                    }
11120                    v
11121                };
11122                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
11123            }
11124            ExprKind::MethodCall {
11125                object,
11126                method,
11127                args,
11128                super_call,
11129            } => {
11130                let obj = self.eval_expr(object)?;
11131                let mut arg_vals = vec![obj.clone()];
11132                for a in args {
11133                    arg_vals.push(self.eval_expr(a)?);
11134                }
11135                if let Some(r) =
11136                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
11137                {
11138                    return r.map_err(Into::into);
11139                }
11140                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
11141                    return r.map_err(Into::into);
11142                }
11143                // Get class name
11144                let class = if let Some(b) = obj.as_blessed_ref() {
11145                    b.class.clone()
11146                } else if let Some(s) = obj.as_str() {
11147                    s // Class->method()
11148                } else {
11149                    return Err(
11150                        StrykeError::runtime("Can't call method on non-object", line).into(),
11151                    );
11152                };
11153                if method == "VERSION" && !*super_call {
11154                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
11155                        return Ok(ver);
11156                    }
11157                }
11158                // UNIVERSAL methods: isa, can, DOES
11159                if !*super_call {
11160                    match method.as_str() {
11161                        "isa" => {
11162                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
11163                            let mro = self.mro_linearize(&class);
11164                            let result = mro.iter().any(|c| c == &target);
11165                            return Ok(StrykeValue::integer(if result { 1 } else { 0 }));
11166                        }
11167                        "can" => {
11168                            let target_method =
11169                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
11170                            let found = self
11171                                .resolve_method_full_name(&class, &target_method, false)
11172                                .and_then(|fq| self.subs.get(&fq))
11173                                .is_some();
11174                            if found {
11175                                return Ok(StrykeValue::code_ref(Arc::new(StrykeSub {
11176                                    name: target_method,
11177                                    params: vec![],
11178                                    body: vec![],
11179                                    closure_env: None,
11180                                    prototype: None,
11181                                    fib_like: None,
11182                                })));
11183                            } else {
11184                                return Ok(StrykeValue::UNDEF);
11185                            }
11186                        }
11187                        "DOES" => {
11188                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
11189                            let mro = self.mro_linearize(&class);
11190                            let result = mro.iter().any(|c| c == &target);
11191                            return Ok(StrykeValue::integer(if result { 1 } else { 0 }));
11192                        }
11193                        _ => {}
11194                    }
11195                }
11196                let full_name = self
11197                    .resolve_method_full_name(&class, method, *super_call)
11198                    .ok_or_else(|| {
11199                        StrykeError::runtime(
11200                            format!(
11201                                "Can't locate method \"{}\" for invocant \"{}\"",
11202                                method, class
11203                            ),
11204                            line,
11205                        )
11206                    })?;
11207                if let Some(sub) = self.subs.get(&full_name).cloned() {
11208                    self.call_sub(&sub, arg_vals, ctx, line)
11209                } else if method == "new" && !*super_call {
11210                    // Default constructor
11211                    self.builtin_new(&class, arg_vals, line)
11212                } else if let Some(r) =
11213                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
11214                {
11215                    r
11216                } else {
11217                    Err(StrykeError::runtime(
11218                        format!(
11219                            "Can't locate method \"{}\" in package \"{}\"",
11220                            method, class
11221                        ),
11222                        line,
11223                    )
11224                    .into())
11225                }
11226            }
11227
11228            // Print/Say/Printf
11229            ExprKind::Print { handle, args } => {
11230                self.exec_print(handle.as_deref(), args, false, line)
11231            }
11232            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
11233            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
11234            ExprKind::Die(args) => {
11235                if args.is_empty() {
11236                    // `die` with no args: re-die with current $@ or "Died"
11237                    let current = self.scope.get_scalar("@");
11238                    let msg = if current.is_undef() || current.to_string().is_empty() {
11239                        let mut m = "Died".to_string();
11240                        m.push_str(&self.die_warn_at_suffix(line));
11241                        m.push('\n');
11242                        m
11243                    } else {
11244                        current.to_string()
11245                    };
11246                    self.fire_pseudosig_die(&msg, line)?;
11247                    return Err(StrykeError::die(msg, line).into());
11248                }
11249                // Single ref argument: store the ref value in $@
11250                if args.len() == 1 {
11251                    let v = self.eval_expr(&args[0])?;
11252                    if v.as_hash_ref().is_some()
11253                        || v.as_blessed_ref().is_some()
11254                        || v.as_array_ref().is_some()
11255                        || v.as_code_ref().is_some()
11256                    {
11257                        let msg = v.to_string();
11258                        self.fire_pseudosig_die(&msg, line)?;
11259                        return Err(StrykeError::die_with_value(v, msg, line).into());
11260                    }
11261                }
11262                let mut msg = String::new();
11263                for a in args {
11264                    let v = self.eval_expr(a)?;
11265                    msg.push_str(&v.to_string());
11266                }
11267                if msg.is_empty() {
11268                    msg = "Died".to_string();
11269                }
11270                if !msg.ends_with('\n') {
11271                    msg.push_str(&self.die_warn_at_suffix(line));
11272                    msg.push('\n');
11273                }
11274                self.fire_pseudosig_die(&msg, line)?;
11275                Err(StrykeError::die(msg, line).into())
11276            }
11277            ExprKind::Warn(args) => {
11278                let mut msg = String::new();
11279                for a in args {
11280                    let v = self.eval_expr(a)?;
11281                    msg.push_str(&v.to_string());
11282                }
11283                if msg.is_empty() {
11284                    msg = "Warning: something's wrong".to_string();
11285                }
11286                if !msg.ends_with('\n') {
11287                    msg.push_str(&self.die_warn_at_suffix(line));
11288                    msg.push('\n');
11289                }
11290                self.fire_pseudosig_warn(&msg, line)?;
11291                Ok(StrykeValue::integer(1))
11292            }
11293
11294            // Regex
11295            ExprKind::Match {
11296                expr,
11297                pattern,
11298                flags,
11299                scalar_g,
11300                delim: _,
11301            } => {
11302                let val = self.eval_expr(expr)?;
11303                if val.is_iterator() {
11304                    let source = crate::map_stream::into_pull_iter(val);
11305                    let re = self.compile_regex(pattern, flags, line)?;
11306                    let global = flags.contains('g');
11307                    if global {
11308                        return Ok(StrykeValue::iterator(std::sync::Arc::new(
11309                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
11310                        )));
11311                    } else {
11312                        return Ok(StrykeValue::iterator(std::sync::Arc::new(
11313                            crate::map_stream::MatchStreamIterator::new(source, re),
11314                        )));
11315                    }
11316                }
11317                let s = val.to_string();
11318                let pos_key = match &expr.kind {
11319                    ExprKind::ScalarVar(n) => n.as_str(),
11320                    _ => "_",
11321                };
11322                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
11323            }
11324            ExprKind::Substitution {
11325                expr,
11326                pattern,
11327                replacement,
11328                flags,
11329                delim: _,
11330            } => {
11331                let val = self.eval_expr(expr)?;
11332                if val.is_iterator() {
11333                    let source = crate::map_stream::into_pull_iter(val);
11334                    let re = self.compile_regex(pattern, flags, line)?;
11335                    let global = flags.contains('g');
11336                    return Ok(StrykeValue::iterator(std::sync::Arc::new(
11337                        crate::map_stream::SubstStreamIterator::new(
11338                            source,
11339                            re,
11340                            normalize_replacement_backrefs(replacement),
11341                            global,
11342                        ),
11343                    )));
11344                }
11345                let s = val.to_string();
11346                self.regex_subst_execute(
11347                    s,
11348                    pattern,
11349                    replacement.as_str(),
11350                    flags.as_str(),
11351                    expr,
11352                    line,
11353                )
11354            }
11355            ExprKind::Transliterate {
11356                expr,
11357                from,
11358                to,
11359                flags,
11360                delim: _,
11361            } => {
11362                let val = self.eval_expr(expr)?;
11363                if val.is_iterator() {
11364                    let source = crate::map_stream::into_pull_iter(val);
11365                    return Ok(StrykeValue::iterator(std::sync::Arc::new(
11366                        crate::map_stream::TransliterateStreamIterator::new(
11367                            source, from, to, flags,
11368                        ),
11369                    )));
11370                }
11371                let s = val.to_string();
11372                self.regex_transliterate_execute(
11373                    s,
11374                    from.as_str(),
11375                    to.as_str(),
11376                    flags.as_str(),
11377                    expr,
11378                    line,
11379                )
11380            }
11381
11382            // List operations
11383            ExprKind::MapExpr {
11384                block,
11385                list,
11386                flatten_array_refs,
11387                stream,
11388            } => {
11389                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11390                if *stream {
11391                    let out =
11392                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
11393                    if ctx == WantarrayCtx::List {
11394                        return Ok(out);
11395                    }
11396                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11397                }
11398                let items = list_val.to_list();
11399                if items.len() == 1 {
11400                    if let Some(p) = items[0].as_pipeline() {
11401                        if *flatten_array_refs {
11402                            return Err(StrykeError::runtime(
11403                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
11404                                line,
11405                            )
11406                            .into());
11407                        }
11408                        let sub = self.anon_coderef_from_block(block);
11409                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
11410                        return Ok(StrykeValue::pipeline(Arc::clone(&p)));
11411                    }
11412                }
11413                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
11414                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
11415                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
11416                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
11417                let mut result = Vec::new();
11418                for item in items {
11419                    self.scope.set_topic(item);
11420                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
11421                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
11422                }
11423                if ctx == WantarrayCtx::List {
11424                    Ok(StrykeValue::array(result))
11425                } else {
11426                    Ok(StrykeValue::integer(result.len() as i64))
11427                }
11428            }
11429            ExprKind::ForEachExpr { block, list } => {
11430                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11431                // Lazy: consume iterator one-at-a-time without materializing.
11432                if list_val.is_iterator() {
11433                    let iter = list_val.into_iterator();
11434                    let mut count = 0i64;
11435                    while let Some(item) = iter.next_item() {
11436                        count += 1;
11437                        self.scope.set_topic(item);
11438                        self.exec_block(block)?;
11439                    }
11440                    return Ok(StrykeValue::integer(count));
11441                }
11442                let items = list_val.to_list();
11443                let count = items.len();
11444                for item in items {
11445                    self.scope.set_topic(item);
11446                    self.exec_block(block)?;
11447                }
11448                Ok(StrykeValue::integer(count as i64))
11449            }
11450            ExprKind::MapExprComma {
11451                expr,
11452                list,
11453                flatten_array_refs,
11454                stream,
11455            } => {
11456                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11457                if *stream {
11458                    let out =
11459                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
11460                    if ctx == WantarrayCtx::List {
11461                        return Ok(out);
11462                    }
11463                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11464                }
11465                let items = list_val.to_list();
11466                let mut result = Vec::new();
11467                for item in items {
11468                    // EXPR-form: no `{}` block boundary, so don't shift the
11469                    // topic chain or zero slot 1+. Just rebind `$_` / `$_0`.
11470                    // This makes `map _1, @$_` read the surrounding fn's
11471                    // second arg per iter; block-form `map { ... }` still
11472                    // gets a full `set_topic` via its CodeRef call.
11473                    self.scope.set_topic_local(item.clone());
11474                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11475                    // Coderef-in-block-position: `map $f, @l` calls `$f($_)`
11476                    // when `$f` is a code reference. Skipped under `--compat`
11477                    // (Perl semantics: re-evaluate expr per iteration as value).
11478                    let val = if !crate::compat_mode() {
11479                        if let Some(sub) = val.as_code_ref() {
11480                            let sub = sub.clone();
11481                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::List, line)?
11482                        } else {
11483                            val
11484                        }
11485                    } else {
11486                        val
11487                    };
11488                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
11489                }
11490                if ctx == WantarrayCtx::List {
11491                    Ok(StrykeValue::array(result))
11492                } else {
11493                    Ok(StrykeValue::integer(result.len() as i64))
11494                }
11495            }
11496            ExprKind::GrepExpr {
11497                block,
11498                list,
11499                keyword,
11500            } => {
11501                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11502                if keyword.is_stream() {
11503                    let out = self.filter_stream_block_output(list_val, block, line)?;
11504                    if ctx == WantarrayCtx::List {
11505                        return Ok(out);
11506                    }
11507                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11508                }
11509                let items = list_val.to_list();
11510                if items.len() == 1 {
11511                    if let Some(p) = items[0].as_pipeline() {
11512                        let sub = self.anon_coderef_from_block(block);
11513                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
11514                        return Ok(StrykeValue::pipeline(Arc::clone(&p)));
11515                    }
11516                }
11517                let mut result = Vec::new();
11518                for item in items {
11519                    self.scope.set_topic(item.clone());
11520                    let val = self.exec_block(block)?;
11521                    // Bare regex in block → match against $_ (Perl: /pat/ in
11522                    // grep is `$_ =~ /pat/`, not a truthy regex object).
11523                    let keep = if let Some(re) = val.as_regex() {
11524                        re.is_match(&item.to_string())
11525                    } else {
11526                        val.is_true()
11527                    };
11528                    if keep {
11529                        result.push(item);
11530                    }
11531                }
11532                if ctx == WantarrayCtx::List {
11533                    Ok(StrykeValue::array(result))
11534                } else {
11535                    Ok(StrykeValue::integer(result.len() as i64))
11536                }
11537            }
11538            ExprKind::GrepExprComma {
11539                expr,
11540                list,
11541                keyword,
11542            } => {
11543                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11544                if keyword.is_stream() {
11545                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
11546                    if ctx == WantarrayCtx::List {
11547                        return Ok(out);
11548                    }
11549                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11550                }
11551                let items = list_val.to_list();
11552                let mut result = Vec::new();
11553                for item in items {
11554                    // EXPR-form: see comment in MapExprComma above. No block
11555                    // boundary, so no chain shift; `_1` reads the surrounding
11556                    // fn's second arg per iter rather than getting nuked.
11557                    self.scope.set_topic_local(item.clone());
11558                    let val = self.eval_expr(expr)?;
11559                    // Coderef-in-block-position: `grep $f, @l` calls `$f($_)`
11560                    // when `$f` is a code reference, then filters by truthiness
11561                    // of the call result. Skipped under `--compat`.
11562                    let val = if !crate::compat_mode() {
11563                        if let Some(sub) = val.as_code_ref() {
11564                            let sub = sub.clone();
11565                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?
11566                        } else {
11567                            val
11568                        }
11569                    } else {
11570                        val
11571                    };
11572                    let keep = if let Some(re) = val.as_regex() {
11573                        re.is_match(&item.to_string())
11574                    } else {
11575                        val.is_true()
11576                    };
11577                    if keep {
11578                        result.push(item);
11579                    }
11580                }
11581                if ctx == WantarrayCtx::List {
11582                    Ok(StrykeValue::array(result))
11583                } else {
11584                    Ok(StrykeValue::integer(result.len() as i64))
11585                }
11586            }
11587            ExprKind::SortExpr { cmp, list } => {
11588                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11589                let mut items = list_val.to_list();
11590                match cmp {
11591                    Some(SortComparator::Code(code_expr)) => {
11592                        let sub = self.eval_expr(code_expr)?;
11593                        let Some(sub) = sub.as_code_ref() else {
11594                            return Err(StrykeError::runtime(
11595                                "sort: comparator must be a code reference",
11596                                line,
11597                            )
11598                            .into());
11599                        };
11600                        // Save topic chain before sort — set_sort_pair writes to $_
11601                        // which corrupts _< for subsequent pipeline stages.
11602                        let saved_topic = self.scope.save_topic_chain();
11603                        let sub = sub.clone();
11604                        items.sort_by(|a, b| {
11605                            // `set_sort_pair` keeps Perl-style `$a`/`$b` package-
11606                            // global access for `sub cmp { $a <=> $b }`. Passing
11607                            // `(a, b)` as positional args lets stryke lambdas
11608                            // `fn ($a, $b) { $b <=> $a }` receive them via @_.
11609                            self.scope.set_sort_pair(a.clone(), b.clone());
11610                            match self.call_sub(&sub, vec![a.clone(), b.clone()], ctx, line) {
11611                                Ok(v) => {
11612                                    let n = v.to_int();
11613                                    if n < 0 {
11614                                        Ordering::Less
11615                                    } else if n > 0 {
11616                                        Ordering::Greater
11617                                    } else {
11618                                        Ordering::Equal
11619                                    }
11620                                }
11621                                Err(_) => Ordering::Equal,
11622                            }
11623                        });
11624                        self.scope.restore_topic_chain(saved_topic);
11625                    }
11626                    Some(SortComparator::Block(cmp_block)) => {
11627                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
11628                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
11629                        } else {
11630                            // Save topic chain before sort — set_sort_pair writes to $_
11631                            // which corrupts _< for subsequent pipeline stages.
11632                            let saved_topic = self.scope.save_topic_chain();
11633                            let cmp_block = cmp_block.clone();
11634                            items.sort_by(|a, b| {
11635                                self.scope.set_sort_pair(a.clone(), b.clone());
11636                                match self.exec_block(&cmp_block) {
11637                                    Ok(v) => {
11638                                        let n = v.to_int();
11639                                        if n < 0 {
11640                                            Ordering::Less
11641                                        } else if n > 0 {
11642                                            Ordering::Greater
11643                                        } else {
11644                                            Ordering::Equal
11645                                        }
11646                                    }
11647                                    Err(_) => Ordering::Equal,
11648                                }
11649                            });
11650                            self.scope.restore_topic_chain(saved_topic);
11651                        }
11652                    }
11653                    None => {
11654                        items.sort_by_key(|a| a.to_string());
11655                    }
11656                }
11657                Ok(StrykeValue::array(items))
11658            }
11659            ExprKind::Rev(expr) => {
11660                // Eval in scalar context first to preserve set/hash/array ref types
11661                let val = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
11662                if val.is_iterator() {
11663                    return Ok(StrykeValue::iterator(Arc::new(
11664                        crate::value::RevIterator::new(val.into_iterator()),
11665                    )));
11666                }
11667                if let Some(s) = crate::value::set_payload(&val) {
11668                    let mut out = crate::value::PerlSet::new();
11669                    for (k, v) in s.iter().rev() {
11670                        out.insert(k.clone(), v.clone());
11671                    }
11672                    return Ok(StrykeValue::set(Arc::new(out)));
11673                }
11674                if let Some(ar) = val.as_array_ref() {
11675                    let items: Vec<_> = ar.read().iter().rev().cloned().collect();
11676                    return Ok(StrykeValue::array_ref(Arc::new(parking_lot::RwLock::new(
11677                        items,
11678                    ))));
11679                }
11680                if let Some(hr) = val.as_hash_ref() {
11681                    let mut out: indexmap::IndexMap<String, StrykeValue> =
11682                        indexmap::IndexMap::new();
11683                    for (k, v) in hr.read().iter() {
11684                        out.insert(v.to_string(), StrykeValue::string(k.clone()));
11685                    }
11686                    return Ok(StrykeValue::hash_ref(Arc::new(parking_lot::RwLock::new(
11687                        out,
11688                    ))));
11689                }
11690                // Re-eval in list context for bare arrays/hashes
11691                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11692                if let Some(hm) = val.as_hash_map() {
11693                    let mut out: indexmap::IndexMap<String, StrykeValue> =
11694                        indexmap::IndexMap::new();
11695                    for (k, v) in hm.iter() {
11696                        out.insert(v.to_string(), StrykeValue::string(k.clone()));
11697                    }
11698                    return Ok(StrykeValue::hash(out));
11699                }
11700                if val.as_array_vec().is_some() {
11701                    let mut items = val.to_list();
11702                    items.reverse();
11703                    Ok(StrykeValue::array(items))
11704                } else {
11705                    let items = val.to_list();
11706                    if items.len() > 1 {
11707                        let mut items = items;
11708                        items.reverse();
11709                        Ok(StrykeValue::array(items))
11710                    } else {
11711                        let s = val.to_string();
11712                        Ok(StrykeValue::string(s.chars().rev().collect()))
11713                    }
11714                }
11715            }
11716            ExprKind::ReverseExpr(list) => {
11717                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11718                match ctx {
11719                    WantarrayCtx::List => {
11720                        let mut items = val.to_list();
11721                        items.reverse();
11722                        Ok(StrykeValue::array(items))
11723                    }
11724                    _ => {
11725                        let items = val.to_list();
11726                        let s: String = items.iter().map(|v| v.to_string()).collect();
11727                        Ok(StrykeValue::string(s.chars().rev().collect()))
11728                    }
11729                }
11730            }
11731
11732            // ── Parallel operations (rayon-powered) ──
11733            ExprKind::ParLinesExpr {
11734                path,
11735                callback,
11736                progress,
11737            } => self.eval_par_lines_expr(
11738                path.as_ref(),
11739                callback.as_ref(),
11740                progress.as_deref(),
11741                line,
11742            ),
11743            ExprKind::ParWalkExpr {
11744                path,
11745                callback,
11746                progress,
11747            } => {
11748                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
11749            }
11750            ExprKind::PwatchExpr { path, callback } => {
11751                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
11752            }
11753            ExprKind::PMapExpr {
11754                block,
11755                list,
11756                progress,
11757                flat_outputs,
11758                on_cluster,
11759                stream,
11760            } => {
11761                let show_progress = progress
11762                    .as_ref()
11763                    .map(|p| self.eval_expr(p))
11764                    .transpose()?
11765                    .map(|v| v.is_true())
11766                    .unwrap_or(false);
11767                // List context for the operand so `@_` / `@arr` flatten to
11768                // their elements instead of numifying to the array length.
11769                // Scalar context was producing `pmap{_*2}` over `@_` of size
11770                // 13 → one iteration with `_=13` → `[26]` (chunk-size × 2)
11771                // instead of 13 doubled values; same shape inside `~p>` chunk
11772                // workers and at top level.
11773                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11774                if let Some(cluster_e) = on_cluster {
11775                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
11776                    return self.eval_pmap_remote(
11777                        cluster_val,
11778                        list_val,
11779                        show_progress,
11780                        block,
11781                        *flat_outputs,
11782                        line,
11783                    );
11784                }
11785                if *stream {
11786                    let source = crate::map_stream::into_pull_iter(list_val);
11787                    let sub = self.anon_coderef_from_block(block);
11788                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
11789                    return Ok(StrykeValue::iterator(Arc::new(
11790                        crate::map_stream::PMapStreamIterator::new(
11791                            source,
11792                            sub,
11793                            self.subs.clone(),
11794                            capture,
11795                            atomic_arrays,
11796                            atomic_hashes,
11797                            *flat_outputs,
11798                        ),
11799                    )));
11800                }
11801                let items = list_val.to_list();
11802                let block = block.clone();
11803                let subs = self.subs.clone();
11804                let (scope_capture, atomic_arrays, atomic_hashes) =
11805                    self.scope.capture_with_atomics();
11806                let pmap_progress = PmapProgress::new(show_progress, items.len());
11807
11808                if *flat_outputs {
11809                    let mut indexed: Vec<(usize, Vec<StrykeValue>)> = items
11810                        .into_par_iter()
11811                        .enumerate()
11812                        .map(|(i, item)| {
11813                            let mut local_interp = VMHelper::new();
11814                            local_interp.subs = subs.clone();
11815                            local_interp.scope.restore_capture(&scope_capture);
11816                            local_interp
11817                                .scope
11818                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11819                            local_interp.enable_parallel_guard();
11820                            local_interp.scope.set_topic(item);
11821                            let val = match local_interp.exec_block(&block) {
11822                                Ok(val) => val,
11823                                Err(_) => StrykeValue::UNDEF,
11824                            };
11825                            let chunk = val.map_flatten_outputs(true);
11826                            pmap_progress.tick();
11827                            (i, chunk)
11828                        })
11829                        .collect();
11830                    pmap_progress.finish();
11831                    indexed.sort_by_key(|(i, _)| *i);
11832                    let results: Vec<StrykeValue> =
11833                        indexed.into_iter().flat_map(|(_, v)| v).collect();
11834                    Ok(StrykeValue::array(results))
11835                } else {
11836                    let results: Vec<StrykeValue> = items
11837                        .into_par_iter()
11838                        .map(|item| {
11839                            let mut local_interp = VMHelper::new();
11840                            local_interp.subs = subs.clone();
11841                            local_interp.scope.restore_capture(&scope_capture);
11842                            local_interp
11843                                .scope
11844                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11845                            local_interp.enable_parallel_guard();
11846                            local_interp.scope.set_topic(item);
11847                            let val = match local_interp.exec_block(&block) {
11848                                Ok(val) => val,
11849                                Err(_) => StrykeValue::UNDEF,
11850                            };
11851                            pmap_progress.tick();
11852                            val
11853                        })
11854                        .collect();
11855                    pmap_progress.finish();
11856                    Ok(StrykeValue::array(results))
11857                }
11858            }
11859            ExprKind::PMapChunkedExpr {
11860                chunk_size,
11861                block,
11862                list,
11863                progress,
11864            } => {
11865                let show_progress = progress
11866                    .as_ref()
11867                    .map(|p| self.eval_expr(p))
11868                    .transpose()?
11869                    .map(|v| v.is_true())
11870                    .unwrap_or(false);
11871                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
11872                let list_val = self.eval_expr(list)?;
11873                let items = list_val.to_list();
11874                let block = block.clone();
11875                let subs = self.subs.clone();
11876                let (scope_capture, atomic_arrays, atomic_hashes) =
11877                    self.scope.capture_with_atomics();
11878
11879                let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = items
11880                    .chunks(chunk_n)
11881                    .enumerate()
11882                    .map(|(i, c)| (i, c.to_vec()))
11883                    .collect();
11884
11885                let n_chunks = indexed_chunks.len();
11886                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
11887
11888                let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
11889                    .into_par_iter()
11890                    .map(|(chunk_idx, chunk)| {
11891                        let mut local_interp = VMHelper::new();
11892                        local_interp.subs = subs.clone();
11893                        local_interp.scope.restore_capture(&scope_capture);
11894                        local_interp
11895                            .scope
11896                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11897                        local_interp.enable_parallel_guard();
11898                        let mut out = Vec::with_capacity(chunk.len());
11899                        for item in chunk {
11900                            local_interp.scope.set_topic(item);
11901                            match local_interp.exec_block(&block) {
11902                                Ok(val) => out.push(val),
11903                                Err(_) => out.push(StrykeValue::UNDEF),
11904                            }
11905                        }
11906                        pmap_progress.tick();
11907                        (chunk_idx, out)
11908                    })
11909                    .collect();
11910
11911                pmap_progress.finish();
11912                chunk_results.sort_by_key(|(i, _)| *i);
11913                let results: Vec<StrykeValue> =
11914                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
11915                Ok(StrykeValue::array(results))
11916            }
11917            ExprKind::PGrepExpr {
11918                block,
11919                list,
11920                progress,
11921                stream,
11922            } => {
11923                let show_progress = progress
11924                    .as_ref()
11925                    .map(|p| self.eval_expr(p))
11926                    .transpose()?
11927                    .map(|v| v.is_true())
11928                    .unwrap_or(false);
11929                let list_val = self.eval_expr(list)?;
11930                if *stream {
11931                    let source = crate::map_stream::into_pull_iter(list_val);
11932                    let sub = self.anon_coderef_from_block(block);
11933                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
11934                    return Ok(StrykeValue::iterator(Arc::new(
11935                        crate::map_stream::PGrepStreamIterator::new(
11936                            source,
11937                            sub,
11938                            self.subs.clone(),
11939                            capture,
11940                            atomic_arrays,
11941                            atomic_hashes,
11942                        ),
11943                    )));
11944                }
11945                let items = list_val.to_list();
11946                let block = block.clone();
11947                let subs = self.subs.clone();
11948                let (scope_capture, atomic_arrays, atomic_hashes) =
11949                    self.scope.capture_with_atomics();
11950                let pmap_progress = PmapProgress::new(show_progress, items.len());
11951
11952                let results: Vec<StrykeValue> = items
11953                    .into_par_iter()
11954                    .filter_map(|item| {
11955                        let mut local_interp = VMHelper::new();
11956                        local_interp.subs = subs.clone();
11957                        local_interp.scope.restore_capture(&scope_capture);
11958                        local_interp
11959                            .scope
11960                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11961                        local_interp.enable_parallel_guard();
11962                        local_interp.scope.set_topic(item.clone());
11963                        let keep = match local_interp.exec_block(&block) {
11964                            Ok(val) => val.is_true(),
11965                            Err(_) => false,
11966                        };
11967                        pmap_progress.tick();
11968                        if keep {
11969                            Some(item)
11970                        } else {
11971                            None
11972                        }
11973                    })
11974                    .collect();
11975                pmap_progress.finish();
11976                Ok(StrykeValue::array(results))
11977            }
11978            ExprKind::ParExpr { block, list } => {
11979                // Generic parallel-chunk wrapper: split input on a sensible
11980                // boundary (UTF-8 char-aligned for strings, element-aligned
11981                // for arrays), evaluate the block per chunk in parallel
11982                // with `$_` bound to the chunk, then concatenate results.
11983                //
11984                // Chunk count is capped at min(n_threads, 8) because each
11985                // chunk pays a fixed `VMHelper::new()` setup cost (env-var
11986                // parsing, PATH/FPATH split, term-info ioctl, IndexMap
11987                // declarations). On 18-core machines, splitting 18 ways
11988                // makes setup overhead dominate the actual work.
11989                // List context for the operand so `@arr` flattens to its
11990                // elements instead of numifying to the array length —
11991                // matches PMapExpr's eval shape so `par { } @arr` and
11992                // `pmap { } @arr` agree on input semantics.
11993                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11994                let n_threads = rayon::current_num_threads().clamp(1, 8);
11995                let chunks = par_chunk_value(&list_val, n_threads);
11996                if chunks.len() < 2 {
11997                    // Below break-even (small input or unsupported value type):
11998                    // run the block once with the original input as `$_`.
11999                    self.scope.set_topic(list_val);
12000                    let v = self.exec_block(block)?;
12001                    return Ok(v);
12002                }
12003                let block_clone = block.clone();
12004                let subs = self.subs.clone();
12005                let (scope_capture, atomic_arrays, atomic_hashes) =
12006                    self.scope.capture_with_atomics();
12007                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
12008                let err_w = Arc::clone(&first_err);
12009                let per_chunk: Vec<Vec<StrykeValue>> = chunks
12010                    .into_par_iter()
12011                    .map(|chunk| {
12012                        if err_w.lock().is_some() {
12013                            return Vec::new();
12014                        }
12015                        let mut local_interp = VMHelper::new();
12016                        local_interp.subs = subs.clone();
12017                        local_interp.scope.restore_capture(&scope_capture);
12018                        local_interp
12019                            .scope
12020                            .restore_atomics(&atomic_arrays, &atomic_hashes);
12021                        local_interp.enable_parallel_guard();
12022                        local_interp.scope.set_topic(chunk);
12023                        match local_interp.exec_block(&block_clone) {
12024                            Ok(v) => v.map_flatten_outputs(true),
12025                            Err(e) => {
12026                                let mut g = err_w.lock();
12027                                if g.is_none() {
12028                                    *g = Some(format!("par: {:?}", e));
12029                                }
12030                                Vec::new()
12031                            }
12032                        }
12033                    })
12034                    .collect();
12035                if let Some(msg) = first_err.lock().take() {
12036                    return Err(FlowOrError::Error(StrykeError::runtime(msg, line)));
12037                }
12038                let total: usize = per_chunk.iter().map(|v| v.len()).sum();
12039                let mut out = Vec::with_capacity(total);
12040                for v in per_chunk {
12041                    out.extend(v);
12042                }
12043                Ok(StrykeValue::array(out))
12044            }
12045            ExprKind::ParReduceExpr {
12046                extract_block,
12047                reduce_block,
12048                list,
12049            } => {
12050                // Chunk INPUT, run extract per chunk in parallel, then
12051                // reduce pairwise across chunks. With an explicit reduce
12052                // block the user controls merging via `$a` / `$b`. Without
12053                // one, the merger is auto-picked based on the first
12054                // chunk's result type:
12055                //   - hash<num>  → key-wise add (canonical histogram merge)
12056                //   - number     → numeric `+`
12057                //   - array/list → concat
12058                //   - string     → concat
12059                // Use List context so `@a` expands to its elements, not its length.
12060                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12061                let n_threads = rayon::current_num_threads().clamp(1, 8);
12062                let chunks = par_chunk_value(&list_val, n_threads);
12063                if chunks.len() < 2 {
12064                    // Single-chunk fallback: bind the chunk elements to `@_`
12065                    // (not wrapped) so `~p> @a sum` correctly passes all elements
12066                    // to `sum(@_)`. Also set `$_` to the first element for
12067                    // backwards compat with scalar-style blocks. Tail in LIST
12068                    // context so `map { ... } @_` returns the mapped array,
12069                    // not its scalar-context count — `~p> @xs map { _*10 }`
12070                    // must yield `[10,20,30,40,50]`, not `5`.
12071                    let chunk_arr = match list_val.as_array_vec() {
12072                        Some(arr) => arr,
12073                        None => vec![list_val.clone()],
12074                    };
12075                    let first = chunk_arr.first().cloned().unwrap_or(StrykeValue::UNDEF);
12076                    self.scope.declare_array("_", chunk_arr);
12077                    self.scope.set_topic(first);
12078                    return self.exec_block_with_tail(extract_block, WantarrayCtx::List);
12079                }
12080                let extract = extract_block.clone();
12081                let subs = self.subs.clone();
12082                let (scope_capture, atomic_arrays, atomic_hashes) =
12083                    self.scope.capture_with_atomics();
12084                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
12085                let err_w = Arc::clone(&first_err);
12086                let per_chunk: Vec<StrykeValue> = chunks
12087                    .into_par_iter()
12088                    .map(|chunk| {
12089                        if err_w.lock().is_some() {
12090                            return StrykeValue::UNDEF;
12091                        }
12092                        let mut local = VMHelper::new();
12093                        local.subs = subs.clone();
12094                        local.scope.restore_capture(&scope_capture);
12095                        local.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
12096                        local.enable_parallel_guard();
12097                        // Bind the chunk elements to `@_` (not wrapped) so
12098                        // `~p> @a sum` correctly passes all elements to `sum(@_)`.
12099                        // Also set `$_` to the first element for backwards compat.
12100                        let chunk_arr = match chunk.as_array_vec() {
12101                            Some(arr) => arr,
12102                            None => vec![chunk.clone()],
12103                        };
12104                        let first = chunk_arr.first().cloned().unwrap_or(StrykeValue::UNDEF);
12105                        local.scope.declare_array("_", chunk_arr);
12106                        local.scope.set_topic(first);
12107                        match local.exec_block_with_tail(&extract, WantarrayCtx::List) {
12108                            Ok(v) => v,
12109                            Err(e) => {
12110                                let mut g = err_w.lock();
12111                                if g.is_none() {
12112                                    *g = Some(format!("par_reduce: {:?}", e));
12113                                }
12114                                StrykeValue::UNDEF
12115                            }
12116                        }
12117                    })
12118                    .collect();
12119                if let Some(msg) = first_err.lock().take() {
12120                    return Err(FlowOrError::Error(StrykeError::runtime(msg, line)));
12121                }
12122                if per_chunk.is_empty() {
12123                    return Ok(StrykeValue::UNDEF);
12124                }
12125                // Explicit reducer: pairwise via user block with $a/$b bound.
12126                if let Some(rb) = reduce_block {
12127                    let mut acc = per_chunk[0].clone();
12128                    for v in per_chunk.into_iter().skip(1) {
12129                        self.scope.declare_scalar("a", acc.clone());
12130                        self.scope.declare_scalar("b", v);
12131                        acc = self.exec_block(rb)?;
12132                    }
12133                    return Ok(acc);
12134                }
12135                // Auto-merge.
12136                Ok(par_reduce_auto_merge(per_chunk))
12137            }
12138            ExprKind::DistReduceExpr {
12139                cluster,
12140                extract_block,
12141                list,
12142            } => {
12143                // Distributed counterpart of `~p>` — chunk the source list
12144                // locally, ship each chunk as one JSON work-item via the
12145                // existing `cluster::run_cluster` SSH dispatcher (one slot
12146                // per ssh process, session_init once per slot, JOB frames
12147                // over a shared queue, retry budget per chunk).
12148                //
12149                // Each remote worker receives one chunk (a JSON array) as
12150                // `$_[0]`. We prepend `local @_ = @{$_[0]}` to the block so
12151                // the user-supplied stage chain sees `@_` bound to the
12152                // chunk's elements — identical surface to `~p>`'s extract
12153                // block.
12154                let cluster_pv = self.eval_expr(cluster)?;
12155                let Some(remote_cluster) = cluster_pv.as_remote_cluster() else {
12156                    return Err(StrykeError::runtime(
12157                        "~d>: expected cluster(...) value after `on`",
12158                        line,
12159                    )
12160                    .into());
12161                };
12162                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12163                let items_flat = list_val.to_list();
12164                if items_flat.is_empty() {
12165                    return Ok(StrykeValue::array(vec![]));
12166                }
12167                let (scope_capture, atomic_arrays, atomic_hashes) =
12168                    self.scope.capture_with_atomics();
12169                if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
12170                    return Err(StrykeError::runtime(
12171                        "~d>: mysync/atomic capture is not supported for remote workers",
12172                        line,
12173                    )
12174                    .into());
12175                }
12176                // Chunk size mirrors `~p>`'s heuristic: oversubscribe slots
12177                // by 4× so faster slots can pull more work via the shared
12178                // queue. Floor at 1, ceil at items.len() so we always get
12179                // at least one chunk per slot when items < slots.
12180                let n_slots = remote_cluster.slots.len().max(1);
12181                let target_chunks = (n_slots * 4).min(items_flat.len()).max(1);
12182                let chunk_size = items_flat.len().div_ceil(target_chunks);
12183                let mut chunk_items: Vec<serde_json::Value> = Vec::new();
12184                let mut chunk_jsons_buf: Vec<StrykeValue> = Vec::new();
12185                for item in items_flat.into_iter() {
12186                    chunk_jsons_buf.push(item);
12187                    if chunk_jsons_buf.len() >= chunk_size {
12188                        let drained: Vec<StrykeValue> = std::mem::take(&mut chunk_jsons_buf);
12189                        let as_array = StrykeValue::array(drained);
12190                        chunk_items.push(
12191                            crate::remote_wire::perl_to_json_value(&as_array)
12192                                .map_err(|e| StrykeError::runtime(e, line))?,
12193                        );
12194                    }
12195                }
12196                if !chunk_jsons_buf.is_empty() {
12197                    let as_array = StrykeValue::array(chunk_jsons_buf);
12198                    chunk_items.push(
12199                        crate::remote_wire::perl_to_json_value(&as_array)
12200                            .map_err(|e| StrykeError::runtime(e, line))?,
12201                    );
12202                }
12203                // Drop scope entries that can't be JSON-marshalled (the
12204                // `RemoteCluster` value itself, file handles, code refs,
12205                // etc.). The remote worker doesn't need them — they're
12206                // dispatcher-side context only. Any reference the user's
12207                // stage block legitimately needs goes through this filter
12208                // anyway, and a `RemoteCluster` in remote scope would be
12209                // nonsensical (no controller to dispatch through there).
12210                let cap_json: Vec<(String, serde_json::Value)> = scope_capture
12211                    .iter()
12212                    .filter_map(|(k, v)| {
12213                        crate::remote_wire::perl_to_json_value(v)
12214                            .ok()
12215                            .map(|j| (k.clone(), j))
12216                    })
12217                    .collect();
12218                let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
12219                // Remote agent (`remote_wire::run_job_local`) sets `$_`
12220                // to the chunk value (a flat array) but does NOT populate
12221                // `@_`. Two bridges:
12222                //   1. `@_ = $_;` prologue — make the user's `@_`-threaded
12223                //      stage chain see the chunk's elements as an array
12224                //      (matches `~p>` chunk-block surface).
12225                //   2. Wrap the body in `[ ... ]` so the last expression's
12226                //      list result survives the worker's scalar-context
12227                //      block return + JSON round-trip. Without this,
12228                //      `map { ... } @_` collapses to `scalar(@result) = N`
12229                //      before serialization.
12230                let user_block_src = crate::fmt::format_block(extract_block);
12231                let block_src = format!("@_ = $_;\n[ {user_block_src} ]");
12232                let result_values = crate::cluster::run_cluster(
12233                    &remote_cluster,
12234                    subs_prelude,
12235                    block_src,
12236                    cap_json,
12237                    chunk_items,
12238                )
12239                .map_err(|e| StrykeError::runtime(format!("~d> remote: {e}"), line))?;
12240                // Each chunk's extract returns a value; flat-concat across
12241                // chunks to mirror `~p>`'s auto-merge for list-shaped output.
12242                // Scalar-shaped chunk results stay as one element each.
12243                let mut merged: Vec<StrykeValue> = Vec::new();
12244                for v in result_values {
12245                    if let Some(items) = v.as_array_vec() {
12246                        merged.extend(items);
12247                    } else if let Some(ar) = v.as_array_ref() {
12248                        merged.extend(ar.read().iter().cloned());
12249                    } else {
12250                        merged.push(v);
12251                    }
12252                }
12253                Ok(StrykeValue::array(merged))
12254            }
12255            ExprKind::PForExpr {
12256                block,
12257                list,
12258                progress,
12259            } => {
12260                let show_progress = progress
12261                    .as_ref()
12262                    .map(|p| self.eval_expr(p))
12263                    .transpose()?
12264                    .map(|v| v.is_true())
12265                    .unwrap_or(false);
12266                let list_val = self.eval_expr(list)?;
12267                let items = list_val.to_list();
12268                let block = block.clone();
12269                let subs = self.subs.clone();
12270                let (scope_capture, atomic_arrays, atomic_hashes) =
12271                    self.scope.capture_with_atomics();
12272
12273                let pmap_progress = PmapProgress::new(show_progress, items.len());
12274                let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
12275                items.into_par_iter().for_each(|item| {
12276                    if first_err.lock().is_some() {
12277                        return;
12278                    }
12279                    let mut local_interp = VMHelper::new();
12280                    local_interp.subs = subs.clone();
12281                    local_interp.scope.restore_capture(&scope_capture);
12282                    local_interp
12283                        .scope
12284                        .restore_atomics(&atomic_arrays, &atomic_hashes);
12285                    local_interp.enable_parallel_guard();
12286                    local_interp.scope.set_topic(item);
12287                    match local_interp.exec_block(&block) {
12288                        Ok(_) => {}
12289                        Err(e) => {
12290                            let stryke = match e {
12291                                FlowOrError::Error(stryke) => stryke,
12292                                FlowOrError::Flow(_) => StrykeError::runtime(
12293                                    "return/last/next/redo not supported inside pfor block",
12294                                    line,
12295                                ),
12296                            };
12297                            let mut g = first_err.lock();
12298                            if g.is_none() {
12299                                *g = Some(stryke);
12300                            }
12301                        }
12302                    }
12303                    pmap_progress.tick();
12304                });
12305                pmap_progress.finish();
12306                if let Some(e) = first_err.lock().take() {
12307                    return Err(FlowOrError::Error(e));
12308                }
12309                Ok(StrykeValue::UNDEF)
12310            }
12311            ExprKind::FanExpr {
12312                count,
12313                block,
12314                progress,
12315                capture,
12316            } => {
12317                let show_progress = progress
12318                    .as_ref()
12319                    .map(|p| self.eval_expr(p))
12320                    .transpose()?
12321                    .map(|v| v.is_true())
12322                    .unwrap_or(false);
12323                let n = match count {
12324                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
12325                    None => self.parallel_thread_count(),
12326                };
12327                let block = block.clone();
12328                let subs = self.subs.clone();
12329                let (scope_capture, atomic_arrays, atomic_hashes) =
12330                    self.scope.capture_with_atomics();
12331
12332                let fan_progress = FanProgress::new(show_progress, n);
12333                if *capture {
12334                    if n == 0 {
12335                        return Ok(StrykeValue::array(Vec::new()));
12336                    }
12337                    let pairs: Vec<(usize, ExecResult)> = (0..n)
12338                        .into_par_iter()
12339                        .map(|i| {
12340                            fan_progress.start_worker(i);
12341                            let mut local_interp = VMHelper::new();
12342                            local_interp.subs = subs.clone();
12343                            local_interp.suppress_stdout = show_progress;
12344                            local_interp.scope.restore_capture(&scope_capture);
12345                            local_interp
12346                                .scope
12347                                .restore_atomics(&atomic_arrays, &atomic_hashes);
12348                            local_interp.enable_parallel_guard();
12349                            local_interp.scope.set_topic(StrykeValue::integer(i as i64));
12350                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
12351                            let res = local_interp.exec_block(&block);
12352                            crate::parallel_trace::fan_worker_set_index(None);
12353                            fan_progress.finish_worker(i);
12354                            (i, res)
12355                        })
12356                        .collect();
12357                    fan_progress.finish();
12358                    let mut pairs = pairs;
12359                    pairs.sort_by_key(|(i, _)| *i);
12360                    let mut out = Vec::with_capacity(n);
12361                    for (_, r) in pairs {
12362                        match r {
12363                            Ok(v) => out.push(v),
12364                            Err(e) => return Err(e),
12365                        }
12366                    }
12367                    return Ok(StrykeValue::array(out));
12368                }
12369                let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
12370                (0..n).into_par_iter().for_each(|i| {
12371                    if first_err.lock().is_some() {
12372                        return;
12373                    }
12374                    fan_progress.start_worker(i);
12375                    let mut local_interp = VMHelper::new();
12376                    local_interp.subs = subs.clone();
12377                    local_interp.suppress_stdout = show_progress;
12378                    local_interp.scope.restore_capture(&scope_capture);
12379                    local_interp
12380                        .scope
12381                        .restore_atomics(&atomic_arrays, &atomic_hashes);
12382                    local_interp.enable_parallel_guard();
12383                    local_interp.scope.set_topic(StrykeValue::integer(i as i64));
12384                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
12385                    match local_interp.exec_block(&block) {
12386                        Ok(_) => {}
12387                        Err(e) => {
12388                            let stryke = match e {
12389                                FlowOrError::Error(stryke) => stryke,
12390                                FlowOrError::Flow(_) => StrykeError::runtime(
12391                                    "return/last/next/redo not supported inside fan block",
12392                                    line,
12393                                ),
12394                            };
12395                            let mut g = first_err.lock();
12396                            if g.is_none() {
12397                                *g = Some(stryke);
12398                            }
12399                        }
12400                    }
12401                    crate::parallel_trace::fan_worker_set_index(None);
12402                    fan_progress.finish_worker(i);
12403                });
12404                fan_progress.finish();
12405                if let Some(e) = first_err.lock().take() {
12406                    return Err(FlowOrError::Error(e));
12407                }
12408                Ok(StrykeValue::UNDEF)
12409            }
12410            ExprKind::RetryBlock {
12411                body,
12412                times,
12413                backoff,
12414            } => self.eval_retry_block(body, times, *backoff, line),
12415            ExprKind::RateLimitBlock {
12416                slot,
12417                max,
12418                window,
12419                body,
12420            } => self.eval_rate_limit_block(*slot, max, window, body, line),
12421            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
12422            ExprKind::GenBlock { body } => {
12423                let g = Arc::new(PerlGenerator {
12424                    block: body.clone(),
12425                    pc: Mutex::new(0),
12426                    scope_started: Mutex::new(false),
12427                    exhausted: Mutex::new(false),
12428                });
12429                Ok(StrykeValue::generator(g))
12430            }
12431            ExprKind::Yield(e) => {
12432                if !self.in_generator {
12433                    return Err(StrykeError::runtime("yield outside gen block", line).into());
12434                }
12435                let v = self.eval_expr(e)?;
12436                Err(FlowOrError::Flow(Flow::Yield(v)))
12437            }
12438            ExprKind::AlgebraicMatch { subject, arms } => {
12439                self.eval_algebraic_match(subject, arms, line)
12440            }
12441            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
12442                Ok(self.spawn_async_block(body))
12443            }
12444            ExprKind::Trace { body } => {
12445                crate::parallel_trace::trace_enter();
12446                let out = self.exec_block(body);
12447                crate::parallel_trace::trace_leave();
12448                out
12449            }
12450            ExprKind::Spinner { message, body } => {
12451                use std::io::Write as _;
12452                let msg = self.eval_expr(message)?.to_string();
12453                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
12454                let done2 = done.clone();
12455                let handle = std::thread::spawn(move || {
12456                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12457                    let mut i = 0;
12458                    let stderr = std::io::stderr();
12459                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
12460                        {
12461                            let stdout = std::io::stdout();
12462                            let _stdout_lock = stdout.lock();
12463                            let mut err = stderr.lock();
12464                            let _ = write!(
12465                                err,
12466                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
12467                                frames[i % frames.len()],
12468                                msg
12469                            );
12470                            let _ = err.flush();
12471                        }
12472                        std::thread::sleep(std::time::Duration::from_millis(80));
12473                        i += 1;
12474                    }
12475                    let mut err = stderr.lock();
12476                    let _ = write!(err, "\r\x1b[2K");
12477                    let _ = err.flush();
12478                });
12479                let result = self.exec_block(body);
12480                done.store(true, std::sync::atomic::Ordering::Relaxed);
12481                let _ = handle.join();
12482                result
12483            }
12484            ExprKind::Timer { body } => {
12485                let start = std::time::Instant::now();
12486                self.exec_block(body)?;
12487                let ms = start.elapsed().as_secs_f64() * 1000.0;
12488                Ok(StrykeValue::float(ms))
12489            }
12490            ExprKind::Bench { body, times } => {
12491                let n = self.eval_expr(times)?.to_int();
12492                if n < 0 {
12493                    return Err(StrykeError::runtime(
12494                        "bench: iteration count must be non-negative",
12495                        line,
12496                    )
12497                    .into());
12498                }
12499                self.run_bench_block(body, n as usize, line)
12500            }
12501            ExprKind::Await(expr) => {
12502                let v = self.eval_expr(expr)?;
12503                if let Some(t) = v.as_async_task() {
12504                    t.await_result().map_err(FlowOrError::from)
12505                } else {
12506                    Ok(v)
12507                }
12508            }
12509            ExprKind::Slurp(e) => {
12510                let path = self.eval_expr(e)?.to_string();
12511                let path = self.resolve_stryke_path_string(&path);
12512                crate::perl_fs::read_file_text_or_glob(&path)
12513                    .map(StrykeValue::string)
12514                    .map_err(|e| {
12515                        FlowOrError::Error(StrykeError::runtime(format!("slurp: {}", e), line))
12516                    })
12517            }
12518            ExprKind::Capture(e) => {
12519                let cmd = self.eval_expr(e)?.to_string();
12520                let output = Command::new("sh")
12521                    .arg("-c")
12522                    .arg(&cmd)
12523                    .output()
12524                    .map_err(|e| {
12525                        FlowOrError::Error(StrykeError::runtime(format!("capture: {}", e), line))
12526                    })?;
12527                self.record_child_exit_status(output.status);
12528                let exitcode = output.status.code().unwrap_or(-1) as i64;
12529                let stdout = decode_utf8_or_latin1(&output.stdout);
12530                let stderr = decode_utf8_or_latin1(&output.stderr);
12531                Ok(StrykeValue::capture(Arc::new(CaptureResult {
12532                    stdout,
12533                    stderr,
12534                    exitcode,
12535                })))
12536            }
12537            ExprKind::Qx(e) => {
12538                let cmd = self.eval_expr(e)?.to_string();
12539                let raw =
12540                    crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)?;
12541                if ctx == WantarrayCtx::List {
12542                    // Perl `qx` in list context yields one element per line,
12543                    // each terminated by `$/` (default `\n`). The final line
12544                    // keeps whatever termination the command produced.
12545                    let s = raw.to_string();
12546                    if s.is_empty() {
12547                        return Ok(StrykeValue::array(Vec::new()));
12548                    }
12549                    let mut lines = Vec::new();
12550                    let mut buf = String::new();
12551                    for c in s.chars() {
12552                        buf.push(c);
12553                        if c == '\n' {
12554                            lines.push(StrykeValue::string(std::mem::take(&mut buf)));
12555                        }
12556                    }
12557                    if !buf.is_empty() {
12558                        lines.push(StrykeValue::string(buf));
12559                    }
12560                    Ok(StrykeValue::array(lines))
12561                } else {
12562                    Ok(raw)
12563                }
12564            }
12565            ExprKind::FetchUrl(e) => {
12566                let url = self.eval_expr(e)?.to_string();
12567                ureq::get(&url)
12568                    .call()
12569                    .map_err(|e| {
12570                        FlowOrError::Error(StrykeError::runtime(format!("fetch_url: {}", e), line))
12571                    })
12572                    .and_then(|r| {
12573                        r.into_string().map(StrykeValue::string).map_err(|e| {
12574                            FlowOrError::Error(StrykeError::runtime(
12575                                format!("fetch_url: {}", e),
12576                                line,
12577                            ))
12578                        })
12579                    })
12580            }
12581            ExprKind::Pchannel { capacity } => {
12582                if let Some(c) = capacity {
12583                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
12584                    Ok(crate::pchannel::create_bounded_pair(n))
12585                } else {
12586                    Ok(crate::pchannel::create_pair())
12587                }
12588            }
12589            ExprKind::PSortExpr {
12590                cmp,
12591                list,
12592                progress,
12593            } => {
12594                let show_progress = progress
12595                    .as_ref()
12596                    .map(|p| self.eval_expr(p))
12597                    .transpose()?
12598                    .map(|v| v.is_true())
12599                    .unwrap_or(false);
12600                let list_val = self.eval_expr(list)?;
12601                let mut items = list_val.to_list();
12602                let pmap_progress = PmapProgress::new(show_progress, 2);
12603                pmap_progress.tick();
12604                if let Some(cmp_block) = cmp {
12605                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
12606                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
12607                    } else {
12608                        let cmp_block = cmp_block.clone();
12609                        let subs = self.subs.clone();
12610                        let scope_capture = self.scope.capture();
12611                        items.par_sort_by(|a, b| {
12612                            let mut local_interp = VMHelper::new();
12613                            local_interp.subs = subs.clone();
12614                            local_interp.scope.restore_capture(&scope_capture);
12615                            local_interp.scope.set_sort_pair(a.clone(), b.clone());
12616                            match local_interp.exec_block(&cmp_block) {
12617                                Ok(v) => {
12618                                    let n = v.to_int();
12619                                    if n < 0 {
12620                                        std::cmp::Ordering::Less
12621                                    } else if n > 0 {
12622                                        std::cmp::Ordering::Greater
12623                                    } else {
12624                                        std::cmp::Ordering::Equal
12625                                    }
12626                                }
12627                                Err(_) => std::cmp::Ordering::Equal,
12628                            }
12629                        });
12630                    }
12631                } else {
12632                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
12633                }
12634                pmap_progress.tick();
12635                pmap_progress.finish();
12636                Ok(StrykeValue::array(items))
12637            }
12638
12639            ExprKind::ReduceExpr { block, list } => {
12640                let list_val = self.eval_expr(list)?;
12641                let items = list_val.to_list();
12642                if items.is_empty() {
12643                    return Ok(StrykeValue::UNDEF);
12644                }
12645                if items.len() == 1 {
12646                    return Ok(items.into_iter().next().unwrap());
12647                }
12648                let block = block.clone();
12649                let subs = self.subs.clone();
12650                let scope_capture = self.scope.capture();
12651                let mut acc = items[0].clone();
12652                for b in items.into_iter().skip(1) {
12653                    let mut local_interp = VMHelper::new();
12654                    local_interp.subs = subs.clone();
12655                    local_interp.scope.restore_capture(&scope_capture);
12656                    local_interp.scope.set_sort_pair(acc, b);
12657                    acc = match local_interp.exec_block(&block) {
12658                        Ok(val) => val,
12659                        Err(_) => StrykeValue::UNDEF,
12660                    };
12661                }
12662                Ok(acc)
12663            }
12664
12665            ExprKind::PReduceExpr {
12666                block,
12667                list,
12668                progress,
12669            } => {
12670                let show_progress = progress
12671                    .as_ref()
12672                    .map(|p| self.eval_expr(p))
12673                    .transpose()?
12674                    .map(|v| v.is_true())
12675                    .unwrap_or(false);
12676                let list_val = self.eval_expr(list)?;
12677                let items = list_val.to_list();
12678                if items.is_empty() {
12679                    return Ok(StrykeValue::UNDEF);
12680                }
12681                if items.len() == 1 {
12682                    return Ok(items.into_iter().next().unwrap());
12683                }
12684                let block = block.clone();
12685                let subs = self.subs.clone();
12686                let scope_capture = self.scope.capture();
12687                let pmap_progress = PmapProgress::new(show_progress, items.len());
12688
12689                let result = items
12690                    .into_par_iter()
12691                    .map(|x| {
12692                        pmap_progress.tick();
12693                        x
12694                    })
12695                    .reduce_with(|a, b| {
12696                        let mut local_interp = VMHelper::new();
12697                        local_interp.subs = subs.clone();
12698                        local_interp.scope.restore_capture(&scope_capture);
12699                        local_interp.scope.set_sort_pair(a, b);
12700                        match local_interp.exec_block(&block) {
12701                            Ok(val) => val,
12702                            Err(_) => StrykeValue::UNDEF,
12703                        }
12704                    });
12705                pmap_progress.finish();
12706                Ok(result.unwrap_or(StrykeValue::UNDEF))
12707            }
12708
12709            ExprKind::PReduceInitExpr {
12710                init,
12711                block,
12712                list,
12713                progress,
12714            } => {
12715                let show_progress = progress
12716                    .as_ref()
12717                    .map(|p| self.eval_expr(p))
12718                    .transpose()?
12719                    .map(|v| v.is_true())
12720                    .unwrap_or(false);
12721                let init_val = self.eval_expr(init)?;
12722                let list_val = self.eval_expr(list)?;
12723                let items = list_val.to_list();
12724                if items.is_empty() {
12725                    return Ok(init_val);
12726                }
12727                let block = block.clone();
12728                let subs = self.subs.clone();
12729                let scope_capture = self.scope.capture();
12730                let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
12731                if items.len() == 1 {
12732                    return Ok(fold_preduce_init_step(
12733                        &subs,
12734                        cap,
12735                        &block,
12736                        preduce_init_fold_identity(&init_val),
12737                        items.into_iter().next().unwrap(),
12738                    ));
12739                }
12740                let pmap_progress = PmapProgress::new(show_progress, items.len());
12741                let result = items
12742                    .into_par_iter()
12743                    .fold(
12744                        || preduce_init_fold_identity(&init_val),
12745                        |acc, item| {
12746                            pmap_progress.tick();
12747                            fold_preduce_init_step(&subs, cap, &block, acc, item)
12748                        },
12749                    )
12750                    .reduce(
12751                        || preduce_init_fold_identity(&init_val),
12752                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
12753                    );
12754                pmap_progress.finish();
12755                Ok(result)
12756            }
12757
12758            ExprKind::PMapReduceExpr {
12759                map_block,
12760                reduce_block,
12761                list,
12762                progress,
12763            } => {
12764                let show_progress = progress
12765                    .as_ref()
12766                    .map(|p| self.eval_expr(p))
12767                    .transpose()?
12768                    .map(|v| v.is_true())
12769                    .unwrap_or(false);
12770                let list_val = self.eval_expr(list)?;
12771                let items = list_val.to_list();
12772                if items.is_empty() {
12773                    return Ok(StrykeValue::UNDEF);
12774                }
12775                let map_block = map_block.clone();
12776                let reduce_block = reduce_block.clone();
12777                let subs = self.subs.clone();
12778                let scope_capture = self.scope.capture();
12779                if items.len() == 1 {
12780                    let mut local_interp = VMHelper::new();
12781                    local_interp.subs = subs.clone();
12782                    local_interp.scope.restore_capture(&scope_capture);
12783                    local_interp.scope.set_topic(items[0].clone());
12784                    return match local_interp.exec_block_no_scope(&map_block) {
12785                        Ok(v) => Ok(v),
12786                        Err(_) => Ok(StrykeValue::UNDEF),
12787                    };
12788                }
12789                let pmap_progress = PmapProgress::new(show_progress, items.len());
12790                let result = items
12791                    .into_par_iter()
12792                    .map(|item| {
12793                        let mut local_interp = VMHelper::new();
12794                        local_interp.subs = subs.clone();
12795                        local_interp.scope.restore_capture(&scope_capture);
12796                        local_interp.scope.set_topic(item);
12797                        let val = match local_interp.exec_block_no_scope(&map_block) {
12798                            Ok(val) => val,
12799                            Err(_) => StrykeValue::UNDEF,
12800                        };
12801                        pmap_progress.tick();
12802                        val
12803                    })
12804                    .reduce_with(|a, b| {
12805                        let mut local_interp = VMHelper::new();
12806                        local_interp.subs = subs.clone();
12807                        local_interp.scope.restore_capture(&scope_capture);
12808                        local_interp.scope.set_sort_pair(a, b);
12809                        match local_interp.exec_block_no_scope(&reduce_block) {
12810                            Ok(val) => val,
12811                            Err(_) => StrykeValue::UNDEF,
12812                        }
12813                    });
12814                pmap_progress.finish();
12815                Ok(result.unwrap_or(StrykeValue::UNDEF))
12816            }
12817
12818            ExprKind::PcacheExpr {
12819                block,
12820                list,
12821                progress,
12822            } => {
12823                let show_progress = progress
12824                    .as_ref()
12825                    .map(|p| self.eval_expr(p))
12826                    .transpose()?
12827                    .map(|v| v.is_true())
12828                    .unwrap_or(false);
12829                let list_val = self.eval_expr(list)?;
12830                let items = list_val.to_list();
12831                let block = block.clone();
12832                let subs = self.subs.clone();
12833                let scope_capture = self.scope.capture();
12834                let cache = &*crate::pcache::GLOBAL_PCACHE;
12835                let pmap_progress = PmapProgress::new(show_progress, items.len());
12836                let results: Vec<StrykeValue> = items
12837                    .into_par_iter()
12838                    .map(|item| {
12839                        let k = crate::pcache::cache_key(&item);
12840                        if let Some(v) = cache.get(&k) {
12841                            pmap_progress.tick();
12842                            return v.clone();
12843                        }
12844                        let mut local_interp = VMHelper::new();
12845                        local_interp.subs = subs.clone();
12846                        local_interp.scope.restore_capture(&scope_capture);
12847                        local_interp.scope.set_topic(item.clone());
12848                        let val = match local_interp.exec_block_no_scope(&block) {
12849                            Ok(v) => v,
12850                            Err(_) => StrykeValue::UNDEF,
12851                        };
12852                        cache.insert(k, val.clone());
12853                        pmap_progress.tick();
12854                        val
12855                    })
12856                    .collect();
12857                pmap_progress.finish();
12858                Ok(StrykeValue::array(results))
12859            }
12860
12861            ExprKind::PselectExpr { receivers, timeout } => {
12862                let mut rx_vals = Vec::with_capacity(receivers.len());
12863                for r in receivers {
12864                    rx_vals.push(self.eval_expr(r)?);
12865                }
12866                let dur = if let Some(t) = timeout.as_ref() {
12867                    Some(std::time::Duration::from_secs_f64(
12868                        self.eval_expr(t)?.to_number().max(0.0),
12869                    ))
12870                } else {
12871                    None
12872                };
12873                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
12874                    &rx_vals, dur, line,
12875                )?)
12876            }
12877
12878            // Array ops
12879            ExprKind::Push { array, values } => {
12880                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
12881            }
12882            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
12883            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
12884            ExprKind::Unshift { array, values } => {
12885                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
12886            }
12887            ExprKind::Splice {
12888                array,
12889                offset,
12890                length,
12891                replacement,
12892            } => self.eval_splice_expr(
12893                array.as_ref(),
12894                offset.as_deref(),
12895                length.as_deref(),
12896                replacement.as_slice(),
12897                ctx,
12898                line,
12899            ),
12900            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
12901            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
12902            ExprKind::Keys(expr) => {
12903                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
12904                let keys = Self::keys_from_value(val, line)?;
12905                if ctx == WantarrayCtx::List {
12906                    Ok(keys)
12907                } else {
12908                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
12909                    Ok(StrykeValue::integer(n as i64))
12910                }
12911            }
12912            ExprKind::Values(expr) => {
12913                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
12914                let vals = Self::values_from_value(val, line)?;
12915                if ctx == WantarrayCtx::List {
12916                    Ok(vals)
12917                } else {
12918                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
12919                    Ok(StrykeValue::integer(n as i64))
12920                }
12921            }
12922            ExprKind::Each(_) => {
12923                // Simplified: returns empty list (full iterator state would need more work)
12924                Ok(StrykeValue::array(vec![]))
12925            }
12926
12927            // String ops
12928            ExprKind::Chomp(expr) => {
12929                let val = self.eval_expr(expr)?;
12930                self.chomp_inplace_execute(val, expr)
12931            }
12932            ExprKind::Chop(expr) => {
12933                let val = self.eval_expr(expr)?;
12934                self.chop_inplace_execute(val, expr)
12935            }
12936            ExprKind::Length(expr) => {
12937                let val = self.eval_expr(expr)?;
12938                Ok(if let Some(a) = val.as_array_vec() {
12939                    StrykeValue::integer(a.len() as i64)
12940                } else if let Some(h) = val.as_hash_map() {
12941                    StrykeValue::integer(h.len() as i64)
12942                } else if let Some(b) = val.as_bytes_arc() {
12943                    // Raw byte buffer: always byte count, regardless of utf8 pragma.
12944                    StrykeValue::integer(b.len() as i64)
12945                } else {
12946                    let s = val.to_string();
12947                    let n = if self.utf8_pragma {
12948                        s.chars().count()
12949                    } else {
12950                        s.len()
12951                    };
12952                    StrykeValue::integer(n as i64)
12953                })
12954            }
12955            ExprKind::Substr {
12956                string,
12957                offset,
12958                length,
12959                replacement,
12960            } => self.eval_substr_expr(
12961                string.as_ref(),
12962                offset.as_ref(),
12963                length.as_deref(),
12964                replacement.as_deref(),
12965                line,
12966            ),
12967            ExprKind::Index {
12968                string,
12969                substr,
12970                position,
12971            } => {
12972                let s = self.eval_expr(string)?.to_string();
12973                let sub = self.eval_expr(substr)?.to_string();
12974                // Perl: negative POS clamps to 0; POS past end returns -1
12975                // (or, for empty needle, returns POS clamped to len).
12976                let pos = if let Some(p) = position {
12977                    let raw = self.eval_expr(p)?.to_int();
12978                    if raw < 0 {
12979                        0usize
12980                    } else {
12981                        (raw as usize).min(s.len())
12982                    }
12983                } else {
12984                    0
12985                };
12986                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
12987                Ok(StrykeValue::integer(result))
12988            }
12989            ExprKind::Rindex {
12990                string,
12991                substr,
12992                position,
12993            } => {
12994                let s = self.eval_expr(string)?.to_string();
12995                let sub = self.eval_expr(substr)?.to_string();
12996                // Perl: negative POS means "search must end at or before POS";
12997                // any negative POS implies no possible match → -1.
12998                let result = if let Some(p) = position {
12999                    let raw = self.eval_expr(p)?.to_int();
13000                    if raw < 0 {
13001                        -1
13002                    } else {
13003                        let end = (raw as usize).saturating_add(sub.len()).min(s.len());
13004                        s[..end].rfind(&sub).map(|i| i as i64).unwrap_or(-1)
13005                    }
13006                } else {
13007                    s.rfind(&sub).map(|i| i as i64).unwrap_or(-1)
13008                };
13009                Ok(StrykeValue::integer(result))
13010            }
13011            ExprKind::Sprintf { format, args } => {
13012                let fmt = self.eval_expr(format)?.to_string();
13013                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
13014                // builtins into individual format arguments.
13015                let mut arg_vals = Vec::new();
13016                for a in args {
13017                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
13018                    if let Some(items) = v.as_array_vec() {
13019                        arg_vals.extend(items);
13020                    } else {
13021                        arg_vals.push(v);
13022                    }
13023                }
13024                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
13025                Ok(StrykeValue::string(s))
13026            }
13027            ExprKind::JoinExpr { separator, list } => {
13028                let sep = self.eval_expr(separator)?.to_string();
13029                // Like Perl 5, arguments after the separator are evaluated in list context so
13030                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
13031                // expands `localtime` to nine fields.
13032                let items = if let ExprKind::List(exprs) = &list.kind {
13033                    let saved = self.wantarray_kind;
13034                    self.wantarray_kind = WantarrayCtx::List;
13035                    let mut vals = Vec::new();
13036                    for e in exprs {
13037                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
13038                        if let Some(items) = v.as_array_vec() {
13039                            vals.extend(items);
13040                        } else if v.is_iterator() {
13041                            // `join "", rev chars(...)` etc. — drain the
13042                            // lazy iterator into items so it joins the
13043                            // sequence instead of stringifying as "Iterator".
13044                            vals.extend(v.into_iterator().collect_all());
13045                        } else {
13046                            vals.push(v);
13047                        }
13048                    }
13049                    self.wantarray_kind = saved;
13050                    vals
13051                } else {
13052                    let saved = self.wantarray_kind;
13053                    self.wantarray_kind = WantarrayCtx::List;
13054                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
13055                    self.wantarray_kind = saved;
13056                    if let Some(items) = v.as_array_vec() {
13057                        items
13058                    } else if v.is_iterator() {
13059                        // `~> ... rev |> join ""` produces an Iterator from
13060                        // the lazy stages; drain it before joining, otherwise
13061                        // it stringifies as "Iterator".
13062                        v.into_iterator().collect_all()
13063                    } else {
13064                        vec![v]
13065                    }
13066                };
13067                let mut strs = Vec::with_capacity(items.len());
13068                for v in &items {
13069                    strs.push(self.stringify_value(v.clone(), line)?);
13070                }
13071                Ok(StrykeValue::string(strs.join(&sep)))
13072            }
13073            ExprKind::SplitExpr {
13074                pattern,
13075                string,
13076                limit,
13077            } => {
13078                let pat_val = self.eval_expr(pattern)?;
13079                // For a regex value, pull the *source* (not the Display form,
13080                // which wraps an empty regex as `(?:)` and would defeat the
13081                // empty-pattern branch below).  Mirrors the VM's Split path.
13082                let pat = pat_val
13083                    .regex_src_and_flags()
13084                    .map(|(s, _)| s)
13085                    .unwrap_or_else(|| pat_val.to_string());
13086                let s = self.eval_expr(string)?.to_string();
13087                if s.is_empty() {
13088                    return Ok(StrykeValue::array(vec![]));
13089                }
13090                // Perl semantics for the limit field:
13091                //   omitted / 0  → no truncation, *strip* trailing empty fields.
13092                //   > 0          → at most LIMIT fields, keep empties up to limit.
13093                //   < 0          → no truncation, *keep* all empties.
13094                // Stryke previously parsed limit as `usize`, which folded a
13095                // user-supplied -1 into a giant positive number and made the
13096                // strip / keep decision ambiguous. Use `i64` so the sign is
13097                // preserved.
13098                let lim_opt: Option<i64> = limit
13099                    .as_ref()
13100                    .map(|l| self.eval_expr(l).map(|v| v.to_int()))
13101                    .transpose()?;
13102                let re = self.compile_regex(&pat, "", line)?;
13103                let mut parts: Vec<String> = match lim_opt {
13104                    Some(l) if l > 0 => re.splitn_strings(&s, l as usize),
13105                    _ => re.split_strings(&s),
13106                };
13107
13108                // Zero-width patterns (`split //, $s`) are defined by Perl as
13109                // "split between every character" — the regex engine, however,
13110                // also matches the empty string at position 0, producing a
13111                // spurious leading empty field that Perl does not emit. Strip
13112                // it before the trailing-empty rule kicks in.
13113                if pat.is_empty() && parts.first().is_some_and(|p| p.is_empty()) {
13114                    parts.remove(0);
13115                }
13116                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted
13117                // or zero. Positive LIMIT keeps trailing empties; negative
13118                // LIMIT also keeps them.
13119                let strip_trailing = matches!(lim_opt, None | Some(0));
13120                if strip_trailing {
13121                    while parts.last().is_some_and(|p| p.is_empty()) {
13122                        parts.pop();
13123                    }
13124                }
13125
13126                Ok(StrykeValue::array(
13127                    parts.into_iter().map(StrykeValue::string).collect(),
13128                ))
13129            }
13130
13131            // Numeric
13132            ExprKind::Abs(expr) => {
13133                let val = self.eval_expr(expr)?;
13134                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
13135                    return r;
13136                }
13137                Ok(StrykeValue::float(val.to_number().abs()))
13138            }
13139            ExprKind::Int(expr) => {
13140                let val = self.eval_expr(expr)?;
13141                Ok(StrykeValue::integer(val.to_number() as i64))
13142            }
13143            ExprKind::Sqrt(expr) => {
13144                let val = self.eval_expr(expr)?;
13145                Ok(StrykeValue::float(val.to_number().sqrt()))
13146            }
13147            ExprKind::Sin(expr) => {
13148                let val = self.eval_expr(expr)?;
13149                Ok(StrykeValue::float(val.to_number().sin()))
13150            }
13151            ExprKind::Cos(expr) => {
13152                let val = self.eval_expr(expr)?;
13153                Ok(StrykeValue::float(val.to_number().cos()))
13154            }
13155            ExprKind::Atan2 { y, x } => {
13156                let yv = self.eval_expr(y)?.to_number();
13157                let xv = self.eval_expr(x)?.to_number();
13158                Ok(StrykeValue::float(yv.atan2(xv)))
13159            }
13160            ExprKind::Exp(expr) => {
13161                let val = self.eval_expr(expr)?;
13162                Ok(StrykeValue::float(val.to_number().exp()))
13163            }
13164            ExprKind::Log(expr) => {
13165                let val = self.eval_expr(expr)?;
13166                Ok(StrykeValue::float(val.to_number().ln()))
13167            }
13168            ExprKind::Rand(upper) => {
13169                let u = match upper {
13170                    Some(e) => self.eval_expr(e)?.to_number(),
13171                    None => 1.0,
13172                };
13173                Ok(StrykeValue::float(self.perl_rand(u)))
13174            }
13175            ExprKind::Srand(seed) => {
13176                let s = match seed {
13177                    Some(e) => Some(self.eval_expr(e)?.to_number()),
13178                    None => None,
13179                };
13180                Ok(StrykeValue::integer(self.perl_srand(s)))
13181            }
13182            ExprKind::Hex(expr) => {
13183                let val = self.eval_expr(expr)?.to_string();
13184                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
13185                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
13186                Ok(StrykeValue::integer(n))
13187            }
13188            ExprKind::Oct(expr) => {
13189                let val = self.eval_expr(expr)?.to_string();
13190                let s = val.trim();
13191                let n = if s.starts_with("0x") || s.starts_with("0X") {
13192                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
13193                } else if s.starts_with("0b") || s.starts_with("0B") {
13194                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
13195                } else if s.starts_with("0o") || s.starts_with("0O") {
13196                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
13197                } else {
13198                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
13199                };
13200                Ok(StrykeValue::integer(n))
13201            }
13202
13203            // Case
13204            ExprKind::Lc(expr) => Ok(StrykeValue::string(
13205                self.eval_expr(expr)?.to_string().to_lowercase(),
13206            )),
13207            ExprKind::Uc(expr) => Ok(StrykeValue::string(
13208                self.eval_expr(expr)?.to_string().to_uppercase(),
13209            )),
13210            ExprKind::Lcfirst(expr) => {
13211                let s = self.eval_expr(expr)?.to_string();
13212                let mut chars = s.chars();
13213                let result = match chars.next() {
13214                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
13215                    None => String::new(),
13216                };
13217                Ok(StrykeValue::string(result))
13218            }
13219            ExprKind::Ucfirst(expr) => {
13220                let s = self.eval_expr(expr)?.to_string();
13221                let mut chars = s.chars();
13222                let result = match chars.next() {
13223                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
13224                    None => String::new(),
13225                };
13226                Ok(StrykeValue::string(result))
13227            }
13228            ExprKind::Fc(expr) => Ok(StrykeValue::string(default_case_fold_str(
13229                &self.eval_expr(expr)?.to_string(),
13230            ))),
13231            ExprKind::Crypt { plaintext, salt } => {
13232                let p = self.eval_expr(plaintext)?.to_string();
13233                let sl = self.eval_expr(salt)?.to_string();
13234                Ok(StrykeValue::string(perl_crypt(&p, &sl)))
13235            }
13236            ExprKind::Pos(e) => {
13237                let key = match e {
13238                    None => "_".to_string(),
13239                    Some(expr) => match &expr.kind {
13240                        ExprKind::ScalarVar(n) => n.clone(),
13241                        _ => self.eval_expr(expr)?.to_string(),
13242                    },
13243                };
13244                Ok(self
13245                    .regex_pos
13246                    .get(&key)
13247                    .copied()
13248                    .flatten()
13249                    .map(|p| StrykeValue::integer(p as i64))
13250                    .unwrap_or(StrykeValue::UNDEF))
13251            }
13252            ExprKind::Study(expr) => {
13253                let s = self.eval_expr(expr)?.to_string();
13254                Ok(Self::study_return_value(&s))
13255            }
13256
13257            // Type
13258            ExprKind::Defined(expr) => {
13259                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
13260                if let ExprKind::SubroutineRef(name) = &expr.kind {
13261                    let exists = self.resolve_sub_by_name(name).is_some();
13262                    return Ok(StrykeValue::integer(if exists { 1 } else { 0 }));
13263                }
13264                let val = self.eval_expr(expr)?;
13265                Ok(StrykeValue::integer(if val.is_undef() { 0 } else { 1 }))
13266            }
13267            ExprKind::Ref(expr) => {
13268                let val = self.eval_expr(expr)?;
13269                Ok(val.ref_type())
13270            }
13271            ExprKind::ScalarContext(expr) => {
13272                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
13273                Ok(v.scalar_context())
13274            }
13275
13276            // Char
13277            ExprKind::Chr(expr) => {
13278                let n = self.eval_expr(expr)?.to_int() as u32;
13279                Ok(StrykeValue::string(
13280                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
13281                ))
13282            }
13283            ExprKind::Ord(expr) => {
13284                let s = self.eval_expr(expr)?.to_string();
13285                Ok(StrykeValue::integer(
13286                    s.chars().next().map(|c| c as i64).unwrap_or(0),
13287                ))
13288            }
13289
13290            // I/O
13291            ExprKind::OpenMyHandle { .. } => Err(StrykeError::runtime(
13292                "internal: `open my $fh` handle used outside open()",
13293                line,
13294            )
13295            .into()),
13296            ExprKind::Open { handle, mode, file } => {
13297                if let ExprKind::OpenMyHandle { name } = &handle.kind {
13298                    self.scope
13299                        .declare_scalar_frozen(name, StrykeValue::UNDEF, false, None)?;
13300                    self.english_note_lexical_scalar(name);
13301                    let mode_s = self.eval_expr(mode)?.to_string();
13302                    let file_opt = if let Some(f) = file {
13303                        Some(self.eval_expr(f)?.to_string())
13304                    } else {
13305                        None
13306                    };
13307                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
13308                    self.scope.set_scalar(name, ret.clone())?;
13309                    return Ok(ret);
13310                }
13311                let handle_s = self.eval_expr(handle)?.to_string();
13312                let handle_name = self.resolve_io_handle_name(&handle_s);
13313                let mode_s = self.eval_expr(mode)?.to_string();
13314                let file_opt = if let Some(f) = file {
13315                    Some(self.eval_expr(f)?.to_string())
13316                } else {
13317                    None
13318                };
13319                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
13320                    .map_err(Into::into)
13321            }
13322            ExprKind::Close(expr) => {
13323                let s = self.eval_expr(expr)?.to_string();
13324                let name = self.resolve_io_handle_name(&s);
13325                self.close_builtin_execute(name).map_err(Into::into)
13326            }
13327            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
13328                self.readline_builtin_execute_list(handle.as_deref())
13329            } else {
13330                self.readline_builtin_execute(handle.as_deref())
13331            }
13332            .map_err(Into::into),
13333            ExprKind::Eof(expr) => match expr {
13334                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
13335                Some(e) => {
13336                    let name = self.eval_expr(e)?;
13337                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
13338                }
13339            },
13340
13341            ExprKind::Opendir { handle, path } => {
13342                let h = self.eval_expr(handle)?.to_string();
13343                let p = self.eval_expr(path)?.to_string();
13344                Ok(self.opendir_handle(&h, &p))
13345            }
13346            ExprKind::Readdir(e) => {
13347                let h = self.eval_expr(e)?.to_string();
13348                Ok(if ctx == WantarrayCtx::List {
13349                    self.readdir_handle_list(&h)
13350                } else {
13351                    self.readdir_handle(&h)
13352                })
13353            }
13354            ExprKind::Closedir(e) => {
13355                let h = self.eval_expr(e)?.to_string();
13356                Ok(self.closedir_handle(&h))
13357            }
13358            ExprKind::Rewinddir(e) => {
13359                let h = self.eval_expr(e)?.to_string();
13360                Ok(self.rewinddir_handle(&h))
13361            }
13362            ExprKind::Telldir(e) => {
13363                let h = self.eval_expr(e)?.to_string();
13364                Ok(self.telldir_handle(&h))
13365            }
13366            ExprKind::Seekdir { handle, position } => {
13367                let h = self.eval_expr(handle)?.to_string();
13368                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
13369                Ok(self.seekdir_handle(&h, pos))
13370            }
13371
13372            // File tests
13373            ExprKind::FileTest { op, expr } => {
13374                let raw = self.eval_expr(expr)?.to_string();
13375                let path = self.resolve_stryke_path_string(&raw);
13376                // -M, -A, -C return fractional days (float), not boolean
13377                if matches!(op, 'M' | 'A' | 'C') {
13378                    #[cfg(unix)]
13379                    {
13380                        return match crate::perl_fs::filetest_age_days(&path, *op) {
13381                            Some(days) => Ok(StrykeValue::float(days)),
13382                            None => Ok(StrykeValue::UNDEF),
13383                        };
13384                    }
13385                    #[cfg(not(unix))]
13386                    return Ok(StrykeValue::UNDEF);
13387                }
13388                // -s returns file size (or undef on error)
13389                if *op == 's' {
13390                    return match std::fs::metadata(&path) {
13391                        Ok(m) => Ok(StrykeValue::integer(m.len() as i64)),
13392                        Err(_) => Ok(StrykeValue::UNDEF),
13393                    };
13394                }
13395                let result = match op {
13396                    'e' => std::path::Path::new(&path).exists(),
13397                    'f' => std::path::Path::new(&path).is_file(),
13398                    'd' => std::path::Path::new(&path).is_dir(),
13399                    'l' => std::path::Path::new(&path).is_symlink(),
13400                    #[cfg(unix)]
13401                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
13402                    #[cfg(not(unix))]
13403                    'r' => std::fs::metadata(&path).is_ok(),
13404                    #[cfg(unix)]
13405                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
13406                    #[cfg(not(unix))]
13407                    'w' => std::fs::metadata(&path).is_ok(),
13408                    #[cfg(unix)]
13409                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
13410                    #[cfg(not(unix))]
13411                    'x' => false,
13412                    #[cfg(unix)]
13413                    'o' => crate::perl_fs::filetest_owned_effective(&path),
13414                    #[cfg(not(unix))]
13415                    'o' => false,
13416                    #[cfg(unix)]
13417                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
13418                    #[cfg(not(unix))]
13419                    'R' => false,
13420                    #[cfg(unix)]
13421                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
13422                    #[cfg(not(unix))]
13423                    'W' => false,
13424                    #[cfg(unix)]
13425                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
13426                    #[cfg(not(unix))]
13427                    'X' => false,
13428                    #[cfg(unix)]
13429                    'O' => crate::perl_fs::filetest_owned_real(&path),
13430                    #[cfg(not(unix))]
13431                    'O' => false,
13432                    'z' => std::fs::metadata(&path)
13433                        .map(|m| m.len() == 0)
13434                        .unwrap_or(true),
13435                    't' => crate::perl_fs::filetest_is_tty(&path),
13436                    #[cfg(unix)]
13437                    'p' => crate::perl_fs::filetest_is_pipe(&path),
13438                    #[cfg(not(unix))]
13439                    'p' => false,
13440                    #[cfg(unix)]
13441                    'S' => crate::perl_fs::filetest_is_socket(&path),
13442                    #[cfg(not(unix))]
13443                    'S' => false,
13444                    #[cfg(unix)]
13445                    'b' => crate::perl_fs::filetest_is_block_device(&path),
13446                    #[cfg(not(unix))]
13447                    'b' => false,
13448                    #[cfg(unix)]
13449                    'c' => crate::perl_fs::filetest_is_char_device(&path),
13450                    #[cfg(not(unix))]
13451                    'c' => false,
13452                    #[cfg(unix)]
13453                    'u' => crate::perl_fs::filetest_is_setuid(&path),
13454                    #[cfg(not(unix))]
13455                    'u' => false,
13456                    #[cfg(unix)]
13457                    'g' => crate::perl_fs::filetest_is_setgid(&path),
13458                    #[cfg(not(unix))]
13459                    'g' => false,
13460                    #[cfg(unix)]
13461                    'k' => crate::perl_fs::filetest_is_sticky(&path),
13462                    #[cfg(not(unix))]
13463                    'k' => false,
13464                    'T' => crate::perl_fs::filetest_is_text(&path),
13465                    'B' => crate::perl_fs::filetest_is_binary(&path),
13466                    _ => false,
13467                };
13468                Ok(StrykeValue::integer(if result { 1 } else { 0 }))
13469            }
13470
13471            // System
13472            ExprKind::System(args) => {
13473                // Perl `system`:
13474                //   - single string  → `sh -c "..."`
13475                //   - 2+ args        → exec program directly with the rest as
13476                //                     argv (skips the shell, so quoting bugs
13477                //                     in user data don't matter).
13478                // Return value is `$?` (status word), not the bare exit code.
13479                let mut cmd_args = Vec::new();
13480                for a in args {
13481                    cmd_args.push(self.eval_expr(a)?.to_string());
13482                }
13483                if cmd_args.is_empty() {
13484                    self.child_exit_status = -1;
13485                    return Ok(StrykeValue::integer(-1));
13486                }
13487                let status = if cmd_args.len() == 1 {
13488                    Command::new("sh").arg("-c").arg(&cmd_args[0]).status()
13489                } else {
13490                    Command::new(&cmd_args[0]).args(&cmd_args[1..]).status()
13491                };
13492                match status {
13493                    Ok(s) => {
13494                        self.record_child_exit_status(s);
13495                        Ok(StrykeValue::integer(self.child_exit_status))
13496                    }
13497                    Err(e) => {
13498                        self.apply_io_error_to_errno(&e);
13499                        self.child_exit_status = -1;
13500                        Ok(StrykeValue::integer(-1))
13501                    }
13502                }
13503            }
13504            ExprKind::Exec(args) => {
13505                let mut cmd_args = Vec::new();
13506                for a in args {
13507                    cmd_args.push(self.eval_expr(a)?.to_string());
13508                }
13509                if cmd_args.is_empty() {
13510                    return Ok(StrykeValue::integer(-1));
13511                }
13512                let status = Command::new("sh")
13513                    .arg("-c")
13514                    .arg(cmd_args.join(" "))
13515                    .status();
13516                match status {
13517                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
13518                    Err(e) => {
13519                        self.apply_io_error_to_errno(&e);
13520                        Ok(StrykeValue::integer(-1))
13521                    }
13522                }
13523            }
13524            ExprKind::Eval(expr) => {
13525                self.eval_nesting += 1;
13526                let out = match &expr.kind {
13527                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
13528                        Ok(v) => {
13529                            self.clear_eval_error();
13530                            Ok(v)
13531                        }
13532                        Err(FlowOrError::Error(e)) => {
13533                            self.set_eval_error_from_perl_error(&e);
13534                            Ok(StrykeValue::UNDEF)
13535                        }
13536                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
13537                    },
13538                    _ => {
13539                        let code = self.eval_expr(expr)?.to_string();
13540                        // Parse and execute the string as Perl code
13541                        match crate::parse_and_run_string(&code, self) {
13542                            Ok(v) => {
13543                                self.clear_eval_error();
13544                                Ok(v)
13545                            }
13546                            Err(e) => {
13547                                self.set_eval_error(e.to_string());
13548                                Ok(StrykeValue::UNDEF)
13549                            }
13550                        }
13551                    }
13552                };
13553                self.eval_nesting -= 1;
13554                out
13555            }
13556            ExprKind::Do(expr) => match &expr.kind {
13557                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
13558                _ => {
13559                    let val = self.eval_expr(expr)?;
13560                    let filename = val.to_string();
13561                    match read_file_text_perl_compat(&filename) {
13562                        Ok(code) => {
13563                            let code = crate::data_section::strip_perl_end_marker(&code);
13564                            match crate::parse_and_run_string_in_file(code, self, &filename) {
13565                                Ok(v) => Ok(v),
13566                                Err(e) => {
13567                                    self.set_eval_error(e.to_string());
13568                                    Ok(StrykeValue::UNDEF)
13569                                }
13570                            }
13571                        }
13572                        Err(e) => {
13573                            self.apply_io_error_to_errno(&e);
13574                            Ok(StrykeValue::UNDEF)
13575                        }
13576                    }
13577                }
13578            },
13579            ExprKind::Require(expr) => {
13580                let spec = self.eval_expr(expr)?.to_string();
13581                self.require_execute(&spec, line)
13582                    .map_err(FlowOrError::Error)
13583            }
13584            ExprKind::Exit(code) => {
13585                let c = if let Some(e) = code {
13586                    self.eval_expr(e)?.to_int() as i32
13587                } else {
13588                    0
13589                };
13590                Err(StrykeError::new(ErrorKind::Exit(c), "", line, &self.file).into())
13591            }
13592            ExprKind::Chdir(expr) => {
13593                let path = self.eval_expr(expr)?.to_string();
13594                match std::env::set_current_dir(&path) {
13595                    Ok(_) => {
13596                        if let Ok(c) = std::env::current_dir() {
13597                            self.stryke_pwd = std::fs::canonicalize(&c).unwrap_or(c);
13598                        }
13599                        Ok(StrykeValue::integer(1))
13600                    }
13601                    Err(e) => {
13602                        self.apply_io_error_to_errno(&e);
13603                        Ok(StrykeValue::integer(0))
13604                    }
13605                }
13606            }
13607            ExprKind::Mkdir { path, mode: _ } => {
13608                let raw = self.eval_expr(path)?.to_string();
13609                let p = self.resolve_stryke_path_string(&raw);
13610                match std::fs::create_dir(&p) {
13611                    Ok(_) => Ok(StrykeValue::integer(1)),
13612                    Err(e) => {
13613                        self.apply_io_error_to_errno(&e);
13614                        Ok(StrykeValue::integer(0))
13615                    }
13616                }
13617            }
13618            ExprKind::Unlink(args) => {
13619                let mut count = 0i64;
13620                for a in args {
13621                    let raw = self.eval_expr(a)?.to_string();
13622                    let path = self.resolve_stryke_path_string(&raw);
13623                    if std::fs::remove_file(&path).is_ok() {
13624                        count += 1;
13625                    }
13626                }
13627                Ok(StrykeValue::integer(count))
13628            }
13629            ExprKind::Rename { old, new } => {
13630                let o_raw = self.eval_expr(old)?.to_string();
13631                let n_raw = self.eval_expr(new)?.to_string();
13632                let o = self.resolve_stryke_path_string(&o_raw);
13633                let n = self.resolve_stryke_path_string(&n_raw);
13634                Ok(crate::perl_fs::rename_paths(&o, &n))
13635            }
13636            ExprKind::Chmod(args) => {
13637                let mode = self.eval_expr(&args[0])?.to_int();
13638                let mut paths = Vec::new();
13639                for a in &args[1..] {
13640                    let raw = self.eval_expr(a)?.to_string();
13641                    paths.push(self.resolve_stryke_path_string(&raw));
13642                }
13643                Ok(StrykeValue::integer(crate::perl_fs::chmod_paths(
13644                    &paths, mode,
13645                )))
13646            }
13647            ExprKind::Chown(args) => {
13648                let uid = self.eval_expr(&args[0])?.to_int();
13649                let gid = self.eval_expr(&args[1])?.to_int();
13650                let mut paths = Vec::new();
13651                for a in &args[2..] {
13652                    let raw = self.eval_expr(a)?.to_string();
13653                    paths.push(self.resolve_stryke_path_string(&raw));
13654                }
13655                Ok(StrykeValue::integer(crate::perl_fs::chown_paths(
13656                    &paths, uid, gid,
13657                )))
13658            }
13659            ExprKind::Stat(e) => {
13660                let raw = self.eval_expr(e)?.to_string();
13661                let path = self.resolve_stryke_path_string(&raw);
13662                Ok(crate::perl_fs::stat_path(&path, false))
13663            }
13664            ExprKind::Lstat(e) => {
13665                let raw = self.eval_expr(e)?.to_string();
13666                let path = self.resolve_stryke_path_string(&raw);
13667                Ok(crate::perl_fs::stat_path(&path, true))
13668            }
13669            ExprKind::Link { old, new } => {
13670                let o_raw = self.eval_expr(old)?.to_string();
13671                let n_raw = self.eval_expr(new)?.to_string();
13672                let o = self.resolve_stryke_path_string(&o_raw);
13673                let n = self.resolve_stryke_path_string(&n_raw);
13674                Ok(crate::perl_fs::link_hard(&o, &n))
13675            }
13676            ExprKind::Symlink { old, new } => {
13677                let o = self.eval_expr(old)?.to_string();
13678                let n_raw = self.eval_expr(new)?.to_string();
13679                let n = self.resolve_stryke_path_string(&n_raw);
13680                Ok(crate::perl_fs::link_sym(&o, &n))
13681            }
13682            ExprKind::Readlink(e) => {
13683                let raw = self.eval_expr(e)?.to_string();
13684                let path = self.resolve_stryke_path_string(&raw);
13685                Ok(crate::perl_fs::read_link(&path))
13686            }
13687            ExprKind::Files(args) => {
13688                let dir_raw = if args.is_empty() {
13689                    ".".to_string()
13690                } else {
13691                    self.eval_expr(&args[0])?.to_string()
13692                };
13693                let dir = self.resolve_stryke_path_string(&dir_raw);
13694                Ok(crate::perl_fs::list_files(&dir))
13695            }
13696            ExprKind::Filesf(args) => {
13697                let dir_raw = if args.is_empty() {
13698                    ".".to_string()
13699                } else {
13700                    self.eval_expr(&args[0])?.to_string()
13701                };
13702                let dir = self.resolve_stryke_path_string(&dir_raw);
13703                Ok(crate::perl_fs::list_filesf(&dir))
13704            }
13705            ExprKind::FilesfRecursive(args) => {
13706                let dir_raw = if args.is_empty() {
13707                    ".".to_string()
13708                } else {
13709                    self.eval_expr(&args[0])?.to_string()
13710                };
13711                let dir = self.resolve_stryke_path_string(&dir_raw);
13712                Ok(StrykeValue::iterator(Arc::new(
13713                    crate::value::FsWalkIterator::new(&dir, true),
13714                )))
13715            }
13716            ExprKind::Dirs(args) => {
13717                let dir_raw = if args.is_empty() {
13718                    ".".to_string()
13719                } else {
13720                    self.eval_expr(&args[0])?.to_string()
13721                };
13722                let dir = self.resolve_stryke_path_string(&dir_raw);
13723                Ok(crate::perl_fs::list_dirs(&dir))
13724            }
13725            ExprKind::DirsRecursive(args) => {
13726                let dir_raw = if args.is_empty() {
13727                    ".".to_string()
13728                } else {
13729                    self.eval_expr(&args[0])?.to_string()
13730                };
13731                let dir = self.resolve_stryke_path_string(&dir_raw);
13732                Ok(StrykeValue::iterator(Arc::new(
13733                    crate::value::FsWalkIterator::new(&dir, false),
13734                )))
13735            }
13736            ExprKind::SymLinks(args) => {
13737                let dir_raw = if args.is_empty() {
13738                    ".".to_string()
13739                } else {
13740                    self.eval_expr(&args[0])?.to_string()
13741                };
13742                let dir = self.resolve_stryke_path_string(&dir_raw);
13743                Ok(crate::perl_fs::list_sym_links(&dir))
13744            }
13745            ExprKind::Sockets(args) => {
13746                let dir_raw = if args.is_empty() {
13747                    ".".to_string()
13748                } else {
13749                    self.eval_expr(&args[0])?.to_string()
13750                };
13751                let dir = self.resolve_stryke_path_string(&dir_raw);
13752                Ok(crate::perl_fs::list_sockets(&dir))
13753            }
13754            ExprKind::Pipes(args) => {
13755                let dir_raw = if args.is_empty() {
13756                    ".".to_string()
13757                } else {
13758                    self.eval_expr(&args[0])?.to_string()
13759                };
13760                let dir = self.resolve_stryke_path_string(&dir_raw);
13761                Ok(crate::perl_fs::list_pipes(&dir))
13762            }
13763            ExprKind::BlockDevices(args) => {
13764                let dir_raw = if args.is_empty() {
13765                    ".".to_string()
13766                } else {
13767                    self.eval_expr(&args[0])?.to_string()
13768                };
13769                let dir = self.resolve_stryke_path_string(&dir_raw);
13770                Ok(crate::perl_fs::list_block_devices(&dir))
13771            }
13772            ExprKind::CharDevices(args) => {
13773                let dir_raw = if args.is_empty() {
13774                    ".".to_string()
13775                } else {
13776                    self.eval_expr(&args[0])?.to_string()
13777                };
13778                let dir = self.resolve_stryke_path_string(&dir_raw);
13779                Ok(crate::perl_fs::list_char_devices(&dir))
13780            }
13781            ExprKind::Executables(args) => {
13782                let dir_raw = if args.is_empty() {
13783                    ".".to_string()
13784                } else {
13785                    self.eval_expr(&args[0])?.to_string()
13786                };
13787                let dir = self.resolve_stryke_path_string(&dir_raw);
13788                Ok(crate::perl_fs::list_executables(&dir))
13789            }
13790            ExprKind::Glob(args) => {
13791                // Pass the user's pattern through unchanged: zsh::glob runs from
13792                // OS cwd, which `chdir` keeps in sync with `stryke_pwd`. Resolving
13793                // relative patterns to absolute paths up front would turn
13794                // `glob("**(/)")` results from "sub" into "/abs/.../sub" — breaking
13795                // the documented contract that relative patterns yield relative
13796                // results (pinned in tests/suite/glob_zsh_qualifiers.rs).
13797                let mut pats = Vec::new();
13798                for a in args {
13799                    pats.push(self.eval_expr(a)?.to_string());
13800                }
13801                Ok(crate::perl_fs::glob_patterns(&pats))
13802            }
13803            ExprKind::GlobPar { args, progress } => {
13804                let mut pats = Vec::new();
13805                for a in args {
13806                    pats.push(self.eval_expr(a)?.to_string());
13807                }
13808                let show_progress = progress
13809                    .as_ref()
13810                    .map(|p| self.eval_expr(p))
13811                    .transpose()?
13812                    .map(|v| v.is_true())
13813                    .unwrap_or(false);
13814                if show_progress {
13815                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
13816                } else {
13817                    Ok(crate::perl_fs::glob_par_patterns(&pats))
13818                }
13819            }
13820            ExprKind::ParSed { args, progress } => {
13821                let has_progress = progress.is_some();
13822                let mut vals: Vec<StrykeValue> = Vec::new();
13823                for a in args {
13824                    vals.push(self.eval_expr(a)?);
13825                }
13826                if let Some(p) = progress {
13827                    vals.push(self.eval_expr(p.as_ref())?);
13828                }
13829                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
13830            }
13831            ExprKind::Bless { ref_expr, class } => {
13832                let val = self.eval_expr(ref_expr)?;
13833                let class_name = if let Some(c) = class {
13834                    self.eval_expr(c)?.to_string()
13835                } else {
13836                    self.scope.get_scalar("__PACKAGE__").to_string()
13837                };
13838                Ok(StrykeValue::blessed(Arc::new(
13839                    crate::value::BlessedRef::new_blessed(class_name, val),
13840                )))
13841            }
13842            ExprKind::Caller(_) => {
13843                // Simplified caller frame: (package, file, line, subname). The
13844                // sub name is the fully-qualified name of the currently
13845                // executing sub (the one that invoked `caller`). Returning it
13846                // unblocks logger / decorator patterns that rely on `caller`
13847                // for "who called me" identification.
13848                let sub_name = self
13849                    .current_sub_stack
13850                    .last()
13851                    .map(|s| StrykeValue::string(s.name.clone()))
13852                    .unwrap_or(StrykeValue::UNDEF);
13853                let pkg = self.current_package();
13854                Ok(StrykeValue::array(vec![
13855                    StrykeValue::string(pkg),
13856                    StrykeValue::string(self.file.clone()),
13857                    StrykeValue::integer(line as i64),
13858                    sub_name,
13859                ]))
13860            }
13861            ExprKind::Wantarray => Ok(match self.wantarray_kind {
13862                WantarrayCtx::Void => StrykeValue::UNDEF,
13863                WantarrayCtx::Scalar => StrykeValue::integer(0),
13864                WantarrayCtx::List => StrykeValue::integer(1),
13865            }),
13866
13867            ExprKind::List(exprs) => {
13868                // In scalar context, the comma operator evaluates to the last element.
13869                if ctx == WantarrayCtx::Scalar {
13870                    if let Some(last) = exprs.last() {
13871                        // Evaluate earlier expressions for side effects
13872                        for e in &exprs[..exprs.len() - 1] {
13873                            self.eval_expr(e)?;
13874                        }
13875                        return self.eval_expr(last);
13876                    } else {
13877                        return Ok(StrykeValue::UNDEF);
13878                    }
13879                }
13880                let mut vals = Vec::new();
13881                for e in exprs {
13882                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
13883                    if let Some(items) = v.as_array_vec() {
13884                        vals.extend(items);
13885                    } else {
13886                        vals.push(v);
13887                    }
13888                }
13889                if vals.len() == 1 {
13890                    Ok(vals.pop().unwrap())
13891                } else {
13892                    Ok(StrykeValue::array(vals))
13893                }
13894            }
13895
13896            // Postfix modifiers
13897            ExprKind::PostfixIf { expr, condition } => {
13898                if self.eval_postfix_condition(condition)? {
13899                    self.eval_expr(expr)
13900                } else {
13901                    Ok(StrykeValue::UNDEF)
13902                }
13903            }
13904            ExprKind::PostfixUnless { expr, condition } => {
13905                if !self.eval_postfix_condition(condition)? {
13906                    self.eval_expr(expr)
13907                } else {
13908                    Ok(StrykeValue::UNDEF)
13909                }
13910            }
13911            ExprKind::PostfixWhile { expr, condition } => {
13912                // `do { ... } while (COND)` — body runs before the first condition check.
13913                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
13914                let is_do_block = matches!(
13915                    &expr.kind,
13916                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
13917                );
13918                let mut last = StrykeValue::UNDEF;
13919                if is_do_block {
13920                    loop {
13921                        last = self.eval_expr(expr)?;
13922                        if !self.eval_postfix_condition(condition)? {
13923                            break;
13924                        }
13925                    }
13926                } else {
13927                    loop {
13928                        if !self.eval_postfix_condition(condition)? {
13929                            break;
13930                        }
13931                        last = self.eval_expr(expr)?;
13932                    }
13933                }
13934                Ok(last)
13935            }
13936            ExprKind::PostfixUntil { expr, condition } => {
13937                let is_do_block = matches!(
13938                    &expr.kind,
13939                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
13940                );
13941                let mut last = StrykeValue::UNDEF;
13942                if is_do_block {
13943                    loop {
13944                        last = self.eval_expr(expr)?;
13945                        if self.eval_postfix_condition(condition)? {
13946                            break;
13947                        }
13948                    }
13949                } else {
13950                    loop {
13951                        if self.eval_postfix_condition(condition)? {
13952                            break;
13953                        }
13954                        last = self.eval_expr(expr)?;
13955                    }
13956                }
13957                Ok(last)
13958            }
13959            ExprKind::PostfixForeach { expr, list } => {
13960                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
13961                let mut last = StrykeValue::UNDEF;
13962                for item in items {
13963                    self.scope.set_topic(item);
13964                    last = self.eval_expr(expr)?;
13965                }
13966                Ok(last)
13967            }
13968        }
13969    }
13970
13971    // ── Helpers ──
13972
13973    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
13974        match op {
13975            BinOp::Add => Some("+"),
13976            BinOp::Sub => Some("-"),
13977            BinOp::Mul => Some("*"),
13978            BinOp::Div => Some("/"),
13979            BinOp::Mod => Some("%"),
13980            BinOp::Pow => Some("**"),
13981            BinOp::Concat => Some("."),
13982            BinOp::StrEq => Some("eq"),
13983            BinOp::NumEq => Some("=="),
13984            BinOp::StrNe => Some("ne"),
13985            BinOp::NumNe => Some("!="),
13986            BinOp::StrLt => Some("lt"),
13987            BinOp::StrGt => Some("gt"),
13988            BinOp::StrLe => Some("le"),
13989            BinOp::StrGe => Some("ge"),
13990            BinOp::NumLt => Some("<"),
13991            BinOp::NumGt => Some(">"),
13992            BinOp::NumLe => Some("<="),
13993            BinOp::NumGe => Some(">="),
13994            BinOp::Spaceship => Some("<=>"),
13995            BinOp::StrCmp => Some("cmp"),
13996            _ => None,
13997        }
13998    }
13999
14000    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
14001    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
14002        map.get("").or_else(|| map.get("\"\""))
14003    }
14004
14005    /// String context for blessed objects with `overload '""'`.
14006    pub(crate) fn stringify_value(
14007        &mut self,
14008        v: StrykeValue,
14009        line: usize,
14010    ) -> Result<String, FlowOrError> {
14011        if let Some(r) = self.try_overload_stringify(&v, line) {
14012            let pv = r?;
14013            return Ok(pv.to_string());
14014        }
14015        Ok(v.to_string())
14016    }
14017
14018    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
14019    pub(crate) fn perl_sprintf_stringify(
14020        &mut self,
14021        fmt: &str,
14022        args: &[StrykeValue],
14023        line: usize,
14024    ) -> Result<String, FlowOrError> {
14025        // Step 1: build the output and collect any `%n` store-targets.
14026        let (out, pending_n) = {
14027            let mut stringify = |v: &StrykeValue| -> Result<String, FlowOrError> {
14028                self.stringify_value(v.clone(), line)
14029            };
14030            perl_sprintf_format_full(fmt, args, &mut stringify)?
14031        };
14032        // Step 2: apply any `%n` writes through the proper scope path.
14033        for (target, count) in pending_n {
14034            self.assign_scalar_ref_deref(target, StrykeValue::integer(count), line)?;
14035        }
14036        Ok(out)
14037    }
14038
14039    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
14040    pub(crate) fn render_format_template(
14041        &mut self,
14042        tmpl: &crate::format::FormatTemplate,
14043        line: usize,
14044    ) -> Result<String, FlowOrError> {
14045        use crate::format::{FormatRecord, PictureSegment};
14046        let mut buf = String::new();
14047        for rec in &tmpl.records {
14048            match rec {
14049                FormatRecord::Literal(s) => {
14050                    buf.push_str(s);
14051                    buf.push('\n');
14052                }
14053                FormatRecord::Picture { segments, exprs } => {
14054                    let mut vals: Vec<String> = Vec::new();
14055                    for e in exprs {
14056                        let v = self.eval_expr(e)?;
14057                        vals.push(self.stringify_value(v, line)?);
14058                    }
14059                    let mut vi = 0usize;
14060                    let mut line_out = String::new();
14061                    for seg in segments {
14062                        match seg {
14063                            PictureSegment::Literal(t) => line_out.push_str(t),
14064                            PictureSegment::Field {
14065                                width,
14066                                align,
14067                                kind: _,
14068                            } => {
14069                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
14070                                vi += 1;
14071                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
14072                            }
14073                        }
14074                    }
14075                    buf.push_str(line_out.trim_end());
14076                    buf.push('\n');
14077                }
14078            }
14079        }
14080        Ok(buf)
14081    }
14082
14083    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
14084    pub(crate) fn resolve_write_output_handle(
14085        &self,
14086        v: &StrykeValue,
14087        line: usize,
14088    ) -> StrykeResult<String> {
14089        if let Some(n) = v.as_io_handle_name() {
14090            let n = self.resolve_io_handle_name(&n);
14091            if self.is_bound_handle(&n) {
14092                return Ok(n);
14093            }
14094        }
14095        if let Some(s) = v.as_str() {
14096            if self.is_bound_handle(&s) {
14097                return Ok(self.resolve_io_handle_name(&s));
14098            }
14099        }
14100        let s = v.to_string();
14101        if self.is_bound_handle(&s) {
14102            return Ok(self.resolve_io_handle_name(&s));
14103        }
14104        Err(StrykeError::runtime(
14105            format!("write: invalid or unopened filehandle {}", s),
14106            line,
14107        ))
14108    }
14109
14110    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
14111    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
14112    /// that handle like `write FH`.
14113    pub(crate) fn write_format_execute(
14114        &mut self,
14115        args: &[StrykeValue],
14116        line: usize,
14117    ) -> StrykeResult<StrykeValue> {
14118        let handle_name = match args.len() {
14119            0 => self.default_print_handle.clone(),
14120            1 => self.resolve_write_output_handle(&args[0], line)?,
14121            _ => {
14122                return Err(StrykeError::runtime("write: too many arguments", line));
14123            }
14124        };
14125        let pkg = self.current_package();
14126        let mut fmt_name = self.scope.get_scalar("~").to_string();
14127        if fmt_name.is_empty() {
14128            fmt_name = "STDOUT".to_string();
14129        }
14130        let key = format!("{}::{}", pkg, fmt_name);
14131        let tmpl = self
14132            .format_templates
14133            .get(&key)
14134            .map(Arc::clone)
14135            .ok_or_else(|| {
14136                StrykeError::runtime(
14137                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
14138                    line,
14139                )
14140            })?;
14141        let out = self
14142            .render_format_template(&tmpl, line)
14143            .map_err(|e| match e {
14144                FlowOrError::Error(e) => e,
14145                FlowOrError::Flow(_) => {
14146                    StrykeError::runtime("write: unexpected control flow", line)
14147                }
14148            })?;
14149        self.write_formatted_print(handle_name.as_str(), &out, line)?;
14150        Ok(StrykeValue::integer(1))
14151    }
14152
14153    pub(crate) fn try_overload_stringify(
14154        &mut self,
14155        v: &StrykeValue,
14156        line: usize,
14157    ) -> Option<ExecResult> {
14158        // Native class instance: look for method named '""' or 'stringify'
14159        if let Some(c) = v.as_class_inst() {
14160            let method_name = c
14161                .def
14162                .method("stringify")
14163                .or_else(|| c.def.method("\"\""))
14164                .filter(|m| m.body.is_some())?;
14165            let body = method_name.body.clone().unwrap();
14166            let params = method_name.params.clone();
14167            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
14168        }
14169        let br = v.as_blessed_ref()?;
14170        let class = br.class.clone();
14171        let map = self.overload_table.get(&class)?;
14172        let sub_short = Self::overload_stringify_method(map)?;
14173        let fq = format!("{}::{}", class, sub_short);
14174        let sub = self.subs.get(&fq)?.clone();
14175        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
14176    }
14177
14178    /// Map overload operator key to native class method name.
14179    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
14180        match key {
14181            "+" => Some("op_add"),
14182            "-" => Some("op_sub"),
14183            "*" => Some("op_mul"),
14184            "/" => Some("op_div"),
14185            "%" => Some("op_mod"),
14186            "**" => Some("op_pow"),
14187            "." => Some("op_concat"),
14188            "==" => Some("op_eq"),
14189            "!=" => Some("op_ne"),
14190            "<" => Some("op_lt"),
14191            ">" => Some("op_gt"),
14192            "<=" => Some("op_le"),
14193            ">=" => Some("op_ge"),
14194            "<=>" => Some("op_spaceship"),
14195            "eq" => Some("op_str_eq"),
14196            "ne" => Some("op_str_ne"),
14197            "lt" => Some("op_str_lt"),
14198            "gt" => Some("op_str_gt"),
14199            "le" => Some("op_str_le"),
14200            "ge" => Some("op_str_ge"),
14201            "cmp" => Some("op_cmp"),
14202            _ => None,
14203        }
14204    }
14205
14206    pub(crate) fn try_overload_binop(
14207        &mut self,
14208        op: BinOp,
14209        lv: &StrykeValue,
14210        rv: &StrykeValue,
14211        line: usize,
14212    ) -> Option<ExecResult> {
14213        let key = Self::overload_key_for_binop(op)?;
14214        // Native class instance overloading
14215        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
14216            (Some(c.def.clone()), lv.clone(), rv.clone())
14217        } else if let Some(c) = rv.as_class_inst() {
14218            (Some(c.def.clone()), rv.clone(), lv.clone())
14219        } else {
14220            (None, lv.clone(), rv.clone())
14221        };
14222        if let Some(ref def) = ci_def {
14223            if let Some(method_name) = Self::overload_method_name_for_key(key) {
14224                if let Some((m, _)) = self.find_class_method(def, method_name) {
14225                    if let Some(ref body) = m.body {
14226                        let params = m.params.clone();
14227                        return Some(self.call_class_method(
14228                            body,
14229                            &params,
14230                            vec![invocant, other],
14231                            line,
14232                        ));
14233                    }
14234                }
14235            }
14236        }
14237        // Blessed ref overloading (existing path)
14238        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
14239            (br.class.clone(), lv.clone(), rv.clone())
14240        } else if let Some(br) = rv.as_blessed_ref() {
14241            (br.class.clone(), rv.clone(), lv.clone())
14242        } else {
14243            return None;
14244        };
14245        let map = self.overload_table.get(&class)?;
14246        let sub_short = if let Some(s) = map.get(key) {
14247            s.clone()
14248        } else if let Some(nm) = map.get("nomethod") {
14249            let fq = format!("{}::{}", class, nm);
14250            let sub = self.subs.get(&fq)?.clone();
14251            return Some(self.call_sub(
14252                &sub,
14253                vec![invocant, other, StrykeValue::string(key.to_string())],
14254                WantarrayCtx::Scalar,
14255                line,
14256            ));
14257        } else {
14258            return None;
14259        };
14260        let fq = format!("{}::{}", class, sub_short);
14261        let sub = self.subs.get(&fq)?.clone();
14262        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
14263    }
14264
14265    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
14266    pub(crate) fn try_overload_unary_dispatch(
14267        &mut self,
14268        op_key: &str,
14269        val: &StrykeValue,
14270        line: usize,
14271    ) -> Option<ExecResult> {
14272        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
14273        if let Some(c) = val.as_class_inst() {
14274            let method_name = match op_key {
14275                "neg" => "op_neg",
14276                "bool" => "op_bool",
14277                "abs" => "op_abs",
14278                "0+" => "op_numify",
14279                _ => return None,
14280            };
14281            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
14282                if let Some(ref body) = m.body {
14283                    let params = m.params.clone();
14284                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
14285                }
14286            }
14287            return None;
14288        }
14289        // Blessed ref path
14290        let br = val.as_blessed_ref()?;
14291        let class = br.class.clone();
14292        let map = self.overload_table.get(&class)?;
14293        if let Some(s) = map.get(op_key) {
14294            let fq = format!("{}::{}", class, s);
14295            let sub = self.subs.get(&fq)?.clone();
14296            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
14297        }
14298        if let Some(nm) = map.get("nomethod") {
14299            let fq = format!("{}::{}", class, nm);
14300            let sub = self.subs.get(&fq)?.clone();
14301            return Some(self.call_sub(
14302                &sub,
14303                vec![val.clone(), StrykeValue::string(op_key.to_string())],
14304                WantarrayCtx::Scalar,
14305                line,
14306            ));
14307        }
14308        None
14309    }
14310
14311    #[inline]
14312    fn eval_binop(
14313        &mut self,
14314        op: BinOp,
14315        lv: &StrykeValue,
14316        rv: &StrykeValue,
14317        _line: usize,
14318    ) -> ExecResult {
14319        Ok(match op {
14320            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
14321            // Perl `+` is numeric addition only; string concatenation is `.`.
14322            BinOp::Add => {
14323                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14324                    StrykeValue::integer(a.wrapping_add(b))
14325                } else {
14326                    StrykeValue::float(lv.to_number() + rv.to_number())
14327                }
14328            }
14329            BinOp::Sub => {
14330                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14331                    StrykeValue::integer(a.wrapping_sub(b))
14332                } else {
14333                    StrykeValue::float(lv.to_number() - rv.to_number())
14334                }
14335            }
14336            BinOp::Mul => {
14337                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14338                    StrykeValue::integer(a.wrapping_mul(b))
14339                } else {
14340                    StrykeValue::float(lv.to_number() * rv.to_number())
14341                }
14342            }
14343            BinOp::Div => {
14344                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14345                    if b == 0 {
14346                        return Err(StrykeError::division_by_zero(
14347                            "Illegal division by zero",
14348                            _line,
14349                        )
14350                        .into());
14351                    }
14352                    if a % b == 0 {
14353                        StrykeValue::integer(a / b)
14354                    } else {
14355                        StrykeValue::float(a as f64 / b as f64)
14356                    }
14357                } else {
14358                    let d = rv.to_number();
14359                    if d == 0.0 {
14360                        return Err(StrykeError::division_by_zero(
14361                            "Illegal division by zero",
14362                            _line,
14363                        )
14364                        .into());
14365                    }
14366                    StrykeValue::float(lv.to_number() / d)
14367                }
14368            }
14369            BinOp::Mod => {
14370                let d = rv.to_int();
14371                if d == 0 {
14372                    return Err(StrykeError::division_by_zero("Illegal modulus zero", _line).into());
14373                }
14374                StrykeValue::integer(crate::value::perl_mod_i64(lv.to_int(), d))
14375            }
14376            BinOp::Pow => {
14377                // Under `--compat` or `use bigint;`, `compat_pow` promotes
14378                // to `BigInt` on overflow; otherwise it falls back to f64
14379                // (matches Perl's default i64-overflow-to-NV behavior).
14380                if crate::compat_mode() || crate::bigint_pragma() {
14381                    crate::value::compat_pow(lv, rv)
14382                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14383                    let int_pow = (b >= 0)
14384                        .then(|| u32::try_from(b).ok())
14385                        .flatten()
14386                        .and_then(|bu| a.checked_pow(bu))
14387                        .map(StrykeValue::integer);
14388                    int_pow
14389                        .unwrap_or_else(|| StrykeValue::float(lv.to_number().powf(rv.to_number())))
14390                } else {
14391                    StrykeValue::float(lv.to_number().powf(rv.to_number()))
14392                }
14393            }
14394            BinOp::Concat => {
14395                let mut s = String::new();
14396                lv.append_to(&mut s);
14397                rv.append_to(&mut s);
14398                StrykeValue::string(s)
14399            }
14400            BinOp::NumEq => {
14401                // Struct equality: compare all fields
14402                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
14403                    if a.def.name != b.def.name {
14404                        StrykeValue::integer(0)
14405                    } else {
14406                        let av = a.get_values();
14407                        let bv = b.get_values();
14408                        let eq = av.len() == bv.len()
14409                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
14410                        StrykeValue::integer(if eq { 1 } else { 0 })
14411                    }
14412                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14413                    StrykeValue::integer(if a == b { 1 } else { 0 })
14414                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
14415                    // Stryke (non-compat) sugar: `==` falls back to string
14416                    // compare when both operands are non-numeric strings, so
14417                    // `"G" == "G"` is true (Perl's `0 == 0` numeric is also
14418                    // true here, but `"G" == "T"` is false in stryke vs
14419                    // also-true in Perl). See `Op::NumEq` in vm.rs.
14420                    StrykeValue::integer(if lv.to_string() == rv.to_string() {
14421                        1
14422                    } else {
14423                        0
14424                    })
14425                } else {
14426                    StrykeValue::integer(if lv.to_number() == rv.to_number() {
14427                        1
14428                    } else {
14429                        0
14430                    })
14431                }
14432            }
14433            BinOp::NumNe => {
14434                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14435                    StrykeValue::integer(if a != b { 1 } else { 0 })
14436                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
14437                    StrykeValue::integer(if lv.to_string() != rv.to_string() {
14438                        1
14439                    } else {
14440                        0
14441                    })
14442                } else {
14443                    StrykeValue::integer(if lv.to_number() != rv.to_number() {
14444                        1
14445                    } else {
14446                        0
14447                    })
14448                }
14449            }
14450            BinOp::NumLt => {
14451                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14452                    StrykeValue::integer(if a < b { 1 } else { 0 })
14453                } else {
14454                    StrykeValue::integer(if lv.to_number() < rv.to_number() {
14455                        1
14456                    } else {
14457                        0
14458                    })
14459                }
14460            }
14461            BinOp::NumGt => {
14462                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14463                    StrykeValue::integer(if a > b { 1 } else { 0 })
14464                } else {
14465                    StrykeValue::integer(if lv.to_number() > rv.to_number() {
14466                        1
14467                    } else {
14468                        0
14469                    })
14470                }
14471            }
14472            BinOp::NumLe => {
14473                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14474                    StrykeValue::integer(if a <= b { 1 } else { 0 })
14475                } else {
14476                    StrykeValue::integer(if lv.to_number() <= rv.to_number() {
14477                        1
14478                    } else {
14479                        0
14480                    })
14481                }
14482            }
14483            BinOp::NumGe => {
14484                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14485                    StrykeValue::integer(if a >= b { 1 } else { 0 })
14486                } else {
14487                    StrykeValue::integer(if lv.to_number() >= rv.to_number() {
14488                        1
14489                    } else {
14490                        0
14491                    })
14492                }
14493            }
14494            BinOp::Spaceship => {
14495                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14496                    StrykeValue::integer(if a < b {
14497                        -1
14498                    } else if a > b {
14499                        1
14500                    } else {
14501                        0
14502                    })
14503                } else {
14504                    let a = lv.to_number();
14505                    let b = rv.to_number();
14506                    StrykeValue::integer(if a < b {
14507                        -1
14508                    } else if a > b {
14509                        1
14510                    } else {
14511                        0
14512                    })
14513                }
14514            }
14515            BinOp::StrEq => StrykeValue::integer(if lv.to_string() == rv.to_string() {
14516                1
14517            } else {
14518                0
14519            }),
14520            BinOp::StrNe => StrykeValue::integer(if lv.to_string() != rv.to_string() {
14521                1
14522            } else {
14523                0
14524            }),
14525            BinOp::StrLt => StrykeValue::integer(if lv.to_string() < rv.to_string() {
14526                1
14527            } else {
14528                0
14529            }),
14530            BinOp::StrGt => StrykeValue::integer(if lv.to_string() > rv.to_string() {
14531                1
14532            } else {
14533                0
14534            }),
14535            BinOp::StrLe => StrykeValue::integer(if lv.to_string() <= rv.to_string() {
14536                1
14537            } else {
14538                0
14539            }),
14540            BinOp::StrGe => StrykeValue::integer(if lv.to_string() >= rv.to_string() {
14541                1
14542            } else {
14543                0
14544            }),
14545            BinOp::StrCmp => {
14546                let cmp = lv.to_string().cmp(&rv.to_string());
14547                StrykeValue::integer(match cmp {
14548                    std::cmp::Ordering::Less => -1,
14549                    std::cmp::Ordering::Greater => 1,
14550                    std::cmp::Ordering::Equal => 0,
14551                })
14552            }
14553            BinOp::BitAnd => {
14554                if let Some(s) = crate::value::set_intersection(lv, rv) {
14555                    s
14556                } else {
14557                    StrykeValue::integer(lv.to_int() & rv.to_int())
14558                }
14559            }
14560            BinOp::BitOr => {
14561                if let Some(s) = crate::value::set_union(lv, rv) {
14562                    s
14563                } else {
14564                    StrykeValue::integer(lv.to_int() | rv.to_int())
14565                }
14566            }
14567            BinOp::BitXor => StrykeValue::integer(lv.to_int() ^ rv.to_int()),
14568            BinOp::ShiftLeft => StrykeValue::integer(perl_shl_i64(lv.to_int(), rv.to_int())),
14569            BinOp::ShiftRight => StrykeValue::integer(perl_shr_i64(lv.to_int(), rv.to_int())),
14570            // These should have been handled by short-circuit above
14571            BinOp::LogAnd
14572            | BinOp::LogOr
14573            | BinOp::DefinedOr
14574            | BinOp::LogAndWord
14575            | BinOp::LogOrWord => unreachable!(),
14576            BinOp::BindMatch | BinOp::BindNotMatch => {
14577                unreachable!("regex bind handled in eval_expr BinOp arm")
14578            }
14579        })
14580    }
14581
14582    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
14583    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
14584    /// length — that was silently wrong vs `perl`.
14585    fn err_modify_symbolic_aggregate_deref_inc_dec(
14586        kind: Sigil,
14587        is_pre: bool,
14588        is_inc: bool,
14589        line: usize,
14590    ) -> FlowOrError {
14591        let agg = match kind {
14592            Sigil::Array => "array",
14593            Sigil::Hash => "hash",
14594            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
14595        };
14596        let op = match (is_pre, is_inc) {
14597            (true, true) => "preincrement (++)",
14598            (true, false) => "predecrement (--)",
14599            (false, true) => "postincrement (++)",
14600            (false, false) => "postdecrement (--)",
14601        };
14602        FlowOrError::Error(StrykeError::runtime(
14603            format!("Can't modify {agg} dereference in {op}"),
14604            line,
14605        ))
14606    }
14607
14608    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
14609    pub(crate) fn symbolic_scalar_ref_postfix(
14610        &mut self,
14611        ref_val: StrykeValue,
14612        decrement: bool,
14613        line: usize,
14614    ) -> Result<StrykeValue, FlowOrError> {
14615        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
14616        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14617        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
14618        Ok(old)
14619    }
14620
14621    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
14622    /// [`Self::assign_value`] and the VM.
14623    pub(crate) fn assign_scalar_ref_deref(
14624        &mut self,
14625        ref_val: StrykeValue,
14626        val: StrykeValue,
14627        line: usize,
14628    ) -> ExecResult {
14629        if let Some(name) = ref_val.as_scalar_binding_name() {
14630            self.set_special_var(&name, &val)
14631                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14632            return Ok(StrykeValue::UNDEF);
14633        }
14634        if let Some(r) = ref_val.as_scalar_ref() {
14635            *r.write() = val;
14636            return Ok(StrykeValue::UNDEF);
14637        }
14638        // Plain primitive scalar value: under no-strict, perl symbolic-derefs
14639        // through the string. With `strict 'refs'`, emit perl's exact diagnostic.
14640        if ref_val.is_integer_like() || ref_val.is_float_like() || ref_val.is_string_like() {
14641            let s = ref_val.to_string();
14642            if self.strict_refs {
14643                return Err(StrykeError::runtime(
14644                    format!(
14645                        "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
14646                        s
14647                    ),
14648                    line,
14649                )
14650                .into());
14651            }
14652            self.set_special_var(&s, &val)
14653                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14654            return Ok(StrykeValue::UNDEF);
14655        }
14656        Err(StrykeError::runtime("Can't assign to non-scalar reference", line).into())
14657    }
14658
14659    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
14660    pub(crate) fn assign_symbolic_array_ref_deref(
14661        &mut self,
14662        ref_val: StrykeValue,
14663        val: StrykeValue,
14664        line: usize,
14665    ) -> ExecResult {
14666        if let Some(a) = ref_val.as_array_ref() {
14667            *a.write() = val.to_list();
14668            return Ok(StrykeValue::UNDEF);
14669        }
14670        if let Some(name) = ref_val.as_array_binding_name() {
14671            self.scope
14672                .set_array(&name, val.to_list())
14673                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14674            return Ok(StrykeValue::UNDEF);
14675        }
14676        if let Some(s) = ref_val.as_str() {
14677            if self.strict_refs {
14678                return Err(StrykeError::runtime(
14679                    format!(
14680                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
14681                        s
14682                    ),
14683                    line,
14684                )
14685                .into());
14686            }
14687            self.scope
14688                .set_array(&s, val.to_list())
14689                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14690            return Ok(StrykeValue::UNDEF);
14691        }
14692        Err(StrykeError::runtime("Can't assign to non-array reference", line).into())
14693    }
14694
14695    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
14696    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
14697    pub(crate) fn assign_symbolic_typeglob_ref_deref(
14698        &mut self,
14699        ref_val: StrykeValue,
14700        val: StrykeValue,
14701        line: usize,
14702    ) -> ExecResult {
14703        let lhs_name = if let Some(s) = ref_val.as_str() {
14704            if self.strict_refs {
14705                return Err(StrykeError::runtime(
14706                    format!(
14707                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
14708                        s
14709                    ),
14710                    line,
14711                )
14712                .into());
14713            }
14714            s.to_string()
14715        } else {
14716            return Err(
14717                StrykeError::runtime("Can't assign to non-glob symbolic reference", line).into(),
14718            );
14719        };
14720        let is_coderef = val.as_code_ref().is_some()
14721            || val
14722                .as_scalar_ref()
14723                .map(|r| r.read().as_code_ref().is_some())
14724                .unwrap_or(false);
14725        if is_coderef {
14726            return self.assign_typeglob_value(&lhs_name, val, line);
14727        }
14728        let rhs_key = val.to_string();
14729        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
14730            .map_err(FlowOrError::Error)?;
14731        Ok(StrykeValue::UNDEF)
14732    }
14733
14734    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
14735    pub(crate) fn assign_symbolic_hash_ref_deref(
14736        &mut self,
14737        ref_val: StrykeValue,
14738        val: StrykeValue,
14739        line: usize,
14740    ) -> ExecResult {
14741        let items = val.to_list();
14742        let mut map = IndexMap::new();
14743        let mut i = 0;
14744        while i + 1 < items.len() {
14745            map.insert(items[i].to_string(), items[i + 1].clone());
14746            i += 2;
14747        }
14748        if let Some(h) = ref_val.as_hash_ref() {
14749            *h.write() = map;
14750            return Ok(StrykeValue::UNDEF);
14751        }
14752        if let Some(name) = ref_val.as_hash_binding_name() {
14753            self.touch_env_hash(&name);
14754            self.scope
14755                .set_hash(&name, map)
14756                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14757            return Ok(StrykeValue::UNDEF);
14758        }
14759        if let Some(s) = ref_val.as_str() {
14760            if self.strict_refs {
14761                return Err(StrykeError::runtime(
14762                    format!(
14763                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
14764                        s
14765                    ),
14766                    line,
14767                )
14768                .into());
14769            }
14770            self.touch_env_hash(&s);
14771            self.scope
14772                .set_hash(&s, map)
14773                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14774            return Ok(StrykeValue::UNDEF);
14775        }
14776        Err(StrykeError::runtime("Can't assign to non-hash reference", line).into())
14777    }
14778
14779    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
14780    pub(crate) fn assign_arrow_hash_deref(
14781        &mut self,
14782        container: StrykeValue,
14783        key: String,
14784        val: StrykeValue,
14785        line: usize,
14786    ) -> ExecResult {
14787        if let Some(b) = container.as_blessed_ref() {
14788            let mut data = b.data.write();
14789            if let Some(r) = data.as_hash_ref() {
14790                r.write().insert(key, val);
14791                return Ok(StrykeValue::UNDEF);
14792            }
14793            if let Some(mut map) = data.as_hash_map() {
14794                map.insert(key, val);
14795                *data = StrykeValue::hash(map);
14796                return Ok(StrykeValue::UNDEF);
14797            }
14798            return Err(
14799                StrykeError::runtime("Can't assign into non-hash blessed ref", line).into(),
14800            );
14801        }
14802        if let Some(r) = container.as_hash_ref() {
14803            r.write().insert(key, val);
14804            return Ok(StrykeValue::UNDEF);
14805        }
14806        if let Some(name) = container.as_hash_binding_name() {
14807            self.touch_env_hash(&name);
14808            self.scope
14809                .set_hash_element(&name, &key, val)
14810                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14811            return Ok(StrykeValue::UNDEF);
14812        }
14813        Err(StrykeError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
14814    }
14815
14816    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
14817    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
14818    pub(crate) fn eval_arrow_array_base(
14819        &mut self,
14820        expr: &Expr,
14821        _line: usize,
14822    ) -> Result<StrykeValue, FlowOrError> {
14823        match &expr.kind {
14824            ExprKind::Deref {
14825                expr: inner,
14826                kind: Sigil::Array | Sigil::Scalar,
14827            } => self.eval_expr(inner),
14828            _ => self.eval_expr(expr),
14829        }
14830    }
14831
14832    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
14833    pub(crate) fn eval_arrow_hash_base(
14834        &mut self,
14835        expr: &Expr,
14836        _line: usize,
14837    ) -> Result<StrykeValue, FlowOrError> {
14838        match &expr.kind {
14839            ExprKind::Deref {
14840                expr: inner,
14841                kind: Sigil::Scalar,
14842            } => self.eval_expr(inner),
14843            _ => self.eval_expr(expr),
14844        }
14845    }
14846
14847    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
14848    pub(crate) fn read_arrow_array_element(
14849        &self,
14850        container: StrykeValue,
14851        idx: i64,
14852        line: usize,
14853    ) -> Result<StrykeValue, FlowOrError> {
14854        if let Some(a) = container.as_array_ref() {
14855            let arr = a.read();
14856            let i = if idx < 0 {
14857                (arr.len() as i64 + idx) as usize
14858            } else {
14859                idx as usize
14860            };
14861            return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
14862        }
14863        if let Some(name) = container.as_array_binding_name() {
14864            return Ok(self.scope.get_array_element(&name, idx));
14865        }
14866        if let Some(arr) = container.as_array_vec() {
14867            let i = if idx < 0 {
14868                (arr.len() as i64 + idx) as usize
14869            } else {
14870                idx as usize
14871            };
14872            return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
14873        }
14874        // Blessed arrayref (e.g. `Pair`) — `pairs` returns blessed `Pair` objects that
14875        // can be indexed via `$_->[0]` / `$_->[1]`.
14876        if let Some(b) = container.as_blessed_ref() {
14877            let inner = b.data.read().clone();
14878            if let Some(a) = inner.as_array_ref() {
14879                let arr = a.read();
14880                let i = if idx < 0 {
14881                    (arr.len() as i64 + idx) as usize
14882                } else {
14883                    idx as usize
14884                };
14885                return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
14886            }
14887        }
14888        Err(StrykeError::runtime("Can't use arrow deref on non-array-ref", line).into())
14889    }
14890
14891    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
14892    pub(crate) fn read_arrow_hash_element(
14893        &mut self,
14894        container: StrykeValue,
14895        key: &str,
14896        line: usize,
14897    ) -> Result<StrykeValue, FlowOrError> {
14898        if let Some(r) = container.as_hash_ref() {
14899            let h = r.read();
14900            return Ok(h.get(key).cloned().unwrap_or(StrykeValue::UNDEF));
14901        }
14902        if let Some(name) = container.as_hash_binding_name() {
14903            self.touch_env_hash(&name);
14904            return Ok(self.scope.get_hash_element(&name, key));
14905        }
14906        if let Some(b) = container.as_blessed_ref() {
14907            let data = b.data.read();
14908            if let Some(v) = data.hash_get(key) {
14909                return Ok(v);
14910            }
14911            if let Some(r) = data.as_hash_ref() {
14912                let h = r.read();
14913                return Ok(h.get(key).cloned().unwrap_or(StrykeValue::UNDEF));
14914            }
14915            return Err(StrykeError::runtime(
14916                "Can't access hash field on non-hash blessed ref",
14917                line,
14918            )
14919            .into());
14920        }
14921        // Struct field access via hash deref syntax: $struct->{field}
14922        if let Some(s) = container.as_struct_inst() {
14923            if let Some(idx) = s.def.field_index(key) {
14924                return Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF));
14925            }
14926            return Err(StrykeError::runtime(
14927                format!("struct {} has no field `{}`", s.def.name, key),
14928                line,
14929            )
14930            .into());
14931        }
14932        // Class instance field access via hash deref: $obj->{field}
14933        if let Some(c) = container.as_class_inst() {
14934            if let Some(idx) = c.def.field_index(key) {
14935                return Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF));
14936            }
14937            return Err(StrykeError::runtime(
14938                format!("class {} has no field `{}`", c.def.name, key),
14939                line,
14940            )
14941            .into());
14942        }
14943        Err(StrykeError::runtime("Can't use arrow deref on non-hash-ref", line).into())
14944    }
14945
14946    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
14947    pub(crate) fn arrow_array_postfix(
14948        &mut self,
14949        container: StrykeValue,
14950        idx: i64,
14951        decrement: bool,
14952        line: usize,
14953    ) -> Result<StrykeValue, FlowOrError> {
14954        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
14955        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14956        self.assign_arrow_array_deref(container, idx, new_val, line)?;
14957        Ok(old)
14958    }
14959
14960    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
14961    pub(crate) fn arrow_hash_postfix(
14962        &mut self,
14963        container: StrykeValue,
14964        key: String,
14965        decrement: bool,
14966        line: usize,
14967    ) -> Result<StrykeValue, FlowOrError> {
14968        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
14969        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14970        self.assign_arrow_hash_deref(container, key, new_val, line)?;
14971        Ok(old)
14972    }
14973
14974    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` evaluation. If a nullary
14975    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
14976    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
14977    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
14978    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
14979    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
14980    /// subs" in use`).
14981    pub(crate) fn resolve_bareword_rvalue(
14982        &mut self,
14983        name: &str,
14984        want: WantarrayCtx,
14985        line: usize,
14986    ) -> Result<StrykeValue, FlowOrError> {
14987        if name == "__PACKAGE__" {
14988            return Ok(StrykeValue::string(self.current_package()));
14989        }
14990        if let Some(sub) = self.resolve_sub_by_name(name) {
14991            return self.call_sub(&sub, vec![], want, line);
14992        }
14993        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
14994        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
14995            return r.map_err(Into::into);
14996        }
14997        Ok(StrykeValue::string(name.to_string()))
14998    }
14999
15000    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
15001    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
15002    /// compound / inc-dec / assign helpers below.
15003    pub(crate) fn arrow_array_slice_values(
15004        &mut self,
15005        container: StrykeValue,
15006        indices: &[i64],
15007        line: usize,
15008    ) -> Result<StrykeValue, FlowOrError> {
15009        let mut out = Vec::with_capacity(indices.len());
15010        for &idx in indices {
15011            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
15012            out.push(v);
15013        }
15014        Ok(StrykeValue::array(out))
15015    }
15016
15017    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment for
15018    /// multi-index `ArrowDeref { Array, List }`. Shared by the VM
15019    /// [`crate::bytecode::Op::SetArrowArraySlice`].
15020    pub(crate) fn assign_arrow_array_slice(
15021        &mut self,
15022        container: StrykeValue,
15023        indices: Vec<i64>,
15024        val: StrykeValue,
15025        line: usize,
15026    ) -> Result<StrykeValue, FlowOrError> {
15027        if indices.is_empty() {
15028            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15029        }
15030        let vals = val.to_list();
15031        for (i, idx) in indices.iter().enumerate() {
15032            let v = vals.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
15033            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
15034        }
15035        Ok(StrykeValue::UNDEF)
15036    }
15037
15038    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
15039    pub(crate) fn flatten_array_slice_index_specs(
15040        &mut self,
15041        indices: &[Expr],
15042    ) -> Result<Vec<i64>, FlowOrError> {
15043        let mut out = Vec::new();
15044        for idx_expr in indices {
15045            let v = if matches!(
15046                idx_expr.kind,
15047                ExprKind::Range { .. } | ExprKind::SliceRange { .. }
15048            ) {
15049                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
15050            } else {
15051                self.eval_expr(idx_expr)?
15052            };
15053            if let Some(list) = v.as_array_vec() {
15054                for idx in list {
15055                    out.push(idx.to_int());
15056                }
15057            } else {
15058                out.push(v.to_int());
15059            }
15060        }
15061        Ok(out)
15062    }
15063
15064    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
15065    pub(crate) fn assign_named_array_slice(
15066        &mut self,
15067        stash_array_name: &str,
15068        indices: Vec<i64>,
15069        val: StrykeValue,
15070        line: usize,
15071    ) -> Result<StrykeValue, FlowOrError> {
15072        if indices.is_empty() {
15073            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15074        }
15075        let vals = val.to_list();
15076        for (i, idx) in indices.iter().enumerate() {
15077            let v = vals.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
15078            self.scope
15079                .set_array_element(stash_array_name, *idx, v)
15080                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15081        }
15082        Ok(StrykeValue::UNDEF)
15083    }
15084
15085    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
15086    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
15087    pub(crate) fn compound_assign_arrow_array_slice(
15088        &mut self,
15089        container: StrykeValue,
15090        indices: Vec<i64>,
15091        op: BinOp,
15092        rhs: StrykeValue,
15093        line: usize,
15094    ) -> Result<StrykeValue, FlowOrError> {
15095        if indices.is_empty() {
15096            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15097        }
15098        let last_idx = *indices.last().expect("non-empty indices");
15099        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
15100        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
15101        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
15102        Ok(new_val)
15103    }
15104
15105    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
15106    /// pre forms return the new value, post forms return the old **last** element.
15107    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
15108    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
15109    pub(crate) fn arrow_array_slice_inc_dec(
15110        &mut self,
15111        container: StrykeValue,
15112        indices: Vec<i64>,
15113        kind: u8,
15114        line: usize,
15115    ) -> Result<StrykeValue, FlowOrError> {
15116        if indices.is_empty() {
15117            return Err(StrykeError::runtime(
15118                "array slice increment needs at least one index",
15119                line,
15120            )
15121            .into());
15122        }
15123        let last_idx = *indices.last().expect("non-empty indices");
15124        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
15125        let new_val = if kind & 1 == 0 {
15126            StrykeValue::integer(last_old.to_int() + 1)
15127        } else {
15128            StrykeValue::integer(last_old.to_int() - 1)
15129        };
15130        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
15131        Ok(if kind < 2 { new_val } else { last_old })
15132    }
15133
15134    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
15135    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
15136    pub(crate) fn named_array_slice_inc_dec(
15137        &mut self,
15138        stash_array_name: &str,
15139        indices: Vec<i64>,
15140        kind: u8,
15141        line: usize,
15142    ) -> Result<StrykeValue, FlowOrError> {
15143        let last_idx = *indices.last().ok_or_else(|| {
15144            StrykeError::runtime("array slice increment needs at least one index", line)
15145        })?;
15146        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
15147        let new_val = if kind & 1 == 0 {
15148            StrykeValue::integer(last_old.to_int() + 1)
15149        } else {
15150            StrykeValue::integer(last_old.to_int() - 1)
15151        };
15152        self.scope
15153            .set_array_element(stash_array_name, last_idx, new_val.clone())
15154            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15155        Ok(if kind < 2 { new_val } else { last_old })
15156    }
15157
15158    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
15159    pub(crate) fn compound_assign_named_array_slice(
15160        &mut self,
15161        stash_array_name: &str,
15162        indices: Vec<i64>,
15163        op: BinOp,
15164        rhs: StrykeValue,
15165        line: usize,
15166    ) -> Result<StrykeValue, FlowOrError> {
15167        if indices.is_empty() {
15168            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15169        }
15170        let last_idx = *indices.last().expect("non-empty indices");
15171        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
15172        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
15173        self.scope
15174            .set_array_element(stash_array_name, last_idx, new_val.clone())
15175            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15176        Ok(new_val)
15177    }
15178
15179    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
15180    pub(crate) fn assign_arrow_array_deref(
15181        &mut self,
15182        container: StrykeValue,
15183        idx: i64,
15184        val: StrykeValue,
15185        line: usize,
15186    ) -> ExecResult {
15187        if let Some(a) = container.as_array_ref() {
15188            let mut arr = a.write();
15189            let i = if idx < 0 {
15190                (arr.len() as i64 + idx) as usize
15191            } else {
15192                idx as usize
15193            };
15194            if i >= arr.len() {
15195                arr.resize(i + 1, StrykeValue::UNDEF);
15196            }
15197            arr[i] = val;
15198            return Ok(StrykeValue::UNDEF);
15199        }
15200        if let Some(name) = container.as_array_binding_name() {
15201            self.scope
15202                .set_array_element(&name, idx, val)
15203                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15204            return Ok(StrykeValue::UNDEF);
15205        }
15206        Err(StrykeError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
15207    }
15208
15209    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
15210    pub(crate) fn assign_typeglob_value(
15211        &mut self,
15212        name: &str,
15213        val: StrykeValue,
15214        line: usize,
15215    ) -> ExecResult {
15216        let sub = if let Some(c) = val.as_code_ref() {
15217            Some(c)
15218        } else if let Some(r) = val.as_scalar_ref() {
15219            r.read().as_code_ref().map(|c| Arc::clone(&c))
15220        } else {
15221            None
15222        };
15223        if let Some(sub) = sub {
15224            let lhs_sub = self.qualify_typeglob_sub_key(name);
15225            self.subs.insert(lhs_sub, sub);
15226            return Ok(StrykeValue::UNDEF);
15227        }
15228        Err(StrykeError::runtime(
15229            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
15230            line,
15231        )
15232        .into())
15233    }
15234
15235    fn assign_value(&mut self, target: &Expr, val: StrykeValue) -> ExecResult {
15236        match &target.kind {
15237            // `substr($s, $o, $l) = $rhs` — equivalent to the 4-arg form
15238            // `substr($s, $o, $l, $rhs)`. Evaluate the offset/length
15239            // sub-exprs, splice the new substring into the target, then
15240            // recursively call assign_value to write the modified string
15241            // through the original `string` lvalue.
15242            ExprKind::Substr {
15243                string,
15244                offset,
15245                length,
15246                replacement: None,
15247            } => {
15248                let s = self.eval_expr(string)?.to_string();
15249                let off = self.eval_expr(offset)?.to_int();
15250                let start = if off < 0 {
15251                    (s.len() as i64 + off).max(0) as usize
15252                } else {
15253                    (off as usize).min(s.len())
15254                };
15255                let len = if let Some(l) = length {
15256                    let lv = self.eval_expr(l)?.to_int();
15257                    if lv < 0 {
15258                        let remaining = s.len().saturating_sub(start) as i64;
15259                        (remaining + lv).max(0) as usize
15260                    } else {
15261                        lv as usize
15262                    }
15263                } else {
15264                    s.len().saturating_sub(start)
15265                };
15266                let end = start.saturating_add(len).min(s.len());
15267                let mut new_s = String::with_capacity(s.len());
15268                new_s.push_str(&s[..start]);
15269                new_s.push_str(&val.to_string());
15270                new_s.push_str(&s[end..]);
15271                self.assign_value(string, StrykeValue::string(new_s))?;
15272                Ok(StrykeValue::UNDEF)
15273            }
15274            // `(my $copy = $orig) =~ s/.../.../` — at this point the
15275            // MyExpr has already been evaluated as a side-effect of
15276            // `eval_expr(target)` upstream (so `$copy` has been declared
15277            // and initialized). The substitution / transliteration helpers
15278            // call back here to write the *new* string. Bind through the
15279            // declared name without re-running the initializer.
15280            ExprKind::MyExpr { decls, .. } => {
15281                let first = decls.first().ok_or_else(|| {
15282                    FlowOrError::Error(StrykeError::runtime(
15283                        "assign_value: empty MyExpr decl list",
15284                        target.line,
15285                    ))
15286                })?;
15287                match first.sigil {
15288                    Sigil::Scalar => {
15289                        let stor = self.tree_scalar_storage_name(&first.name);
15290                        self.set_special_var(&stor, &val)
15291                            .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
15292                        Ok(StrykeValue::UNDEF)
15293                    }
15294                    Sigil::Array => {
15295                        self.scope.set_array(&first.name, val.to_list())?;
15296                        Ok(StrykeValue::UNDEF)
15297                    }
15298                    Sigil::Hash => {
15299                        let items = val.to_list();
15300                        let mut map = IndexMap::new();
15301                        let mut i = 0;
15302                        while i + 1 < items.len() {
15303                            map.insert(items[i].to_string(), items[i + 1].clone());
15304                            i += 2;
15305                        }
15306                        self.scope.set_hash(&first.name, map)?;
15307                        Ok(StrykeValue::UNDEF)
15308                    }
15309                    Sigil::Typeglob => Ok(StrykeValue::UNDEF),
15310                }
15311            }
15312            ExprKind::ScalarVar(name) => {
15313                let stor = self.tree_scalar_storage_name(name);
15314                if self.scope.is_scalar_frozen(&stor) {
15315                    return Err(FlowOrError::Error(StrykeError::runtime(
15316                        format!("Modification of a frozen value: ${}", name),
15317                        target.line,
15318                    )));
15319                }
15320                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
15321                    let class = obj
15322                        .as_blessed_ref()
15323                        .map(|b| b.class.clone())
15324                        .unwrap_or_default();
15325                    let full = format!("{}::STORE", class);
15326                    if let Some(sub) = self.subs.get(&full).cloned() {
15327                        let arg_vals = vec![obj, val];
15328                        return match self.call_sub(
15329                            &sub,
15330                            arg_vals,
15331                            WantarrayCtx::Scalar,
15332                            target.line,
15333                        ) {
15334                            Ok(_) => Ok(StrykeValue::UNDEF),
15335                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15336                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15337                        };
15338                    }
15339                }
15340                self.set_special_var(&stor, &val)
15341                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
15342                Ok(StrykeValue::UNDEF)
15343            }
15344            ExprKind::ArrayVar(name) => {
15345                if self.scope.is_array_frozen(name) {
15346                    return Err(StrykeError::runtime(
15347                        format!("Modification of a frozen value: @{}", name),
15348                        target.line,
15349                    )
15350                    .into());
15351                }
15352                if self.strict_vars
15353                    && !name.contains("::")
15354                    && !self.scope.array_binding_exists(name)
15355                {
15356                    return Err(StrykeError::runtime(
15357                        format!(
15358                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
15359                            name, name
15360                        ),
15361                        target.line,
15362                    )
15363                    .into());
15364                }
15365                self.scope.set_array(name, val.to_list())?;
15366                Ok(StrykeValue::UNDEF)
15367            }
15368            ExprKind::HashVar(name) => {
15369                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
15370                {
15371                    return Err(StrykeError::runtime(
15372                        format!(
15373                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15374                            name, name
15375                        ),
15376                        target.line,
15377                    )
15378                    .into());
15379                }
15380                let items = val.to_list();
15381                let mut map = IndexMap::new();
15382                let mut i = 0;
15383                while i + 1 < items.len() {
15384                    map.insert(items[i].to_string(), items[i + 1].clone());
15385                    i += 2;
15386                }
15387                self.scope.set_hash(name, map)?;
15388                Ok(StrykeValue::UNDEF)
15389            }
15390            ExprKind::ArrayElement { array, index } => {
15391                if self.strict_vars
15392                    && !array.contains("::")
15393                    && !self.scope.array_binding_exists(array)
15394                {
15395                    return Err(StrykeError::runtime(
15396                        format!(
15397                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
15398                            array, array
15399                        ),
15400                        target.line,
15401                    )
15402                    .into());
15403                }
15404                if self.scope.is_array_frozen(array) {
15405                    return Err(StrykeError::runtime(
15406                        format!("Modification of a frozen value: @{}", array),
15407                        target.line,
15408                    )
15409                    .into());
15410                }
15411                let idx = self.eval_expr(index)?.to_int();
15412                let aname = self.stash_array_name_for_package(array);
15413                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
15414                    let class = obj
15415                        .as_blessed_ref()
15416                        .map(|b| b.class.clone())
15417                        .unwrap_or_default();
15418                    let full = format!("{}::STORE", class);
15419                    if let Some(sub) = self.subs.get(&full).cloned() {
15420                        let arg_vals = vec![obj, StrykeValue::integer(idx), val];
15421                        return match self.call_sub(
15422                            &sub,
15423                            arg_vals,
15424                            WantarrayCtx::Scalar,
15425                            target.line,
15426                        ) {
15427                            Ok(_) => Ok(StrykeValue::UNDEF),
15428                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15429                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15430                        };
15431                    }
15432                }
15433                self.scope.set_array_element(&aname, idx, val)?;
15434                Ok(StrykeValue::UNDEF)
15435            }
15436            ExprKind::ArraySlice { array, indices } => {
15437                if indices.is_empty() {
15438                    return Err(
15439                        StrykeError::runtime("assign to empty array slice", target.line).into(),
15440                    );
15441                }
15442                self.check_strict_array_var(array, target.line)?;
15443                if self.scope.is_array_frozen(array) {
15444                    return Err(StrykeError::runtime(
15445                        format!("Modification of a frozen value: @{}", array),
15446                        target.line,
15447                    )
15448                    .into());
15449                }
15450                let aname = self.stash_array_name_for_package(array);
15451                let flat = self.flatten_array_slice_index_specs(indices)?;
15452                self.assign_named_array_slice(&aname, flat, val, target.line)
15453            }
15454            ExprKind::HashElement { hash, key } => {
15455                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
15456                {
15457                    return Err(StrykeError::runtime(
15458                        format!(
15459                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15460                            hash, hash
15461                        ),
15462                        target.line,
15463                    )
15464                    .into());
15465                }
15466                if self.scope.is_hash_frozen(hash) {
15467                    return Err(StrykeError::runtime(
15468                        format!("Modification of a frozen value: %%{}", hash),
15469                        target.line,
15470                    )
15471                    .into());
15472                }
15473                let k = self.eval_expr(key)?.to_string();
15474                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
15475                    let class = obj
15476                        .as_blessed_ref()
15477                        .map(|b| b.class.clone())
15478                        .unwrap_or_default();
15479                    let full = format!("{}::STORE", class);
15480                    if let Some(sub) = self.subs.get(&full).cloned() {
15481                        let arg_vals = vec![obj, StrykeValue::string(k), val];
15482                        return match self.call_sub(
15483                            &sub,
15484                            arg_vals,
15485                            WantarrayCtx::Scalar,
15486                            target.line,
15487                        ) {
15488                            Ok(_) => Ok(StrykeValue::UNDEF),
15489                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15490                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15491                        };
15492                    }
15493                }
15494                let hname = self.tree_hash_storage_name(hash);
15495                self.scope.set_hash_element(&hname, &k, val)?;
15496                Ok(StrykeValue::UNDEF)
15497            }
15498            ExprKind::HashSlice { hash, keys } => {
15499                if keys.is_empty() {
15500                    return Err(
15501                        StrykeError::runtime("assign to empty hash slice", target.line).into(),
15502                    );
15503                }
15504                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
15505                {
15506                    return Err(StrykeError::runtime(
15507                        format!(
15508                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15509                            hash, hash
15510                        ),
15511                        target.line,
15512                    )
15513                    .into());
15514                }
15515                if self.scope.is_hash_frozen(hash) {
15516                    return Err(StrykeError::runtime(
15517                        format!("Modification of a frozen value: %%{}", hash),
15518                        target.line,
15519                    )
15520                    .into());
15521                }
15522                let mut key_vals = Vec::with_capacity(keys.len());
15523                for key_expr in keys {
15524                    let v = if matches!(
15525                        key_expr.kind,
15526                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
15527                    ) {
15528                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
15529                    } else {
15530                        self.eval_expr(key_expr)?
15531                    };
15532                    key_vals.push(v);
15533                }
15534                self.assign_named_hash_slice(hash, key_vals, val, target.line)
15535            }
15536            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
15537            ExprKind::TypeglobExpr(e) => {
15538                let name = self.eval_expr(e)?.to_string();
15539                let synthetic = Expr {
15540                    kind: ExprKind::Typeglob(name),
15541                    line: target.line,
15542                };
15543                self.assign_value(&synthetic, val)
15544            }
15545            ExprKind::AnonymousListSlice { source, indices } => {
15546                if let ExprKind::Deref {
15547                    expr: inner,
15548                    kind: Sigil::Array,
15549                } = &source.kind
15550                {
15551                    let container = self.eval_arrow_array_base(inner, target.line)?;
15552                    let vals = val.to_list();
15553                    let n = indices.len().min(vals.len());
15554                    for i in 0..n {
15555                        let idx = self.eval_expr(&indices[i])?.to_int();
15556                        self.assign_arrow_array_deref(
15557                            container.clone(),
15558                            idx,
15559                            vals[i].clone(),
15560                            target.line,
15561                        )?;
15562                    }
15563                    return Ok(StrykeValue::UNDEF);
15564                }
15565                Err(
15566                    StrykeError::runtime("assign to list slice: unsupported base", target.line)
15567                        .into(),
15568                )
15569            }
15570            ExprKind::ArrowDeref {
15571                expr,
15572                index,
15573                kind: DerefKind::Hash,
15574            } => {
15575                let key = self.eval_expr(index)?.to_string();
15576                let container = self.eval_expr(expr)?;
15577                self.assign_arrow_hash_deref(container, key, val, target.line)
15578            }
15579            ExprKind::ArrowDeref {
15580                expr,
15581                index,
15582                kind: DerefKind::Array,
15583            } => {
15584                let container = self.eval_arrow_array_base(expr, target.line)?;
15585                if let ExprKind::List(indices) = &index.kind {
15586                    let vals = val.to_list();
15587                    let n = indices.len().min(vals.len());
15588                    for i in 0..n {
15589                        let idx = self.eval_expr(&indices[i])?.to_int();
15590                        self.assign_arrow_array_deref(
15591                            container.clone(),
15592                            idx,
15593                            vals[i].clone(),
15594                            target.line,
15595                        )?;
15596                    }
15597                    return Ok(StrykeValue::UNDEF);
15598                }
15599                let idx = self.eval_expr(index)?.to_int();
15600                self.assign_arrow_array_deref(container, idx, val, target.line)
15601            }
15602            ExprKind::HashSliceDeref { container, keys } => {
15603                let href = self.eval_expr(container)?;
15604                let mut key_vals = Vec::with_capacity(keys.len());
15605                for key_expr in keys {
15606                    key_vals.push(self.eval_expr(key_expr)?);
15607                }
15608                self.assign_hash_slice_deref(href, key_vals, val, target.line)
15609            }
15610            ExprKind::Deref {
15611                expr,
15612                kind: Sigil::Scalar,
15613            } => {
15614                let ref_val = self.eval_expr(expr)?;
15615                self.assign_scalar_ref_deref(ref_val, val, target.line)
15616            }
15617            ExprKind::Deref {
15618                expr,
15619                kind: Sigil::Array,
15620            } => {
15621                let ref_val = self.eval_expr(expr)?;
15622                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
15623            }
15624            ExprKind::Deref {
15625                expr,
15626                kind: Sigil::Hash,
15627            } => {
15628                let ref_val = self.eval_expr(expr)?;
15629                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
15630            }
15631            ExprKind::Deref {
15632                expr,
15633                kind: Sigil::Typeglob,
15634            } => {
15635                let ref_val = self.eval_expr(expr)?;
15636                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
15637            }
15638            ExprKind::Pos(inner) => {
15639                let key = match inner {
15640                    None => "_".to_string(),
15641                    Some(expr) => match &expr.kind {
15642                        ExprKind::ScalarVar(n) => n.clone(),
15643                        _ => self.eval_expr(expr)?.to_string(),
15644                    },
15645                };
15646                if val.is_undef() {
15647                    self.regex_pos.insert(key, None);
15648                } else {
15649                    let u = val.to_int().max(0) as usize;
15650                    self.regex_pos.insert(key, Some(u));
15651                }
15652                Ok(StrykeValue::UNDEF)
15653            }
15654            // List assignment: `($a, $b, ...) = (val1, val2, ...)`
15655            // RHS is already fully evaluated — distribute elements to targets.
15656            ExprKind::List(targets) => {
15657                let items = val.to_list();
15658                for (i, t) in targets.iter().enumerate() {
15659                    let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
15660                    self.assign_value(t, v)?;
15661                }
15662                Ok(StrykeValue::UNDEF)
15663            }
15664            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
15665            // write the substitution result back to the assignment target.
15666            ExprKind::Assign { target, .. } => self.assign_value(target, val),
15667            _ => Ok(StrykeValue::UNDEF),
15668        }
15669    }
15670
15671    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
15672    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
15673        (name.starts_with('#') && name.len() > 1)
15674            || name.starts_with('^')
15675            || matches!(
15676                name,
15677                "$$" | "0"
15678                    | "!"
15679                    | "@"
15680                    | "/"
15681                    | "\\"
15682                    | ","
15683                    | "."
15684                    | "]"
15685                    | ";"
15686                    | "ARGV"
15687                    | "^I"
15688                    | "^D"
15689                    | "^P"
15690                    | "^S"
15691                    | "^W"
15692                    | "^O"
15693                    | "^T"
15694                    | "^V"
15695                    | "^E"
15696                    | "^H"
15697                    | "^WARNING_BITS"
15698                    | "^GLOBAL_PHASE"
15699                    | "^MATCH"
15700                    | "^PREMATCH"
15701                    | "^POSTMATCH"
15702                    | "^LAST_SUBMATCH_RESULT"
15703                    | "<"
15704                    | ">"
15705                    | "("
15706                    | ")"
15707                    | "?"
15708                    | "|"
15709                    | "\""
15710                    | "+"
15711                    | "%"
15712                    | "="
15713                    | "-"
15714                    | ":"
15715                    | "*"
15716                    | "INC"
15717            )
15718            || crate::english::is_known_alias(name)
15719    }
15720
15721    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
15722    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
15723    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
15724    /// [`Self::english_no_match_vars`] is set.
15725    #[inline]
15726    /// English alias resolution + `our`/`oursync` package qualification in one call.
15727    /// Returns the storage key the scope expects: `$ARG` → `_`, then `our $x` → `Pkg::x`.
15728    /// Use this for compound ops (`++`, `--`, `+=`, `||=`, etc.) so atomic-RMW lookups
15729    /// hit the package-qualified cell stored by `oursync`.
15730    pub(crate) fn resolved_scalar_storage_name(&self, name: &str) -> String {
15731        self.tree_scalar_storage_name(self.english_scalar_name(name))
15732    }
15733
15734    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
15735        if !self.english_enabled {
15736            return name;
15737        }
15738        if self
15739            .english_lexical_scalars
15740            .iter()
15741            .any(|s| s.contains(name))
15742        {
15743            return name;
15744        }
15745        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
15746            return short;
15747        }
15748        name
15749    }
15750
15751    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
15752    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
15753        // `$#name = N` resizes `@name` (Perl: setting the last index). The
15754        // bare-set path stores under literal `#name` as a separate scalar
15755        // and silently does nothing useful — match the read-side handling
15756        // by routing through `set_special_var`.
15757        (name.starts_with('#') && name.len() > 1)
15758            || name.starts_with('^')
15759            || matches!(
15760                name,
15761                "0" | "/"
15762                    | "\\"
15763                    | ","
15764                    | ";"
15765                    | "\""
15766                    | "%"
15767                    | "="
15768                    | "-"
15769                    | ":"
15770                    | "*"
15771                    | "INC"
15772                    | "^I"
15773                    | "^D"
15774                    | "^P"
15775                    | "^W"
15776                    | "^H"
15777                    | "^WARNING_BITS"
15778                    | "$$"
15779                    | "]"
15780                    | "^S"
15781                    | "ARGV"
15782                    | "|"
15783                    | "+"
15784                    | "?"
15785                    | "!"
15786                    | "@"
15787                    | "."
15788            )
15789            || crate::english::is_known_alias(name)
15790    }
15791
15792    pub(crate) fn get_special_var(&self, name: &str) -> StrykeValue {
15793        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
15794        let name = if !crate::compat_mode() {
15795            match name {
15796                "NR" => ".",
15797                "RS" => "/",
15798                "OFS" => ",",
15799                "ORS" => "\\",
15800                "NF" => {
15801                    let len = self.scope.array_len("F");
15802                    return StrykeValue::integer(len as i64);
15803                }
15804                _ => self.english_scalar_name(name),
15805            }
15806        } else {
15807            self.english_scalar_name(name)
15808        };
15809        match name {
15810            "$$" => StrykeValue::integer(std::process::id() as i64),
15811            "_" => self.scope.get_scalar("_"),
15812            "^MATCH" => StrykeValue::string(self.last_match.clone()),
15813            "^PREMATCH" => StrykeValue::string(self.prematch.clone()),
15814            "^POSTMATCH" => StrykeValue::string(self.postmatch.clone()),
15815            "^LAST_SUBMATCH_RESULT" => StrykeValue::string(self.last_paren_match.clone()),
15816            "0" => StrykeValue::string(self.program_name.clone()),
15817            "!" => StrykeValue::errno_dual(self.errno_code, self.errno.clone()),
15818            "@" => {
15819                if let Some(ref v) = self.eval_error_value {
15820                    v.clone()
15821                } else {
15822                    StrykeValue::errno_dual(self.eval_error_code, self.eval_error.clone())
15823                }
15824            }
15825            "/" => match &self.irs {
15826                Some(s) => StrykeValue::string(s.clone()),
15827                None => StrykeValue::UNDEF,
15828            },
15829            "\\" => StrykeValue::string(self.ors.clone()),
15830            "," => StrykeValue::string(self.ofs.clone()),
15831            "." => {
15832                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
15833                if self.last_readline_handle.is_empty() {
15834                    if self.line_number == 0 {
15835                        StrykeValue::UNDEF
15836                    } else {
15837                        StrykeValue::integer(self.line_number)
15838                    }
15839                } else {
15840                    StrykeValue::integer(
15841                        *self
15842                            .handle_line_numbers
15843                            .get(&self.last_readline_handle)
15844                            .unwrap_or(&0),
15845                    )
15846                }
15847            }
15848            "]" => StrykeValue::float(perl_bracket_version()),
15849            ";" => StrykeValue::string(self.subscript_sep.clone()),
15850            "ARGV" => StrykeValue::string(self.argv_current_file.clone()),
15851            "^I" => StrykeValue::string(self.inplace_edit.clone()),
15852            "^D" => StrykeValue::integer(self.debug_flags),
15853            "^P" => StrykeValue::integer(self.perl_debug_flags),
15854            "^S" => StrykeValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
15855            "^W" => StrykeValue::integer(if self.warnings { 1 } else { 0 }),
15856            "^O" => StrykeValue::string(perl_osname()),
15857            "^T" => StrykeValue::integer(self.script_start_time),
15858            "^V" => StrykeValue::string(perl_version_v_string()),
15859            "^E" => StrykeValue::string(extended_os_error_string()),
15860            "^H" => StrykeValue::integer(self.compile_hints),
15861            "^WARNING_BITS" => StrykeValue::integer(self.warning_bits),
15862            "^GLOBAL_PHASE" => StrykeValue::string(self.global_phase.clone()),
15863            "<" | ">" => StrykeValue::integer(unix_id_for_special(name)),
15864            "(" | ")" => StrykeValue::string(unix_group_list_for_special(name)),
15865            "?" => StrykeValue::integer(self.child_exit_status),
15866            "|" => StrykeValue::integer(if self.output_autoflush { 1 } else { 0 }),
15867            "\"" => StrykeValue::string(self.list_separator.clone()),
15868            "+" => StrykeValue::string(self.last_paren_match.clone()),
15869            "%" => StrykeValue::integer(self.format_page_number),
15870            "=" => StrykeValue::integer(self.format_lines_per_page),
15871            "-" => StrykeValue::integer(self.format_lines_left),
15872            ":" => StrykeValue::string(self.format_line_break_chars.clone()),
15873            "*" => StrykeValue::integer(if self.multiline_match { 1 } else { 0 }),
15874            "^" => StrykeValue::string(self.format_top_name.clone()),
15875            "INC" => StrykeValue::integer(self.inc_hook_index),
15876            "^A" => StrykeValue::string(self.accumulator_format.clone()),
15877            "^C" => StrykeValue::integer(if self.sigint_pending_caret.replace(false) {
15878                1
15879            } else {
15880                0
15881            }),
15882            "^F" => StrykeValue::integer(self.max_system_fd),
15883            "^L" => StrykeValue::string(self.formfeed_string.clone()),
15884            "^M" => StrykeValue::string(self.emergency_memory.clone()),
15885            "^N" => StrykeValue::string(self.last_subpattern_name.clone()),
15886            "^X" => StrykeValue::string(self.executable_path.clone()),
15887            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
15888            "^TAINT" | "^TAINTED" => StrykeValue::integer(0),
15889            "^UNICODE" => StrykeValue::integer(if self.utf8_pragma { 1 } else { 0 }),
15890            "^OPEN" => StrykeValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
15891            "^UTF8LOCALE" => StrykeValue::integer(0),
15892            "^UTF8CACHE" => StrykeValue::integer(-1),
15893            _ if name.starts_with('^') && name.len() > 1 => self
15894                .special_caret_scalars
15895                .get(name)
15896                .cloned()
15897                .unwrap_or(StrykeValue::UNDEF),
15898            _ if name.starts_with('#') && name.len() > 1 => {
15899                let arr = &name[1..];
15900                let aname = self.stash_array_name_for_package(arr);
15901                let len = self.scope.array_len(&aname);
15902                StrykeValue::integer(len as i64 - 1)
15903            }
15904            _ => self.scope.get_scalar(name),
15905        }
15906    }
15907
15908    pub(crate) fn set_special_var(
15909        &mut self,
15910        name: &str,
15911        val: &StrykeValue,
15912    ) -> Result<(), StrykeError> {
15913        let name = self.english_scalar_name(name);
15914        match name {
15915            "!" => {
15916                let code = val.to_int() as i32;
15917                self.errno_code = code;
15918                self.errno = if code == 0 {
15919                    String::new()
15920                } else {
15921                    std::io::Error::from_raw_os_error(code).to_string()
15922                };
15923            }
15924            "@" => {
15925                if let Some((code, msg)) = val.errno_dual_parts() {
15926                    self.eval_error_code = code;
15927                    self.eval_error = msg;
15928                } else {
15929                    self.eval_error = val.to_string();
15930                    let mut code = val.to_int() as i32;
15931                    if code == 0 && !self.eval_error.is_empty() {
15932                        code = 1;
15933                    }
15934                    self.eval_error_code = code;
15935                }
15936            }
15937            "." => {
15938                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
15939                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
15940                let n = val.to_int();
15941                if self.last_readline_handle.is_empty() {
15942                    self.line_number = n;
15943                } else {
15944                    self.handle_line_numbers
15945                        .insert(self.last_readline_handle.clone(), n);
15946                }
15947            }
15948            "0" => self.program_name = val.to_string(),
15949            "/" => {
15950                self.irs = if val.is_undef() {
15951                    None
15952                } else {
15953                    Some(val.to_string())
15954                }
15955            }
15956            "\\" => self.ors = val.to_string(),
15957            "," => self.ofs = val.to_string(),
15958            ";" => self.subscript_sep = val.to_string(),
15959            "\"" => self.list_separator = val.to_string(),
15960            "%" => self.format_page_number = val.to_int(),
15961            "=" => self.format_lines_per_page = val.to_int(),
15962            "-" => self.format_lines_left = val.to_int(),
15963            ":" => self.format_line_break_chars = val.to_string(),
15964            "*" => self.multiline_match = val.to_int() != 0,
15965            "^" => self.format_top_name = val.to_string(),
15966            "INC" => self.inc_hook_index = val.to_int(),
15967            "^A" => self.accumulator_format = val.to_string(),
15968            "^F" => self.max_system_fd = val.to_int(),
15969            "^L" => self.formfeed_string = val.to_string(),
15970            "^M" => self.emergency_memory = val.to_string(),
15971            "^I" => self.inplace_edit = val.to_string(),
15972            "^D" => self.debug_flags = val.to_int(),
15973            "^P" => self.perl_debug_flags = val.to_int(),
15974            "^W" => self.warnings = val.to_int() != 0,
15975            "^H" => self.compile_hints = val.to_int(),
15976            "^WARNING_BITS" => self.warning_bits = val.to_int(),
15977            "|" => {
15978                self.output_autoflush = val.to_int() != 0;
15979                if self.output_autoflush {
15980                    let _ = io::stdout().flush();
15981                }
15982            }
15983            // Read-only or pid-backed
15984            "$$"
15985            | "]"
15986            | "^S"
15987            | "ARGV"
15988            | "?"
15989            | "^O"
15990            | "^T"
15991            | "^V"
15992            | "^E"
15993            | "^GLOBAL_PHASE"
15994            | "^MATCH"
15995            | "^PREMATCH"
15996            | "^POSTMATCH"
15997            | "^LAST_SUBMATCH_RESULT"
15998            | "^C"
15999            | "^N"
16000            | "^X"
16001            | "^TAINT"
16002            | "^TAINTED"
16003            | "^UNICODE"
16004            | "^UTF8LOCALE"
16005            | "^UTF8CACHE"
16006            | "+"
16007            | "<"
16008            | ">"
16009            | "("
16010            | ")" => {}
16011            _ if name.starts_with('^') && name.len() > 1 => {
16012                self.special_caret_scalars
16013                    .insert(name.to_string(), val.clone());
16014            }
16015            _ if name.starts_with('#') && name.len() > 1 => {
16016                // `$#name = N` resizes `@name` to length `N + 1`. Truncates
16017                // when N < current_last_idx, extends with `undef` otherwise.
16018                let arr = &name[1..];
16019                let aname = self.stash_array_name_for_package(arr);
16020                let new_last = val.to_int();
16021                let new_len = if new_last < 0 {
16022                    0
16023                } else {
16024                    (new_last as usize) + 1
16025                };
16026                let mut current = self.scope.get_array(&aname);
16027                current.resize(new_len, StrykeValue::UNDEF);
16028                self.scope.set_array(&aname, current)?;
16029            }
16030            _ => self.scope.set_scalar(name, val.clone())?,
16031        }
16032        Ok(())
16033    }
16034
16035    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
16036        match &expr.kind {
16037            // Route through `tree_array_storage_name` so bare `@servers` inside
16038            // `package Config` resolves to the same `Config::servers` storage
16039            // that the read path (and external `@Config::servers` mutations) see.
16040            ExprKind::ArrayVar(name) => Ok(self.tree_array_storage_name(name)),
16041            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
16042            _ => Err(StrykeError::runtime("Expected array", expr.line).into()),
16043        }
16044    }
16045
16046    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
16047    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
16048        match &expr.kind {
16049            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
16050            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
16051            _ => expr,
16052        }
16053    }
16054
16055    /// `@$aref` / `@{...}` after optional peeling — for `SpliceExpr` / `pop` operations.
16056    fn try_eval_array_deref_container(
16057        &mut self,
16058        expr: &Expr,
16059    ) -> Result<Option<StrykeValue>, FlowOrError> {
16060        let e = Self::peel_array_builtin_operand(expr);
16061        if let ExprKind::Deref {
16062            expr: inner,
16063            kind: Sigil::Array,
16064        } = &e.kind
16065        {
16066            return Ok(Some(self.eval_or_autoviv_array_ref(inner)?));
16067        }
16068        Ok(None)
16069    }
16070
16071    /// Evaluate `inner` and return an array ref, auto-vivifying when the result is undef
16072    /// and `inner` denotes a writable lvalue (scalar var, hash element, array element).
16073    /// Mirrors Perl 5: `push @{$h{k}}, $x` creates `$h{k}` as an arrayref on demand.
16074    fn eval_or_autoviv_array_ref(&mut self, inner: &Expr) -> Result<StrykeValue, FlowOrError> {
16075        let line = inner.line;
16076        let val = self.eval_expr(inner)?;
16077        if !val.is_undef() {
16078            return Ok(val);
16079        }
16080        let new_ref = StrykeValue::array_ref(Arc::new(RwLock::new(Vec::new())));
16081        match &inner.kind {
16082            ExprKind::ScalarVar(name) => {
16083                self.scope
16084                    .set_scalar(name, new_ref.clone())
16085                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
16086                Ok(new_ref)
16087            }
16088            ExprKind::HashElement { hash, key } => {
16089                let k = self.eval_expr(key)?.to_string();
16090                self.scope
16091                    .set_hash_element(hash, &k, new_ref.clone())
16092                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
16093                Ok(new_ref)
16094            }
16095            ExprKind::ArrayElement { array, index } => {
16096                let i = self.eval_expr(index)?.to_int();
16097                self.scope
16098                    .set_array_element(array, i, new_ref.clone())
16099                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
16100                Ok(new_ref)
16101            }
16102            _ => Ok(val),
16103        }
16104    }
16105
16106    /// Current package (`main` when `__PACKAGE__` is unset or empty).
16107    pub(crate) fn current_package(&self) -> String {
16108        let s = self.scope.get_scalar("__PACKAGE__").to_string();
16109        if s.is_empty() {
16110            "main".to_string()
16111        } else {
16112            s
16113        }
16114    }
16115
16116    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
16117    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
16118    pub(crate) fn package_version_scalar(
16119        &mut self,
16120        package: &str,
16121    ) -> StrykeResult<Option<StrykeValue>> {
16122        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
16123        let _ = self
16124            .scope
16125            .set_scalar("__PACKAGE__", StrykeValue::string(package.to_string()));
16126        let ver = self.get_special_var("VERSION");
16127        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
16128        Ok(if ver.is_undef() { None } else { Some(ver) })
16129    }
16130
16131    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
16132    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<StrykeSub>> {
16133        let root = if start_package.is_empty() {
16134            "main"
16135        } else {
16136            start_package
16137        };
16138        for pkg in self.mro_linearize(root) {
16139            let key = if pkg == "main" {
16140                "AUTOLOAD".to_string()
16141            } else {
16142                format!("{}::AUTOLOAD", pkg)
16143            };
16144            if let Some(s) = self.subs.get(&key) {
16145                return Some(s.clone());
16146            }
16147        }
16148        None
16149    }
16150
16151    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
16152    /// qualified missing sub or method name and invoke the handler (same argument list as the
16153    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
16154    /// the package prefix of the missing name (or current package).
16155    pub(crate) fn try_autoload_call(
16156        &mut self,
16157        missing_name: &str,
16158        args: Vec<StrykeValue>,
16159        line: usize,
16160        want: WantarrayCtx,
16161        method_invocant_class: Option<&str>,
16162    ) -> Option<ExecResult> {
16163        let pkg = self.current_package();
16164        let full = if missing_name.contains("::") {
16165            missing_name.to_string()
16166        } else {
16167            format!("{}::{}", pkg, missing_name)
16168        };
16169        let start_pkg = method_invocant_class.unwrap_or_else(|| {
16170            full.rsplit_once("::")
16171                .map(|(p, _)| p)
16172                .filter(|p| !p.is_empty())
16173                .unwrap_or("main")
16174        });
16175        let sub = self.resolve_autoload_sub(start_pkg)?;
16176        if let Err(e) = self
16177            .scope
16178            .set_scalar("AUTOLOAD", StrykeValue::string(full.clone()))
16179        {
16180            return Some(Err(e.into()));
16181        }
16182        Some(self.call_sub(&sub, args, want, line))
16183    }
16184
16185    pub(crate) fn with_topic_default_args(&self, args: Vec<StrykeValue>) -> Vec<StrykeValue> {
16186        if args.is_empty() {
16187            vec![self.scope.get_scalar("_").clone()]
16188        } else {
16189            args
16190        }
16191    }
16192
16193    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
16194    /// and [`crate::bytecode::Op::IndirectCall`].
16195    pub(crate) fn dispatch_indirect_call(
16196        &mut self,
16197        target: StrykeValue,
16198        arg_vals: Vec<StrykeValue>,
16199        want: WantarrayCtx,
16200        line: usize,
16201    ) -> ExecResult {
16202        if let Some(sub) = target.as_code_ref() {
16203            return self.call_sub(&sub, arg_vals, want, line);
16204        }
16205        if let Some(name) = target.as_str() {
16206            return self.call_named_sub(&name, arg_vals, line, want);
16207        }
16208        Err(StrykeError::runtime("Can't use non-code reference as a subroutine", line).into())
16209    }
16210
16211    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
16212    /// Bare-name dispatch for stryke list builtins (`sum`, `min`, `uniq`, `reduce`, `zip`, …).
16213    /// Resolves short aliases (`uq`, `shuf`, `chk`, `win`, `fst`, `rd`, `med`, `std`, `var`, …)
16214    /// and forwards to [`crate::list_builtins::dispatch_by_name`].
16215    pub(crate) fn call_bare_list_builtin(
16216        &mut self,
16217        name: &str,
16218        args: Vec<StrykeValue>,
16219        line: usize,
16220        want: WantarrayCtx,
16221    ) -> ExecResult {
16222        let canonical = match name {
16223            "distinct" | "uq" => "uniq",
16224            "shuf" => "shuffle",
16225            "chk" => "chunked",
16226            "win" => "windowed",
16227            "zp" => "zip",
16228            "fst" => "first",
16229            "rd" => "reduce",
16230            "med" => "median",
16231            "std" => "stddev",
16232            "var" => "variance",
16233            other => other,
16234        };
16235        // List builtins like `sum`, `min`, `uniq` operate on a list — an empty
16236        // input must aggregate to the identity (0/undef), NOT default to $_.
16237        // `sum(@empty_after_grep)` was returning $_ before this; that produced
16238        // surprising results downstream (e.g. `… |> grep {0} |> sum` = topic).
16239        match crate::list_builtins::dispatch_by_name(self, canonical, &args, want) {
16240            Some(r) => r,
16241            None => Err(StrykeError::runtime(
16242                format!("internal: not a stryke list builtin: {name}"),
16243                line,
16244            )
16245            .into()),
16246        }
16247    }
16248
16249    fn call_named_sub(
16250        &mut self,
16251        name: &str,
16252        args: Vec<StrykeValue>,
16253        line: usize,
16254        want: WantarrayCtx,
16255    ) -> ExecResult {
16256        if let Some(sub) = self.resolve_sub_by_name(name) {
16257            let args = self.with_topic_default_args(args);
16258            // The sub's home package is the qualifier from the resolved registry key.
16259            // `StrykeSub.name` itself may be bare; pass an explicit override so call_sub can
16260            // switch `__PACKAGE__` for cross-package `our`/`oursync` qualification.
16261            let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
16262            return self.call_sub_with_package(&sub, args, want, line, pkg);
16263        }
16264        match name {
16265            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
16266            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
16267            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
16268            | "none" | "notall" | "first" | "fst" | "find_index" | "firstidx" | "first_index"
16269            | "reduce" | "rd" | "reductions" | "sum" | "sum0" | "product" | "min" | "max"
16270            | "minstr" | "maxstr" | "mean" | "median" | "med" | "mode" | "stddev" | "std"
16271            | "variance" | "var" | "pairs" | "unpairs" | "pairkeys" | "pairvalues" | "pairgrep"
16272            | "pairmap" | "pairfirst" | "blessed" | "refaddr" | "reftype" | "looks_like_number"
16273            | "weaken" | "unweaken" | "isweak" | "set_subname" | "subname"
16274            | "unicode_to_native" => self.call_bare_list_builtin(name, args, line, want),
16275            "deque" => {
16276                if !args.is_empty() {
16277                    return Err(StrykeError::runtime("deque() takes no arguments", line).into());
16278                }
16279                Ok(StrykeValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
16280            }
16281            "defer__internal" => {
16282                if args.len() != 1 {
16283                    return Err(StrykeError::runtime(
16284                        "defer__internal expects one coderef argument",
16285                        line,
16286                    )
16287                    .into());
16288                }
16289                self.scope.push_defer(args[0].clone());
16290                Ok(StrykeValue::UNDEF)
16291            }
16292            "heap" => {
16293                if args.len() != 1 {
16294                    return Err(
16295                        StrykeError::runtime("heap() expects one comparator sub", line).into(),
16296                    );
16297                }
16298                if let Some(sub) = args[0].as_code_ref() {
16299                    Ok(StrykeValue::heap(Arc::new(Mutex::new(PerlHeap {
16300                        items: Vec::new(),
16301                        cmp: Arc::clone(&sub),
16302                    }))))
16303                } else {
16304                    Err(StrykeError::runtime("heap() requires a code reference", line).into())
16305                }
16306            }
16307            "pipeline" => {
16308                let mut items = Vec::new();
16309                for v in args {
16310                    if let Some(a) = v.as_array_vec() {
16311                        items.extend(a);
16312                    } else {
16313                        items.push(v);
16314                    }
16315                }
16316                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
16317                    source: items,
16318                    ops: Vec::new(),
16319                    has_scalar_terminal: false,
16320                    par_stream: false,
16321                    streaming: false,
16322                    streaming_workers: 0,
16323                    streaming_buffer: 256,
16324                }))))
16325            }
16326            "par_pipeline" => {
16327                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
16328                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
16329                        .map_err(Into::into);
16330                }
16331                Ok(self.builtin_par_pipeline_stream(&args, line)?)
16332            }
16333            "par_pipeline_stream" => {
16334                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
16335                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
16336                        .map_err(Into::into);
16337                }
16338                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
16339            }
16340            "ppool" => {
16341                if args.len() != 1 {
16342                    return Err(StrykeError::runtime(
16343                        "ppool() expects one argument (worker count)",
16344                        line,
16345                    )
16346                    .into());
16347                }
16348                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
16349            }
16350            "barrier" => {
16351                if args.len() != 1 {
16352                    return Err(StrykeError::runtime(
16353                        "barrier() expects one argument (party count)",
16354                        line,
16355                    )
16356                    .into());
16357                }
16358                let n = args[0].to_int().max(1) as usize;
16359                Ok(StrykeValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
16360            }
16361            "cluster" => {
16362                let items = if args.len() == 1 {
16363                    args[0].to_list()
16364                } else {
16365                    args.to_vec()
16366                };
16367                let c = RemoteCluster::from_list_args(&items)
16368                    .map_err(|msg| StrykeError::runtime(msg, line))?;
16369                Ok(StrykeValue::remote_cluster(Arc::new(c)))
16370            }
16371            _ => {
16372                // Late static binding: static::method() resolves to runtime class of $self
16373                if let Some(method_name) = name.strip_prefix("static::") {
16374                    let self_val = self.scope.get_scalar("self");
16375                    if let Some(c) = self_val.as_class_inst() {
16376                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
16377                            if let Some(ref body) = m.body {
16378                                let params = m.params.clone();
16379                                let mut call_args = vec![self_val.clone()];
16380                                call_args.extend(args);
16381                                return match self.call_class_method(body, &params, call_args, line)
16382                                {
16383                                    Ok(v) => Ok(v),
16384                                    Err(FlowOrError::Error(e)) => Err(e.into()),
16385                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16386                                    Err(e) => Err(e),
16387                                };
16388                            }
16389                        }
16390                        return Err(StrykeError::runtime(
16391                            format!(
16392                                "static::{} — method not found on class {}",
16393                                method_name, c.def.name
16394                            ),
16395                            line,
16396                        )
16397                        .into());
16398                    }
16399                    return Err(StrykeError::runtime(
16400                        "static:: can only be used inside a class method",
16401                        line,
16402                    )
16403                    .into());
16404                }
16405                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
16406                if let Some(def) = self.struct_defs.get(name).cloned() {
16407                    return self.struct_construct(&def, args, line);
16408                }
16409                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
16410                if let Some(def) = self.class_defs.get(name).cloned() {
16411                    return self.class_construct(&def, args, line);
16412                }
16413                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
16414                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
16415                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
16416                        return self.enum_construct(&def, variant_name, args, line);
16417                    }
16418                }
16419                // Check for static class method or static field: Math::add(...) / Counter::count()
16420                if let Some((class_name, member_name)) = name.rsplit_once("::") {
16421                    if let Some(def) = self.class_defs.get(class_name).cloned() {
16422                        // Static method
16423                        if let Some(m) = def.method(member_name) {
16424                            if m.is_static {
16425                                if let Some(ref body) = m.body {
16426                                    let params = m.params.clone();
16427                                    return match self.call_static_class_method(
16428                                        body,
16429                                        &params,
16430                                        args.clone(),
16431                                        line,
16432                                    ) {
16433                                        Ok(v) => Ok(v),
16434                                        Err(FlowOrError::Error(e)) => Err(e.into()),
16435                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16436                                        Err(e) => Err(e),
16437                                    };
16438                                }
16439                            }
16440                        }
16441                        // Static field access: getter (0 args) or setter (1 arg)
16442                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
16443                            let key = format!("{}::{}", class_name, member_name);
16444                            match args.len() {
16445                                0 => {
16446                                    let val = self.scope.get_scalar(&key);
16447                                    return Ok(val);
16448                                }
16449                                1 => {
16450                                    let _ = self.scope.set_scalar(&key, args[0].clone());
16451                                    return Ok(args[0].clone());
16452                                }
16453                                _ => {
16454                                    return Err(StrykeError::runtime(
16455                                        format!(
16456                                            "static field `{}::{}` takes 0 or 1 arguments",
16457                                            class_name, member_name
16458                                        ),
16459                                        line,
16460                                    )
16461                                    .into());
16462                                }
16463                            }
16464                        }
16465                    }
16466                }
16467                let args = self.with_topic_default_args(args);
16468                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
16469                    return r;
16470                }
16471                Err(StrykeError::runtime(self.undefined_subroutine_call_message(name), line).into())
16472            }
16473        }
16474    }
16475
16476    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
16477    pub(crate) fn struct_construct(
16478        &mut self,
16479        def: &Arc<StructDef>,
16480        args: Vec<StrykeValue>,
16481        line: usize,
16482    ) -> ExecResult {
16483        // Detect if args are named (key => value pairs) or positional
16484        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
16485        let is_named = args.len() >= 2
16486            && args.len().is_multiple_of(2)
16487            && args.iter().step_by(2).all(|v| {
16488                let s = v.to_string();
16489                def.field_index(&s).is_some()
16490            });
16491
16492        let provided = if is_named {
16493            // Named construction: Point(x => 1, y => 2)
16494            let mut pairs = Vec::new();
16495            let mut i = 0;
16496            while i + 1 < args.len() {
16497                let k = args[i].to_string();
16498                let v = args[i + 1].clone();
16499                pairs.push((k, v));
16500                i += 2;
16501            }
16502            pairs
16503        } else {
16504            // Positional construction: Point(1, 2) fills fields in declaration order
16505            def.fields
16506                .iter()
16507                .zip(args.iter())
16508                .map(|(f, v)| (f.name.clone(), v.clone()))
16509                .collect()
16510        };
16511
16512        // Evaluate default expressions
16513        let mut defaults = Vec::with_capacity(def.fields.len());
16514        for field in &def.fields {
16515            if let Some(ref expr) = field.default {
16516                let val = self.eval_expr(expr)?;
16517                defaults.push(Some(val));
16518            } else {
16519                defaults.push(None);
16520            }
16521        }
16522
16523        Ok(crate::native_data::struct_new_with_defaults(
16524            def, &provided, &defaults, line,
16525        )?)
16526    }
16527
16528    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
16529    pub(crate) fn class_construct(
16530        &mut self,
16531        def: &Arc<ClassDef>,
16532        args: Vec<StrykeValue>,
16533        _line: usize,
16534    ) -> ExecResult {
16535        use crate::value::ClassInstance;
16536
16537        // Prevent instantiation of abstract classes
16538        if def.is_abstract {
16539            return Err(StrykeError::runtime(
16540                format!("cannot instantiate abstract class `{}`", def.name),
16541                _line,
16542            )
16543            .into());
16544        }
16545
16546        // Collect all fields from inheritance chain (parent fields first)
16547        let all_fields = self.collect_class_fields(def);
16548
16549        // Check if args are named
16550        let is_named = args.len() >= 2
16551            && args.len().is_multiple_of(2)
16552            && args.iter().step_by(2).all(|v| {
16553                let s = v.to_string();
16554                all_fields.iter().any(|(name, _, _)| name == &s)
16555            });
16556
16557        let provided: Vec<(String, StrykeValue)> = if is_named {
16558            let mut pairs = Vec::new();
16559            let mut i = 0;
16560            while i + 1 < args.len() {
16561                let k = args[i].to_string();
16562                let v = args[i + 1].clone();
16563                pairs.push((k, v));
16564                i += 2;
16565            }
16566            pairs
16567        } else {
16568            all_fields
16569                .iter()
16570                .zip(args.iter())
16571                .map(|((name, _, _), v)| (name.clone(), v.clone()))
16572                .collect()
16573        };
16574
16575        // Build values array for all fields (inherited + own) with type checking
16576        let mut values = Vec::with_capacity(all_fields.len());
16577        for (name, default, ty) in &all_fields {
16578            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
16579                val.clone()
16580            } else if let Some(ref expr) = default {
16581                self.eval_expr(expr)?
16582            } else {
16583                StrykeValue::UNDEF
16584            };
16585            ty.check_value(&val).map_err(|msg| {
16586                StrykeError::type_error(
16587                    format!("class {} field `{}`: {}", def.name, name, msg),
16588                    _line,
16589                )
16590            })?;
16591            values.push(val);
16592        }
16593
16594        // Compute full ISA chain for type checking
16595        let isa_chain = self.mro_linearize(&def.name);
16596        let instance = StrykeValue::class_inst(Arc::new(ClassInstance::new_with_isa(
16597            Arc::clone(def),
16598            values,
16599            isa_chain,
16600        )));
16601
16602        // Call BUILD hooks: parent BUILD first, then child BUILD
16603        let build_chain = self.collect_build_chain(def);
16604        if !build_chain.is_empty() {
16605            for (body, params) in &build_chain {
16606                let call_args = vec![instance.clone()];
16607                match self.call_class_method(body, params, call_args, _line) {
16608                    Ok(_) => {}
16609                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
16610                    Err(e) => return Err(e),
16611                }
16612            }
16613        }
16614
16615        Ok(instance)
16616    }
16617
16618    /// Collect BUILD methods from parent to child order.
16619    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
16620        let mut chain = Vec::new();
16621        // Parent BUILD first
16622        for parent_name in &def.extends {
16623            if let Some(parent_def) = self.class_defs.get(parent_name) {
16624                chain.extend(self.collect_build_chain(parent_def));
16625            }
16626        }
16627        // Own BUILD
16628        if let Some(m) = def.method("BUILD") {
16629            if let Some(ref body) = m.body {
16630                chain.push((body.clone(), m.params.clone()));
16631            }
16632        }
16633        chain
16634    }
16635
16636    /// Recursively flatten class/struct instances and the hashes/arrays
16637    /// they contain into a plain hashref tree. Atoms (numbers, strings,
16638    /// undef, code refs, regex refs, blessed-non-hash refs, …) round-trip
16639    /// unchanged. Used by `$obj->to_hash_rec` for both class and struct
16640    /// receivers.
16641    pub(crate) fn deep_to_hash_value(&self, v: &StrykeValue) -> StrykeValue {
16642        // Class instance: hashref of fields, recursing into each value.
16643        if let Some(c) = v.as_class_inst() {
16644            let all_fields = self.collect_class_fields_full(&c.def);
16645            let values = c.get_values();
16646            let mut map = IndexMap::new();
16647            for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
16648                if let Some(elem) = values.get(i) {
16649                    map.insert(name.clone(), self.deep_to_hash_value(elem));
16650                }
16651            }
16652            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16653        }
16654        // Struct instance: same shape, declaration order.
16655        if let Some(s) = v.as_struct_inst() {
16656            let values = s.get_values();
16657            let mut map = IndexMap::new();
16658            for (i, field) in s.def.fields.iter().enumerate() {
16659                if let Some(elem) = values.get(i) {
16660                    map.insert(field.name.clone(), self.deep_to_hash_value(elem));
16661                }
16662            }
16663            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16664        }
16665        // Hashref: clone keys, recurse into values.
16666        if let Some(r) = v.as_hash_ref() {
16667            let inner = r.read().clone();
16668            let mut map = IndexMap::new();
16669            for (k, val) in inner.into_iter() {
16670                map.insert(k, self.deep_to_hash_value(&val));
16671            }
16672            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16673        }
16674        // Arrayref: recurse into elements.
16675        if let Some(r) = v.as_array_ref() {
16676            let inner = r.read().clone();
16677            let out: Vec<StrykeValue> = inner.iter().map(|e| self.deep_to_hash_value(e)).collect();
16678            return StrykeValue::array_ref(Arc::new(RwLock::new(out)));
16679        }
16680        // Everything else (scalars, blessed refs, code refs, enums, …)
16681        // round-trips unchanged. Enum instances stringify naturally
16682        // through their existing `Display` so callers see a stable name.
16683        v.clone()
16684    }
16685
16686    /// Collect all fields from a class and its parent hierarchy (parent fields first).
16687    /// Returns (name, default, type, visibility, owning_class_name).
16688    fn collect_class_fields(
16689        &self,
16690        def: &ClassDef,
16691    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
16692        self.collect_class_fields_full(def)
16693            .into_iter()
16694            .map(|(name, default, ty, _, _)| (name, default, ty))
16695            .collect()
16696    }
16697
16698    /// Like collect_class_fields but includes visibility and owning class name.
16699    fn collect_class_fields_full(
16700        &self,
16701        def: &ClassDef,
16702    ) -> Vec<(
16703        String,
16704        Option<Expr>,
16705        crate::ast::PerlTypeName,
16706        crate::ast::Visibility,
16707        String,
16708    )> {
16709        let mut all_fields = Vec::new();
16710
16711        for parent_name in &def.extends {
16712            if let Some(parent_def) = self.class_defs.get(parent_name) {
16713                let parent_fields = self.collect_class_fields_full(parent_def);
16714                all_fields.extend(parent_fields);
16715            }
16716        }
16717
16718        for field in &def.fields {
16719            all_fields.push((
16720                field.name.clone(),
16721                field.default.clone(),
16722                field.ty.clone(),
16723                field.visibility,
16724                def.name.clone(),
16725            ));
16726        }
16727
16728        all_fields
16729    }
16730
16731    /// Collect all method names from class and parents (deduplicates, child overrides parent).
16732    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
16733        // Parent methods first
16734        for parent_name in &def.extends {
16735            if let Some(parent_def) = self.class_defs.get(parent_name) {
16736                self.collect_class_method_names(parent_def, names);
16737            }
16738        }
16739        // Own methods (add if not already present — child overrides parent name)
16740        for m in &def.methods {
16741            if !m.is_static && !names.contains(&m.name) {
16742                names.push(m.name.clone());
16743            }
16744        }
16745    }
16746
16747    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
16748    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
16749        let mut chain = Vec::new();
16750        // Own DESTROY first
16751        if let Some(m) = def.method("DESTROY") {
16752            if let Some(ref body) = m.body {
16753                chain.push((body.clone(), m.params.clone()));
16754            }
16755        }
16756        // Then parent DESTROY
16757        for parent_name in &def.extends {
16758            if let Some(parent_def) = self.class_defs.get(parent_name) {
16759                chain.extend(self.collect_destroy_chain(parent_def));
16760            }
16761        }
16762        chain
16763    }
16764
16765    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
16766    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
16767        if let Some(def) = self.class_defs.get(child) {
16768            for parent in &def.extends {
16769                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
16770                    return true;
16771                }
16772            }
16773        }
16774        false
16775    }
16776
16777    /// Find a method in a class or its parent hierarchy (child methods override parent).
16778    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
16779        // First check the current class
16780        if let Some(m) = def.method(method) {
16781            return Some((m.clone(), def.name.clone()));
16782        }
16783        // Then check parent classes
16784        for parent_name in &def.extends {
16785            if let Some(parent_def) = self.class_defs.get(parent_name) {
16786                if let Some(result) = self.find_class_method(parent_def, method) {
16787                    return Some(result);
16788                }
16789            }
16790        }
16791        None
16792    }
16793
16794    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
16795    pub(crate) fn enum_construct(
16796        &mut self,
16797        def: &Arc<EnumDef>,
16798        variant_name: &str,
16799        args: Vec<StrykeValue>,
16800        line: usize,
16801    ) -> ExecResult {
16802        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
16803            FlowOrError::Error(StrykeError::runtime(
16804                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
16805                line,
16806            ))
16807        })?;
16808        let variant = &def.variants[variant_idx];
16809        let data = if variant.ty.is_some() {
16810            if args.is_empty() {
16811                return Err(StrykeError::runtime(
16812                    format!(
16813                        "enum variant `{}::{}` requires data",
16814                        def.name, variant_name
16815                    ),
16816                    line,
16817                )
16818                .into());
16819            }
16820            if args.len() == 1 {
16821                args.into_iter().next().unwrap()
16822            } else {
16823                StrykeValue::array(args)
16824            }
16825        } else {
16826            if !args.is_empty() {
16827                return Err(StrykeError::runtime(
16828                    format!(
16829                        "enum variant `{}::{}` does not take data",
16830                        def.name, variant_name
16831                    ),
16832                    line,
16833                )
16834                .into());
16835            }
16836            StrykeValue::UNDEF
16837        };
16838        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
16839        Ok(StrykeValue::enum_inst(Arc::new(inst)))
16840    }
16841
16842    /// True if `name` is a registered or standard process-global handle.
16843    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
16844        matches!(name, "STDIN" | "STDOUT" | "STDERR")
16845            || self.input_handles.contains_key(name)
16846            || self.output_handles.contains_key(name)
16847            || self.io_file_slots.contains_key(name)
16848            || self.pipe_children.contains_key(name)
16849    }
16850
16851    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
16852    pub(crate) fn io_handle_method(
16853        &mut self,
16854        name: &str,
16855        method: &str,
16856        args: &[StrykeValue],
16857        line: usize,
16858    ) -> StrykeResult<StrykeValue> {
16859        match method {
16860            "print" => self.io_handle_print(name, args, false, line),
16861            "say" => self.io_handle_print(name, args, true, line),
16862            "printf" => self.io_handle_printf(name, args, line),
16863            "getline" | "readline" => {
16864                if !args.is_empty() {
16865                    return Err(StrykeError::runtime(
16866                        format!("{}: too many arguments", method),
16867                        line,
16868                    ));
16869                }
16870                self.readline_builtin_execute(Some(name))
16871            }
16872            "close" => {
16873                if !args.is_empty() {
16874                    return Err(StrykeError::runtime("close: too many arguments", line));
16875                }
16876                self.close_builtin_execute(name.to_string())
16877            }
16878            "eof" => {
16879                if !args.is_empty() {
16880                    return Err(StrykeError::runtime("eof: too many arguments", line));
16881                }
16882                let at_eof = !self.has_input_handle(name);
16883                Ok(StrykeValue::integer(if at_eof { 1 } else { 0 }))
16884            }
16885            "getc" => {
16886                if !args.is_empty() {
16887                    return Err(StrykeError::runtime("getc: too many arguments", line));
16888                }
16889                match crate::builtins::try_builtin(
16890                    self,
16891                    "getc",
16892                    &[StrykeValue::string(name.to_string())],
16893                    line,
16894                ) {
16895                    Some(r) => r,
16896                    None => Err(StrykeError::runtime("getc: not available", line)),
16897                }
16898            }
16899            "binmode" => match crate::builtins::try_builtin(
16900                self,
16901                "binmode",
16902                &[StrykeValue::string(name.to_string())],
16903                line,
16904            ) {
16905                Some(r) => r,
16906                None => Err(StrykeError::runtime("binmode: not available", line)),
16907            },
16908            "fileno" => match crate::builtins::try_builtin(
16909                self,
16910                "fileno",
16911                &[StrykeValue::string(name.to_string())],
16912                line,
16913            ) {
16914                Some(r) => r,
16915                None => Err(StrykeError::runtime("fileno: not available", line)),
16916            },
16917            "flush" => {
16918                if !args.is_empty() {
16919                    return Err(StrykeError::runtime("flush: too many arguments", line));
16920                }
16921                self.io_handle_flush(name, line)
16922            }
16923            _ => Err(StrykeError::runtime(
16924                format!("Unknown method for filehandle: {}", method),
16925                line,
16926            )),
16927        }
16928    }
16929
16930    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> StrykeResult<StrykeValue> {
16931        match handle_name {
16932            "STDOUT" => {
16933                let _ = IoWrite::flush(&mut io::stdout());
16934            }
16935            "STDERR" => {
16936                let _ = IoWrite::flush(&mut io::stderr());
16937            }
16938            name => {
16939                if let Some(writer) = self.output_handles.get_mut(name) {
16940                    let _ = IoWrite::flush(&mut *writer);
16941                } else {
16942                    return Err(StrykeError::runtime(
16943                        format!("flush on unopened filehandle {}", name),
16944                        line,
16945                    ));
16946                }
16947            }
16948        }
16949        Ok(StrykeValue::integer(1))
16950    }
16951
16952    fn io_handle_print(
16953        &mut self,
16954        handle_name: &str,
16955        args: &[StrykeValue],
16956        newline: bool,
16957        line: usize,
16958    ) -> StrykeResult<StrykeValue> {
16959        if newline && (self.feature_bits & FEAT_SAY) == 0 {
16960            return Err(StrykeError::runtime(
16961                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
16962                line,
16963            ));
16964        }
16965        let mut output = String::new();
16966        if args.is_empty() {
16967            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
16968            output.push_str(&self.scope.get_scalar("_").to_string());
16969        } else {
16970            for (i, val) in args.iter().enumerate() {
16971                if i > 0 && !self.ofs.is_empty() {
16972                    output.push_str(&self.ofs);
16973                }
16974                output.push_str(&val.to_string());
16975            }
16976        }
16977        if newline {
16978            output.push('\n');
16979        }
16980        output.push_str(&self.ors);
16981
16982        self.write_formatted_print(handle_name, &output, line)?;
16983        Ok(StrykeValue::integer(1))
16984    }
16985
16986    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
16987    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
16988    pub(crate) fn write_formatted_print(
16989        &mut self,
16990        handle_name: &str,
16991        output: &str,
16992        line: usize,
16993    ) -> StrykeResult<()> {
16994        match handle_name {
16995            "STDOUT" => {
16996                if !self.suppress_stdout {
16997                    print!("{}", output);
16998                    if self.output_autoflush {
16999                        let _ = io::stdout().flush();
17000                    }
17001                }
17002            }
17003            "STDERR" => {
17004                eprint!("{}", output);
17005                let _ = io::stderr().flush();
17006            }
17007            name => {
17008                if let Some(writer) = self.output_handles.get_mut(name) {
17009                    let _ = writer.write_all(output.as_bytes());
17010                    if self.output_autoflush {
17011                        let _ = writer.flush();
17012                    }
17013                } else {
17014                    return Err(StrykeError::runtime(
17015                        format!("print on unopened filehandle {}", name),
17016                        line,
17017                    ));
17018                }
17019            }
17020        }
17021        Ok(())
17022    }
17023
17024    fn io_handle_printf(
17025        &mut self,
17026        handle_name: &str,
17027        args: &[StrykeValue],
17028        line: usize,
17029    ) -> StrykeResult<StrykeValue> {
17030        let (fmt, rest): (String, &[StrykeValue]) = if args.is_empty() {
17031            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
17032                Ok(s) => s,
17033                Err(FlowOrError::Error(e)) => return Err(e),
17034                Err(FlowOrError::Flow(_)) => {
17035                    return Err(StrykeError::runtime(
17036                        "printf: unexpected control flow in sprintf",
17037                        line,
17038                    ));
17039                }
17040            };
17041            (s, &[])
17042        } else {
17043            (args[0].to_string(), &args[1..])
17044        };
17045        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
17046            Ok(s) => s,
17047            Err(FlowOrError::Error(e)) => return Err(e),
17048            Err(FlowOrError::Flow(_)) => {
17049                return Err(StrykeError::runtime(
17050                    "printf: unexpected control flow in sprintf",
17051                    line,
17052                ));
17053            }
17054        };
17055        match handle_name {
17056            "STDOUT" => {
17057                if !self.suppress_stdout {
17058                    print!("{}", output);
17059                    if self.output_autoflush {
17060                        let _ = IoWrite::flush(&mut io::stdout());
17061                    }
17062                }
17063            }
17064            "STDERR" => {
17065                eprint!("{}", output);
17066                let _ = IoWrite::flush(&mut io::stderr());
17067            }
17068            name => {
17069                if let Some(writer) = self.output_handles.get_mut(name) {
17070                    let _ = writer.write_all(output.as_bytes());
17071                    if self.output_autoflush {
17072                        let _ = writer.flush();
17073                    }
17074                } else {
17075                    return Err(StrykeError::runtime(
17076                        format!("printf on unopened filehandle {}", name),
17077                        line,
17078                    ));
17079                }
17080            }
17081        }
17082        Ok(StrykeValue::integer(1))
17083    }
17084
17085    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
17086    pub(crate) fn try_native_method(
17087        &mut self,
17088        receiver: &StrykeValue,
17089        method: &str,
17090        args: &[StrykeValue],
17091        line: usize,
17092    ) -> Option<StrykeResult<StrykeValue>> {
17093        if let Some(name) = receiver.as_io_handle_name() {
17094            return Some(self.io_handle_method(&name, method, args, line));
17095        }
17096        if let Some(ref s) = receiver.as_str() {
17097            if self.is_bound_handle(s) {
17098                return Some(self.io_handle_method(s, method, args, line));
17099            }
17100        }
17101        if let Some(c) = receiver.as_sqlite_conn() {
17102            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
17103        }
17104        if let Some(s) = receiver.as_struct_inst() {
17105            // Field access: $p->x or $p->x(value)
17106            if let Some(idx) = s.def.field_index(method) {
17107                match args.len() {
17108                    0 => {
17109                        return Some(Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF)));
17110                    }
17111                    1 => {
17112                        let field = &s.def.fields[idx];
17113                        let new_val = args[0].clone();
17114                        if let Err(msg) = field.ty.check_value(&new_val) {
17115                            return Some(Err(StrykeError::type_error(
17116                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
17117                                line,
17118                            )));
17119                        }
17120                        s.set_field(idx, new_val.clone());
17121                        return Some(Ok(new_val));
17122                    }
17123                    _ => {
17124                        return Some(Err(StrykeError::runtime(
17125                            format!(
17126                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
17127                                method,
17128                                args.len()
17129                            ),
17130                            line,
17131                        )));
17132                    }
17133                }
17134            }
17135            // Built-in struct methods
17136            match method {
17137                "with" => {
17138                    // Functional update: $p->with(x => 5) returns new instance with changed field
17139                    let mut new_values = s.get_values();
17140                    let mut i = 0;
17141                    while i + 1 < args.len() {
17142                        let k = args[i].to_string();
17143                        let v = args[i + 1].clone();
17144                        if let Some(idx) = s.def.field_index(&k) {
17145                            let field = &s.def.fields[idx];
17146                            if let Err(msg) = field.ty.check_value(&v) {
17147                                return Some(Err(StrykeError::type_error(
17148                                    format!(
17149                                        "struct {} field `{}`: {}",
17150                                        s.def.name, field.name, msg
17151                                    ),
17152                                    line,
17153                                )));
17154                            }
17155                            new_values[idx] = v;
17156                        } else {
17157                            return Some(Err(StrykeError::runtime(
17158                                format!("struct {}: unknown field `{}`", s.def.name, k),
17159                                line,
17160                            )));
17161                        }
17162                        i += 2;
17163                    }
17164                    return Some(Ok(StrykeValue::struct_inst(Arc::new(
17165                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
17166                    ))));
17167                }
17168                "to_hash" => {
17169                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
17170                    if !args.is_empty() {
17171                        return Some(Err(StrykeError::runtime(
17172                            "struct to_hash takes no arguments",
17173                            line,
17174                        )));
17175                    }
17176                    let mut map = IndexMap::new();
17177                    let values = s.get_values();
17178                    for (i, field) in s.def.fields.iter().enumerate() {
17179                        map.insert(field.name.clone(), values[i].clone());
17180                    }
17181                    return Some(Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map)))));
17182                }
17183                "to_hash_rec" | "to_hash_deep" => {
17184                    // Like to_hash but recurse: nested struct/class/hash/
17185                    // array values become plain hashref/arrayref trees.
17186                    if !args.is_empty() {
17187                        return Some(Err(StrykeError::runtime(
17188                            "struct to_hash_rec takes no arguments",
17189                            line,
17190                        )));
17191                    }
17192                    return Some(Ok(self.deep_to_hash_value(receiver)));
17193                }
17194                "fields" => {
17195                    // Field list: $p->fields returns field names
17196                    if !args.is_empty() {
17197                        return Some(Err(StrykeError::runtime(
17198                            "struct fields takes no arguments",
17199                            line,
17200                        )));
17201                    }
17202                    let names: Vec<StrykeValue> = s
17203                        .def
17204                        .fields
17205                        .iter()
17206                        .map(|f| StrykeValue::string(f.name.clone()))
17207                        .collect();
17208                    return Some(Ok(StrykeValue::array(names)));
17209                }
17210                "clone" => {
17211                    // Clone: $p->clone deep copies
17212                    if !args.is_empty() {
17213                        return Some(Err(StrykeError::runtime(
17214                            "struct clone takes no arguments",
17215                            line,
17216                        )));
17217                    }
17218                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
17219                    return Some(Ok(StrykeValue::struct_inst(Arc::new(
17220                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
17221                    ))));
17222                }
17223                _ => {}
17224            }
17225            // User-defined struct method
17226            if let Some(m) = s.def.method(method) {
17227                let body = m.body.clone();
17228                let params = m.params.clone();
17229                // Build args: $self is the receiver, then the passed args
17230                let mut call_args = vec![receiver.clone()];
17231                call_args.extend(args.iter().cloned());
17232                return Some(
17233                    match self.call_struct_method(&body, &params, call_args, line) {
17234                        Ok(v) => Ok(v),
17235                        Err(FlowOrError::Error(e)) => Err(e),
17236                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17237                        Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
17238                            "unexpected control flow in struct method",
17239                            line,
17240                        )),
17241                    },
17242                );
17243            }
17244            return None;
17245        }
17246        // Class instance method dispatch
17247        if let Some(c) = receiver.as_class_inst() {
17248            // Collect all fields from inheritance chain (with visibility)
17249            let all_fields_full = self.collect_class_fields_full(&c.def);
17250            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
17251                .iter()
17252                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
17253                .collect();
17254
17255            // Field access: $obj->name or $obj->name(value)
17256            if let Some(idx) = all_fields_full
17257                .iter()
17258                .position(|(name, _, _, _, _)| name == method)
17259            {
17260                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
17261
17262                // Enforce field visibility
17263                match vis {
17264                    crate::ast::Visibility::Private => {
17265                        // Only accessible from within the owning class's methods
17266                        let caller_class = self
17267                            .scope
17268                            .get_scalar("self")
17269                            .as_class_inst()
17270                            .map(|ci| ci.def.name.clone());
17271                        if caller_class.as_deref() != Some(owner_class.as_str()) {
17272                            return Some(Err(StrykeError::runtime(
17273                                format!("field `{}` of class {} is private", method, owner_class),
17274                                line,
17275                            )));
17276                        }
17277                    }
17278                    crate::ast::Visibility::Protected => {
17279                        // Accessible from owning class or subclasses
17280                        let caller_class = self
17281                            .scope
17282                            .get_scalar("self")
17283                            .as_class_inst()
17284                            .map(|ci| ci.def.name.clone());
17285                        let allowed = caller_class.as_deref().is_some_and(|caller| {
17286                            caller == owner_class || self.class_inherits_from(caller, owner_class)
17287                        });
17288                        if !allowed {
17289                            return Some(Err(StrykeError::runtime(
17290                                format!("field `{}` of class {} is protected", method, owner_class),
17291                                line,
17292                            )));
17293                        }
17294                    }
17295                    crate::ast::Visibility::Public => {}
17296                }
17297
17298                match args.len() {
17299                    0 => {
17300                        return Some(Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF)));
17301                    }
17302                    1 => {
17303                        let new_val = args[0].clone();
17304                        if let Err(msg) = ty.check_value(&new_val) {
17305                            return Some(Err(StrykeError::type_error(
17306                                format!("class {} field `{}`: {}", c.def.name, method, msg),
17307                                line,
17308                            )));
17309                        }
17310                        c.set_field(idx, new_val.clone());
17311                        return Some(Ok(new_val));
17312                    }
17313                    _ => {
17314                        return Some(Err(StrykeError::runtime(
17315                            format!(
17316                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
17317                                method,
17318                                args.len()
17319                            ),
17320                            line,
17321                        )));
17322                    }
17323                }
17324            }
17325            // Built-in class methods (use all_fields for inheritance)
17326            match method {
17327                "with" => {
17328                    let mut new_values = c.get_values();
17329                    let mut i = 0;
17330                    while i + 1 < args.len() {
17331                        let k = args[i].to_string();
17332                        let v = args[i + 1].clone();
17333                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
17334                            let (_, _, ref ty) = all_fields[idx];
17335                            if let Err(msg) = ty.check_value(&v) {
17336                                return Some(Err(StrykeError::type_error(
17337                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
17338                                    line,
17339                                )));
17340                            }
17341                            new_values[idx] = v;
17342                        } else {
17343                            return Some(Err(StrykeError::runtime(
17344                                format!("class {}: unknown field `{}`", c.def.name, k),
17345                                line,
17346                            )));
17347                        }
17348                        i += 2;
17349                    }
17350                    return Some(Ok(StrykeValue::class_inst(Arc::new(
17351                        crate::value::ClassInstance::new_with_isa(
17352                            Arc::clone(&c.def),
17353                            new_values,
17354                            c.isa_chain.clone(),
17355                        ),
17356                    ))));
17357                }
17358                "to_hash" => {
17359                    if !args.is_empty() {
17360                        return Some(Err(StrykeError::runtime(
17361                            "class to_hash takes no arguments",
17362                            line,
17363                        )));
17364                    }
17365                    let mut map = IndexMap::new();
17366                    let values = c.get_values();
17367                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
17368                        if let Some(v) = values.get(i) {
17369                            map.insert(name.clone(), v.clone());
17370                        }
17371                    }
17372                    return Some(Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map)))));
17373                }
17374                "to_hash_rec" | "to_hash_deep" => {
17375                    // Recursive flatten: nested class/struct/hash/array
17376                    // values become plain hashref/arrayref trees, so the
17377                    // result is JSON-serializable end-to-end without any
17378                    // surviving ClassInstance/StructInstance leaves.
17379                    if !args.is_empty() {
17380                        return Some(Err(StrykeError::runtime(
17381                            "class to_hash_rec takes no arguments",
17382                            line,
17383                        )));
17384                    }
17385                    return Some(Ok(self.deep_to_hash_value(receiver)));
17386                }
17387                "fields" => {
17388                    if !args.is_empty() {
17389                        return Some(Err(StrykeError::runtime(
17390                            "class fields takes no arguments",
17391                            line,
17392                        )));
17393                    }
17394                    let names: Vec<StrykeValue> = all_fields
17395                        .iter()
17396                        .map(|(name, _, _)| StrykeValue::string(name.clone()))
17397                        .collect();
17398                    return Some(Ok(StrykeValue::array(names)));
17399                }
17400                "clone" => {
17401                    if !args.is_empty() {
17402                        return Some(Err(StrykeError::runtime(
17403                            "class clone takes no arguments",
17404                            line,
17405                        )));
17406                    }
17407                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
17408                    return Some(Ok(StrykeValue::class_inst(Arc::new(
17409                        crate::value::ClassInstance::new_with_isa(
17410                            Arc::clone(&c.def),
17411                            new_values,
17412                            c.isa_chain.clone(),
17413                        ),
17414                    ))));
17415                }
17416                "isa" => {
17417                    if args.len() != 1 {
17418                        return Some(Err(StrykeError::runtime("isa requires one argument", line)));
17419                    }
17420                    let class_name = args[0].to_string();
17421                    let is_a = c.isa(&class_name);
17422                    return Some(Ok(if is_a {
17423                        StrykeValue::integer(1)
17424                    } else {
17425                        StrykeValue::string(String::new())
17426                    }));
17427                }
17428                "does" => {
17429                    if args.len() != 1 {
17430                        return Some(Err(StrykeError::runtime(
17431                            "does requires one argument",
17432                            line,
17433                        )));
17434                    }
17435                    let trait_name = args[0].to_string();
17436                    let implements = c.def.implements.contains(&trait_name);
17437                    return Some(Ok(if implements {
17438                        StrykeValue::integer(1)
17439                    } else {
17440                        StrykeValue::string(String::new())
17441                    }));
17442                }
17443                "methods" => {
17444                    if !args.is_empty() {
17445                        return Some(Err(StrykeError::runtime(
17446                            "methods takes no arguments",
17447                            line,
17448                        )));
17449                    }
17450                    let mut names = Vec::new();
17451                    self.collect_class_method_names(&c.def, &mut names);
17452                    let values: Vec<StrykeValue> =
17453                        names.into_iter().map(StrykeValue::string).collect();
17454                    return Some(Ok(StrykeValue::array(values)));
17455                }
17456                "superclass" => {
17457                    if !args.is_empty() {
17458                        return Some(Err(StrykeError::runtime(
17459                            "superclass takes no arguments",
17460                            line,
17461                        )));
17462                    }
17463                    let parents: Vec<StrykeValue> = c
17464                        .def
17465                        .extends
17466                        .iter()
17467                        .map(|s| StrykeValue::string(s.clone()))
17468                        .collect();
17469                    return Some(Ok(StrykeValue::array(parents)));
17470                }
17471                "destroy" => {
17472                    // Explicit destructor call — runs DESTROY chain child-first
17473                    let destroy_chain = self.collect_destroy_chain(&c.def);
17474                    for (body, params) in &destroy_chain {
17475                        let call_args = vec![receiver.clone()];
17476                        match self.call_class_method(body, params, call_args, line) {
17477                            Ok(_) => {}
17478                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
17479                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
17480                            Err(_) => {}
17481                        }
17482                    }
17483                    return Some(Ok(StrykeValue::UNDEF));
17484                }
17485                _ => {}
17486            }
17487            // User-defined class method (search inheritance chain)
17488            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
17489                // Check visibility
17490                match m.visibility {
17491                    crate::ast::Visibility::Private => {
17492                        let caller_class = self
17493                            .scope
17494                            .get_scalar("self")
17495                            .as_class_inst()
17496                            .map(|ci| ci.def.name.clone());
17497                        if caller_class.as_deref() != Some(owner_class.as_str()) {
17498                            return Some(Err(StrykeError::runtime(
17499                                format!("method `{}` of class {} is private", method, owner_class),
17500                                line,
17501                            )));
17502                        }
17503                    }
17504                    crate::ast::Visibility::Protected => {
17505                        let caller_class = self
17506                            .scope
17507                            .get_scalar("self")
17508                            .as_class_inst()
17509                            .map(|ci| ci.def.name.clone());
17510                        let allowed = caller_class.as_deref().is_some_and(|caller| {
17511                            caller == owner_class.as_str()
17512                                || self.class_inherits_from(caller, owner_class)
17513                        });
17514                        if !allowed {
17515                            return Some(Err(StrykeError::runtime(
17516                                format!(
17517                                    "method `{}` of class {} is protected",
17518                                    method, owner_class
17519                                ),
17520                                line,
17521                            )));
17522                        }
17523                    }
17524                    crate::ast::Visibility::Public => {}
17525                }
17526                if let Some(ref body) = m.body {
17527                    let params = m.params.clone();
17528                    let mut call_args = vec![receiver.clone()];
17529                    call_args.extend(args.iter().cloned());
17530                    return Some(
17531                        match self.call_class_method(body, &params, call_args, line) {
17532                            Ok(v) => Ok(v),
17533                            Err(FlowOrError::Error(e)) => Err(e),
17534                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17535                            Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
17536                                "unexpected control flow in class method",
17537                                line,
17538                            )),
17539                        },
17540                    );
17541                }
17542            }
17543            return None;
17544        }
17545        if let Some(d) = receiver.as_dataframe() {
17546            return Some(self.dataframe_method(d, method, args, line));
17547        }
17548        if let Some(s) = crate::value::set_payload(receiver) {
17549            return Some(self.set_method(s, method, args, line));
17550        }
17551        if let Some(d) = receiver.as_deque() {
17552            return Some(self.deque_method(d, method, args, line));
17553        }
17554        if let Some(h) = receiver.as_heap_pq() {
17555            return Some(self.heap_method(h, method, args, line));
17556        }
17557        if let Some(p) = receiver.as_pipeline() {
17558            return Some(self.pipeline_method(p, method, args, line));
17559        }
17560        if let Some(c) = receiver.as_capture() {
17561            return Some(self.capture_method(c, method, args, line));
17562        }
17563        if let Some(p) = receiver.as_ppool() {
17564            return Some(self.ppool_method(p, method, args, line));
17565        }
17566        if let Some(b) = receiver.as_barrier() {
17567            return Some(self.barrier_method(b, method, args, line));
17568        }
17569        if let Some(g) = receiver.as_generator() {
17570            if method == "next" {
17571                if !args.is_empty() {
17572                    return Some(Err(StrykeError::runtime(
17573                        "generator->next takes no arguments",
17574                        line,
17575                    )));
17576                }
17577                return Some(self.generator_next(&g));
17578            }
17579            return None;
17580        }
17581        if let Some(arc) = receiver.as_atomic_arc() {
17582            let inner = arc.lock().clone();
17583            if let Some(d) = inner.as_deque() {
17584                return Some(self.deque_method(d, method, args, line));
17585            }
17586            if let Some(h) = inner.as_heap_pq() {
17587                return Some(self.heap_method(h, method, args, line));
17588            }
17589        }
17590        None
17591    }
17592
17593    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
17594    fn dataframe_method(
17595        &mut self,
17596        d: Arc<Mutex<PerlDataFrame>>,
17597        method: &str,
17598        args: &[StrykeValue],
17599        line: usize,
17600    ) -> StrykeResult<StrykeValue> {
17601        match method {
17602            "nrow" | "nrows" => {
17603                if !args.is_empty() {
17604                    return Err(StrykeError::runtime(
17605                        format!("dataframe {} takes no arguments", method),
17606                        line,
17607                    ));
17608                }
17609                Ok(StrykeValue::integer(d.lock().nrows() as i64))
17610            }
17611            "ncol" | "ncols" => {
17612                if !args.is_empty() {
17613                    return Err(StrykeError::runtime(
17614                        format!("dataframe {} takes no arguments", method),
17615                        line,
17616                    ));
17617                }
17618                Ok(StrykeValue::integer(d.lock().ncols() as i64))
17619            }
17620            "filter" => {
17621                if args.len() != 1 {
17622                    return Err(StrykeError::runtime(
17623                        "dataframe filter expects 1 argument (sub)",
17624                        line,
17625                    ));
17626                }
17627                let Some(sub) = args[0].as_code_ref() else {
17628                    return Err(StrykeError::runtime(
17629                        "dataframe filter expects a code reference",
17630                        line,
17631                    ));
17632                };
17633                let df_guard = d.lock();
17634                let n = df_guard.nrows();
17635                let mut keep = vec![false; n];
17636                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
17637                    let row = df_guard.row_hashref(r);
17638                    self.scope_push_hook();
17639                    self.scope.set_topic(row);
17640                    if let Some(ref env) = sub.closure_env {
17641                        self.scope.restore_capture(env);
17642                    }
17643                    let pass = match self.exec_block_no_scope(&sub.body) {
17644                        Ok(v) => v.is_true(),
17645                        Err(_) => false,
17646                    };
17647                    self.scope_pop_hook();
17648                    *row_keep = pass;
17649                }
17650                let columns = df_guard.columns.clone();
17651                let cols: Vec<Vec<StrykeValue>> = (0..df_guard.ncols())
17652                    .map(|i| {
17653                        let mut out = Vec::new();
17654                        for (r, pass_row) in keep.iter().enumerate().take(n) {
17655                            if *pass_row {
17656                                out.push(df_guard.cols[i][r].clone());
17657                            }
17658                        }
17659                        out
17660                    })
17661                    .collect();
17662                let group_by = df_guard.group_by.clone();
17663                drop(df_guard);
17664                let new_df = PerlDataFrame {
17665                    columns,
17666                    cols,
17667                    group_by,
17668                };
17669                Ok(StrykeValue::dataframe(Arc::new(Mutex::new(new_df))))
17670            }
17671            "group_by" => {
17672                if args.len() != 1 {
17673                    return Err(StrykeError::runtime(
17674                        "dataframe group_by expects 1 column name",
17675                        line,
17676                    ));
17677                }
17678                let key = args[0].to_string();
17679                let inner = d.lock();
17680                if inner.col_index(&key).is_none() {
17681                    return Err(StrykeError::runtime(
17682                        format!("dataframe group_by: unknown column \"{}\"", key),
17683                        line,
17684                    ));
17685                }
17686                let new_df = PerlDataFrame {
17687                    columns: inner.columns.clone(),
17688                    cols: inner.cols.clone(),
17689                    group_by: Some(key),
17690                };
17691                Ok(StrykeValue::dataframe(Arc::new(Mutex::new(new_df))))
17692            }
17693            "sum" => {
17694                if args.len() != 1 {
17695                    return Err(StrykeError::runtime(
17696                        "dataframe sum expects 1 column name",
17697                        line,
17698                    ));
17699                }
17700                let col_name = args[0].to_string();
17701                let inner = d.lock();
17702                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
17703                    StrykeError::runtime(
17704                        format!("dataframe sum: unknown column \"{}\"", col_name),
17705                        line,
17706                    )
17707                })?;
17708                match &inner.group_by {
17709                    Some(gcol) => {
17710                        let gi = inner.col_index(gcol).ok_or_else(|| {
17711                            StrykeError::runtime(
17712                                format!("dataframe sum: unknown group column \"{}\"", gcol),
17713                                line,
17714                            )
17715                        })?;
17716                        let mut acc: IndexMap<String, f64> = IndexMap::new();
17717                        for r in 0..inner.nrows() {
17718                            let k = inner.cols[gi][r].to_string();
17719                            let v = inner.cols[val_idx][r].to_number();
17720                            *acc.entry(k).or_insert(0.0) += v;
17721                        }
17722                        let keys: Vec<String> = acc.keys().cloned().collect();
17723                        let sums: Vec<f64> = acc.values().copied().collect();
17724                        let cols = vec![
17725                            keys.into_iter().map(StrykeValue::string).collect(),
17726                            sums.into_iter().map(StrykeValue::float).collect(),
17727                        ];
17728                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
17729                        let out = PerlDataFrame {
17730                            columns,
17731                            cols,
17732                            group_by: None,
17733                        };
17734                        Ok(StrykeValue::dataframe(Arc::new(Mutex::new(out))))
17735                    }
17736                    None => {
17737                        let total: f64 = (0..inner.nrows())
17738                            .map(|r| inner.cols[val_idx][r].to_number())
17739                            .sum();
17740                        Ok(StrykeValue::float(total))
17741                    }
17742                }
17743            }
17744            _ => Err(StrykeError::runtime(
17745                format!("Unknown method for dataframe: {}", method),
17746                line,
17747            )),
17748        }
17749    }
17750
17751    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
17752    fn set_method(
17753        &self,
17754        s: Arc<crate::value::PerlSet>,
17755        method: &str,
17756        args: &[StrykeValue],
17757        line: usize,
17758    ) -> StrykeResult<StrykeValue> {
17759        match method {
17760            "has" | "contains" | "member" => {
17761                if args.len() != 1 {
17762                    return Err(StrykeError::runtime(
17763                        "set->has expects one argument (element)",
17764                        line,
17765                    ));
17766                }
17767                let k = crate::value::set_member_key(&args[0]);
17768                Ok(StrykeValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
17769            }
17770            "size" | "len" | "count" => {
17771                if !args.is_empty() {
17772                    return Err(StrykeError::runtime("set->size takes no arguments", line));
17773                }
17774                Ok(StrykeValue::integer(s.len() as i64))
17775            }
17776            "values" | "list" | "elements" => {
17777                if !args.is_empty() {
17778                    return Err(StrykeError::runtime("set->values takes no arguments", line));
17779                }
17780                Ok(StrykeValue::array(s.values().cloned().collect()))
17781            }
17782            _ => Err(StrykeError::runtime(
17783                format!("Unknown method for set: {}", method),
17784                line,
17785            )),
17786        }
17787    }
17788
17789    fn deque_method(
17790        &mut self,
17791        d: Arc<Mutex<VecDeque<StrykeValue>>>,
17792        method: &str,
17793        args: &[StrykeValue],
17794        line: usize,
17795    ) -> StrykeResult<StrykeValue> {
17796        match method {
17797            "push_back" => {
17798                if args.len() != 1 {
17799                    return Err(StrykeError::runtime("push_back expects 1 argument", line));
17800                }
17801                d.lock().push_back(args[0].clone());
17802                Ok(StrykeValue::integer(d.lock().len() as i64))
17803            }
17804            "push_front" => {
17805                if args.len() != 1 {
17806                    return Err(StrykeError::runtime("push_front expects 1 argument", line));
17807                }
17808                d.lock().push_front(args[0].clone());
17809                Ok(StrykeValue::integer(d.lock().len() as i64))
17810            }
17811            "pop_back" => Ok(d.lock().pop_back().unwrap_or(StrykeValue::UNDEF)),
17812            "pop_front" => Ok(d.lock().pop_front().unwrap_or(StrykeValue::UNDEF)),
17813            "size" | "len" => Ok(StrykeValue::integer(d.lock().len() as i64)),
17814            _ => Err(StrykeError::runtime(
17815                format!("Unknown method for deque: {}", method),
17816                line,
17817            )),
17818        }
17819    }
17820
17821    fn heap_method(
17822        &mut self,
17823        h: Arc<Mutex<PerlHeap>>,
17824        method: &str,
17825        args: &[StrykeValue],
17826        line: usize,
17827    ) -> StrykeResult<StrykeValue> {
17828        match method {
17829            "push" => {
17830                if args.len() != 1 {
17831                    return Err(StrykeError::runtime("heap push expects 1 argument", line));
17832                }
17833                let mut g = h.lock();
17834                let n = g.items.len();
17835                g.items.push(args[0].clone());
17836                let cmp = g.cmp.clone();
17837                drop(g);
17838                let mut g = h.lock();
17839                self.heap_sift_up(&mut g.items, &cmp, n);
17840                Ok(StrykeValue::integer(g.items.len() as i64))
17841            }
17842            "pop" => {
17843                let mut g = h.lock();
17844                if g.items.is_empty() {
17845                    return Ok(StrykeValue::UNDEF);
17846                }
17847                let cmp = g.cmp.clone();
17848                let n = g.items.len();
17849                g.items.swap(0, n - 1);
17850                let v = g.items.pop().unwrap();
17851                if !g.items.is_empty() {
17852                    self.heap_sift_down(&mut g.items, &cmp, 0);
17853                }
17854                Ok(v)
17855            }
17856            "peek" => Ok(h
17857                .lock()
17858                .items
17859                .first()
17860                .cloned()
17861                .unwrap_or(StrykeValue::UNDEF)),
17862            _ => Err(StrykeError::runtime(
17863                format!("Unknown method for heap: {}", method),
17864                line,
17865            )),
17866        }
17867    }
17868
17869    fn ppool_method(
17870        &mut self,
17871        pool: PerlPpool,
17872        method: &str,
17873        args: &[StrykeValue],
17874        line: usize,
17875    ) -> StrykeResult<StrykeValue> {
17876        match method {
17877            "submit" => pool.submit(self, args, line),
17878            "collect" => {
17879                if !args.is_empty() {
17880                    return Err(StrykeError::runtime("collect() takes no arguments", line));
17881                }
17882                pool.collect(line)
17883            }
17884            _ => Err(StrykeError::runtime(
17885                format!("Unknown method for ppool: {}", method),
17886                line,
17887            )),
17888        }
17889    }
17890
17891    fn barrier_method(
17892        &self,
17893        barrier: PerlBarrier,
17894        method: &str,
17895        args: &[StrykeValue],
17896        line: usize,
17897    ) -> StrykeResult<StrykeValue> {
17898        match method {
17899            "wait" => {
17900                if !args.is_empty() {
17901                    return Err(StrykeError::runtime("wait() takes no arguments", line));
17902                }
17903                let _ = barrier.0.wait();
17904                Ok(StrykeValue::integer(1))
17905            }
17906            _ => Err(StrykeError::runtime(
17907                format!("Unknown method for barrier: {}", method),
17908                line,
17909            )),
17910        }
17911    }
17912
17913    fn capture_method(
17914        &self,
17915        c: Arc<CaptureResult>,
17916        method: &str,
17917        args: &[StrykeValue],
17918        line: usize,
17919    ) -> StrykeResult<StrykeValue> {
17920        if !args.is_empty() {
17921            return Err(StrykeError::runtime(
17922                format!("capture: {} takes no arguments", method),
17923                line,
17924            ));
17925        }
17926        match method {
17927            "stdout" => Ok(StrykeValue::string(c.stdout.clone())),
17928            "stderr" => Ok(StrykeValue::string(c.stderr.clone())),
17929            "exitcode" => Ok(StrykeValue::integer(c.exitcode)),
17930            "failed" => Ok(StrykeValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
17931            _ => Err(StrykeError::runtime(
17932                format!("Unknown method for capture: {}", method),
17933                line,
17934            )),
17935        }
17936    }
17937
17938    pub(crate) fn builtin_par_pipeline_stream(
17939        &mut self,
17940        args: &[StrykeValue],
17941        _line: usize,
17942    ) -> StrykeResult<StrykeValue> {
17943        let mut items = Vec::new();
17944        for v in args {
17945            if let Some(a) = v.as_array_vec() {
17946                items.extend(a);
17947            } else {
17948                items.push(v.clone());
17949            }
17950        }
17951        Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
17952            source: items,
17953            ops: Vec::new(),
17954            has_scalar_terminal: false,
17955            par_stream: true,
17956            streaming: false,
17957            streaming_workers: 0,
17958            streaming_buffer: 256,
17959        }))))
17960    }
17961
17962    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
17963    /// that wires ops through bounded channels on `collect()`.
17964    pub(crate) fn builtin_par_pipeline_stream_new(
17965        &mut self,
17966        args: &[StrykeValue],
17967        _line: usize,
17968    ) -> StrykeResult<StrykeValue> {
17969        let mut items = Vec::new();
17970        let mut workers: usize = 0;
17971        let mut buffer: usize = 256;
17972        // Separate list items from keyword args (workers => N, buffer => N).
17973        let mut i = 0;
17974        while i < args.len() {
17975            let s = args[i].to_string();
17976            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
17977                let val = args[i + 1].to_int().max(1) as usize;
17978                if s == "workers" {
17979                    workers = val;
17980                } else {
17981                    buffer = val;
17982                }
17983                i += 2;
17984            } else if let Some(a) = args[i].as_array_vec() {
17985                items.extend(a);
17986                i += 1;
17987            } else {
17988                items.push(args[i].clone());
17989                i += 1;
17990            }
17991        }
17992        Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
17993            source: items,
17994            ops: Vec::new(),
17995            has_scalar_terminal: false,
17996            par_stream: false,
17997            streaming: true,
17998            streaming_workers: workers,
17999            streaming_buffer: buffer,
18000        }))))
18001    }
18002
18003    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
18004    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<StrykeSub> {
18005        let line = 1usize;
18006        let body = vec![Statement {
18007            label: None,
18008            kind: StmtKind::Expression(Expr {
18009                kind: ExprKind::BinOp {
18010                    left: Box::new(Expr {
18011                        kind: ExprKind::ScalarVar("_".into()),
18012                        line,
18013                    }),
18014                    op: BinOp::Mul,
18015                    right: Box::new(Expr {
18016                        kind: ExprKind::Integer(k),
18017                        line,
18018                    }),
18019                },
18020                line,
18021            }),
18022            line,
18023        }];
18024        Arc::new(StrykeSub {
18025            name: "__pipeline_int_mul__".into(),
18026            params: vec![],
18027            body,
18028            closure_env: None,
18029            prototype: None,
18030            fib_like: None,
18031        })
18032    }
18033
18034    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<StrykeSub> {
18035        let captured = self.scope.capture();
18036        Arc::new(StrykeSub {
18037            name: "__ANON__".into(),
18038            params: vec![],
18039            body: block.clone(),
18040            closure_env: Some(captured),
18041            prototype: None,
18042            fib_like: None,
18043        })
18044    }
18045
18046    pub(crate) fn builtin_collect_execute(
18047        &mut self,
18048        args: &[StrykeValue],
18049        line: usize,
18050    ) -> StrykeResult<StrykeValue> {
18051        if args.is_empty() {
18052            return Err(StrykeError::runtime(
18053                "collect() expects at least one argument",
18054                line,
18055            ));
18056        }
18057        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
18058        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
18059        if args.len() == 1 {
18060            if let Some(p) = args[0].as_pipeline() {
18061                return self.pipeline_collect(&p, line);
18062            }
18063            return Ok(StrykeValue::array(args[0].to_list()));
18064        }
18065        Ok(StrykeValue::array(args.to_vec()))
18066    }
18067
18068    pub(crate) fn pipeline_push(
18069        &self,
18070        p: &Arc<Mutex<PipelineInner>>,
18071        op: PipelineOp,
18072        line: usize,
18073    ) -> StrykeResult<()> {
18074        let mut g = p.lock();
18075        if g.has_scalar_terminal {
18076            return Err(StrykeError::runtime(
18077                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
18078                line,
18079            ));
18080        }
18081        if matches!(
18082            &op,
18083            PipelineOp::PReduce { .. }
18084                | PipelineOp::PReduceInit { .. }
18085                | PipelineOp::PMapReduce { .. }
18086        ) {
18087            g.has_scalar_terminal = true;
18088        }
18089        g.ops.push(op);
18090        Ok(())
18091    }
18092
18093    fn pipeline_parse_sub_progress(
18094        args: &[StrykeValue],
18095        line: usize,
18096        name: &str,
18097    ) -> StrykeResult<(Arc<StrykeSub>, bool)> {
18098        if args.is_empty() {
18099            return Err(StrykeError::runtime(
18100                format!("pipeline {}: expects at least 1 argument (code ref)", name),
18101                line,
18102            ));
18103        }
18104        let Some(sub) = args[0].as_code_ref() else {
18105            return Err(StrykeError::runtime(
18106                format!("pipeline {}: first argument must be a code reference", name),
18107                line,
18108            ));
18109        };
18110        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
18111        if args.len() > 2 {
18112            return Err(StrykeError::runtime(
18113                format!(
18114                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
18115                    name
18116                ),
18117                line,
18118            ));
18119        }
18120        Ok((sub, progress))
18121    }
18122
18123    pub(crate) fn pipeline_method(
18124        &mut self,
18125        p: Arc<Mutex<PipelineInner>>,
18126        method: &str,
18127        args: &[StrykeValue],
18128        line: usize,
18129    ) -> StrykeResult<StrykeValue> {
18130        match method {
18131            "filter" | "f" | "grep" => {
18132                if args.len() != 1 {
18133                    return Err(StrykeError::runtime(
18134                        "pipeline filter/grep expects 1 argument (sub)",
18135                        line,
18136                    ));
18137                }
18138                let Some(sub) = args[0].as_code_ref() else {
18139                    return Err(StrykeError::runtime(
18140                        "pipeline filter/grep expects a code reference",
18141                        line,
18142                    ));
18143                };
18144                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
18145                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18146            }
18147            "map" => {
18148                if args.len() != 1 {
18149                    return Err(StrykeError::runtime(
18150                        "pipeline map expects 1 argument (sub)",
18151                        line,
18152                    ));
18153                }
18154                let Some(sub) = args[0].as_code_ref() else {
18155                    return Err(StrykeError::runtime(
18156                        "pipeline map expects a code reference",
18157                        line,
18158                    ));
18159                };
18160                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
18161                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18162            }
18163            "tap" | "peek" => {
18164                if args.len() != 1 {
18165                    return Err(StrykeError::runtime(
18166                        "pipeline tap/peek expects 1 argument (sub)",
18167                        line,
18168                    ));
18169                }
18170                let Some(sub) = args[0].as_code_ref() else {
18171                    return Err(StrykeError::runtime(
18172                        "pipeline tap/peek expects a code reference",
18173                        line,
18174                    ));
18175                };
18176                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
18177                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18178            }
18179            "take" => {
18180                if args.len() != 1 {
18181                    return Err(StrykeError::runtime(
18182                        "pipeline take expects 1 argument",
18183                        line,
18184                    ));
18185                }
18186                let n = args[0].to_int();
18187                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
18188                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18189            }
18190            "pmap" => {
18191                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
18192                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
18193                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18194            }
18195            "pgrep" => {
18196                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
18197                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
18198                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18199            }
18200            "pfor" => {
18201                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
18202                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
18203                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18204            }
18205            "pmap_chunked" => {
18206                if args.len() < 2 {
18207                    return Err(StrykeError::runtime(
18208                        "pipeline pmap_chunked expects chunk size and a code reference",
18209                        line,
18210                    ));
18211                }
18212                let chunk = args[0].to_int().max(1);
18213                let Some(sub) = args[1].as_code_ref() else {
18214                    return Err(StrykeError::runtime(
18215                        "pipeline pmap_chunked: second argument must be a code reference",
18216                        line,
18217                    ));
18218                };
18219                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
18220                if args.len() > 3 {
18221                    return Err(StrykeError::runtime(
18222                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
18223                        line,
18224                    ));
18225                }
18226                self.pipeline_push(
18227                    &p,
18228                    PipelineOp::PMapChunked {
18229                        chunk,
18230                        sub,
18231                        progress,
18232                    },
18233                    line,
18234                )?;
18235                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18236            }
18237            "psort" => {
18238                let (cmp, progress) = match args.len() {
18239                    0 => (None, false),
18240                    1 => {
18241                        if let Some(s) = args[0].as_code_ref() {
18242                            (Some(s), false)
18243                        } else {
18244                            (None, args[0].is_true())
18245                        }
18246                    }
18247                    2 => {
18248                        let Some(s) = args[0].as_code_ref() else {
18249                            return Err(StrykeError::runtime(
18250                                "pipeline psort: with two arguments, the first must be a comparator sub",
18251                                line,
18252                            ));
18253                        };
18254                        (Some(s), args[1].is_true())
18255                    }
18256                    _ => {
18257                        return Err(StrykeError::runtime(
18258                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
18259                            line,
18260                        ));
18261                    }
18262                };
18263                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
18264                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18265            }
18266            "pcache" => {
18267                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
18268                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
18269                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18270            }
18271            "preduce" => {
18272                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
18273                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
18274                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18275            }
18276            "preduce_init" => {
18277                if args.len() < 2 {
18278                    return Err(StrykeError::runtime(
18279                        "pipeline preduce_init expects init value and a code reference",
18280                        line,
18281                    ));
18282                }
18283                let init = args[0].clone();
18284                let Some(sub) = args[1].as_code_ref() else {
18285                    return Err(StrykeError::runtime(
18286                        "pipeline preduce_init: second argument must be a code reference",
18287                        line,
18288                    ));
18289                };
18290                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
18291                if args.len() > 3 {
18292                    return Err(StrykeError::runtime(
18293                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
18294                        line,
18295                    ));
18296                }
18297                self.pipeline_push(
18298                    &p,
18299                    PipelineOp::PReduceInit {
18300                        init,
18301                        sub,
18302                        progress,
18303                    },
18304                    line,
18305                )?;
18306                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18307            }
18308            "pmap_reduce" => {
18309                if args.len() < 2 {
18310                    return Err(StrykeError::runtime(
18311                        "pipeline pmap_reduce expects map sub and reduce sub",
18312                        line,
18313                    ));
18314                }
18315                let Some(map) = args[0].as_code_ref() else {
18316                    return Err(StrykeError::runtime(
18317                        "pipeline pmap_reduce: first argument must be a code reference (map)",
18318                        line,
18319                    ));
18320                };
18321                let Some(reduce) = args[1].as_code_ref() else {
18322                    return Err(StrykeError::runtime(
18323                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
18324                        line,
18325                    ));
18326                };
18327                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
18328                if args.len() > 3 {
18329                    return Err(StrykeError::runtime(
18330                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
18331                        line,
18332                    ));
18333                }
18334                self.pipeline_push(
18335                    &p,
18336                    PipelineOp::PMapReduce {
18337                        map,
18338                        reduce,
18339                        progress,
18340                    },
18341                    line,
18342                )?;
18343                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18344            }
18345            "collect" => {
18346                if !args.is_empty() {
18347                    return Err(StrykeError::runtime(
18348                        "pipeline collect takes no arguments",
18349                        line,
18350                    ));
18351                }
18352                self.pipeline_collect(&p, line)
18353            }
18354            _ => {
18355                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
18356                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
18357                if let Some(sub) = self.resolve_sub_by_name(method) {
18358                    if !args.is_empty() {
18359                        return Err(StrykeError::runtime(
18360                            format!(
18361                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
18362                                method
18363                            ),
18364                            line,
18365                        ));
18366                    }
18367                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
18368                    Ok(StrykeValue::pipeline(Arc::clone(&p)))
18369                } else {
18370                    Err(StrykeError::runtime(
18371                        format!("Unknown method for pipeline: {}", method),
18372                        line,
18373                    ))
18374                }
18375            }
18376        }
18377    }
18378
18379    fn pipeline_parallel_map(
18380        &mut self,
18381        items: Vec<StrykeValue>,
18382        sub: &Arc<StrykeSub>,
18383        progress: bool,
18384    ) -> Vec<StrykeValue> {
18385        let subs = self.subs.clone();
18386        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18387        let pmap_progress = PmapProgress::new(progress, items.len());
18388        let results: Vec<StrykeValue> = items
18389            .into_par_iter()
18390            .map(|item| {
18391                let mut local_interp = VMHelper::new();
18392                local_interp.subs = subs.clone();
18393                local_interp.scope.restore_capture(&scope_capture);
18394                local_interp
18395                    .scope
18396                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18397                local_interp.enable_parallel_guard();
18398                local_interp.scope.set_topic(item);
18399                local_interp.scope_push_hook();
18400                let val = match local_interp.exec_block_no_scope(&sub.body) {
18401                    Ok(val) => val,
18402                    Err(_) => StrykeValue::UNDEF,
18403                };
18404                local_interp.scope_pop_hook();
18405                pmap_progress.tick();
18406                val
18407            })
18408            .collect();
18409        pmap_progress.finish();
18410        results
18411    }
18412
18413    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
18414    fn pipeline_par_stream_filter(
18415        &mut self,
18416        items: Vec<StrykeValue>,
18417        sub: &Arc<StrykeSub>,
18418    ) -> Vec<StrykeValue> {
18419        if items.is_empty() {
18420            return items;
18421        }
18422        let subs = self.subs.clone();
18423        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18424        let indexed: Vec<(usize, StrykeValue)> = items.into_iter().enumerate().collect();
18425        let mut kept: Vec<(usize, StrykeValue)> = indexed
18426            .into_par_iter()
18427            .filter_map(|(i, item)| {
18428                let mut local_interp = VMHelper::new();
18429                local_interp.subs = subs.clone();
18430                local_interp.scope.restore_capture(&scope_capture);
18431                local_interp
18432                    .scope
18433                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18434                local_interp.enable_parallel_guard();
18435                local_interp.scope.set_topic(item.clone());
18436                local_interp.scope_push_hook();
18437                let keep = match local_interp.exec_block_no_scope(&sub.body) {
18438                    Ok(val) => val.is_true(),
18439                    Err(_) => false,
18440                };
18441                local_interp.scope_pop_hook();
18442                if keep {
18443                    Some((i, item))
18444                } else {
18445                    None
18446                }
18447            })
18448            .collect();
18449        kept.sort_by_key(|(i, _)| *i);
18450        kept.into_iter().map(|(_, x)| x).collect()
18451    }
18452
18453    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
18454    fn pipeline_par_stream_map(
18455        &mut self,
18456        items: Vec<StrykeValue>,
18457        sub: &Arc<StrykeSub>,
18458    ) -> Vec<StrykeValue> {
18459        if items.is_empty() {
18460            return items;
18461        }
18462        let subs = self.subs.clone();
18463        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18464        let indexed: Vec<(usize, StrykeValue)> = items.into_iter().enumerate().collect();
18465        let mut mapped: Vec<(usize, StrykeValue)> = indexed
18466            .into_par_iter()
18467            .map(|(i, item)| {
18468                let mut local_interp = VMHelper::new();
18469                local_interp.subs = subs.clone();
18470                local_interp.scope.restore_capture(&scope_capture);
18471                local_interp
18472                    .scope
18473                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18474                local_interp.enable_parallel_guard();
18475                local_interp.scope.set_topic(item);
18476                local_interp.scope_push_hook();
18477                let val = match local_interp.exec_block_no_scope(&sub.body) {
18478                    Ok(val) => val,
18479                    Err(_) => StrykeValue::UNDEF,
18480                };
18481                local_interp.scope_pop_hook();
18482                (i, val)
18483            })
18484            .collect();
18485        mapped.sort_by_key(|(i, _)| *i);
18486        mapped.into_iter().map(|(_, x)| x).collect()
18487    }
18488
18489    fn pipeline_collect(
18490        &mut self,
18491        p: &Arc<Mutex<PipelineInner>>,
18492        line: usize,
18493    ) -> StrykeResult<StrykeValue> {
18494        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
18495            let g = p.lock();
18496            (
18497                g.source.clone(),
18498                g.ops.clone(),
18499                g.par_stream,
18500                g.streaming,
18501                g.streaming_workers,
18502                g.streaming_buffer,
18503            )
18504        };
18505        if streaming {
18506            return self.pipeline_collect_streaming(
18507                v,
18508                &ops,
18509                streaming_workers,
18510                streaming_buffer,
18511                line,
18512            );
18513        }
18514        for op in ops {
18515            match op {
18516                PipelineOp::Filter(sub) => {
18517                    if par_stream {
18518                        v = self.pipeline_par_stream_filter(v, &sub);
18519                    } else {
18520                        let mut out = Vec::new();
18521                        for item in v {
18522                            self.scope_push_hook();
18523                            self.scope.set_topic(item.clone());
18524                            if let Some(ref env) = sub.closure_env {
18525                                self.scope.restore_capture(env);
18526                            }
18527                            let keep = match self.exec_block_no_scope(&sub.body) {
18528                                Ok(val) => val.is_true(),
18529                                Err(_) => false,
18530                            };
18531                            self.scope_pop_hook();
18532                            if keep {
18533                                out.push(item);
18534                            }
18535                        }
18536                        v = out;
18537                    }
18538                }
18539                PipelineOp::Map(sub) => {
18540                    if par_stream {
18541                        v = self.pipeline_par_stream_map(v, &sub);
18542                    } else {
18543                        let mut out = Vec::new();
18544                        for item in v {
18545                            self.scope_push_hook();
18546                            self.scope.set_topic(item);
18547                            if let Some(ref env) = sub.closure_env {
18548                                self.scope.restore_capture(env);
18549                            }
18550                            let mapped = match self.exec_block_no_scope(&sub.body) {
18551                                Ok(val) => val,
18552                                Err(_) => StrykeValue::UNDEF,
18553                            };
18554                            self.scope_pop_hook();
18555                            out.push(mapped);
18556                        }
18557                        v = out;
18558                    }
18559                }
18560                PipelineOp::Tap(sub) => {
18561                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
18562                        Ok(_) => {}
18563                        Err(FlowOrError::Error(e)) => return Err(e),
18564                        Err(FlowOrError::Flow(_)) => {
18565                            return Err(StrykeError::runtime(
18566                                "tap: unsupported control flow in block",
18567                                line,
18568                            ));
18569                        }
18570                    }
18571                }
18572                PipelineOp::Take(n) => {
18573                    let n = n.max(0) as usize;
18574                    if v.len() > n {
18575                        v.truncate(n);
18576                    }
18577                }
18578                PipelineOp::PMap { sub, progress } => {
18579                    v = self.pipeline_parallel_map(v, &sub, progress);
18580                }
18581                PipelineOp::PGrep { sub, progress } => {
18582                    let subs = self.subs.clone();
18583                    let (scope_capture, atomic_arrays, atomic_hashes) =
18584                        self.scope.capture_with_atomics();
18585                    let pmap_progress = PmapProgress::new(progress, v.len());
18586                    v = v
18587                        .into_par_iter()
18588                        .filter_map(|item| {
18589                            let mut local_interp = VMHelper::new();
18590                            local_interp.subs = subs.clone();
18591                            local_interp.scope.restore_capture(&scope_capture);
18592                            local_interp
18593                                .scope
18594                                .restore_atomics(&atomic_arrays, &atomic_hashes);
18595                            local_interp.enable_parallel_guard();
18596                            local_interp.scope.set_topic(item.clone());
18597                            local_interp.scope_push_hook();
18598                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
18599                                Ok(val) => val.is_true(),
18600                                Err(_) => false,
18601                            };
18602                            local_interp.scope_pop_hook();
18603                            pmap_progress.tick();
18604                            if keep {
18605                                Some(item)
18606                            } else {
18607                                None
18608                            }
18609                        })
18610                        .collect();
18611                    pmap_progress.finish();
18612                }
18613                PipelineOp::PFor { sub, progress } => {
18614                    let subs = self.subs.clone();
18615                    let (scope_capture, atomic_arrays, atomic_hashes) =
18616                        self.scope.capture_with_atomics();
18617                    let pmap_progress = PmapProgress::new(progress, v.len());
18618                    let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
18619                    v.clone().into_par_iter().for_each(|item| {
18620                        if first_err.lock().is_some() {
18621                            return;
18622                        }
18623                        let mut local_interp = VMHelper::new();
18624                        local_interp.subs = subs.clone();
18625                        local_interp.scope.restore_capture(&scope_capture);
18626                        local_interp
18627                            .scope
18628                            .restore_atomics(&atomic_arrays, &atomic_hashes);
18629                        local_interp.enable_parallel_guard();
18630                        local_interp.scope.set_topic(item);
18631                        local_interp.scope_push_hook();
18632                        match local_interp.exec_block_no_scope(&sub.body) {
18633                            Ok(_) => {}
18634                            Err(e) => {
18635                                let stryke = match e {
18636                                    FlowOrError::Error(stryke) => stryke,
18637                                    FlowOrError::Flow(_) => StrykeError::runtime(
18638                                        "return/last/next/redo not supported inside pipeline pfor block",
18639                                        line,
18640                                    ),
18641                                };
18642                                let mut g = first_err.lock();
18643                                if g.is_none() {
18644                                    *g = Some(stryke);
18645                                }
18646                            }
18647                        }
18648                        local_interp.scope_pop_hook();
18649                        pmap_progress.tick();
18650                    });
18651                    pmap_progress.finish();
18652                    let pfor_err = first_err.lock().take();
18653                    if let Some(e) = pfor_err {
18654                        return Err(e);
18655                    }
18656                }
18657                PipelineOp::PMapChunked {
18658                    chunk,
18659                    sub,
18660                    progress,
18661                } => {
18662                    let chunk_n = chunk.max(1) as usize;
18663                    let subs = self.subs.clone();
18664                    let (scope_capture, atomic_arrays, atomic_hashes) =
18665                        self.scope.capture_with_atomics();
18666                    let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = v
18667                        .chunks(chunk_n)
18668                        .enumerate()
18669                        .map(|(i, c)| (i, c.to_vec()))
18670                        .collect();
18671                    let n_chunks = indexed_chunks.len();
18672                    let pmap_progress = PmapProgress::new(progress, n_chunks);
18673                    let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
18674                        .into_par_iter()
18675                        .map(|(chunk_idx, chunk)| {
18676                            let mut local_interp = VMHelper::new();
18677                            local_interp.subs = subs.clone();
18678                            local_interp.scope.restore_capture(&scope_capture);
18679                            local_interp
18680                                .scope
18681                                .restore_atomics(&atomic_arrays, &atomic_hashes);
18682                            local_interp.enable_parallel_guard();
18683                            let mut out = Vec::with_capacity(chunk.len());
18684                            for item in chunk {
18685                                local_interp.scope.set_topic(item);
18686                                local_interp.scope_push_hook();
18687                                match local_interp.exec_block_no_scope(&sub.body) {
18688                                    Ok(val) => {
18689                                        local_interp.scope_pop_hook();
18690                                        out.push(val);
18691                                    }
18692                                    Err(_) => {
18693                                        local_interp.scope_pop_hook();
18694                                        out.push(StrykeValue::UNDEF);
18695                                    }
18696                                }
18697                            }
18698                            pmap_progress.tick();
18699                            (chunk_idx, out)
18700                        })
18701                        .collect();
18702                    pmap_progress.finish();
18703                    chunk_results.sort_by_key(|(i, _)| *i);
18704                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
18705                }
18706                PipelineOp::PSort { cmp, progress } => {
18707                    let pmap_progress = PmapProgress::new(progress, 2);
18708                    pmap_progress.tick();
18709                    match cmp {
18710                        Some(cmp_block) => {
18711                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
18712                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
18713                            } else {
18714                                let subs = self.subs.clone();
18715                                let scope_capture = self.scope.capture();
18716                                v.par_sort_by(|a, b| {
18717                                    let mut local_interp = VMHelper::new();
18718                                    local_interp.subs = subs.clone();
18719                                    local_interp.scope.restore_capture(&scope_capture);
18720                                    local_interp.enable_parallel_guard();
18721                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
18722                                    local_interp.scope_push_hook();
18723                                    let ord =
18724                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
18725                                            Ok(v) => {
18726                                                let n = v.to_int();
18727                                                if n < 0 {
18728                                                    std::cmp::Ordering::Less
18729                                                } else if n > 0 {
18730                                                    std::cmp::Ordering::Greater
18731                                                } else {
18732                                                    std::cmp::Ordering::Equal
18733                                                }
18734                                            }
18735                                            Err(_) => std::cmp::Ordering::Equal,
18736                                        };
18737                                    local_interp.scope_pop_hook();
18738                                    ord
18739                                });
18740                            }
18741                        }
18742                        None => {
18743                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
18744                        }
18745                    }
18746                    pmap_progress.tick();
18747                    pmap_progress.finish();
18748                }
18749                PipelineOp::PCache { sub, progress } => {
18750                    let subs = self.subs.clone();
18751                    let scope_capture = self.scope.capture();
18752                    let cache = &*crate::pcache::GLOBAL_PCACHE;
18753                    let pmap_progress = PmapProgress::new(progress, v.len());
18754                    v = v
18755                        .into_par_iter()
18756                        .map(|item| {
18757                            let k = crate::pcache::cache_key(&item);
18758                            if let Some(cached) = cache.get(&k) {
18759                                pmap_progress.tick();
18760                                return cached.clone();
18761                            }
18762                            let mut local_interp = VMHelper::new();
18763                            local_interp.subs = subs.clone();
18764                            local_interp.scope.restore_capture(&scope_capture);
18765                            local_interp.enable_parallel_guard();
18766                            local_interp.scope.set_topic(item.clone());
18767                            local_interp.scope_push_hook();
18768                            let val = match local_interp.exec_block_no_scope(&sub.body) {
18769                                Ok(v) => v,
18770                                Err(_) => StrykeValue::UNDEF,
18771                            };
18772                            local_interp.scope_pop_hook();
18773                            cache.insert(k, val.clone());
18774                            pmap_progress.tick();
18775                            val
18776                        })
18777                        .collect();
18778                    pmap_progress.finish();
18779                }
18780                PipelineOp::PReduce { sub, progress } => {
18781                    if v.is_empty() {
18782                        return Ok(StrykeValue::UNDEF);
18783                    }
18784                    if v.len() == 1 {
18785                        return Ok(v.into_iter().next().unwrap());
18786                    }
18787                    let block = sub.body.clone();
18788                    let subs = self.subs.clone();
18789                    let scope_capture = self.scope.capture();
18790                    let pmap_progress = PmapProgress::new(progress, v.len());
18791                    let result = v
18792                        .into_par_iter()
18793                        .map(|x| {
18794                            pmap_progress.tick();
18795                            x
18796                        })
18797                        .reduce_with(|a, b| {
18798                            let mut local_interp = VMHelper::new();
18799                            local_interp.subs = subs.clone();
18800                            local_interp.scope.restore_capture(&scope_capture);
18801                            local_interp.enable_parallel_guard();
18802                            local_interp.scope.set_sort_pair(a, b);
18803                            match local_interp.exec_block(&block) {
18804                                Ok(val) => val,
18805                                Err(_) => StrykeValue::UNDEF,
18806                            }
18807                        });
18808                    pmap_progress.finish();
18809                    return Ok(result.unwrap_or(StrykeValue::UNDEF));
18810                }
18811                PipelineOp::PReduceInit {
18812                    init,
18813                    sub,
18814                    progress,
18815                } => {
18816                    if v.is_empty() {
18817                        return Ok(init);
18818                    }
18819                    let block = sub.body.clone();
18820                    let subs = self.subs.clone();
18821                    let scope_capture = self.scope.capture();
18822                    let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
18823                    if v.len() == 1 {
18824                        return Ok(fold_preduce_init_step(
18825                            &subs,
18826                            cap,
18827                            &block,
18828                            preduce_init_fold_identity(&init),
18829                            v.into_iter().next().unwrap(),
18830                        ));
18831                    }
18832                    let pmap_progress = PmapProgress::new(progress, v.len());
18833                    let result = v
18834                        .into_par_iter()
18835                        .fold(
18836                            || preduce_init_fold_identity(&init),
18837                            |acc, item| {
18838                                pmap_progress.tick();
18839                                fold_preduce_init_step(&subs, cap, &block, acc, item)
18840                            },
18841                        )
18842                        .reduce(
18843                            || preduce_init_fold_identity(&init),
18844                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
18845                        );
18846                    pmap_progress.finish();
18847                    return Ok(result);
18848                }
18849                PipelineOp::PMapReduce {
18850                    map,
18851                    reduce,
18852                    progress,
18853                } => {
18854                    if v.is_empty() {
18855                        return Ok(StrykeValue::UNDEF);
18856                    }
18857                    let map_block = map.body.clone();
18858                    let reduce_block = reduce.body.clone();
18859                    let subs = self.subs.clone();
18860                    let scope_capture = self.scope.capture();
18861                    if v.len() == 1 {
18862                        let mut local_interp = VMHelper::new();
18863                        local_interp.subs = subs.clone();
18864                        local_interp.scope.restore_capture(&scope_capture);
18865                        local_interp.scope.set_topic(v[0].clone());
18866                        return match local_interp.exec_block_no_scope(&map_block) {
18867                            Ok(val) => Ok(val),
18868                            Err(_) => Ok(StrykeValue::UNDEF),
18869                        };
18870                    }
18871                    let pmap_progress = PmapProgress::new(progress, v.len());
18872                    let result = v
18873                        .into_par_iter()
18874                        .map(|item| {
18875                            let mut local_interp = VMHelper::new();
18876                            local_interp.subs = subs.clone();
18877                            local_interp.scope.restore_capture(&scope_capture);
18878                            local_interp.scope.set_topic(item);
18879                            let val = match local_interp.exec_block_no_scope(&map_block) {
18880                                Ok(val) => val,
18881                                Err(_) => StrykeValue::UNDEF,
18882                            };
18883                            pmap_progress.tick();
18884                            val
18885                        })
18886                        .reduce_with(|a, b| {
18887                            let mut local_interp = VMHelper::new();
18888                            local_interp.subs = subs.clone();
18889                            local_interp.scope.restore_capture(&scope_capture);
18890                            local_interp.scope.set_sort_pair(a, b);
18891                            match local_interp.exec_block_no_scope(&reduce_block) {
18892                                Ok(val) => val,
18893                                Err(_) => StrykeValue::UNDEF,
18894                            }
18895                        });
18896                    pmap_progress.finish();
18897                    return Ok(result.unwrap_or(StrykeValue::UNDEF));
18898                }
18899            }
18900        }
18901        Ok(StrykeValue::array(v))
18902    }
18903
18904    /// Streaming collect: wire pipeline ops through bounded channels so items flow
18905    /// between stages concurrently.  Order is **not** preserved.
18906    fn pipeline_collect_streaming(
18907        &mut self,
18908        source: Vec<StrykeValue>,
18909        ops: &[PipelineOp],
18910        workers_per_stage: usize,
18911        buffer: usize,
18912        line: usize,
18913    ) -> StrykeResult<StrykeValue> {
18914        use crossbeam::channel::{bounded, Receiver, Sender};
18915
18916        // Validate: reject ops that require all items (can't stream).
18917        for op in ops {
18918            match op {
18919                PipelineOp::PSort { .. }
18920                | PipelineOp::PReduce { .. }
18921                | PipelineOp::PReduceInit { .. }
18922                | PipelineOp::PMapReduce { .. }
18923                | PipelineOp::PMapChunked { .. } => {
18924                    return Err(StrykeError::runtime(
18925                        format!(
18926                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
18927                            std::mem::discriminant(op)
18928                        ),
18929                        line,
18930                    ));
18931                }
18932                _ => {}
18933            }
18934        }
18935
18936        // Filter out non-streamable ops and collect streamable ones.
18937        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
18938        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
18939        if streamable_ops.is_empty() {
18940            return Ok(StrykeValue::array(source));
18941        }
18942
18943        let n_stages = streamable_ops.len();
18944        let wn = if workers_per_stage > 0 {
18945            workers_per_stage
18946        } else {
18947            self.parallel_thread_count()
18948        };
18949        let subs = self.subs.clone();
18950        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18951
18952        // Build channels: one between each pair of stages, plus one for output.
18953        // channel[0]: source → stage 0
18954        // channel[i]: stage i-1 → stage i
18955        // channel[n_stages]: stage n_stages-1 → collector
18956        let mut channels: Vec<(Sender<StrykeValue>, Receiver<StrykeValue>)> =
18957            (0..=n_stages).map(|_| bounded(buffer)).collect();
18958
18959        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
18960        let take_done: Arc<std::sync::atomic::AtomicBool> =
18961            Arc::new(std::sync::atomic::AtomicBool::new(false));
18962
18963        // Collect senders/receivers for each stage.
18964        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
18965        let source_tx = channels[0].0.clone();
18966        let result_rx = channels[n_stages].1.clone();
18967        let results: Arc<Mutex<Vec<StrykeValue>>> = Arc::new(Mutex::new(Vec::new()));
18968
18969        std::thread::scope(|scope| {
18970            // Collector thread: drain results concurrently to avoid deadlock
18971            // when bounded channels fill up.
18972            let result_rx_c = result_rx.clone();
18973            let results_c = Arc::clone(&results);
18974            scope.spawn(move || {
18975                while let Ok(item) = result_rx_c.recv() {
18976                    results_c.lock().push(item);
18977                }
18978            });
18979
18980            // Source feeder thread.
18981            let err_s = Arc::clone(&err);
18982            let take_done_s = Arc::clone(&take_done);
18983            scope.spawn(move || {
18984                for item in source {
18985                    if err_s.lock().is_some()
18986                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
18987                    {
18988                        break;
18989                    }
18990                    if source_tx.send(item).is_err() {
18991                        break;
18992                    }
18993                }
18994            });
18995
18996            // Spawn workers for each stage.
18997            for (stage_idx, op) in streamable_ops.iter().enumerate() {
18998                let rx = channels[stage_idx].1.clone();
18999                let tx = channels[stage_idx + 1].0.clone();
19000
19001                for _ in 0..wn {
19002                    let rx = rx.clone();
19003                    let tx = tx.clone();
19004                    let subs = subs.clone();
19005                    let capture = capture.clone();
19006                    let atomic_arrays = atomic_arrays.clone();
19007                    let atomic_hashes = atomic_hashes.clone();
19008                    let err_w = Arc::clone(&err);
19009                    let take_done_w = Arc::clone(&take_done);
19010
19011                    match *op {
19012                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
19013                            let sub = Arc::clone(sub);
19014                            scope.spawn(move || {
19015                                while let Ok(item) = rx.recv() {
19016                                    if err_w.lock().is_some() {
19017                                        break;
19018                                    }
19019                                    let mut interp = VMHelper::new();
19020                                    interp.subs = subs.clone();
19021                                    interp.scope.restore_capture(&capture);
19022                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
19023                                    interp.enable_parallel_guard();
19024                                    interp.scope.set_topic(item.clone());
19025                                    interp.scope_push_hook();
19026                                    let keep = match interp.exec_block_no_scope(&sub.body) {
19027                                        Ok(val) => val.is_true(),
19028                                        Err(_) => false,
19029                                    };
19030                                    interp.scope_pop_hook();
19031                                    if keep && tx.send(item).is_err() {
19032                                        break;
19033                                    }
19034                                }
19035                            });
19036                        }
19037                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
19038                            let sub = Arc::clone(sub);
19039                            scope.spawn(move || {
19040                                while let Ok(item) = rx.recv() {
19041                                    if err_w.lock().is_some() {
19042                                        break;
19043                                    }
19044                                    let mut interp = VMHelper::new();
19045                                    interp.subs = subs.clone();
19046                                    interp.scope.restore_capture(&capture);
19047                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
19048                                    interp.enable_parallel_guard();
19049                                    interp.scope.set_topic(item);
19050                                    interp.scope_push_hook();
19051                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
19052                                        Ok(val) => val,
19053                                        Err(_) => StrykeValue::UNDEF,
19054                                    };
19055                                    interp.scope_pop_hook();
19056                                    if tx.send(mapped).is_err() {
19057                                        break;
19058                                    }
19059                                }
19060                            });
19061                        }
19062                        PipelineOp::Take(n) => {
19063                            let limit = (*n).max(0) as usize;
19064                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
19065                            let count_w = Arc::clone(&count);
19066                            scope.spawn(move || {
19067                                while let Ok(item) = rx.recv() {
19068                                    let prev =
19069                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
19070                                    if prev >= limit {
19071                                        take_done_w
19072                                            .store(true, std::sync::atomic::Ordering::Relaxed);
19073                                        break;
19074                                    }
19075                                    if tx.send(item).is_err() {
19076                                        break;
19077                                    }
19078                                }
19079                            });
19080                            // Take only needs 1 worker; skip remaining worker spawns.
19081                            break;
19082                        }
19083                        PipelineOp::PFor { ref sub, .. } => {
19084                            let sub = Arc::clone(sub);
19085                            scope.spawn(move || {
19086                                while let Ok(item) = rx.recv() {
19087                                    if err_w.lock().is_some() {
19088                                        break;
19089                                    }
19090                                    let mut interp = VMHelper::new();
19091                                    interp.subs = subs.clone();
19092                                    interp.scope.restore_capture(&capture);
19093                                    interp
19094                                        .scope
19095                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
19096                                    interp.enable_parallel_guard();
19097                                    interp.scope.set_topic(item.clone());
19098                                    interp.scope_push_hook();
19099                                    match interp.exec_block_no_scope(&sub.body) {
19100                                        Ok(_) => {}
19101                                        Err(e) => {
19102                                            let msg = match e {
19103                                                FlowOrError::Error(stryke) => stryke.to_string(),
19104                                                FlowOrError::Flow(_) => {
19105                                                    "unexpected control flow in par_pipeline_stream pfor".into()
19106                                                }
19107                                            };
19108                                            let mut g = err_w.lock();
19109                                            if g.is_none() {
19110                                                *g = Some(msg);
19111                                            }
19112                                            interp.scope_pop_hook();
19113                                            break;
19114                                        }
19115                                    }
19116                                    interp.scope_pop_hook();
19117                                    if tx.send(item).is_err() {
19118                                        break;
19119                                    }
19120                                }
19121                            });
19122                        }
19123                        PipelineOp::Tap(ref sub) => {
19124                            let sub = Arc::clone(sub);
19125                            scope.spawn(move || {
19126                                while let Ok(item) = rx.recv() {
19127                                    if err_w.lock().is_some() {
19128                                        break;
19129                                    }
19130                                    let mut interp = VMHelper::new();
19131                                    interp.subs = subs.clone();
19132                                    interp.scope.restore_capture(&capture);
19133                                    interp
19134                                        .scope
19135                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
19136                                    interp.enable_parallel_guard();
19137                                    match interp.call_sub(
19138                                        &sub,
19139                                        vec![item.clone()],
19140                                        WantarrayCtx::Void,
19141                                        line,
19142                                    )
19143                                    {
19144                                        Ok(_) => {}
19145                                        Err(e) => {
19146                                            let msg = match e {
19147                                                FlowOrError::Error(stryke) => stryke.to_string(),
19148                                                FlowOrError::Flow(_) => {
19149                                                    "unexpected control flow in par_pipeline_stream tap"
19150                                                        .into()
19151                                                }
19152                                            };
19153                                            let mut g = err_w.lock();
19154                                            if g.is_none() {
19155                                                *g = Some(msg);
19156                                            }
19157                                            break;
19158                                        }
19159                                    }
19160                                    if tx.send(item).is_err() {
19161                                        break;
19162                                    }
19163                                }
19164                            });
19165                        }
19166                        PipelineOp::PCache { ref sub, .. } => {
19167                            let sub = Arc::clone(sub);
19168                            scope.spawn(move || {
19169                                while let Ok(item) = rx.recv() {
19170                                    if err_w.lock().is_some() {
19171                                        break;
19172                                    }
19173                                    let k = crate::pcache::cache_key(&item);
19174                                    let val = if let Some(cached) =
19175                                        crate::pcache::GLOBAL_PCACHE.get(&k)
19176                                    {
19177                                        cached.clone()
19178                                    } else {
19179                                        let mut interp = VMHelper::new();
19180                                        interp.subs = subs.clone();
19181                                        interp.scope.restore_capture(&capture);
19182                                        interp
19183                                            .scope
19184                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
19185                                        interp.enable_parallel_guard();
19186                                        interp.scope.set_topic(item);
19187                                        interp.scope_push_hook();
19188                                        let v = match interp.exec_block_no_scope(&sub.body) {
19189                                            Ok(v) => v,
19190                                            Err(_) => StrykeValue::UNDEF,
19191                                        };
19192                                        interp.scope_pop_hook();
19193                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
19194                                        v
19195                                    };
19196                                    if tx.send(val).is_err() {
19197                                        break;
19198                                    }
19199                                }
19200                            });
19201                        }
19202                        // Non-streaming ops already rejected above.
19203                        _ => unreachable!(),
19204                    }
19205                }
19206            }
19207
19208            // Drop our copies of intermediate senders/receivers so channels disconnect
19209            // when workers finish.  Also drop result_rx so the collector thread exits
19210            // once all stage workers are done.
19211            channels.clear();
19212            drop(result_rx);
19213        });
19214
19215        if let Some(msg) = err.lock().take() {
19216            return Err(StrykeError::runtime(msg, line));
19217        }
19218
19219        let results = std::mem::take(&mut *results.lock());
19220        Ok(StrykeValue::array(results))
19221    }
19222
19223    fn heap_compare(&mut self, cmp: &Arc<StrykeSub>, a: &StrykeValue, b: &StrykeValue) -> Ordering {
19224        self.scope_push_hook();
19225        if let Some(ref env) = cmp.closure_env {
19226            self.scope.restore_capture(env);
19227        }
19228        self.scope.set_sort_pair(a.clone(), b.clone());
19229        let ord = match self.exec_block_no_scope(&cmp.body) {
19230            Ok(v) => {
19231                let n = v.to_int();
19232                if n < 0 {
19233                    Ordering::Less
19234                } else if n > 0 {
19235                    Ordering::Greater
19236                } else {
19237                    Ordering::Equal
19238                }
19239            }
19240            Err(_) => Ordering::Equal,
19241        };
19242        self.scope_pop_hook();
19243        ord
19244    }
19245
19246    fn heap_sift_up(&mut self, items: &mut [StrykeValue], cmp: &Arc<StrykeSub>, mut i: usize) {
19247        while i > 0 {
19248            let p = (i - 1) / 2;
19249            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
19250                break;
19251            }
19252            items.swap(i, p);
19253            i = p;
19254        }
19255    }
19256
19257    fn heap_sift_down(&mut self, items: &mut [StrykeValue], cmp: &Arc<StrykeSub>, mut i: usize) {
19258        let n = items.len();
19259        loop {
19260            let mut sm = i;
19261            let l = 2 * i + 1;
19262            let r = 2 * i + 2;
19263            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
19264                sm = l;
19265            }
19266            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
19267                sm = r;
19268            }
19269            if sm == i {
19270                break;
19271            }
19272            items.swap(i, sm);
19273            i = sm;
19274        }
19275    }
19276
19277    fn hash_for_signature_destruct(
19278        &mut self,
19279        v: &StrykeValue,
19280        line: usize,
19281    ) -> StrykeResult<IndexMap<String, StrykeValue>> {
19282        let Some(m) = self.match_subject_as_hash(v) else {
19283            return Err(StrykeError::runtime(
19284                format!(
19285                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
19286                    v.ref_type()
19287                ),
19288                line,
19289            ));
19290        };
19291        Ok(m)
19292    }
19293
19294    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
19295    pub(crate) fn apply_sub_signature(
19296        &mut self,
19297        sub: &StrykeSub,
19298        argv: &[StrykeValue],
19299        line: usize,
19300    ) -> StrykeResult<()> {
19301        if sub.params.is_empty() {
19302            return Ok(());
19303        }
19304        let mut i = 0usize;
19305        for p in &sub.params {
19306            match p {
19307                SubSigParam::Scalar(name, ty, default) => {
19308                    let val = if i < argv.len() {
19309                        argv[i].clone()
19310                    } else if let Some(default_expr) = default {
19311                        match self.eval_expr(default_expr) {
19312                            Ok(v) => v,
19313                            Err(FlowOrError::Error(e)) => return Err(e),
19314                            Err(FlowOrError::Flow(_)) => {
19315                                return Err(StrykeError::runtime(
19316                                    "unexpected control flow in parameter default",
19317                                    line,
19318                                ))
19319                            }
19320                        }
19321                    } else {
19322                        StrykeValue::UNDEF
19323                    };
19324                    i += 1;
19325                    if let Some(t) = ty {
19326                        if let Err(e) = t.check_value(&val) {
19327                            return Err(StrykeError::runtime(
19328                                format!("sub parameter ${}: {}", name, e),
19329                                line,
19330                            ));
19331                        }
19332                    }
19333                    let n = self.english_scalar_name(name);
19334                    self.scope.declare_scalar(n, val);
19335                }
19336                SubSigParam::Array(name, default) => {
19337                    let rest: Vec<StrykeValue> = if i < argv.len() {
19338                        let r = argv[i..].to_vec();
19339                        i = argv.len();
19340                        r
19341                    } else if let Some(default_expr) = default {
19342                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19343                            Ok(v) => v,
19344                            Err(FlowOrError::Error(e)) => return Err(e),
19345                            Err(FlowOrError::Flow(_)) => {
19346                                return Err(StrykeError::runtime(
19347                                    "unexpected control flow in parameter default",
19348                                    line,
19349                                ))
19350                            }
19351                        };
19352                        val.to_list()
19353                    } else {
19354                        vec![]
19355                    };
19356                    let aname = self.stash_array_name_for_package(name);
19357                    self.scope.declare_array(&aname, rest);
19358                }
19359                SubSigParam::Hash(name, default) => {
19360                    let rest: Vec<StrykeValue> = if i < argv.len() {
19361                        let r = argv[i..].to_vec();
19362                        i = argv.len();
19363                        r
19364                    } else if let Some(default_expr) = default {
19365                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19366                            Ok(v) => v,
19367                            Err(FlowOrError::Error(e)) => return Err(e),
19368                            Err(FlowOrError::Flow(_)) => {
19369                                return Err(StrykeError::runtime(
19370                                    "unexpected control flow in parameter default",
19371                                    line,
19372                                ))
19373                            }
19374                        };
19375                        val.to_list()
19376                    } else {
19377                        vec![]
19378                    };
19379                    let mut map = IndexMap::new();
19380                    let mut j = 0;
19381                    while j + 1 < rest.len() {
19382                        map.insert(rest[j].to_string(), rest[j + 1].clone());
19383                        j += 2;
19384                    }
19385                    self.scope.declare_hash(name, map);
19386                }
19387                SubSigParam::ArrayDestruct(elems) => {
19388                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19389                    i += 1;
19390                    let Some(arr) = self.match_subject_as_array(&arg) else {
19391                        return Err(StrykeError::runtime(
19392                            format!(
19393                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
19394                                arg.ref_type()
19395                            ),
19396                            line,
19397                        ));
19398                    };
19399                    let binds = self
19400                        .match_array_pattern_elems(&arr, elems, line)
19401                        .map_err(|e| match e {
19402                            FlowOrError::Error(stryke) => stryke,
19403                            FlowOrError::Flow(_) => StrykeError::runtime(
19404                                "unexpected flow in sub signature array destruct",
19405                                line,
19406                            ),
19407                        })?;
19408                    let Some(binds) = binds else {
19409                        return Err(StrykeError::runtime(
19410                            "sub signature array destruct: length or element mismatch",
19411                            line,
19412                        ));
19413                    };
19414                    for b in binds {
19415                        match b {
19416                            PatternBinding::Scalar(name, v) => {
19417                                let n = self.english_scalar_name(&name);
19418                                self.scope.declare_scalar(n, v);
19419                            }
19420                            PatternBinding::Array(name, elems) => {
19421                                self.scope.declare_array(&name, elems);
19422                            }
19423                        }
19424                    }
19425                }
19426                SubSigParam::HashDestruct(pairs) => {
19427                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19428                    i += 1;
19429                    let map = self.hash_for_signature_destruct(&arg, line)?;
19430                    for (key, varname) in pairs {
19431                        let v = map.get(key).cloned().unwrap_or(StrykeValue::UNDEF);
19432                        let n = self.english_scalar_name(varname);
19433                        self.scope.declare_scalar(n, v);
19434                    }
19435                }
19436            }
19437        }
19438        Ok(())
19439    }
19440
19441    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
19442    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
19443    /// These are `StrykeSub`s with empty bodies and magic keys in `closure_env`.
19444    pub(crate) fn try_hof_dispatch(
19445        &mut self,
19446        sub: &StrykeSub,
19447        args: &[StrykeValue],
19448        want: WantarrayCtx,
19449        line: usize,
19450    ) -> Option<ExecResult> {
19451        let env = sub.closure_env.as_ref()?;
19452        fn env_get<'a>(env: &'a [(String, StrykeValue)], key: &str) -> Option<&'a StrykeValue> {
19453            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
19454        }
19455
19456        match sub.name.as_str() {
19457            // ── compose: right-to-left function application ──
19458            "__comp__" => {
19459                let fns = env_get(env, "__comp_fns__")?.to_list();
19460                let mut val = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
19461                for f in fns.iter().rev() {
19462                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
19463                        Ok(v) => val = v,
19464                        Err(e) => return Some(Err(e)),
19465                    }
19466                }
19467                Some(Ok(val))
19468            }
19469            // ── constantly: always return the captured value ──
19470            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
19471            // ── juxt: call each fn with same args, collect results ──
19472            "__juxt__" => {
19473                let fns = env_get(env, "__juxt_fns__")?.to_list();
19474                let mut results = Vec::with_capacity(fns.len());
19475                for f in &fns {
19476                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
19477                        Ok(v) => results.push(v),
19478                        Err(e) => return Some(Err(e)),
19479                    }
19480                }
19481                Some(Ok(StrykeValue::array(results)))
19482            }
19483            // ── partial: prepend bound args ──
19484            "__partial__" => {
19485                let fn_val = env_get(env, "__partial_fn__")?.clone();
19486                let bound = env_get(env, "__partial_args__")?.to_list();
19487                let mut all_args = bound;
19488                all_args.extend_from_slice(args);
19489                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
19490            }
19491            // ── complement: negate the result ──
19492            "__complement__" => {
19493                let fn_val = env_get(env, "__complement_fn__")?.clone();
19494                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19495                    Ok(v) => Some(Ok(StrykeValue::integer(if v.is_true() { 0 } else { 1 }))),
19496                    Err(e) => Some(Err(e)),
19497                }
19498            }
19499            // ── fnil: replace undef args with defaults ──
19500            "__fnil__" => {
19501                let fn_val = env_get(env, "__fnil_fn__")?.clone();
19502                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
19503                let mut patched = args.to_vec();
19504                for (i, d) in defaults.iter().enumerate() {
19505                    if i < patched.len() {
19506                        if patched[i].is_undef() {
19507                            patched[i] = d.clone();
19508                        }
19509                    } else {
19510                        patched.push(d.clone());
19511                    }
19512                }
19513                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
19514            }
19515            // ── memoize: cache by stringified args ──
19516            "__memoize__" => {
19517                let fn_val = env_get(env, "__memoize_fn__")?.clone();
19518                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
19519                let key = args
19520                    .iter()
19521                    .map(|a| a.to_string())
19522                    .collect::<Vec<_>>()
19523                    .join("\x00");
19524                if let Some(href) = cache_ref.as_hash_ref() {
19525                    if let Some(cached) = href.read().get(&key) {
19526                        return Some(Ok(cached.clone()));
19527                    }
19528                }
19529                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19530                    Ok(v) => {
19531                        if let Some(href) = cache_ref.as_hash_ref() {
19532                            href.write().insert(key, v.clone());
19533                        }
19534                        Some(Ok(v))
19535                    }
19536                    Err(e) => Some(Err(e)),
19537                }
19538            }
19539            // ── curry: accumulate args until arity reached ──
19540            "__curry__" => {
19541                let fn_val = env_get(env, "__curry_fn__")?.clone();
19542                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
19543                let bound = env_get(env, "__curry_bound__")?.to_list();
19544                let mut all = bound;
19545                all.extend_from_slice(args);
19546                if all.len() >= arity {
19547                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
19548                } else {
19549                    let curry_sub = StrykeSub {
19550                        name: "__curry__".to_string(),
19551                        params: vec![],
19552                        body: vec![],
19553                        closure_env: Some(vec![
19554                            ("__curry_fn__".to_string(), fn_val),
19555                            (
19556                                "__curry_arity__".to_string(),
19557                                StrykeValue::integer(arity as i64),
19558                            ),
19559                            ("__curry_bound__".to_string(), StrykeValue::array(all)),
19560                        ]),
19561                        prototype: None,
19562                        fib_like: None,
19563                    };
19564                    Some(Ok(StrykeValue::code_ref(Arc::new(curry_sub))))
19565                }
19566            }
19567            // ── once: call once, cache forever ──
19568            "__once__" => {
19569                let cache_ref = env_get(env, "__once_cache__")?.clone();
19570                if let Some(href) = cache_ref.as_hash_ref() {
19571                    let r = href.read();
19572                    if r.contains_key("done") {
19573                        return Some(Ok(r.get("val").cloned().unwrap_or(StrykeValue::UNDEF)));
19574                    }
19575                }
19576                let fn_val = env_get(env, "__once_fn__")?.clone();
19577                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19578                    Ok(v) => {
19579                        if let Some(href) = cache_ref.as_hash_ref() {
19580                            let mut w = href.write();
19581                            w.insert("done".to_string(), StrykeValue::integer(1));
19582                            w.insert("val".to_string(), v.clone());
19583                        }
19584                        Some(Ok(v))
19585                    }
19586                    Err(e) => Some(Err(e)),
19587                }
19588            }
19589            _ => None,
19590        }
19591    }
19592
19593    pub(crate) fn call_sub(
19594        &mut self,
19595        sub: &StrykeSub,
19596        args: Vec<StrykeValue>,
19597        want: WantarrayCtx,
19598        line: usize,
19599    ) -> ExecResult {
19600        // Default path: derive the package from `sub.name` if it is qualified. Bare-named
19601        // subs (registered without a `Pkg::` prefix) leave `__PACKAGE__` untouched.
19602        let pkg = sub.name.rsplit_once("::").map(|(p, _)| p.to_string());
19603        self.call_sub_with_package(sub, args, want, line, pkg)
19604    }
19605
19606    /// Internal helper: like [`Self::call_sub`] but takes an explicit home-package override
19607    /// (used by [`Self::call_named_sub`], which knows the qualified registry key even when
19608    /// the cached `StrykeSub.name` is bare).
19609    fn call_sub_with_package(
19610        &mut self,
19611        sub: &StrykeSub,
19612        args: Vec<StrykeValue>,
19613        want: WantarrayCtx,
19614        _line: usize,
19615        home_package: Option<String>,
19616    ) -> ExecResult {
19617        // Push current sub for __SUB__ access
19618        self.current_sub_stack.push(Arc::new(sub.clone()));
19619
19620        // Single frame for both @_ and the block's local variables —
19621        // avoids the double push_frame/pop_frame overhead per call.
19622        self.scope_push_hook();
19623        self.scope.declare_array("_", args.clone());
19624        if let Some(ref env) = sub.closure_env {
19625            self.scope.restore_capture(env);
19626        }
19627        // Switch `__PACKAGE__` to the sub's home package so cross-package `our`/`oursync`
19628        // qualifies correctly inside the body. Bytecode VM rewrites at compile time so it
19629        // never needed this; the tree walker (used by parallel workers) does need it.
19630        // Goes AFTER restore_capture so the closure's captured `__PACKAGE__` doesn't
19631        // overwrite our home-package switch.
19632        if let Some(pkg) = home_package {
19633            self.scope
19634                .declare_scalar("__PACKAGE__", StrykeValue::string(pkg));
19635        }
19636        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
19637        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
19638        // Must be AFTER restore_capture so we don't get shadowed by captured $_
19639        self.scope.set_closure_args(&args);
19640        // Move `@_` out so `fib_like` / hof dispatch take `&[StrykeValue]` without cloning.
19641        let argv = self.scope.take_sub_underscore().unwrap_or_default();
19642        self.apply_sub_signature(sub, &argv, _line)?;
19643        let saved = self.wantarray_kind;
19644        self.wantarray_kind = want;
19645        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
19646            self.wantarray_kind = saved;
19647            self.scope_pop_hook();
19648            self.current_sub_stack.pop();
19649            return match r {
19650                Ok(v) => Ok(v),
19651                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19652                Err(e) => Err(e),
19653            };
19654        }
19655        if let Some(pat) = sub.fib_like.as_ref() {
19656            if argv.len() == 1 {
19657                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
19658                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
19659                    if let Some(p) = &mut self.profiler {
19660                        p.enter_sub(&sub.name);
19661                    }
19662                    self.debugger_enter_sub(&sub.name);
19663                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
19664                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
19665                        p.exit_sub(t0.elapsed());
19666                    }
19667                    self.debugger_leave_sub();
19668                    self.wantarray_kind = saved;
19669                    self.scope_pop_hook();
19670                    self.current_sub_stack.pop();
19671                    return Ok(StrykeValue::integer(n));
19672                }
19673            }
19674        }
19675        self.scope.declare_array("_", argv.clone());
19676        // Note: set_closure_args was already called at line 15077; don't call it again
19677        // as that would incorrectly shift the outer topic stack a second time.
19678        let t0 = self.profiler.is_some().then(std::time::Instant::now);
19679        if let Some(p) = &mut self.profiler {
19680            p.enter_sub(&sub.name);
19681        }
19682        self.debugger_enter_sub(&sub.name);
19683        // Always evaluate the function body's last expression in List context so
19684        // `@array` returns the array contents, not the count. The caller adapts the
19685        // return value to their own wantarray context after receiving it.
19686        let result = self.exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List);
19687        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
19688            p.exit_sub(t0.elapsed());
19689        }
19690        self.debugger_leave_sub();
19691        // For goto &sub, capture @_ before popping the frame
19692        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
19693            Some(self.scope.get_array("_"))
19694        } else {
19695            None
19696        };
19697        self.wantarray_kind = saved;
19698        self.scope_pop_hook();
19699        self.current_sub_stack.pop();
19700        match result {
19701            Ok(v) => Ok(v),
19702            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19703            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
19704                // goto &sub — tail call: look up target and call with same @_
19705                let goto_args = goto_args.unwrap_or_default();
19706                let fqn = if target_name.contains("::") {
19707                    target_name.clone()
19708                } else {
19709                    format!("{}::{}", self.current_package(), target_name)
19710                };
19711                if let Some(target_sub) = self
19712                    .subs
19713                    .get(&fqn)
19714                    .cloned()
19715                    .or_else(|| self.subs.get(&target_name).cloned())
19716                {
19717                    self.call_sub(&target_sub, goto_args, want, _line)
19718                } else {
19719                    Err(StrykeError::runtime(
19720                        format!("Undefined subroutine &{}", target_name),
19721                        _line,
19722                    )
19723                    .into())
19724                }
19725            }
19726            Err(FlowOrError::Flow(Flow::Yield(_))) => {
19727                Err(StrykeError::runtime("yield is only valid inside gen { }", 0).into())
19728            }
19729            Err(e) => Err(e),
19730        }
19731    }
19732
19733    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
19734    fn call_struct_method(
19735        &mut self,
19736        body: &Block,
19737        params: &[SubSigParam],
19738        args: Vec<StrykeValue>,
19739        line: usize,
19740    ) -> ExecResult {
19741        self.scope_push_hook();
19742        self.scope.declare_array("_", args.clone());
19743        // Bind $self to first arg (the receiver)
19744        if let Some(self_val) = args.first() {
19745            self.scope.declare_scalar("self", self_val.clone());
19746        }
19747        // Set $_0, $_1, etc. for the EXPLICIT args (skip $self at args[0]) so
19748        // `fn tom { _ * 2 }; obj->tom(99)` works identically to the standalone
19749        // `fn tom { _ * 2 }; tom(99)` — both treat the first EXPLICIT arg as
19750        // the topic. `$self` stays accessible via the dedicated `$self` binding
19751        // above and via `$_[0]` (full `@_` retained).
19752        if args.len() > 1 {
19753            self.scope.set_closure_args(&args[1..]);
19754        }
19755        // Apply signature if provided - skip the first arg ($self) for user params
19756        let user_args: Vec<StrykeValue> = args.iter().skip(1).cloned().collect();
19757        self.apply_params_to_argv(params, &user_args, line)?;
19758        let result = self.exec_block_no_scope(body);
19759        self.scope_pop_hook();
19760        match result {
19761            Ok(v) => Ok(v),
19762            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19763            Err(e) => Err(e),
19764        }
19765    }
19766
19767    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
19768    pub(crate) fn call_class_method(
19769        &mut self,
19770        body: &Block,
19771        params: &[SubSigParam],
19772        args: Vec<StrykeValue>,
19773        line: usize,
19774    ) -> ExecResult {
19775        self.call_class_method_inner(body, params, args, line, false)
19776    }
19777
19778    /// Call a static class method: `Math::add(...)`.
19779    pub(crate) fn call_static_class_method(
19780        &mut self,
19781        body: &Block,
19782        params: &[SubSigParam],
19783        args: Vec<StrykeValue>,
19784        line: usize,
19785    ) -> ExecResult {
19786        self.call_class_method_inner(body, params, args, line, true)
19787    }
19788
19789    fn call_class_method_inner(
19790        &mut self,
19791        body: &Block,
19792        params: &[SubSigParam],
19793        args: Vec<StrykeValue>,
19794        line: usize,
19795        is_static: bool,
19796    ) -> ExecResult {
19797        self.scope_push_hook();
19798        self.scope.declare_array("_", args.clone());
19799        if !is_static {
19800            // Bind $self to first arg (the receiver) for instance methods
19801            if let Some(self_val) = args.first() {
19802                self.scope.declare_scalar("self", self_val.clone());
19803            }
19804        }
19805        // Set $_0, $_1, etc. for the EXPLICIT args. For instance methods skip
19806        // args[0] (which is $self) so `fn tom { _ * 2 }; obj->tom(99)` behaves
19807        // the same as `fn tom { _ * 2 }; tom(99)` — both treat the first
19808        // EXPLICIT arg as the topic. `$self` is still accessible via the
19809        // dedicated `$self` binding above and via `$_[0]` (full `@_` retained).
19810        // Static methods have no `$self`, so the full args list IS the topic.
19811        if is_static {
19812            self.scope.set_closure_args(&args);
19813        } else if args.len() > 1 {
19814            self.scope.set_closure_args(&args[1..]);
19815        }
19816        // Apply signature: skip first arg ($self) only for instance methods
19817        let user_args: Vec<StrykeValue> = if is_static {
19818            args.clone()
19819        } else {
19820            args.iter().skip(1).cloned().collect()
19821        };
19822        self.apply_params_to_argv(params, &user_args, line)?;
19823        let result = self.exec_block_no_scope(body);
19824        self.scope_pop_hook();
19825        match result {
19826            Ok(v) => Ok(v),
19827            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19828            Err(e) => Err(e),
19829        }
19830    }
19831
19832    /// Apply SubSigParam bindings without the full StrykeSub machinery.
19833    fn apply_params_to_argv(
19834        &mut self,
19835        params: &[SubSigParam],
19836        argv: &[StrykeValue],
19837        line: usize,
19838    ) -> StrykeResult<()> {
19839        let mut i = 0;
19840        for param in params {
19841            match param {
19842                SubSigParam::Scalar(name, ty_opt, default) => {
19843                    let v = if i < argv.len() {
19844                        argv[i].clone()
19845                    } else if let Some(default_expr) = default {
19846                        match self.eval_expr(default_expr) {
19847                            Ok(v) => v,
19848                            Err(FlowOrError::Error(e)) => return Err(e),
19849                            Err(FlowOrError::Flow(_)) => {
19850                                return Err(StrykeError::runtime(
19851                                    "unexpected control flow in parameter default",
19852                                    line,
19853                                ))
19854                            }
19855                        }
19856                    } else {
19857                        StrykeValue::UNDEF
19858                    };
19859                    i += 1;
19860                    if let Some(ty) = ty_opt {
19861                        ty.check_value(&v).map_err(|msg| {
19862                            StrykeError::type_error(
19863                                format!("method parameter ${}: {}", name, msg),
19864                                line,
19865                            )
19866                        })?;
19867                    }
19868                    let n = self.english_scalar_name(name);
19869                    self.scope.declare_scalar(n, v);
19870                }
19871                SubSigParam::Array(name, default) => {
19872                    let rest: Vec<StrykeValue> = if i < argv.len() {
19873                        let r = argv[i..].to_vec();
19874                        i = argv.len();
19875                        r
19876                    } else if let Some(default_expr) = default {
19877                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19878                            Ok(v) => v,
19879                            Err(FlowOrError::Error(e)) => return Err(e),
19880                            Err(FlowOrError::Flow(_)) => {
19881                                return Err(StrykeError::runtime(
19882                                    "unexpected control flow in parameter default",
19883                                    line,
19884                                ))
19885                            }
19886                        };
19887                        val.to_list()
19888                    } else {
19889                        vec![]
19890                    };
19891                    let aname = self.stash_array_name_for_package(name);
19892                    self.scope.declare_array(&aname, rest);
19893                }
19894                SubSigParam::Hash(name, default) => {
19895                    let rest: Vec<StrykeValue> = if i < argv.len() {
19896                        let r = argv[i..].to_vec();
19897                        i = argv.len();
19898                        r
19899                    } else if let Some(default_expr) = default {
19900                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19901                            Ok(v) => v,
19902                            Err(FlowOrError::Error(e)) => return Err(e),
19903                            Err(FlowOrError::Flow(_)) => {
19904                                return Err(StrykeError::runtime(
19905                                    "unexpected control flow in parameter default",
19906                                    line,
19907                                ))
19908                            }
19909                        };
19910                        val.to_list()
19911                    } else {
19912                        vec![]
19913                    };
19914                    let mut map = IndexMap::new();
19915                    let mut j = 0;
19916                    while j + 1 < rest.len() {
19917                        map.insert(rest[j].to_string(), rest[j + 1].clone());
19918                        j += 2;
19919                    }
19920                    self.scope.declare_hash(name, map);
19921                }
19922                SubSigParam::ArrayDestruct(elems) => {
19923                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19924                    i += 1;
19925                    let Some(arr) = self.match_subject_as_array(&arg) else {
19926                        return Err(StrykeError::runtime(
19927                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
19928                            line,
19929                        ));
19930                    };
19931                    let binds = self
19932                        .match_array_pattern_elems(&arr, elems, line)
19933                        .map_err(|e| match e {
19934                            FlowOrError::Error(stryke) => stryke,
19935                            FlowOrError::Flow(_) => StrykeError::runtime(
19936                                "unexpected flow in method array destruct",
19937                                line,
19938                            ),
19939                        })?;
19940                    let Some(binds) = binds else {
19941                        return Err(StrykeError::runtime(
19942                            format!(
19943                                "method parameter: array destructure failed at position {}",
19944                                i
19945                            ),
19946                            line,
19947                        ));
19948                    };
19949                    for b in binds {
19950                        match b {
19951                            PatternBinding::Scalar(name, v) => {
19952                                let n = self.english_scalar_name(&name);
19953                                self.scope.declare_scalar(n, v);
19954                            }
19955                            PatternBinding::Array(name, elems) => {
19956                                self.scope.declare_array(&name, elems);
19957                            }
19958                        }
19959                    }
19960                }
19961                SubSigParam::HashDestruct(pairs) => {
19962                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19963                    i += 1;
19964                    let map = self.hash_for_signature_destruct(&arg, line)?;
19965                    for (key, varname) in pairs {
19966                        let v = map.get(key).cloned().unwrap_or(StrykeValue::UNDEF);
19967                        let n = self.english_scalar_name(varname);
19968                        self.scope.declare_scalar(n, v);
19969                    }
19970                }
19971            }
19972        }
19973        Ok(())
19974    }
19975
19976    fn builtin_new(&mut self, class: &str, args: Vec<StrykeValue>, line: usize) -> ExecResult {
19977        if class == "Set" {
19978            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
19979        }
19980        if let Some(def) = self.struct_defs.get(class).cloned() {
19981            let mut provided = Vec::new();
19982            let mut i = 1;
19983            while i + 1 < args.len() {
19984                let k = args[i].to_string();
19985                let v = args[i + 1].clone();
19986                provided.push((k, v));
19987                i += 2;
19988            }
19989            let mut defaults = Vec::with_capacity(def.fields.len());
19990            for field in &def.fields {
19991                if let Some(ref expr) = field.default {
19992                    let val = self.eval_expr(expr)?;
19993                    defaults.push(Some(val));
19994                } else {
19995                    defaults.push(None);
19996                }
19997            }
19998            return Ok(crate::native_data::struct_new_with_defaults(
19999                &def, &provided, &defaults, line,
20000            )?);
20001        }
20002        // Stryke `class` declarations route through `class_construct` so the
20003        // result is a real `ClassInstance` (typed-my checks, isa walk, BUILD
20004        // hooks, etc.). Without this, `Class->new` for a registered class
20005        // fell through to the default Perl-style blessed-hashref path,
20006        // breaking `typed my $x : Class = Class->new` even though the
20007        // runtime check for `Struct(name)` was already in place. Skip
20008        // `args[0]` (the class-name receiver) since `class_construct`
20009        // expects user args only.
20010        if let Some(def) = self.class_defs.get(class).cloned() {
20011            let user_args: Vec<StrykeValue> = args.into_iter().skip(1).collect();
20012            return self.class_construct(&def, user_args, line);
20013        }
20014        // Default OO constructor: Class->new(%args) → bless {%args}, class
20015        let mut map = IndexMap::new();
20016        let mut i = 1; // skip $self (first arg is class name)
20017        while i + 1 < args.len() {
20018            let k = args[i].to_string();
20019            let v = args[i + 1].clone();
20020            map.insert(k, v);
20021            i += 2;
20022        }
20023        Ok(StrykeValue::blessed(Arc::new(
20024            crate::value::BlessedRef::new_blessed(class.to_string(), StrykeValue::hash(map)),
20025        )))
20026    }
20027
20028    fn exec_print(
20029        &mut self,
20030        handle: Option<&str>,
20031        args: &[Expr],
20032        newline: bool,
20033        line: usize,
20034    ) -> ExecResult {
20035        if newline && (self.feature_bits & FEAT_SAY) == 0 {
20036            return Err(StrykeError::runtime(
20037                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
20038                line,
20039            )
20040            .into());
20041        }
20042        let mut output = String::new();
20043        if args.is_empty() {
20044            // Perl: print with no LIST prints $_ (same for say).
20045            let topic = self.scope.get_scalar("_").clone();
20046            let s = self.stringify_value(topic, line)?;
20047            output.push_str(&s);
20048        } else {
20049            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
20050            // between those top-level expressions only (not between elements of an expanded `@arr`).
20051            for (i, a) in args.iter().enumerate() {
20052                if i > 0 {
20053                    output.push_str(&self.ofs);
20054                }
20055                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
20056                for item in val.to_list() {
20057                    let s = self.stringify_value(item, line)?;
20058                    output.push_str(&s);
20059                }
20060            }
20061        }
20062        if newline {
20063            output.push('\n');
20064        }
20065        output.push_str(&self.ors);
20066
20067        let handle_name =
20068            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
20069        self.write_formatted_print(handle_name.as_str(), &output, line)?;
20070        Ok(StrykeValue::integer(1))
20071    }
20072
20073    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
20074        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
20075            // Perl: printf with no args uses $_ as the format string.
20076            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
20077            (s, &[])
20078        } else {
20079            (self.eval_expr(&args[0])?.to_string(), &args[1..])
20080        };
20081        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
20082        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
20083        // ranges to flip-flop values, so go through list-context eval and splat.
20084        let mut arg_vals = Vec::new();
20085        for a in rest {
20086            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
20087            if let Some(items) = v.as_array_vec() {
20088                arg_vals.extend(items);
20089            } else {
20090                arg_vals.push(v);
20091            }
20092        }
20093        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
20094        let handle_name =
20095            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
20096        match handle_name.as_str() {
20097            "STDOUT" => {
20098                if !self.suppress_stdout {
20099                    print!("{}", output);
20100                    if self.output_autoflush {
20101                        let _ = io::stdout().flush();
20102                    }
20103                }
20104            }
20105            "STDERR" => {
20106                eprint!("{}", output);
20107                let _ = io::stderr().flush();
20108            }
20109            name => {
20110                if let Some(writer) = self.output_handles.get_mut(name) {
20111                    let _ = writer.write_all(output.as_bytes());
20112                    if self.output_autoflush {
20113                        let _ = writer.flush();
20114                    }
20115                }
20116            }
20117        }
20118        Ok(StrykeValue::integer(1))
20119    }
20120
20121    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
20122    pub(crate) fn eval_substr_expr(
20123        &mut self,
20124        string: &Expr,
20125        offset: &Expr,
20126        length: Option<&Expr>,
20127        replacement: Option<&Expr>,
20128        _line: usize,
20129    ) -> Result<StrykeValue, FlowOrError> {
20130        let s = self.eval_expr(string)?.to_string();
20131        let off = self.eval_expr(offset)?.to_int();
20132        let start = if off < 0 {
20133            (s.len() as i64 + off).max(0) as usize
20134        } else {
20135            off as usize
20136        };
20137        let len = if let Some(l) = length {
20138            let len_val = self.eval_expr(l)?.to_int();
20139            if len_val < 0 {
20140                // Negative length: count from end of string
20141                let remaining = s.len().saturating_sub(start) as i64;
20142                (remaining + len_val).max(0) as usize
20143            } else {
20144                len_val as usize
20145            }
20146        } else {
20147            s.len().saturating_sub(start)
20148        };
20149        let end = start.saturating_add(len).min(s.len());
20150        let result = s.get(start..end).unwrap_or("").to_string();
20151        if let Some(rep) = replacement {
20152            let rep_s = self.eval_expr(rep)?.to_string();
20153            let mut new_s = String::new();
20154            new_s.push_str(&s[..start]);
20155            new_s.push_str(&rep_s);
20156            new_s.push_str(&s[end..]);
20157            self.assign_value(string, StrykeValue::string(new_s))?;
20158        }
20159        Ok(StrykeValue::string(result))
20160    }
20161
20162    pub(crate) fn eval_push_expr(
20163        &mut self,
20164        array: &Expr,
20165        values: &[Expr],
20166        line: usize,
20167    ) -> Result<StrykeValue, FlowOrError> {
20168        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20169            for v in values {
20170                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20171                self.push_array_deref_value(aref.clone(), val, line)?;
20172            }
20173            let len = self.array_deref_len(aref, line)?;
20174            return Ok(StrykeValue::integer(len));
20175        }
20176        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20177        if self.scope.is_array_frozen(&arr_name) {
20178            return Err(StrykeError::runtime(
20179                format!("Modification of a frozen value: @{}", arr_name),
20180                line,
20181            )
20182            .into());
20183        }
20184        for v in values {
20185            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20186            if let Some(items) = val.as_array_vec() {
20187                for item in items {
20188                    self.scope
20189                        .push_to_array(&arr_name, item)
20190                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20191                }
20192            } else {
20193                self.scope
20194                    .push_to_array(&arr_name, val)
20195                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20196            }
20197        }
20198        let len = self.scope.array_len(&arr_name);
20199        Ok(StrykeValue::integer(len as i64))
20200    }
20201
20202    pub(crate) fn eval_pop_expr(
20203        &mut self,
20204        array: &Expr,
20205        line: usize,
20206    ) -> Result<StrykeValue, FlowOrError> {
20207        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20208            return self.pop_array_deref(aref, line);
20209        }
20210        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20211        self.scope
20212            .pop_from_array(&arr_name)
20213            .map_err(|e| FlowOrError::Error(e.at_line(line)))
20214    }
20215
20216    pub(crate) fn eval_shift_expr(
20217        &mut self,
20218        array: &Expr,
20219        line: usize,
20220    ) -> Result<StrykeValue, FlowOrError> {
20221        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20222            return self.shift_array_deref(aref, line);
20223        }
20224        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20225        self.scope
20226            .shift_from_array(&arr_name)
20227            .map_err(|e| FlowOrError::Error(e.at_line(line)))
20228    }
20229
20230    pub(crate) fn eval_unshift_expr(
20231        &mut self,
20232        array: &Expr,
20233        values: &[Expr],
20234        line: usize,
20235    ) -> Result<StrykeValue, FlowOrError> {
20236        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20237            let mut vals = Vec::new();
20238            for v in values {
20239                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20240                if let Some(items) = val.as_array_vec() {
20241                    vals.extend(items);
20242                } else {
20243                    vals.push(val);
20244                }
20245            }
20246            let len = self.unshift_array_deref_multi(aref, vals, line)?;
20247            return Ok(StrykeValue::integer(len));
20248        }
20249        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20250        let mut vals = Vec::new();
20251        for v in values {
20252            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20253            if let Some(items) = val.as_array_vec() {
20254                vals.extend(items);
20255            } else {
20256                vals.push(val);
20257            }
20258        }
20259        let arr = self
20260            .scope
20261            .get_array_mut(&arr_name)
20262            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20263        for (i, v) in vals.into_iter().enumerate() {
20264            arr.insert(i, v);
20265        }
20266        let len = arr.len();
20267        Ok(StrykeValue::integer(len as i64))
20268    }
20269
20270    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
20271    pub(crate) fn push_array_deref_value(
20272        &mut self,
20273        arr_ref: StrykeValue,
20274        val: StrykeValue,
20275        line: usize,
20276    ) -> Result<(), FlowOrError> {
20277        // Resolve binding refs in the value being stored so they snapshot
20278        // the current scope data and survive scope pop.
20279        let val = self.scope.resolve_container_binding_ref(val);
20280        if let Some(r) = arr_ref.as_array_ref() {
20281            let mut w = r.write();
20282            if let Some(items) = val.as_array_vec() {
20283                w.extend(items.iter().cloned());
20284            } else {
20285                w.push(val);
20286            }
20287            return Ok(());
20288        }
20289        if let Some(name) = arr_ref.as_array_binding_name() {
20290            if let Some(items) = val.as_array_vec() {
20291                for item in items {
20292                    self.scope
20293                        .push_to_array(&name, item)
20294                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20295                }
20296            } else {
20297                self.scope
20298                    .push_to_array(&name, val)
20299                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20300            }
20301            return Ok(());
20302        }
20303        if let Some(s) = arr_ref.as_str() {
20304            if self.strict_refs {
20305                return Err(StrykeError::runtime(
20306                    format!(
20307                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20308                        s
20309                    ),
20310                    line,
20311                )
20312                .into());
20313            }
20314            let name = s.to_string();
20315            if let Some(items) = val.as_array_vec() {
20316                for item in items {
20317                    self.scope
20318                        .push_to_array(&name, item)
20319                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20320                }
20321            } else {
20322                self.scope
20323                    .push_to_array(&name, val)
20324                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20325            }
20326            return Ok(());
20327        }
20328        Err(StrykeError::runtime("push argument is not an ARRAY reference", line).into())
20329    }
20330
20331    pub(crate) fn array_deref_len(
20332        &self,
20333        arr_ref: StrykeValue,
20334        line: usize,
20335    ) -> Result<i64, FlowOrError> {
20336        if let Some(r) = arr_ref.as_array_ref() {
20337            return Ok(r.read().len() as i64);
20338        }
20339        if let Some(name) = arr_ref.as_array_binding_name() {
20340            return Ok(self.scope.array_len(&name) as i64);
20341        }
20342        if let Some(s) = arr_ref.as_str() {
20343            if self.strict_refs {
20344                return Err(StrykeError::runtime(
20345                    format!(
20346                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20347                        s
20348                    ),
20349                    line,
20350                )
20351                .into());
20352            }
20353            return Ok(self.scope.array_len(&s) as i64);
20354        }
20355        Err(StrykeError::runtime("argument is not an ARRAY reference", line).into())
20356    }
20357
20358    pub(crate) fn pop_array_deref(
20359        &mut self,
20360        arr_ref: StrykeValue,
20361        line: usize,
20362    ) -> Result<StrykeValue, FlowOrError> {
20363        if let Some(r) = arr_ref.as_array_ref() {
20364            let mut w = r.write();
20365            return Ok(w.pop().unwrap_or(StrykeValue::UNDEF));
20366        }
20367        if let Some(name) = arr_ref.as_array_binding_name() {
20368            return self
20369                .scope
20370                .pop_from_array(&name)
20371                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20372        }
20373        if let Some(s) = arr_ref.as_str() {
20374            if self.strict_refs {
20375                return Err(StrykeError::runtime(
20376                    format!(
20377                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20378                        s
20379                    ),
20380                    line,
20381                )
20382                .into());
20383            }
20384            return self
20385                .scope
20386                .pop_from_array(&s)
20387                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20388        }
20389        Err(StrykeError::runtime("pop argument is not an ARRAY reference", line).into())
20390    }
20391
20392    pub(crate) fn shift_array_deref(
20393        &mut self,
20394        arr_ref: StrykeValue,
20395        line: usize,
20396    ) -> Result<StrykeValue, FlowOrError> {
20397        if let Some(r) = arr_ref.as_array_ref() {
20398            let mut w = r.write();
20399            return Ok(if w.is_empty() {
20400                StrykeValue::UNDEF
20401            } else {
20402                w.remove(0)
20403            });
20404        }
20405        if let Some(name) = arr_ref.as_array_binding_name() {
20406            return self
20407                .scope
20408                .shift_from_array(&name)
20409                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20410        }
20411        if let Some(s) = arr_ref.as_str() {
20412            if self.strict_refs {
20413                return Err(StrykeError::runtime(
20414                    format!(
20415                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20416                        s
20417                    ),
20418                    line,
20419                )
20420                .into());
20421            }
20422            return self
20423                .scope
20424                .shift_from_array(&s)
20425                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20426        }
20427        Err(StrykeError::runtime("shift argument is not an ARRAY reference", line).into())
20428    }
20429
20430    pub(crate) fn unshift_array_deref_multi(
20431        &mut self,
20432        arr_ref: StrykeValue,
20433        vals: Vec<StrykeValue>,
20434        line: usize,
20435    ) -> Result<i64, FlowOrError> {
20436        let mut flat: Vec<StrykeValue> = Vec::new();
20437        for v in vals {
20438            if let Some(items) = v.as_array_vec() {
20439                flat.extend(items);
20440            } else {
20441                flat.push(v);
20442            }
20443        }
20444        if let Some(r) = arr_ref.as_array_ref() {
20445            let mut w = r.write();
20446            for (i, v) in flat.into_iter().enumerate() {
20447                w.insert(i, v);
20448            }
20449            return Ok(w.len() as i64);
20450        }
20451        if let Some(name) = arr_ref.as_array_binding_name() {
20452            let arr = self
20453                .scope
20454                .get_array_mut(&name)
20455                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20456            for (i, v) in flat.into_iter().enumerate() {
20457                arr.insert(i, v);
20458            }
20459            return Ok(arr.len() as i64);
20460        }
20461        if let Some(s) = arr_ref.as_str() {
20462            if self.strict_refs {
20463                return Err(StrykeError::runtime(
20464                    format!(
20465                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20466                        s
20467                    ),
20468                    line,
20469                )
20470                .into());
20471            }
20472            let name = s.to_string();
20473            let arr = self
20474                .scope
20475                .get_array_mut(&name)
20476                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20477            for (i, v) in flat.into_iter().enumerate() {
20478                arr.insert(i, v);
20479            }
20480            return Ok(arr.len() as i64);
20481        }
20482        Err(StrykeError::runtime("unshift argument is not an ARRAY reference", line).into())
20483    }
20484
20485    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
20486    /// / compiler wraps `splice` like other context-sensitive builtins).
20487    pub(crate) fn splice_array_deref(
20488        &mut self,
20489        aref: StrykeValue,
20490        offset_val: StrykeValue,
20491        length_val: StrykeValue,
20492        rep_vals: Vec<StrykeValue>,
20493        line: usize,
20494    ) -> Result<StrykeValue, FlowOrError> {
20495        let ctx = self.wantarray_kind;
20496        if let Some(r) = aref.as_array_ref() {
20497            let arr_len = r.read().len();
20498            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20499            let mut w = r.write();
20500            let removed: Vec<StrykeValue> = w.drain(off..end).collect();
20501            for (i, v) in rep_vals.into_iter().enumerate() {
20502                w.insert(off + i, v);
20503            }
20504            return Ok(match ctx {
20505                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20506                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20507            });
20508        }
20509        if let Some(name) = aref.as_array_binding_name() {
20510            let arr_len = self.scope.array_len(&name);
20511            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20512            let removed = self
20513                .scope
20514                .splice_in_place(&name, off, end, rep_vals)
20515                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20516            return Ok(match ctx {
20517                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20518                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20519            });
20520        }
20521        if let Some(s) = aref.as_str() {
20522            if self.strict_refs {
20523                return Err(StrykeError::runtime(
20524                    format!(
20525                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20526                        s
20527                    ),
20528                    line,
20529                )
20530                .into());
20531            }
20532            let arr_len = self.scope.array_len(&s);
20533            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20534            let removed = self
20535                .scope
20536                .splice_in_place(&s, off, end, rep_vals)
20537                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20538            return Ok(match ctx {
20539                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20540                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20541            });
20542        }
20543        Err(StrykeError::runtime("splice argument is not an ARRAY reference", line).into())
20544    }
20545
20546    /// Splice's LIST argument is Perl list context — any `@arr` / range /
20547    /// list-returning expression is flattened into the inserted values
20548    /// instead of being scalarized to its element count. Mirrors how
20549    /// `push`/`unshift` evaluate trailing args.
20550    fn eval_splice_replacement(
20551        &mut self,
20552        replacement: &[Expr],
20553    ) -> Result<Vec<StrykeValue>, FlowOrError> {
20554        let saved = self.wantarray_kind;
20555        self.wantarray_kind = WantarrayCtx::List;
20556        let mut out = Vec::new();
20557        for r in replacement {
20558            let v = self.eval_expr_ctx(r, WantarrayCtx::List)?;
20559            if let Some(items) = v.as_array_vec() {
20560                out.extend(items);
20561            } else if v.is_iterator() {
20562                out.extend(v.into_iterator().collect_all());
20563            } else {
20564                out.push(v);
20565            }
20566        }
20567        self.wantarray_kind = saved;
20568        Ok(out)
20569    }
20570
20571    pub(crate) fn eval_splice_expr(
20572        &mut self,
20573        array: &Expr,
20574        offset: Option<&Expr>,
20575        length: Option<&Expr>,
20576        replacement: &[Expr],
20577        ctx: WantarrayCtx,
20578        line: usize,
20579    ) -> Result<StrykeValue, FlowOrError> {
20580        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20581            let offset_val = if let Some(o) = offset {
20582                self.eval_expr(o)?
20583            } else {
20584                StrykeValue::integer(0)
20585            };
20586            let length_val = if let Some(l) = length {
20587                self.eval_expr(l)?
20588            } else {
20589                StrykeValue::UNDEF
20590            };
20591            let rep_vals = self.eval_splice_replacement(replacement)?;
20592            let saved = self.wantarray_kind;
20593            self.wantarray_kind = ctx;
20594            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
20595            self.wantarray_kind = saved;
20596            return out;
20597        }
20598        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20599        let arr_len = self.scope.array_len(&arr_name);
20600        let offset_val = if let Some(o) = offset {
20601            self.eval_expr(o)?
20602        } else {
20603            StrykeValue::integer(0)
20604        };
20605        let length_val = if let Some(l) = length {
20606            self.eval_expr(l)?
20607        } else {
20608            StrykeValue::UNDEF
20609        };
20610        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20611        let rep_vals = self.eval_splice_replacement(replacement)?;
20612        let removed = self
20613            .scope
20614            .splice_in_place(&arr_name, off, end, rep_vals)
20615            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20616        Ok(match ctx {
20617            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20618            WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20619        })
20620    }
20621
20622    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
20623    pub(crate) fn keys_from_value(
20624        val: StrykeValue,
20625        line: usize,
20626    ) -> Result<StrykeValue, FlowOrError> {
20627        if let Some(h) = val.as_hash_map() {
20628            Ok(StrykeValue::array(
20629                h.keys().map(|k| StrykeValue::string(k.clone())).collect(),
20630            ))
20631        } else if let Some(r) = val.as_hash_ref() {
20632            Ok(StrykeValue::array(
20633                r.read()
20634                    .keys()
20635                    .map(|k| StrykeValue::string(k.clone()))
20636                    .collect(),
20637            ))
20638        } else {
20639            Err(StrykeError::runtime("keys requires hash", line).into())
20640        }
20641    }
20642
20643    pub(crate) fn eval_keys_expr(
20644        &mut self,
20645        expr: &Expr,
20646        line: usize,
20647    ) -> Result<StrykeValue, FlowOrError> {
20648        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
20649        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
20650        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
20651        Self::keys_from_value(val, line)
20652    }
20653
20654    /// Result of `values EXPR` after `EXPR` has been evaluated.
20655    pub(crate) fn values_from_value(
20656        val: StrykeValue,
20657        line: usize,
20658    ) -> Result<StrykeValue, FlowOrError> {
20659        if let Some(h) = val.as_hash_map() {
20660            Ok(StrykeValue::array(h.values().cloned().collect()))
20661        } else if let Some(r) = val.as_hash_ref() {
20662            Ok(StrykeValue::array(r.read().values().cloned().collect()))
20663        } else {
20664            Err(StrykeError::runtime("values requires hash", line).into())
20665        }
20666    }
20667
20668    pub(crate) fn eval_values_expr(
20669        &mut self,
20670        expr: &Expr,
20671        line: usize,
20672    ) -> Result<StrykeValue, FlowOrError> {
20673        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
20674        Self::values_from_value(val, line)
20675    }
20676
20677    pub(crate) fn eval_delete_operand(
20678        &mut self,
20679        expr: &Expr,
20680        line: usize,
20681    ) -> Result<StrykeValue, FlowOrError> {
20682        match &expr.kind {
20683            ExprKind::HashElement { hash, key } => {
20684                let k = self.eval_expr(key)?.to_string();
20685                self.touch_env_hash(hash);
20686                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
20687                    let class = obj
20688                        .as_blessed_ref()
20689                        .map(|b| b.class.clone())
20690                        .unwrap_or_default();
20691                    let full = format!("{}::DELETE", class);
20692                    if let Some(sub) = self.subs.get(&full).cloned() {
20693                        return self.call_sub(
20694                            &sub,
20695                            vec![obj, StrykeValue::string(k)],
20696                            WantarrayCtx::Scalar,
20697                            line,
20698                        );
20699                    }
20700                }
20701                self.scope
20702                    .delete_hash_element(hash, &k)
20703                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
20704            }
20705            ExprKind::ArrayElement { array, index } => {
20706                self.check_strict_array_var(array, line)?;
20707                let idx = self.eval_expr(index)?.to_int();
20708                let aname = self.stash_array_name_for_package(array);
20709                self.scope
20710                    .delete_array_element(&aname, idx)
20711                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
20712            }
20713            ExprKind::ArrowDeref {
20714                expr: inner,
20715                index,
20716                kind: DerefKind::Hash,
20717            } => {
20718                let k = self.eval_expr(index)?.to_string();
20719                let container = self.eval_expr(inner)?;
20720                self.delete_arrow_hash_element(container, &k, line)
20721                    .map_err(Into::into)
20722            }
20723            ExprKind::ArrowDeref {
20724                expr: inner,
20725                index,
20726                kind: DerefKind::Array,
20727            } => {
20728                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
20729                    return Err(StrykeError::runtime(
20730                        "delete on array element needs scalar subscript",
20731                        line,
20732                    )
20733                    .into());
20734                }
20735                let container = self.eval_expr(inner)?;
20736                let idx = self.eval_expr(index)?.to_int();
20737                self.delete_arrow_array_element(container, idx, line)
20738                    .map_err(Into::into)
20739            }
20740            // `delete @h{KEYS}` — Perl slice form. Drains every named key,
20741            // returning the list of deleted values (undef when absent).
20742            ExprKind::HashSlice { hash, keys } => {
20743                self.touch_env_hash(hash);
20744                let mut all_keys: Vec<String> = Vec::new();
20745                for key_expr in keys {
20746                    all_keys.extend(self.eval_hash_slice_key_components(key_expr)?);
20747                }
20748                let mut deleted = Vec::with_capacity(all_keys.len());
20749                for k in &all_keys {
20750                    let v = self
20751                        .scope
20752                        .delete_hash_element(hash, k)
20753                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20754                    deleted.push(v);
20755                }
20756                Ok(StrykeValue::array(deleted))
20757            }
20758            // `delete @a[INDICES]` — Perl array-slice form.
20759            ExprKind::ArraySlice { array, indices } => {
20760                self.check_strict_array_var(array, line)?;
20761                let aname = self.stash_array_name_for_package(array);
20762                let mut all_idx: Vec<i64> = Vec::new();
20763                for idx_expr in indices {
20764                    let v = self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?;
20765                    if let Some(items) = v.as_array_vec() {
20766                        all_idx.extend(items.iter().map(|x| x.to_int()));
20767                    } else if let Some(r) = v.as_array_ref() {
20768                        all_idx.extend(r.read().iter().map(|x| x.to_int()));
20769                    } else if v.is_iterator() {
20770                        all_idx.extend(v.into_iterator().collect_all().iter().map(|x| x.to_int()));
20771                    } else {
20772                        all_idx.push(v.to_int());
20773                    }
20774                }
20775                let mut deleted = Vec::with_capacity(all_idx.len());
20776                for idx in &all_idx {
20777                    let v = self
20778                        .scope
20779                        .delete_array_element(&aname, *idx)
20780                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20781                    deleted.push(v);
20782                }
20783                Ok(StrykeValue::array(deleted))
20784            }
20785            _ => Err(StrykeError::runtime("delete requires hash or array element", line).into()),
20786        }
20787    }
20788
20789    /// Evaluate a deref-chain in "exists mode" — like [`Self::eval_expr`] but
20790    /// recursively walks `ArrowDeref` chains and turns undef-intermediate
20791    /// derefs into undef (instead of erroring). Used by
20792    /// [`Self::eval_exists_operand`] so `exists $h{x}{y}{z}` returns 0 for
20793    /// any missing level. (BUG-009)
20794    fn eval_expr_exists_mode(&mut self, expr: &Expr) -> Result<StrykeValue, FlowOrError> {
20795        match &expr.kind {
20796            ExprKind::ArrowDeref {
20797                expr: inner,
20798                index,
20799                kind: DerefKind::Hash,
20800            } => {
20801                let inner_val = self.eval_expr_exists_mode(inner)?;
20802                if inner_val.is_undef() {
20803                    return Ok(StrykeValue::UNDEF);
20804                }
20805                if let Some(r) = inner_val.as_hash_ref() {
20806                    let k = self.eval_expr(index)?.to_string();
20807                    return Ok(r.read().get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
20808                }
20809                if let Some(b) = inner_val.as_blessed_ref() {
20810                    let data = b.data.read();
20811                    if let Some(r) = data.as_hash_ref() {
20812                        let k = self.eval_expr(index)?.to_string();
20813                        return Ok(r.read().get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
20814                    }
20815                }
20816                // Struct / class instance — look up the field by name and
20817                // return its value. Without this, `exists $struct->{f}->{k}`
20818                // soft-fails to false even when the field is a real hashref.
20819                if let Some(s) = inner_val.as_struct_inst() {
20820                    let k = self.eval_expr(index)?.to_string();
20821                    if let Some(idx) = s.def.field_index(&k) {
20822                        return Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF));
20823                    }
20824                    return Ok(StrykeValue::UNDEF);
20825                }
20826                if let Some(c) = inner_val.as_class_inst() {
20827                    let k = self.eval_expr(index)?.to_string();
20828                    if let Some(idx) = c.def.field_index(&k) {
20829                        return Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF));
20830                    }
20831                    return Ok(StrykeValue::UNDEF);
20832                }
20833                Ok(StrykeValue::UNDEF)
20834            }
20835            ExprKind::ArrowDeref {
20836                expr: inner,
20837                index,
20838                kind: DerefKind::Array,
20839            } => {
20840                let inner_val = self.eval_expr_exists_mode(inner)?;
20841                if inner_val.is_undef() {
20842                    return Ok(StrykeValue::UNDEF);
20843                }
20844                if let Some(r) = inner_val.as_array_ref() {
20845                    let idx = self.eval_expr(index)?.to_int();
20846                    let arr = r.read();
20847                    let i = if idx < 0 {
20848                        (arr.len() as i64 + idx).max(0) as usize
20849                    } else {
20850                        idx as usize
20851                    };
20852                    return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
20853                }
20854                Ok(StrykeValue::UNDEF)
20855            }
20856            _ => self.eval_expr(expr),
20857        }
20858    }
20859
20860    pub(crate) fn eval_exists_operand(
20861        &mut self,
20862        expr: &Expr,
20863        line: usize,
20864    ) -> Result<StrykeValue, FlowOrError> {
20865        match &expr.kind {
20866            ExprKind::HashElement { hash, key } => {
20867                let k = self.eval_expr(key)?.to_string();
20868                self.touch_env_hash(hash);
20869                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
20870                    let class = obj
20871                        .as_blessed_ref()
20872                        .map(|b| b.class.clone())
20873                        .unwrap_or_default();
20874                    let full = format!("{}::EXISTS", class);
20875                    if let Some(sub) = self.subs.get(&full).cloned() {
20876                        return self.call_sub(
20877                            &sub,
20878                            vec![obj, StrykeValue::string(k)],
20879                            WantarrayCtx::Scalar,
20880                            line,
20881                        );
20882                    }
20883                }
20884                Ok(StrykeValue::integer(
20885                    if self.scope.exists_hash_element(hash, &k) {
20886                        1
20887                    } else {
20888                        0
20889                    },
20890                ))
20891            }
20892            ExprKind::ArrayElement { array, index } => {
20893                self.check_strict_array_var(array, line)?;
20894                let idx = self.eval_expr(index)?.to_int();
20895                let aname = self.stash_array_name_for_package(array);
20896                Ok(StrykeValue::integer(
20897                    if self.scope.exists_array_element(&aname, idx) {
20898                        1
20899                    } else {
20900                        0
20901                    },
20902                ))
20903            }
20904            ExprKind::ArrowDeref {
20905                expr: inner,
20906                index,
20907                kind: DerefKind::Hash,
20908            } => {
20909                let k = self.eval_expr(index)?.to_string();
20910                // Evaluate the chain in "exists mode" — undef intermediates
20911                // propagate as undef instead of erroring on missing-key
20912                // deref, matching Perl's `exists $h{x}{y}{z}` returning 0
20913                // for any missing level. (BUG-009)
20914                let container = match self.eval_expr_exists_mode(inner) {
20915                    Ok(v) => v,
20916                    Err(_) => return Ok(StrykeValue::integer(0)),
20917                };
20918                if container.is_undef() {
20919                    return Ok(StrykeValue::integer(0));
20920                }
20921                let yes = self.exists_arrow_hash_element(container, &k, line)?;
20922                Ok(StrykeValue::integer(if yes { 1 } else { 0 }))
20923            }
20924            ExprKind::ArrowDeref {
20925                expr: inner,
20926                index,
20927                kind: DerefKind::Array,
20928            } => {
20929                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
20930                    return Err(StrykeError::runtime(
20931                        "exists on array element needs scalar subscript",
20932                        line,
20933                    )
20934                    .into());
20935                }
20936                let container = match self.eval_expr_exists_mode(inner) {
20937                    Ok(v) => v,
20938                    Err(_) => return Ok(StrykeValue::integer(0)),
20939                };
20940                if container.is_undef() {
20941                    return Ok(StrykeValue::integer(0));
20942                }
20943                let idx = self.eval_expr(index)?.to_int();
20944                let yes = self.exists_arrow_array_element(container, idx, line)?;
20945                Ok(StrykeValue::integer(if yes { 1 } else { 0 }))
20946            }
20947            ExprKind::SubroutineRef(name) => {
20948                // `exists &name` / `exists &Pkg::name` — true when the
20949                // subroutine has been declared (whether or not it's
20950                // defined, mirroring Perl's `exists &subname`).
20951                let resolved = self.resolve_sub_by_name(name);
20952                Ok(StrykeValue::integer(if resolved.is_some() { 1 } else { 0 }))
20953            }
20954            _ => Err(StrykeError::runtime("exists requires hash or array element", line).into()),
20955        }
20956    }
20957
20958    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
20959    ///
20960    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
20961    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
20962    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
20963    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
20964    /// the new path amortizes the handshake across the whole map.
20965    pub(crate) fn eval_pmap_remote(
20966        &mut self,
20967        cluster_pv: StrykeValue,
20968        list_pv: StrykeValue,
20969        show_progress: bool,
20970        block: &Block,
20971        flat_outputs: bool,
20972        line: usize,
20973    ) -> Result<StrykeValue, FlowOrError> {
20974        let Some(cluster) = cluster_pv.as_remote_cluster() else {
20975            return Err(StrykeError::runtime("pmap_on: expected cluster(...) value", line).into());
20976        };
20977        let items = list_pv.to_list();
20978        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20979        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
20980            return Err(StrykeError::runtime(
20981                "pmap_on: mysync/atomic capture is not supported for remote workers",
20982                line,
20983            )
20984            .into());
20985        }
20986        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
20987            .map_err(|e| StrykeError::runtime(e, line))?;
20988        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
20989        let block_src = crate::fmt::format_block(block);
20990        let item_jsons = crate::cluster::perl_items_to_json(&items)
20991            .map_err(|e| StrykeError::runtime(e, line))?;
20992
20993        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
20994        // synchronous from the caller's POV, so we drive the bar before/after the call.
20995        let pmap_progress = PmapProgress::new(show_progress, items.len());
20996        let result_values =
20997            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
20998                .map_err(|e| StrykeError::runtime(format!("pmap_on remote: {e}"), line))?;
20999        for _ in 0..result_values.len() {
21000            pmap_progress.tick();
21001        }
21002        pmap_progress.finish();
21003
21004        if flat_outputs {
21005            let flattened: Vec<StrykeValue> = result_values
21006                .into_iter()
21007                .flat_map(|v| v.map_flatten_outputs(true))
21008                .collect();
21009            Ok(StrykeValue::array(flattened))
21010        } else {
21011            Ok(StrykeValue::array(result_values))
21012        }
21013    }
21014
21015    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
21016    pub(crate) fn eval_par_lines_expr(
21017        &mut self,
21018        path: &Expr,
21019        callback: &Expr,
21020        progress: Option<&Expr>,
21021        line: usize,
21022    ) -> Result<StrykeValue, FlowOrError> {
21023        let show_progress = progress
21024            .map(|p| self.eval_expr(p))
21025            .transpose()?
21026            .map(|v| v.is_true())
21027            .unwrap_or(false);
21028        let raw = self.eval_expr(path)?.to_string();
21029        let path_s = self.resolve_stryke_path_string(&raw);
21030        let cb_val = self.eval_expr(callback)?;
21031        let sub = if let Some(s) = cb_val.as_code_ref() {
21032            s
21033        } else {
21034            return Err(StrykeError::runtime(
21035                "par_lines: second argument must be a code reference",
21036                line,
21037            )
21038            .into());
21039        };
21040        let subs = self.subs.clone();
21041        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
21042        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
21043            FlowOrError::Error(StrykeError::runtime(format!("par_lines: {}", e), line))
21044        })?;
21045        let mmap = unsafe {
21046            memmap2::Mmap::map(&file).map_err(|e| {
21047                FlowOrError::Error(StrykeError::runtime(
21048                    format!("par_lines: mmap: {}", e),
21049                    line,
21050                ))
21051            })?
21052        };
21053        let data: &[u8] = &mmap;
21054        if data.is_empty() {
21055            return Ok(StrykeValue::UNDEF);
21056        }
21057        let line_total = crate::par_lines::line_count_bytes(data);
21058        let pmap_progress = PmapProgress::new(show_progress, line_total);
21059        if self.num_threads == 0 {
21060            self.num_threads = rayon::current_num_threads();
21061        }
21062        let num_chunks = self.num_threads.saturating_mul(8).max(1);
21063        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
21064        chunks.into_par_iter().try_for_each(|(start, end)| {
21065            let slice = &data[start..end];
21066            let mut s = 0usize;
21067            while s < slice.len() {
21068                let e = slice[s..]
21069                    .iter()
21070                    .position(|&b| b == b'\n')
21071                    .map(|p| s + p)
21072                    .unwrap_or(slice.len());
21073                let line_bytes = &slice[s..e];
21074                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
21075                let mut local_interp = VMHelper::new();
21076                local_interp.subs = subs.clone();
21077                local_interp.scope.restore_capture(&scope_capture);
21078                local_interp
21079                    .scope
21080                    .restore_atomics(&atomic_arrays, &atomic_hashes);
21081                local_interp.enable_parallel_guard();
21082                local_interp.scope.set_topic(StrykeValue::string(line_str));
21083                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
21084                    Ok(_) => {}
21085                    Err(e) => return Err(e),
21086                }
21087                pmap_progress.tick();
21088                if e >= slice.len() {
21089                    break;
21090                }
21091                s = e + 1;
21092            }
21093            Ok(())
21094        })?;
21095        pmap_progress.finish();
21096        Ok(StrykeValue::UNDEF)
21097    }
21098
21099    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
21100    pub(crate) fn eval_par_walk_expr(
21101        &mut self,
21102        path: &Expr,
21103        callback: &Expr,
21104        progress: Option<&Expr>,
21105        line: usize,
21106    ) -> Result<StrykeValue, FlowOrError> {
21107        let show_progress = progress
21108            .map(|p| self.eval_expr(p))
21109            .transpose()?
21110            .map(|v| v.is_true())
21111            .unwrap_or(false);
21112        let path_val = self.eval_expr(path)?;
21113        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
21114            arr.into_iter()
21115                .map(|v| PathBuf::from(v.to_string()))
21116                .collect()
21117        } else {
21118            vec![PathBuf::from(path_val.to_string())]
21119        };
21120        let cb_val = self.eval_expr(callback)?;
21121        let sub = if let Some(s) = cb_val.as_code_ref() {
21122            s
21123        } else {
21124            return Err(StrykeError::runtime(
21125                "par_walk: second argument must be a code reference",
21126                line,
21127            )
21128            .into());
21129        };
21130        let subs = self.subs.clone();
21131        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
21132
21133        if show_progress {
21134            let paths = crate::par_walk::collect_paths(&roots);
21135            let pmap_progress = PmapProgress::new(true, paths.len());
21136            paths.into_par_iter().try_for_each(|p| {
21137                let s = p.to_string_lossy().into_owned();
21138                let mut local_interp = VMHelper::new();
21139                local_interp.subs = subs.clone();
21140                local_interp.scope.restore_capture(&scope_capture);
21141                local_interp
21142                    .scope
21143                    .restore_atomics(&atomic_arrays, &atomic_hashes);
21144                local_interp.enable_parallel_guard();
21145                local_interp.scope.set_topic(StrykeValue::string(s));
21146                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
21147                    Ok(_) => {}
21148                    Err(e) => return Err(e),
21149                }
21150                pmap_progress.tick();
21151                Ok(())
21152            })?;
21153            pmap_progress.finish();
21154        } else {
21155            for r in &roots {
21156                par_walk_recursive(
21157                    r.as_path(),
21158                    &sub,
21159                    &subs,
21160                    &scope_capture,
21161                    &atomic_arrays,
21162                    &atomic_hashes,
21163                    line,
21164                )?;
21165            }
21166        }
21167        Ok(StrykeValue::UNDEF)
21168    }
21169
21170    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
21171    pub(crate) fn builtin_par_sed(
21172        &mut self,
21173        args: &[StrykeValue],
21174        line: usize,
21175        has_progress: bool,
21176    ) -> StrykeResult<StrykeValue> {
21177        let show_progress = if has_progress {
21178            args.last().map(|v| v.is_true()).unwrap_or(false)
21179        } else {
21180            false
21181        };
21182        let slice = if has_progress {
21183            &args[..args.len().saturating_sub(1)]
21184        } else {
21185            args
21186        };
21187        if slice.len() < 3 {
21188            return Err(StrykeError::runtime(
21189                "par_sed: need pattern, replacement, and at least one file path",
21190                line,
21191            ));
21192        }
21193        let pat_val = &slice[0];
21194        let repl = slice[1].to_string();
21195        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
21196
21197        let re = if let Some(rx) = pat_val.as_regex() {
21198            rx
21199        } else {
21200            let pattern = pat_val.to_string();
21201            match self.compile_regex(&pattern, "g", line) {
21202                Ok(r) => r,
21203                Err(FlowOrError::Error(e)) => return Err(e),
21204                Err(FlowOrError::Flow(f)) => {
21205                    return Err(StrykeError::runtime(format!("par_sed: {:?}", f), line))
21206                }
21207            }
21208        };
21209
21210        let pmap = PmapProgress::new(show_progress, files.len());
21211        let touched = AtomicUsize::new(0);
21212        files.par_iter().try_for_each(|path| {
21213            let content = read_file_text_perl_compat(path)
21214                .map_err(|e| StrykeError::runtime(format!("par_sed {}: {}", path, e), line))?;
21215            let new_s = re.replace_all(&content, &repl);
21216            if new_s != content {
21217                std::fs::write(path, new_s.as_bytes())
21218                    .map_err(|e| StrykeError::runtime(format!("par_sed {}: {}", path, e), line))?;
21219                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
21220            }
21221            pmap.tick();
21222            Ok(())
21223        })?;
21224        pmap.finish();
21225        Ok(StrykeValue::integer(
21226            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
21227        ))
21228    }
21229
21230    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
21231    pub(crate) fn eval_pwatch_expr(
21232        &mut self,
21233        path: &Expr,
21234        callback: &Expr,
21235        line: usize,
21236    ) -> Result<StrykeValue, FlowOrError> {
21237        let pattern_s = self.eval_expr(path)?.to_string();
21238        let cb_val = self.eval_expr(callback)?;
21239        let sub = if let Some(s) = cb_val.as_code_ref() {
21240            s
21241        } else {
21242            return Err(StrykeError::runtime(
21243                "pwatch: second argument must be a code reference",
21244                line,
21245            )
21246            .into());
21247        };
21248        let subs = self.subs.clone();
21249        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
21250        crate::pwatch::run_pwatch(
21251            &pattern_s,
21252            sub,
21253            subs,
21254            scope_capture,
21255            atomic_arrays,
21256            atomic_hashes,
21257            line,
21258        )
21259        .map_err(FlowOrError::Error)
21260    }
21261
21262    /// Interpolate `$var` in s/// replacement strings, preserving numeric backrefs ($1, $2, etc.).
21263    fn interpolate_replacement_string(&self, replacement: &str) -> String {
21264        let mut out = String::with_capacity(replacement.len());
21265        let chars: Vec<char> = replacement.chars().collect();
21266        let mut i = 0;
21267        while i < chars.len() {
21268            if chars[i] == '\\' && i + 1 < chars.len() {
21269                out.push(chars[i]);
21270                out.push(chars[i + 1]);
21271                i += 2;
21272                continue;
21273            }
21274            if chars[i] == '$' && i + 1 < chars.len() {
21275                let start = i;
21276                i += 1;
21277                if chars[i].is_ascii_digit() {
21278                    out.push('$');
21279                    while i < chars.len() && chars[i].is_ascii_digit() {
21280                        out.push(chars[i]);
21281                        i += 1;
21282                    }
21283                    continue;
21284                }
21285                if chars[i] == '&' || chars[i] == '`' || chars[i] == '\'' {
21286                    out.push('$');
21287                    out.push(chars[i]);
21288                    i += 1;
21289                    continue;
21290                }
21291                if !chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{' {
21292                    out.push('$');
21293                    continue;
21294                }
21295                let mut name = String::new();
21296                if chars[i] == '{' {
21297                    i += 1;
21298                    while i < chars.len() && chars[i] != '}' {
21299                        name.push(chars[i]);
21300                        i += 1;
21301                    }
21302                    if i < chars.len() {
21303                        i += 1;
21304                    }
21305                } else {
21306                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
21307                        name.push(chars[i]);
21308                        i += 1;
21309                    }
21310                }
21311                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
21312                    let val = self.scope.get_scalar(&name);
21313                    out.push_str(&val.to_string());
21314                } else if !name.is_empty() {
21315                    out.push_str(&replacement[start..i]);
21316                } else {
21317                    out.push('$');
21318                }
21319                continue;
21320            }
21321            out.push(chars[i]);
21322            i += 1;
21323        }
21324        out
21325    }
21326
21327    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
21328    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
21329        let mut out = String::with_capacity(pattern.len());
21330        let chars: Vec<char> = pattern.chars().collect();
21331        let mut i = 0;
21332        while i < chars.len() {
21333            if chars[i] == '\\' && i + 1 < chars.len() {
21334                // Preserve escape sequences (including \$ which is literal $)
21335                out.push(chars[i]);
21336                out.push(chars[i + 1]);
21337                i += 2;
21338                continue;
21339            }
21340            if chars[i] == '$' && i + 1 < chars.len() {
21341                i += 1;
21342                // `$` at end of pattern is an anchor, not a variable
21343                if i >= chars.len()
21344                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
21345                {
21346                    out.push('$');
21347                    continue;
21348                }
21349                let mut name = String::new();
21350                if chars[i] == '{' {
21351                    i += 1;
21352                    while i < chars.len() && chars[i] != '}' {
21353                        name.push(chars[i]);
21354                        i += 1;
21355                    }
21356                    if i < chars.len() {
21357                        i += 1;
21358                    } // skip }
21359                } else {
21360                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
21361                        name.push(chars[i]);
21362                        i += 1;
21363                    }
21364                }
21365                if !name.is_empty() {
21366                    let val = self.scope.get_scalar(&name);
21367                    out.push_str(&val.to_string());
21368                } else {
21369                    out.push('$');
21370                }
21371                continue;
21372            }
21373            out.push(chars[i]);
21374            i += 1;
21375        }
21376        out
21377    }
21378
21379    pub(crate) fn compile_regex(
21380        &mut self,
21381        pattern: &str,
21382        flags: &str,
21383        line: usize,
21384    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
21385        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
21386        let pattern = if pattern.contains('$') || pattern.contains('@') {
21387            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
21388        } else {
21389            std::borrow::Cow::Borrowed(pattern)
21390        };
21391        let pattern = pattern.as_ref();
21392        // Fast path: same regex as last call (common in loops).
21393        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
21394        let multiline = self.multiline_match;
21395        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
21396            if lp == pattern && lf == flags && *lm == multiline {
21397                return Ok(lr.clone());
21398            }
21399        }
21400        // Slow path: HashMap lookup
21401        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
21402        if let Some(cached) = self.regex_cache.get(&key) {
21403            self.regex_last = Some((
21404                pattern.to_string(),
21405                flags.to_string(),
21406                multiline,
21407                cached.clone(),
21408            ));
21409            return Ok(cached.clone());
21410        }
21411        let expanded = expand_perl_regex_quotemeta(pattern);
21412        let expanded = expand_perl_regex_octal_escapes(&expanded);
21413        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
21414        let mut re_str = String::new();
21415        if flags.contains('i') {
21416            re_str.push_str("(?i)");
21417        }
21418        if flags.contains('s') {
21419            re_str.push_str("(?s)");
21420        }
21421        if flags.contains('m') {
21422            re_str.push_str("(?m)");
21423        }
21424        if flags.contains('x') {
21425            re_str.push_str("(?x)");
21426        }
21427        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
21428        if multiline {
21429            re_str.push_str("(?s)");
21430        }
21431        re_str.push_str(&expanded);
21432        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
21433            FlowOrError::Error(StrykeError::runtime(
21434                format!("Invalid regex /{}/: {}", pattern, e),
21435                line,
21436            ))
21437        })?;
21438        let arc = re;
21439        self.regex_last = Some((
21440            pattern.to_string(),
21441            flags.to_string(),
21442            multiline,
21443            arc.clone(),
21444        ));
21445        self.regex_cache.insert(key, arc.clone());
21446        Ok(arc)
21447    }
21448
21449    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
21450    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
21451        if self.last_readline_handle.is_empty() {
21452            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
21453        }
21454        let n = *self
21455            .handle_line_numbers
21456            .get(&self.last_readline_handle)
21457            .unwrap_or(&0);
21458        if n <= 0 {
21459            return None;
21460        }
21461        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
21462        {
21463            return Some(("<>".to_string(), n));
21464        }
21465        if self.last_readline_handle == "STDIN" {
21466            return Some((self.last_stdin_die_bracket.clone(), n));
21467        }
21468        Some((format!("<{}>", self.last_readline_handle), n))
21469    }
21470
21471    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
21472    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
21473        let mut s = format!(" at {} line {}", self.file, source_line);
21474        if let Some((bracket, n)) = self.die_warn_io_annotation() {
21475            s.push_str(&format!(", {} line {}.", bracket, n));
21476        } else {
21477            s.push('.');
21478        }
21479        s
21480    }
21481
21482    /// Process a line in -n/-p mode via the VM.
21483    ///
21484    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
21485    /// file so `eof` with no arguments matches Perl behavior on that line.
21486    pub fn process_line(
21487        &mut self,
21488        line_str: &str,
21489        _program: &Program,
21490        is_last_input_line: bool,
21491    ) -> StrykeResult<Option<String>> {
21492        let chunk = self
21493            .line_mode_chunk
21494            .as_ref()
21495            .expect("process_line called without compiled chunk — execute() must run first")
21496            .clone();
21497        crate::run_line_body(&chunk, self, line_str, is_last_input_line)
21498    }
21499}
21500
21501/// Mirrors `vm.rs::both_non_numeric_strings`. Used by the tree-walker's
21502/// `==` / `!=` to decide whether to fall back to string compare in
21503/// stryke non-compat mode.
21504fn both_non_numeric_strings_iv(a: &StrykeValue, b: &StrykeValue) -> bool {
21505    if !a.is_string_like() || !b.is_string_like() {
21506        return false;
21507    }
21508    let sa = a.to_string();
21509    let sb = b.to_string();
21510    let looks = |s: &str| {
21511        let t = s.trim();
21512        !t.is_empty() && t.parse::<f64>().is_ok()
21513    };
21514    !looks(&sa) && !looks(&sb)
21515}
21516
21517fn par_walk_invoke_entry(
21518    path: &Path,
21519    sub: &Arc<StrykeSub>,
21520    subs: &HashMap<String, Arc<StrykeSub>>,
21521    scope_capture: &[(String, StrykeValue)],
21522    atomic_arrays: &[(String, crate::scope::AtomicArray)],
21523    atomic_hashes: &[(String, crate::scope::AtomicHash)],
21524    line: usize,
21525) -> Result<(), FlowOrError> {
21526    let s = path.to_string_lossy().into_owned();
21527    let mut local_interp = VMHelper::new();
21528    local_interp.subs = subs.clone();
21529    local_interp.scope.restore_capture(scope_capture);
21530    local_interp
21531        .scope
21532        .restore_atomics(atomic_arrays, atomic_hashes);
21533    local_interp.enable_parallel_guard();
21534    local_interp.scope.set_topic(StrykeValue::string(s));
21535    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
21536    Ok(())
21537}
21538
21539fn par_walk_recursive(
21540    path: &Path,
21541    sub: &Arc<StrykeSub>,
21542    subs: &HashMap<String, Arc<StrykeSub>>,
21543    scope_capture: &[(String, StrykeValue)],
21544    atomic_arrays: &[(String, crate::scope::AtomicArray)],
21545    atomic_hashes: &[(String, crate::scope::AtomicHash)],
21546    line: usize,
21547) -> Result<(), FlowOrError> {
21548    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
21549        return par_walk_invoke_entry(
21550            path,
21551            sub,
21552            subs,
21553            scope_capture,
21554            atomic_arrays,
21555            atomic_hashes,
21556            line,
21557        );
21558    }
21559    if !path.is_dir() {
21560        return Ok(());
21561    }
21562    par_walk_invoke_entry(
21563        path,
21564        sub,
21565        subs,
21566        scope_capture,
21567        atomic_arrays,
21568        atomic_hashes,
21569        line,
21570    )?;
21571    let read = match std::fs::read_dir(path) {
21572        Ok(r) => r,
21573        Err(_) => return Ok(()),
21574    };
21575    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
21576    entries.par_iter().try_for_each(|e| {
21577        par_walk_recursive(
21578            &e.path(),
21579            sub,
21580            subs,
21581            scope_capture,
21582            atomic_arrays,
21583            atomic_hashes,
21584            line,
21585        )
21586    })?;
21587    Ok(())
21588}
21589
21590/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
21591/// Reformat Rust's `{:e}` / `{:E}` exponent style (`1.234568e4`) to the
21592/// Perl/C convention (`1.234568e+04`). Adds a sign character to the
21593/// exponent and zero-pads it to at least two digits.
21594/// Perl-style magical string increment.
21595///
21596/// Returns `Some(new)` when `s` matches `^[A-Za-z]+[0-9]*$` (i.e. some
21597/// letters, optionally followed by digits, ending at the end of string)
21598/// or is the empty string (which becomes `"1"`). Returns `None` for any
21599/// other shape — pure digits, leading whitespace, mixed letters/digits,
21600/// embedded punctuation, etc. — so the caller can fall back to a plain
21601/// numeric increment.
21602///
21603/// Carry rules:
21604/// - In the digit suffix, `9 -> 0` carries left.
21605/// - In the letter prefix, `z -> a` and `Z -> A` carry left.
21606/// - When a carry exits the leftmost letter, a fresh `a` or `A` is
21607///   prepended (case-matched to the first character of the original).
21608///
21609/// Split a `StrykeValue` into approximately `n_threads` chunks for the
21610/// `par { BLOCK }` runtime. Strings are partitioned on UTF-8 char-aligned
21611/// byte boundaries; arrays/lists on element boundaries. Other scalar
21612/// types (int, float, undef, ref) return a single-chunk Vec containing
21613/// the value unchanged — the caller should handle this fallback.
21614///
21615/// Returned chunks are themselves `StrykeValue` so the worker can bind
21616/// each to `$_` and invoke the user's block.
21617fn par_chunk_value(v: &StrykeValue, n_threads: usize) -> Vec<StrykeValue> {
21618    let n = n_threads.max(1);
21619    // String input: split on char boundaries.
21620    if let Some(s) = v.as_str() {
21621        let bytes = s.as_bytes();
21622        if bytes.len() < 16_384 || n < 2 {
21623            return vec![StrykeValue::string(s)];
21624        }
21625        let target = bytes.len().div_ceil(n);
21626        let mut splits = vec![0usize];
21627        let mut cursor = target;
21628        while cursor < bytes.len() {
21629            // Walk forward until we hit a UTF-8 leading byte (`0xxxxxxx` or `11xxxxxx`).
21630            while cursor < bytes.len() && (bytes[cursor] & 0xC0) == 0x80 {
21631                cursor += 1;
21632            }
21633            splits.push(cursor);
21634            cursor += target;
21635        }
21636        splits.push(bytes.len());
21637        return splits
21638            .windows(2)
21639            .map(|w| {
21640                let chunk = std::str::from_utf8(&bytes[w[0]..w[1]]).unwrap_or("");
21641                StrykeValue::string(chunk.to_string())
21642            })
21643            .collect();
21644    }
21645    // Array / list input: split on element boundaries.
21646    if let Some(arr) = v.as_array_vec() {
21647        if arr.len() < 32 || n < 2 {
21648            return vec![StrykeValue::array(arr)];
21649        }
21650        let target = arr.len().div_ceil(n);
21651        let mut chunks = Vec::with_capacity(n);
21652        for slice in arr.chunks(target) {
21653            chunks.push(StrykeValue::array(slice.to_vec()));
21654        }
21655        return chunks;
21656    }
21657    if let Some(arr_ref) = v.as_array_ref() {
21658        let arr = arr_ref.read().clone();
21659        if arr.len() < 32 || n < 2 {
21660            return vec![StrykeValue::array(arr)];
21661        }
21662        let target = arr.len().div_ceil(n);
21663        let mut chunks = Vec::with_capacity(n);
21664        for slice in arr.chunks(target) {
21665            chunks.push(StrykeValue::array(slice.to_vec()));
21666        }
21667        return chunks;
21668    }
21669    // Fallback: single chunk holding the original value.
21670    vec![v.clone()]
21671}
21672
21673/// Auto-merge a list of `par_reduce` per-chunk results when no explicit
21674/// reduce block is supplied. Picks the merger by inspecting the first
21675/// chunk's value type:
21676///
21677/// - **Hash with numeric values** → key-wise add (canonical histogram merge)
21678/// - **Number** → numeric `+`
21679/// - **Array / list** → concat
21680/// - **String** → concat
21681/// - **Anything else** → return chunks as a flat array (caller can post-process)
21682fn par_reduce_auto_merge(chunks: Vec<StrykeValue>) -> StrykeValue {
21683    if chunks.is_empty() {
21684        return StrykeValue::UNDEF;
21685    }
21686    let first = &chunks[0];
21687    // Hash<number> add-merge.
21688    if let Some(_h) = first.as_hash_ref() {
21689        let mut out: indexmap::IndexMap<String, f64> = indexmap::IndexMap::new();
21690        for chunk in &chunks {
21691            if let Some(hr) = chunk.as_hash_ref() {
21692                for (k, v) in hr.read().iter() {
21693                    *out.entry(k.clone()).or_insert(0.0) += v.to_number();
21694                }
21695            }
21696        }
21697        // Round-trip integer values back to integers so `freq`-style
21698        // hashes stay integer-typed downstream.
21699        let mut indexmap_out: indexmap::IndexMap<String, StrykeValue> = indexmap::IndexMap::new();
21700        for (k, v) in out {
21701            let pv = if v == v.trunc() && v.abs() < 1e15 {
21702                StrykeValue::integer(v as i64)
21703            } else {
21704                StrykeValue::float(v)
21705            };
21706            indexmap_out.insert(k, pv);
21707        }
21708        return StrykeValue::hash_ref(Arc::new(parking_lot::RwLock::new(indexmap_out)));
21709    }
21710    // Numeric add-merge (int or float).
21711    if first.is_integer_like() || first.is_float_like() {
21712        let s: f64 = chunks.iter().map(|v| v.to_number()).sum();
21713        if s == s.trunc() && s.abs() < 1e15 {
21714            return StrykeValue::integer(s as i64);
21715        }
21716        return StrykeValue::float(s);
21717    }
21718    // Array concat.
21719    if first.as_array_vec().is_some() || first.as_array_ref().is_some() {
21720        let mut out = Vec::new();
21721        for v in &chunks {
21722            out.extend(v.map_flatten_outputs(true));
21723        }
21724        return StrykeValue::array(out);
21725    }
21726    // String concat.
21727    if first.is_string_like() {
21728        let mut out = String::new();
21729        for v in &chunks {
21730            out.push_str(&v.to_string());
21731        }
21732        return StrykeValue::string(out);
21733    }
21734    // Fallback: flat list of chunk results.
21735    StrykeValue::array(chunks)
21736}
21737
21738/// Decrement has no magic counterpart in Perl 5; this helper is for `++`
21739/// only.
21740fn perl_magic_str_inc(s: &str) -> Option<String> {
21741    if s.is_empty() {
21742        return Some("1".to_string());
21743    }
21744    let bytes = s.as_bytes();
21745    let mut i = 0;
21746    while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
21747        i += 1;
21748    }
21749    let letters_end = i;
21750    while i < bytes.len() && bytes[i].is_ascii_digit() {
21751        i += 1;
21752    }
21753    if i != bytes.len() {
21754        return None;
21755    }
21756    if letters_end == 0 {
21757        // Pure digits: Perl handles these as plain numbers, so defer.
21758        return None;
21759    }
21760
21761    let mut result: Vec<u8> = bytes.to_vec();
21762    let mut carry = true;
21763    let mut idx = result.len();
21764
21765    // Phase 1: digits, right to left.
21766    while carry && idx > letters_end {
21767        idx -= 1;
21768        if result[idx] == b'9' {
21769            result[idx] = b'0';
21770            // carry stays true
21771        } else {
21772            result[idx] += 1;
21773            carry = false;
21774        }
21775    }
21776
21777    // Phase 2: letters, right to left.
21778    while carry && idx > 0 {
21779        idx -= 1;
21780        let c = result[idx];
21781        if c == b'z' {
21782            result[idx] = b'a';
21783        } else if c == b'Z' {
21784            result[idx] = b'A';
21785        } else {
21786            result[idx] += 1;
21787            carry = false;
21788        }
21789    }
21790
21791    // Phase 3: prepend a fresh letter if the carry escaped.
21792    if carry {
21793        let prepend = if bytes[0].is_ascii_uppercase() {
21794            b'A'
21795        } else {
21796            b'a'
21797        };
21798        let mut grown = Vec::with_capacity(result.len() + 1);
21799        grown.push(prepend);
21800        grown.extend_from_slice(&result);
21801        return String::from_utf8(grown).ok();
21802    }
21803
21804    String::from_utf8(result).ok()
21805}
21806
21807/// `++$x` semantics: try magic string increment first when the value is
21808/// already a string; fall back to a numeric +1 for everything else
21809/// (integers, floats, undef, plain numeric strings).
21810pub(crate) fn perl_inc(v: &StrykeValue) -> StrykeValue {
21811    if let Some(s) = v.as_str() {
21812        if let Some(new_s) = perl_magic_str_inc(&s) {
21813            return StrykeValue::string(new_s);
21814        }
21815    }
21816    StrykeValue::integer(v.to_int() + 1)
21817}
21818
21819fn perl_exponent_form(rust_repr: &str, upper: bool) -> String {
21820    let marker = if upper { 'E' } else { 'e' };
21821    if let Some(pos) = rust_repr.find(marker) {
21822        let (mantissa, after) = rust_repr.split_at(pos);
21823        let exp_part = &after[1..]; // skip the 'e' / 'E'
21824        let (sign, digits) = match exp_part.chars().next() {
21825            Some('+') => ("+", &exp_part[1..]),
21826            Some('-') => ("-", &exp_part[1..]),
21827            _ => ("+", exp_part),
21828        };
21829        let padded = if digits.len() < 2 {
21830            format!("0{}", digits)
21831        } else {
21832            digits.to_string()
21833        };
21834        return format!("{}{}{}{}", mantissa, marker, sign, padded);
21835    }
21836    rust_repr.to_string()
21837}
21838
21839/// Hex-float format (`%a` / `%A`). Produces strings like `0x1.8p+0` for
21840/// 1.5 — sign, normalized hex mantissa, then `p[+-]N` decimal exponent of
21841/// the radix-2 form. Matches C99 / POSIX `%a`.
21842fn perl_hex_float(n: f64, upper: bool) -> String {
21843    if n.is_nan() {
21844        return if upper { "NAN" } else { "nan" }.to_string();
21845    }
21846    if n.is_infinite() {
21847        let sign = if n.is_sign_negative() { "-" } else { "" };
21848        let body = if upper { "INF" } else { "inf" };
21849        return format!("{}{}", sign, body);
21850    }
21851    let prefix = if upper { "0X" } else { "0x" };
21852    let p_letter = if upper { 'P' } else { 'p' };
21853    let bits = n.to_bits();
21854    let sign_bit = bits >> 63;
21855    let exp_bits = (bits >> 52) & 0x7FF;
21856    let mant_bits = bits & 0x000F_FFFF_FFFF_FFFF;
21857    let sign_str = if sign_bit == 1 { "-" } else { "" };
21858    if exp_bits == 0 && mant_bits == 0 {
21859        return format!("{}{}{}{}{}", sign_str, prefix, "0", p_letter, "+0");
21860    }
21861    let (lead_digit, exp_unbiased): (u64, i32) = if exp_bits == 0 {
21862        // Subnormal: implicit leading 0, exponent fixed at -1022.
21863        (0, -1022)
21864    } else {
21865        (1, (exp_bits as i32) - 1023)
21866    };
21867    let exp_sign = if exp_unbiased >= 0 { "+" } else { "-" };
21868    let exp_abs = exp_unbiased.unsigned_abs();
21869    if mant_bits == 0 {
21870        return format!(
21871            "{}{}{}{}{}{}",
21872            sign_str, prefix, lead_digit, p_letter, exp_sign, exp_abs
21873        );
21874    }
21875    // 52 mantissa bits = 13 hex digits.
21876    let mant_hex = format!("{:013x}", mant_bits);
21877    let trimmed = mant_hex.trim_end_matches('0');
21878    let mant_str = if upper {
21879        trimmed.to_uppercase()
21880    } else {
21881        trimmed.to_string()
21882    };
21883    format!(
21884        "{}{}{}.{}{}{}{}",
21885        sign_str, prefix, lead_digit, mant_str, p_letter, exp_sign, exp_abs
21886    )
21887}
21888
21889/// Format a value with `%g`-style "shortest of %e or %f, strip trailing
21890/// zeros". Precision is the number of *significant* digits (default 6).
21891fn perl_g_form(n: f64, prec: usize, upper: bool) -> String {
21892    let prec = prec.max(1);
21893    if !n.is_finite() {
21894        return if upper {
21895            format!("{}", n).to_uppercase()
21896        } else {
21897            format!("{}", n)
21898        };
21899    }
21900    // Compute base-10 exponent.
21901    let abs = n.abs();
21902    let x = if abs == 0.0 {
21903        0i32
21904    } else {
21905        abs.log10().floor() as i32
21906    };
21907    // %g rule: use exponential form if x < -4 OR x >= prec.
21908    let use_e = x < -4 || x >= prec as i32;
21909    // Always work in lowercase-`e` form internally so the trim logic has a
21910    // single shape; upcase the marker letter at the end for `%G`.
21911    let formatted = if use_e {
21912        let raw = format!("{:.*e}", prec - 1, n);
21913        perl_exponent_form(&raw, false)
21914    } else {
21915        let f_prec = (prec as i32 - 1 - x).max(0) as usize;
21916        format!("{:.*}", f_prec, n)
21917    };
21918    // Strip trailing zeros from the fractional part (and a trailing '.'),
21919    // but only on the mantissa side — leave the exponent untouched.
21920    let (mant, exp) = if let Some(pos) = formatted.find('e') {
21921        (formatted[..pos].to_string(), formatted[pos..].to_string())
21922    } else {
21923        (formatted.clone(), String::new())
21924    };
21925    let trimmed = if mant.contains('.') {
21926        let t = mant.trim_end_matches('0');
21927        let t = t.trim_end_matches('.');
21928        t.to_string()
21929    } else {
21930        mant
21931    };
21932    let combined = format!("{}{}", trimmed, exp);
21933    if upper {
21934        combined.replace('e', "E")
21935    } else {
21936        combined
21937    }
21938}
21939
21940/// Public sprintf entry point. Returns the formatted string plus the list
21941/// of `%n` store-targets and counts that the caller should apply via
21942/// [`VMHelper::assign_scalar_ref_deref`]. Callers that don't use `%n`
21943/// can ignore the second tuple element.
21944pub(crate) fn perl_sprintf_format_full<F>(
21945    fmt: &str,
21946    args: &[StrykeValue],
21947    string_for_s: &mut F,
21948) -> Result<(String, Vec<(StrykeValue, i64)>), FlowOrError>
21949where
21950    F: FnMut(&StrykeValue) -> Result<String, FlowOrError>,
21951{
21952    let mut pending_n: Vec<(StrykeValue, i64)> = Vec::new();
21953    let mut result = String::new();
21954    let mut arg_idx = 0;
21955    let chars: Vec<char> = fmt.chars().collect();
21956    let mut i = 0;
21957
21958    // Helper to consume the next arg as an i64 (used for `*` width / precision).
21959    let take_arg_int = |args: &[StrykeValue], idx: &mut usize| -> i64 {
21960        let v = args.get(*idx).cloned().unwrap_or(StrykeValue::UNDEF);
21961        *idx += 1;
21962        v.to_int()
21963    };
21964
21965    while i < chars.len() {
21966        if chars[i] == '%' {
21967            i += 1;
21968            if i >= chars.len() {
21969                break;
21970            }
21971            if chars[i] == '%' {
21972                result.push('%');
21973                i += 1;
21974                continue;
21975            }
21976
21977            // Positional `%N$...`: take this conversion's value from args[N-1]
21978            // instead of advancing the sequential cursor. Must be the very
21979            // first thing after `%`. We peek for `digits$` and rewind if the
21980            // `$` isn't there (the digits could just be a width).
21981            let mut positional: Option<usize> = None;
21982            {
21983                let saved = i;
21984                let mut digits = String::new();
21985                let mut j = i;
21986                while j < chars.len() && chars[j].is_ascii_digit() {
21987                    digits.push(chars[j]);
21988                    j += 1;
21989                }
21990                if j < chars.len() && chars[j] == '$' && !digits.is_empty() {
21991                    if let Ok(n) = digits.parse::<usize>() {
21992                        if n >= 1 {
21993                            positional = Some(n - 1);
21994                            i = j + 1; // consume the digits and the '$'
21995                        }
21996                    }
21997                }
21998                if positional.is_none() {
21999                    i = saved;
22000                }
22001            }
22002
22003            // Parse format specifier
22004            let mut flags = String::new();
22005            while i < chars.len() && "-+ #0".contains(chars[i]) {
22006                flags.push(chars[i]);
22007                i += 1;
22008            }
22009            // Vector flag: `v` (separator = ".") or `*v` (separator = next arg).
22010            // When set, the conversion runs once per byte of the value's
22011            // string form, joining results with the separator.
22012            let mut vector_sep: Option<String> = None;
22013            if i < chars.len() && chars[i] == 'v' {
22014                vector_sep = Some(".".to_string());
22015                i += 1;
22016            } else if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == 'v' {
22017                let sep_arg = args.get(arg_idx).cloned().unwrap_or(StrykeValue::UNDEF);
22018                arg_idx += 1;
22019                vector_sep = Some(sep_arg.to_string());
22020                i += 2;
22021            }
22022            // Width: either `*` (consume an arg) or run of digits.
22023            let mut width = String::new();
22024            let mut left_align = flags.contains('-');
22025            if i < chars.len() && chars[i] == '*' {
22026                let n = take_arg_int(args, &mut arg_idx);
22027                if n < 0 {
22028                    // Negative width means left-align with |n|.
22029                    left_align = true;
22030                    width = (-n).to_string();
22031                } else {
22032                    width = n.to_string();
22033                }
22034                i += 1;
22035            } else {
22036                while i < chars.len() && chars[i].is_ascii_digit() {
22037                    width.push(chars[i]);
22038                    i += 1;
22039                }
22040            }
22041            // Precision: `.*` or `.<digits>` (or nothing).
22042            let mut precision = String::new();
22043            if i < chars.len() && chars[i] == '.' {
22044                i += 1;
22045                if i < chars.len() && chars[i] == '*' {
22046                    let n = take_arg_int(args, &mut arg_idx);
22047                    precision = n.max(0).to_string();
22048                    i += 1;
22049                } else {
22050                    while i < chars.len() && chars[i].is_ascii_digit() {
22051                        precision.push(chars[i]);
22052                        i += 1;
22053                    }
22054                    // ".<no digits>" means precision 0 (Perl/C convention).
22055                    if precision.is_empty() {
22056                        precision = "0".to_string();
22057                    }
22058                }
22059            }
22060            if i >= chars.len() {
22061                break;
22062            }
22063            let spec = chars[i];
22064            i += 1;
22065
22066            // For vector conversions the conversion's value-arg is the
22067            // string whose bytes we'll iterate; for non-vector, it's the
22068            // value we format. Either way the index resolution is the
22069            // same: positional or sequential.
22070            let arg = if let Some(idx) = positional {
22071                args.get(idx).cloned().unwrap_or(StrykeValue::UNDEF)
22072            } else {
22073                let v = args.get(arg_idx).cloned().unwrap_or(StrykeValue::UNDEF);
22074                arg_idx += 1;
22075                v
22076            };
22077
22078            let w: usize = width.parse().unwrap_or(0);
22079            let p: usize = precision.parse().unwrap_or(6);
22080
22081            let zero_pad = flags.contains('0') && !left_align;
22082            let plus = flags.contains('+');
22083            let space = flags.contains(' ');
22084            let hash = flags.contains('#');
22085
22086            // Apply width + alignment to a body string. Honors zero-pad for
22087            // numerics (caller passes the raw signed body so we can splice
22088            // zeros after the sign).
22089            let pad_align = |body: &str, width: usize, left: bool, zero: bool| -> String {
22090                if width == 0 || body.len() >= width {
22091                    return body.to_string();
22092                }
22093                if zero && !left {
22094                    if let Some(rest) = body.strip_prefix('-') {
22095                        return format!("-{:0>width$}", rest, width = width - 1);
22096                    }
22097                    if let Some(rest) = body.strip_prefix('+') {
22098                        return format!("+{:0>width$}", rest, width = width - 1);
22099                    }
22100                    return format!("{:0>width$}", body, width = width);
22101                }
22102                if left {
22103                    format!("{:<width$}", body, width = width)
22104                } else {
22105                    format!("{:>width$}", body, width = width)
22106                }
22107            };
22108
22109            // Format a single integer with the inner spec for `%v...`. No
22110            // width/precision is applied here — those are deferred to the
22111            // joined result. Supports the common int-shape conversions.
22112            let format_int_for_vector = |n: i64, spec: char| -> String {
22113                match spec {
22114                    'd' | 'i' => format!("{}", n),
22115                    'u' => format!("{}", n as u64),
22116                    'x' => {
22117                        if hash && n != 0 {
22118                            format!("0x{:x}", n)
22119                        } else {
22120                            format!("{:x}", n)
22121                        }
22122                    }
22123                    'X' => {
22124                        if hash && n != 0 {
22125                            format!("0X{:X}", n)
22126                        } else {
22127                            format!("{:X}", n)
22128                        }
22129                    }
22130                    'o' => {
22131                        if hash && n != 0 {
22132                            format!("0{:o}", n)
22133                        } else {
22134                            format!("{:o}", n)
22135                        }
22136                    }
22137                    'b' => {
22138                        if hash && n != 0 {
22139                            format!("0b{:b}", n)
22140                        } else {
22141                            format!("{:b}", n)
22142                        }
22143                    }
22144                    'c' => char::from_u32(n as u32)
22145                        .map(|c| c.to_string())
22146                        .unwrap_or_default(),
22147                    _ => format!("{}", n),
22148                }
22149            };
22150
22151            // `%v` short-circuit: format each byte of the arg's string form
22152            // with the inner spec, join with `vector_sep`, then pad/align
22153            // the joined string. Skips the regular per-spec match below.
22154            if let Some(ref sep) = vector_sep {
22155                let s = arg.to_string();
22156                let parts: Vec<String> = s
22157                    .bytes()
22158                    .map(|b| format_int_for_vector(b as i64, spec))
22159                    .collect();
22160                let body = parts.join(sep);
22161                let final_body = if width.is_empty() {
22162                    body
22163                } else if left_align {
22164                    format!("{:<width$}", body, width = w)
22165                } else {
22166                    format!("{:>width$}", body, width = w)
22167                };
22168                result.push_str(&final_body);
22169                continue;
22170            }
22171
22172            let formatted = match spec {
22173                'd' | 'i' => {
22174                    let v = arg.to_int();
22175                    let body = if plus && v >= 0 {
22176                        format!("+{}", v)
22177                    } else if space && v >= 0 {
22178                        format!(" {}", v)
22179                    } else {
22180                        format!("{}", v)
22181                    };
22182                    pad_align(&body, w, left_align, zero_pad)
22183                }
22184                'u' => {
22185                    let v = arg.to_int() as u64;
22186                    pad_align(&format!("{}", v), w, left_align, zero_pad)
22187                }
22188                'f' => {
22189                    let n = arg.to_number();
22190                    let body = if plus && n.is_sign_positive() {
22191                        format!("+{:.*}", p, n)
22192                    } else if space && n.is_sign_positive() {
22193                        format!(" {:.*}", p, n)
22194                    } else {
22195                        format!("{:.*}", p, n)
22196                    };
22197                    pad_align(&body, w, left_align, zero_pad)
22198                }
22199                'e' => {
22200                    let n = arg.to_number();
22201                    let raw = format!("{:.*e}", p, n);
22202                    let body0 = perl_exponent_form(&raw, false);
22203                    let body = if plus && n.is_sign_positive() {
22204                        format!("+{}", body0)
22205                    } else if space && n.is_sign_positive() {
22206                        format!(" {}", body0)
22207                    } else {
22208                        body0
22209                    };
22210                    pad_align(&body, w, left_align, zero_pad)
22211                }
22212                'E' => {
22213                    let n = arg.to_number();
22214                    let raw = format!("{:.*E}", p, n);
22215                    let body0 = perl_exponent_form(&raw, true);
22216                    let body = if plus && n.is_sign_positive() {
22217                        format!("+{}", body0)
22218                    } else if space && n.is_sign_positive() {
22219                        format!(" {}", body0)
22220                    } else {
22221                        body0
22222                    };
22223                    pad_align(&body, w, left_align, zero_pad)
22224                }
22225                'g' => {
22226                    let n = arg.to_number();
22227                    // For %g, precision means "significant digits" (default 6).
22228                    let prec_g = if precision.is_empty() { 6 } else { p };
22229                    let body0 = perl_g_form(n, prec_g, false);
22230                    let body = if plus && n.is_sign_positive() {
22231                        format!("+{}", body0)
22232                    } else if space && n.is_sign_positive() {
22233                        format!(" {}", body0)
22234                    } else {
22235                        body0
22236                    };
22237                    pad_align(&body, w, left_align, zero_pad)
22238                }
22239                'G' => {
22240                    let n = arg.to_number();
22241                    let prec_g = if precision.is_empty() { 6 } else { p };
22242                    let body0 = perl_g_form(n, prec_g, true);
22243                    let body = if plus && n.is_sign_positive() {
22244                        format!("+{}", body0)
22245                    } else if space && n.is_sign_positive() {
22246                        format!(" {}", body0)
22247                    } else {
22248                        body0
22249                    };
22250                    pad_align(&body, w, left_align, zero_pad)
22251                }
22252                's' => {
22253                    let s = string_for_s(&arg)?;
22254                    let body = if !precision.is_empty() {
22255                        s.chars().take(p).collect::<String>()
22256                    } else {
22257                        s
22258                    };
22259                    if left_align {
22260                        format!("{:<width$}", body, width = w)
22261                    } else {
22262                        format!("{:>width$}", body, width = w)
22263                    }
22264                }
22265                'x' => {
22266                    let v = arg.to_int();
22267                    let body = if hash && v != 0 {
22268                        format!("0x{:x}", v)
22269                    } else {
22270                        format!("{:x}", v)
22271                    };
22272                    pad_align(&body, w, left_align, zero_pad)
22273                }
22274                'X' => {
22275                    let v = arg.to_int();
22276                    let body = if hash && v != 0 {
22277                        format!("0X{:X}", v)
22278                    } else {
22279                        format!("{:X}", v)
22280                    };
22281                    pad_align(&body, w, left_align, zero_pad)
22282                }
22283                'o' => {
22284                    let v = arg.to_int();
22285                    let body = if hash && v != 0 {
22286                        format!("0{:o}", v)
22287                    } else {
22288                        format!("{:o}", v)
22289                    };
22290                    pad_align(&body, w, left_align, zero_pad)
22291                }
22292                'b' => {
22293                    let v = arg.to_int();
22294                    let body = if hash && v != 0 {
22295                        format!("0b{:b}", v)
22296                    } else {
22297                        format!("{:b}", v)
22298                    };
22299                    pad_align(&body, w, left_align, zero_pad)
22300                }
22301                'c' => char::from_u32(arg.to_int() as u32)
22302                    .map(|c| c.to_string())
22303                    .unwrap_or_default(),
22304                'a' | 'A' => {
22305                    let upper = spec == 'A';
22306                    let body0 = perl_hex_float(arg.to_number(), upper);
22307                    let body = if plus && !body0.starts_with('-') {
22308                        format!("+{}", body0)
22309                    } else if space && !body0.starts_with('-') {
22310                        format!(" {}", body0)
22311                    } else {
22312                        body0
22313                    };
22314                    pad_align(&body, w, left_align, zero_pad)
22315                }
22316                'p' => {
22317                    // Stryke uses placeholder addresses for refs; emit the
22318                    // same `0x...` form here so output stays deterministic
22319                    // and machine-comparable across runs.
22320                    pad_align("0x...", w, left_align, false)
22321                }
22322                'n' => {
22323                    // Write the number of bytes emitted so far into the
22324                    // referent of the arg (must be a scalar ref, e.g.
22325                    // `\$count`). `%n` does NOT consume an output slot, so
22326                    // the formatted body is empty. The store is queued and
22327                    // applied by the caller after formatting finishes —
22328                    // works for both `HeapObject::ScalarRef` and the
22329                    // `ScalarBindingRef` shape that `\$my_var` produces.
22330                    pending_n.push((arg.clone(), result.len() as i64));
22331                    String::new()
22332                }
22333                _ => arg.to_string(),
22334            };
22335
22336            result.push_str(&formatted);
22337        } else {
22338            result.push(chars[i]);
22339            i += 1;
22340        }
22341    }
22342    Ok((result, pending_n))
22343}
22344
22345#[cfg(test)]
22346mod regex_expand_tests {
22347    use super::VMHelper;
22348
22349    #[test]
22350    fn compile_regex_quotemeta_qe_matches_literal() {
22351        let mut i = VMHelper::new();
22352        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
22353        assert!(re.is_match("a.c"));
22354        assert!(!re.is_match("abc"));
22355    }
22356
22357    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
22358    /// stay literal (not rewritten to `(?:\n?\z)`).
22359    #[test]
22360    fn compile_regex_char_class_leading_close_bracket_is_literal() {
22361        let mut i = VMHelper::new();
22362        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
22363        assert!(re.is_match("$"));
22364        assert!(re.is_match("]"));
22365        assert!(!re.is_match("x"));
22366    }
22367}
22368
22369#[cfg(test)]
22370mod special_scalar_name_tests {
22371    use super::VMHelper;
22372
22373    #[test]
22374    fn special_scalar_name_for_get_matches_magic_globals() {
22375        assert!(VMHelper::is_special_scalar_name_for_get("0"));
22376        assert!(VMHelper::is_special_scalar_name_for_get("!"));
22377        assert!(VMHelper::is_special_scalar_name_for_get("^W"));
22378        assert!(VMHelper::is_special_scalar_name_for_get("^O"));
22379        assert!(VMHelper::is_special_scalar_name_for_get("^MATCH"));
22380        assert!(VMHelper::is_special_scalar_name_for_get("<"));
22381        assert!(VMHelper::is_special_scalar_name_for_get("?"));
22382        assert!(VMHelper::is_special_scalar_name_for_get("|"));
22383        assert!(VMHelper::is_special_scalar_name_for_get("^UNICODE"));
22384        assert!(VMHelper::is_special_scalar_name_for_get("\""));
22385        assert!(!VMHelper::is_special_scalar_name_for_get("foo"));
22386        assert!(!VMHelper::is_special_scalar_name_for_get("plainvar"));
22387    }
22388
22389    #[test]
22390    fn special_scalar_name_for_set_matches_set_special_var_arms() {
22391        assert!(VMHelper::is_special_scalar_name_for_set("0"));
22392        assert!(VMHelper::is_special_scalar_name_for_set("^D"));
22393        assert!(VMHelper::is_special_scalar_name_for_set("^H"));
22394        assert!(VMHelper::is_special_scalar_name_for_set("^WARNING_BITS"));
22395        assert!(VMHelper::is_special_scalar_name_for_set("ARGV"));
22396        assert!(VMHelper::is_special_scalar_name_for_set("|"));
22397        assert!(VMHelper::is_special_scalar_name_for_set("?"));
22398        assert!(VMHelper::is_special_scalar_name_for_set("^UNICODE"));
22399        assert!(VMHelper::is_special_scalar_name_for_set("."));
22400        assert!(!VMHelper::is_special_scalar_name_for_set("foo"));
22401        assert!(!VMHelper::is_special_scalar_name_for_set("__PACKAGE__"));
22402    }
22403
22404    #[test]
22405    fn caret_and_id_specials_roundtrip_get() {
22406        let i = VMHelper::new();
22407        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
22408        assert_eq!(
22409            i.get_special_var("^V").to_string(),
22410            format!("v{}", env!("CARGO_PKG_VERSION"))
22411        );
22412        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
22413        assert!(i.get_special_var("^T").to_int() >= 0);
22414        #[cfg(unix)]
22415        {
22416            assert!(i.get_special_var("<").to_int() >= 0);
22417        }
22418    }
22419
22420    #[test]
22421    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
22422        let mut i = VMHelper::new();
22423        i.last_readline_handle.clear();
22424        i.line_number = 3;
22425        i.prepare_flip_flop_vm_slots(1);
22426        assert_eq!(
22427            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
22428            1
22429        );
22430        assert!(i.flip_flop_active[0]);
22431        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
22432        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
22433        assert_eq!(
22434            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
22435            1
22436        );
22437        assert!(i.flip_flop_active[0]);
22438    }
22439
22440    #[test]
22441    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
22442        let mut i = VMHelper::new();
22443        i.last_readline_handle.clear();
22444        i.line_number = 2;
22445        i.prepare_flip_flop_vm_slots(1);
22446        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
22447        assert!(i.flip_flop_active[0]);
22448        i.line_number = 3;
22449        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
22450        assert!(!i.flip_flop_active[0]);
22451        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
22452    }
22453}