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    /// `scope` field.
578    pub scope: Scope,
579    pub(crate) subs: HashMap<String, Arc<StrykeSub>>,
580    /// AOP advice registry — populated by `Op::RegisterAdvice` from `before|after|around` decls.
581    pub(crate) intercepts: Vec<crate::aop::Intercept>,
582    /// Auto-incremented for the next registered intercept id (1-based; matches zshrs).
583    pub(crate) next_intercept_id: u32,
584    /// Stack of active around-advice contexts; `proceed()` reads the top frame.
585    pub(crate) intercept_ctx_stack: Vec<crate::aop::InterceptCtx>,
586    /// Re-entrancy guard: while running advice for a name, calling that same name from inside
587    /// the body skips advice and runs the original directly. Prevents infinite recursion when
588    /// an advice body uses the same sub it advises.
589    pub(crate) intercept_active_names: Vec<String>,
590    pub(crate) file: String,
591    /// File handles: name → writer
592    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
593    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
594    /// Output separator ($,)
595    pub ofs: String,
596    /// Output record separator ($\)
597    pub ors: String,
598    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
599    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
600    pub irs: Option<String>,
601    /// $! — last OS error
602    pub errno: String,
603    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
604    pub errno_code: i32,
605    /// $@ — last eval error (string)
606    pub eval_error: String,
607    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
608    pub eval_error_code: i32,
609    /// When `die` is called with a ref argument, the ref value is preserved here.
610    pub eval_error_value: Option<StrykeValue>,
611    /// @ARGV
612    pub argv: Vec<String>,
613    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
614    pub env: IndexMap<String, StrykeValue>,
615    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
616    pub env_materialized: bool,
617    /// $0
618    pub program_name: String,
619    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle).
620    /// In the `-n`/`-p` loop this is the per-file record number (awk `FNR`) — it is
621    /// reset to 0 at each input-file boundary in `main.rs`.
622    pub line_number: i64,
623    /// Cumulative record number across all input files in the `-n`/`-p` loop (awk `NR`).
624    /// Unlike `line_number` it is never reset at a file boundary.
625    pub nr: i64,
626    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
627    pub last_readline_handle: String,
628    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
629    pub(crate) last_stdin_die_bracket: String,
630    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
631    pub handle_line_numbers: HashMap<String, i64>,
632    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
633    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
634    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
635    pub(crate) flip_flop_active: Vec<bool>,
636    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
637    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
638    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
639    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
640    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
641    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
642    pub(crate) flip_flop_sequence: Vec<i64>,
643    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
644    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
645    /// range on one line return the same sequence number).
646    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
647    /// Scalar `..` / `...` flip-flop state (key: `Expr` address).
648    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
649    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
650    pub sigint_pending_caret: Cell<bool>,
651    /// Auto-split mode (-a)
652    pub auto_split: bool,
653    /// Field separator for -F
654    pub field_separator: Option<String>,
655    /// BEGIN blocks
656    begin_blocks: Vec<Block>,
657    /// `UNITCHECK` blocks (LIFO at run)
658    unit_check_blocks: Vec<Block>,
659    /// `CHECK` blocks (LIFO at run)
660    check_blocks: Vec<Block>,
661    /// `INIT` blocks (FIFO at run)
662    init_blocks: Vec<Block>,
663    /// END blocks
664    end_blocks: Vec<Block>,
665    /// -w warnings / `use warnings` / `$^W`
666    pub warnings: bool,
667    /// Output autoflush (`$|`).
668    pub output_autoflush: bool,
669    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
670    pub default_print_handle: String,
671    /// Suppress stdout output (fan workers with progress bars).
672    pub suppress_stdout: bool,
673    /// Per-instance test counters for `assert_*` / `test_run` / `test_skip` (stryke
674    /// `.stk` test framework). Atomics so the immutable-ref builtin signature
675    /// (`fn(&VMHelper, ...)`) can mutate without changing every call site. Replaces
676    /// the previous `AtomicUsize` process-globals which leaked counts across runs
677    /// in a single process — embedders running multiple `.stk` programs in one
678    /// `VMHelper` would see the previous run's counts contaminate the next.
679    pub test_pass_count: std::sync::atomic::AtomicUsize,
680    /// `test_fail_count` field.
681    pub test_fail_count: std::sync::atomic::AtomicUsize,
682    /// `test_skip_count` field.
683    pub test_skip_count: std::sync::atomic::AtomicUsize,
684    /// Cumulative-across-the-whole-run counters. The `test_pass_count` /
685    /// `test_fail_count` / `test_skip_count` triplet above is reset by
686    /// `test_run` after it prints its per-block summary, so external
687    /// embedders (the worker-pool test runner reading counts after
688    /// `execute()` returns) see 0 if any test in the file already
689    /// finished a `test_run` block. The `_total` counters are summed-in
690    /// by `test_run` *before* the reset, never themselves reset, and
691    /// give the harness an honest "how many assertions did this run
692    /// actually do" number.
693    pub test_pass_total: std::sync::atomic::AtomicUsize,
694    /// `test_fail_total` field.
695    pub test_fail_total: std::sync::atomic::AtomicUsize,
696    /// `test_skip_total` field.
697    pub test_skip_total: std::sync::atomic::AtomicUsize,
698    /// Set to `true` by `test_run` when any assertion failed during the run.
699    /// CLI driver (`main.rs`) reads this after `execute` returns and exits with
700    /// code 1. Replaces the previous in-VM `std::process::exit(1)` which made
701    /// embedding (running a `.stk` program from a Rust harness) impossible —
702    /// any failing test would kill the host process.
703    pub test_run_failed: std::sync::atomic::AtomicBool,
704    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
705    pub child_exit_status: i64,
706    /// Last successful match (`$&`, `${^MATCH}`).
707    pub last_match: String,
708    /// Before match (`` $` ``, `${^PREMATCH}`).
709    pub prematch: String,
710    /// After match (`$'`, `${^POSTMATCH}`).
711    pub postmatch: String,
712    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
713    pub last_paren_match: String,
714    /// List separator for array stringification in concatenation / interpolation (`$"`).
715    pub list_separator: String,
716    /// Script start time (`$^T`) — seconds since Unix epoch.
717    pub script_start_time: i64,
718    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
719    pub compile_hints: i64,
720    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
721    pub warning_bits: i64,
722    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
723    pub global_phase: String,
724    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
725    pub subscript_sep: String,
726    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
727    /// The `stryke` driver sets this from `-i` / `-i.ext`.
728    pub inplace_edit: String,
729    /// `$^D` — debugging flags (integer; mostly ignored).
730    pub debug_flags: i64,
731    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
732    pub perl_debug_flags: i64,
733    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
734    pub eval_nesting: u32,
735    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
736    pub argv_current_file: String,
737    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
738    pub(crate) diamond_next_idx: usize,
739    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
740    pub(crate) diamond_reader: Option<BufReader<File>>,
741    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
742    pub strict_refs: bool,
743    /// `strict_subs` field.
744    pub strict_subs: bool,
745    /// `strict_vars` field.
746    pub strict_vars: bool,
747    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
748    pub utf8_pragma: bool,
749    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
750    pub open_pragma_utf8: bool,
751    /// `use feature` — bit flags (`FEAT_*`).
752    pub feature_bits: u64,
753    /// Number of parallel threads
754    pub num_threads: usize,
755    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
756    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
757    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
758    /// Third flag: `$*` multiline (prepends `(?s)` when true).
759    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
760    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
761    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
762    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
763    /// scope population entirely on cache hit.
764    ///
765    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
766    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
767    /// capture-var side-effect replay is forced on the next hit.
768    regex_match_memo: Option<RegexMatchMemo>,
769    /// False when the user (or some non-regex code path) has written to one of the capture
770    /// variables since the last `apply_regex_captures` call. The memoized match result is still
771    /// valid, but the scope side effects need to be reapplied on the next hit.
772    regex_capture_scope_fresh: bool,
773    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
774    pub(crate) regex_pos: HashMap<String, Option<usize>>,
775    /// Persistent storage for `state` variables, keyed by "line:name".
776    pub(crate) state_vars: HashMap<String, StrykeValue>,
777    /// Per-frame tracking of state variable bindings: (var_name, state_key).
778    pub(crate) state_bindings_stack: Vec<Vec<(String, String)>>,
779    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
780    pub(crate) rand_rng: StdRng,
781    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
782    pub(crate) dir_handles: HashMap<String, DirHandleState>,
783    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
784    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
785    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
786    pub(crate) pipe_children: HashMap<String, Child>,
787    /// Sockets from `socket` / `accept` / `connect`.
788    pub(crate) socket_handles: HashMap<String, StrykeSocket>,
789    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
790    pub(crate) wantarray_kind: WantarrayCtx,
791    /// `struct Name { ... }` definitions (merged from VM chunks).
792    pub struct_defs: HashMap<String, Arc<StructDef>>,
793    /// `enum Name { ... }` definitions (merged from VM chunks).
794    pub enum_defs: HashMap<String, Arc<EnumDef>>,
795    /// `class Name extends ... impl ... { ... }` definitions.
796    pub class_defs: HashMap<String, Arc<ClassDef>>,
797    /// `trait Name { ... }` definitions.
798    pub trait_defs: HashMap<String, Arc<TraitDef>>,
799    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
800    /// call/return (JIT disabled); per-statement lines and subs.
801    pub profiler: Option<Profiler>,
802    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
803    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
804    /// Virtual modules: path → source (for AOT bundles). Checked before filesystem in `require`.
805    pub(crate) virtual_modules: HashMap<String, String>,
806    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
807    pub(crate) tied_hashes: HashMap<String, StrykeValue>,
808    /// `tie $name` — TIESCALAR object for FETCH/STORE.
809    pub(crate) tied_scalars: HashMap<String, StrykeValue>,
810    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
811    pub(crate) tied_arrays: HashMap<String, StrykeValue>,
812    /// `use overload` — class → Perl overload key → short method name in that package.
813    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
814    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
815    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
816    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
817    pub(crate) special_caret_scalars: HashMap<String, StrykeValue>,
818    /// `$%` — format output page number.
819    pub format_page_number: i64,
820    /// `$=` — format lines per page.
821    pub format_lines_per_page: i64,
822    /// `$-` — lines remaining on format page.
823    pub format_lines_left: i64,
824    /// `$:` — characters to break format lines (Perl default `\n`).
825    pub format_line_break_chars: String,
826    /// `$^` — top-of-form format name.
827    pub format_top_name: String,
828    /// `$^A` — format write accumulator.
829    pub accumulator_format: String,
830    /// `$^F` — max system file descriptor (Perl default 2).
831    pub max_system_fd: i64,
832    /// `$^M` — emergency memory buffer (no-op pool in stryke).
833    pub emergency_memory: String,
834    /// `$^N` — last opened named regexp capture name.
835    pub last_subpattern_name: String,
836    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
837    pub inc_hook_index: i64,
838    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
839    pub multiline_match: bool,
840    /// `$^X` — path to this executable (cached).
841    pub executable_path: String,
842    /// `$^L` — formfeed string for formats (Perl default `\f`).
843    pub formfeed_string: String,
844    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
845    pub(crate) glob_handle_alias: HashMap<String, String>,
846    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
847    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
848    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
849    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
850    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
851    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
852    pub(crate) special_var_restore_frames: Vec<Vec<(String, StrykeValue)>>,
853    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
854    /// Lazy-init flag: reflection hashes (`%b`, `%stryke::builtins`, etc.)
855    /// are only built on first access to avoid startup cost.
856    pub(crate) reflection_hashes_ready: bool,
857    pub(crate) english_enabled: bool,
858    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
859    pub(crate) english_no_match_vars: bool,
860    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
861    /// available for the rest of the program — Perl exports them into the caller's namespace
862    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
863    pub(crate) english_match_vars_ever_enabled: bool,
864    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
865    english_lexical_scalars: Vec<HashSet<String>>,
866    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
867    our_lexical_scalars: Vec<HashSet<String>>,
868    /// Bare names from `our @arr` per frame — drives package qualification in
869    /// [`Self::tree_array_storage_name`] so `our @x` reads route through the
870    /// package stash while `my @x` stays lexical.
871    our_lexical_arrays: Vec<HashSet<String>>,
872    /// Bare names from `our %h` per frame — companion to
873    /// [`Self::our_lexical_arrays`].
874    our_lexical_hashes: Vec<HashSet<String>>,
875    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
876    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
877    pub vm_jit_enabled: bool,
878    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
879    pub disasm_bytecode: bool,
880    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from a bytecode cache hit. When
881    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
882    /// (`.take()`) on first read so re-entry compiles normally.
883    pub cached_chunk: Option<crate::bytecode::Chunk>,
884    /// Sideband: script path for bytecode cache save after compilation (mtime-based).
885    pub cache_script_path: Option<std::path::PathBuf>,
886    /// Interpreter base for relative filesystem paths (`cd` updates this; OS `chdir` does not).
887    pub(crate) stryke_pwd: PathBuf,
888    /// Set while stepping a `gen { }` body (`yield`).
889    pub(crate) in_generator: bool,
890    /// `-n`/`-p` driver: prelude only; body runs per line in [`Self::process_line_vm`].
891    pub line_mode_skip_main: bool,
892    /// Pre-compiled chunk for `-n`/`-p` line mode. Stored after the prelude `execute()` call
893    /// so `process_line_vm` can re-execute the body portion per input line.
894    pub line_mode_chunk: Option<crate::bytecode::Chunk>,
895    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
896    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
897    /// matches Perl (true on the last line of that source).
898    pub(crate) line_mode_eof_pending: bool,
899    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
900    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
901    pub line_mode_stdin_pending: VecDeque<String>,
902    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
903    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
904    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
905    pub(crate) log_level_override: Option<LogLevelFilter>,
906    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
907    /// Pushed on `call_sub` entry, popped on exit.
908    pub(crate) current_sub_stack: Vec<Arc<StrykeSub>>,
909    /// Interactive debugger state (`-d` flag).
910    pub debugger: Option<crate::debugger::Debugger>,
911    /// Call stack for debugger: (sub_name, call_line).
912    pub(crate) debug_call_stack: Vec<(String, usize)>,
913}
914
915/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
916#[derive(Debug, Clone, Default)]
917pub struct ReplCompletionSnapshot {
918    /// `subs` field.
919    pub subs: Vec<String>,
920    /// `blessed_scalars` field.
921    pub blessed_scalars: HashMap<String, String>,
922    /// `isa_for_class` field.
923    pub isa_for_class: HashMap<String, Vec<String>>,
924}
925
926impl ReplCompletionSnapshot {
927    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
928    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
929        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
930        let mro = linearize_c3(class, &parents, 0);
931        let mut names = HashSet::new();
932        for pkg in &mro {
933            if pkg == "UNIVERSAL" {
934                continue;
935            }
936            let prefix = format!("{}::", pkg);
937            for k in &self.subs {
938                if k.starts_with(&prefix) {
939                    let rest = &k[prefix.len()..];
940                    if !rest.contains("::") {
941                        names.insert(rest.to_string());
942                    }
943                }
944            }
945        }
946        for k in &self.subs {
947            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
948                if !rest.contains("::") {
949                    names.insert(rest.to_string());
950                }
951            }
952        }
953        let mut v: Vec<String> = names.into_iter().collect();
954        v.sort();
955        v
956    }
957}
958
959fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
960    let left = left.trim_end();
961    if left.is_empty() {
962        return None;
963    }
964    if let Some(i) = left.rfind('$') {
965        let name = left[i + 1..].trim();
966        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
967            return state.blessed_scalars.get(name).cloned();
968        }
969    }
970    let tok = left.split_whitespace().last()?;
971    if tok.contains("::") {
972        return Some(tok.to_string());
973    }
974    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
975        return Some(tok.to_string());
976    }
977    None
978}
979
980/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
981pub fn repl_arrow_method_completions(
982    state: &ReplCompletionSnapshot,
983    line: &str,
984    pos: usize,
985) -> Option<(usize, Vec<String>)> {
986    let pos = pos.min(line.len());
987    let before = &line[..pos];
988    let arrow_idx = before.rfind("->")?;
989    let after_arrow = &before[arrow_idx + 2..];
990    let rest = after_arrow.trim_start();
991    let ws_len = after_arrow.len() - rest.len();
992    let method_start = arrow_idx + 2 + ws_len;
993    let method_prefix = &line[method_start..pos];
994    if !method_prefix
995        .chars()
996        .all(|c| c.is_alphanumeric() || c == '_')
997    {
998        return None;
999    }
1000    let left = line[..arrow_idx].trim_end();
1001    let class = repl_resolve_class_for_arrow(state, left)?;
1002    let mut methods = state.methods_for_class(&class);
1003    methods.retain(|m| m.starts_with(method_prefix));
1004    Some((method_start, methods))
1005}
1006
1007/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
1008#[derive(Debug, Clone, Default)]
1009pub(crate) struct ModuleExportLists {
1010    /// Default imports for `use Module` with no list.
1011    pub export: Vec<String>,
1012    /// Extra symbols allowed in `use Module qw(name)`.
1013    pub export_ok: Vec<String>,
1014}
1015
1016/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
1017fn piped_shell_command(cmd: &str) -> Command {
1018    if cfg!(windows) {
1019        let mut c = Command::new("cmd");
1020        c.arg("/C").arg(cmd);
1021        c
1022    } else {
1023        let mut c = Command::new("sh");
1024        c.arg("-c").arg(cmd);
1025        c
1026    }
1027}
1028
1029/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
1030/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
1031/// so the Rust `regex` crate can match them.
1032/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
1033/// so the Rust regex crate can match NUL and other octal-specified bytes.
1034/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
1035fn expand_perl_regex_octal_escapes(pat: &str) -> String {
1036    let mut out = String::with_capacity(pat.len());
1037    let mut it = pat.chars().peekable();
1038    while let Some(c) = it.next() {
1039        if c == '\\' {
1040            if let Some(&'0') = it.peek() {
1041                // Collect up to 3 octal digits starting with '0'
1042                let mut oct = String::new();
1043                while oct.len() < 3 {
1044                    if let Some(&d) = it.peek() {
1045                        if ('0'..='7').contains(&d) {
1046                            oct.push(d);
1047                            it.next();
1048                        } else {
1049                            break;
1050                        }
1051                    } else {
1052                        break;
1053                    }
1054                }
1055                if let Ok(val) = u8::from_str_radix(&oct, 8) {
1056                    out.push_str(&format!("\\x{:02x}", val));
1057                } else {
1058                    out.push('\\');
1059                    out.push_str(&oct);
1060                }
1061                continue;
1062            }
1063        }
1064        out.push(c);
1065    }
1066    out
1067}
1068
1069fn expand_perl_regex_quotemeta(pat: &str) -> String {
1070    let mut out = String::with_capacity(pat.len().saturating_mul(2));
1071    let mut it = pat.chars().peekable();
1072    let mut in_q = false;
1073    while let Some(c) = it.next() {
1074        if in_q {
1075            if c == '\\' && it.peek() == Some(&'E') {
1076                it.next();
1077                in_q = false;
1078                continue;
1079            }
1080            out.push_str(&perl_quotemeta(&c.to_string()));
1081            continue;
1082        }
1083        if c == '\\' && it.peek() == Some(&'Q') {
1084            it.next();
1085            in_q = true;
1086            continue;
1087        }
1088        out.push(c);
1089    }
1090    out
1091}
1092
1093/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
1094///
1095/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
1096/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
1097///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
1098pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
1099    let mut out = String::with_capacity(replacement.len() + 8);
1100    let mut it = replacement.chars().peekable();
1101    while let Some(c) = it.next() {
1102        if c == '\\' {
1103            match it.peek() {
1104                Some(&d) if d.is_ascii_digit() => {
1105                    it.next();
1106                    out.push_str("${");
1107                    out.push(d);
1108                    while let Some(&d2) = it.peek() {
1109                        if !d2.is_ascii_digit() {
1110                            break;
1111                        }
1112                        it.next();
1113                        out.push(d2);
1114                    }
1115                    out.push('}');
1116                }
1117                Some(&'\\') => {
1118                    it.next();
1119                    out.push('\\');
1120                }
1121                _ => out.push('\\'),
1122            }
1123        } else if c == '$' {
1124            match it.peek() {
1125                Some(&d) if d.is_ascii_digit() => {
1126                    it.next();
1127                    out.push_str("${");
1128                    out.push(d);
1129                    while let Some(&d2) = it.peek() {
1130                        if !d2.is_ascii_digit() {
1131                            break;
1132                        }
1133                        it.next();
1134                        out.push(d2);
1135                    }
1136                    out.push('}');
1137                }
1138                Some(&'{') => {
1139                    // already braced — pass through as-is
1140                    out.push('$');
1141                }
1142                _ => out.push('$'),
1143            }
1144        } else {
1145            out.push(c);
1146        }
1147    }
1148    out
1149}
1150
1151/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
1152/// past the closing `]`.
1153fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
1154    debug_assert_eq!(chars.get(i), Some(&'['));
1155    out.push('[');
1156    i += 1;
1157    if i < chars.len() && chars[i] == '^' {
1158        out.push('^');
1159        i += 1;
1160    }
1161    if i >= chars.len() {
1162        return i;
1163    }
1164    // `]` as the first class character is literal iff another unescaped `]` closes the class
1165    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
1166    // this `]`.
1167    if chars[i] == ']' {
1168        if i + 1 < chars.len() && chars[i + 1] == ']' {
1169            // `[]]` / `[^]]`: literal `]` then the closing `]`.
1170            out.push(']');
1171            i += 1;
1172        } else {
1173            let mut scan = i + 1;
1174            let mut found_closing = false;
1175            while scan < chars.len() {
1176                if chars[scan] == '\\' && scan + 1 < chars.len() {
1177                    scan += 2;
1178                    continue;
1179                }
1180                if chars[scan] == ']' {
1181                    found_closing = true;
1182                    break;
1183                }
1184                scan += 1;
1185            }
1186            if found_closing {
1187                out.push(']');
1188                i += 1;
1189            } else {
1190                out.push(']');
1191                return i + 1;
1192            }
1193        }
1194    }
1195    while i < chars.len() && chars[i] != ']' {
1196        if chars[i] == '\\' && i + 1 < chars.len() {
1197            out.push(chars[i]);
1198            out.push(chars[i + 1]);
1199            i += 2;
1200            continue;
1201        }
1202        out.push(chars[i]);
1203        i += 1;
1204    }
1205    if i < chars.len() {
1206        out.push(']');
1207        i += 1;
1208    }
1209    i
1210}
1211
1212/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
1213/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
1214/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1215/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1216fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1217    if multiline_flag {
1218        return pat.to_string();
1219    }
1220    let chars: Vec<char> = pat.chars().collect();
1221    let mut out = String::with_capacity(pat.len().saturating_add(16));
1222    let mut i = 0usize;
1223    while i < chars.len() {
1224        let c = chars[i];
1225        if c == '\\' && i + 1 < chars.len() {
1226            out.push(c);
1227            out.push(chars[i + 1]);
1228            i += 2;
1229            continue;
1230        }
1231        if c == '[' {
1232            i = copy_regex_char_class(&chars, i, &mut out);
1233            continue;
1234        }
1235        if c == '$' {
1236            if let Some(&next) = chars.get(i + 1) {
1237                if next.is_ascii_digit() {
1238                    out.push(c);
1239                    i += 1;
1240                    continue;
1241                }
1242                if next == '{' {
1243                    out.push(c);
1244                    i += 1;
1245                    continue;
1246                }
1247                if next.is_ascii_alphanumeric() || next == '_' {
1248                    out.push(c);
1249                    i += 1;
1250                    continue;
1251                }
1252            }
1253            out.push_str("(?=\\n?\\z)");
1254            i += 1;
1255            continue;
1256        }
1257        out.push(c);
1258        i += 1;
1259    }
1260    out
1261}
1262
1263/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1264#[derive(Debug, Clone)]
1265pub(crate) struct DirHandleState {
1266    pub entries: Vec<String>,
1267    pub pos: usize,
1268}
1269
1270/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1271pub(crate) fn perl_osname() -> String {
1272    match std::env::consts::OS {
1273        "linux" => "linux".to_string(),
1274        "macos" => "darwin".to_string(),
1275        "windows" => "MSWin32".to_string(),
1276        other => other.to_string(),
1277    }
1278}
1279
1280fn perl_version_v_string() -> String {
1281    format!("v{}", env!("CARGO_PKG_VERSION"))
1282}
1283
1284fn extended_os_error_string() -> String {
1285    std::io::Error::last_os_error().to_string()
1286}
1287
1288#[cfg(unix)]
1289fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1290    unsafe {
1291        (
1292            libc::getuid() as i64,
1293            libc::geteuid() as i64,
1294            libc::getgid() as i64,
1295            libc::getegid() as i64,
1296        )
1297    }
1298}
1299
1300#[cfg(not(unix))]
1301fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1302    (0, 0, 0, 0)
1303}
1304
1305fn unix_id_for_special(name: &str) -> i64 {
1306    let (r, e, _, _) = unix_real_effective_ids();
1307    match name {
1308        "<" => r,
1309        ">" => e,
1310        _ => 0,
1311    }
1312}
1313
1314#[cfg(unix)]
1315fn unix_group_list_string(primary: libc::gid_t) -> String {
1316    let mut buf = vec![0 as libc::gid_t; 256];
1317    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1318    if n <= 0 {
1319        return format!("{}", primary);
1320    }
1321    let mut parts = vec![format!("{}", primary)];
1322    for g in buf.iter().take(n as usize) {
1323        parts.push(format!("{}", g));
1324    }
1325    parts.join(" ")
1326}
1327
1328/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1329#[cfg(unix)]
1330fn unix_group_list_for_special(name: &str) -> String {
1331    let (_, _, gid, egid) = unix_real_effective_ids();
1332    match name {
1333        "(" => unix_group_list_string(gid as libc::gid_t),
1334        ")" => unix_group_list_string(egid as libc::gid_t),
1335        _ => String::new(),
1336    }
1337}
1338
1339#[cfg(not(unix))]
1340fn unix_group_list_for_special(_name: &str) -> String {
1341    String::new()
1342}
1343
1344/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1345/// `~/.ssh/config` and keys).
1346#[cfg(unix)]
1347fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1348    use libc::{getpwuid_r, getuid};
1349    use std::ffi::CStr;
1350    use std::os::unix::ffi::OsStringExt;
1351    let uid = unsafe { getuid() };
1352    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1353    let mut result: *mut libc::passwd = std::ptr::null_mut();
1354    let mut buf = vec![0u8; 16_384];
1355    let rc = unsafe {
1356        getpwuid_r(
1357            uid,
1358            &mut pw,
1359            buf.as_mut_ptr().cast::<libc::c_char>(),
1360            buf.len(),
1361            &mut result,
1362        )
1363    };
1364    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1365        return None;
1366    }
1367    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1368    if bytes.is_empty() {
1369        return None;
1370    }
1371    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1372}
1373
1374/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1375#[cfg(unix)]
1376fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1377    use libc::getpwnam_r;
1378    use std::ffi::{CStr, CString};
1379    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1380    let bytes = login.as_bytes();
1381    if bytes.is_empty() || bytes.contains(&0) {
1382        return None;
1383    }
1384    let cname = CString::new(bytes).ok()?;
1385    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1386    let mut result: *mut libc::passwd = std::ptr::null_mut();
1387    let mut buf = vec![0u8; 16_384];
1388    let rc = unsafe {
1389        getpwnam_r(
1390            cname.as_ptr(),
1391            &mut pw,
1392            buf.as_mut_ptr().cast::<libc::c_char>(),
1393            buf.len(),
1394            &mut result,
1395        )
1396    };
1397    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1398        return None;
1399    }
1400    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1401    if dir_bytes.is_empty() {
1402        return None;
1403    }
1404    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1405}
1406
1407impl Default for VMHelper {
1408    fn default() -> Self {
1409        Self::new()
1410    }
1411}
1412
1413/// How [`VMHelper::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1414#[derive(Clone, Copy)]
1415pub(crate) enum CaptureAllMode {
1416    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1417    Empty,
1418    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1419    Append,
1420    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1421    Skip,
1422}
1423
1424/// Result of [`VMHelper::try_resolve_via_lockfile`].
1425///
1426/// - `Found(path)` — store / lockfile resolution succeeded.
1427/// - `NotFound` — nothing matched at any anchor; caller falls
1428///   through to `@INC`. This is the normal "no project, no
1429///   installed package" path.
1430/// - `PinUnsatisfied(message)` — resolution *would* have hit but
1431///   a pin (lockfile entry whose store dir is missing, or a
1432///   use-site `use Module VERSION` whose store dir is missing)
1433///   refused to fall through. The caller MUST surface this to
1434///   the user as a runtime error instead of silently dropping
1435///   into `@INC` — that drop-through was the version-disrespect
1436///   bug ("stryke use must respect package version").
1437pub(crate) enum LockfileResolution {
1438    Found(std::path::PathBuf),
1439    NotFound,
1440    PinUnsatisfied(String),
1441}
1442
1443impl VMHelper {
1444    /// `new` — see implementation.
1445    pub fn new() -> Self {
1446        let mut scope = Scope::new();
1447        scope.declare_array("INC", vec![StrykeValue::string(".".to_string())]);
1448        scope.declare_hash("INC", IndexMap::new());
1449        scope.declare_array("ARGV", vec![]);
1450        scope.declare_array("_", vec![]);
1451
1452        // @path / @p — $PATH split by OS path separator, frozen (immutable)
1453        let path_vec: Vec<StrykeValue> = std::env::var("PATH")
1454            .unwrap_or_default()
1455            .split(if cfg!(windows) { ';' } else { ':' })
1456            .filter(|s| !s.is_empty())
1457            .map(|p| StrykeValue::string(p.to_string()))
1458            .collect();
1459        scope.declare_array_frozen("path", path_vec.clone(), true);
1460        scope.declare_array_frozen("p", path_vec, true);
1461
1462        // @fpath / @f — $FPATH (zsh function path) split by ':', frozen
1463        let fpath_vec: Vec<StrykeValue> = std::env::var("FPATH")
1464            .unwrap_or_default()
1465            .split(':')
1466            .filter(|s| !s.is_empty())
1467            .map(|p| StrykeValue::string(p.to_string()))
1468            .collect();
1469        scope.declare_array_frozen("fpath", fpath_vec.clone(), true);
1470        scope.declare_array_frozen("f", fpath_vec, true);
1471        scope.declare_hash("ENV", IndexMap::new());
1472        scope.declare_hash("SIG", IndexMap::new());
1473
1474        // %term — terminal info (frozen)
1475        let term_map = build_term_hash();
1476        scope.declare_hash_global_frozen("term", term_map);
1477
1478        // %uname — system identification (frozen, Unix only)
1479        #[cfg(unix)]
1480        {
1481            let uname_map = build_uname_hash();
1482            scope.declare_hash_global_frozen("uname", uname_map);
1483        }
1484        #[cfg(not(unix))]
1485        {
1486            scope.declare_hash_global_frozen("uname", IndexMap::new());
1487        }
1488
1489        // %limits — resource limits (frozen, Unix only)
1490        #[cfg(unix)]
1491        {
1492            let limits_map = build_limits_hash();
1493            scope.declare_hash_global_frozen("limits", limits_map);
1494        }
1495        #[cfg(not(unix))]
1496        {
1497            scope.declare_hash_global_frozen("limits", IndexMap::new());
1498        }
1499
1500        // Reflection hashes — populated from `build.rs`-generated tables so
1501        // they track the real parser/dispatcher/LSP without hand-maintenance.
1502        // Eleven hashes; all lookups are O(1). Forward maps:
1503        //   %b   / %stryke::builtins      — callable name → category ("parallel", "string", …)
1504        //   %k   / %stryke::keywords      — language keyword → category ("control", "decl", …)
1505        //   %o   / %stryke::operators     — symbol operator → category ("arith", "pipeline", …)
1506        //   %v   / %stryke::special_vars  — special var spelling (sigil included) → category
1507        //   %pc  / %stryke::perl_compats  — subset: Perl 5 core only
1508        //   %e   / %stryke::extensions    — subset: stryke-only
1509        //   %a   / %stryke::aliases       — alias → primary
1510        //   %d   / %stryke::descriptions  — name → LSP one-liner (sparse)
1511        //   %all / %stryke::all           — primaries + aliases + keywords (union)
1512        // Inverted indexes for constant-time reverse queries:
1513        //   %c   / %stryke::categories    — category → arrayref of names
1514        //   %p   / %stryke::primaries     — primary → arrayref of aliases
1515        //
1516        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1517        // together they cover `keys %builtins`. Short aliases use the
1518        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1519        // Reflection hashes are lazily initialized on first access
1520        // (see `ensure_reflection_hashes`). Only declare the version scalar
1521        // eagerly since it's trivial.
1522        scope.declare_scalar(
1523            "stryke::VERSION",
1524            StrykeValue::string(env!("CARGO_PKG_VERSION").to_string()),
1525        );
1526        scope.declare_array("-", vec![]);
1527        scope.declare_array("+", vec![]);
1528        scope.declare_array("^CAPTURE", vec![]);
1529        scope.declare_array("^CAPTURE_ALL", vec![]);
1530        scope.declare_hash("^HOOK", IndexMap::new());
1531        scope.declare_scalar("~", StrykeValue::string("STDOUT".to_string()));
1532
1533        let script_start_time = std::time::SystemTime::now()
1534            .duration_since(std::time::UNIX_EPOCH)
1535            .map(|d| d.as_secs() as i64)
1536            .unwrap_or(0);
1537
1538        let executable_path = cached_executable_path();
1539
1540        let stryke_pwd_init = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1541        let stryke_pwd = std::fs::canonicalize(&stryke_pwd_init).unwrap_or(stryke_pwd_init);
1542
1543        let mut special_caret_scalars: HashMap<String, StrykeValue> = HashMap::new();
1544        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1545            special_caret_scalars.insert(format!("^{}", name), StrykeValue::UNDEF);
1546        }
1547
1548        let mut s = Self {
1549            scope,
1550            subs: HashMap::new(),
1551            intercepts: Vec::new(),
1552            next_intercept_id: 1,
1553            intercept_ctx_stack: Vec::new(),
1554            intercept_active_names: Vec::new(),
1555            struct_defs: HashMap::new(),
1556            enum_defs: HashMap::new(),
1557            class_defs: HashMap::new(),
1558            trait_defs: HashMap::new(),
1559            file: "-e".to_string(),
1560            output_handles: HashMap::new(),
1561            input_handles: HashMap::new(),
1562            ofs: String::new(),
1563            ors: String::new(),
1564            irs: Some("\n".to_string()),
1565            errno: String::new(),
1566            errno_code: 0,
1567            eval_error: String::new(),
1568            eval_error_code: 0,
1569            eval_error_value: None,
1570            argv: Vec::new(),
1571            env: IndexMap::new(),
1572            env_materialized: false,
1573            program_name: "stryke".to_string(),
1574            line_number: 0,
1575            nr: 0,
1576            last_readline_handle: String::new(),
1577            last_stdin_die_bracket: "<STDIN>".to_string(),
1578            handle_line_numbers: HashMap::new(),
1579            flip_flop_active: Vec::new(),
1580            flip_flop_exclusive_left_line: Vec::new(),
1581            flip_flop_sequence: Vec::new(),
1582            flip_flop_last_dot: Vec::new(),
1583            flip_flop_tree: HashMap::new(),
1584            sigint_pending_caret: Cell::new(false),
1585            auto_split: false,
1586            field_separator: None,
1587            begin_blocks: Vec::new(),
1588            unit_check_blocks: Vec::new(),
1589            check_blocks: Vec::new(),
1590            init_blocks: Vec::new(),
1591            end_blocks: Vec::new(),
1592            warnings: false,
1593            output_autoflush: false,
1594            default_print_handle: "STDOUT".to_string(),
1595            suppress_stdout: false,
1596            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1597            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1598            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1599            test_pass_total: std::sync::atomic::AtomicUsize::new(0),
1600            test_fail_total: std::sync::atomic::AtomicUsize::new(0),
1601            test_skip_total: std::sync::atomic::AtomicUsize::new(0),
1602            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1603            child_exit_status: 0,
1604            last_match: String::new(),
1605            prematch: String::new(),
1606            postmatch: String::new(),
1607            last_paren_match: String::new(),
1608            list_separator: " ".to_string(),
1609            script_start_time,
1610            compile_hints: 0,
1611            warning_bits: 0,
1612            global_phase: "RUN".to_string(),
1613            subscript_sep: "\x1c".to_string(),
1614            inplace_edit: String::new(),
1615            debug_flags: 0,
1616            perl_debug_flags: 0,
1617            eval_nesting: 0,
1618            argv_current_file: String::new(),
1619            diamond_next_idx: 0,
1620            diamond_reader: None,
1621            strict_refs: false,
1622            strict_subs: false,
1623            strict_vars: false,
1624            utf8_pragma: false,
1625            open_pragma_utf8: false,
1626            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1627            feature_bits: FEAT_SAY,
1628            num_threads: 0, // lazily read from rayon on first parallel op
1629            regex_cache: HashMap::new(),
1630            regex_last: None,
1631            regex_match_memo: None,
1632            regex_capture_scope_fresh: false,
1633            regex_pos: HashMap::new(),
1634            state_vars: HashMap::new(),
1635            state_bindings_stack: Vec::new(),
1636            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1637            dir_handles: HashMap::new(),
1638            io_file_slots: HashMap::new(),
1639            pipe_children: HashMap::new(),
1640            socket_handles: HashMap::new(),
1641            wantarray_kind: WantarrayCtx::Scalar,
1642            profiler: None,
1643            module_export_lists: HashMap::new(),
1644            virtual_modules: HashMap::new(),
1645            tied_hashes: HashMap::new(),
1646            tied_scalars: HashMap::new(),
1647            tied_arrays: HashMap::new(),
1648            overload_table: HashMap::new(),
1649            format_templates: HashMap::new(),
1650            special_caret_scalars,
1651            format_page_number: 0,
1652            format_lines_per_page: 60,
1653            format_lines_left: 0,
1654            format_line_break_chars: "\n".to_string(),
1655            format_top_name: String::new(),
1656            accumulator_format: String::new(),
1657            max_system_fd: 2,
1658            emergency_memory: String::new(),
1659            last_subpattern_name: String::new(),
1660            inc_hook_index: 0,
1661            multiline_match: false,
1662            executable_path,
1663            formfeed_string: "\x0c".to_string(),
1664            glob_handle_alias: HashMap::new(),
1665            glob_restore_frames: vec![Vec::new()],
1666            special_var_restore_frames: vec![Vec::new()],
1667            reflection_hashes_ready: false,
1668            english_enabled: false,
1669            english_no_match_vars: false,
1670            english_match_vars_ever_enabled: false,
1671            english_lexical_scalars: vec![HashSet::new()],
1672            our_lexical_scalars: vec![HashSet::new()],
1673            our_lexical_arrays: vec![HashSet::new()],
1674            our_lexical_hashes: vec![HashSet::new()],
1675            vm_jit_enabled: !matches!(
1676                std::env::var("STRYKE_NO_JIT"),
1677                Ok(v)
1678                    if v == "1"
1679                        || v.eq_ignore_ascii_case("true")
1680                        || v.eq_ignore_ascii_case("yes")
1681            ),
1682            disasm_bytecode: false,
1683            cached_chunk: None,
1684            cache_script_path: None,
1685            stryke_pwd,
1686            in_generator: false,
1687            line_mode_skip_main: false,
1688            line_mode_chunk: None,
1689            line_mode_eof_pending: false,
1690            line_mode_stdin_pending: VecDeque::new(),
1691            rate_limit_slots: Vec::new(),
1692            log_level_override: None,
1693            current_sub_stack: Vec::new(),
1694            debugger: None,
1695            debug_call_stack: Vec::new(),
1696        };
1697        s.install_overload_pragma_stubs();
1698        s
1699    }
1700
1701    /// Lazily populate the reflection hashes (`%b`, `%stryke::builtins`, etc.)
1702    /// on first access. This avoids building ~12k hash entries on startup for
1703    /// one-liners that never touch introspection.
1704    pub(crate) fn ensure_reflection_hashes(&mut self) {
1705        if self.reflection_hashes_ready {
1706            return;
1707        }
1708        self.reflection_hashes_ready = true;
1709        // Package stashes (`%main::` / `%Pkg::`) are Perl-spec, install in
1710        // every mode — `--compat` does not turn off the symbol table.
1711        self.refresh_package_stashes();
1712        // Everything below is stryke-only. `--compat` skips the entire block
1713        // so a Perl 5 script sees no extension hashes and can use `%all` /
1714        // `%b` / `%parameters` / `%stryke::*` etc. as ordinary user hashes.
1715        if crate::compat_mode() {
1716            return;
1717        }
1718        let builtins_map = crate::builtins::builtins_hash_map();
1719        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1720        let extensions_map = crate::builtins::extensions_hash_map();
1721        let aliases_map = crate::builtins::aliases_hash_map();
1722        let descriptions_map = crate::builtins::descriptions_hash_map();
1723        let categories_map = crate::builtins::categories_hash_map();
1724        let primaries_map = crate::builtins::primaries_hash_map();
1725        let keywords_map = crate::builtins::keywords_hash_map();
1726        let operators_map = crate::builtins::operators_hash_map();
1727        let special_vars_map = crate::builtins::special_vars_hash_map();
1728        let all_map = crate::builtins::all_hash_map();
1729        self.scope
1730            .declare_hash_global_frozen("stryke::builtins", builtins_map.clone());
1731        self.scope
1732            .declare_hash_global_frozen("stryke::perl_compats", perl_compats_map.clone());
1733        self.scope
1734            .declare_hash_global_frozen("stryke::extensions", extensions_map.clone());
1735        self.scope
1736            .declare_hash_global_frozen("stryke::aliases", aliases_map.clone());
1737        self.scope
1738            .declare_hash_global_frozen("stryke::descriptions", descriptions_map.clone());
1739        self.scope
1740            .declare_hash_global_frozen("stryke::categories", categories_map.clone());
1741        self.scope
1742            .declare_hash_global_frozen("stryke::primaries", primaries_map.clone());
1743        self.scope
1744            .declare_hash_global_frozen("stryke::keywords", keywords_map.clone());
1745        self.scope
1746            .declare_hash_global_frozen("stryke::operators", operators_map.clone());
1747        self.scope
1748            .declare_hash_global_frozen("stryke::special_vars", special_vars_map.clone());
1749        self.scope
1750            .declare_hash_global_frozen("stryke::all", all_map.clone());
1751        // Short aliases: only declare if no user-declared hash with that name
1752        // exists, to avoid overwriting `my %e` etc.
1753        for (name, val) in [
1754            ("b", builtins_map),
1755            ("pc", perl_compats_map),
1756            ("e", extensions_map),
1757            ("a", aliases_map),
1758            ("d", descriptions_map),
1759            ("c", categories_map),
1760            ("p", primaries_map),
1761            ("k", keywords_map),
1762            ("o", operators_map),
1763            ("v", special_vars_map),
1764            ("all", all_map),
1765        ] {
1766            if !self.scope.any_frame_has_hash(name) {
1767                self.scope.declare_hash_global_frozen(name, val);
1768            }
1769        }
1770        // Initial install of `%parameters` (zsh-`$parameters` analogue).
1771        // Refreshed automatically on every read via `touch_env_hash`.
1772        if !self.scope.has_lexical_hash("parameters") {
1773            self.refresh_parameters_hash();
1774        }
1775    }
1776
1777    /// Rebuild `%parameters` (zsh-`$parameters` analogue) from the current
1778    /// scope. Maps every live sigil-prefixed name (`$x`, `@a`, `%h`, …) to its
1779    /// kind string (`"scalar"`, `"array"`, `"hash"`, `"atomic_array"`,
1780    /// `"atomic_hash"`, `"shared_array"`, `"shared_hash"`). Installed as a
1781    /// frozen global hash so user code can read it but not assign into it
1782    /// (parallel to `%all` / `%b` / `%stryke::*`). Refreshed automatically on
1783    /// every `%parameters` read via the `touch_env_hash` hook, so the snapshot
1784    /// is always current — the user never needs to call this directly.
1785    pub fn refresh_parameters_hash(&mut self) {
1786        let pairs = self.scope.parameters_pairs();
1787        let mut h: indexmap::IndexMap<String, StrykeValue> =
1788            indexmap::IndexMap::with_capacity(pairs.len());
1789        for (name, kind) in pairs {
1790            h.insert(name, StrykeValue::string(kind.to_string()));
1791        }
1792        // declare_hash_global_frozen overwrites unconditionally, so each
1793        // refresh replaces the prior snapshot.
1794        self.scope.declare_hash_global_frozen("parameters", h);
1795    }
1796
1797    /// Populate `%main::` / `%Foo::` package stashes with current symbol-table
1798    /// state so `keys %main::` and `keys %Foo::` enumerate live names. Maps
1799    /// each unqualified name → its kind string (`"scalar"`, `"array"`,
1800    /// `"hash"`, `"sub"`). Stryke has no real Perl typeglob layer; the kind
1801    /// string is the most useful per-symbol value we can offer.
1802    ///
1803    /// Callable repeatedly — overwrites prior stashes — so the REPL refreshes
1804    /// after every line and scripts can call it explicitly via the
1805    /// `refresh_stashes()` builtin if they want post-eval visibility.
1806    pub fn refresh_package_stashes(&mut self) {
1807        use indexmap::IndexMap;
1808
1809        let mut by_pkg: std::collections::HashMap<String, IndexMap<String, StrykeValue>> =
1810            std::collections::HashMap::new();
1811
1812        let record = |pkg: &str,
1813                      sym: &str,
1814                      kind: &str,
1815                      map: &mut std::collections::HashMap<
1816            String,
1817            IndexMap<String, StrykeValue>,
1818        >| {
1819            map.entry(pkg.to_string())
1820                .or_default()
1821                .insert(sym.to_string(), StrykeValue::string(kind.to_string()));
1822        };
1823
1824        // Subs: keys like "main::foo" / "Foo::Bar::baz".
1825        for key in self.subs.keys() {
1826            if let Some(idx) = key.rfind("::") {
1827                let (pkg, rest) = key.split_at(idx);
1828                let sym = &rest[2..];
1829                if pkg.is_empty() || sym.is_empty() {
1830                    continue;
1831                }
1832                record(pkg, sym, "sub", &mut by_pkg);
1833            } else {
1834                // Bare-name sub (no package qualifier) lives in main::.
1835                record("main", key, "sub", &mut by_pkg);
1836            }
1837        }
1838
1839        // Package-qualified scalars / arrays / hashes from every frame.
1840        for frame in self.scope.frames_for_introspection() {
1841            let (scalars, arrays, hashes) = frame;
1842            for name in scalars {
1843                if let Some(idx) = name.rfind("::") {
1844                    let (pkg, rest) = name.split_at(idx);
1845                    let sym = &rest[2..];
1846                    if !pkg.is_empty() && !sym.is_empty() {
1847                        record(pkg, sym, "scalar", &mut by_pkg);
1848                    }
1849                }
1850            }
1851            for name in arrays {
1852                if let Some(idx) = name.rfind("::") {
1853                    let (pkg, rest) = name.split_at(idx);
1854                    let sym = &rest[2..];
1855                    if !pkg.is_empty() && !sym.is_empty() {
1856                        record(pkg, sym, "array", &mut by_pkg);
1857                    }
1858                }
1859            }
1860            for name in hashes {
1861                if let Some(idx) = name.rfind("::") {
1862                    let (pkg, rest) = name.split_at(idx);
1863                    let sym = &rest[2..];
1864                    if !pkg.is_empty() && !sym.is_empty() {
1865                        record(pkg, sym, "hash", &mut by_pkg);
1866                    }
1867                }
1868            }
1869        }
1870
1871        // Install each `%Pkg::` in the global frame. Lexer emits the trailing
1872        // `::` as part of the name, so the stash hash lives under that exact
1873        // key. `declare_hash_global_frozen` overwrites any prior copy.
1874        for (pkg, mut entries) in by_pkg {
1875            entries.sort_keys();
1876            let key = format!("{}::", pkg);
1877            self.scope.declare_hash_global_frozen(&key, entries);
1878        }
1879    }
1880
1881    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1882    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1883    /// strict subs and to satisfy `use overload ();` call sites.
1884    fn install_overload_pragma_stubs(&mut self) {
1885        let empty: Block = vec![];
1886        for key in ["overload::import", "overload::unimport"] {
1887            let name = key.to_string();
1888            self.subs.insert(
1889                name.clone(),
1890                Arc::new(StrykeSub {
1891                    name,
1892                    params: vec![],
1893                    body: empty.clone(),
1894                    prototype: None,
1895                    closure_env: None,
1896                    fib_like: None,
1897                }),
1898            );
1899        }
1900    }
1901
1902    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1903    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1904    pub fn line_mode_worker_clone(&self) -> VMHelper {
1905        VMHelper {
1906            scope: self.scope.clone(),
1907            subs: self.subs.clone(),
1908            intercepts: self.intercepts.clone(),
1909            next_intercept_id: self.next_intercept_id,
1910            intercept_ctx_stack: self.intercept_ctx_stack.clone(),
1911            intercept_active_names: self.intercept_active_names.clone(),
1912            struct_defs: self.struct_defs.clone(),
1913            enum_defs: self.enum_defs.clone(),
1914            class_defs: self.class_defs.clone(),
1915            trait_defs: self.trait_defs.clone(),
1916            file: self.file.clone(),
1917            output_handles: HashMap::new(),
1918            input_handles: HashMap::new(),
1919            ofs: self.ofs.clone(),
1920            ors: self.ors.clone(),
1921            irs: self.irs.clone(),
1922            errno: self.errno.clone(),
1923            errno_code: self.errno_code,
1924            eval_error: self.eval_error.clone(),
1925            eval_error_code: self.eval_error_code,
1926            eval_error_value: self.eval_error_value.clone(),
1927            argv: self.argv.clone(),
1928            env: self.env.clone(),
1929            env_materialized: self.env_materialized,
1930            program_name: self.program_name.clone(),
1931            line_number: 0,
1932            nr: 0,
1933            last_readline_handle: String::new(),
1934            last_stdin_die_bracket: "<STDIN>".to_string(),
1935            handle_line_numbers: HashMap::new(),
1936            flip_flop_active: Vec::new(),
1937            flip_flop_exclusive_left_line: Vec::new(),
1938            flip_flop_sequence: Vec::new(),
1939            flip_flop_last_dot: Vec::new(),
1940            flip_flop_tree: HashMap::new(),
1941            sigint_pending_caret: Cell::new(false),
1942            auto_split: self.auto_split,
1943            field_separator: self.field_separator.clone(),
1944            begin_blocks: self.begin_blocks.clone(),
1945            unit_check_blocks: self.unit_check_blocks.clone(),
1946            check_blocks: self.check_blocks.clone(),
1947            init_blocks: self.init_blocks.clone(),
1948            end_blocks: self.end_blocks.clone(),
1949            warnings: self.warnings,
1950            output_autoflush: self.output_autoflush,
1951            default_print_handle: self.default_print_handle.clone(),
1952            suppress_stdout: self.suppress_stdout,
1953            // Workers start with fresh test counters — they don't share with the
1954            // parent. The parent is responsible for aggregating across workers if
1955            // it cares (none of the current parallel callers do).
1956            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1957            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1958            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1959            test_pass_total: std::sync::atomic::AtomicUsize::new(0),
1960            test_fail_total: std::sync::atomic::AtomicUsize::new(0),
1961            test_skip_total: std::sync::atomic::AtomicUsize::new(0),
1962            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1963            child_exit_status: self.child_exit_status,
1964            last_match: self.last_match.clone(),
1965            prematch: self.prematch.clone(),
1966            postmatch: self.postmatch.clone(),
1967            last_paren_match: self.last_paren_match.clone(),
1968            list_separator: self.list_separator.clone(),
1969            script_start_time: self.script_start_time,
1970            compile_hints: self.compile_hints,
1971            warning_bits: self.warning_bits,
1972            global_phase: self.global_phase.clone(),
1973            subscript_sep: self.subscript_sep.clone(),
1974            inplace_edit: self.inplace_edit.clone(),
1975            debug_flags: self.debug_flags,
1976            perl_debug_flags: self.perl_debug_flags,
1977            eval_nesting: self.eval_nesting,
1978            argv_current_file: String::new(),
1979            diamond_next_idx: 0,
1980            diamond_reader: None,
1981            strict_refs: self.strict_refs,
1982            strict_subs: self.strict_subs,
1983            strict_vars: self.strict_vars,
1984            utf8_pragma: self.utf8_pragma,
1985            open_pragma_utf8: self.open_pragma_utf8,
1986            feature_bits: self.feature_bits,
1987            num_threads: 0,
1988            regex_cache: self.regex_cache.clone(),
1989            regex_last: self.regex_last.clone(),
1990            regex_match_memo: self.regex_match_memo.clone(),
1991            regex_capture_scope_fresh: false,
1992            regex_pos: self.regex_pos.clone(),
1993            state_vars: self.state_vars.clone(),
1994            state_bindings_stack: Vec::new(),
1995            rand_rng: self.rand_rng.clone(),
1996            dir_handles: HashMap::new(),
1997            io_file_slots: HashMap::new(),
1998            pipe_children: HashMap::new(),
1999            socket_handles: HashMap::new(),
2000            wantarray_kind: self.wantarray_kind,
2001            profiler: None,
2002            module_export_lists: self.module_export_lists.clone(),
2003            virtual_modules: self.virtual_modules.clone(),
2004            tied_hashes: self.tied_hashes.clone(),
2005            tied_scalars: self.tied_scalars.clone(),
2006            tied_arrays: self.tied_arrays.clone(),
2007            overload_table: self.overload_table.clone(),
2008            format_templates: self.format_templates.clone(),
2009            special_caret_scalars: self.special_caret_scalars.clone(),
2010            format_page_number: self.format_page_number,
2011            format_lines_per_page: self.format_lines_per_page,
2012            format_lines_left: self.format_lines_left,
2013            format_line_break_chars: self.format_line_break_chars.clone(),
2014            format_top_name: self.format_top_name.clone(),
2015            accumulator_format: self.accumulator_format.clone(),
2016            max_system_fd: self.max_system_fd,
2017            emergency_memory: self.emergency_memory.clone(),
2018            last_subpattern_name: self.last_subpattern_name.clone(),
2019            inc_hook_index: self.inc_hook_index,
2020            multiline_match: self.multiline_match,
2021            executable_path: self.executable_path.clone(),
2022            formfeed_string: self.formfeed_string.clone(),
2023            glob_handle_alias: self.glob_handle_alias.clone(),
2024            glob_restore_frames: self.glob_restore_frames.clone(),
2025            special_var_restore_frames: self.special_var_restore_frames.clone(),
2026            reflection_hashes_ready: self.reflection_hashes_ready,
2027            english_enabled: self.english_enabled,
2028            english_no_match_vars: self.english_no_match_vars,
2029            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
2030            english_lexical_scalars: self.english_lexical_scalars.clone(),
2031            our_lexical_scalars: self.our_lexical_scalars.clone(),
2032            our_lexical_arrays: self.our_lexical_arrays.clone(),
2033            our_lexical_hashes: self.our_lexical_hashes.clone(),
2034            vm_jit_enabled: self.vm_jit_enabled,
2035            disasm_bytecode: self.disasm_bytecode,
2036            // Sideband cache fields belong to the top-level driver, not line-mode workers.
2037            cached_chunk: None,
2038            cache_script_path: None,
2039            stryke_pwd: self.stryke_pwd.clone(),
2040            in_generator: false,
2041            line_mode_skip_main: false,
2042            line_mode_chunk: self.line_mode_chunk.clone(),
2043            line_mode_eof_pending: false,
2044            line_mode_stdin_pending: VecDeque::new(),
2045            rate_limit_slots: Vec::new(),
2046            log_level_override: self.log_level_override,
2047            current_sub_stack: Vec::new(),
2048            debugger: None,
2049            debug_call_stack: Vec::new(),
2050        }
2051    }
2052
2053    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
2054    pub(crate) fn parallel_thread_count(&mut self) -> usize {
2055        if self.num_threads == 0 {
2056            self.num_threads = rayon::current_num_threads();
2057        }
2058        self.num_threads
2059    }
2060
2061    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
2062    pub(crate) fn eval_par_list_call(
2063        &mut self,
2064        name: &str,
2065        args: &[StrykeValue],
2066        ctx: WantarrayCtx,
2067        line: usize,
2068    ) -> StrykeResult<StrykeValue> {
2069        match name {
2070            "puniq" => {
2071                let (list_src, show_prog) = match args.len() {
2072                    0 => return Err(StrykeError::runtime("puniq: expected LIST", line)),
2073                    1 => (&args[0], false),
2074                    2 => (&args[0], args[1].is_true()),
2075                    _ => {
2076                        return Err(StrykeError::runtime(
2077                            "puniq: expected LIST [, progress => EXPR]",
2078                            line,
2079                        ));
2080                    }
2081                };
2082                let list = list_src.to_list();
2083                let n_threads = self.parallel_thread_count();
2084                let pmap_progress = PmapProgress::new(show_prog, list.len());
2085                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
2086                pmap_progress.finish();
2087                if ctx == WantarrayCtx::List {
2088                    Ok(StrykeValue::array(out))
2089                } else {
2090                    Ok(StrykeValue::integer(out.len() as i64))
2091                }
2092            }
2093            "pfirst" => {
2094                let (code_val, list_src, show_prog) = match args.len() {
2095                    2 => (&args[0], &args[1], false),
2096                    3 => (&args[0], &args[1], args[2].is_true()),
2097                    _ => {
2098                        return Err(StrykeError::runtime(
2099                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
2100                            line,
2101                        ));
2102                    }
2103                };
2104                let Some(sub) = code_val.as_code_ref() else {
2105                    return Err(StrykeError::runtime(
2106                        "pfirst: first argument must be a code reference",
2107                        line,
2108                    ));
2109                };
2110                let sub = sub.clone();
2111                let list = list_src.to_list();
2112                if list.is_empty() {
2113                    return Ok(StrykeValue::UNDEF);
2114                }
2115                let pmap_progress = PmapProgress::new(show_prog, list.len());
2116                let subs = self.subs.clone();
2117                let (scope_capture, atomic_arrays, atomic_hashes) =
2118                    self.scope.capture_with_atomics();
2119                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
2120                    let mut local_interp = VMHelper::new();
2121                    local_interp.subs = subs.clone();
2122                    local_interp.scope.restore_capture(&scope_capture);
2123                    local_interp
2124                        .scope
2125                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2126                    local_interp.enable_parallel_guard();
2127                    local_interp.scope.set_topic(item);
2128                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2129                        Ok(v) => v.is_true(),
2130                        Err(_) => false,
2131                    }
2132                });
2133                pmap_progress.finish();
2134                Ok(out.unwrap_or(StrykeValue::UNDEF))
2135            }
2136            "pany" => {
2137                let (code_val, list_src, show_prog) = match args.len() {
2138                    2 => (&args[0], &args[1], false),
2139                    3 => (&args[0], &args[1], args[2].is_true()),
2140                    _ => {
2141                        return Err(StrykeError::runtime(
2142                            "pany: expected BLOCK, LIST [, progress => EXPR]",
2143                            line,
2144                        ));
2145                    }
2146                };
2147                let Some(sub) = code_val.as_code_ref() else {
2148                    return Err(StrykeError::runtime(
2149                        "pany: first argument must be a code reference",
2150                        line,
2151                    ));
2152                };
2153                let sub = sub.clone();
2154                let list = list_src.to_list();
2155                let pmap_progress = PmapProgress::new(show_prog, list.len());
2156                let subs = self.subs.clone();
2157                let (scope_capture, atomic_arrays, atomic_hashes) =
2158                    self.scope.capture_with_atomics();
2159                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
2160                    let mut local_interp = VMHelper::new();
2161                    local_interp.subs = subs.clone();
2162                    local_interp.scope.restore_capture(&scope_capture);
2163                    local_interp
2164                        .scope
2165                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2166                    local_interp.enable_parallel_guard();
2167                    local_interp.scope.set_topic(item);
2168                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2169                        Ok(v) => v.is_true(),
2170                        Err(_) => false,
2171                    }
2172                });
2173                pmap_progress.finish();
2174                Ok(StrykeValue::integer(if b { 1 } else { 0 }))
2175            }
2176            _ => Err(StrykeError::runtime(
2177                format!("internal: unknown par_list builtin {name}"),
2178                line,
2179            )),
2180        }
2181    }
2182
2183    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
2184        #[cfg(unix)]
2185        if let Some(sig) = s.signal() {
2186            return sig as i64 & 0x7f;
2187        }
2188        let code = s.code().unwrap_or(0) as i64;
2189        code << 8
2190    }
2191
2192    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
2193        self.child_exit_status = self.encode_exit_status(s);
2194    }
2195
2196    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
2197    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
2198        // Perl's $! is the bare description ("No such file or directory"),
2199        // not Rust's "<desc> (os error N)" form. Strip the trailing parenthetical.
2200        let s = e.to_string();
2201        let stripped = s
2202            .rfind(" (os error ")
2203            .map(|i| s[..i].to_string())
2204            .unwrap_or(s);
2205        self.errno = stripped;
2206        self.errno_code = e.raw_os_error().unwrap_or(0);
2207    }
2208
2209    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
2210    ///
2211    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
2212    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
2213    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
2214    ///
2215    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
2216    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
2217    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
2218    /// `~/.ssh/config` and keys apply.
2219    pub(crate) fn ssh_builtin_execute(
2220        &mut self,
2221        args: &[StrykeValue],
2222    ) -> StrykeResult<StrykeValue> {
2223        use std::process::Command;
2224        let mut cmd = Command::new("ssh");
2225        #[cfg(unix)]
2226        {
2227            use libc::geteuid;
2228            let home_for_ssh = if unsafe { geteuid() } == 0 {
2229                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
2230            } else {
2231                None
2232            };
2233            if let Some(h) = home_for_ssh {
2234                cmd.env("HOME", h);
2235            } else if std::env::var_os("HOME").is_none() {
2236                if let Some(h) = pw_home_dir_for_current_uid() {
2237                    cmd.env("HOME", h);
2238                }
2239            }
2240        }
2241        for a in args {
2242            cmd.arg(a.to_string());
2243        }
2244        match cmd.status() {
2245            Ok(s) => {
2246                self.record_child_exit_status(s);
2247                Ok(StrykeValue::integer(s.code().unwrap_or(-1) as i64))
2248            }
2249            Err(e) => {
2250                self.apply_io_error_to_errno(&e);
2251                Ok(StrykeValue::integer(-1))
2252            }
2253        }
2254    }
2255
2256    /// Set `$@` message; numeric side is `0` if empty, else `1`.
2257    pub(crate) fn set_eval_error(&mut self, msg: String) {
2258        self.eval_error = msg;
2259        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2260        self.eval_error_value = None;
2261    }
2262
2263    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &StrykeError) {
2264        self.eval_error = e.to_string();
2265        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2266        self.eval_error_value = e.die_value.clone();
2267    }
2268
2269    pub(crate) fn clear_eval_error(&mut self) {
2270        self.eval_error = String::new();
2271        self.eval_error_code = 0;
2272        self.eval_error_value = None;
2273    }
2274
2275    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
2276    fn bump_line_for_handle(&mut self, handle_key: &str) {
2277        self.last_readline_handle = handle_key.to_string();
2278        *self
2279            .handle_line_numbers
2280            .entry(handle_key.to_string())
2281            .or_insert(0) += 1;
2282    }
2283
2284    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
2285    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
2286        if name.starts_with('^') {
2287            return name.to_string();
2288        }
2289        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
2290            let pkg = self.current_package();
2291            if !pkg.is_empty() && pkg != "main" {
2292                return format!("{}::{}", pkg, name);
2293            }
2294        }
2295        name.to_string()
2296    }
2297
2298    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
2299    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
2300        if name.contains("::") {
2301            return name.to_string();
2302        }
2303        let pkg = self.current_package();
2304        if pkg.is_empty() || pkg == "main" {
2305            format!("main::{}", name)
2306        } else {
2307            format!("{}::{}", pkg, name)
2308        }
2309    }
2310
2311    /// Bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
2312    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
2313        if name.contains("::") {
2314            return name.to_string();
2315        }
2316        for (lex, our) in self
2317            .english_lexical_scalars
2318            .iter()
2319            .zip(self.our_lexical_scalars.iter())
2320            .rev()
2321        {
2322            if lex.contains(name) {
2323                if our.contains(name) {
2324                    return self.stash_scalar_name_for_package(name);
2325                }
2326                return name.to_string();
2327            }
2328        }
2329        name.to_string()
2330    }
2331
2332    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
2333    pub(crate) fn tie_execute(
2334        &mut self,
2335        target_kind: u8,
2336        target_name: &str,
2337        class_and_args: Vec<StrykeValue>,
2338        line: usize,
2339    ) -> StrykeResult<StrykeValue> {
2340        let mut it = class_and_args.into_iter();
2341        let class = it.next().unwrap_or(StrykeValue::UNDEF);
2342        let pkg = class.to_string();
2343        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
2344        let tie_ctor = match target_kind {
2345            0 => "TIESCALAR",
2346            1 => "TIEARRAY",
2347            2 => "TIEHASH",
2348            _ => return Err(StrykeError::runtime("tie: invalid target kind", line)),
2349        };
2350        let tie_fn = format!("{}::{}", pkg, tie_ctor);
2351        let sub =
2352            self.subs.get(&tie_fn).cloned().ok_or_else(|| {
2353                StrykeError::runtime(format!("tie: cannot find &{}", tie_fn), line)
2354            })?;
2355        let mut call_args = vec![StrykeValue::string(pkg.clone())];
2356        call_args.extend(it);
2357        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
2358            Ok(v) => v,
2359            Err(FlowOrError::Flow(_)) => StrykeValue::UNDEF,
2360            Err(FlowOrError::Error(e)) => return Err(e),
2361        };
2362        match target_kind {
2363            0 => {
2364                self.tied_scalars.insert(target_name.to_string(), obj);
2365            }
2366            1 => {
2367                let key = self.stash_array_name_for_package(target_name);
2368                self.tied_arrays.insert(key, obj);
2369            }
2370            2 => {
2371                self.tied_hashes.insert(target_name.to_string(), obj);
2372            }
2373            _ => return Err(StrykeError::runtime("tie: invalid target kind", line)),
2374        }
2375        Ok(StrykeValue::UNDEF)
2376    }
2377
2378    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
2379    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
2380        let key = format!("{}::ISA", class);
2381        self.scope
2382            .get_array(&key)
2383            .into_iter()
2384            .map(|v| v.to_string())
2385            .collect()
2386    }
2387
2388    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
2389        let p = |c: &str| self.parents_of_class(c);
2390        linearize_c3(class, &p, 0)
2391    }
2392
2393    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
2394    pub(crate) fn resolve_method_full_name(
2395        &self,
2396        invocant_class: &str,
2397        method: &str,
2398        super_mode: bool,
2399    ) -> Option<String> {
2400        let mro = self.mro_linearize(invocant_class);
2401        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
2402        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
2403        // even when running `C::meth`.
2404        let start = if super_mode {
2405            mro.iter()
2406                .position(|p| p == invocant_class)
2407                .map(|i| i + 1)
2408                // If the class string does not appear in MRO (should be rare), skip the first
2409                // entry so we still search parents before giving up.
2410                .unwrap_or(1)
2411        } else {
2412            0
2413        };
2414        for pkg in mro.iter().skip(start) {
2415            if pkg == "UNIVERSAL" {
2416                continue;
2417            }
2418            let fq = format!("{}::{}", pkg, method);
2419            if self.subs.contains_key(&fq) {
2420                return Some(fq);
2421            }
2422        }
2423        mro.iter()
2424            .skip(start)
2425            .find(|p| *p != "UNIVERSAL")
2426            .map(|pkg| format!("{}::{}", pkg, method))
2427    }
2428
2429    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
2430        if let Some(alias) = self.glob_handle_alias.get(name) {
2431            return alias.clone();
2432        }
2433        // `print $fh …` stores the handle as "$varname"; resolve it by
2434        // reading the scalar variable which holds the IO handle name.
2435        if let Some(var_name) = name.strip_prefix('$') {
2436            let val = self.scope.get_scalar(var_name);
2437            let s = val.to_string();
2438            if !s.is_empty() {
2439                return self.resolve_io_handle_name(&s);
2440            }
2441        }
2442        name.to_string()
2443    }
2444
2445    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
2446    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
2447        if name.contains("::") {
2448            name.to_string()
2449        } else {
2450            self.qualify_sub_key(name)
2451        }
2452    }
2453
2454    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
2455    pub(crate) fn copy_typeglob_slots(
2456        &mut self,
2457        lhs: &str,
2458        rhs: &str,
2459        line: usize,
2460    ) -> StrykeResult<()> {
2461        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
2462        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
2463        match self.subs.get(&rhs_sub).cloned() {
2464            Some(s) => {
2465                self.subs.insert(lhs_sub, s);
2466            }
2467            None => {
2468                self.subs.remove(&lhs_sub);
2469            }
2470        }
2471        let sv = self.scope.get_scalar(rhs);
2472        self.scope
2473            .set_scalar(lhs, sv.clone())
2474            .map_err(|e| e.at_line(line))?;
2475        let lhs_an = self.stash_array_name_for_package(lhs);
2476        let rhs_an = self.stash_array_name_for_package(rhs);
2477        let av = self.scope.get_array(&rhs_an);
2478        self.scope
2479            .set_array(&lhs_an, av.clone())
2480            .map_err(|e| e.at_line(line))?;
2481        let hv = self.scope.get_hash(rhs);
2482        self.scope
2483            .set_hash(lhs, hv.clone())
2484            .map_err(|e| e.at_line(line))?;
2485        match self.glob_handle_alias.get(rhs).cloned() {
2486            Some(t) => {
2487                self.glob_handle_alias.insert(lhs.to_string(), t);
2488            }
2489            None => {
2490                self.glob_handle_alias.remove(lhs);
2491            }
2492        }
2493        Ok(())
2494    }
2495
2496    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2497    pub(crate) fn install_format_decl(
2498        &mut self,
2499        basename: &str,
2500        lines: &[String],
2501        line: usize,
2502    ) -> StrykeResult<()> {
2503        let pkg = self.current_package();
2504        let key = format!("{}::{}", pkg, basename);
2505        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2506        self.format_templates.insert(key, Arc::new(tmpl));
2507        Ok(())
2508    }
2509
2510    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2511    /// Anonymous overload handlers are emitted by the parser as a synthetic
2512    /// `__overload_anon_N` SubDecl at the top of the program (registered under
2513    /// `main::`); re-bind a clone under the current package so the dispatch
2514    /// `Pkg::__overload_anon_N` lookup at runtime resolves. (PARITY-012)
2515    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2516        let pkg = self.current_package();
2517        for (_, v) in pairs {
2518            if v.starts_with("__overload_anon_") {
2519                // Synthetic anon-overload subs are emitted at the top of the
2520                // program, before any user `package N` statement, so they're
2521                // registered under the bare name (qualify_sub_key returns the
2522                // unqualified form for the `main` package). Re-bind a clone
2523                // under `Pkg::name` so the dispatch lookup `Pkg::sub_short`
2524                // resolves.
2525                let pkg_key = format!("{}::{}", pkg, v);
2526                if !self.subs.contains_key(&pkg_key) {
2527                    let src = if let Some(s) = self.subs.get(v) {
2528                        Some(s.clone())
2529                    } else {
2530                        self.subs.get(&format!("main::{}", v)).cloned()
2531                    };
2532                    if let Some(sub) = src {
2533                        self.subs.insert(pkg_key, sub);
2534                    }
2535                }
2536            }
2537        }
2538        let ent = self.overload_table.entry(pkg).or_default();
2539        for (k, v) in pairs {
2540            ent.insert(k.clone(), v.clone());
2541        }
2542    }
2543
2544    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2545    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2546    pub(crate) fn local_declare_typeglob(
2547        &mut self,
2548        lhs: &str,
2549        rhs: Option<&str>,
2550        line: usize,
2551    ) -> StrykeResult<()> {
2552        let old = self.glob_handle_alias.remove(lhs);
2553        let Some(frame) = self.glob_restore_frames.last_mut() else {
2554            return Err(StrykeError::runtime(
2555                "internal: no glob restore frame for local *GLOB",
2556                line,
2557            ));
2558        };
2559        frame.push((lhs.to_string(), old));
2560        if let Some(r) = rhs {
2561            self.glob_handle_alias
2562                .insert(lhs.to_string(), r.to_string());
2563        }
2564        Ok(())
2565    }
2566
2567    pub(crate) fn scope_push_hook(&mut self) {
2568        self.scope.push_frame();
2569        self.glob_restore_frames.push(Vec::new());
2570        self.special_var_restore_frames.push(Vec::new());
2571        self.english_lexical_scalars.push(HashSet::new());
2572        self.our_lexical_scalars.push(HashSet::new());
2573        self.our_lexical_arrays.push(HashSet::new());
2574        self.our_lexical_hashes.push(HashSet::new());
2575        self.state_bindings_stack.push(Vec::new());
2576    }
2577
2578    #[inline]
2579    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2580        if let Some(s) = self.english_lexical_scalars.last_mut() {
2581            s.insert(name.to_string());
2582        }
2583    }
2584
2585    /// Snapshot the `english_lexical_scalars` stack for parallel worker spawn (rayon
2586    /// closures need owned `Vec<HashSet<String>>` they can `clone()` per-worker).
2587    #[inline]
2588    pub(crate) fn english_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2589        self.english_lexical_scalars.clone()
2590    }
2591
2592    /// Snapshot the `our_lexical_scalars` stack — companion to
2593    /// [`Self::english_lexical_scalars_clone`].
2594    #[inline]
2595    pub(crate) fn our_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2596        self.our_lexical_scalars.clone()
2597    }
2598
2599    /// Replace `english_lexical_scalars` wholesale (parallel-worker setup).
2600    #[inline]
2601    pub(crate) fn set_english_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2602        self.english_lexical_scalars = v;
2603    }
2604
2605    /// Replace `our_lexical_scalars` wholesale (parallel-worker setup).
2606    #[inline]
2607    pub(crate) fn set_our_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2608        self.our_lexical_scalars = v;
2609    }
2610
2611    #[inline]
2612    fn note_our_scalar(&mut self, bare_name: &str) {
2613        if let Some(s) = self.our_lexical_scalars.last_mut() {
2614            s.insert(bare_name.to_string());
2615        }
2616    }
2617
2618    #[inline]
2619    fn note_our_array(&mut self, bare_name: &str) {
2620        if let Some(s) = self.our_lexical_arrays.last_mut() {
2621            s.insert(bare_name.to_string());
2622        }
2623    }
2624
2625    #[inline]
2626    fn note_our_hash(&mut self, bare_name: &str) {
2627        if let Some(s) = self.our_lexical_hashes.last_mut() {
2628            s.insert(bare_name.to_string());
2629        }
2630    }
2631
2632    /// Package stash key for `our @arr` — mirrors [`Self::stash_scalar_name_for_package`].
2633    /// Used at the `our`-decl site to choose the storage key; bare-name reads go
2634    /// through [`Self::tree_array_storage_name`] which only qualifies when the
2635    /// surrounding scope actually has a matching `our` binding.
2636    pub(crate) fn stash_array_full_name_for_package(&self, name: &str) -> String {
2637        if name.contains("::") {
2638            return name.to_string();
2639        }
2640        let pkg = self.current_package();
2641        if pkg.is_empty() || pkg == "main" {
2642            format!("main::{}", name)
2643        } else {
2644            format!("{}::{}", pkg, name)
2645        }
2646    }
2647
2648    /// Package stash key for `our %h` — companion to
2649    /// [`Self::stash_array_full_name_for_package`].
2650    pub(crate) fn stash_hash_full_name_for_package(&self, name: &str) -> String {
2651        if name.contains("::") {
2652            return name.to_string();
2653        }
2654        let pkg = self.current_package();
2655        if pkg.is_empty() || pkg == "main" {
2656            format!("main::{}", name)
2657        } else {
2658            format!("{}::{}", pkg, name)
2659        }
2660    }
2661
2662    /// Bare `@x` after `our @x` reads the package stash array. Walk the
2663    /// lexical chain inside-out and qualify only when an enclosing scope
2664    /// marked the name as `our`.
2665    pub(crate) fn tree_array_storage_name(&self, name: &str) -> String {
2666        if name.contains("::") {
2667            return name.to_string();
2668        }
2669        for ours in self.our_lexical_arrays.iter().rev() {
2670            if ours.contains(name) {
2671                return self.stash_array_full_name_for_package(name);
2672            }
2673        }
2674        name.to_string()
2675    }
2676
2677    /// Bare `%h` after `our %h` reads the package stash hash. See
2678    /// [`Self::tree_array_storage_name`] for the resolution policy.
2679    pub(crate) fn tree_hash_storage_name(&self, name: &str) -> String {
2680        if name.contains("::") {
2681            return name.to_string();
2682        }
2683        for ours in self.our_lexical_hashes.iter().rev() {
2684            if ours.contains(name) {
2685                return self.stash_hash_full_name_for_package(name);
2686            }
2687        }
2688        name.to_string()
2689    }
2690
2691    /// Public wrapper for [`Self::english_note_lexical_scalar`] — used by bytecode
2692    /// `Op::DeclareOurSync*` to register bare names so worker `tree_scalar_storage_name`
2693    /// reads rewrite to `Pkg::x`.
2694    #[inline]
2695    pub(crate) fn english_note_lexical_scalar_pub(&mut self, name: &str) {
2696        self.english_note_lexical_scalar(name);
2697    }
2698
2699    /// Public wrapper for [`Self::note_our_scalar`] — see [`Self::english_note_lexical_scalar_pub`].
2700    #[inline]
2701    pub(crate) fn note_our_scalar_pub(&mut self, bare_name: &str) {
2702        self.note_our_scalar(bare_name);
2703    }
2704
2705    pub(crate) fn scope_pop_hook(&mut self) {
2706        if !self.scope.can_pop_frame() {
2707            return;
2708        }
2709        // Execute deferred blocks in LIFO order before popping the frame.
2710        // Important: defer blocks run in the CURRENT scope (not a new frame),
2711        // so they can modify variables in the enclosing scope.
2712        let defers = self.scope.take_defers();
2713        for coderef in defers {
2714            if let Some(sub) = coderef.as_code_ref() {
2715                // Execute the defer block body directly in the current scope,
2716                // without creating a new frame or restoring closure captures.
2717                // This allows defer { $x = 100 } to modify the outer $x.
2718                let saved_wa = self.wantarray_kind;
2719                self.wantarray_kind = WantarrayCtx::Void;
2720                let _ = self.exec_block_no_scope(&sub.body);
2721                self.wantarray_kind = saved_wa;
2722            }
2723        }
2724        // Save state variable values back before popping the frame
2725        if let Some(bindings) = self.state_bindings_stack.pop() {
2726            for (var_name, state_key) in &bindings {
2727                let val = self.scope.get_scalar(var_name).clone();
2728                self.state_vars.insert(state_key.clone(), val);
2729            }
2730        }
2731        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2732        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2733        if let Some(entries) = self.special_var_restore_frames.pop() {
2734            for (name, old) in entries.into_iter().rev() {
2735                let _ = self.set_special_var(&name, &old);
2736            }
2737        }
2738        if let Some(entries) = self.glob_restore_frames.pop() {
2739            for (name, old) in entries.into_iter().rev() {
2740                match old {
2741                    Some(s) => {
2742                        self.glob_handle_alias.insert(name, s);
2743                    }
2744                    None => {
2745                        self.glob_handle_alias.remove(&name);
2746                    }
2747                }
2748            }
2749        }
2750        self.scope.pop_frame();
2751        let _ = self.english_lexical_scalars.pop();
2752        let _ = self.our_lexical_scalars.pop();
2753        let _ = self.our_lexical_arrays.pop();
2754        let _ = self.our_lexical_hashes.pop();
2755    }
2756
2757    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2758    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2759    #[inline]
2760    pub(crate) fn enable_parallel_guard(&mut self) {
2761        self.scope.set_parallel_guard(true);
2762    }
2763
2764    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues after VM compilation.
2765    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2766        self.begin_blocks.clear();
2767        self.unit_check_blocks.clear();
2768        self.check_blocks.clear();
2769        self.init_blocks.clear();
2770        self.end_blocks.clear();
2771    }
2772
2773    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2774    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2775    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2776    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2777    /// there because it only calls [`Scope::pop_frame`].
2778    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2779        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2780            self.scope_pop_hook();
2781        }
2782    }
2783
2784    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2785    ///
2786    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2787    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2788    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2789    /// workers that call `perl_signal::poll`).
2790    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> StrykeResult<()> {
2791        self.touch_env_hash("SIG");
2792        let v = self.scope.get_hash_element("SIG", sig);
2793        if v.is_undef() {
2794            return Self::default_sig_action(sig);
2795        }
2796        if let Some(s) = v.as_str() {
2797            if s == "IGNORE" {
2798                return Ok(());
2799            }
2800            if s == "DEFAULT" {
2801                return Self::default_sig_action(sig);
2802            }
2803        }
2804        if let Some(sub) = v.as_code_ref() {
2805            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2806                Ok(_) => Ok(()),
2807                Err(FlowOrError::Flow(_)) => Ok(()),
2808                Err(FlowOrError::Error(e)) => Err(e),
2809            }
2810        } else {
2811            Self::default_sig_action(sig)
2812        }
2813    }
2814
2815    /// Dispatch `$SIG{__WARN__}` if a coderef is installed; fall back to stderr.
2816    /// Recursion is guarded by temporarily clearing the slot during dispatch so
2817    /// a `__WARN__` handler that itself calls `warn` does not loop.
2818    pub(crate) fn fire_pseudosig_warn(&mut self, msg: &str, line: usize) -> StrykeResult<()> {
2819        self.touch_env_hash("SIG");
2820        let slot = self.scope.get_hash_element("SIG", "__WARN__");
2821        if let Some(sub) = slot.as_code_ref() {
2822            let prev = slot;
2823            let _ = self
2824                .scope
2825                .set_hash_element("SIG", "__WARN__", StrykeValue::UNDEF);
2826            let arg = StrykeValue::string(msg.to_string());
2827            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2828            let _ = self.scope.set_hash_element("SIG", "__WARN__", prev);
2829            return match r {
2830                Ok(_) => Ok(()),
2831                Err(FlowOrError::Flow(_)) => Ok(()),
2832                Err(FlowOrError::Error(e)) => Err(e),
2833            };
2834        }
2835        eprint!("{}", msg);
2836        Ok(())
2837    }
2838
2839    /// Dispatch `$SIG{__DIE__}` if a coderef is installed. Perl semantics:
2840    /// the handler runs even when the die is going to be caught by an `eval`,
2841    /// and the die still propagates afterwards. If the handler itself dies,
2842    /// that error replaces the original. Recursion is guarded by temporarily
2843    /// clearing the slot during dispatch.
2844    pub(crate) fn fire_pseudosig_die(&mut self, msg: &str, line: usize) -> StrykeResult<()> {
2845        self.touch_env_hash("SIG");
2846        let slot = self.scope.get_hash_element("SIG", "__DIE__");
2847        if let Some(sub) = slot.as_code_ref() {
2848            let prev = slot;
2849            let _ = self
2850                .scope
2851                .set_hash_element("SIG", "__DIE__", StrykeValue::UNDEF);
2852            let arg = StrykeValue::string(msg.to_string());
2853            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2854            let _ = self.scope.set_hash_element("SIG", "__DIE__", prev);
2855            return match r {
2856                Ok(_) => Ok(()),
2857                Err(FlowOrError::Flow(_)) => Ok(()),
2858                Err(FlowOrError::Error(e)) => Err(e),
2859            };
2860        }
2861        Ok(())
2862    }
2863
2864    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2865    #[inline]
2866    fn default_sig_action(sig: &str) -> StrykeResult<()> {
2867        match sig {
2868            // 128 + signal number (common shell convention)
2869            "INT" => std::process::exit(130),
2870            "TERM" => std::process::exit(143),
2871            "ALRM" => std::process::exit(142),
2872            // Default for SIGCHLD is ignore
2873            "CHLD" => Ok(()),
2874            _ => Ok(()),
2875        }
2876    }
2877
2878    /// Notify the debugger that a user sub call is about to happen. No-op
2879    /// when no debugger is attached. Paired with [`Self::debugger_leave_sub`]
2880    /// after the call returns — the depth counter is what makes step-over
2881    /// skip past UDFs instead of stepping into them.
2882    #[inline]
2883    pub fn debugger_enter_sub(&mut self, name: &str) {
2884        if let Some(dbg) = &mut self.debugger {
2885            dbg.enter_sub(name);
2886        }
2887    }
2888
2889    /// Pair to [`Self::debugger_enter_sub`].
2890    #[inline]
2891    pub fn debugger_leave_sub(&mut self) {
2892        if let Some(dbg) = &mut self.debugger {
2893            dbg.leave_sub();
2894        }
2895    }
2896
2897    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2898    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2899    pub fn materialize_env_if_needed(&mut self) {
2900        if self.env_materialized {
2901            return;
2902        }
2903        self.env = std::env::vars()
2904            .map(|(k, v)| (k, StrykeValue::string(v)))
2905            .collect();
2906        self.scope
2907            .set_hash("ENV", self.env.clone())
2908            .expect("set %ENV");
2909        self.env_materialized = true;
2910    }
2911
2912    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2913    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2914        self.materialize_env_if_needed();
2915        if let Some(x) = self.log_level_override {
2916            return x;
2917        }
2918        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2919        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2920    }
2921
2922    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2923    pub(crate) fn no_color_effective(&mut self) -> bool {
2924        self.materialize_env_if_needed();
2925        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2926        if v.is_undef() {
2927            return false;
2928        }
2929        !v.to_string().is_empty()
2930    }
2931
2932    #[inline]
2933    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2934        // `%main::ENV` ≡ `%ENV`, `%main::parameters` ≡ `%parameters`,
2935        // `%main::a` ≡ `%a`, etc. Strip the `main::` qualifier so the
2936        // lazy-materialize / reflection-hash branches fire on the
2937        // canonical bare name. Without this, `exists $main::ENV{PATH}`
2938        // returns 0 on a fresh interpreter because ENV never gets
2939        // materialized.
2940        let hash_name: &str = crate::scope::strip_main_prefix(hash_name).unwrap_or(hash_name);
2941        if hash_name == "ENV" {
2942            self.materialize_env_if_needed();
2943        } else if hash_name == "parameters"
2944            && !crate::compat_mode()
2945            && !self.scope.has_lexical_hash("parameters")
2946        {
2947            // `%parameters` (zsh `$parameters` analogue) — rebuild on every
2948            // read so it always reflects current scope state. Frozen install,
2949            // so user code can read but not assign into it. Stryke-only;
2950            // `--compat` skips the auto-refresh so Perl 5 scripts that use
2951            // `%parameters` for their own purposes are unaffected.
2952            self.ensure_reflection_hashes();
2953            self.refresh_parameters_hash();
2954        } else if hash_name.ends_with("::") && hash_name.len() > 2 {
2955            // `%main::` / `%Foo::` — repopulate from current symbol table on
2956            // every read so newly-defined subs / `our` vars become visible
2957            // without an explicit `refresh_stashes()` call. Cheap: walks
2958            // `subs` keys + frame name lists, no value cloning.
2959            self.refresh_package_stashes();
2960        } else if !self.reflection_hashes_ready && !self.scope.has_lexical_hash(hash_name) {
2961            match hash_name {
2962                "b"
2963                | "pc"
2964                | "e"
2965                | "a"
2966                | "d"
2967                | "c"
2968                | "p"
2969                | "k"
2970                | "o"
2971                | "v"
2972                | "all"
2973                | "stryke::builtins"
2974                | "stryke::perl_compats"
2975                | "stryke::extensions"
2976                | "stryke::aliases"
2977                | "stryke::descriptions"
2978                | "stryke::categories"
2979                | "stryke::primaries"
2980                | "stryke::keywords"
2981                | "stryke::operators"
2982                | "stryke::special_vars"
2983                | "stryke::all" => {
2984                    self.ensure_reflection_hashes();
2985                }
2986                _ => {}
2987            }
2988        }
2989    }
2990
2991    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2992    pub(crate) fn exists_arrow_hash_element(
2993        &self,
2994        container: StrykeValue,
2995        key: &str,
2996        line: usize,
2997    ) -> StrykeResult<bool> {
2998        if let Some(r) = container.as_hash_ref() {
2999            return Ok(r.read().contains_key(key));
3000        }
3001        if let Some(b) = container.as_blessed_ref() {
3002            let data = b.data.read();
3003            if let Some(r) = data.as_hash_ref() {
3004                return Ok(r.read().contains_key(key));
3005            }
3006            if let Some(hm) = data.as_hash_map() {
3007                return Ok(hm.contains_key(key));
3008            }
3009            return Err(StrykeError::runtime(
3010                "exists argument is not a HASH reference",
3011                line,
3012            ));
3013        }
3014        // `exists $h{x}{y}` when `$h{x}` is undef OR a non-hash scalar: Perl
3015        // returns false for the deepest test without erroring. Stryke
3016        // previously errored on the intermediate. Match Perl. (BUG-009)
3017        let _ = line;
3018        Ok(false)
3019    }
3020
3021    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
3022    pub(crate) fn delete_arrow_hash_element(
3023        &self,
3024        container: StrykeValue,
3025        key: &str,
3026        line: usize,
3027    ) -> StrykeResult<StrykeValue> {
3028        if let Some(r) = container.as_hash_ref() {
3029            return Ok(r.write().shift_remove(key).unwrap_or(StrykeValue::UNDEF));
3030        }
3031        if let Some(b) = container.as_blessed_ref() {
3032            let mut data = b.data.write();
3033            if let Some(r) = data.as_hash_ref() {
3034                return Ok(r.write().shift_remove(key).unwrap_or(StrykeValue::UNDEF));
3035            }
3036            if let Some(mut map) = data.as_hash_map() {
3037                let v = map.shift_remove(key).unwrap_or(StrykeValue::UNDEF);
3038                *data = StrykeValue::hash(map);
3039                return Ok(v);
3040            }
3041            return Err(StrykeError::runtime(
3042                "delete argument is not a HASH reference",
3043                line,
3044            ));
3045        }
3046        Err(StrykeError::runtime(
3047            "delete argument is not a HASH reference",
3048            line,
3049        ))
3050    }
3051
3052    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
3053    pub(crate) fn exists_arrow_array_element(
3054        &self,
3055        container: StrykeValue,
3056        idx: i64,
3057        line: usize,
3058    ) -> StrykeResult<bool> {
3059        if let Some(a) = container.as_array_ref() {
3060            let arr = a.read();
3061            let i = if idx < 0 {
3062                (arr.len() as i64 + idx) as usize
3063            } else {
3064                idx as usize
3065            };
3066            return Ok(i < arr.len());
3067        }
3068        // `exists $a[5][0]` when `$a[5]` is missing OR a non-array scalar:
3069        // Perl returns false at the deepest test without erroring. (BUG-009)
3070        let _ = line;
3071        Ok(false)
3072    }
3073
3074    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
3075    pub(crate) fn delete_arrow_array_element(
3076        &self,
3077        container: StrykeValue,
3078        idx: i64,
3079        line: usize,
3080    ) -> StrykeResult<StrykeValue> {
3081        if let Some(a) = container.as_array_ref() {
3082            let mut arr = a.write();
3083            let i = if idx < 0 {
3084                (arr.len() as i64 + idx) as usize
3085            } else {
3086                idx as usize
3087            };
3088            if i >= arr.len() {
3089                return Ok(StrykeValue::UNDEF);
3090            }
3091            let old = arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
3092            arr[i] = StrykeValue::UNDEF;
3093            return Ok(old);
3094        }
3095        Err(StrykeError::runtime(
3096            "delete argument is not an ARRAY reference",
3097            line,
3098        ))
3099    }
3100
3101    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
3102    pub(crate) fn inc_directories(&self) -> Vec<String> {
3103        let mut v: Vec<String> = self
3104            .scope
3105            .get_array("INC")
3106            .into_iter()
3107            .map(|x| x.to_string())
3108            .filter(|s| !s.is_empty())
3109            .collect();
3110        if v.is_empty() {
3111            v.push(".".to_string());
3112        }
3113        v
3114    }
3115
3116    #[inline]
3117    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
3118        matches!(
3119            name,
3120            "_" | "0"
3121                | "!"
3122                | "@"
3123                | "/"
3124                | "\\"
3125                | ","
3126                | "."
3127                | "__PACKAGE__"
3128                | "$$"
3129                | "|"
3130                | "?"
3131                | "\""
3132                | "&"
3133                | "`"
3134                | "'"
3135                | "+"
3136                | "<"
3137                | ">"
3138                | "("
3139                | ")"
3140                | "]"
3141                | ";"
3142                | "ARGV"
3143                | "%"
3144                | "="
3145                | "-"
3146                | ":"
3147                | "*"
3148                | "INC"
3149                // sort/reduce comparator slots — predefined package globals
3150                // ($main::a, $main::b). Perl exempts them globally, not just
3151                // inside sort blocks, so any reference compiles cleanly.
3152                | "a"
3153                | "b"
3154        ) || name.chars().all(|c| c.is_ascii_digit())
3155            || name.starts_with('^')
3156            || (name.starts_with('#') && name.len() > 1)
3157            // Stryke implicit closure-param slots (`$_0`, `$_1`, …, `$_99`).
3158            // These are auto-bound inside any block that takes positional
3159            // arguments (sort comparators, reduce blocks, sub bodies, map/
3160            // grep blocks). Treat them like the digit-only match groups —
3161            // exempt globally so a strict-vars check inside a `preduce {
3162            // $_0 + $_1 }` block doesn't reject them as undeclared.
3163            || (name.starts_with('_')
3164                && name.len() > 1
3165                && name[1..].chars().all(|c| c.is_ascii_digit()))
3166    }
3167
3168    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3169        if !self.strict_vars
3170            || Self::strict_scalar_exempt(name)
3171            || name.contains("::")
3172            || self.scope.scalar_binding_exists(name)
3173        {
3174            return Ok(());
3175        }
3176        Err(StrykeError::runtime(
3177            format!(
3178                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
3179                name, name
3180            ),
3181            line,
3182        )
3183        .into())
3184    }
3185
3186    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3187        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
3188            return Ok(());
3189        }
3190        Err(StrykeError::runtime(
3191            format!(
3192                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
3193                name, name
3194            ),
3195            line,
3196        )
3197        .into())
3198    }
3199
3200    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3201        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
3202        if !self.strict_vars
3203            || name.contains("::")
3204            || self.scope.hash_binding_exists(name)
3205            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
3206        {
3207            return Ok(());
3208        }
3209        Err(StrykeError::runtime(
3210            format!(
3211                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
3212                name, name
3213            ),
3214            line,
3215        )
3216        .into())
3217    }
3218
3219    fn looks_like_version_only(spec: &str) -> bool {
3220        let t = spec.trim();
3221        !t.is_empty()
3222            && !t.contains('/')
3223            && !t.contains('\\')
3224            && !t.contains("::")
3225            && t.chars()
3226                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
3227            && t.chars().any(|c| c.is_ascii_digit())
3228    }
3229
3230    fn module_spec_to_relpath(spec: &str) -> String {
3231        let t = spec.trim();
3232        if t.contains("::") {
3233            format!("{}.pm", t.replace("::", "/"))
3234        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
3235            t.replace('\\', "/")
3236        } else {
3237            format!("{}.pm", t)
3238        }
3239    }
3240
3241    /// Lockfile- and pin-driven module resolution (RFC §"Module Resolution").
3242    /// Tries the script's project root first (walked up from `script_file`'s
3243    /// directory), then the cwd's project root, then cwd itself. The first
3244    /// anchor whose `resolve_module` returns `Some` wins.
3245    ///
3246    /// Anchoring at the script's directory (not just cwd) means `s main.stk`
3247    /// from a sibling directory still finds the project-local `lib/Foo.stk`
3248    /// and the project's `stryke.lock`. Previously this used cwd only, so
3249    /// `s --lint /abs/path/proj/main.stk` from /tmp would skip arms 1 and 2
3250    /// entirely and only hit arm 3 (global installed.toml).
3251    ///
3252    /// The `relpath` arg is the `@INC`-style path (`Foo/Bar.pm`) used
3253    /// elsewhere in `require`; it is converted to a logical name (`Foo::Bar`)
3254    /// for the resolver. `.pm`, `.pl`, and `.stk` suffixes are all stripped.
3255    fn try_resolve_via_lockfile(
3256        script_file: &str,
3257        relpath: &str,
3258        version: Option<&str>,
3259    ) -> LockfileResolution {
3260        let stem = relpath
3261            .strip_suffix(".pm")
3262            .or_else(|| relpath.strip_suffix(".pl"))
3263            .or_else(|| relpath.strip_suffix(".stk"))
3264            .unwrap_or(relpath);
3265        let logical = stem.replace('/', "::");
3266
3267        let mut anchors: Vec<std::path::PathBuf> = Vec::new();
3268        if !script_file.is_empty()
3269            && script_file != "-e"
3270            && script_file != "-"
3271            && script_file != "repl"
3272        {
3273            let script_dir = std::path::Path::new(script_file)
3274                .parent()
3275                .map(std::path::Path::to_path_buf)
3276                .unwrap_or_else(|| std::path::PathBuf::from("."));
3277            if let Some(root) = crate::pkg::commands::find_project_root(&script_dir) {
3278                anchors.push(root);
3279            }
3280            anchors.push(script_dir);
3281        }
3282        if let Ok(cwd) = std::env::current_dir() {
3283            if let Some(root) = crate::pkg::commands::find_project_root(&cwd) {
3284                anchors.push(root);
3285            }
3286            anchors.push(cwd);
3287        }
3288
3289        // First anchor with a refused pin wins — once a project's
3290        // lockfile or a use-site pin can't be honored, falling through
3291        // to a different anchor's @INC silently substitutes a wrong
3292        // version. Surface the refusal.
3293        let mut pin_err: Option<String> = None;
3294        for anchor in anchors {
3295            match crate::pkg::commands::resolve_module(&anchor, &logical, version) {
3296                Ok(Some(found)) => return LockfileResolution::Found(found),
3297                Ok(None) => continue,
3298                Err(e) => {
3299                    if pin_err.is_none() {
3300                        pin_err = Some(e.to_string());
3301                    }
3302                }
3303            }
3304        }
3305        if let Some(msg) = pin_err {
3306            return LockfileResolution::PinUnsatisfied(msg);
3307        }
3308        LockfileResolution::NotFound
3309    }
3310
3311    /// `sub name` in `package P` → stash key `P::name`. `sub Q::name { }` is already fully
3312    /// qualified — do not prepend the current package. Unqualified names in `main` are stored
3313    /// **bare** (`name`), matching the compiler's `Op::Call` interning so the VM's
3314    /// `sub_for_closure_restore` lookup hits in one step.
3315    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
3316        if name.contains("::") {
3317            return name.to_string();
3318        }
3319        let pkg = self.current_package();
3320        if pkg.is_empty() || pkg == "main" {
3321            name.to_string()
3322        } else {
3323            format!("{}::{}", pkg, name)
3324        }
3325    }
3326
3327    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
3328    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
3329        let mut msg = format!("Undefined subroutine &{}", name);
3330        if self.strict_subs {
3331            msg.push_str(
3332                " (strict subs: declare the sub or use a fully qualified name before calling)",
3333            );
3334        }
3335        msg
3336    }
3337
3338    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
3339    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
3340        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
3341        if self.strict_subs {
3342            msg.push_str(
3343                " (strict subs: declare the sub or use a fully qualified name before calling)",
3344            );
3345        }
3346        msg
3347    }
3348
3349    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
3350    fn import_alias_key(&self, short: &str) -> String {
3351        self.qualify_sub_key(short)
3352    }
3353
3354    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
3355    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
3356        if imports.len() == 1 {
3357            match &imports[0].kind {
3358                ExprKind::QW(ws) => return ws.is_empty(),
3359                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
3360                ExprKind::List(xs) => return xs.is_empty(),
3361                _ => {}
3362            }
3363        }
3364        false
3365    }
3366
3367    /// After `require`, copy `Module::export` → caller stash per `use` list.
3368    fn apply_module_import(
3369        &mut self,
3370        module: &str,
3371        imports: &[Expr],
3372        line: usize,
3373    ) -> StrykeResult<()> {
3374        if imports.is_empty() {
3375            return self.import_all_from_module(module, line);
3376        }
3377        if Self::is_explicit_empty_import_list(imports) {
3378            return Ok(());
3379        }
3380        let names = Self::pragma_import_strings(imports, line)?;
3381        if names.is_empty() {
3382            return Ok(());
3383        }
3384        for name in names {
3385            self.import_one_symbol(module, &name, line)?;
3386        }
3387        Ok(())
3388    }
3389
3390    fn import_all_from_module(&mut self, module: &str, line: usize) -> StrykeResult<()> {
3391        if let Some(lists) = self.module_export_lists.get(module) {
3392            let export: Vec<String> = lists.export.clone();
3393            for short in export {
3394                self.import_named_sub(module, &short, line)?;
3395            }
3396            return Ok(());
3397        }
3398        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
3399        let prefix = format!("{}::", module);
3400        let keys: Vec<String> = self
3401            .subs
3402            .keys()
3403            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
3404            .cloned()
3405            .collect();
3406        for k in keys {
3407            let short = k[prefix.len()..].to_string();
3408            if let Some(sub) = self.subs.get(&k).cloned() {
3409                let alias = self.import_alias_key(&short);
3410                self.subs.insert(alias, sub);
3411            }
3412        }
3413        Ok(())
3414    }
3415
3416    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
3417    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> StrykeResult<()> {
3418        let qual = format!("{}::{}", module, short);
3419        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
3420            StrykeError::runtime(
3421                format!(
3422                    "`{}` is not defined in module `{}` (expected `{}`)",
3423                    short, module, qual
3424                ),
3425                line,
3426            )
3427        })?;
3428        let alias = self.import_alias_key(short);
3429        self.subs.insert(alias, sub);
3430        Ok(())
3431    }
3432
3433    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> StrykeResult<()> {
3434        if let Some(lists) = self.module_export_lists.get(module) {
3435            let allowed: HashSet<&str> = lists
3436                .export
3437                .iter()
3438                .map(|s| s.as_str())
3439                .chain(lists.export_ok.iter().map(|s| s.as_str()))
3440                .collect();
3441            if !allowed.contains(export) {
3442                return Err(StrykeError::runtime(
3443                    format!(
3444                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
3445                        export, module
3446                    ),
3447                    line,
3448                ));
3449            }
3450        }
3451        self.import_named_sub(module, export, line)
3452    }
3453
3454    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
3455    fn record_exporter_our_array_name(&mut self, name: &str, items: &[StrykeValue]) {
3456        if name != "EXPORT" && name != "EXPORT_OK" {
3457            return;
3458        }
3459        let pkg = self.current_package();
3460        if pkg.is_empty() || pkg == "main" {
3461            return;
3462        }
3463        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
3464        let ent = self.module_export_lists.entry(pkg).or_default();
3465        if name == "EXPORT" {
3466            ent.export = names;
3467        } else {
3468            ent.export_ok = names;
3469        }
3470    }
3471
3472    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
3473    /// Refresh [`StrykeSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
3474    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
3475    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
3476        let key = self.qualify_sub_key(name);
3477        let Some(sub) = self.subs.get(&key).cloned() else {
3478            return;
3479        };
3480        let captured = self.scope.capture();
3481        let closure_env = if captured.is_empty() {
3482            None
3483        } else {
3484            Some(captured)
3485        };
3486        let mut new_sub = (*sub).clone();
3487        new_sub.closure_env = closure_env;
3488        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
3489        self.subs.insert(key, Arc::new(new_sub));
3490    }
3491
3492    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<StrykeSub>> {
3493        if let Some(s) = self.subs.get(name) {
3494            return Some(s.clone());
3495        }
3496        if !name.contains("::") {
3497            // Non-`main` packages store subs at `Pkg::name`; resolve bare callers there.
3498            let pkg = self.current_package();
3499            if !pkg.is_empty() && pkg != "main" {
3500                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
3501                q.push_str(&pkg);
3502                q.push_str("::");
3503                q.push_str(name);
3504                return self.subs.get(&q).cloned();
3505            }
3506            return None;
3507        }
3508        // `\&main::greet` / `defined &main::greet`: subs in `main` are stored bare so the
3509        // compiler's `Op::Call("greet", ...)` and the runtime stash lookup share a key.
3510        // Strip the `main::` qualifier and try the bare form so explicit qualified callers
3511        // still resolve to the same sub.
3512        if let Some(rest) = name.strip_prefix("main::") {
3513            if !rest.contains("::") {
3514                return self.subs.get(rest).cloned();
3515            }
3516        }
3517        None
3518    }
3519
3520    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
3521    /// before calling `import`).
3522    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
3523        if let Some(first) = imports.first() {
3524            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
3525                return &imports[1..];
3526            }
3527        }
3528        imports
3529    }
3530
3531    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
3532    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> StrykeResult<Vec<String>> {
3533        let mut out = Vec::new();
3534        for e in imports {
3535            match &e.kind {
3536                ExprKind::String(s) => out.push(s.clone()),
3537                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
3538                ExprKind::Integer(n) => out.push(n.to_string()),
3539                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
3540                // a single interpolated variable.  Reconstruct the sigil+name form.
3541                ExprKind::InterpolatedString(parts) => {
3542                    let mut s = String::new();
3543                    for p in parts {
3544                        match p {
3545                            StringPart::Literal(l) => s.push_str(l),
3546                            StringPart::ScalarVar(v) => {
3547                                s.push('$');
3548                                s.push_str(v);
3549                            }
3550                            StringPart::ArrayVar(v) => {
3551                                s.push('@');
3552                                s.push_str(v);
3553                            }
3554                            _ => {
3555                                return Err(StrykeError::runtime(
3556                                    "pragma import must be a compile-time string, qw(), or integer",
3557                                    e.line.max(default_line),
3558                                ));
3559                            }
3560                        }
3561                    }
3562                    out.push(s);
3563                }
3564                _ => {
3565                    return Err(StrykeError::runtime(
3566                        "pragma import must be a compile-time string, qw(), or integer",
3567                        e.line.max(default_line),
3568                    ));
3569                }
3570            }
3571        }
3572        Ok(out)
3573    }
3574
3575    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3576        if imports.is_empty() {
3577            self.strict_refs = true;
3578            self.strict_subs = true;
3579            self.strict_vars = true;
3580            return Ok(());
3581        }
3582        let names = Self::pragma_import_strings(imports, line)?;
3583        for name in names {
3584            match name.as_str() {
3585                "refs" => self.strict_refs = true,
3586                "subs" => self.strict_subs = true,
3587                "vars" => self.strict_vars = true,
3588                _ => {
3589                    return Err(StrykeError::runtime(
3590                        format!("Unknown strict mode `{}`", name),
3591                        line,
3592                    ));
3593                }
3594            }
3595        }
3596        Ok(())
3597    }
3598
3599    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3600        if imports.is_empty() {
3601            self.strict_refs = false;
3602            self.strict_subs = false;
3603            self.strict_vars = false;
3604            return Ok(());
3605        }
3606        let names = Self::pragma_import_strings(imports, line)?;
3607        for name in names {
3608            match name.as_str() {
3609                "refs" => self.strict_refs = false,
3610                "subs" => self.strict_subs = false,
3611                "vars" => self.strict_vars = false,
3612                _ => {
3613                    return Err(StrykeError::runtime(
3614                        format!("Unknown strict mode `{}`", name),
3615                        line,
3616                    ));
3617                }
3618            }
3619        }
3620        Ok(())
3621    }
3622
3623    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3624        let items = Self::pragma_import_strings(imports, line)?;
3625        if items.is_empty() {
3626            return Err(StrykeError::runtime(
3627                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
3628                line,
3629            ));
3630        }
3631        for item in items {
3632            let s = item.trim();
3633            if let Some(rest) = s.strip_prefix(':') {
3634                self.apply_feature_bundle(rest, line)?;
3635            } else {
3636                self.apply_feature_name(s, true, line)?;
3637            }
3638        }
3639        Ok(())
3640    }
3641
3642    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3643        if imports.is_empty() {
3644            self.feature_bits = 0;
3645            return Ok(());
3646        }
3647        let items = Self::pragma_import_strings(imports, line)?;
3648        for item in items {
3649            let s = item.trim();
3650            if let Some(rest) = s.strip_prefix(':') {
3651                self.clear_feature_bundle(rest);
3652            } else {
3653                self.apply_feature_name(s, false, line)?;
3654            }
3655        }
3656        Ok(())
3657    }
3658
3659    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> StrykeResult<()> {
3660        let key = v.trim();
3661        match key {
3662            "5.10" | "5.010" | "5.10.0" => {
3663                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3664            }
3665            "5.12" | "5.012" | "5.12.0" => {
3666                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3667            }
3668            _ => {
3669                return Err(StrykeError::runtime(
3670                    format!("unsupported feature bundle :{}", key),
3671                    line,
3672                ));
3673            }
3674        }
3675        Ok(())
3676    }
3677
3678    fn clear_feature_bundle(&mut self, v: &str) {
3679        let key = v.trim();
3680        if matches!(
3681            key,
3682            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
3683        ) {
3684            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
3685        }
3686    }
3687
3688    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> StrykeResult<()> {
3689        let bit = match name {
3690            "say" => FEAT_SAY,
3691            "state" => FEAT_STATE,
3692            "switch" => FEAT_SWITCH,
3693            "unicode_strings" => FEAT_UNICODE_STRINGS,
3694            // Features that stryke accepts as known but tracks no separate bit for —
3695            // either always-on, always-off, or syntactic sugar already enabled.
3696            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
3697            "postderef"
3698            | "postderef_qq"
3699            | "evalbytes"
3700            | "current_sub"
3701            | "fc"
3702            | "lexical_subs"
3703            | "signatures"
3704            | "refaliasing"
3705            | "bitwise"
3706            | "isa"
3707            | "indirect"
3708            | "multidimensional"
3709            | "bareword_filehandles"
3710            | "try"
3711            | "defer"
3712            | "extra_paired_delimiters"
3713            | "module_true"
3714            | "class"
3715            | "array_base" => return Ok(()),
3716            _ => {
3717                return Err(StrykeError::runtime(
3718                    format!("unknown feature `{}`", name),
3719                    line,
3720                ));
3721            }
3722        };
3723        if enable {
3724            self.feature_bits |= bit;
3725        } else {
3726            self.feature_bits &= !bit;
3727        }
3728        Ok(())
3729    }
3730
3731    /// `use Module VERSION` — Perl-style version pin on a `require` call.
3732    /// Same flow as [`Self::require_execute`] but threads the version
3733    /// constraint down to the lockfile/store resolver so we land on
3734    /// `<store>/<name>@<version>/` directly. `None` means "no pin"
3735    /// and falls back to the lockfile / installed-index lookup.
3736    pub(crate) fn require_execute_versioned(
3737        &mut self,
3738        spec: &str,
3739        version: Option<&str>,
3740        line: usize,
3741    ) -> StrykeResult<StrykeValue> {
3742        // Pragmas (strict/utf8/feature/…) have no package version; they
3743        // short-circuit before any store lookup, so the pin is irrelevant.
3744        // Defer to the unversioned path for that case.
3745        if version.is_none() {
3746            return self.require_execute(spec, line);
3747        }
3748        let t = spec.trim();
3749        if t.is_empty() {
3750            return Err(StrykeError::runtime("require: empty argument", line));
3751        }
3752        let p = Path::new(t);
3753        if p.is_absolute() || t.starts_with("./") || t.starts_with("../") {
3754            // A pin on a path-style require is meaningless — the path
3755            // already names the file. Fall back to the unversioned path.
3756            return self.require_execute(spec, line);
3757        }
3758        if Self::looks_like_version_only(t) {
3759            return Ok(StrykeValue::integer(1));
3760        }
3761        let relpath = Self::module_spec_to_relpath(t);
3762        self.require_from_inc_versioned(&relpath, version, line)
3763    }
3764
3765    /// `require EXPR` — load once, record `%INC`, return `1` on success.
3766    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> StrykeResult<StrykeValue> {
3767        let t = spec.trim();
3768        if t.is_empty() {
3769            return Err(StrykeError::runtime("require: empty argument", line));
3770        }
3771        match t {
3772            "strict" => {
3773                self.apply_use_strict(&[], line)?;
3774                return Ok(StrykeValue::integer(1));
3775            }
3776            "utf8" => {
3777                self.utf8_pragma = true;
3778                return Ok(StrykeValue::integer(1));
3779            }
3780            "feature" | "v5" => {
3781                return Ok(StrykeValue::integer(1));
3782            }
3783            "warnings" => {
3784                self.warnings = true;
3785                return Ok(StrykeValue::integer(1));
3786            }
3787            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
3788                return Ok(StrykeValue::integer(1));
3789            }
3790            _ => {}
3791        }
3792        let p = Path::new(t);
3793        if p.is_absolute() {
3794            return self.require_absolute_path(p, line);
3795        }
3796        if t.starts_with("./") || t.starts_with("../") {
3797            return self.require_relative_path(p, line);
3798        }
3799        if Self::looks_like_version_only(t) {
3800            return Ok(StrykeValue::integer(1));
3801        }
3802        let relpath = Self::module_spec_to_relpath(t);
3803        self.require_from_inc(&relpath, line)
3804    }
3805
3806    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
3807    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> StrykeResult<()> {
3808        let v = self.scope.get_hash_element("^HOOK", key);
3809        if v.is_undef() {
3810            return Ok(());
3811        }
3812        let Some(sub) = v.as_code_ref() else {
3813            return Ok(());
3814        };
3815        let r = self.call_sub(
3816            sub.as_ref(),
3817            vec![StrykeValue::string(path.to_string())],
3818            WantarrayCtx::Scalar,
3819            line,
3820        );
3821        match r {
3822            Ok(_) => Ok(()),
3823            Err(FlowOrError::Error(e)) => Err(e),
3824            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
3825            Err(FlowOrError::Flow(other)) => Err(StrykeError::runtime(
3826                format!(
3827                    "require hook {:?} returned unexpected control flow: {:?}",
3828                    key, other
3829                ),
3830                line,
3831            )),
3832        }
3833    }
3834
3835    fn require_absolute_path(&mut self, path: &Path, line: usize) -> StrykeResult<StrykeValue> {
3836        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
3837        let key = canon.to_string_lossy().into_owned();
3838        if self.scope.exists_hash_element("INC", &key) {
3839            return Ok(StrykeValue::integer(1));
3840        }
3841        self.invoke_require_hook("require__before", &key, line)?;
3842        let code = read_file_text_perl_compat(&canon).map_err(|e| {
3843            StrykeError::runtime(
3844                format!("Can't open {} for reading: {}", canon.display(), e),
3845                line,
3846            )
3847        })?;
3848        let code = crate::data_section::strip_perl_end_marker(&code);
3849        self.scope
3850            .set_hash_element("INC", &key, StrykeValue::string(key.clone()))?;
3851        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3852        let r = crate::parse_and_run_module_in_file(code, self, &key);
3853        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3854        r?;
3855        self.invoke_require_hook("require__after", &key, line)?;
3856        Ok(StrykeValue::integer(1))
3857    }
3858
3859    fn require_relative_path(&mut self, path: &Path, line: usize) -> StrykeResult<StrykeValue> {
3860        // First try the path verbatim (cwd-relative — backward-compat
3861        // for `stryke ./Foo.stk` where the user has chdir'd into the
3862        // exercise dir).
3863        if path.exists() {
3864            return self.require_absolute_path(path, line);
3865        }
3866        // Fall back to resolving against the directory of the calling
3867        // script (`self.file`). This makes `require "./Foo.stk"` work
3868        // when the test is invoked via absolute path:
3869        //   `stryke /abs/path/to/proj/t/test_foo.stk` ← cwd unrelated
3870        //   `require "./Foo.stk"` resolves to `/abs/path/to/proj/Foo.stk`
3871        //
3872        // Walks up from `self.file`'s parent dir trying each ancestor —
3873        // covers both same-dir requires AND the common `t/` test-subdir
3874        // pattern where the test file is in `t/` and the source is one
3875        // level up.
3876        if !self.file.is_empty() {
3877            let caller = Path::new(&self.file);
3878            let mut anchor = caller.parent();
3879            while let Some(dir) = anchor {
3880                let candidate = dir.join(path);
3881                if candidate.exists() {
3882                    return self.require_absolute_path(&candidate, line);
3883                }
3884                anchor = dir.parent();
3885                // Stop once we hit the filesystem root to avoid
3886                // unbounded ascent. `parent()` on `/` returns None.
3887                if anchor.map(|p| p.as_os_str().is_empty()).unwrap_or(false) {
3888                    break;
3889                }
3890            }
3891        }
3892        Err(StrykeError::runtime(
3893            format!(
3894                "Can't locate {} (relative path does not exist)",
3895                path.display()
3896            ),
3897            line,
3898        ))
3899    }
3900
3901    fn require_from_inc(&mut self, relpath: &str, line: usize) -> StrykeResult<StrykeValue> {
3902        self.require_from_inc_versioned(relpath, None, line)
3903    }
3904
3905    fn require_from_inc_versioned(
3906        &mut self,
3907        relpath: &str,
3908        version: Option<&str>,
3909        line: usize,
3910    ) -> StrykeResult<StrykeValue> {
3911        if self.scope.exists_hash_element("INC", relpath) {
3912            return Ok(StrykeValue::integer(1));
3913        }
3914        self.invoke_require_hook("require__before", relpath, line)?;
3915
3916        // Lockfile-driven module resolution. When the cwd is inside a stryke
3917        // project (`stryke.toml` reachable), `use Foo::Bar` first looks at
3918        // `lib/Foo/Bar.stk` and then at lockfile-pinned store entries before
3919        // falling through to `@INC`. See docs/PACKAGE_REGISTRY.md §"Module
3920        // Resolution".
3921        //
3922        // `version` (Some when the caller is `use Module VERSION`) pins
3923        // the store lookup to `<store>/<name>@<version>/` directly —
3924        // bypasses both the lockfile and the global installed.toml. An
3925        // explicit pin always wins; a `None` pin defers to the
3926        // lockfile/index resolution.
3927        match Self::try_resolve_via_lockfile(&self.file, relpath, version) {
3928            LockfileResolution::Found(found) => {
3929                let code = read_file_text_perl_compat(&found).map_err(|e| {
3930                    StrykeError::runtime(
3931                        format!("Can't open {} for reading: {}", found.display(), e),
3932                        line,
3933                    )
3934                })?;
3935                let code = crate::data_section::strip_perl_end_marker(&code);
3936                let abs = found.canonicalize().unwrap_or(found);
3937                let abs_s = abs.to_string_lossy().into_owned();
3938                self.scope
3939                    .set_hash_element("INC", relpath, StrykeValue::string(abs_s.clone()))?;
3940                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3941                let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3942                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3943                r?;
3944                self.invoke_require_hook("require__after", relpath, line)?;
3945                return Ok(StrykeValue::integer(1));
3946            }
3947            // A pin (lockfile entry whose store dir is missing, or a
3948            // use-site `use Module VERSION` that didn't land) cannot
3949            // fall through to `@INC` — that would silently load a
3950            // different file under the pinned name. Surface the
3951            // refusal as a runtime error so the user runs `s install`
3952            // (or fixes the pin) instead of debugging a wrong version
3953            // they didn't ask for.
3954            LockfileResolution::PinUnsatisfied(msg) => {
3955                return Err(StrykeError::runtime(msg, line));
3956            }
3957            LockfileResolution::NotFound => {}
3958        }
3959
3960        // Check virtual modules first (AOT bundles).
3961        if let Some(code) = self.virtual_modules.get(relpath).cloned() {
3962            let code = crate::data_section::strip_perl_end_marker(&code);
3963            self.scope.set_hash_element(
3964                "INC",
3965                relpath,
3966                StrykeValue::string(format!("(virtual)/{}", relpath)),
3967            )?;
3968            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3969            let r = crate::parse_and_run_module_in_file(code, self, relpath);
3970            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3971            r?;
3972            self.invoke_require_hook("require__after", relpath, line)?;
3973            return Ok(StrykeValue::integer(1));
3974        }
3975
3976        for dir in self.inc_directories() {
3977            let full = Path::new(&dir).join(relpath);
3978            if full.is_file() {
3979                let code = read_file_text_perl_compat(&full).map_err(|e| {
3980                    StrykeError::runtime(
3981                        format!("Can't open {} for reading: {}", full.display(), e),
3982                        line,
3983                    )
3984                })?;
3985                let code = crate::data_section::strip_perl_end_marker(&code);
3986                let abs = full.canonicalize().unwrap_or(full);
3987                let abs_s = abs.to_string_lossy().into_owned();
3988                self.scope
3989                    .set_hash_element("INC", relpath, StrykeValue::string(abs_s.clone()))?;
3990                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3991                let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3992                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3993                r?;
3994                self.invoke_require_hook("require__after", relpath, line)?;
3995                return Ok(StrykeValue::integer(1));
3996            }
3997        }
3998        Err(StrykeError::runtime(
3999            format!(
4000                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
4001                relpath
4002            ),
4003            line,
4004        ))
4005    }
4006
4007    /// Register a virtual module (for AOT bundles). Path should be relative like "lib/foo.stk".
4008    pub fn register_virtual_module(&mut self, path: String, source: String) {
4009        self.virtual_modules.insert(path, source);
4010    }
4011
4012    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
4013    pub(crate) fn exec_use_stmt(
4014        &mut self,
4015        module: &str,
4016        imports: &[Expr],
4017        version: Option<&str>,
4018        line: usize,
4019    ) -> StrykeResult<()> {
4020        match module {
4021            "strict" => self.apply_use_strict(imports, line),
4022            "utf8" => {
4023                if !imports.is_empty() {
4024                    return Err(StrykeError::runtime("use utf8 takes no arguments", line));
4025                }
4026                self.utf8_pragma = true;
4027                Ok(())
4028            }
4029            "feature" => self.apply_use_feature(imports, line),
4030            "v5" => Ok(()),
4031            "warnings" => {
4032                self.warnings = true;
4033                Ok(())
4034            }
4035            "English" => {
4036                self.english_enabled = true;
4037                let args = Self::pragma_import_strings(imports, line)?;
4038                let no_match = args.iter().any(|a| a == "-no_match_vars");
4039                // Once match vars are exported (use English without -no_match_vars),
4040                // they stay available for the rest of the program — Perl exports them
4041                // into the caller's namespace and later pragmas cannot un-export them.
4042                if !no_match {
4043                    self.english_match_vars_ever_enabled = true;
4044                }
4045                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
4046                Ok(())
4047            }
4048            "Env" => self.apply_use_env(imports, line),
4049            "open" => self.apply_use_open(imports, line),
4050            "constant" => self.apply_use_constant(imports, line),
4051            "bigint" | "bignum" | "bigrat" => {
4052                // Activate BigInt promotion for `**` (and any other op
4053                // that consults the bigint pragma). `bignum` and
4054                // `bigrat` are routed here too — stryke doesn't yet
4055                // distinguish them from `bigint` for arithmetic, but
4056                // accepting them prevents the default-load path from
4057                // searching @INC for a CPAN module that won't parse.
4058                crate::set_bigint_pragma(true);
4059                Ok(())
4060            }
4061            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
4062            _ => {
4063                self.require_execute_versioned(module, version, line)?;
4064                let imports = Self::imports_after_leading_use_version(imports);
4065                self.apply_module_import(module, imports, line)?;
4066                Ok(())
4067            }
4068        }
4069    }
4070
4071    /// `no strict 'refs'`, `no warnings`, `no feature`, …
4072    pub(crate) fn exec_no_stmt(
4073        &mut self,
4074        module: &str,
4075        imports: &[Expr],
4076        line: usize,
4077    ) -> StrykeResult<()> {
4078        match module {
4079            "strict" => self.apply_no_strict(imports, line),
4080            "utf8" => {
4081                if !imports.is_empty() {
4082                    return Err(StrykeError::runtime("no utf8 takes no arguments", line));
4083                }
4084                self.utf8_pragma = false;
4085                Ok(())
4086            }
4087            "feature" => self.apply_no_feature(imports, line),
4088            "v5" => Ok(()),
4089            "warnings" => {
4090                self.warnings = false;
4091                Ok(())
4092            }
4093            "English" => {
4094                self.english_enabled = false;
4095                // Don't reset no_match_vars here — if match vars were ever enabled,
4096                // they persist (Perl's export cannot be un-exported).
4097                if !self.english_match_vars_ever_enabled {
4098                    self.english_no_match_vars = false;
4099                }
4100                Ok(())
4101            }
4102            "open" => {
4103                self.open_pragma_utf8 = false;
4104                Ok(())
4105            }
4106            "bigint" | "bignum" | "bigrat" => {
4107                crate::set_bigint_pragma(false);
4108                Ok(())
4109            }
4110            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
4111            _ => Ok(()),
4112        }
4113    }
4114
4115    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
4116    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
4117        let names = Self::pragma_import_strings(imports, line)?;
4118        for n in names {
4119            let key = n.trim_start_matches('@');
4120            if key.eq_ignore_ascii_case("PATH") {
4121                let path_env = std::env::var("PATH").unwrap_or_default();
4122                let path_vec: Vec<StrykeValue> = std::env::split_paths(&path_env)
4123                    .map(|p| StrykeValue::string(p.to_string_lossy().into_owned()))
4124                    .collect();
4125                let aname = self.stash_array_name_for_package("PATH");
4126                self.scope.declare_array(&aname, path_vec);
4127            }
4128        }
4129        Ok(())
4130    }
4131
4132    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
4133    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
4134        let items = Self::pragma_import_strings(imports, line)?;
4135        for item in items {
4136            let s = item.trim();
4137            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
4138                self.open_pragma_utf8 = true;
4139                continue;
4140            }
4141            if let Some(rest) = s.strip_prefix(":encoding(") {
4142                if let Some(inner) = rest.strip_suffix(')') {
4143                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
4144                        self.open_pragma_utf8 = true;
4145                    }
4146                }
4147            }
4148        }
4149        Ok(())
4150    }
4151
4152    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
4153    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
4154        if imports.is_empty() {
4155            return Ok(());
4156        }
4157        // `use constant 1.03;` — version check only (ignored here).
4158        if imports.len() == 1 {
4159            match &imports[0].kind {
4160                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
4161                _ => {}
4162            }
4163        }
4164        for imp in imports {
4165            match &imp.kind {
4166                ExprKind::List(items) => {
4167                    if items.len() % 2 != 0 {
4168                        return Err(StrykeError::runtime(
4169                            format!(
4170                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
4171                                items.len()
4172                            ),
4173                            line,
4174                        ));
4175                    }
4176                    let mut i = 0;
4177                    while i < items.len() {
4178                        let name = self.use_constant_name_from_expr(&items[i], line)?;
4179                        self.install_constant_from_expr(&name, &items[i + 1], line)?;
4180                        i += 2;
4181                    }
4182                }
4183                // `use constant { A => 1, B => 2 }` — hashref-block form
4184                // (BUG-086). Each `(key_expr, value_expr)` pair installs a
4185                // constant subroutine whose body returns the value expression
4186                // verbatim, so list-valued constants like
4187                // `MULTI => [1, 2, 3]` retain their original shape.
4188                ExprKind::HashRef(pairs) => {
4189                    for (key_expr, value_expr) in pairs {
4190                        let name = self.use_constant_name_from_expr(key_expr, line)?;
4191                        self.install_constant_from_expr(&name, value_expr, line)?;
4192                    }
4193                }
4194                _ => {
4195                    return Err(StrykeError::runtime(
4196                        "use constant: expected list of NAME => VALUE pairs",
4197                        line,
4198                    ));
4199                }
4200            }
4201        }
4202        Ok(())
4203    }
4204
4205    /// Pull the constant's name out of the `NAME =>` slot. The parser
4206    /// usually delivers it as `String(...)` via fat-arrow auto-quoting,
4207    /// but bare-identifier keys in a hashref-block come through as
4208    /// `Bareword(...)` too.
4209    fn use_constant_name_from_expr(&self, e: &Expr, line: usize) -> StrykeResult<String> {
4210        match &e.kind {
4211            ExprKind::String(s) => Ok(s.clone()),
4212            ExprKind::Bareword(s) => Ok(s.clone()),
4213            _ => Err(StrykeError::runtime(
4214                "use constant: constant name must be a string literal",
4215                line,
4216            )),
4217        }
4218    }
4219
4220    /// Install a constant subroutine from the raw value expression.
4221    ///
4222    /// For multi-value parenthesized lists (`(1, 2, 3)`) the body keeps the
4223    /// `List` shape so callers see the full list — fixing BUG-086 where
4224    /// the value was eagerly evaluated and collapsed to its last comma
4225    /// operand via scalar coercion. For every other shape we keep the
4226    /// original eval-once-and-freeze semantics so non-pure initializers
4227    /// like `use constant START_TIME => time()` only run once.
4228    fn install_constant_from_expr(
4229        &mut self,
4230        name: &str,
4231        value: &Expr,
4232        line: usize,
4233    ) -> StrykeResult<()> {
4234        // Multi-value list form: install the `List` AST directly so the
4235        // constant returns the full list (not just the last comma operand).
4236        // The body is a bare `Expression(...)` statement — implicit return
4237        // hands the value back in the caller's wantarray context, the same
4238        // way `fn arr = (1, 2, 3)` does. Wrapping in a `Return(...)`
4239        // statement instead would collapse to scalar context for
4240        // bareword-call sites like `my @a = ARR`.
4241        if matches!(value.kind, ExprKind::List(_)) {
4242            let key = self.qualify_sub_key(name);
4243            let body = vec![Statement {
4244                label: None,
4245                kind: StmtKind::Expression(value.clone()),
4246                line,
4247            }];
4248            self.subs.insert(
4249                key.clone(),
4250                Arc::new(StrykeSub {
4251                    name: key,
4252                    params: vec![],
4253                    body,
4254                    prototype: None,
4255                    closure_env: None,
4256                    fib_like: None,
4257                }),
4258            );
4259            return Ok(());
4260        }
4261        // Scalar / arrayref / hashref / etc.: eval once and freeze, the
4262        // same path single-pair `use constant` has used since day one.
4263        let val = match self.eval_expr(value) {
4264            Ok(v) => v,
4265            Err(FlowOrError::Error(e)) => return Err(e),
4266            Err(FlowOrError::Flow(_)) => {
4267                return Err(StrykeError::runtime(
4268                    "use constant: unexpected control flow in initializer",
4269                    line,
4270                ));
4271            }
4272        };
4273        self.install_constant_sub(name, &val, line)
4274    }
4275
4276    fn install_constant_sub(
4277        &mut self,
4278        name: &str,
4279        val: &StrykeValue,
4280        line: usize,
4281    ) -> StrykeResult<()> {
4282        let key = self.qualify_sub_key(name);
4283        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
4284        let body = vec![Statement {
4285            label: None,
4286            kind: StmtKind::Return(Some(ret_expr)),
4287            line,
4288        }];
4289        self.subs.insert(
4290            key.clone(),
4291            Arc::new(StrykeSub {
4292                name: key,
4293                params: vec![],
4294                body,
4295                prototype: None,
4296                closure_env: None,
4297                fib_like: None,
4298            }),
4299        );
4300        Ok(())
4301    }
4302
4303    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
4304    fn perl_value_to_const_literal_expr(&self, v: &StrykeValue, line: usize) -> StrykeResult<Expr> {
4305        if v.is_undef() {
4306            return Ok(Expr {
4307                kind: ExprKind::Undef,
4308                line,
4309            });
4310        }
4311        if let Some(n) = v.as_integer() {
4312            return Ok(Expr {
4313                kind: ExprKind::Integer(n),
4314                line,
4315            });
4316        }
4317        if let Some(f) = v.as_float() {
4318            return Ok(Expr {
4319                kind: ExprKind::Float(f),
4320                line,
4321            });
4322        }
4323        if let Some(s) = v.as_str() {
4324            return Ok(Expr {
4325                kind: ExprKind::String(s),
4326                line,
4327            });
4328        }
4329        if let Some(arr) = v.as_array_vec() {
4330            let mut elems = Vec::with_capacity(arr.len());
4331            for e in &arr {
4332                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
4333            }
4334            return Ok(Expr {
4335                kind: ExprKind::ArrayRef(elems),
4336                line,
4337            });
4338        }
4339        if let Some(h) = v.as_hash_map() {
4340            let mut pairs = Vec::with_capacity(h.len());
4341            for (k, vv) in h.iter() {
4342                pairs.push((
4343                    Expr {
4344                        kind: ExprKind::String(k.clone()),
4345                        line,
4346                    },
4347                    self.perl_value_to_const_literal_expr(vv, line)?,
4348                ));
4349            }
4350            return Ok(Expr {
4351                kind: ExprKind::HashRef(pairs),
4352                line,
4353            });
4354        }
4355        if let Some(aref) = v.as_array_ref() {
4356            let arr = aref.read();
4357            let mut elems = Vec::with_capacity(arr.len());
4358            for e in arr.iter() {
4359                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
4360            }
4361            return Ok(Expr {
4362                kind: ExprKind::ArrayRef(elems),
4363                line,
4364            });
4365        }
4366        if let Some(href) = v.as_hash_ref() {
4367            let h = href.read();
4368            let mut pairs = Vec::with_capacity(h.len());
4369            for (k, vv) in h.iter() {
4370                pairs.push((
4371                    Expr {
4372                        kind: ExprKind::String(k.clone()),
4373                        line,
4374                    },
4375                    self.perl_value_to_const_literal_expr(vv, line)?,
4376                ));
4377            }
4378            return Ok(Expr {
4379                kind: ExprKind::HashRef(pairs),
4380                line,
4381            });
4382        }
4383        Err(StrykeError::runtime(
4384            format!("use constant: unsupported value type ({v:?})"),
4385            line,
4386        ))
4387    }
4388
4389    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
4390    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> StrykeResult<()> {
4391        // Reset per-interpreter pragma flags. Each new program scan starts
4392        // clean; pragmas activate only when the program contains `use utf8;`
4393        // / `use bigint;` etc. (Globals like `BIGINT_PRAGMA` stay sticky
4394        // across runs in the same process — bigint tests that need
4395        // isolation use subprocess invocation.)
4396        self.utf8_pragma = false;
4397        for stmt in &program.statements {
4398            match &stmt.kind {
4399                StmtKind::Package { name } => {
4400                    let _ = self
4401                        .scope
4402                        .set_scalar("__PACKAGE__", StrykeValue::string(name.clone()));
4403                }
4404                StmtKind::SubDecl {
4405                    name,
4406                    params,
4407                    body,
4408                    prototype,
4409                } => {
4410                    let key = self.qualify_sub_key(name);
4411                    let mut sub = StrykeSub {
4412                        name: name.clone(),
4413                        params: params.clone(),
4414                        body: body.clone(),
4415                        closure_env: None,
4416                        prototype: prototype.clone(),
4417                        fib_like: None,
4418                    };
4419                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
4420                    self.subs.insert(key, Arc::new(sub));
4421                }
4422                StmtKind::UsePerlVersion { .. } => {}
4423                StmtKind::Use { module, imports, version } => {
4424                    self.exec_use_stmt(module, imports, version.as_deref(), stmt.line)?;
4425                }
4426                StmtKind::UseOverload { pairs } => {
4427                    self.install_use_overload_pairs(pairs);
4428                }
4429                StmtKind::FormatDecl { name, lines } => {
4430                    self.install_format_decl(name, lines, stmt.line)?;
4431                }
4432                StmtKind::No { module, imports } => {
4433                    self.exec_no_stmt(module, imports, stmt.line)?;
4434                }
4435                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
4436                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
4437                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
4438                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
4439                StmtKind::End(block) => self.end_blocks.push(block.clone()),
4440                _ => {}
4441            }
4442        }
4443        Ok(())
4444    }
4445
4446    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
4447    pub fn install_data_handle(&mut self, data: Vec<u8>) {
4448        self.input_handles.insert(
4449            "DATA".to_string(),
4450            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
4451        );
4452    }
4453
4454    /// Resolve `path` against [`Self::stryke_pwd`] when relative; absolute paths unchanged.
4455    #[inline]
4456    pub(crate) fn resolve_stryke_path(&self, path: &str) -> PathBuf {
4457        if path.is_empty() {
4458            return self.stryke_pwd.clone();
4459        }
4460        let p = Path::new(path);
4461        if p.is_absolute() {
4462            PathBuf::from(path)
4463        } else {
4464            self.stryke_pwd.join(path)
4465        }
4466    }
4467
4468    pub(crate) fn resolve_stryke_path_string(&self, path: &str) -> String {
4469        self.resolve_stryke_path(path)
4470            .to_string_lossy()
4471            .into_owned()
4472    }
4473
4474    /// `cd DIR` / `cd()` — set the interpreter working directory for relative path builtins
4475    /// (does not call OS `chdir`; use `chdir` for that). Returns `1` on success, `0` on failure
4476    /// and sets `$!` / errno. With no arguments, changes to `$HOME` / `%USERPROFILE%` when set.
4477    pub(crate) fn builtin_cd_execute(
4478        &mut self,
4479        args: &[StrykeValue],
4480        _line: usize,
4481    ) -> StrykeResult<StrykeValue> {
4482        let dest: PathBuf = if args.is_empty() {
4483            let home = std::env::var_os("HOME")
4484                .or_else(|| std::env::var_os("USERPROFILE"))
4485                .map(PathBuf::from);
4486            let Some(h) = home else {
4487                return Ok(StrykeValue::integer(0));
4488            };
4489            h
4490        } else {
4491            let raw = args[0].to_string();
4492            if raw.is_empty() {
4493                return Ok(StrykeValue::integer(0));
4494            }
4495            self.resolve_stryke_path(&raw)
4496        };
4497        match std::fs::metadata(&dest) {
4498            Ok(m) if m.is_dir() => {
4499                self.stryke_pwd = std::fs::canonicalize(&dest).unwrap_or(dest);
4500                Ok(StrykeValue::integer(1))
4501            }
4502            Ok(_) => Ok(StrykeValue::integer(0)),
4503            Err(e) => {
4504                self.apply_io_error_to_errno(&e);
4505                Ok(StrykeValue::integer(0))
4506            }
4507        }
4508    }
4509
4510    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
4511    ///
4512    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
4513    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
4514    /// [`piped_shell_command`]).
4515    pub(crate) fn open_builtin_execute(
4516        &mut self,
4517        handle_name: String,
4518        mode_s: String,
4519        file_opt: Option<String>,
4520        line: usize,
4521    ) -> StrykeResult<StrykeValue> {
4522        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
4523        // - leading `|`  → pipe to command (write to child's stdin)
4524        // - trailing `|` → pipe from command (read child's stdout)
4525        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
4526        let (actual_mode, path) = if let Some(f) = file_opt {
4527            (mode_s, f)
4528        } else {
4529            let trimmed = mode_s.trim();
4530            if let Some(rest) = trimmed.strip_prefix('|') {
4531                ("|-".to_string(), rest.trim_start().to_string())
4532            } else if trimmed.ends_with('|') {
4533                let mut cmd = trimmed.to_string();
4534                cmd.pop(); // trailing `|` that selects pipe-from-command
4535                ("-|".to_string(), cmd.trim_end().to_string())
4536            } else if let Some(rest) = trimmed.strip_prefix(">>") {
4537                (">>".to_string(), rest.trim().to_string())
4538            } else if let Some(rest) = trimmed.strip_prefix('>') {
4539                (">".to_string(), rest.trim().to_string())
4540            } else if let Some(rest) = trimmed.strip_prefix('<') {
4541                ("<".to_string(), rest.trim().to_string())
4542            } else {
4543                ("<".to_string(), trimmed.to_string())
4544            }
4545        };
4546        let handle_return = handle_name.clone();
4547        let file_path = match actual_mode.as_str() {
4548            "<" | ">" | ">>" => self.resolve_stryke_path_string(&path),
4549            _ => path.clone(),
4550        };
4551        match actual_mode.as_str() {
4552            "-|" => {
4553                let mut cmd = piped_shell_command(&path);
4554                cmd.stdout(Stdio::piped());
4555                let mut child = cmd.spawn().map_err(|e| {
4556                    self.apply_io_error_to_errno(&e);
4557                    StrykeError::runtime(format!("Can't open pipe from command: {}", e), line)
4558                })?;
4559                let stdout = child
4560                    .stdout
4561                    .take()
4562                    .ok_or_else(|| StrykeError::runtime("pipe: child has no stdout", line))?;
4563                self.input_handles
4564                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
4565                self.pipe_children.insert(handle_name, child);
4566            }
4567            "|-" => {
4568                let mut cmd = piped_shell_command(&path);
4569                cmd.stdin(Stdio::piped());
4570                let mut child = cmd.spawn().map_err(|e| {
4571                    self.apply_io_error_to_errno(&e);
4572                    StrykeError::runtime(format!("Can't open pipe to command: {}", e), line)
4573                })?;
4574                let stdin = child
4575                    .stdin
4576                    .take()
4577                    .ok_or_else(|| StrykeError::runtime("pipe: child has no stdin", line))?;
4578                self.output_handles
4579                    .insert(handle_name.clone(), Box::new(stdin));
4580                self.pipe_children.insert(handle_name, child);
4581            }
4582            "<" => {
4583                let file = match std::fs::File::open(&file_path) {
4584                    Ok(f) => f,
4585                    Err(e) => {
4586                        self.apply_io_error_to_errno(&e);
4587                        return Ok(StrykeValue::integer(0));
4588                    }
4589                };
4590                let shared = Arc::new(Mutex::new(file));
4591                self.io_file_slots
4592                    .insert(handle_name.clone(), Arc::clone(&shared));
4593                self.input_handles.insert(
4594                    handle_name.clone(),
4595                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
4596                );
4597            }
4598            ">" => {
4599                let file = match std::fs::File::create(&file_path) {
4600                    Ok(f) => f,
4601                    Err(e) => {
4602                        self.apply_io_error_to_errno(&e);
4603                        return Ok(StrykeValue::integer(0));
4604                    }
4605                };
4606                let shared = Arc::new(Mutex::new(file));
4607                self.io_file_slots
4608                    .insert(handle_name.clone(), Arc::clone(&shared));
4609                self.output_handles.insert(
4610                    handle_name.clone(),
4611                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4612                );
4613            }
4614            ">>" => {
4615                let file = match std::fs::OpenOptions::new()
4616                    .append(true)
4617                    .create(true)
4618                    .open(&file_path)
4619                {
4620                    Ok(f) => f,
4621                    Err(e) => {
4622                        self.apply_io_error_to_errno(&e);
4623                        return Ok(StrykeValue::integer(0));
4624                    }
4625                };
4626                let shared = Arc::new(Mutex::new(file));
4627                self.io_file_slots
4628                    .insert(handle_name.clone(), Arc::clone(&shared));
4629                self.output_handles.insert(
4630                    handle_name.clone(),
4631                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4632                );
4633            }
4634            _ => {
4635                return Err(StrykeError::runtime(
4636                    format!("Unknown open mode '{}'", actual_mode),
4637                    line,
4638                ));
4639            }
4640        }
4641        Ok(StrykeValue::io_handle(handle_return))
4642    }
4643
4644    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
4645    /// matches the previous key under [`StrykeValue::str_eq`]. Returns a list of arrayrefs
4646    /// (same outer shape as `chunked`).
4647    pub(crate) fn eval_chunk_by_builtin(
4648        &mut self,
4649        key_spec: &Expr,
4650        list_expr: &Expr,
4651        ctx: WantarrayCtx,
4652        line: usize,
4653    ) -> ExecResult {
4654        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
4655        let chunks = match &key_spec.kind {
4656            ExprKind::CodeRef { .. } => {
4657                let cr = self.eval_expr(key_spec)?;
4658                let Some(sub) = cr.as_code_ref() else {
4659                    return Err(StrykeError::runtime(
4660                        "group_by/chunk_by: first argument must be { BLOCK }",
4661                        line,
4662                    )
4663                    .into());
4664                };
4665                let sub = sub.clone();
4666                let mut chunks: Vec<StrykeValue> = Vec::new();
4667                let mut run: Vec<StrykeValue> = Vec::new();
4668                let mut prev_key: Option<StrykeValue> = None;
4669                for item in list {
4670                    self.scope.set_topic(item.clone());
4671                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
4672                        Ok(k) => k,
4673                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
4674                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
4675                        Err(_) => StrykeValue::UNDEF,
4676                    };
4677                    match &prev_key {
4678                        None => {
4679                            run.push(item);
4680                            prev_key = Some(key);
4681                        }
4682                        Some(pk) => {
4683                            if key.str_eq(pk) {
4684                                run.push(item);
4685                            } else {
4686                                chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
4687                                    std::mem::take(&mut run),
4688                                ))));
4689                                run.push(item);
4690                                prev_key = Some(key);
4691                            }
4692                        }
4693                    }
4694                }
4695                if !run.is_empty() {
4696                    chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
4697                }
4698                chunks
4699            }
4700            _ => {
4701                let mut chunks: Vec<StrykeValue> = Vec::new();
4702                let mut run: Vec<StrykeValue> = Vec::new();
4703                let mut prev_key: Option<StrykeValue> = None;
4704                for item in list {
4705                    self.scope.set_topic(item.clone());
4706                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
4707                    match &prev_key {
4708                        None => {
4709                            run.push(item);
4710                            prev_key = Some(key);
4711                        }
4712                        Some(pk) => {
4713                            if key.str_eq(pk) {
4714                                run.push(item);
4715                            } else {
4716                                chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
4717                                    std::mem::take(&mut run),
4718                                ))));
4719                                run.push(item);
4720                                prev_key = Some(key);
4721                            }
4722                        }
4723                    }
4724                }
4725                if !run.is_empty() {
4726                    chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
4727                }
4728                chunks
4729            }
4730        };
4731        Ok(match ctx {
4732            WantarrayCtx::List => StrykeValue::array(chunks),
4733            WantarrayCtx::Scalar => StrykeValue::integer(chunks.len() as i64),
4734            WantarrayCtx::Void => StrykeValue::UNDEF,
4735        })
4736    }
4737
4738    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
4739    pub(crate) fn list_higher_order_block_builtin(
4740        &mut self,
4741        name: &str,
4742        args: &[StrykeValue],
4743        line: usize,
4744    ) -> StrykeResult<StrykeValue> {
4745        match self.list_higher_order_block_builtin_exec(name, args, line) {
4746            Ok(v) => Ok(v),
4747            Err(FlowOrError::Error(e)) => Err(e),
4748            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
4749            Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
4750                format!("{name}: unsupported control flow in block"),
4751                line,
4752            )),
4753        }
4754    }
4755
4756    fn list_higher_order_block_builtin_exec(
4757        &mut self,
4758        name: &str,
4759        args: &[StrykeValue],
4760        line: usize,
4761    ) -> ExecResult {
4762        if args.is_empty() {
4763            return Err(
4764                StrykeError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
4765            );
4766        }
4767        let Some(sub) = args[0].as_code_ref() else {
4768            return Err(StrykeError::runtime(
4769                format!("{name}: first argument must be {{ BLOCK }}"),
4770                line,
4771            )
4772            .into());
4773        };
4774        let sub = sub.clone();
4775        let items: Vec<StrykeValue> = args[1..].to_vec();
4776        if matches!(name, "tap" | "peek") && items.len() == 1 {
4777            if let Some(p) = items[0].as_pipeline() {
4778                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
4779                return Ok(StrykeValue::pipeline(Arc::clone(&p)));
4780            }
4781            let v = &items[0];
4782            if v.is_iterator() || v.as_array_vec().is_some() {
4783                let source = crate::map_stream::into_pull_iter(v.clone());
4784                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
4785                return Ok(StrykeValue::iterator(Arc::new(
4786                    crate::map_stream::TapIterator::new(
4787                        source,
4788                        sub,
4789                        self.subs.clone(),
4790                        capture,
4791                        atomic_arrays,
4792                        atomic_hashes,
4793                    ),
4794                )));
4795            }
4796        }
4797        // Streaming optimization disabled for these functions because the pre-captured
4798        // coderef from args[0] has its closure_env populated at parse time, which causes
4799        // $_ to get stale values on subsequent calls. These functions work correctly in
4800        // the non-streaming eager path below.
4801        let wa = self.wantarray_kind;
4802        match name {
4803            "take_while" => {
4804                let mut out = Vec::new();
4805                for item in items {
4806                    // `call_sub` binds the item to @_/positional params (so
4807                    // stryke lambdas work) and to `$_` via `set_closure_args`
4808                    // (so `_`-using blocks work). Replaces the old
4809                    // `exec_block(&sub.body)` path which only set the topic.
4810                    self.scope.set_topic(item.clone());
4811                    let pred =
4812                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4813                    if !pred.is_true() {
4814                        break;
4815                    }
4816                    out.push(item);
4817                }
4818                Ok(match wa {
4819                    WantarrayCtx::List => StrykeValue::array(out),
4820                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4821                    WantarrayCtx::Void => StrykeValue::UNDEF,
4822                })
4823            }
4824            "drop_while" | "skip_while" => {
4825                let mut i = 0usize;
4826                while i < items.len() {
4827                    let it = items[i].clone();
4828                    self.scope.set_topic(it.clone());
4829                    let pred = self.call_sub(&sub, vec![it], WantarrayCtx::Scalar, line)?;
4830                    if !pred.is_true() {
4831                        break;
4832                    }
4833                    i += 1;
4834                }
4835                let rest = items[i..].to_vec();
4836                Ok(match wa {
4837                    WantarrayCtx::List => StrykeValue::array(rest),
4838                    WantarrayCtx::Scalar => StrykeValue::integer(rest.len() as i64),
4839                    WantarrayCtx::Void => StrykeValue::UNDEF,
4840                })
4841            }
4842            "reject" | "grepv" => {
4843                let mut out = Vec::new();
4844                for item in items {
4845                    self.scope.set_topic(item.clone());
4846                    let pred =
4847                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4848                    if !pred.is_true() {
4849                        out.push(item);
4850                    }
4851                }
4852                Ok(match wa {
4853                    WantarrayCtx::List => StrykeValue::array(out),
4854                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4855                    WantarrayCtx::Void => StrykeValue::UNDEF,
4856                })
4857            }
4858            "tap" | "peek" => {
4859                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
4860                Ok(match wa {
4861                    WantarrayCtx::List => StrykeValue::array(items),
4862                    WantarrayCtx::Scalar => StrykeValue::integer(items.len() as i64),
4863                    WantarrayCtx::Void => StrykeValue::UNDEF,
4864                })
4865            }
4866            "partition" => {
4867                let mut yes = Vec::new();
4868                let mut no = Vec::new();
4869                for item in items {
4870                    self.scope.set_topic(item.clone());
4871                    let pred =
4872                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4873                    if pred.is_true() {
4874                        yes.push(item);
4875                    } else {
4876                        no.push(item);
4877                    }
4878                }
4879                let yes_ref = StrykeValue::array_ref(Arc::new(RwLock::new(yes)));
4880                let no_ref = StrykeValue::array_ref(Arc::new(RwLock::new(no)));
4881                Ok(match wa {
4882                    WantarrayCtx::List => StrykeValue::array(vec![yes_ref, no_ref]),
4883                    WantarrayCtx::Scalar => StrykeValue::integer(2),
4884                    WantarrayCtx::Void => StrykeValue::UNDEF,
4885                })
4886            }
4887            "min_by" => {
4888                let mut best: Option<(StrykeValue, StrykeValue)> = None;
4889                for item in items {
4890                    self.scope.set_topic(item.clone());
4891                    let key =
4892                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4893                    best = Some(match best {
4894                        None => (item, key),
4895                        Some((bv, bk)) => {
4896                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
4897                                (item, key)
4898                            } else {
4899                                (bv, bk)
4900                            }
4901                        }
4902                    });
4903                }
4904                Ok(best.map(|(v, _)| v).unwrap_or(StrykeValue::UNDEF))
4905            }
4906            "max_by" => {
4907                let mut best: Option<(StrykeValue, StrykeValue)> = None;
4908                for item in items {
4909                    self.scope.set_topic(item.clone());
4910                    let key =
4911                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4912                    best = Some(match best {
4913                        None => (item, key),
4914                        Some((bv, bk)) => {
4915                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
4916                                (item, key)
4917                            } else {
4918                                (bv, bk)
4919                            }
4920                        }
4921                    });
4922                }
4923                Ok(best.map(|(v, _)| v).unwrap_or(StrykeValue::UNDEF))
4924            }
4925            "zip_with" => {
4926                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
4927                // Flatten items, then treat each array ref/binding as a separate list.
4928                let flat: Vec<StrykeValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
4929                let refs: Vec<Vec<StrykeValue>> = flat
4930                    .iter()
4931                    .map(|el| {
4932                        if let Some(ar) = el.as_array_ref() {
4933                            ar.read().clone()
4934                        } else if let Some(name) = el.as_array_binding_name() {
4935                            self.scope.get_array(&name)
4936                        } else {
4937                            vec![el.clone()]
4938                        }
4939                    })
4940                    .collect();
4941                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
4942                let mut out = Vec::with_capacity(max_len);
4943                for i in 0..max_len {
4944                    let pair: Vec<StrykeValue> = refs
4945                        .iter()
4946                        .map(|l| l.get(i).cloned().unwrap_or(StrykeValue::UNDEF))
4947                        .collect();
4948                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
4949                    out.push(result);
4950                }
4951                Ok(match wa {
4952                    WantarrayCtx::List => StrykeValue::array(out),
4953                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4954                    WantarrayCtx::Void => StrykeValue::UNDEF,
4955                })
4956            }
4957            "count_by" => {
4958                let mut counts = indexmap::IndexMap::new();
4959                for item in items {
4960                    self.scope.set_topic(item.clone());
4961                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4962                    let k = key.to_string();
4963                    let entry = counts.entry(k).or_insert(StrykeValue::integer(0));
4964                    *entry = StrykeValue::integer(entry.to_int() + 1);
4965                }
4966                Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(counts))))
4967            }
4968            _ => Err(StrykeError::runtime(
4969                format!("internal: unknown list block builtin `{name}`"),
4970                line,
4971            )
4972            .into()),
4973        }
4974    }
4975
4976    /// `rmdir LIST` — remove empty directories; returns count removed.
4977    pub(crate) fn builtin_rmdir_execute(
4978        &mut self,
4979        args: &[StrykeValue],
4980        _line: usize,
4981    ) -> StrykeResult<StrykeValue> {
4982        let mut count = 0i64;
4983        for a in args {
4984            let p = a.to_string();
4985            if p.is_empty() {
4986                continue;
4987            }
4988            let p = self.resolve_stryke_path_string(&p);
4989            if std::fs::remove_dir(&p).is_ok() {
4990                count += 1;
4991            }
4992        }
4993        Ok(StrykeValue::integer(count))
4994    }
4995
4996    /// `touch FILE, ...` — create if absent, update timestamps to now.
4997    pub(crate) fn builtin_touch_execute(
4998        &mut self,
4999        args: &[StrykeValue],
5000        _line: usize,
5001    ) -> StrykeResult<StrykeValue> {
5002        let paths: Vec<String> = args
5003            .iter()
5004            .map(|v| self.resolve_stryke_path_string(&v.to_string()))
5005            .collect();
5006        Ok(StrykeValue::integer(crate::perl_fs::touch_paths(&paths)))
5007    }
5008
5009    /// `utime ATIME, MTIME, LIST`
5010    pub(crate) fn builtin_utime_execute(
5011        &mut self,
5012        args: &[StrykeValue],
5013        line: usize,
5014    ) -> StrykeResult<StrykeValue> {
5015        if args.len() < 3 {
5016            return Err(StrykeError::runtime(
5017                "utime requires at least three arguments (atime, mtime, files...)",
5018                line,
5019            ));
5020        }
5021        let at = args[0].to_int();
5022        let mt = args[1].to_int();
5023        let paths: Vec<String> = args
5024            .iter()
5025            .skip(2)
5026            .map(|v| self.resolve_stryke_path_string(&v.to_string()))
5027            .collect();
5028        let n = crate::perl_fs::utime_paths(at, mt, &paths);
5029        #[cfg(not(unix))]
5030        if !paths.is_empty() && n == 0 {
5031            return Err(StrykeError::runtime(
5032                "utime is not supported on this platform",
5033                line,
5034            ));
5035        }
5036        Ok(StrykeValue::integer(n))
5037    }
5038
5039    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
5040    pub(crate) fn builtin_umask_execute(
5041        &mut self,
5042        args: &[StrykeValue],
5043        line: usize,
5044    ) -> StrykeResult<StrykeValue> {
5045        #[cfg(unix)]
5046        {
5047            let _ = line;
5048            if args.is_empty() {
5049                let cur = unsafe { libc::umask(0) };
5050                unsafe { libc::umask(cur) };
5051                return Ok(StrykeValue::integer(cur as i64));
5052            }
5053            let new_m = args[0].to_int() as libc::mode_t;
5054            let old = unsafe { libc::umask(new_m) };
5055            Ok(StrykeValue::integer(old as i64))
5056        }
5057        #[cfg(not(unix))]
5058        {
5059            let _ = args;
5060            Err(StrykeError::runtime(
5061                "umask is not supported on this platform",
5062                line,
5063            ))
5064        }
5065    }
5066
5067    /// `getcwd` — current directory or undef on failure.
5068    pub(crate) fn builtin_getcwd_execute(
5069        &mut self,
5070        args: &[StrykeValue],
5071        line: usize,
5072    ) -> StrykeResult<StrykeValue> {
5073        if !args.is_empty() {
5074            return Err(StrykeError::runtime("getcwd takes no arguments", line));
5075        }
5076        match std::env::current_dir() {
5077            Ok(p) => Ok(StrykeValue::string(p.to_string_lossy().into_owned())),
5078            Err(e) => {
5079                self.apply_io_error_to_errno(&e);
5080                Ok(StrykeValue::UNDEF)
5081            }
5082        }
5083    }
5084
5085    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
5086    pub(crate) fn builtin_realpath_execute(
5087        &mut self,
5088        args: &[StrykeValue],
5089        line: usize,
5090    ) -> StrykeResult<StrykeValue> {
5091        let path = args
5092            .first()
5093            .ok_or_else(|| StrykeError::runtime("realpath: need path", line))?
5094            .to_string();
5095        if path.is_empty() {
5096            return Err(StrykeError::runtime("realpath: need path", line));
5097        }
5098        let path = self.resolve_stryke_path_string(&path);
5099        match crate::perl_fs::realpath_resolved(&path) {
5100            Ok(s) => Ok(StrykeValue::string(s)),
5101            Err(e) => {
5102                self.apply_io_error_to_errno(&e);
5103                Ok(StrykeValue::UNDEF)
5104            }
5105        }
5106    }
5107
5108    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
5109    pub(crate) fn builtin_pipe_execute(
5110        &mut self,
5111        args: &[StrykeValue],
5112        line: usize,
5113    ) -> StrykeResult<StrykeValue> {
5114        if args.len() != 2 {
5115            return Err(StrykeError::runtime(
5116                "pipe requires exactly two arguments",
5117                line,
5118            ));
5119        }
5120        #[cfg(unix)]
5121        {
5122            use std::fs::File;
5123            use std::os::unix::io::FromRawFd;
5124
5125            let read_name = args[0].to_string();
5126            let write_name = args[1].to_string();
5127            if read_name.is_empty() || write_name.is_empty() {
5128                return Err(StrykeError::runtime("pipe: invalid handle name", line));
5129            }
5130            let mut fds = [0i32; 2];
5131            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
5132                let e = std::io::Error::last_os_error();
5133                self.apply_io_error_to_errno(&e);
5134                return Ok(StrykeValue::integer(0));
5135            }
5136            let read_file = unsafe { File::from_raw_fd(fds[0]) };
5137            let write_file = unsafe { File::from_raw_fd(fds[1]) };
5138
5139            let read_shared = Arc::new(Mutex::new(read_file));
5140            let write_shared = Arc::new(Mutex::new(write_file));
5141
5142            self.close_builtin_execute(read_name.clone()).ok();
5143            self.close_builtin_execute(write_name.clone()).ok();
5144
5145            self.io_file_slots
5146                .insert(read_name.clone(), Arc::clone(&read_shared));
5147            self.input_handles.insert(
5148                read_name,
5149                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
5150            );
5151
5152            self.io_file_slots
5153                .insert(write_name.clone(), Arc::clone(&write_shared));
5154            self.output_handles
5155                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
5156
5157            Ok(StrykeValue::integer(1))
5158        }
5159        #[cfg(not(unix))]
5160        {
5161            let _ = args;
5162            Err(StrykeError::runtime(
5163                "pipe is not supported on this platform",
5164                line,
5165            ))
5166        }
5167    }
5168
5169    pub(crate) fn close_builtin_execute(&mut self, name: String) -> StrykeResult<StrykeValue> {
5170        self.output_handles.remove(&name);
5171        self.input_handles.remove(&name);
5172        self.io_file_slots.remove(&name);
5173        if let Some(mut child) = self.pipe_children.remove(&name) {
5174            if let Ok(st) = child.wait() {
5175                self.record_child_exit_status(st);
5176            }
5177        }
5178        Ok(StrykeValue::integer(1))
5179    }
5180
5181    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
5182        self.input_handles.contains_key(name)
5183    }
5184
5185    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
5186    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
5187    /// readline-level EOF tracking exists.
5188    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
5189        self.line_mode_eof_pending
5190    }
5191
5192    /// `eof` / `eof()` / `eof FH` — shared by [`crate::vm::VM`] and
5193    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
5194    /// not [`ExprKind::Eof`]).
5195    pub(crate) fn eof_builtin_execute(
5196        &mut self,
5197        args: &[StrykeValue],
5198        line: usize,
5199    ) -> StrykeResult<StrykeValue> {
5200        match args.len() {
5201            0 => Ok(StrykeValue::integer(if self.eof_without_arg_is_true() {
5202                1
5203            } else {
5204                0
5205            })),
5206            1 => {
5207                let name = args[0].to_string();
5208                // `eof FH` is true when the handle is closed *or* the next
5209                // read would return no data. Peek the BufReader with
5210                // `fill_buf` so we don't consume the byte.
5211                use std::io::BufRead;
5212                let at_eof = match self.input_handles.get_mut(&name) {
5213                    None => true,
5214                    Some(reader) => match reader.fill_buf() {
5215                        Ok(buf) => buf.is_empty(),
5216                        Err(_) => true,
5217                    },
5218                };
5219                Ok(StrykeValue::integer(if at_eof { 1 } else { 0 }))
5220            }
5221            _ => Err(StrykeError::runtime("eof: too many arguments", line)),
5222        }
5223    }
5224
5225    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
5226    /// `0`, stringifies to `""`) for `""`.
5227    pub(crate) fn study_return_value(s: &str) -> StrykeValue {
5228        if s.is_empty() {
5229            StrykeValue::string(String::new())
5230        } else {
5231            StrykeValue::integer(1)
5232        }
5233    }
5234
5235    pub(crate) fn readline_builtin_execute(
5236        &mut self,
5237        handle: Option<&str>,
5238    ) -> StrykeResult<StrykeValue> {
5239        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
5240        if handle.is_none() {
5241            let argv = self.scope.get_array("ARGV");
5242            if !argv.is_empty() {
5243                loop {
5244                    if self.diamond_reader.is_none() {
5245                        while self.diamond_next_idx < argv.len() {
5246                            let path = self.resolve_stryke_path_string(
5247                                &argv[self.diamond_next_idx].to_string(),
5248                            );
5249                            self.diamond_next_idx += 1;
5250                            match File::open(&path) {
5251                                Ok(f) => {
5252                                    self.argv_current_file = path;
5253                                    self.diamond_reader = Some(BufReader::new(f));
5254                                    break;
5255                                }
5256                                Err(e) => {
5257                                    self.apply_io_error_to_errno(&e);
5258                                }
5259                            }
5260                        }
5261                        if self.diamond_reader.is_none() {
5262                            return Ok(StrykeValue::UNDEF);
5263                        }
5264                    }
5265                    let mut line_str = String::new();
5266                    let read_result: Result<usize, io::Error> =
5267                        if let Some(reader) = self.diamond_reader.as_mut() {
5268                            if self.open_pragma_utf8 {
5269                                let mut buf = Vec::new();
5270                                reader.read_until(b'\n', &mut buf).inspect(|n| {
5271                                    if *n > 0 {
5272                                        line_str = String::from_utf8_lossy(&buf).into_owned();
5273                                    }
5274                                })
5275                            } else {
5276                                let mut buf = Vec::new();
5277                                match reader.read_until(b'\n', &mut buf) {
5278                                    Ok(n) => {
5279                                        if n > 0 {
5280                                            line_str =
5281                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
5282                                                &buf,
5283                                            );
5284                                        }
5285                                        Ok(n)
5286                                    }
5287                                    Err(e) => Err(e),
5288                                }
5289                            }
5290                        } else {
5291                            unreachable!()
5292                        };
5293                    match read_result {
5294                        Ok(0) => {
5295                            self.diamond_reader = None;
5296                            continue;
5297                        }
5298                        Ok(_) => {
5299                            self.bump_line_for_handle(&self.argv_current_file.clone());
5300                            return Ok(StrykeValue::string(line_str));
5301                        }
5302                        Err(e) => {
5303                            self.apply_io_error_to_errno(&e);
5304                            self.diamond_reader = None;
5305                            continue;
5306                        }
5307                    }
5308                }
5309            } else {
5310                self.argv_current_file.clear();
5311            }
5312        }
5313
5314        let handle_name = handle.unwrap_or("STDIN");
5315        let mut line_str = String::new();
5316        if handle_name == "STDIN" {
5317            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
5318                self.last_stdin_die_bracket = if handle.is_none() {
5319                    "<>".to_string()
5320                } else {
5321                    "<STDIN>".to_string()
5322                };
5323                self.bump_line_for_handle("STDIN");
5324                return Ok(StrykeValue::string(queued));
5325            }
5326            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
5327                let mut buf = Vec::new();
5328                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
5329                    if *n > 0 {
5330                        line_str = String::from_utf8_lossy(&buf).into_owned();
5331                    }
5332                })
5333            } else {
5334                let mut buf = Vec::new();
5335                let mut lock = io::stdin().lock();
5336                match lock.read_until(b'\n', &mut buf) {
5337                    Ok(n) => {
5338                        if n > 0 {
5339                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
5340                        }
5341                        Ok(n)
5342                    }
5343                    Err(e) => Err(e),
5344                }
5345            };
5346            match r {
5347                Ok(0) => Ok(StrykeValue::UNDEF),
5348                Ok(_) => {
5349                    self.last_stdin_die_bracket = if handle.is_none() {
5350                        "<>".to_string()
5351                    } else {
5352                        "<STDIN>".to_string()
5353                    };
5354                    self.bump_line_for_handle("STDIN");
5355                    Ok(StrykeValue::string(line_str))
5356                }
5357                Err(e) => {
5358                    self.apply_io_error_to_errno(&e);
5359                    Ok(StrykeValue::UNDEF)
5360                }
5361            }
5362        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
5363            // Check $/ for slurp mode (None/undef = read entire file)
5364            let slurp_mode = self.irs.is_none();
5365            let r: Result<usize, io::Error> = if slurp_mode {
5366                // Slurp mode: read entire remaining content
5367                let mut buf = Vec::new();
5368                match reader.read_to_end(&mut buf) {
5369                    Ok(n) => {
5370                        if n > 0 {
5371                            line_str = if self.open_pragma_utf8 {
5372                                String::from_utf8_lossy(&buf).into_owned()
5373                            } else {
5374                                crate::perl_decode::decode_utf8_or_latin1_read_until(&buf)
5375                            };
5376                        }
5377                        Ok(n)
5378                    }
5379                    Err(e) => Err(e),
5380                }
5381            } else if self.open_pragma_utf8 {
5382                let mut buf = Vec::new();
5383                reader.read_until(b'\n', &mut buf).inspect(|n| {
5384                    if *n > 0 {
5385                        line_str = String::from_utf8_lossy(&buf).into_owned();
5386                    }
5387                })
5388            } else {
5389                let mut buf = Vec::new();
5390                match reader.read_until(b'\n', &mut buf) {
5391                    Ok(n) => {
5392                        if n > 0 {
5393                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
5394                        }
5395                        Ok(n)
5396                    }
5397                    Err(e) => Err(e),
5398                }
5399            };
5400            match r {
5401                Ok(0) => Ok(StrykeValue::UNDEF),
5402                Ok(_) => {
5403                    self.bump_line_for_handle(handle_name);
5404                    Ok(StrykeValue::string(line_str))
5405                }
5406                Err(e) => {
5407                    self.apply_io_error_to_errno(&e);
5408                    Ok(StrykeValue::UNDEF)
5409                }
5410            }
5411        } else {
5412            Ok(StrykeValue::UNDEF)
5413        }
5414    }
5415
5416    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
5417    pub(crate) fn readline_builtin_execute_list(
5418        &mut self,
5419        handle: Option<&str>,
5420    ) -> StrykeResult<StrykeValue> {
5421        let mut lines = Vec::new();
5422        loop {
5423            let v = self.readline_builtin_execute(handle)?;
5424            if v.is_undef() {
5425                break;
5426            }
5427            lines.push(v);
5428        }
5429        Ok(StrykeValue::array(lines))
5430    }
5431
5432    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> StrykeValue {
5433        let path = self.resolve_stryke_path_string(path);
5434        match std::fs::read_dir(&path) {
5435            Ok(rd) => {
5436                // Perl-compat: `readdir` returns `.` and `..` as the first
5437                // two entries (mirrors POSIX `readdir(3)`). Rust's
5438                // `std::fs::read_dir` strips them, so prepend them here so
5439                // existing Perl idioms like
5440                //     while (my $e = readdir($dh)) { next if $e eq '.' || $e eq '..'; … }
5441                // keep working unchanged.
5442                let mut entries: Vec<String> = vec![".".to_string(), "..".to_string()];
5443                entries.extend(rd.filter_map(|e| {
5444                    e.ok().map(|e| e.file_name().to_string_lossy().into_owned())
5445                }));
5446                self.dir_handles
5447                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
5448                StrykeValue::integer(1)
5449            }
5450            Err(e) => {
5451                self.apply_io_error_to_errno(&e);
5452                StrykeValue::integer(0)
5453            }
5454        }
5455    }
5456
5457    pub(crate) fn readdir_handle(&mut self, handle: &str) -> StrykeValue {
5458        if let Some(dh) = self.dir_handles.get_mut(handle) {
5459            if dh.pos < dh.entries.len() {
5460                let s = dh.entries[dh.pos].clone();
5461                dh.pos += 1;
5462                StrykeValue::string(s)
5463            } else {
5464                StrykeValue::UNDEF
5465            }
5466        } else {
5467            StrykeValue::UNDEF
5468        }
5469    }
5470
5471    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
5472    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> StrykeValue {
5473        if let Some(dh) = self.dir_handles.get_mut(handle) {
5474            let rest: Vec<StrykeValue> = dh.entries[dh.pos..]
5475                .iter()
5476                .cloned()
5477                .map(StrykeValue::string)
5478                .collect();
5479            dh.pos = dh.entries.len();
5480            StrykeValue::array(rest)
5481        } else {
5482            StrykeValue::array(Vec::new())
5483        }
5484    }
5485
5486    pub(crate) fn closedir_handle(&mut self, handle: &str) -> StrykeValue {
5487        StrykeValue::integer(if self.dir_handles.remove(handle).is_some() {
5488            1
5489        } else {
5490            0
5491        })
5492    }
5493
5494    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> StrykeValue {
5495        if let Some(dh) = self.dir_handles.get_mut(handle) {
5496            dh.pos = 0;
5497            StrykeValue::integer(1)
5498        } else {
5499            StrykeValue::integer(0)
5500        }
5501    }
5502
5503    pub(crate) fn telldir_handle(&mut self, handle: &str) -> StrykeValue {
5504        self.dir_handles
5505            .get(handle)
5506            .map(|dh| StrykeValue::integer(dh.pos as i64))
5507            .unwrap_or(StrykeValue::UNDEF)
5508    }
5509
5510    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> StrykeValue {
5511        if let Some(dh) = self.dir_handles.get_mut(handle) {
5512            dh.pos = pos.min(dh.entries.len());
5513            StrykeValue::integer(1)
5514        } else {
5515            StrykeValue::integer(0)
5516        }
5517    }
5518
5519    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
5520    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
5521    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
5522    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
5523    #[inline]
5524    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
5525        crate::special_vars::is_regex_match_scalar_name(name)
5526    }
5527
5528    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
5529    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
5530    /// `apply_regex_captures` instead of short-circuiting.
5531    #[inline]
5532    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
5533        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
5534            self.regex_capture_scope_fresh = false;
5535        }
5536    }
5537
5538    pub(crate) fn apply_regex_captures(
5539        &mut self,
5540        haystack: &str,
5541        offset: usize,
5542        re: &PerlCompiledRegex,
5543        caps: &PerlCaptures<'_>,
5544        capture_all: CaptureAllMode,
5545    ) -> Result<(), FlowOrError> {
5546        let m0 = caps.get(0).expect("regex capture 0");
5547        let s0 = offset + m0.start;
5548        let e0 = offset + m0.end;
5549        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
5550        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
5551        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
5552        let mut last_paren = String::new();
5553        for i in 1..caps.len() {
5554            if let Some(m) = caps.get(i) {
5555                last_paren = m.text.to_string();
5556            }
5557        }
5558        self.last_paren_match = last_paren;
5559        self.last_subpattern_name = String::new();
5560        for n in re.capture_names().flatten() {
5561            if caps.name(n).is_some() {
5562                self.last_subpattern_name = n.to_string();
5563            }
5564        }
5565        self.scope
5566            .set_scalar("&", StrykeValue::string(self.last_match.clone()))?;
5567        self.scope
5568            .set_scalar("`", StrykeValue::string(self.prematch.clone()))?;
5569        self.scope
5570            .set_scalar("'", StrykeValue::string(self.postmatch.clone()))?;
5571        self.scope
5572            .set_scalar("+", StrykeValue::string(self.last_paren_match.clone()))?;
5573        for i in 1..caps.len() {
5574            if let Some(m) = caps.get(i) {
5575                self.scope
5576                    .set_scalar(&i.to_string(), StrykeValue::string(m.text.to_string()))?;
5577            }
5578        }
5579        let mut start_arr = vec![StrykeValue::integer(s0 as i64)];
5580        let mut end_arr = vec![StrykeValue::integer(e0 as i64)];
5581        for i in 1..caps.len() {
5582            if let Some(m) = caps.get(i) {
5583                start_arr.push(StrykeValue::integer((offset + m.start) as i64));
5584                end_arr.push(StrykeValue::integer((offset + m.end) as i64));
5585            } else {
5586                start_arr.push(StrykeValue::integer(-1));
5587                end_arr.push(StrykeValue::integer(-1));
5588            }
5589        }
5590        self.scope.set_array("-", start_arr)?;
5591        self.scope.set_array("+", end_arr)?;
5592        let mut named = IndexMap::new();
5593        for name in re.capture_names().flatten() {
5594            if let Some(m) = caps.name(name) {
5595                named.insert(name.to_string(), StrykeValue::string(m.text.to_string()));
5596            }
5597        }
5598        self.scope.set_hash("+", named.clone())?;
5599        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
5600        let mut named_minus = IndexMap::new();
5601        for (name, val) in &named {
5602            named_minus.insert(
5603                name.clone(),
5604                StrykeValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
5605            );
5606        }
5607        self.scope.set_hash("-", named_minus)?;
5608        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
5609        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
5610        match capture_all {
5611            CaptureAllMode::Empty => {
5612                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5613            }
5614            CaptureAllMode::Append => {
5615                let mut rows = self.scope.get_array("^CAPTURE_ALL");
5616                rows.push(StrykeValue::array(cap_flat));
5617                self.scope.set_array("^CAPTURE_ALL", rows)?;
5618            }
5619            CaptureAllMode::Skip => {}
5620        }
5621        Ok(())
5622    }
5623
5624    pub(crate) fn clear_flip_flop_state(&mut self) {
5625        self.flip_flop_active.clear();
5626        self.flip_flop_exclusive_left_line.clear();
5627        self.flip_flop_sequence.clear();
5628        self.flip_flop_last_dot.clear();
5629        self.flip_flop_tree.clear();
5630    }
5631
5632    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
5633        self.flip_flop_active.resize(slots as usize, false);
5634        self.flip_flop_active.fill(false);
5635        self.flip_flop_exclusive_left_line
5636            .resize(slots as usize, None);
5637        self.flip_flop_exclusive_left_line.fill(None);
5638        self.flip_flop_sequence.resize(slots as usize, 0);
5639        self.flip_flop_sequence.fill(0);
5640        self.flip_flop_last_dot.resize(slots as usize, None);
5641        self.flip_flop_last_dot.fill(None);
5642    }
5643
5644    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
5645    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
5646    /// [`Self::handle_line_numbers`]).
5647    #[inline]
5648    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
5649        if self.last_readline_handle.is_empty() {
5650            self.line_number
5651        } else {
5652            *self
5653                .handle_line_numbers
5654                .get(&self.last_readline_handle)
5655                .unwrap_or(&0)
5656        }
5657    }
5658
5659    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
5660    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
5661    ///
5662    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
5663    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
5664    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
5665    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
5666    /// get `0` / `N` from [`StrykeValue::to_int`].
5667    pub(crate) fn scalar_flip_flop_eval(
5668        &mut self,
5669        left: i64,
5670        right: i64,
5671        slot: usize,
5672        exclusive: bool,
5673    ) -> StrykeResult<StrykeValue> {
5674        if self.flip_flop_active.len() <= slot {
5675            self.flip_flop_active.resize(slot + 1, false);
5676        }
5677        if self.flip_flop_exclusive_left_line.len() <= slot {
5678            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5679        }
5680        if self.flip_flop_sequence.len() <= slot {
5681            self.flip_flop_sequence.resize(slot + 1, 0);
5682        }
5683        if self.flip_flop_last_dot.len() <= slot {
5684            self.flip_flop_last_dot.resize(slot + 1, None);
5685        }
5686        let dot = self.scalar_flipflop_dot_line();
5687        let active = &mut self.flip_flop_active[slot];
5688        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5689        let seq = &mut self.flip_flop_sequence[slot];
5690        let last_dot = &mut self.flip_flop_last_dot[slot];
5691        if !*active {
5692            if dot == left {
5693                *active = true;
5694                *seq = 1;
5695                *last_dot = Some(dot);
5696                if exclusive {
5697                    *excl_left = Some(dot);
5698                } else {
5699                    *excl_left = None;
5700                    if dot == right {
5701                        *active = false;
5702                        return Ok(StrykeValue::string(format!("{}E0", *seq)));
5703                    }
5704                }
5705                return Ok(StrykeValue::string(seq.to_string()));
5706            }
5707            *last_dot = Some(dot);
5708            return Ok(StrykeValue::string(String::new()));
5709        }
5710        // Already active: increment the sequence once per new `$.`, so a second evaluation on
5711        // the same line reads the same number (matches Perl `pp_flop`).
5712        if *last_dot != Some(dot) {
5713            *seq += 1;
5714            *last_dot = Some(dot);
5715        }
5716        let cur_seq = *seq;
5717        if let Some(ll) = *excl_left {
5718            if dot == right && dot > ll {
5719                *active = false;
5720                *excl_left = None;
5721                *seq = 0;
5722                return Ok(StrykeValue::string(format!("{}E0", cur_seq)));
5723            }
5724        } else if dot == right {
5725            *active = false;
5726            *seq = 0;
5727            return Ok(StrykeValue::string(format!("{}E0", cur_seq)));
5728        }
5729        Ok(StrykeValue::string(cur_seq.to_string()))
5730    }
5731
5732    fn regex_flip_flop_transition(
5733        active: &mut bool,
5734        excl_left: &mut Option<i64>,
5735        exclusive: bool,
5736        dot: i64,
5737        left_m: bool,
5738        right_m: bool,
5739    ) -> i64 {
5740        if !*active {
5741            if left_m {
5742                *active = true;
5743                if exclusive {
5744                    *excl_left = Some(dot);
5745                } else {
5746                    *excl_left = None;
5747                    if right_m {
5748                        *active = false;
5749                    }
5750                }
5751                return 1;
5752            }
5753            return 0;
5754        }
5755        if let Some(ll) = *excl_left {
5756            if right_m && dot > ll {
5757                *active = false;
5758                *excl_left = None;
5759            }
5760        } else if right_m {
5761            *active = false;
5762        }
5763        1
5764    }
5765
5766    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
5767    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
5768    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
5769    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
5770    pub(crate) fn regex_flip_flop_eval(
5771        &mut self,
5772        left_pat: &str,
5773        left_flags: &str,
5774        right_pat: &str,
5775        right_flags: &str,
5776        slot: usize,
5777        exclusive: bool,
5778        line: usize,
5779    ) -> StrykeResult<StrykeValue> {
5780        let dot = self.scalar_flipflop_dot_line();
5781        let subject = self.scope.get_scalar("_").to_string();
5782        let left_re = self
5783            .compile_regex(left_pat, left_flags, line)
5784            .map_err(|e| match e {
5785                FlowOrError::Error(err) => err,
5786                FlowOrError::Flow(_) => {
5787                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5788                }
5789            })?;
5790        let right_re = self
5791            .compile_regex(right_pat, right_flags, line)
5792            .map_err(|e| match e {
5793                FlowOrError::Error(err) => err,
5794                FlowOrError::Flow(_) => {
5795                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5796                }
5797            })?;
5798        let left_m = left_re.is_match(&subject);
5799        let right_m = right_re.is_match(&subject);
5800        if self.flip_flop_active.len() <= slot {
5801            self.flip_flop_active.resize(slot + 1, false);
5802        }
5803        if self.flip_flop_exclusive_left_line.len() <= slot {
5804            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5805        }
5806        let active = &mut self.flip_flop_active[slot];
5807        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5808        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5809            active, excl_left, exclusive, dot, left_m, right_m,
5810        )))
5811    }
5812
5813    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
5814    pub(crate) fn regex_flip_flop_eval_dynamic_right(
5815        &mut self,
5816        left_pat: &str,
5817        left_flags: &str,
5818        slot: usize,
5819        exclusive: bool,
5820        line: usize,
5821        right_m: bool,
5822    ) -> StrykeResult<StrykeValue> {
5823        let dot = self.scalar_flipflop_dot_line();
5824        let subject = self.scope.get_scalar("_").to_string();
5825        let left_re = self
5826            .compile_regex(left_pat, left_flags, line)
5827            .map_err(|e| match e {
5828                FlowOrError::Error(err) => err,
5829                FlowOrError::Flow(_) => {
5830                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5831                }
5832            })?;
5833        let left_m = left_re.is_match(&subject);
5834        if self.flip_flop_active.len() <= slot {
5835            self.flip_flop_active.resize(slot + 1, false);
5836        }
5837        if self.flip_flop_exclusive_left_line.len() <= slot {
5838            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5839        }
5840        let active = &mut self.flip_flop_active[slot];
5841        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5842        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5843            active, excl_left, exclusive, dot, left_m, right_m,
5844        )))
5845    }
5846
5847    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
5848    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
5849        &mut self,
5850        left_pat: &str,
5851        left_flags: &str,
5852        slot: usize,
5853        exclusive: bool,
5854        line: usize,
5855        rhs_line: i64,
5856    ) -> StrykeResult<StrykeValue> {
5857        let dot = self.scalar_flipflop_dot_line();
5858        let subject = self.scope.get_scalar("_").to_string();
5859        let left_re = self
5860            .compile_regex(left_pat, left_flags, line)
5861            .map_err(|e| match e {
5862                FlowOrError::Error(err) => err,
5863                FlowOrError::Flow(_) => {
5864                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5865                }
5866            })?;
5867        let left_m = left_re.is_match(&subject);
5868        let right_m = dot == rhs_line;
5869        if self.flip_flop_active.len() <= slot {
5870            self.flip_flop_active.resize(slot + 1, false);
5871        }
5872        if self.flip_flop_exclusive_left_line.len() <= slot {
5873            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5874        }
5875        let active = &mut self.flip_flop_active[slot];
5876        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5877        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5878            active, excl_left, exclusive, dot, left_m, right_m,
5879        )))
5880    }
5881
5882    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
5883    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
5884    /// right test until `$.` is strictly past the line where the left regex matched (same as
5885    /// [`Self::regex_flip_flop_eval`]).
5886    pub(crate) fn regex_eof_flip_flop_eval(
5887        &mut self,
5888        left_pat: &str,
5889        left_flags: &str,
5890        slot: usize,
5891        exclusive: bool,
5892        line: usize,
5893    ) -> StrykeResult<StrykeValue> {
5894        let dot = self.scalar_flipflop_dot_line();
5895        let subject = self.scope.get_scalar("_").to_string();
5896        let left_re = self
5897            .compile_regex(left_pat, left_flags, line)
5898            .map_err(|e| match e {
5899                FlowOrError::Error(err) => err,
5900                FlowOrError::Flow(_) => {
5901                    StrykeError::runtime("unexpected flow in regex/eof flip-flop", line)
5902                }
5903            })?;
5904        let left_m = left_re.is_match(&subject);
5905        let right_m = self.eof_without_arg_is_true();
5906        if self.flip_flop_active.len() <= slot {
5907            self.flip_flop_active.resize(slot + 1, false);
5908        }
5909        if self.flip_flop_exclusive_left_line.len() <= slot {
5910            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5911        }
5912        let active = &mut self.flip_flop_active[slot];
5913        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5914        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5915            active, excl_left, exclusive, dot, left_m, right_m,
5916        )))
5917    }
5918
5919    /// Shared `chomp` implementation (mutates `target`).
5920    /// `read(FH, $buf, LEN)` — read from filehandle into named variable.
5921    /// Returns bytes read count (or error). Called from VM's ReadIntoVar op.
5922    pub(crate) fn builtin_read_into(
5923        &mut self,
5924        fh_val: StrykeValue,
5925        var_name: &str,
5926        length: usize,
5927        line: usize,
5928    ) -> ExecResult {
5929        use std::io::Read;
5930        let fh = fh_val
5931            .as_io_handle_name()
5932            .unwrap_or_else(|| fh_val.to_string());
5933        let mut buf = vec![0u8; length];
5934        let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
5935            slot.lock().read(&mut buf).unwrap_or(0)
5936        } else if fh == "STDIN" {
5937            std::io::stdin().read(&mut buf).unwrap_or(0)
5938        } else {
5939            return Err(StrykeError::runtime(format!("read: unopened handle {}", fh), line).into());
5940        };
5941        buf.truncate(n);
5942        let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
5943        let _ = self
5944            .scope
5945            .set_scalar(var_name, StrykeValue::string(read_str));
5946        Ok(StrykeValue::integer(n as i64))
5947    }
5948
5949    pub(crate) fn chomp_inplace_execute(&mut self, val: StrykeValue, target: &Expr) -> ExecResult {
5950        // Perl's `chomp` on `@arr` / `%hash` iterates and chomps every
5951        // element in place, returning the *total count* of newlines
5952        // removed. Pre-fix this collapsed the array/hash to its
5953        // stringified form, chomped that, and reassigned a scalar
5954        // back — silently destroying the container.
5955        match &target.kind {
5956            ExprKind::ArrayVar(name) => {
5957                let arr = self.scope.get_array(name);
5958                let mut total = 0i64;
5959                let mut new_arr = Vec::with_capacity(arr.len());
5960                for v in arr {
5961                    let mut s = v.to_string();
5962                    if s.ends_with('\n') {
5963                        s.pop();
5964                        total += 1;
5965                    }
5966                    new_arr.push(StrykeValue::string(s));
5967                }
5968                self.scope
5969                    .set_array(name, new_arr)
5970                    .map_err(FlowOrError::Error)?;
5971                return Ok(StrykeValue::integer(total));
5972            }
5973            ExprKind::HashVar(name) => {
5974                let h = self.scope.get_hash(name);
5975                let mut total = 0i64;
5976                let mut new_h: indexmap::IndexMap<String, StrykeValue> =
5977                    indexmap::IndexMap::with_capacity(h.len());
5978                for (k, v) in h {
5979                    let mut s = v.to_string();
5980                    if s.ends_with('\n') {
5981                        s.pop();
5982                        total += 1;
5983                    }
5984                    new_h.insert(k, StrykeValue::string(s));
5985                }
5986                self.scope
5987                    .set_hash(name, new_h)
5988                    .map_err(FlowOrError::Error)?;
5989                return Ok(StrykeValue::integer(total));
5990            }
5991            _ => {}
5992        }
5993        let mut s = val.to_string();
5994        let removed = if s.ends_with('\n') {
5995            s.pop();
5996            1i64
5997        } else {
5998            0i64
5999        };
6000        self.assign_value(target, StrykeValue::string(s))?;
6001        Ok(StrykeValue::integer(removed))
6002    }
6003
6004    /// Shared `chop` implementation (mutates `target`).
6005    pub(crate) fn chop_inplace_execute(&mut self, val: StrykeValue, target: &Expr) -> ExecResult {
6006        // Perl's `chop @arr` / `chop %hash` chops every element in
6007        // place and returns the *last character chopped*. Without
6008        // this branch the call stringified the whole container,
6009        // chopped one byte off the joined form, and reassigned a
6010        // scalar back — destroying the array.
6011        match &target.kind {
6012            ExprKind::ArrayVar(name) => {
6013                let arr = self.scope.get_array(name);
6014                let mut last = StrykeValue::UNDEF;
6015                let mut new_arr = Vec::with_capacity(arr.len());
6016                for v in arr {
6017                    let mut s = v.to_string();
6018                    if let Some(c) = s.pop() {
6019                        last = StrykeValue::string(c.to_string());
6020                    }
6021                    new_arr.push(StrykeValue::string(s));
6022                }
6023                self.scope
6024                    .set_array(name, new_arr)
6025                    .map_err(FlowOrError::Error)?;
6026                return Ok(last);
6027            }
6028            ExprKind::HashVar(name) => {
6029                let h = self.scope.get_hash(name);
6030                let mut last = StrykeValue::UNDEF;
6031                let mut new_h: indexmap::IndexMap<String, StrykeValue> =
6032                    indexmap::IndexMap::with_capacity(h.len());
6033                for (k, v) in h {
6034                    let mut s = v.to_string();
6035                    if let Some(c) = s.pop() {
6036                        last = StrykeValue::string(c.to_string());
6037                    }
6038                    new_h.insert(k, StrykeValue::string(s));
6039                }
6040                self.scope
6041                    .set_hash(name, new_h)
6042                    .map_err(FlowOrError::Error)?;
6043                return Ok(last);
6044            }
6045            _ => {}
6046        }
6047        let mut s = val.to_string();
6048        let chopped = s
6049            .pop()
6050            .map(|c| StrykeValue::string(c.to_string()))
6051            .unwrap_or(StrykeValue::UNDEF);
6052        self.assign_value(target, StrykeValue::string(s))?;
6053        Ok(chopped)
6054    }
6055
6056    /// Shared regex match implementation (`pos` is updated for scalar `/g`).
6057    pub(crate) fn regex_match_execute(
6058        &mut self,
6059        s: String,
6060        pattern: &str,
6061        flags: &str,
6062        scalar_g: bool,
6063        pos_key: &str,
6064        line: usize,
6065    ) -> ExecResult {
6066        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
6067        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
6068        // also keep per-pattern `pos()` state that the memo doesn't track.
6069        //
6070        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
6071        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
6072        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
6073        if !flags.contains('g') && !scalar_g {
6074            let memo_hit = {
6075                if let Some(ref mem) = self.regex_match_memo {
6076                    mem.pattern == pattern
6077                        && mem.flags == flags
6078                        && mem.multiline == self.multiline_match
6079                        && mem.haystack == s
6080                } else {
6081                    false
6082                }
6083            };
6084            if memo_hit {
6085                if self.regex_capture_scope_fresh {
6086                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
6087                }
6088                // Memo hit but scope side effects were invalidated. Re-apply captures
6089                // from the memoized haystack + a fresh compiled regex.
6090                let (memo_s, memo_result) = {
6091                    let mem = self.regex_match_memo.as_ref().expect("memo");
6092                    (mem.haystack.clone(), mem.result.clone())
6093                };
6094                let re = self.compile_regex(pattern, flags, line)?;
6095                if let Some(caps) = re.captures(&memo_s) {
6096                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
6097                }
6098                self.regex_capture_scope_fresh = true;
6099                return Ok(memo_result);
6100            }
6101        }
6102        let re = self.compile_regex(pattern, flags, line)?;
6103        if flags.contains('g') && scalar_g {
6104            let key = pos_key.to_string();
6105            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
6106            if start == 0 {
6107                self.scope.set_array("^CAPTURE_ALL", vec![])?;
6108            }
6109            if start > s.len() {
6110                self.regex_pos.insert(key, None);
6111                return Ok(StrykeValue::integer(0));
6112            }
6113            let sub = s.get(start..).unwrap_or("");
6114            if let Some(caps) = re.captures(sub) {
6115                let overall = caps.get(0).expect("capture 0");
6116                let abs_end = start + overall.end;
6117                self.regex_pos.insert(key, Some(abs_end));
6118                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
6119                Ok(StrykeValue::integer(1))
6120            } else {
6121                self.regex_pos.insert(key, None);
6122                Ok(StrykeValue::integer(0))
6123            }
6124        } else if flags.contains('g') {
6125            let mut rows = Vec::new();
6126            let mut last_caps: Option<PerlCaptures<'_>> = None;
6127            let mut has_groups = false;
6128            // Flattened per-match captures so list-context `/g` returns
6129            // `($1_a, $2_a, $1_b, $2_b, …)` instead of the joined overall
6130            // match strings (Perl's documented behavior).
6131            let mut flat_captures: Vec<StrykeValue> = Vec::new();
6132            for caps in re.captures_iter(&s) {
6133                if caps.len() > 1 {
6134                    has_groups = true;
6135                }
6136                let cap_row = crate::perl_regex::numbered_capture_flat(&caps);
6137                flat_captures.extend(cap_row.iter().cloned());
6138                rows.push(StrykeValue::array(cap_row));
6139                last_caps = Some(caps);
6140            }
6141            self.scope.set_array("^CAPTURE_ALL", rows)?;
6142            if has_groups {
6143                if flat_captures.is_empty() {
6144                    return Ok(StrykeValue::integer(0));
6145                }
6146                if let Some(caps) = last_caps {
6147                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
6148                }
6149                return Ok(StrykeValue::array(flat_captures));
6150            }
6151            let matches: Vec<StrykeValue> = match &*re {
6152                PerlCompiledRegex::Rust(r) => r
6153                    .find_iter(&s)
6154                    .map(|m| StrykeValue::string(m.as_str().to_string()))
6155                    .collect(),
6156                PerlCompiledRegex::Fancy(r) => r
6157                    .find_iter(&s)
6158                    .filter_map(|m| m.ok())
6159                    .map(|m| StrykeValue::string(m.as_str().to_string()))
6160                    .collect(),
6161                PerlCompiledRegex::Pcre2(r) => r
6162                    .find_iter(s.as_bytes())
6163                    .filter_map(|m| m.ok())
6164                    .map(|m| {
6165                        let t = s.get(m.start()..m.end()).unwrap_or("");
6166                        StrykeValue::string(t.to_string())
6167                    })
6168                    .collect(),
6169            };
6170            if matches.is_empty() {
6171                Ok(StrykeValue::integer(0))
6172            } else {
6173                if let Some(caps) = last_caps {
6174                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
6175                }
6176                Ok(StrykeValue::array(matches))
6177            }
6178        } else if let Some(caps) = re.captures(&s) {
6179            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
6180            // Perl list-context `m//` returns the captures as a list when the
6181            // pattern has groups (so `my ($k, $v) = $s =~ /^(\w+)=(\d+)/` works).
6182            // When every capture is `undef` (e.g. only optional groups that
6183            // never fired, like `/^-?\d+(\.\d+)?$/` on "123") fall back to a
6184            // truthy `1`: returning `(undef,)` would silently flip every
6185            // Perl `if ($s =~ /…/)` idiom called through a fn whose body
6186            // happens to be in list context. Real captures still propagate.
6187            let list_context = self.wantarray_kind == WantarrayCtx::List;
6188            let has_groups = caps.len() > 1;
6189            let result = if list_context && has_groups {
6190                let cap_vec = crate::perl_regex::numbered_capture_flat(&caps);
6191                let any_defined = cap_vec.iter().any(|v| !v.is_undef());
6192                if any_defined {
6193                    StrykeValue::array(cap_vec)
6194                } else {
6195                    StrykeValue::integer(1)
6196                }
6197            } else {
6198                StrykeValue::integer(1)
6199            };
6200            // Only memoize when the result is the scalar form; otherwise the
6201            // memo can't know about wantarray.
6202            if matches!(result.as_integer(), Some(1)) {
6203                self.regex_match_memo = Some(RegexMatchMemo {
6204                    pattern: pattern.to_string(),
6205                    flags: flags.to_string(),
6206                    multiline: self.multiline_match,
6207                    haystack: s,
6208                    result: result.clone(),
6209                });
6210            }
6211            self.regex_capture_scope_fresh = true;
6212            Ok(result)
6213        } else {
6214            // No match: list context yields the empty list, scalar 0.
6215            let list_context = self.wantarray_kind == WantarrayCtx::List;
6216            let result = if list_context {
6217                StrykeValue::array(Vec::new())
6218            } else {
6219                StrykeValue::integer(0)
6220            };
6221            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
6222            if !list_context {
6223                self.regex_match_memo = Some(RegexMatchMemo {
6224                    pattern: pattern.to_string(),
6225                    flags: flags.to_string(),
6226                    multiline: self.multiline_match,
6227                    haystack: s,
6228                    result: result.clone(),
6229                });
6230            }
6231            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
6232            // the last successful match (if any) set them to. Don't flip the flag.
6233            Ok(result)
6234        }
6235    }
6236
6237    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
6238    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
6239    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
6240    pub(crate) fn expand_env_braces_in_subst(
6241        &mut self,
6242        raw: &str,
6243        line: usize,
6244    ) -> StrykeResult<String> {
6245        self.materialize_env_if_needed();
6246        let mut out = String::new();
6247        let mut rest = raw;
6248        while let Some(idx) = rest.find("$ENV{") {
6249            out.push_str(&rest[..idx]);
6250            let after = &rest[idx + 5..];
6251            let end = after
6252                .find('}')
6253                .ok_or_else(|| StrykeError::runtime("Unclosed $ENV{...} in s///", line))?;
6254            let key = &after[..end];
6255            let val = self.scope.get_hash_element("ENV", key);
6256            out.push_str(&val.to_string());
6257            rest = &after[end + 1..];
6258        }
6259        out.push_str(rest);
6260        Ok(out)
6261    }
6262
6263    /// Shared `s///` implementation.
6264    ///
6265    /// Perl replacement strings accept both `\1` and `$1` for back-references.
6266    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
6267    /// understand `$N`, so we normalise here.
6268    pub(crate) fn regex_subst_execute(
6269        &mut self,
6270        s: String,
6271        pattern: &str,
6272        replacement: &str,
6273        flags: &str,
6274        target: &Expr,
6275        line: usize,
6276    ) -> ExecResult {
6277        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
6278        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
6279        let re = self.compile_regex(&pattern, &re_flags, line)?;
6280        if flags.contains('e') {
6281            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
6282        }
6283        let replacement = self.expand_env_braces_in_subst(replacement, line)?;
6284        let replacement = self.interpolate_replacement_string(&replacement);
6285        let replacement = normalize_replacement_backrefs(&replacement);
6286        let last_caps = if flags.contains('g') {
6287            let mut rows = Vec::new();
6288            let mut last = None;
6289            for caps in re.captures_iter(&s) {
6290                rows.push(StrykeValue::array(
6291                    crate::perl_regex::numbered_capture_flat(&caps),
6292                ));
6293                last = Some(caps);
6294            }
6295            self.scope.set_array("^CAPTURE_ALL", rows)?;
6296            last
6297        } else {
6298            re.captures(&s)
6299        };
6300        if let Some(caps) = last_caps {
6301            let mode = if flags.contains('g') {
6302                CaptureAllMode::Skip
6303            } else {
6304                CaptureAllMode::Empty
6305            };
6306            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
6307        }
6308        let (new_s, count) = if flags.contains('g') {
6309            let count = re.find_iter_count(&s);
6310            (re.replace_all(&s, replacement.as_str()), count)
6311        } else {
6312            let count = if re.is_match(&s) { 1 } else { 0 };
6313            (re.replace(&s, replacement.as_str()), count)
6314        };
6315        if flags.contains('r') {
6316            // /r — non-destructive: return the modified string, leave target unchanged
6317            Ok(StrykeValue::string(new_s))
6318        } else {
6319            self.assign_value(target, StrykeValue::string(new_s))?;
6320            Ok(StrykeValue::integer(count as i64))
6321        }
6322    }
6323
6324    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
6325    /// and executes the string; the next round uses [`StrykeValue::to_string`] of the prior value).
6326    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
6327        let prep_source = |raw: &str| -> String {
6328            let mut code = raw.trim().to_string();
6329            if !code.ends_with(';') {
6330                code.push(';');
6331            }
6332            code
6333        };
6334        let mut cur = prep_source(replacement);
6335        let mut last = StrykeValue::UNDEF;
6336        for round in 0..e_count {
6337            last = crate::parse_and_run_string(&cur, self)?;
6338            if round + 1 < e_count {
6339                cur = prep_source(&last.to_string());
6340            }
6341        }
6342        Ok(last)
6343    }
6344
6345    fn regex_subst_execute_eval(
6346        &mut self,
6347        s: String,
6348        re: &PerlCompiledRegex,
6349        replacement: &str,
6350        flags: &str,
6351        target: &Expr,
6352        line: usize,
6353    ) -> ExecResult {
6354        let e_count = flags.chars().filter(|c| *c == 'e').count();
6355        if e_count == 0 {
6356            return Err(StrykeError::runtime("s///e: internal error (no e flag)", line).into());
6357        }
6358
6359        if flags.contains('g') {
6360            let mut rows = Vec::new();
6361            let mut out = String::new();
6362            let mut last = 0usize;
6363            let mut count = 0usize;
6364            for caps in re.captures_iter(&s) {
6365                let m0 = caps.get(0).expect("regex capture 0");
6366                out.push_str(&s[last..m0.start]);
6367                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
6368                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
6369                out.push_str(&repl_val.to_string());
6370                last = m0.end;
6371                count += 1;
6372                rows.push(StrykeValue::array(
6373                    crate::perl_regex::numbered_capture_flat(&caps),
6374                ));
6375            }
6376            self.scope.set_array("^CAPTURE_ALL", rows)?;
6377            out.push_str(&s[last..]);
6378            if flags.contains('r') {
6379                return Ok(StrykeValue::string(out));
6380            }
6381            self.assign_value(target, StrykeValue::string(out))?;
6382            return Ok(StrykeValue::integer(count as i64));
6383        }
6384        if let Some(caps) = re.captures(&s) {
6385            let m0 = caps.get(0).expect("regex capture 0");
6386            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
6387            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
6388            let mut out = String::new();
6389            out.push_str(&s[..m0.start]);
6390            out.push_str(&repl_val.to_string());
6391            out.push_str(&s[m0.end..]);
6392            if flags.contains('r') {
6393                return Ok(StrykeValue::string(out));
6394            }
6395            self.assign_value(target, StrykeValue::string(out))?;
6396            return Ok(StrykeValue::integer(1));
6397        }
6398        if flags.contains('r') {
6399            return Ok(StrykeValue::string(s));
6400        }
6401        self.assign_value(target, StrykeValue::string(s))?;
6402        Ok(StrykeValue::integer(0))
6403    }
6404
6405    /// Shared `tr///` implementation.
6406    pub(crate) fn regex_transliterate_execute(
6407        &mut self,
6408        s: String,
6409        from: &str,
6410        to: &str,
6411        flags: &str,
6412        target: &Expr,
6413        line: usize,
6414    ) -> ExecResult {
6415        let _ = line;
6416        let from_chars = Self::tr_expand_ranges(from);
6417        let to_chars = Self::tr_expand_ranges(to);
6418        let delete_mode = flags.contains('d');
6419        let complement = flags.contains('c');
6420        let squash = flags.contains('s');
6421
6422        let mut count = 0i64;
6423        let mut new_s = String::with_capacity(s.len());
6424        let mut last_out: Option<char> = None;
6425        for c in s.chars() {
6426            let in_from = from_chars.iter().position(|&fc| fc == c);
6427            let matched = if complement {
6428                in_from.is_none()
6429            } else {
6430                in_from.is_some()
6431            };
6432            if !matched {
6433                new_s.push(c);
6434                last_out = Some(c);
6435                continue;
6436            }
6437            count += 1;
6438            // Pick the replacement character.
6439            //   - complement: every matched char maps to the LAST char of `to`
6440            //     (Perl behavior); `tr/0-9//c` with empty `to` falls through to
6441            //     the keep/delete decision below.
6442            //   - direct: matched chars map by position, with the last `to`
6443            //     char duplicating if `from` is longer (unless `/d` is set).
6444            let out_c = if complement {
6445                to_chars.last().copied()
6446            } else if let Some(pos) = in_from {
6447                if pos < to_chars.len() {
6448                    Some(to_chars[pos])
6449                } else if delete_mode {
6450                    None
6451                } else {
6452                    to_chars.last().copied().or(Some(c))
6453                }
6454            } else {
6455                None
6456            };
6457            let out_c = match out_c {
6458                Some(c) => c,
6459                None => {
6460                    // No replacement available — either `/d` deletes, or there
6461                    // was no `to` char at all; keep the original character to
6462                    // match Perl's "tr/X//" identity behavior.
6463                    if delete_mode {
6464                        continue;
6465                    }
6466                    c
6467                }
6468            };
6469            if squash && last_out == Some(out_c) {
6470                continue;
6471            }
6472            new_s.push(out_c);
6473            last_out = Some(out_c);
6474        }
6475
6476        if flags.contains('r') {
6477            // /r — non-destructive: return the modified string, leave target unchanged
6478            Ok(StrykeValue::string(new_s))
6479        } else {
6480            self.assign_value(target, StrykeValue::string(new_s))?;
6481            Ok(StrykeValue::integer(count))
6482        }
6483    }
6484
6485    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
6486    /// A literal `-` at the start or end of the spec is kept as-is.
6487    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
6488        let raw: Vec<char> = spec.chars().collect();
6489        let mut out = Vec::with_capacity(raw.len());
6490        let mut i = 0;
6491        while i < raw.len() {
6492            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
6493                let start = raw[i] as u32;
6494                let end = raw[i + 2] as u32;
6495                for code in start..=end {
6496                    if let Some(c) = char::from_u32(code) {
6497                        out.push(c);
6498                    }
6499                }
6500                i += 3;
6501            } else {
6502                out.push(raw[i]);
6503                i += 1;
6504            }
6505        }
6506        out
6507    }
6508
6509    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
6510    pub(crate) fn splice_builtin_execute(
6511        &mut self,
6512        args: &[StrykeValue],
6513        line: usize,
6514    ) -> StrykeResult<StrykeValue> {
6515        if args.is_empty() {
6516            return Err(StrykeError::runtime("splice: missing array", line));
6517        }
6518        let arr_name = args[0].to_string();
6519        let arr_len = self.scope.array_len(&arr_name);
6520        let offset_val = args
6521            .get(1)
6522            .cloned()
6523            .unwrap_or_else(|| StrykeValue::integer(0));
6524        let length_val = match args.get(2) {
6525            None => StrykeValue::UNDEF,
6526            Some(v) => v.clone(),
6527        };
6528        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
6529        // Perl's `splice LIST` is list context — `@arr` and other list-valued
6530        // operands splat into the replacement, instead of being scalarized to
6531        // their element count. Mirrors `push`/`unshift` flattening.
6532        let mut rep_vals: Vec<StrykeValue> = Vec::new();
6533        for a in args.iter().skip(3) {
6534            if let Some(items) = a.as_array_vec() {
6535                rep_vals.extend(items);
6536            } else {
6537                rep_vals.push(a.clone());
6538            }
6539        }
6540        let removed = self.scope.splice_in_place(&arr_name, off, end, rep_vals)?;
6541        Ok(match self.wantarray_kind {
6542            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
6543            WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
6544        })
6545    }
6546
6547    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
6548    pub(crate) fn unshift_builtin_execute(
6549        &mut self,
6550        args: &[StrykeValue],
6551        line: usize,
6552    ) -> StrykeResult<StrykeValue> {
6553        if args.is_empty() {
6554            return Err(StrykeError::runtime("unshift: missing array", line));
6555        }
6556        let arr_name = args[0].to_string();
6557        let mut flat_vals: Vec<StrykeValue> = Vec::new();
6558        for a in args.iter().skip(1) {
6559            if let Some(items) = a.as_array_vec() {
6560                flat_vals.extend(items);
6561            } else {
6562                flat_vals.push(a.clone());
6563            }
6564        }
6565        let arr = self.scope.get_array_mut(&arr_name)?;
6566        for (i, v) in flat_vals.into_iter().enumerate() {
6567            arr.insert(i, v);
6568        }
6569        Ok(StrykeValue::integer(arr.len() as i64))
6570    }
6571
6572    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
6573    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
6574    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
6575        if upper == 0.0 {
6576            self.rand_rng.gen_range(0.0..1.0)
6577        } else if upper > 0.0 {
6578            self.rand_rng.gen_range(0.0..upper)
6579        } else {
6580            self.rand_rng.gen_range(upper..0.0)
6581        }
6582    }
6583
6584    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
6585    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
6586        let n = if let Some(s) = seed {
6587            s as i64
6588        } else {
6589            std::time::SystemTime::now()
6590                .duration_since(std::time::UNIX_EPOCH)
6591                .map(|d| d.as_secs() as i64)
6592                .unwrap_or(1)
6593        };
6594        let mag = n.unsigned_abs();
6595        self.rand_rng = StdRng::seed_from_u64(mag);
6596        n.abs()
6597    }
6598    /// `set_file` — see implementation.
6599    pub fn set_file(&mut self, file: &str) {
6600        self.file = file.to_string();
6601    }
6602
6603    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
6604    pub fn repl_completion_names(&self) -> Vec<String> {
6605        let mut v = self.scope.repl_binding_names();
6606        v.extend(self.subs.keys().cloned());
6607        v.sort();
6608        v.dedup();
6609        v
6610    }
6611
6612    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
6613    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
6614        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
6615        subs.sort();
6616        let mut classes: HashSet<String> = HashSet::new();
6617        for k in &subs {
6618            if let Some((pkg, rest)) = k.split_once("::") {
6619                if !rest.contains("::") {
6620                    classes.insert(pkg.to_string());
6621                }
6622            }
6623        }
6624        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
6625        for bn in self.scope.repl_binding_names() {
6626            if let Some(r) = bn.strip_prefix('$') {
6627                let v = self.scope.get_scalar(r);
6628                if let Some(b) = v.as_blessed_ref() {
6629                    blessed_scalars.insert(r.to_string(), b.class.clone());
6630                    classes.insert(b.class.clone());
6631                }
6632            }
6633        }
6634        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
6635        for c in classes {
6636            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
6637        }
6638        ReplCompletionSnapshot {
6639            subs,
6640            blessed_scalars,
6641            isa_for_class,
6642        }
6643    }
6644
6645    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
6646        if n == 0 {
6647            return Err(FlowOrError::Error(StrykeError::runtime(
6648                "bench: iteration count must be positive",
6649                line,
6650            )));
6651        }
6652        let mut samples = Vec::with_capacity(n);
6653        for _ in 0..n {
6654            let start = std::time::Instant::now();
6655            self.exec_block(body)?;
6656            samples.push(start.elapsed().as_secs_f64() * 1000.0);
6657        }
6658        let mut sorted = samples.clone();
6659        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6660        let min_ms = sorted[0];
6661        let mean = samples.iter().sum::<f64>() / n as f64;
6662        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
6663            .saturating_sub(1)
6664            .min(n - 1);
6665        let p99_ms = sorted[p99_idx];
6666        Ok(StrykeValue::string(format!(
6667            "bench: n={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
6668            n, min_ms, mean, p99_ms
6669        )))
6670    }
6671    /// `execute` — see implementation.
6672    pub fn execute(&mut self, program: &Program) -> StrykeResult<StrykeValue> {
6673        // Snapshot the (possibly empty) class registry into the
6674        // thread-local that the free-function serializers consult, so
6675        // that `to_json($obj)` can resolve inheritance fields without
6676        // taking a `&VMHelper`. Done unconditionally — cheap clone of
6677        // an Arc<HashMap>-shaped structure.
6678        crate::serialize_normalize::install_class_defs(self.class_defs.clone());
6679        // `-n`/`-p`: compile and run only the prelude, store chunk for per-line re-execution.
6680        if self.line_mode_skip_main {
6681            crate::compile_and_run_prelude(program, self)?;
6682            return Ok(StrykeValue::UNDEF);
6683        }
6684        crate::try_vm_execute(program, self)
6685            .expect("VM compilation must succeed — all execution is VM-only")
6686    }
6687
6688    /// Run the compiled `END` region **once** after the `-n`/`-p` line loop.
6689    ///
6690    /// In line mode the `END` region is compiled AFTER the per-line body's `Halt` (see
6691    /// `Compiler::with_line_mode`), so [`Self::process_line`] stops at the main `Halt` and never
6692    /// runs `END` during the loop. Here we run it exactly once by resuming the pristine chunk at
6693    /// [`crate::bytecode::Chunk::line_mode_end_ip`] (`None` when the program has no `END` blocks).
6694    /// This keeps `END` execution on the VM (no tree-walker divergence) and matches Perl: `END`
6695    /// fires once at program exit, not per input line.
6696    pub fn run_end_blocks(&mut self) -> StrykeResult<()> {
6697        let Some(chunk) = self.line_mode_chunk.take() else {
6698            return Ok(());
6699        };
6700        // `line_mode_end_ip` is `Some` only when the program has `END` blocks; the region sits
6701        // after the per-line `Halt`, so we resume the pristine chunk there and run it once.
6702        if let Some(end_ip) = chunk.line_mode_end_ip {
6703            let vm_jit = self.vm_jit_enabled && self.profiler.is_none();
6704            let mut vm = crate::vm::VM::new(&chunk, self);
6705            vm.set_jit_enabled(vm_jit);
6706            vm.ip = end_ip;
6707            let _ = vm.execute()?;
6708        }
6709        self.line_mode_chunk = Some(chunk);
6710        Ok(())
6711    }
6712
6713    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
6714    /// and drain remaining `DESTROY` callbacks.
6715    pub fn run_global_teardown(&mut self) -> StrykeResult<()> {
6716        self.global_phase = "DESTRUCT".to_string();
6717        self.drain_pending_destroys(0)
6718    }
6719
6720    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
6721    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> StrykeResult<()> {
6722        loop {
6723            let batch = crate::pending_destroy::take_queue();
6724            if batch.is_empty() {
6725                break;
6726            }
6727            for (class, payload) in batch {
6728                let fq = format!("{}::DESTROY", class);
6729                let Some(sub) = self.subs.get(&fq).cloned() else {
6730                    continue;
6731                };
6732                let inv = StrykeValue::blessed(Arc::new(
6733                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
6734                ));
6735                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
6736                    Ok(_) => {}
6737                    Err(FlowOrError::Error(e)) => return Err(e),
6738                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
6739                    Err(FlowOrError::Flow(other)) => {
6740                        return Err(StrykeError::runtime(
6741                            format!("DESTROY: unexpected control flow ({other:?})"),
6742                            line,
6743                        ));
6744                    }
6745                }
6746            }
6747        }
6748        Ok(())
6749    }
6750
6751    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
6752        self.exec_block_with_tail(block, WantarrayCtx::Void)
6753    }
6754
6755    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
6756    /// Non-final statements stay void context.
6757    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6758        let uses_goto = block
6759            .iter()
6760            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
6761        if uses_goto {
6762            self.scope_push_hook();
6763            let r = self.exec_block_with_goto_tail(block, tail);
6764            self.scope_pop_hook();
6765            r
6766        } else {
6767            self.scope_push_hook();
6768            let result = self.exec_block_no_scope_with_tail(block, tail);
6769            self.scope_pop_hook();
6770            result
6771        }
6772    }
6773
6774    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6775        let mut map: HashMap<String, usize> = HashMap::new();
6776        for (i, s) in block.iter().enumerate() {
6777            if let Some(l) = &s.label {
6778                map.insert(l.clone(), i);
6779            }
6780        }
6781        let mut pc = 0usize;
6782        let mut last = StrykeValue::UNDEF;
6783        let last_idx = block.len().saturating_sub(1);
6784        while pc < block.len() {
6785            if let StmtKind::Goto { target } = &block[pc].kind {
6786                let line = block[pc].line;
6787                let name = self.eval_expr(target)?.to_string();
6788                pc = *map.get(&name).ok_or_else(|| {
6789                    FlowOrError::Error(StrykeError::runtime(
6790                        format!("goto: unknown label {}", name),
6791                        line,
6792                    ))
6793                })?;
6794                continue;
6795            }
6796            let v = if pc == last_idx {
6797                match &block[pc].kind {
6798                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
6799                    _ => self.exec_statement(&block[pc])?,
6800                }
6801            } else {
6802                self.exec_statement(&block[pc])?
6803            };
6804            last = v;
6805            pc += 1;
6806        }
6807        Ok(last)
6808    }
6809
6810    /// Execute block statements without pushing/popping a scope frame.
6811    /// Used internally by loops and the VM for sub calls.
6812    #[inline]
6813    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
6814        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
6815    }
6816
6817    pub(crate) fn exec_block_no_scope_with_tail(
6818        &mut self,
6819        block: &Block,
6820        tail: WantarrayCtx,
6821    ) -> ExecResult {
6822        if block.is_empty() {
6823            return Ok(StrykeValue::UNDEF);
6824        }
6825        let last_i = block.len() - 1;
6826        for (i, stmt) in block.iter().enumerate() {
6827            if i < last_i {
6828                self.exec_statement(stmt)?;
6829            } else {
6830                return match &stmt.kind {
6831                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
6832                    _ => self.exec_statement(stmt),
6833                };
6834            }
6835        }
6836        Ok(StrykeValue::UNDEF)
6837    }
6838
6839    /// Spawn `block` on a worker thread; returns an [`StrykeValue::AsyncTask`] handle (`async { }` / `spawn { }`).
6840    pub(crate) fn spawn_async_block(&self, block: &Block) -> StrykeValue {
6841        use parking_lot::Mutex as ParkMutex;
6842
6843        let block = block.clone();
6844        let subs = self.subs.clone();
6845        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6846        let result = Arc::new(ParkMutex::new(None));
6847        let join = Arc::new(ParkMutex::new(None));
6848        let result2 = result.clone();
6849        let h = std::thread::spawn(move || {
6850            let mut interp = VMHelper::new();
6851            interp.subs = subs;
6852            interp.scope.restore_capture(&scalars);
6853            interp.scope.restore_atomics(&aar, &ahash);
6854            interp.enable_parallel_guard();
6855            let r = match interp.exec_block(&block) {
6856                Ok(v) => Ok(v),
6857                Err(FlowOrError::Error(e)) => Err(e),
6858                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6859                    Err(StrykeError::runtime("yield inside async/spawn block", 0))
6860                }
6861                Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
6862            };
6863            *result2.lock() = Some(r);
6864        });
6865        *join.lock() = Some(h);
6866        StrykeValue::async_task(Arc::new(StrykeAsyncTask { result, join }))
6867    }
6868
6869    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
6870    pub(crate) fn eval_timeout_block(
6871        &mut self,
6872        body: &Block,
6873        secs: f64,
6874        line: usize,
6875    ) -> ExecResult {
6876        use std::sync::mpsc::channel;
6877        use std::time::Duration;
6878
6879        let block = body.clone();
6880        let subs = self.subs.clone();
6881        let struct_defs = self.struct_defs.clone();
6882        let enum_defs = self.enum_defs.clone();
6883        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6884        self.materialize_env_if_needed();
6885        let env = self.env.clone();
6886        let argv = self.argv.clone();
6887        let inc = self.scope.get_array("INC");
6888        let (tx, rx) = channel::<StrykeResult<StrykeValue>>();
6889        let _handle = std::thread::spawn(move || {
6890            let mut interp = VMHelper::new();
6891            interp.subs = subs;
6892            interp.struct_defs = struct_defs;
6893            interp.enum_defs = enum_defs;
6894            interp.env = env.clone();
6895            interp.argv = argv.clone();
6896            interp.scope.declare_array(
6897                "ARGV",
6898                argv.iter()
6899                    .map(|s| StrykeValue::string(s.clone()))
6900                    .collect(),
6901            );
6902            for (k, v) in env {
6903                interp
6904                    .scope
6905                    .set_hash_element("ENV", &k, v)
6906                    .expect("set ENV in timeout thread");
6907            }
6908            interp.scope.declare_array("INC", inc);
6909            interp.scope.restore_capture(&scalars);
6910            interp.scope.restore_atomics(&aar, &ahash);
6911            interp.enable_parallel_guard();
6912            let out: StrykeResult<StrykeValue> = match interp.exec_block(&block) {
6913                Ok(v) => Ok(v),
6914                Err(FlowOrError::Error(e)) => Err(e),
6915                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6916                    Err(StrykeError::runtime("yield inside eval_timeout block", 0))
6917                }
6918                Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
6919            };
6920            let _ = tx.send(out);
6921        });
6922        let dur = Duration::from_secs_f64(secs.max(0.0));
6923        match rx.recv_timeout(dur) {
6924            Ok(Ok(v)) => Ok(v),
6925            Ok(Err(e)) => Err(FlowOrError::Error(e)),
6926            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(StrykeError::runtime(
6927                format!(
6928                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
6929                    secs
6930                ),
6931                line,
6932            )
6933            .into()),
6934            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(StrykeError::runtime(
6935                "eval_timeout: worker thread panicked or disconnected",
6936                line,
6937            )
6938            .into()),
6939        }
6940    }
6941
6942    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
6943        let mut last = StrykeValue::UNDEF;
6944        for stmt in body {
6945            match &stmt.kind {
6946                StmtKind::When { cond, body: wb } => {
6947                    if self.when_matches(cond)? {
6948                        return self.exec_block_smart(wb);
6949                    }
6950                }
6951                StmtKind::DefaultCase { body: db } => {
6952                    return self.exec_block_smart(db);
6953                }
6954                _ => {
6955                    last = self.exec_statement(stmt)?;
6956                }
6957            }
6958        }
6959        Ok(last)
6960    }
6961
6962    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
6963    pub(crate) fn exec_given_with_topic_value(
6964        &mut self,
6965        topic: StrykeValue,
6966        body: &Block,
6967    ) -> ExecResult {
6968        self.scope_push_hook();
6969        self.scope.declare_scalar("_", topic);
6970        self.english_note_lexical_scalar("_");
6971        let r = self.exec_given_body(body);
6972        self.scope_pop_hook();
6973        r
6974    }
6975
6976    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
6977        let t = self.eval_expr(topic)?;
6978        self.exec_given_with_topic_value(t, body)
6979    }
6980
6981    /// `when (COND)` — topic is `$_` (set by `given`).
6982    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6983        let topic = self.scope.get_scalar("_");
6984        let line = cond.line;
6985        match &cond.kind {
6986            ExprKind::Regex(pattern, flags) => {
6987                let re = self.compile_regex(pattern, flags, line)?;
6988                let s = topic.to_string();
6989                Ok(re.is_match(&s))
6990            }
6991            ExprKind::String(s) => Ok(topic.to_string() == *s),
6992            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
6993            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
6994            _ => {
6995                let c = self.eval_expr(cond)?;
6996                Ok(self.smartmatch_when(&topic, &c))
6997            }
6998        }
6999    }
7000
7001    fn smartmatch_when(&self, topic: &StrykeValue, c: &StrykeValue) -> bool {
7002        if let Some(re) = c.as_regex() {
7003            return re.is_match(&topic.to_string());
7004        }
7005        // ARRAY / array-ref RHS: smartmatch is "any element matches the topic"
7006        // (`$x ~~ @list` → `grep { $x ~~ $_ } @list` per `perlop`).
7007        // Without this branch, `when ([2, 3, 5, 7])` always falls through to
7008        // `default` because the array stringified ("23 5 7"-ish) won't equal
7009        // the scalar. Recurse so nested arrays / regexes inside the array
7010        // still work.
7011        if let Some(arr) = c.as_array_ref() {
7012            let arr = arr.read();
7013            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
7014        }
7015        if let Some(arr) = c.as_array_vec() {
7016            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
7017        }
7018        // HASH / hash-ref RHS: "topic is a key" (`$x ~~ %h` → `exists $h{$x}`).
7019        if let Some(href) = c.as_hash_ref() {
7020            return href.read().contains_key(&topic.to_string());
7021        }
7022        if let Some(h) = c.as_hash_map() {
7023            return h.contains_key(&topic.to_string());
7024        }
7025        // Coderef RHS: call it with the topic, treat truthy result as match.
7026        if let Some(sub) = c.as_code_ref() {
7027            // smartmatch_when is `&self`; we can't `call_sub` (needs `&mut`).
7028            // For now, fall through to string equality. Future: hoist
7029            // when_matches to use `&mut self` so coderef RHS can fire.
7030            let _ = sub;
7031        }
7032        // Numeric equality if both sides parse as numbers.
7033        if let (Some(a), Some(b)) = (topic.as_integer(), c.as_integer()) {
7034            return a == b;
7035        }
7036        topic.to_string() == c.to_string()
7037    }
7038
7039    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
7040    pub(crate) fn eval_boolean_rvalue_condition(
7041        &mut self,
7042        cond: &Expr,
7043    ) -> Result<bool, FlowOrError> {
7044        match &cond.kind {
7045            ExprKind::Regex(pattern, flags) => {
7046                let topic = self.scope.get_scalar("_");
7047                let line = cond.line;
7048                let s = topic.to_string();
7049                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
7050                Ok(v.is_true())
7051            }
7052            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
7053            ExprKind::ReadLine(_) => {
7054                let v = self.eval_expr(cond)?;
7055                self.scope.set_topic(v.clone());
7056                Ok(!v.is_undef())
7057            }
7058            _ => {
7059                let v = self.eval_expr(cond)?;
7060                Ok(v.is_true())
7061            }
7062        }
7063    }
7064
7065    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
7066    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
7067        self.eval_boolean_rvalue_condition(cond)
7068    }
7069
7070    pub(crate) fn eval_algebraic_match(
7071        &mut self,
7072        subject: &Expr,
7073        arms: &[MatchArm],
7074        line: usize,
7075    ) -> ExecResult {
7076        let val = self.eval_algebraic_match_subject(subject, line)?;
7077        self.eval_algebraic_match_with_subject_value(val, arms, line)
7078    }
7079
7080    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
7081    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
7082        match &subject.kind {
7083            ExprKind::ArrayVar(name) => {
7084                self.check_strict_array_var(name, line)?;
7085                let aname = self.stash_array_name_for_package(name);
7086                Ok(StrykeValue::array_binding_ref(aname))
7087            }
7088            ExprKind::HashVar(name) => {
7089                self.check_strict_hash_var(name, line)?;
7090                self.touch_env_hash(name);
7091                Ok(StrykeValue::hash_binding_ref(name.clone()))
7092            }
7093            _ => self.eval_expr(subject),
7094        }
7095    }
7096
7097    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
7098    pub(crate) fn eval_algebraic_match_with_subject_value(
7099        &mut self,
7100        val: StrykeValue,
7101        arms: &[MatchArm],
7102        line: usize,
7103    ) -> ExecResult {
7104        // Exhaustive enum match: check variant coverage before matching
7105        if let Some(e) = val.as_enum_inst() {
7106            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
7107            if !has_catchall {
7108                let covered: Vec<String> = arms
7109                    .iter()
7110                    .filter_map(|a| {
7111                        if let MatchPattern::Value(expr) = &a.pattern {
7112                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
7113                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
7114                            }
7115                        }
7116                        None
7117                    })
7118                    .collect();
7119                let missing: Vec<&str> = e
7120                    .def
7121                    .variants
7122                    .iter()
7123                    .filter(|v| !covered.contains(&v.name))
7124                    .map(|v| v.name.as_str())
7125                    .collect();
7126                if !missing.is_empty() {
7127                    return Err(StrykeError::runtime(
7128                        format!(
7129                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
7130                            e.def.name,
7131                            missing.join(", ")
7132                        ),
7133                        line,
7134                    )
7135                    .into());
7136                }
7137            }
7138        }
7139        for arm in arms {
7140            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
7141                let re = self.compile_regex(pattern, flags, line)?;
7142                let s = val.to_string();
7143                if let Some(caps) = re.captures(&s) {
7144                    self.scope_push_hook();
7145                    self.scope.declare_scalar("_", val.clone());
7146                    self.english_note_lexical_scalar("_");
7147                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
7148                    let guard_ok = if let Some(g) = &arm.guard {
7149                        self.eval_expr(g)?.is_true()
7150                    } else {
7151                        true
7152                    };
7153                    if !guard_ok {
7154                        self.scope_pop_hook();
7155                        continue;
7156                    }
7157                    let out = self.eval_expr(&arm.body);
7158                    self.scope_pop_hook();
7159                    return out;
7160                }
7161                continue;
7162            }
7163            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
7164                self.scope_push_hook();
7165                self.scope.declare_scalar("_", val.clone());
7166                self.english_note_lexical_scalar("_");
7167                for b in bindings {
7168                    match b {
7169                        PatternBinding::Scalar(name, v) => {
7170                            self.scope.declare_scalar(&name, v);
7171                            self.english_note_lexical_scalar(&name);
7172                        }
7173                        PatternBinding::Array(name, elems) => {
7174                            self.scope.declare_array(&name, elems);
7175                        }
7176                    }
7177                }
7178                let guard_ok = if let Some(g) = &arm.guard {
7179                    self.eval_expr(g)?.is_true()
7180                } else {
7181                    true
7182                };
7183                if !guard_ok {
7184                    self.scope_pop_hook();
7185                    continue;
7186                }
7187                let out = self.eval_expr(&arm.body);
7188                self.scope_pop_hook();
7189                return out;
7190            }
7191        }
7192        Err(StrykeError::runtime(
7193            "match: no arm matched the value (add a `_` catch-all)",
7194            line,
7195        )
7196        .into())
7197    }
7198
7199    fn parse_duration_seconds(pv: &StrykeValue) -> Option<f64> {
7200        let s = pv.to_string();
7201        let s = s.trim();
7202        if let Some(rest) = s.strip_suffix("ms") {
7203            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
7204        }
7205        if let Some(rest) = s.strip_suffix('s') {
7206            return rest.trim().parse::<f64>().ok();
7207        }
7208        if let Some(rest) = s.strip_suffix('m') {
7209            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
7210        }
7211        s.parse::<f64>().ok()
7212    }
7213
7214    fn eval_retry_block(
7215        &mut self,
7216        body: &Block,
7217        times: &Expr,
7218        backoff: RetryBackoff,
7219        _line: usize,
7220    ) -> ExecResult {
7221        let max = self.eval_expr(times)?.to_int().max(1) as usize;
7222        let base_ms: u64 = 10;
7223        let mut attempt = 0usize;
7224        loop {
7225            attempt += 1;
7226            match self.exec_block(body) {
7227                Ok(v) => return Ok(v),
7228                Err(FlowOrError::Error(e)) => {
7229                    if attempt >= max {
7230                        return Err(FlowOrError::Error(e));
7231                    }
7232                    let delay_ms = match backoff {
7233                        RetryBackoff::None => 0,
7234                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
7235                        RetryBackoff::Exponential => {
7236                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
7237                        }
7238                    };
7239                    if delay_ms > 0 {
7240                        std::thread::sleep(Duration::from_millis(delay_ms));
7241                    }
7242                }
7243                Err(e) => return Err(e),
7244            }
7245        }
7246    }
7247
7248    fn eval_rate_limit_block(
7249        &mut self,
7250        slot: u32,
7251        max: &Expr,
7252        window: &Expr,
7253        body: &Block,
7254        _line: usize,
7255    ) -> ExecResult {
7256        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
7257        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
7258            .filter(|s| *s > 0.0)
7259            .unwrap_or(1.0);
7260        let window_d = Duration::from_secs_f64(window_sec);
7261        let slot = slot as usize;
7262        while self.rate_limit_slots.len() <= slot {
7263            self.rate_limit_slots.push(VecDeque::new());
7264        }
7265        {
7266            let dq = &mut self.rate_limit_slots[slot];
7267            loop {
7268                let now = Instant::now();
7269                while let Some(t0) = dq.front().copied() {
7270                    if now.duration_since(t0) >= window_d {
7271                        dq.pop_front();
7272                    } else {
7273                        break;
7274                    }
7275                }
7276                if dq.len() < max_n || max_n == 0 {
7277                    break;
7278                }
7279                let t0 = dq.front().copied().unwrap();
7280                let wait = window_d.saturating_sub(now.duration_since(t0));
7281                if wait.is_zero() {
7282                    dq.pop_front();
7283                    continue;
7284                }
7285                std::thread::sleep(wait);
7286            }
7287            dq.push_back(Instant::now());
7288        }
7289        self.exec_block(body)
7290    }
7291
7292    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
7293        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
7294            .filter(|s| *s > 0.0)
7295            .unwrap_or(1.0);
7296        loop {
7297            match self.exec_block(body) {
7298                Ok(_) => {}
7299                Err(e) => return Err(e),
7300            }
7301            std::thread::sleep(Duration::from_secs_f64(sec));
7302        }
7303    }
7304
7305    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
7306    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> StrykeResult<StrykeValue> {
7307        let pair = |value: StrykeValue, more: i64| {
7308            StrykeValue::array_ref(Arc::new(RwLock::new(vec![
7309                value,
7310                StrykeValue::integer(more),
7311            ])))
7312        };
7313        let mut exhausted = gen.exhausted.lock();
7314        if *exhausted {
7315            return Ok(pair(StrykeValue::UNDEF, 0));
7316        }
7317        let mut pc = gen.pc.lock();
7318        let mut scope_started = gen.scope_started.lock();
7319        if *pc >= gen.block.len() {
7320            if *scope_started {
7321                self.scope_pop_hook();
7322                *scope_started = false;
7323            }
7324            *exhausted = true;
7325            return Ok(pair(StrykeValue::UNDEF, 0));
7326        }
7327        if !*scope_started {
7328            self.scope_push_hook();
7329            *scope_started = true;
7330        }
7331        self.in_generator = true;
7332        while *pc < gen.block.len() {
7333            let stmt = &gen.block[*pc];
7334            match self.exec_statement(stmt) {
7335                Ok(_) => {
7336                    *pc += 1;
7337                }
7338                Err(FlowOrError::Flow(Flow::Yield(v))) => {
7339                    *pc += 1;
7340                    self.in_generator = false;
7341                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
7342                    // binds in the caller block, not inside a frame left across yield.
7343                    if *scope_started {
7344                        self.scope_pop_hook();
7345                        *scope_started = false;
7346                    }
7347                    return Ok(pair(v, 1));
7348                }
7349                Err(e) => {
7350                    self.in_generator = false;
7351                    if *scope_started {
7352                        self.scope_pop_hook();
7353                        *scope_started = false;
7354                    }
7355                    return Err(match e {
7356                        FlowOrError::Error(ee) => ee,
7357                        FlowOrError::Flow(Flow::Yield(_)) => {
7358                            unreachable!("yield handled above")
7359                        }
7360                        FlowOrError::Flow(flow) => StrykeError::runtime(
7361                            format!("unexpected control flow in generator: {:?}", flow),
7362                            0,
7363                        ),
7364                    });
7365                }
7366            }
7367        }
7368        self.in_generator = false;
7369        if *scope_started {
7370            self.scope_pop_hook();
7371            *scope_started = false;
7372        }
7373        *exhausted = true;
7374        Ok(pair(StrykeValue::UNDEF, 0))
7375    }
7376
7377    fn match_pattern_try(
7378        &mut self,
7379        subject: &StrykeValue,
7380        pattern: &MatchPattern,
7381        line: usize,
7382    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7383        match pattern {
7384            MatchPattern::Any => Ok(Some(vec![])),
7385            MatchPattern::Regex { .. } => {
7386                unreachable!("regex arms are handled in eval_algebraic_match")
7387            }
7388            MatchPattern::Value(expr) => {
7389                if self.match_pattern_value_alternation(subject, expr, line)? {
7390                    Ok(Some(vec![]))
7391                } else {
7392                    Ok(None)
7393                }
7394            }
7395            MatchPattern::Array(elems) => {
7396                let Some(arr) = self.match_subject_as_array(subject) else {
7397                    return Ok(None);
7398                };
7399                self.match_array_pattern_elems(&arr, elems, line)
7400            }
7401            MatchPattern::Hash(pairs) => {
7402                let Some(h) = self.match_subject_as_hash(subject) else {
7403                    return Ok(None);
7404                };
7405                self.match_hash_pattern_pairs(&h, pairs, line)
7406            }
7407            MatchPattern::OptionSome(name) => {
7408                let Some(arr) = self.match_subject_as_array(subject) else {
7409                    return Ok(None);
7410                };
7411                if arr.len() < 2 {
7412                    return Ok(None);
7413                }
7414                if !arr[1].is_true() {
7415                    return Ok(None);
7416                }
7417                Ok(Some(vec![PatternBinding::Scalar(
7418                    name.clone(),
7419                    arr[0].clone(),
7420                )]))
7421            }
7422        }
7423    }
7424
7425    /// Handle pattern alternation (e.g., `"foo" | "bar" | "baz"`) in match patterns.
7426    /// If the expression is a BitOr chain, recursively check if subject matches any alternative.
7427    fn match_pattern_value_alternation(
7428        &mut self,
7429        subject: &StrykeValue,
7430        expr: &Expr,
7431        _line: usize,
7432    ) -> Result<bool, FlowOrError> {
7433        if let ExprKind::BinOp {
7434            left,
7435            op: BinOp::BitOr,
7436            right,
7437        } = &expr.kind
7438        {
7439            if self.match_pattern_value_alternation(subject, left, _line)? {
7440                return Ok(true);
7441            }
7442            return self.match_pattern_value_alternation(subject, right, _line);
7443        }
7444        let pv = self.eval_expr(expr)?;
7445        Ok(self.smartmatch_when(subject, &pv))
7446    }
7447
7448    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
7449    fn match_subject_as_array(&self, v: &StrykeValue) -> Option<Vec<StrykeValue>> {
7450        if let Some(a) = v.as_array_vec() {
7451            return Some(a);
7452        }
7453        if let Some(r) = v.as_array_ref() {
7454            return Some(r.read().clone());
7455        }
7456        if let Some(name) = v.as_array_binding_name() {
7457            return Some(self.scope.get_array(&name));
7458        }
7459        None
7460    }
7461
7462    fn match_subject_as_hash(&mut self, v: &StrykeValue) -> Option<IndexMap<String, StrykeValue>> {
7463        if let Some(h) = v.as_hash_map() {
7464            return Some(h);
7465        }
7466        if let Some(r) = v.as_hash_ref() {
7467            return Some(r.read().clone());
7468        }
7469        if let Some(name) = v.as_hash_binding_name() {
7470            self.touch_env_hash(&name);
7471            return Some(self.scope.get_hash(&name));
7472        }
7473        None
7474    }
7475
7476    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
7477    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
7478    pub(crate) fn hash_slice_deref_values(
7479        &mut self,
7480        container: &StrykeValue,
7481        key_values: &[StrykeValue],
7482        line: usize,
7483    ) -> Result<StrykeValue, FlowOrError> {
7484        let h = if let Some(m) = self.match_subject_as_hash(container) {
7485            m
7486        } else {
7487            return Err(StrykeError::runtime(
7488                "Hash slice dereference needs a hash or hash reference value",
7489                line,
7490            )
7491            .into());
7492        };
7493        let mut result = Vec::new();
7494        for kv in key_values {
7495            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
7496                vv.iter().map(|x| x.to_string()).collect()
7497            } else {
7498                vec![kv.to_string()]
7499            };
7500            for k in key_strings {
7501                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
7502            }
7503        }
7504        Ok(StrykeValue::array(result))
7505    }
7506
7507    /// Single-key write for a hash slice container (hash ref or package hash name).
7508    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
7509    pub(crate) fn assign_hash_slice_one_key(
7510        &mut self,
7511        container: StrykeValue,
7512        key: &str,
7513        val: StrykeValue,
7514        line: usize,
7515    ) -> Result<StrykeValue, FlowOrError> {
7516        if let Some(r) = container.as_hash_ref() {
7517            r.write().insert(key.to_string(), val);
7518            return Ok(StrykeValue::UNDEF);
7519        }
7520        if let Some(name) = container.as_hash_binding_name() {
7521            self.touch_env_hash(&name);
7522            self.scope
7523                .set_hash_element(&name, key, val)
7524                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7525            return Ok(StrykeValue::UNDEF);
7526        }
7527        if let Some(s) = container.as_str() {
7528            self.touch_env_hash(&s);
7529            if self.strict_refs {
7530                return Err(StrykeError::runtime(
7531                    format!(
7532                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7533                        s
7534                    ),
7535                    line,
7536                )
7537                .into());
7538            }
7539            self.scope
7540                .set_hash_element(&s, key, val)
7541                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7542            return Ok(StrykeValue::UNDEF);
7543        }
7544        Err(StrykeError::runtime(
7545            "Hash slice assignment needs a hash or hash reference value",
7546            line,
7547        )
7548        .into())
7549    }
7550
7551    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
7552    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
7553    pub(crate) fn assign_named_hash_slice(
7554        &mut self,
7555        hash: &str,
7556        key_values: Vec<StrykeValue>,
7557        val: StrykeValue,
7558        line: usize,
7559    ) -> Result<StrykeValue, FlowOrError> {
7560        self.touch_env_hash(hash);
7561        let mut ks: Vec<String> = Vec::new();
7562        for kv in key_values {
7563            if let Some(vv) = kv.as_array_vec() {
7564                ks.extend(vv.iter().map(|x| x.to_string()));
7565            } else {
7566                ks.push(kv.to_string());
7567            }
7568        }
7569        if ks.is_empty() {
7570            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7571        }
7572        let items = val.to_list();
7573        for (i, k) in ks.iter().enumerate() {
7574            let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7575            self.scope
7576                .set_hash_element(hash, k, v)
7577                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7578        }
7579        Ok(StrykeValue::UNDEF)
7580    }
7581
7582    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
7583    pub(crate) fn assign_hash_slice_deref(
7584        &mut self,
7585        container: StrykeValue,
7586        key_values: Vec<StrykeValue>,
7587        val: StrykeValue,
7588        line: usize,
7589    ) -> Result<StrykeValue, FlowOrError> {
7590        let mut ks: Vec<String> = Vec::new();
7591        for kv in key_values {
7592            if let Some(vv) = kv.as_array_vec() {
7593                ks.extend(vv.iter().map(|x| x.to_string()));
7594            } else {
7595                ks.push(kv.to_string());
7596            }
7597        }
7598        if ks.is_empty() {
7599            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7600        }
7601        let items = val.to_list();
7602        if let Some(r) = container.as_hash_ref() {
7603            let mut h = r.write();
7604            for (i, k) in ks.iter().enumerate() {
7605                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7606                h.insert(k.clone(), v);
7607            }
7608            return Ok(StrykeValue::UNDEF);
7609        }
7610        if let Some(name) = container.as_hash_binding_name() {
7611            self.touch_env_hash(&name);
7612            for (i, k) in ks.iter().enumerate() {
7613                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7614                self.scope
7615                    .set_hash_element(&name, k, v)
7616                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7617            }
7618            return Ok(StrykeValue::UNDEF);
7619        }
7620        if let Some(s) = container.as_str() {
7621            if self.strict_refs {
7622                return Err(StrykeError::runtime(
7623                    format!(
7624                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7625                        s
7626                    ),
7627                    line,
7628                )
7629                .into());
7630            }
7631            self.touch_env_hash(&s);
7632            for (i, k) in ks.iter().enumerate() {
7633                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7634                self.scope
7635                    .set_hash_element(&s, k, v)
7636                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7637            }
7638            return Ok(StrykeValue::UNDEF);
7639        }
7640        Err(StrykeError::runtime(
7641            "Hash slice assignment needs a hash or hash reference value",
7642            line,
7643        )
7644        .into())
7645    }
7646
7647    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
7648    /// Perl 5 applies the compound op only to the **last** slice element.
7649    pub(crate) fn compound_assign_hash_slice_deref(
7650        &mut self,
7651        container: StrykeValue,
7652        key_values: Vec<StrykeValue>,
7653        op: BinOp,
7654        rhs: StrykeValue,
7655        line: usize,
7656    ) -> Result<StrykeValue, FlowOrError> {
7657        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7658        let last_old = old_list
7659            .to_list()
7660            .last()
7661            .cloned()
7662            .unwrap_or(StrykeValue::UNDEF);
7663        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7664        let mut ks: Vec<String> = Vec::new();
7665        for kv in &key_values {
7666            if let Some(vv) = kv.as_array_vec() {
7667                ks.extend(vv.iter().map(|x| x.to_string()));
7668            } else {
7669                ks.push(kv.to_string());
7670            }
7671        }
7672        if ks.is_empty() {
7673            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7674        }
7675        let last_key = ks.last().expect("non-empty ks");
7676        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7677        Ok(new_val)
7678    }
7679
7680    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
7681    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
7682    /// the **old** value of that last element.
7683    ///
7684    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
7685    pub(crate) fn hash_slice_deref_inc_dec(
7686        &mut self,
7687        container: StrykeValue,
7688        key_values: Vec<StrykeValue>,
7689        kind: u8,
7690        line: usize,
7691    ) -> Result<StrykeValue, FlowOrError> {
7692        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7693        let last_old = old_list
7694            .to_list()
7695            .last()
7696            .cloned()
7697            .unwrap_or(StrykeValue::UNDEF);
7698        let new_val = if kind & 1 == 0 {
7699            StrykeValue::integer(last_old.to_int() + 1)
7700        } else {
7701            StrykeValue::integer(last_old.to_int() - 1)
7702        };
7703        let mut ks: Vec<String> = Vec::new();
7704        for kv in &key_values {
7705            if let Some(vv) = kv.as_array_vec() {
7706                ks.extend(vv.iter().map(|x| x.to_string()));
7707            } else {
7708                ks.push(kv.to_string());
7709            }
7710        }
7711        let last_key = ks.last().ok_or_else(|| {
7712            StrykeError::runtime("Hash slice increment needs at least one key", line)
7713        })?;
7714        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7715        Ok(if kind < 2 { new_val } else { last_old })
7716    }
7717
7718    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[StrykeValue]) -> StrykeValue {
7719        self.touch_env_hash(hash);
7720        let h = self.scope.get_hash(hash);
7721        let mut result = Vec::new();
7722        for kv in key_values {
7723            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
7724                vv.iter().map(|x| x.to_string()).collect()
7725            } else {
7726                vec![kv.to_string()]
7727            };
7728            for k in key_strings {
7729                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
7730            }
7731        }
7732        StrykeValue::array(result)
7733    }
7734
7735    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
7736    pub(crate) fn compound_assign_named_hash_slice(
7737        &mut self,
7738        hash: &str,
7739        key_values: Vec<StrykeValue>,
7740        op: BinOp,
7741        rhs: StrykeValue,
7742        line: usize,
7743    ) -> Result<StrykeValue, FlowOrError> {
7744        let old_list = self.hash_slice_named_values(hash, &key_values);
7745        let last_old = old_list
7746            .to_list()
7747            .last()
7748            .cloned()
7749            .unwrap_or(StrykeValue::UNDEF);
7750        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7751        let mut ks: Vec<String> = Vec::new();
7752        for kv in &key_values {
7753            if let Some(vv) = kv.as_array_vec() {
7754                ks.extend(vv.iter().map(|x| x.to_string()));
7755            } else {
7756                ks.push(kv.to_string());
7757            }
7758        }
7759        if ks.is_empty() {
7760            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7761        }
7762        let last_key = ks.last().expect("non-empty ks");
7763        let container = StrykeValue::string(hash.to_string());
7764        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7765        Ok(new_val)
7766    }
7767
7768    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
7769    pub(crate) fn named_hash_slice_inc_dec(
7770        &mut self,
7771        hash: &str,
7772        key_values: Vec<StrykeValue>,
7773        kind: u8,
7774        line: usize,
7775    ) -> Result<StrykeValue, FlowOrError> {
7776        let old_list = self.hash_slice_named_values(hash, &key_values);
7777        let last_old = old_list
7778            .to_list()
7779            .last()
7780            .cloned()
7781            .unwrap_or(StrykeValue::UNDEF);
7782        let new_val = if kind & 1 == 0 {
7783            StrykeValue::integer(last_old.to_int() + 1)
7784        } else {
7785            StrykeValue::integer(last_old.to_int() - 1)
7786        };
7787        let mut ks: Vec<String> = Vec::new();
7788        for kv in &key_values {
7789            if let Some(vv) = kv.as_array_vec() {
7790                ks.extend(vv.iter().map(|x| x.to_string()));
7791            } else {
7792                ks.push(kv.to_string());
7793            }
7794        }
7795        let last_key = ks.last().ok_or_else(|| {
7796            StrykeError::runtime("Hash slice increment needs at least one key", line)
7797        })?;
7798        let container = StrykeValue::string(hash.to_string());
7799        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7800        Ok(if kind < 2 { new_val } else { last_old })
7801    }
7802
7803    fn match_array_pattern_elems(
7804        &mut self,
7805        arr: &[StrykeValue],
7806        elems: &[MatchArrayElem],
7807        line: usize,
7808    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7809        let has_rest = elems
7810            .iter()
7811            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
7812        let mut binds: Vec<PatternBinding> = Vec::new();
7813        let mut idx = 0usize;
7814        for (i, elem) in elems.iter().enumerate() {
7815            match elem {
7816                MatchArrayElem::Rest => {
7817                    if i != elems.len() - 1 {
7818                        return Err(StrykeError::runtime(
7819                            "internal: `*` must be last in array match pattern",
7820                            line,
7821                        )
7822                        .into());
7823                    }
7824                    return Ok(Some(binds));
7825                }
7826                MatchArrayElem::RestBind(name) => {
7827                    if i != elems.len() - 1 {
7828                        return Err(StrykeError::runtime(
7829                            "internal: `@name` rest bind must be last in array match pattern",
7830                            line,
7831                        )
7832                        .into());
7833                    }
7834                    let tail = arr[idx..].to_vec();
7835                    binds.push(PatternBinding::Array(name.clone(), tail));
7836                    return Ok(Some(binds));
7837                }
7838                MatchArrayElem::CaptureScalar(name) => {
7839                    if idx >= arr.len() {
7840                        return Ok(None);
7841                    }
7842                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
7843                    idx += 1;
7844                }
7845                MatchArrayElem::Expr(e) => {
7846                    if idx >= arr.len() {
7847                        return Ok(None);
7848                    }
7849                    let expected = self.eval_expr(e)?;
7850                    if !self.smartmatch_when(&arr[idx], &expected) {
7851                        return Ok(None);
7852                    }
7853                    idx += 1;
7854                }
7855            }
7856        }
7857        if !has_rest && idx != arr.len() {
7858            return Ok(None);
7859        }
7860        Ok(Some(binds))
7861    }
7862
7863    fn match_hash_pattern_pairs(
7864        &mut self,
7865        h: &IndexMap<String, StrykeValue>,
7866        pairs: &[MatchHashPair],
7867        _line: usize,
7868    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7869        let mut binds = Vec::new();
7870        for pair in pairs {
7871            match pair {
7872                MatchHashPair::KeyOnly { key } => {
7873                    let ks = self.eval_expr(key)?.to_string();
7874                    if !h.contains_key(&ks) {
7875                        return Ok(None);
7876                    }
7877                }
7878                MatchHashPair::Capture { key, name } => {
7879                    let ks = self.eval_expr(key)?.to_string();
7880                    let Some(v) = h.get(&ks) else {
7881                        return Ok(None);
7882                    };
7883                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
7884                }
7885            }
7886        }
7887        Ok(Some(binds))
7888    }
7889
7890    /// Check if a block declares variables (needs its own scope frame).
7891    #[inline]
7892    fn block_needs_scope(block: &Block) -> bool {
7893        block.iter().any(|s| match &s.kind {
7894            StmtKind::My(_)
7895            | StmtKind::Our(_)
7896            | StmtKind::Local(_)
7897            | StmtKind::State(_)
7898            | StmtKind::LocalExpr { .. } => true,
7899            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
7900            _ => false,
7901        })
7902    }
7903
7904    /// Execute block, only pushing a scope frame if needed.
7905    #[inline]
7906    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
7907        if Self::block_needs_scope(block) {
7908            self.exec_block(block)
7909        } else {
7910            self.exec_block_no_scope(block)
7911        }
7912    }
7913
7914    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
7915        let t0 = self.profiler.is_some().then(std::time::Instant::now);
7916        let r = self.exec_statement_inner(stmt);
7917        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
7918            prof.on_line(&self.file, stmt.line, t0.elapsed());
7919        }
7920        r
7921    }
7922
7923    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
7924        if let Err(e) = crate::perl_signal::poll(self) {
7925            return Err(FlowOrError::Error(e));
7926        }
7927        if let Err(e) = self.drain_pending_destroys(stmt.line) {
7928            return Err(FlowOrError::Error(e));
7929        }
7930        match &stmt.kind {
7931            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
7932            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
7933            StmtKind::If {
7934                condition,
7935                body,
7936                elsifs,
7937                else_block,
7938            } => {
7939                if self.eval_boolean_rvalue_condition(condition)? {
7940                    return self.exec_block(body);
7941                }
7942                for (c, b) in elsifs {
7943                    if self.eval_boolean_rvalue_condition(c)? {
7944                        return self.exec_block(b);
7945                    }
7946                }
7947                if let Some(eb) = else_block {
7948                    return self.exec_block(eb);
7949                }
7950                Ok(StrykeValue::UNDEF)
7951            }
7952            StmtKind::Unless {
7953                condition,
7954                body,
7955                else_block,
7956            } => {
7957                if !self.eval_boolean_rvalue_condition(condition)? {
7958                    return self.exec_block(body);
7959                }
7960                if let Some(eb) = else_block {
7961                    return self.exec_block(eb);
7962                }
7963                Ok(StrykeValue::UNDEF)
7964            }
7965            StmtKind::While {
7966                condition,
7967                body,
7968                label,
7969                continue_block,
7970            } => {
7971                'outer: loop {
7972                    if !self.eval_boolean_rvalue_condition(condition)? {
7973                        break;
7974                    }
7975                    'inner: loop {
7976                        match self.exec_block_smart(body) {
7977                            Ok(_) => break 'inner,
7978                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7979                                if l == label || l.is_none() =>
7980                            {
7981                                break 'outer;
7982                            }
7983                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7984                                if l == label || l.is_none() =>
7985                            {
7986                                if let Some(cb) = continue_block {
7987                                    let _ = self.exec_block_smart(cb);
7988                                }
7989                                continue 'outer;
7990                            }
7991                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7992                                if l == label || l.is_none() =>
7993                            {
7994                                continue 'inner;
7995                            }
7996                            Err(e) => return Err(e),
7997                        }
7998                    }
7999                    if let Some(cb) = continue_block {
8000                        let _ = self.exec_block_smart(cb);
8001                    }
8002                }
8003                Ok(StrykeValue::UNDEF)
8004            }
8005            StmtKind::Until {
8006                condition,
8007                body,
8008                label,
8009                continue_block,
8010            } => {
8011                'outer: loop {
8012                    if self.eval_boolean_rvalue_condition(condition)? {
8013                        break;
8014                    }
8015                    'inner: loop {
8016                        match self.exec_block(body) {
8017                            Ok(_) => break 'inner,
8018                            Err(FlowOrError::Flow(Flow::Last(ref l)))
8019                                if l == label || l.is_none() =>
8020                            {
8021                                break 'outer;
8022                            }
8023                            Err(FlowOrError::Flow(Flow::Next(ref l)))
8024                                if l == label || l.is_none() =>
8025                            {
8026                                if let Some(cb) = continue_block {
8027                                    let _ = self.exec_block_smart(cb);
8028                                }
8029                                continue 'outer;
8030                            }
8031                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
8032                                if l == label || l.is_none() =>
8033                            {
8034                                continue 'inner;
8035                            }
8036                            Err(e) => return Err(e),
8037                        }
8038                    }
8039                    if let Some(cb) = continue_block {
8040                        let _ = self.exec_block_smart(cb);
8041                    }
8042                }
8043                Ok(StrykeValue::UNDEF)
8044            }
8045            StmtKind::DoWhile { body, condition } => {
8046                loop {
8047                    self.exec_block(body)?;
8048                    if !self.eval_boolean_rvalue_condition(condition)? {
8049                        break;
8050                    }
8051                }
8052                Ok(StrykeValue::UNDEF)
8053            }
8054            StmtKind::For {
8055                init,
8056                condition,
8057                step,
8058                body,
8059                label,
8060                continue_block,
8061            } => {
8062                self.scope_push_hook();
8063                if let Some(init) = init {
8064                    self.exec_statement(init)?;
8065                }
8066                'outer: loop {
8067                    if let Some(cond) = condition {
8068                        if !self.eval_boolean_rvalue_condition(cond)? {
8069                            break;
8070                        }
8071                    }
8072                    'inner: loop {
8073                        match self.exec_block_smart(body) {
8074                            Ok(_) => break 'inner,
8075                            Err(FlowOrError::Flow(Flow::Last(ref l)))
8076                                if l == label || l.is_none() =>
8077                            {
8078                                break 'outer;
8079                            }
8080                            Err(FlowOrError::Flow(Flow::Next(ref l)))
8081                                if l == label || l.is_none() =>
8082                            {
8083                                if let Some(cb) = continue_block {
8084                                    let _ = self.exec_block_smart(cb);
8085                                }
8086                                if let Some(step) = step {
8087                                    self.eval_expr(step)?;
8088                                }
8089                                continue 'outer;
8090                            }
8091                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
8092                                if l == label || l.is_none() =>
8093                            {
8094                                continue 'inner;
8095                            }
8096                            Err(e) => {
8097                                self.scope_pop_hook();
8098                                return Err(e);
8099                            }
8100                        }
8101                    }
8102                    if let Some(cb) = continue_block {
8103                        let _ = self.exec_block_smart(cb);
8104                    }
8105                    if let Some(step) = step {
8106                        self.eval_expr(step)?;
8107                    }
8108                }
8109                self.scope_pop_hook();
8110                Ok(StrykeValue::UNDEF)
8111            }
8112            StmtKind::Foreach {
8113                var,
8114                list,
8115                body,
8116                label,
8117                continue_block,
8118            } => {
8119                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
8120                // Lazy iterator special-case: drive `next_item()` without
8121                // calling `to_list()`, which would materialise every value
8122                // into a `Vec` and defeat the streaming promise of producers
8123                // like `ingest` / `FsWalkIterator`. Existing producers were
8124                // already lazy when piped to `|> e`; this extends streaming
8125                // to plain `for my $x (iter)` loops without changing any
8126                // other type's behaviour.
8127                if list_val.is_iterator() {
8128                    let iter = list_val.into_iterator();
8129                    self.scope_push_hook();
8130                    self.scope.declare_scalar(var, StrykeValue::UNDEF);
8131                    self.english_note_lexical_scalar(var);
8132                    'outer_iter: loop {
8133                        let next = match iter.next_item() {
8134                            Some(v) => v,
8135                            None => break 'outer_iter,
8136                        };
8137                        if var == "_" {
8138                            self.scope.set_topic(next);
8139                        } else {
8140                            self.scope
8141                                .set_scalar(var, next)
8142                                .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
8143                        }
8144                        'inner_iter: loop {
8145                            match self.exec_block_smart(body) {
8146                                Ok(_) => break 'inner_iter,
8147                                Err(FlowOrError::Flow(Flow::Last(ref l)))
8148                                    if l == label || l.is_none() =>
8149                                {
8150                                    break 'outer_iter;
8151                                }
8152                                Err(FlowOrError::Flow(Flow::Next(ref l)))
8153                                    if l == label || l.is_none() =>
8154                                {
8155                                    if let Some(cb) = continue_block {
8156                                        let _ = self.exec_block_smart(cb);
8157                                    }
8158                                    continue 'outer_iter;
8159                                }
8160                                Err(FlowOrError::Flow(Flow::Redo(ref l)))
8161                                    if l == label || l.is_none() =>
8162                                {
8163                                    continue 'inner_iter;
8164                                }
8165                                Err(e) => {
8166                                    self.scope_pop_hook();
8167                                    return Err(e);
8168                                }
8169                            }
8170                        }
8171                        if let Some(cb) = continue_block {
8172                            let _ = self.exec_block_smart(cb);
8173                        }
8174                    }
8175                    self.scope_pop_hook();
8176                    return Ok(StrykeValue::UNDEF);
8177                }
8178                let items = list_val.to_list();
8179                self.scope_push_hook();
8180                self.scope.declare_scalar(var, StrykeValue::UNDEF);
8181                self.english_note_lexical_scalar(var);
8182                let mut i = 0usize;
8183                'outer: while i < items.len() {
8184                    // For the implicit topic loop (`for (@list) { ... }`,
8185                    // var=="_"), use `set_topic` so `$_`, `$_0`, `_`, `_0`
8186                    // all alias the iter value. Plain `set_scalar("_", ...)`
8187                    // only updates `$_` and leaves `$_0` undef, violating
8188                    // the four-way aliasing invariant. Explicit named loops
8189                    // (`for my $x (@list)`) keep the simple scalar binding.
8190                    if var == "_" {
8191                        self.scope.set_topic(items[i].clone());
8192                    } else {
8193                        self.scope
8194                            .set_scalar(var, items[i].clone())
8195                            .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
8196                    }
8197                    'inner: loop {
8198                        match self.exec_block_smart(body) {
8199                            Ok(_) => break 'inner,
8200                            Err(FlowOrError::Flow(Flow::Last(ref l)))
8201                                if l == label || l.is_none() =>
8202                            {
8203                                break 'outer;
8204                            }
8205                            Err(FlowOrError::Flow(Flow::Next(ref l)))
8206                                if l == label || l.is_none() =>
8207                            {
8208                                if let Some(cb) = continue_block {
8209                                    let _ = self.exec_block_smart(cb);
8210                                }
8211                                i += 1;
8212                                continue 'outer;
8213                            }
8214                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
8215                                if l == label || l.is_none() =>
8216                            {
8217                                continue 'inner;
8218                            }
8219                            Err(e) => {
8220                                self.scope_pop_hook();
8221                                return Err(e);
8222                            }
8223                        }
8224                    }
8225                    if let Some(cb) = continue_block {
8226                        let _ = self.exec_block_smart(cb);
8227                    }
8228                    i += 1;
8229                }
8230                self.scope_pop_hook();
8231                Ok(StrykeValue::UNDEF)
8232            }
8233            StmtKind::SubDecl {
8234                name,
8235                params,
8236                body,
8237                prototype,
8238            } => {
8239                let key = self.qualify_sub_key(name);
8240                let captured = self.scope.capture();
8241                let closure_env = if captured.is_empty() {
8242                    None
8243                } else {
8244                    Some(captured)
8245                };
8246                let mut sub = StrykeSub {
8247                    name: name.clone(),
8248                    params: params.clone(),
8249                    body: body.clone(),
8250                    closure_env,
8251                    prototype: prototype.clone(),
8252                    fib_like: None,
8253                };
8254                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
8255                self.subs.insert(key, Arc::new(sub));
8256                Ok(StrykeValue::UNDEF)
8257            }
8258            StmtKind::StructDecl { def } => {
8259                if self.struct_defs.contains_key(&def.name) {
8260                    return Err(StrykeError::runtime(
8261                        format!("duplicate struct `{}`", def.name),
8262                        stmt.line,
8263                    )
8264                    .into());
8265                }
8266                self.struct_defs
8267                    .insert(def.name.clone(), Arc::new(def.clone()));
8268                Ok(StrykeValue::UNDEF)
8269            }
8270            StmtKind::EnumDecl { def } => {
8271                if self.enum_defs.contains_key(&def.name) {
8272                    return Err(StrykeError::runtime(
8273                        format!("duplicate enum `{}`", def.name),
8274                        stmt.line,
8275                    )
8276                    .into());
8277                }
8278                self.enum_defs
8279                    .insert(def.name.clone(), Arc::new(def.clone()));
8280                Ok(StrykeValue::UNDEF)
8281            }
8282            StmtKind::ClassDecl { def } => {
8283                if self.class_defs.contains_key(&def.name) {
8284                    return Err(StrykeError::runtime(
8285                        format!("duplicate class `{}`", def.name),
8286                        stmt.line,
8287                    )
8288                    .into());
8289                }
8290                // Final class enforcement: prevent subclassing
8291                for parent_name in &def.extends {
8292                    if let Some(parent_def) = self.class_defs.get(parent_name) {
8293                        if parent_def.is_final {
8294                            return Err(StrykeError::runtime(
8295                                format!("cannot extend final class `{}`", parent_name),
8296                                stmt.line,
8297                            )
8298                            .into());
8299                        }
8300                        // Final method enforcement: prevent overriding
8301                        for m in &def.methods {
8302                            if let Some(parent_method) = parent_def.method(&m.name) {
8303                                if parent_method.is_final {
8304                                    return Err(StrykeError::runtime(
8305                                        format!(
8306                                            "cannot override final method `{}` from class `{}`",
8307                                            m.name, parent_name
8308                                        ),
8309                                        stmt.line,
8310                                    )
8311                                    .into());
8312                                }
8313                            }
8314                        }
8315                    }
8316                }
8317                // Trait contract enforcement + default method inheritance
8318                let mut def = def.clone();
8319                for trait_name in &def.implements.clone() {
8320                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
8321                        for required in trait_def.required_methods() {
8322                            let has_method = def.methods.iter().any(|m| m.name == required.name);
8323                            if !has_method {
8324                                return Err(StrykeError::runtime(
8325                                    format!(
8326                                        "class `{}` implements trait `{}` but does not define required method `{}`",
8327                                        def.name, trait_name, required.name
8328                                    ),
8329                                    stmt.line,
8330                                )
8331                                .into());
8332                            }
8333                        }
8334                        // Inherit default methods from trait (methods with bodies)
8335                        for tm in &trait_def.methods {
8336                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
8337                                def.methods.push(tm.clone());
8338                            }
8339                        }
8340                    }
8341                }
8342                // Abstract method enforcement: concrete subclasses must implement
8343                // all abstract methods (body-less methods) from abstract parents
8344                if !def.is_abstract {
8345                    for parent_name in &def.extends.clone() {
8346                        if let Some(parent_def) = self.class_defs.get(parent_name) {
8347                            if parent_def.is_abstract {
8348                                for m in &parent_def.methods {
8349                                    if m.body.is_none()
8350                                        && !def.methods.iter().any(|dm| dm.name == m.name)
8351                                    {
8352                                        return Err(StrykeError::runtime(
8353                                            format!(
8354                                                "class `{}` must implement abstract method `{}` from `{}`",
8355                                                def.name, m.name, parent_name
8356                                            ),
8357                                            stmt.line,
8358                                        )
8359                                        .into());
8360                                    }
8361                                }
8362                            }
8363                        }
8364                    }
8365                }
8366                // Initialize static fields
8367                for sf in &def.static_fields {
8368                    let val = if let Some(ref expr) = sf.default {
8369                        self.eval_expr(expr)?
8370                    } else {
8371                        StrykeValue::UNDEF
8372                    };
8373                    let key = format!("{}::{}", def.name, sf.name);
8374                    self.scope.declare_scalar(&key, val);
8375                }
8376                // Register class methods into self.subs so method dispatch finds them.
8377                for m in &def.methods {
8378                    if let Some(ref body) = m.body {
8379                        let fq = format!("{}::{}", def.name, m.name);
8380                        let sub = Arc::new(StrykeSub {
8381                            name: fq.clone(),
8382                            params: m.params.clone(),
8383                            body: body.clone(),
8384                            closure_env: None,
8385                            prototype: None,
8386                            fib_like: None,
8387                        });
8388                        self.subs.insert(fq, sub);
8389                    }
8390                }
8391                // Set @ClassName::ISA so MRO/isa resolution works.
8392                if !def.extends.is_empty() {
8393                    let isa_key = format!("{}::ISA", def.name);
8394                    let parents: Vec<StrykeValue> = def
8395                        .extends
8396                        .iter()
8397                        .map(|p| StrykeValue::string(p.clone()))
8398                        .collect();
8399                    self.scope.declare_array(&isa_key, parents);
8400                }
8401                let arc_def = Arc::new(def);
8402                self.class_defs
8403                    .insert(arc_def.name.clone(), Arc::clone(&arc_def));
8404                // Mirror the new class into the serializer-visible
8405                // thread-local registry so `to_json($obj)` etc. can walk
8406                // its inheritance chain.
8407                crate::serialize_normalize::register_class_def(arc_def);
8408                Ok(StrykeValue::UNDEF)
8409            }
8410            StmtKind::TraitDecl { def } => {
8411                if self.trait_defs.contains_key(&def.name) {
8412                    return Err(StrykeError::runtime(
8413                        format!("duplicate trait `{}`", def.name),
8414                        stmt.line,
8415                    )
8416                    .into());
8417                }
8418                self.trait_defs
8419                    .insert(def.name.clone(), Arc::new(def.clone()));
8420                Ok(StrykeValue::UNDEF)
8421            }
8422            StmtKind::My(decls) | StmtKind::Our(decls) => {
8423                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
8424                // For list assignment my ($a, $b) = (10, 20), distribute elements.
8425                // All decls share the same initializer in the AST (parser clones it).
8426                if decls.len() > 1 && decls[0].initializer.is_some() {
8427                    let val = self.eval_expr_ctx(
8428                        decls[0].initializer.as_ref().unwrap(),
8429                        WantarrayCtx::List,
8430                    )?;
8431                    let items = val.to_list();
8432                    let mut idx = 0;
8433                    for decl in decls {
8434                        match decl.sigil {
8435                            Sigil::Scalar => {
8436                                let v = items.get(idx).cloned().unwrap_or(StrykeValue::UNDEF);
8437                                let skey = if is_our {
8438                                    self.stash_scalar_name_for_package(&decl.name)
8439                                } else {
8440                                    decl.name.clone()
8441                                };
8442                                self.scope.declare_scalar_frozen(
8443                                    &skey,
8444                                    v,
8445                                    decl.frozen,
8446                                    decl.type_annotation.clone(),
8447                                )?;
8448                                self.english_note_lexical_scalar(&decl.name);
8449                                if is_our {
8450                                    self.note_our_scalar(&decl.name);
8451                                }
8452                                idx += 1;
8453                            }
8454                            Sigil::Array => {
8455                                // Array slurps remaining elements
8456                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8457                                idx = items.len();
8458                                if is_our {
8459                                    self.record_exporter_our_array_name(&decl.name, &rest);
8460                                }
8461                                let aname = if is_our {
8462                                    self.stash_array_full_name_for_package(&decl.name)
8463                                } else {
8464                                    self.stash_array_name_for_package(&decl.name)
8465                                };
8466                                self.scope.declare_array(&aname, rest);
8467                                if is_our {
8468                                    self.note_our_array(&decl.name);
8469                                }
8470                            }
8471                            Sigil::Hash => {
8472                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8473                                idx = items.len();
8474                                let mut map = IndexMap::new();
8475                                let mut i = 0;
8476                                while i + 1 < rest.len() {
8477                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
8478                                    i += 2;
8479                                }
8480                                let hname = if is_our {
8481                                    self.stash_hash_full_name_for_package(&decl.name)
8482                                } else {
8483                                    decl.name.clone()
8484                                };
8485                                self.scope.declare_hash(&hname, map);
8486                                if is_our {
8487                                    self.note_our_hash(&decl.name);
8488                                }
8489                            }
8490                            Sigil::Typeglob => {
8491                                return Err(StrykeError::runtime(
8492                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
8493                                    stmt.line,
8494                                )
8495                                .into());
8496                            }
8497                        }
8498                    }
8499                } else {
8500                    // Single decl or no initializer
8501                    for decl in decls {
8502                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
8503                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
8504                        // compound op reads the lhs (see system Exporter.pm).
8505                        let compound_init = decl
8506                            .initializer
8507                            .as_ref()
8508                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
8509
8510                        if compound_init {
8511                            match decl.sigil {
8512                                Sigil::Typeglob => {
8513                                    return Err(StrykeError::runtime(
8514                                        "compound assignment on typeglob declaration is not supported",
8515                                        stmt.line,
8516                                    )
8517                                    .into());
8518                                }
8519                                Sigil::Scalar => {
8520                                    let skey = if is_our {
8521                                        self.stash_scalar_name_for_package(&decl.name)
8522                                    } else {
8523                                        decl.name.clone()
8524                                    };
8525                                    self.scope.declare_scalar_frozen(
8526                                        &skey,
8527                                        StrykeValue::UNDEF,
8528                                        decl.frozen,
8529                                        decl.type_annotation.clone(),
8530                                    )?;
8531                                    self.english_note_lexical_scalar(&decl.name);
8532                                    if is_our {
8533                                        self.note_our_scalar(&decl.name);
8534                                    }
8535                                    let init = decl.initializer.as_ref().unwrap();
8536                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8537                                }
8538                                Sigil::Array => {
8539                                    let aname = if is_our {
8540                                        self.stash_array_full_name_for_package(&decl.name)
8541                                    } else {
8542                                        self.stash_array_name_for_package(&decl.name)
8543                                    };
8544                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
8545                                    if is_our {
8546                                        self.note_our_array(&decl.name);
8547                                    }
8548                                    let init = decl.initializer.as_ref().unwrap();
8549                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8550                                    if is_our {
8551                                        let items = self.scope.get_array(&aname);
8552                                        self.record_exporter_our_array_name(&decl.name, &items);
8553                                    }
8554                                }
8555                                Sigil::Hash => {
8556                                    let hname = if is_our {
8557                                        self.stash_hash_full_name_for_package(&decl.name)
8558                                    } else {
8559                                        decl.name.clone()
8560                                    };
8561                                    self.scope.declare_hash_frozen(
8562                                        &hname,
8563                                        IndexMap::new(),
8564                                        decl.frozen,
8565                                    );
8566                                    if is_our {
8567                                        self.note_our_hash(&decl.name);
8568                                    }
8569                                    let init = decl.initializer.as_ref().unwrap();
8570                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8571                                }
8572                            }
8573                            continue;
8574                        }
8575
8576                        let val = if let Some(init) = &decl.initializer {
8577                            let ctx = match decl.sigil {
8578                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
8579                                Sigil::Scalar if decl.list_context => WantarrayCtx::List,
8580                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
8581                            };
8582                            let v = self.eval_expr_ctx(init, ctx)?;
8583                            // my ($x) = @arr → extract first element from list
8584                            if decl.sigil == Sigil::Scalar && decl.list_context {
8585                                v.to_list().first().cloned().unwrap_or(StrykeValue::UNDEF)
8586                            } else {
8587                                v
8588                            }
8589                        } else {
8590                            StrykeValue::UNDEF
8591                        };
8592                        match decl.sigil {
8593                            Sigil::Typeglob => {
8594                                return Err(StrykeError::runtime(
8595                                    "`my *FH` / typeglob declaration is not supported",
8596                                    stmt.line,
8597                                )
8598                                .into());
8599                            }
8600                            Sigil::Scalar => {
8601                                let skey = if is_our {
8602                                    self.stash_scalar_name_for_package(&decl.name)
8603                                } else {
8604                                    decl.name.clone()
8605                                };
8606                                self.scope.declare_scalar_frozen(
8607                                    &skey,
8608                                    val,
8609                                    decl.frozen,
8610                                    decl.type_annotation.clone(),
8611                                )?;
8612                                self.english_note_lexical_scalar(&decl.name);
8613                                if is_our {
8614                                    self.note_our_scalar(&decl.name);
8615                                }
8616                            }
8617                            Sigil::Array => {
8618                                let items = val.to_list();
8619                                if is_our {
8620                                    self.record_exporter_our_array_name(&decl.name, &items);
8621                                }
8622                                let aname = if is_our {
8623                                    self.stash_array_full_name_for_package(&decl.name)
8624                                } else {
8625                                    self.stash_array_name_for_package(&decl.name)
8626                                };
8627                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
8628                                if is_our {
8629                                    self.note_our_array(&decl.name);
8630                                }
8631                            }
8632                            Sigil::Hash => {
8633                                let items = val.to_list();
8634                                let mut map = IndexMap::new();
8635                                let mut i = 0;
8636                                while i + 1 < items.len() {
8637                                    let k = items[i].to_string();
8638                                    let v = items[i + 1].clone();
8639                                    map.insert(k, v);
8640                                    i += 2;
8641                                }
8642                                let hname = if is_our {
8643                                    self.stash_hash_full_name_for_package(&decl.name)
8644                                } else {
8645                                    decl.name.clone()
8646                                };
8647                                self.scope.declare_hash_frozen(&hname, map, decl.frozen);
8648                                if is_our {
8649                                    self.note_our_hash(&decl.name);
8650                                }
8651                            }
8652                        }
8653                    }
8654                }
8655                Ok(StrykeValue::UNDEF)
8656            }
8657            StmtKind::State(decls) => {
8658                // `state` variables persist across subroutine calls.
8659                // Key by source line + name for uniqueness.
8660                for decl in decls {
8661                    let state_key = format!("{}:{}", stmt.line, decl.name);
8662                    match decl.sigil {
8663                        Sigil::Scalar => {
8664                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
8665                                // Already initialized — declare with persisted value
8666                                self.scope.declare_scalar(&decl.name, prev);
8667                            } else {
8668                                // First encounter — evaluate initializer
8669                                let val = if let Some(init) = &decl.initializer {
8670                                    self.eval_expr(init)?
8671                                } else {
8672                                    StrykeValue::UNDEF
8673                                };
8674                                self.state_vars.insert(state_key.clone(), val.clone());
8675                                self.scope.declare_scalar(&decl.name, val);
8676                            }
8677                            // Register for save-back when scope pops
8678                            if let Some(frame) = self.state_bindings_stack.last_mut() {
8679                                frame.push((decl.name.clone(), state_key));
8680                            }
8681                        }
8682                        _ => {
8683                            // For arrays/hashes, fall back to simple my-like behavior
8684                            let val = if let Some(init) = &decl.initializer {
8685                                self.eval_expr(init)?
8686                            } else {
8687                                StrykeValue::UNDEF
8688                            };
8689                            match decl.sigil {
8690                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
8691                                Sigil::Hash => {
8692                                    let items = val.to_list();
8693                                    let mut map = IndexMap::new();
8694                                    let mut i = 0;
8695                                    while i + 1 < items.len() {
8696                                        map.insert(items[i].to_string(), items[i + 1].clone());
8697                                        i += 2;
8698                                    }
8699                                    self.scope.declare_hash(&decl.name, map);
8700                                }
8701                                _ => {}
8702                            }
8703                        }
8704                    }
8705                }
8706                Ok(StrykeValue::UNDEF)
8707            }
8708            StmtKind::Local(decls) => {
8709                if decls.len() > 1 && decls[0].initializer.is_some() {
8710                    let val = self.eval_expr_ctx(
8711                        decls[0].initializer.as_ref().unwrap(),
8712                        WantarrayCtx::List,
8713                    )?;
8714                    let items = val.to_list();
8715                    let mut idx = 0;
8716                    for decl in decls {
8717                        match decl.sigil {
8718                            Sigil::Scalar => {
8719                                let v = items.get(idx).cloned().unwrap_or(StrykeValue::UNDEF);
8720                                idx += 1;
8721                                self.scope.local_set_scalar(&decl.name, v)?;
8722                            }
8723                            Sigil::Array => {
8724                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8725                                idx = items.len();
8726                                self.scope.local_set_array(&decl.name, rest)?;
8727                            }
8728                            Sigil::Hash => {
8729                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8730                                idx = items.len();
8731                                if decl.name == "ENV" {
8732                                    self.materialize_env_if_needed();
8733                                }
8734                                let mut map = IndexMap::new();
8735                                let mut i = 0;
8736                                while i + 1 < rest.len() {
8737                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
8738                                    i += 2;
8739                                }
8740                                self.scope.local_set_hash(&decl.name, map)?;
8741                            }
8742                            Sigil::Typeglob => {
8743                                return Err(StrykeError::runtime(
8744                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
8745                                    stmt.line,
8746                                )
8747                                .into());
8748                            }
8749                        }
8750                    }
8751                    Ok(val)
8752                } else {
8753                    let mut last_val = StrykeValue::UNDEF;
8754                    for decl in decls {
8755                        let val = if let Some(init) = &decl.initializer {
8756                            let ctx = match decl.sigil {
8757                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
8758                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
8759                            };
8760                            self.eval_expr_ctx(init, ctx)?
8761                        } else {
8762                            StrykeValue::UNDEF
8763                        };
8764                        last_val = val.clone();
8765                        match decl.sigil {
8766                            Sigil::Typeglob => {
8767                                let old = self.glob_handle_alias.remove(&decl.name);
8768                                if let Some(frame) = self.glob_restore_frames.last_mut() {
8769                                    frame.push((decl.name.clone(), old));
8770                                }
8771                                if let Some(init) = &decl.initializer {
8772                                    if let ExprKind::Typeglob(rhs) = &init.kind {
8773                                        self.glob_handle_alias
8774                                            .insert(decl.name.clone(), rhs.clone());
8775                                    } else {
8776                                        return Err(StrykeError::runtime(
8777                                            "local *GLOB = *OTHER — right side must be a typeglob",
8778                                            stmt.line,
8779                                        )
8780                                        .into());
8781                                    }
8782                                }
8783                            }
8784                            Sigil::Scalar => {
8785                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
8786                                // must update the interpreter's backing field too — these are
8787                                // not stored in `Scope`. Save the prior value for restoration
8788                                // on `scope_pop_hook` so the block-exit restore is visible to
8789                                // print/I/O code.
8790                                if Self::is_special_scalar_name_for_set(&decl.name) {
8791                                    let old = self.get_special_var(&decl.name);
8792                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
8793                                    {
8794                                        frame.push((decl.name.clone(), old));
8795                                    }
8796                                    self.set_special_var(&decl.name, &val)
8797                                        .map_err(|e| e.at_line(stmt.line))?;
8798                                }
8799                                self.scope.local_set_scalar(&decl.name, val)?;
8800                            }
8801                            Sigil::Array => {
8802                                self.scope.local_set_array(&decl.name, val.to_list())?;
8803                            }
8804                            Sigil::Hash => {
8805                                if decl.name == "ENV" {
8806                                    self.materialize_env_if_needed();
8807                                }
8808                                let items = val.to_list();
8809                                let mut map = IndexMap::new();
8810                                let mut i = 0;
8811                                while i + 1 < items.len() {
8812                                    let k = items[i].to_string();
8813                                    let v = items[i + 1].clone();
8814                                    map.insert(k, v);
8815                                    i += 2;
8816                                }
8817                                self.scope.local_set_hash(&decl.name, map)?;
8818                            }
8819                        }
8820                    }
8821                    Ok(last_val)
8822                }
8823            }
8824            StmtKind::LocalExpr {
8825                target,
8826                initializer,
8827            } => {
8828                let rhs_name = |init: &Expr| -> StrykeResult<Option<String>> {
8829                    match &init.kind {
8830                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
8831                        _ => Err(StrykeError::runtime(
8832                            "local *GLOB = *OTHER — right side must be a typeglob",
8833                            stmt.line,
8834                        )),
8835                    }
8836                };
8837                match &target.kind {
8838                    ExprKind::Typeglob(name) => {
8839                        let rhs = if let Some(init) = initializer {
8840                            rhs_name(init)?
8841                        } else {
8842                            None
8843                        };
8844                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
8845                        return Ok(StrykeValue::UNDEF);
8846                    }
8847                    ExprKind::Deref {
8848                        expr,
8849                        kind: Sigil::Typeglob,
8850                    } => {
8851                        let lhs = self.eval_expr(expr)?.to_string();
8852                        let rhs = if let Some(init) = initializer {
8853                            rhs_name(init)?
8854                        } else {
8855                            None
8856                        };
8857                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8858                        return Ok(StrykeValue::UNDEF);
8859                    }
8860                    ExprKind::TypeglobExpr(e) => {
8861                        let lhs = self.eval_expr(e)?.to_string();
8862                        let rhs = if let Some(init) = initializer {
8863                            rhs_name(init)?
8864                        } else {
8865                            None
8866                        };
8867                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8868                        return Ok(StrykeValue::UNDEF);
8869                    }
8870                    _ => {}
8871                }
8872                let val = if let Some(init) = initializer {
8873                    let ctx = match &target.kind {
8874                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
8875                        _ => WantarrayCtx::Scalar,
8876                    };
8877                    self.eval_expr_ctx(init, ctx)?
8878                } else {
8879                    StrykeValue::UNDEF
8880                };
8881                match &target.kind {
8882                    ExprKind::ScalarVar(name) => {
8883                        // `local $X = …` on a special var — see twin block in
8884                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
8885                        if Self::is_special_scalar_name_for_set(name) {
8886                            let old = self.get_special_var(name);
8887                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
8888                                frame.push((name.clone(), old));
8889                            }
8890                            self.set_special_var(name, &val)
8891                                .map_err(|e| e.at_line(stmt.line))?;
8892                        }
8893                        self.scope.local_set_scalar(name, val.clone())?;
8894                    }
8895                    ExprKind::ArrayVar(name) => {
8896                        self.scope.local_set_array(name, val.to_list())?;
8897                    }
8898                    ExprKind::HashVar(name) => {
8899                        if name == "ENV" {
8900                            self.materialize_env_if_needed();
8901                        }
8902                        let items = val.to_list();
8903                        let mut map = IndexMap::new();
8904                        let mut i = 0;
8905                        while i + 1 < items.len() {
8906                            map.insert(items[i].to_string(), items[i + 1].clone());
8907                            i += 2;
8908                        }
8909                        self.scope.local_set_hash(name, map)?;
8910                    }
8911                    ExprKind::HashElement { hash, key } => {
8912                        let ks = self.eval_expr(key)?.to_string();
8913                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
8914                    }
8915                    ExprKind::ArrayElement { array, index } => {
8916                        self.check_strict_array_var(array, stmt.line)?;
8917                        let aname = self.stash_array_name_for_package(array);
8918                        let idx = self.eval_expr(index)?.to_int();
8919                        self.scope
8920                            .local_set_array_element(&aname, idx, val.clone())?;
8921                    }
8922                    _ => {
8923                        return Err(StrykeError::runtime(
8924                            format!(
8925                                "local on this lvalue is not supported yet ({:?})",
8926                                target.kind
8927                            ),
8928                            stmt.line,
8929                        )
8930                        .into());
8931                    }
8932                }
8933                Ok(val)
8934            }
8935            StmtKind::MySync(decls) => {
8936                for decl in decls {
8937                    let val = if let Some(init) = &decl.initializer {
8938                        self.eval_expr(init)?
8939                    } else {
8940                        StrykeValue::UNDEF
8941                    };
8942                    match decl.sigil {
8943                        Sigil::Typeglob => {
8944                            return Err(StrykeError::runtime(
8945                                "`mysync` does not support typeglob variables",
8946                                stmt.line,
8947                            )
8948                            .into());
8949                        }
8950                        Sigil::Scalar => {
8951                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
8952                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
8953                            let stored = if val.is_mysync_deque_or_heap() {
8954                                val
8955                            } else {
8956                                StrykeValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(
8957                                    val,
8958                                )))
8959                            };
8960                            self.scope.declare_scalar(&decl.name, stored);
8961                        }
8962                        Sigil::Array => {
8963                            self.scope.declare_atomic_array(&decl.name, val.to_list());
8964                        }
8965                        Sigil::Hash => {
8966                            let items = val.to_list();
8967                            let mut map = IndexMap::new();
8968                            let mut i = 0;
8969                            while i + 1 < items.len() {
8970                                map.insert(items[i].to_string(), items[i + 1].clone());
8971                                i += 2;
8972                            }
8973                            self.scope.declare_atomic_hash(&decl.name, map);
8974                        }
8975                    }
8976                }
8977                Ok(StrykeValue::UNDEF)
8978            }
8979            StmtKind::OurSync(decls) => {
8980                // The fan/pmap/pfor workers execute closure bodies via this tree-walker
8981                // (`exec_block_no_scope`), not the bytecode VM — so `oursync` MUST register
8982                // each declared name in `english_lexical_scalars` + `our_lexical_scalars`
8983                // for `tree_scalar_storage_name` to rewrite later `$x` reads to `Pkg::x`.
8984                // Without this, worker `$x` reads see UNDEF (the qualified key isn't
8985                // queried) even though capture/restore brought the cell across.
8986                for decl in decls {
8987                    let val = if let Some(init) = &decl.initializer {
8988                        self.eval_expr(init)?
8989                    } else {
8990                        StrykeValue::UNDEF
8991                    };
8992                    match decl.sigil {
8993                        Sigil::Typeglob => {
8994                            return Err(StrykeError::runtime(
8995                                "`oursync` does not support typeglob variables",
8996                                stmt.line,
8997                            )
8998                            .into());
8999                        }
9000                        Sigil::Scalar => {
9001                            let stash = self.stash_scalar_name_for_package(&decl.name);
9002                            let stored = if val.is_mysync_deque_or_heap() {
9003                                val
9004                            } else {
9005                                StrykeValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(
9006                                    val,
9007                                )))
9008                            };
9009                            self.scope.declare_scalar(&stash, stored);
9010                            self.english_note_lexical_scalar(&decl.name);
9011                            self.note_our_scalar(&decl.name);
9012                        }
9013                        Sigil::Array => {
9014                            let stash = self.stash_array_name_for_package(&decl.name);
9015                            self.scope.declare_atomic_array(&stash, val.to_list());
9016                            self.english_note_lexical_scalar(&decl.name);
9017                            self.note_our_scalar(&decl.name);
9018                        }
9019                        Sigil::Hash => {
9020                            let items = val.to_list();
9021                            let mut map = IndexMap::new();
9022                            let mut i = 0;
9023                            while i + 1 < items.len() {
9024                                map.insert(items[i].to_string(), items[i + 1].clone());
9025                                i += 2;
9026                            }
9027                            // Match `our %h` convention: bare hash name (existing
9028                            // cross-package quirk).
9029                            self.scope.declare_atomic_hash(&decl.name, map);
9030                            self.english_note_lexical_scalar(&decl.name);
9031                            self.note_our_scalar(&decl.name);
9032                        }
9033                    }
9034                }
9035                Ok(StrykeValue::UNDEF)
9036            }
9037            StmtKind::Package { name } => {
9038                // Minimal package support — just set a variable
9039                let _ = self
9040                    .scope
9041                    .set_scalar("__PACKAGE__", StrykeValue::string(name.clone()));
9042                Ok(StrykeValue::UNDEF)
9043            }
9044            StmtKind::UsePerlVersion { .. } => Ok(StrykeValue::UNDEF),
9045            StmtKind::Use { .. } => {
9046                // Handled in `prepare_program_top_level` before BEGIN / main.
9047                Ok(StrykeValue::UNDEF)
9048            }
9049            StmtKind::UseOverload { pairs } => {
9050                self.install_use_overload_pairs(pairs);
9051                Ok(StrykeValue::UNDEF)
9052            }
9053            StmtKind::No { .. } => {
9054                // Handled in `prepare_program_top_level` (same phase as `use`).
9055                Ok(StrykeValue::UNDEF)
9056            }
9057            StmtKind::Return(val) => {
9058                let v = if let Some(e) = val {
9059                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
9060                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
9061                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
9062                    self.eval_expr_ctx(e, self.wantarray_kind)?
9063                } else {
9064                    StrykeValue::UNDEF
9065                };
9066                Err(Flow::Return(v).into())
9067            }
9068            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
9069            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
9070            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
9071            StmtKind::Block(block) => self.exec_block(block),
9072            StmtKind::Begin(_)
9073            | StmtKind::UnitCheck(_)
9074            | StmtKind::Check(_)
9075            | StmtKind::Init(_)
9076            | StmtKind::End(_) => Ok(StrykeValue::UNDEF),
9077            StmtKind::Empty => Ok(StrykeValue::UNDEF),
9078            StmtKind::Goto { target } => {
9079                // goto &sub — tail call
9080                if let ExprKind::SubroutineRef(name) = &target.kind {
9081                    return Err(Flow::GotoSub(name.clone()).into());
9082                }
9083                Err(StrykeError::runtime("goto reached outside goto-aware block", stmt.line).into())
9084            }
9085            StmtKind::EvalTimeout { timeout, body } => {
9086                let secs = self.eval_expr(timeout)?.to_number();
9087                self.eval_timeout_block(body, secs, stmt.line)
9088            }
9089            StmtKind::Tie {
9090                target,
9091                class,
9092                args,
9093            } => {
9094                let kind = match &target {
9095                    TieTarget::Scalar(_) => 0u8,
9096                    TieTarget::Array(_) => 1u8,
9097                    TieTarget::Hash(_) => 2u8,
9098                };
9099                let name = match &target {
9100                    TieTarget::Scalar(s) => s.as_str(),
9101                    TieTarget::Array(a) => a.as_str(),
9102                    TieTarget::Hash(h) => h.as_str(),
9103                };
9104                let mut vals = vec![self.eval_expr(class)?];
9105                for a in args {
9106                    vals.push(self.eval_expr(a)?);
9107                }
9108                self.tie_execute(kind, name, vals, stmt.line)
9109                    .map_err(Into::into)
9110            }
9111            StmtKind::TryCatch {
9112                try_block,
9113                catch_var,
9114                catch_block,
9115                finally_block,
9116            } => match self.exec_block(try_block) {
9117                Ok(v) => {
9118                    if let Some(fb) = finally_block {
9119                        self.exec_block(fb)?;
9120                    }
9121                    Ok(v)
9122                }
9123                Err(FlowOrError::Error(e)) => {
9124                    if matches!(e.kind, ErrorKind::Exit(_)) {
9125                        return Err(FlowOrError::Error(e));
9126                    }
9127                    self.scope_push_hook();
9128                    self.scope
9129                        .declare_scalar(catch_var, StrykeValue::string(e.to_string()));
9130                    self.english_note_lexical_scalar(catch_var);
9131                    let r = self.exec_block(catch_block);
9132                    self.scope_pop_hook();
9133                    if let Some(fb) = finally_block {
9134                        self.exec_block(fb)?;
9135                    }
9136                    r
9137                }
9138                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
9139            },
9140            StmtKind::Given { topic, body } => self.exec_given(topic, body),
9141            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(StrykeError::runtime(
9142                "when/default may only appear inside a given block",
9143                stmt.line,
9144            )
9145            .into()),
9146            StmtKind::FormatDecl { .. } => {
9147                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
9148                Ok(StrykeValue::UNDEF)
9149            }
9150            StmtKind::AdviceDecl {
9151                kind,
9152                pattern,
9153                body,
9154            } => {
9155                // Tree-walker registration path: only reached if bytecode compilation
9156                // bailed out (extremely rare — production programs always run through
9157                // the VM). The body has no compiled bytecode region, so we tag it with
9158                // `u16::MAX` and `dispatch_with_advice` will refuse to fire it rather
9159                // than silently fall back to the AST tree-walker.
9160                let id = self.next_intercept_id;
9161                self.next_intercept_id = id.saturating_add(1);
9162                self.intercepts.push(crate::aop::Intercept {
9163                    id,
9164                    kind: *kind,
9165                    pattern: pattern.clone(),
9166                    body: body.clone(),
9167                    body_block_idx: u16::MAX,
9168                });
9169                Ok(StrykeValue::UNDEF)
9170            }
9171            StmtKind::Continue(block) => self.exec_block_smart(block),
9172        }
9173    }
9174
9175    #[inline]
9176    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
9177        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
9178    }
9179
9180    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
9181    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
9182    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
9183    pub(crate) fn scalar_compound_assign_scalar_target(
9184        &mut self,
9185        name: &str,
9186        op: BinOp,
9187        rhs: StrykeValue,
9188    ) -> Result<StrykeValue, StrykeError> {
9189        if op == BinOp::Concat {
9190            return self.scope.scalar_concat_inplace(name, &rhs);
9191        }
9192        self.scope
9193            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs))
9194    }
9195
9196    fn compound_scalar_binop(old: &StrykeValue, op: BinOp, rhs: &StrykeValue) -> StrykeValue {
9197        match op {
9198            BinOp::Add => {
9199                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9200                    StrykeValue::integer(a.wrapping_add(b))
9201                } else {
9202                    StrykeValue::float(old.to_number() + rhs.to_number())
9203                }
9204            }
9205            BinOp::Sub => {
9206                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9207                    StrykeValue::integer(a.wrapping_sub(b))
9208                } else {
9209                    StrykeValue::float(old.to_number() - rhs.to_number())
9210                }
9211            }
9212            BinOp::Mul => {
9213                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9214                    StrykeValue::integer(a.wrapping_mul(b))
9215                } else {
9216                    StrykeValue::float(old.to_number() * rhs.to_number())
9217                }
9218            }
9219            BinOp::BitAnd => {
9220                if let Some(s) = crate::value::set_intersection(old, rhs) {
9221                    s
9222                } else {
9223                    StrykeValue::integer(old.to_int() & rhs.to_int())
9224                }
9225            }
9226            BinOp::BitOr => {
9227                if let Some(s) = crate::value::set_union(old, rhs) {
9228                    s
9229                } else {
9230                    StrykeValue::integer(old.to_int() | rhs.to_int())
9231                }
9232            }
9233            BinOp::BitXor => StrykeValue::integer(old.to_int() ^ rhs.to_int()),
9234            BinOp::ShiftLeft => StrykeValue::integer(perl_shl_i64(old.to_int(), rhs.to_int())),
9235            BinOp::ShiftRight => StrykeValue::integer(perl_shr_i64(old.to_int(), rhs.to_int())),
9236            BinOp::Div => StrykeValue::float(old.to_number() / rhs.to_number()),
9237            BinOp::Mod => {
9238                // Return 0 on b==0 silently — this helper is the
9239                // `$x OP= rhs` atomic-mutate path which can't propagate
9240                // errors. The non-compound `%` path (eval_binop) raises
9241                // `ErrorKind::DivisionByZero`.
9242                let b = rhs.to_int();
9243                if b == 0 {
9244                    StrykeValue::integer(0)
9245                } else {
9246                    StrykeValue::integer(crate::value::perl_mod_i64(old.to_int(), b))
9247                }
9248            }
9249            BinOp::Pow => StrykeValue::float(old.to_number().powf(rhs.to_number())),
9250            BinOp::LogOr => {
9251                if old.is_true() {
9252                    old.clone()
9253                } else {
9254                    rhs.clone()
9255                }
9256            }
9257            BinOp::DefinedOr => {
9258                if !old.is_undef() {
9259                    old.clone()
9260                } else {
9261                    rhs.clone()
9262                }
9263            }
9264            BinOp::LogAnd => {
9265                if old.is_true() {
9266                    rhs.clone()
9267                } else {
9268                    old.clone()
9269                }
9270            }
9271            _ => StrykeValue::float(old.to_number() + rhs.to_number()),
9272        }
9273    }
9274
9275    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
9276    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
9277    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
9278    fn eval_hash_slice_key_components(
9279        &mut self,
9280        key_expr: &Expr,
9281    ) -> Result<Vec<String>, FlowOrError> {
9282        // Keys for `@h{LIST}` are always evaluated in Perl list context — an
9283        // `@ks` operand splats to its elements instead of scalarizing to its
9284        // count, and an `\@ref` operand unwraps after deref. Without this the
9285        // array-var form returned the empty list (BUG-028).
9286        let v = self.eval_expr_ctx(key_expr, WantarrayCtx::List)?;
9287        if let Some(vv) = v.as_array_vec() {
9288            return Ok(vv.iter().map(|x| x.to_string()).collect());
9289        }
9290        if let Some(r) = v.as_array_ref() {
9291            return Ok(r.read().iter().map(|x| x.to_string()).collect());
9292        }
9293        if v.is_iterator() {
9294            return Ok(v
9295                .into_iterator()
9296                .collect_all()
9297                .iter()
9298                .map(|x| x.to_string())
9299                .collect());
9300        }
9301        Ok(vec![v.to_string()])
9302    }
9303
9304    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
9305    pub(crate) fn symbolic_deref(
9306        &mut self,
9307        val: StrykeValue,
9308        kind: Sigil,
9309        line: usize,
9310    ) -> ExecResult {
9311        match kind {
9312            Sigil::Scalar => {
9313                if let Some(name) = val.as_scalar_binding_name() {
9314                    return Ok(self.get_special_var(&name));
9315                }
9316                if let Some(r) = val.as_scalar_ref() {
9317                    return Ok(r.read().clone());
9318                }
9319                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
9320                if let Some(r) = val.as_array_ref() {
9321                    return Ok(StrykeValue::array(r.read().clone()));
9322                }
9323                if let Some(name) = val.as_array_binding_name() {
9324                    return Ok(StrykeValue::array(self.scope.get_array(&name)));
9325                }
9326                if let Some(r) = val.as_hash_ref() {
9327                    return Ok(StrykeValue::hash(r.read().clone()));
9328                }
9329                if let Some(name) = val.as_hash_binding_name() {
9330                    self.touch_env_hash(&name);
9331                    return Ok(StrykeValue::hash(self.scope.get_hash(&name)));
9332                }
9333                if let Some(s) = val.as_str() {
9334                    if self.strict_refs {
9335                        return Err(StrykeError::runtime(
9336                            format!(
9337                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
9338                                s
9339                            ),
9340                            line,
9341                        )
9342                        .into());
9343                    }
9344                    return Ok(self.get_special_var(&s));
9345                }
9346                Err(StrykeError::runtime("Can't dereference non-reference as scalar", line).into())
9347            }
9348            Sigil::Array => {
9349                if let Some(r) = val.as_array_ref() {
9350                    return Ok(StrykeValue::array(r.read().clone()));
9351                }
9352                if let Some(name) = val.as_array_binding_name() {
9353                    return Ok(StrykeValue::array(self.scope.get_array(&name)));
9354                }
9355                if val.is_undef() {
9356                    if self.strict_refs {
9357                        return Err(StrykeError::runtime(
9358                            "Can't use an undefined value as an ARRAY reference",
9359                            line,
9360                        )
9361                        .into());
9362                    }
9363                    return Ok(StrykeValue::array(vec![]));
9364                }
9365                // Plain primitive scalar (int, float, string): under no-strict, perl
9366                // treats this as a symbolic ref `@{$val_as_string}` and silently
9367                // returns the (likely empty) named array. Under strict refs, error.
9368                // Heap objects (Pair, Generator, blessed-non-ref) fall through to
9369                // the dereference-error so we don't silently swallow real bugs.
9370                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
9371                    let s = val.to_string();
9372                    if self.strict_refs {
9373                        return Err(StrykeError::runtime(
9374                            format!(
9375                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
9376                                s
9377                            ),
9378                            line,
9379                        )
9380                        .into());
9381                    }
9382                    return Ok(StrykeValue::array(self.scope.get_array(&s)));
9383                }
9384                Err(StrykeError::runtime("Can't dereference non-reference as array", line).into())
9385            }
9386            Sigil::Hash => {
9387                if let Some(r) = val.as_hash_ref() {
9388                    return Ok(StrykeValue::hash(r.read().clone()));
9389                }
9390                if let Some(name) = val.as_hash_binding_name() {
9391                    self.touch_env_hash(&name);
9392                    return Ok(StrykeValue::hash(self.scope.get_hash(&name)));
9393                }
9394                // Stryke `class C { ... }` instances answer to `%$obj` by
9395                // flattening their field name/value pairs — the same shape
9396                // a Perl-style blessed hashref produces. This keeps the
9397                // canonical introspection idiom (`keys %$obj`, `values
9398                // %$obj`) working for stryke-native OO too. Order matches
9399                // the inheritance-collected field order from
9400                // `collect_class_fields_full`.
9401                if let Some(c) = val.as_class_inst() {
9402                    let all_fields = self.collect_class_fields_full(&c.def);
9403                    let values = c.get_values();
9404                    let mut map = IndexMap::new();
9405                    for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
9406                        if let Some(v) = values.get(i) {
9407                            map.insert(name.clone(), v.clone());
9408                        }
9409                    }
9410                    return Ok(StrykeValue::hash(map));
9411                }
9412                // Same for stryke `struct S { ... }` instances — keep them
9413                // introspectable through the Perl-style hash-deref idiom.
9414                if let Some(s) = val.as_struct_inst() {
9415                    let values = s.get_values();
9416                    let mut map = IndexMap::new();
9417                    for (i, field) in s.def.fields.iter().enumerate() {
9418                        if let Some(v) = values.get(i) {
9419                            map.insert(field.name.clone(), v.clone());
9420                        }
9421                    }
9422                    return Ok(StrykeValue::hash(map));
9423                }
9424                // Blessed-ref escape hatch: when the inner data is a hash,
9425                // unwrap and treat the deref as if it targeted the inner
9426                // hash. Old Perl OO code that wrote `%$self` on a blessed
9427                // hashref keeps working without an extra unbless step.
9428                if let Some(b) = val.as_blessed_ref() {
9429                    let inner = b.data.read().clone();
9430                    if let Some(r) = inner.as_hash_ref() {
9431                        return Ok(StrykeValue::hash(r.read().clone()));
9432                    }
9433                    if let Some(h) = inner.as_hash_map() {
9434                        return Ok(StrykeValue::hash(h));
9435                    }
9436                }
9437                if val.is_undef() {
9438                    if self.strict_refs {
9439                        return Err(StrykeError::runtime(
9440                            "Can't use an undefined value as a HASH reference",
9441                            line,
9442                        )
9443                        .into());
9444                    }
9445                    return Ok(StrykeValue::hash(IndexMap::new()));
9446                }
9447                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
9448                    let s = val.to_string();
9449                    if self.strict_refs {
9450                        return Err(StrykeError::runtime(
9451                            format!(
9452                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
9453                                s
9454                            ),
9455                            line,
9456                        )
9457                        .into());
9458                    }
9459                    self.touch_env_hash(&s);
9460                    return Ok(StrykeValue::hash(self.scope.get_hash(&s)));
9461                }
9462                Err(StrykeError::runtime("Can't dereference non-reference as hash", line).into())
9463            }
9464            Sigil::Typeglob => {
9465                if let Some(s) = val.as_str() {
9466                    return Ok(StrykeValue::string(self.resolve_io_handle_name(&s)));
9467                }
9468                Err(
9469                    StrykeError::runtime("Can't dereference non-reference as typeglob", line)
9470                        .into(),
9471                )
9472            }
9473        }
9474    }
9475
9476    /// `qq` list join expects a plain array; if a bare [`StrykeValue::array_ref`] reaches join, peel
9477    /// one level so elements stringify like Perl (`"@$r"`).
9478    #[inline]
9479    pub(crate) fn peel_array_ref_for_list_join(&self, v: StrykeValue) -> StrykeValue {
9480        if let Some(r) = v.as_array_ref() {
9481            return StrykeValue::array(r.read().clone());
9482        }
9483        v
9484    }
9485
9486    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
9487    pub(crate) fn make_array_ref_alias(&self, val: StrykeValue, line: usize) -> ExecResult {
9488        if let Some(a) = val.as_array_ref() {
9489            return Ok(StrykeValue::array_ref(Arc::clone(&a)));
9490        }
9491        if let Some(name) = val.as_array_binding_name() {
9492            return Ok(StrykeValue::array_binding_ref(name));
9493        }
9494        if let Some(s) = val.as_str() {
9495            if self.strict_refs {
9496                return Err(StrykeError::runtime(
9497                    format!(
9498                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
9499                        s
9500                    ),
9501                    line,
9502                )
9503                .into());
9504            }
9505            return Ok(StrykeValue::array_binding_ref(s.to_string()));
9506        }
9507        if let Some(r) = val.as_scalar_ref() {
9508            let inner = r.read().clone();
9509            return self.make_array_ref_alias(inner, line);
9510        }
9511        Err(StrykeError::runtime("Can't make array reference from value", line).into())
9512    }
9513
9514    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
9515    pub(crate) fn make_hash_ref_alias(&self, val: StrykeValue, line: usize) -> ExecResult {
9516        if let Some(h) = val.as_hash_ref() {
9517            return Ok(StrykeValue::hash_ref(Arc::clone(&h)));
9518        }
9519        if let Some(name) = val.as_hash_binding_name() {
9520            return Ok(StrykeValue::hash_binding_ref(name));
9521        }
9522        if let Some(s) = val.as_str() {
9523            if self.strict_refs {
9524                return Err(StrykeError::runtime(
9525                    format!(
9526                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
9527                        s
9528                    ),
9529                    line,
9530                )
9531                .into());
9532            }
9533            return Ok(StrykeValue::hash_binding_ref(s.to_string()));
9534        }
9535        if let Some(r) = val.as_scalar_ref() {
9536            let inner = r.read().clone();
9537            return self.make_hash_ref_alias(inner, line);
9538        }
9539        Err(StrykeError::runtime("Can't make hash reference from value", line).into())
9540    }
9541
9542    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
9543    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
9544    pub(crate) fn process_case_escapes(s: &str) -> String {
9545        // Quick check: if no backslash, nothing to do
9546        if !s.contains('\\') {
9547            return s.to_string();
9548        }
9549        let mut result = String::with_capacity(s.len());
9550        let mut chars = s.chars().peekable();
9551        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
9552        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
9553
9554        while let Some(c) = chars.next() {
9555            if c == '\\' {
9556                match chars.peek() {
9557                    Some(&'U') => {
9558                        chars.next();
9559                        mode = Some('U');
9560                        continue;
9561                    }
9562                    Some(&'L') => {
9563                        chars.next();
9564                        mode = Some('L');
9565                        continue;
9566                    }
9567                    Some(&'Q') => {
9568                        chars.next();
9569                        mode = Some('Q');
9570                        continue;
9571                    }
9572                    Some(&'E') => {
9573                        chars.next();
9574                        mode = None;
9575                        next_char_mod = None;
9576                        continue;
9577                    }
9578                    Some(&'u') => {
9579                        chars.next();
9580                        next_char_mod = Some('u');
9581                        continue;
9582                    }
9583                    Some(&'l') => {
9584                        chars.next();
9585                        next_char_mod = Some('l');
9586                        continue;
9587                    }
9588                    _ => {}
9589                }
9590            }
9591
9592            let ch = c;
9593
9594            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
9595            if let Some(m) = next_char_mod.take() {
9596                let transformed = match m {
9597                    'u' => ch.to_uppercase().next().unwrap_or(ch),
9598                    'l' => ch.to_lowercase().next().unwrap_or(ch),
9599                    _ => ch,
9600                };
9601                result.push(transformed);
9602            } else {
9603                // Apply ongoing mode
9604                match mode {
9605                    Some('U') => {
9606                        for uc in ch.to_uppercase() {
9607                            result.push(uc);
9608                        }
9609                    }
9610                    Some('L') => {
9611                        for lc in ch.to_lowercase() {
9612                            result.push(lc);
9613                        }
9614                    }
9615                    Some('Q') => {
9616                        if !ch.is_ascii_alphanumeric() && ch != '_' {
9617                            result.push('\\');
9618                        }
9619                        result.push(ch);
9620                    }
9621                    None | Some(_) => {
9622                        result.push(ch);
9623                    }
9624                }
9625            }
9626        }
9627        result
9628    }
9629
9630    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
9631        let line = expr.line;
9632        match &expr.kind {
9633            ExprKind::Integer(n) => Ok(StrykeValue::integer(*n)),
9634            ExprKind::Float(f) => Ok(StrykeValue::float(*f)),
9635            ExprKind::String(s) => {
9636                let processed = Self::process_case_escapes(s);
9637                Ok(StrykeValue::string(processed))
9638            }
9639            ExprKind::Bareword(s) => {
9640                if s == "__PACKAGE__" {
9641                    return Ok(StrykeValue::string(self.current_package()));
9642                }
9643                if let Some(sub) = self.resolve_sub_by_name(s) {
9644                    return self.call_sub(&sub, vec![], ctx, line);
9645                }
9646                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
9647                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
9648                    return r.map_err(Into::into);
9649                }
9650                Ok(StrykeValue::string(s.clone()))
9651            }
9652            ExprKind::Undef => Ok(StrykeValue::UNDEF),
9653            ExprKind::MagicConst(MagicConstKind::File) => {
9654                Ok(StrykeValue::string(self.file.clone()))
9655            }
9656            ExprKind::MagicConst(MagicConstKind::Line) => {
9657                Ok(StrykeValue::integer(expr.line as i64))
9658            }
9659            ExprKind::MagicConst(MagicConstKind::Sub) => {
9660                if let Some(sub) = self.current_sub_stack.last().cloned() {
9661                    Ok(StrykeValue::code_ref(sub))
9662                } else {
9663                    Ok(StrykeValue::UNDEF)
9664                }
9665            }
9666            ExprKind::Regex(pattern, flags) => {
9667                if ctx == WantarrayCtx::Void {
9668                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
9669                    let topic = self.scope.get_scalar("_");
9670                    let s = topic.to_string();
9671                    self.regex_match_execute(s, pattern, flags, false, "_", line)
9672                } else {
9673                    let re = self.compile_regex(pattern, flags, line)?;
9674                    Ok(StrykeValue::regex(re, pattern.clone(), flags.clone()))
9675                }
9676            }
9677            ExprKind::QW(words) => Ok(StrykeValue::array(
9678                words
9679                    .iter()
9680                    .map(|w| StrykeValue::string(w.clone()))
9681                    .collect(),
9682            )),
9683
9684            // Interpolated strings
9685            ExprKind::InterpolatedString(parts) => {
9686                let mut raw_result = String::new();
9687                for part in parts {
9688                    match part {
9689                        StringPart::Literal(s) => raw_result.push_str(s),
9690                        StringPart::ScalarVar(name) => {
9691                            self.check_strict_scalar_var(name, line)?;
9692                            let val = self.get_special_var(name);
9693                            let s = self.stringify_value(val, line)?;
9694                            raw_result.push_str(&s);
9695                        }
9696                        StringPart::ArrayVar(name) => {
9697                            self.check_strict_array_var(name, line)?;
9698                            let aname = self.stash_array_name_for_package(name);
9699                            let arr = self.scope.get_array(&aname);
9700                            let mut parts = Vec::with_capacity(arr.len());
9701                            for v in &arr {
9702                                parts.push(self.stringify_value(v.clone(), line)?);
9703                            }
9704                            let sep = self.list_separator.clone();
9705                            raw_result.push_str(&parts.join(&sep));
9706                        }
9707                        StringPart::Expr(e) => {
9708                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
9709                                self.check_strict_array_var(array, line)?;
9710                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9711                                let val = self.peel_array_ref_for_list_join(val);
9712                                let list = val.to_list();
9713                                let sep = self.list_separator.clone();
9714                                let mut parts = Vec::with_capacity(list.len());
9715                                for v in list {
9716                                    parts.push(self.stringify_value(v, line)?);
9717                                }
9718                                raw_result.push_str(&parts.join(&sep));
9719                            } else if let ExprKind::Deref {
9720                                kind: Sigil::Array, ..
9721                            } = &e.kind
9722                            {
9723                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9724                                let val = self.peel_array_ref_for_list_join(val);
9725                                let list = val.to_list();
9726                                let sep = self.list_separator.clone();
9727                                let mut parts = Vec::with_capacity(list.len());
9728                                for v in list {
9729                                    parts.push(self.stringify_value(v, line)?);
9730                                }
9731                                raw_result.push_str(&parts.join(&sep));
9732                            } else {
9733                                let val = self.eval_expr(e)?;
9734                                let s = self.stringify_value(val, line)?;
9735                                raw_result.push_str(&s);
9736                            }
9737                        }
9738                    }
9739                }
9740                let result = Self::process_case_escapes(&raw_result);
9741                Ok(StrykeValue::string(result))
9742            }
9743
9744            // Variables
9745            ExprKind::ScalarVar(name) => {
9746                self.check_strict_scalar_var(name, line)?;
9747                let stor = self.tree_scalar_storage_name(name);
9748                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
9749                    let class = obj
9750                        .as_blessed_ref()
9751                        .map(|b| b.class.clone())
9752                        .unwrap_or_default();
9753                    let full = format!("{}::FETCH", class);
9754                    if let Some(sub) = self.subs.get(&full).cloned() {
9755                        return self.call_sub(&sub, vec![obj], ctx, line);
9756                    }
9757                }
9758                Ok(self.get_special_var(&stor))
9759            }
9760            ExprKind::ArrayVar(name) => {
9761                self.check_strict_array_var(name, line)?;
9762                let qualified = self.tree_array_storage_name(name);
9763                let aname = self.stash_array_name_for_package(&qualified);
9764                let arr = self.scope.get_array(&aname);
9765                if ctx == WantarrayCtx::List {
9766                    Ok(StrykeValue::array(arr))
9767                } else {
9768                    Ok(StrykeValue::integer(arr.len() as i64))
9769                }
9770            }
9771            ExprKind::HashVar(name) => {
9772                self.check_strict_hash_var(name, line)?;
9773                self.touch_env_hash(name);
9774                let hname = self.tree_hash_storage_name(name);
9775                let h = self.scope.get_hash(&hname);
9776                let pv = StrykeValue::hash(h);
9777                if ctx == WantarrayCtx::List {
9778                    Ok(pv)
9779                } else {
9780                    Ok(pv.scalar_context())
9781                }
9782            }
9783            ExprKind::Typeglob(name) => {
9784                let n = self.resolve_io_handle_name(name);
9785                Ok(StrykeValue::string(n))
9786            }
9787            ExprKind::TypeglobExpr(e) => {
9788                let name = self.eval_expr(e)?.to_string();
9789                let n = self.resolve_io_handle_name(&name);
9790                Ok(StrykeValue::string(n))
9791            }
9792            ExprKind::ArrayElement { array, index } => {
9793                // Stryke string-index sugar: bareword `_[N]` parses to an
9794                // ArrayElement with a `__topicstr__N` synthetic name. Strip
9795                // the prefix and treat as substr-of-topic. Differs from
9796                // `$_[N]` (sigil form) which keeps Perl's @_-access.
9797                if let Some(real) = array.strip_prefix("__topicstr__") {
9798                    let s = self.scope.get_scalar(real).to_string();
9799                    if let ExprKind::Range {
9800                        from,
9801                        to,
9802                        exclusive,
9803                        step,
9804                    } = &index.kind
9805                    {
9806                        let n = s.chars().count() as i64;
9807                        let mut from_i = self.eval_expr(from)?.to_int();
9808                        let mut to_i = self.eval_expr(to)?.to_int();
9809                        let step_i = match step {
9810                            Some(e) => self.eval_expr(e)?.to_int(),
9811                            None => 1,
9812                        };
9813                        if from_i < 0 {
9814                            from_i += n
9815                        }
9816                        if to_i < 0 {
9817                            to_i += n
9818                        }
9819                        if *exclusive {
9820                            to_i -= 1
9821                        }
9822                        let chars: Vec<char> = s.chars().collect();
9823                        let mut out = String::new();
9824                        if step_i > 0 {
9825                            let mut i = from_i;
9826                            while i <= to_i && i < n {
9827                                if i >= 0 {
9828                                    out.push(chars[i as usize]);
9829                                }
9830                                i += step_i;
9831                            }
9832                        } else if step_i < 0 {
9833                            let mut i = from_i;
9834                            while i >= to_i && i >= 0 {
9835                                if i < n {
9836                                    out.push(chars[i as usize]);
9837                                }
9838                                i += step_i;
9839                            }
9840                        }
9841                        return Ok(StrykeValue::string(out));
9842                    }
9843                    let idx = self.eval_expr(index)?.to_int();
9844                    let n = s.chars().count() as i64;
9845                    let i = if idx < 0 { idx + n } else { idx };
9846                    return Ok(if i >= 0 && i < n {
9847                        s.chars()
9848                            .nth(i as usize)
9849                            .map(|c| StrykeValue::string(c.to_string()))
9850                            .unwrap_or(StrykeValue::UNDEF)
9851                    } else {
9852                        StrykeValue::UNDEF
9853                    });
9854                }
9855                self.check_strict_array_var(array, line)?;
9856                // Stryke (non-compat) string-slice sugar: when the index is
9857                // a `from:to[:step]` range AND the target is a string, return
9858                // a substring with optional step. Mirrors Python `s[1:10:2]`.
9859                // Detect this BEFORE collapsing the range to an int.
9860                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9861                    if let ExprKind::Range {
9862                        from,
9863                        to,
9864                        exclusive,
9865                        step,
9866                    } = &index.kind
9867                    {
9868                        let aname_check = self.stash_array_name_for_package(array);
9869                        let prefer_scalar =
9870                            array == "_" || self.scope.get_array(&aname_check).is_empty();
9871                        if prefer_scalar {
9872                            let s = self.scope.get_scalar(array).to_string();
9873                            if !s.is_empty() {
9874                                let n = s.chars().count() as i64;
9875                                let mut from_i = self.eval_expr(from)?.to_int();
9876                                let mut to_i = self.eval_expr(to)?.to_int();
9877                                let step_i = match step {
9878                                    Some(e) => self.eval_expr(e)?.to_int(),
9879                                    None => 1,
9880                                };
9881                                if from_i < 0 {
9882                                    from_i += n
9883                                }
9884                                if to_i < 0 {
9885                                    to_i += n
9886                                }
9887                                if *exclusive {
9888                                    to_i -= 1
9889                                }
9890                                let chars: Vec<char> = s.chars().collect();
9891                                let mut out = String::new();
9892                                if step_i > 0 {
9893                                    let mut i = from_i;
9894                                    while i <= to_i && i < n {
9895                                        if i >= 0 {
9896                                            out.push(chars[i as usize]);
9897                                        }
9898                                        i += step_i;
9899                                    }
9900                                } else if step_i < 0 {
9901                                    let mut i = from_i;
9902                                    while i >= to_i && i >= 0 {
9903                                        if i < n {
9904                                            out.push(chars[i as usize]);
9905                                        }
9906                                        i += step_i;
9907                                    }
9908                                }
9909                                return Ok(StrykeValue::string(out));
9910                            }
9911                        }
9912                    }
9913                }
9914                let idx = self.eval_expr(index)?.to_int();
9915                let aname = self.stash_array_name_for_package(array);
9916                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
9917                    let class = obj
9918                        .as_blessed_ref()
9919                        .map(|b| b.class.clone())
9920                        .unwrap_or_default();
9921                    let full = format!("{}::FETCH", class);
9922                    if let Some(sub) = self.subs.get(&full).cloned() {
9923                        let arg_vals = vec![obj, StrykeValue::integer(idx)];
9924                        return self.call_sub(&sub, arg_vals, ctx, line);
9925                    }
9926                }
9927                // Stryke (non-compat) sugar: `$name[i]` indexes by Unicode
9928                // char when `@name` is missing/empty but `$name` is a
9929                // non-empty string. So `$s[0]` is the first grapheme of
9930                // `$s`. NB: `$_[0]` keeps Perl's `@_`-access semantics
9931                // because `@_` is populated inside any sub call; the
9932                // bareword `_[0]` parses to the same AST node, so both
9933                // forms behave alike — use `substr(_, 0, 1)` for char-of-
9934                // topic when inside a sub. Compat mode = Perl semantics.
9935                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9936                    let prefer_scalar = self.scope.get_array(&aname).is_empty();
9937                    if prefer_scalar {
9938                        let s = self.scope.get_scalar(array).to_string();
9939                        if !s.is_empty() {
9940                            let n = s.chars().count() as i64;
9941                            let i = if idx < 0 { idx + n } else { idx };
9942                            if i >= 0 && i < n {
9943                                if let Some(c) = s.chars().nth(i as usize) {
9944                                    return Ok(StrykeValue::string(c.to_string()));
9945                                }
9946                            }
9947                            return Ok(StrykeValue::UNDEF);
9948                        }
9949                    }
9950                }
9951                Ok(self.scope.get_array_element(&aname, idx))
9952            }
9953            ExprKind::HashElement { hash, key } => {
9954                self.check_strict_hash_var(hash, line)?;
9955                let k = self.eval_expr(key)?.to_string();
9956                self.touch_env_hash(hash);
9957                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
9958                    let class = obj
9959                        .as_blessed_ref()
9960                        .map(|b| b.class.clone())
9961                        .unwrap_or_default();
9962                    let full = format!("{}::FETCH", class);
9963                    if let Some(sub) = self.subs.get(&full).cloned() {
9964                        let arg_vals = vec![obj, StrykeValue::string(k)];
9965                        return self.call_sub(&sub, arg_vals, ctx, line);
9966                    }
9967                }
9968                let hname = self.tree_hash_storage_name(hash);
9969                Ok(self.scope.get_hash_element(&hname, &k))
9970            }
9971            ExprKind::ArraySlice { array, indices } => {
9972                self.check_strict_array_var(array, line)?;
9973                let aname = self.stash_array_name_for_package(array);
9974                let flat = self.flatten_array_slice_index_specs(indices)?;
9975                let mut result = Vec::with_capacity(flat.len());
9976                for idx in flat {
9977                    result.push(self.scope.get_array_element(&aname, idx));
9978                }
9979                Ok(StrykeValue::array(result))
9980            }
9981            ExprKind::HashSlice { hash, keys } => {
9982                self.check_strict_hash_var(hash, line)?;
9983                self.touch_env_hash(hash);
9984                let mut result = Vec::new();
9985                for key_expr in keys {
9986                    for k in self.eval_hash_slice_key_components(key_expr)? {
9987                        result.push(self.scope.get_hash_element(hash, &k));
9988                    }
9989                }
9990                Ok(StrykeValue::array(result))
9991            }
9992            ExprKind::HashKvSlice { hash, keys } => {
9993                // `%h{KEYS}` — Perl 5.20+ key-value slice. Returns a flat
9994                // (key, value, key, value, ...) list. (BUG-008)
9995                self.check_strict_hash_var(hash, line)?;
9996                self.touch_env_hash(hash);
9997                let mut result = Vec::new();
9998                for key_expr in keys {
9999                    for k in self.eval_hash_slice_key_components(key_expr)? {
10000                        let v = self.scope.get_hash_element(hash, &k);
10001                        result.push(StrykeValue::string(k));
10002                        result.push(v);
10003                    }
10004                }
10005                Ok(StrykeValue::array(result))
10006            }
10007            ExprKind::HashSliceDeref { container, keys } => {
10008                let hv = self.eval_expr(container)?;
10009                let mut key_vals = Vec::with_capacity(keys.len());
10010                for key_expr in keys {
10011                    let v = if matches!(
10012                        key_expr.kind,
10013                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
10014                    ) {
10015                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
10016                    } else {
10017                        self.eval_expr(key_expr)?
10018                    };
10019                    key_vals.push(v);
10020                }
10021                self.hash_slice_deref_values(&hv, &key_vals, line)
10022            }
10023            ExprKind::AnonymousListSlice { source, indices } => {
10024                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
10025                let items = list_val.to_list();
10026                let flat = self.flatten_array_slice_index_specs(indices)?;
10027                let mut out = Vec::with_capacity(flat.len());
10028                for idx in flat {
10029                    let i = if idx < 0 {
10030                        (items.len() as i64 + idx) as usize
10031                    } else {
10032                        idx as usize
10033                    };
10034                    out.push(items.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
10035                }
10036                let arr = StrykeValue::array(out);
10037                if ctx != WantarrayCtx::List {
10038                    let v = arr.to_list();
10039                    Ok(v.last().cloned().unwrap_or(StrykeValue::UNDEF))
10040                } else {
10041                    Ok(arr)
10042                }
10043            }
10044
10045            // References
10046            ExprKind::ScalarRef(inner) => match &inner.kind {
10047                ExprKind::ScalarVar(name) => Ok(StrykeValue::scalar_binding_ref(name.clone())),
10048                ExprKind::ArrayVar(name) => {
10049                    self.check_strict_array_var(name, line)?;
10050                    let aname = self.stash_array_name_for_package(name);
10051                    // Promote the scope's array to shared Arc-backed storage.
10052                    // Both the scope and the returned ref share the same Arc.
10053                    let arc = self.scope.promote_array_to_shared(&aname);
10054                    Ok(StrykeValue::array_ref(arc))
10055                }
10056                ExprKind::HashVar(name) => {
10057                    self.check_strict_hash_var(name, line)?;
10058                    let arc = self.scope.promote_hash_to_shared(name);
10059                    Ok(StrykeValue::hash_ref(arc))
10060                }
10061                ExprKind::Deref {
10062                    expr: e,
10063                    kind: Sigil::Array,
10064                } => {
10065                    let v = self.eval_expr(e)?;
10066                    self.make_array_ref_alias(v, line)
10067                }
10068                ExprKind::Deref {
10069                    expr: e,
10070                    kind: Sigil::Hash,
10071                } => {
10072                    let v = self.eval_expr(e)?;
10073                    self.make_hash_ref_alias(v, line)
10074                }
10075                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
10076                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
10077                    Ok(StrykeValue::array_ref(Arc::new(RwLock::new(
10078                        list.to_list(),
10079                    ))))
10080                }
10081                ExprKind::HashSliceDeref { .. } => {
10082                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
10083                    Ok(StrykeValue::array_ref(Arc::new(RwLock::new(
10084                        list.to_list(),
10085                    ))))
10086                }
10087                _ => {
10088                    let val = self.eval_expr(inner)?;
10089                    Ok(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))))
10090                }
10091            },
10092            ExprKind::ArrayRef(elems) => {
10093                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
10094                // variables flatten into the ref rather than collapsing to a scalar count /
10095                // flip-flop value.
10096                let mut arr = Vec::with_capacity(elems.len());
10097                for e in elems {
10098                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
10099                    let v = self.scope.resolve_container_binding_ref(v);
10100                    if let Some(vec) = v.as_array_vec() {
10101                        arr.extend(vec);
10102                    } else {
10103                        arr.push(v);
10104                    }
10105                }
10106                Ok(StrykeValue::array_ref(Arc::new(RwLock::new(arr))))
10107            }
10108            ExprKind::HashRef(pairs) => {
10109                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
10110                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
10111                let mut map = IndexMap::new();
10112                for (k, v) in pairs {
10113                    let key_str = self.eval_expr(k)?.to_string();
10114                    if key_str == "__HASH_SPREAD__" {
10115                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
10116                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
10117                        let items = spread.to_list();
10118                        let mut i = 0;
10119                        while i + 1 < items.len() {
10120                            map.insert(items[i].to_string(), items[i + 1].clone());
10121                            i += 2;
10122                        }
10123                    } else {
10124                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
10125                        map.insert(key_str, val);
10126                    }
10127                }
10128                Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map))))
10129            }
10130            ExprKind::CodeRef { params, body } => {
10131                let captured = self.scope.capture();
10132                Ok(StrykeValue::code_ref(Arc::new(StrykeSub {
10133                    name: "__ANON__".to_string(),
10134                    params: params.clone(),
10135                    body: body.clone(),
10136                    closure_env: Some(captured),
10137                    prototype: None,
10138                    fib_like: None,
10139                })))
10140            }
10141            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
10142            ExprKind::SubroutineCodeRef(name) => {
10143                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
10144                    StrykeError::runtime(self.undefined_subroutine_resolve_message(name), line)
10145                })?;
10146                Ok(StrykeValue::code_ref(sub))
10147            }
10148            ExprKind::DynamicSubCodeRef(expr) => {
10149                let name = self.eval_expr(expr)?.to_string();
10150                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
10151                    StrykeError::runtime(self.undefined_subroutine_resolve_message(&name), line)
10152                })?;
10153                Ok(StrykeValue::code_ref(sub))
10154            }
10155            ExprKind::Deref { expr, kind } => {
10156                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
10157                    let val = self.eval_expr(expr)?;
10158                    let n = self.array_deref_len(val, line)?;
10159                    return Ok(StrykeValue::integer(n));
10160                }
10161                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
10162                    let val = self.eval_expr(expr)?;
10163                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
10164                    return Ok(h.scalar_context());
10165                }
10166                let val = self.eval_expr(expr)?;
10167                self.symbolic_deref(val, *kind, line)
10168            }
10169            ExprKind::ArrowDeref { expr, index, kind } => {
10170                match kind {
10171                    DerefKind::Array => {
10172                        let container = self.eval_arrow_array_base(expr, line)?;
10173                        if let ExprKind::List(indices) = &index.kind {
10174                            let mut out = Vec::with_capacity(indices.len());
10175                            for ix in indices {
10176                                let idx = self.eval_expr(ix)?.to_int();
10177                                out.push(self.read_arrow_array_element(
10178                                    container.clone(),
10179                                    idx,
10180                                    line,
10181                                )?);
10182                            }
10183                            let arr = StrykeValue::array(out);
10184                            if ctx != WantarrayCtx::List {
10185                                let v = arr.to_list();
10186                                return Ok(v.last().cloned().unwrap_or(StrykeValue::UNDEF));
10187                            }
10188                            return Ok(arr);
10189                        }
10190                        let idx = self.eval_expr(index)?.to_int();
10191                        self.read_arrow_array_element(container, idx, line)
10192                    }
10193                    DerefKind::Hash => {
10194                        let val = self.eval_arrow_hash_base(expr, line)?;
10195                        let key = self.eval_expr(index)?.to_string();
10196                        self.read_arrow_hash_element(val, key.as_str(), line)
10197                    }
10198                    DerefKind::Call => {
10199                        // $coderef->(args). BUG-037: explicit `@array` / `%hash`
10200                        // arguments flatten into the call list (mirrors Perl's
10201                        // call list semantics so `$f->(@_)` does not pass
10202                        // `scalar(@_)`). Other arg shapes — `qw(...)`, list
10203                        // expressions, function calls — keep the original
10204                        // scalar-context evaluation so threading-style call
10205                        // sites (`~> qw(a b c d) fn { ... }`) pass the LHS as
10206                        // one threaded value rather than as flattened elements.
10207                        let val = self.eval_expr(expr)?;
10208                        if let ExprKind::List(ref arg_exprs) = index.kind {
10209                            let mut args = Vec::with_capacity(arg_exprs.len());
10210                            for a in arg_exprs {
10211                                if matches!(a.kind, ExprKind::ArrayVar(_) | ExprKind::HashVar(_)) {
10212                                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
10213                                    if let Some(items) = v.as_array_vec() {
10214                                        args.extend(items);
10215                                    } else {
10216                                        args.push(v);
10217                                    }
10218                                } else {
10219                                    args.push(self.eval_expr(a)?);
10220                                }
10221                            }
10222                            // Auto-deref ScalarRef for closure self-reference: $f->()
10223                            let callable = if let Some(inner) = val.as_scalar_ref() {
10224                                inner.read().clone()
10225                            } else {
10226                                val
10227                            };
10228                            if let Some(sub) = callable.as_code_ref() {
10229                                return self.call_sub(&sub, args, ctx, line);
10230                            }
10231                            Err(StrykeError::runtime("Not a code reference", line).into())
10232                        } else {
10233                            Err(StrykeError::runtime("Invalid call deref", line).into())
10234                        }
10235                    }
10236                }
10237            }
10238
10239            // Binary operators
10240            ExprKind::BinOp { left, op, right } => {
10241                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
10242                match op {
10243                    BinOp::BindMatch => {
10244                        let lv = self.eval_expr(left)?;
10245                        let rv = self.eval_expr(right)?;
10246                        let s = lv.to_string();
10247                        let pat = rv.to_string();
10248                        return self.regex_match_execute(s, &pat, "", false, "_", line);
10249                    }
10250                    BinOp::BindNotMatch => {
10251                        let lv = self.eval_expr(left)?;
10252                        let rv = self.eval_expr(right)?;
10253                        let s = lv.to_string();
10254                        let pat = rv.to_string();
10255                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
10256                        return Ok(StrykeValue::integer(if m.is_true() { 0 } else { 1 }));
10257                    }
10258                    BinOp::LogAnd | BinOp::LogAndWord => {
10259                        match &left.kind {
10260                            ExprKind::Regex(_, _) => {
10261                                if !self.eval_boolean_rvalue_condition(left)? {
10262                                    return Ok(StrykeValue::string(String::new()));
10263                                }
10264                            }
10265                            _ => {
10266                                let lv = self.eval_expr(left)?;
10267                                if !lv.is_true() {
10268                                    return Ok(lv);
10269                                }
10270                            }
10271                        }
10272                        return match &right.kind {
10273                            ExprKind::Regex(_, _) => Ok(StrykeValue::integer(
10274                                if self.eval_boolean_rvalue_condition(right)? {
10275                                    1
10276                                } else {
10277                                    0
10278                                },
10279                            )),
10280                            _ => self.eval_expr(right),
10281                        };
10282                    }
10283                    BinOp::LogOr | BinOp::LogOrWord => {
10284                        match &left.kind {
10285                            ExprKind::Regex(_, _) => {
10286                                if self.eval_boolean_rvalue_condition(left)? {
10287                                    return Ok(StrykeValue::integer(1));
10288                                }
10289                            }
10290                            _ => {
10291                                let lv = self.eval_expr(left)?;
10292                                if lv.is_true() {
10293                                    return Ok(lv);
10294                                }
10295                            }
10296                        }
10297                        return match &right.kind {
10298                            ExprKind::Regex(_, _) => Ok(StrykeValue::integer(
10299                                if self.eval_boolean_rvalue_condition(right)? {
10300                                    1
10301                                } else {
10302                                    0
10303                                },
10304                            )),
10305                            _ => self.eval_expr(right),
10306                        };
10307                    }
10308                    BinOp::DefinedOr => {
10309                        let lv = self.eval_expr(left)?;
10310                        if !lv.is_undef() {
10311                            return Ok(lv);
10312                        }
10313                        return self.eval_expr(right);
10314                    }
10315                    _ => {}
10316                }
10317                let lv = self.eval_expr(left)?;
10318                let rv = self.eval_expr(right)?;
10319                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
10320                    return r;
10321                }
10322                self.eval_binop(*op, &lv, &rv, line)
10323            }
10324
10325            // Unary
10326            ExprKind::UnaryOp { op, expr } => match op {
10327                UnaryOp::PreIncrement => {
10328                    if let ExprKind::ScalarVar(name) = &expr.kind {
10329                        self.check_strict_scalar_var(name, line)?;
10330                        let n = self.resolved_scalar_storage_name(name);
10331                        return Ok(self
10332                            .scope
10333                            .atomic_mutate(&n, perl_inc)
10334                            .map_err(|e| e.at_line(line))?);
10335                    }
10336                    if let ExprKind::Deref { kind, .. } = &expr.kind {
10337                        if matches!(kind, Sigil::Array | Sigil::Hash) {
10338                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
10339                                *kind, true, true, line,
10340                            ));
10341                        }
10342                    }
10343                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
10344                        let href = self.eval_expr(container)?;
10345                        let mut key_vals = Vec::with_capacity(keys.len());
10346                        for key_expr in keys {
10347                            key_vals.push(self.eval_expr(key_expr)?);
10348                        }
10349                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
10350                    }
10351                    if let ExprKind::ArrowDeref {
10352                        expr: arr_expr,
10353                        index,
10354                        kind: DerefKind::Array,
10355                    } = &expr.kind
10356                    {
10357                        if let ExprKind::List(indices) = &index.kind {
10358                            let container = self.eval_arrow_array_base(arr_expr, line)?;
10359                            let mut idxs = Vec::with_capacity(indices.len());
10360                            for ix in indices {
10361                                idxs.push(self.eval_expr(ix)?.to_int());
10362                            }
10363                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
10364                        }
10365                    }
10366                    let val = self.eval_expr(expr)?;
10367                    let new_val = perl_inc(&val);
10368                    self.assign_value(expr, new_val.clone())?;
10369                    Ok(new_val)
10370                }
10371                UnaryOp::PreDecrement => {
10372                    if let ExprKind::ScalarVar(name) = &expr.kind {
10373                        self.check_strict_scalar_var(name, line)?;
10374                        let n = self.resolved_scalar_storage_name(name);
10375                        return Ok(self
10376                            .scope
10377                            .atomic_mutate(&n, |v| StrykeValue::integer(v.to_int() - 1))
10378                            .map_err(|e| e.at_line(line))?);
10379                    }
10380                    if let ExprKind::Deref { kind, .. } = &expr.kind {
10381                        if matches!(kind, Sigil::Array | Sigil::Hash) {
10382                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
10383                                *kind, true, false, line,
10384                            ));
10385                        }
10386                    }
10387                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
10388                        let href = self.eval_expr(container)?;
10389                        let mut key_vals = Vec::with_capacity(keys.len());
10390                        for key_expr in keys {
10391                            key_vals.push(self.eval_expr(key_expr)?);
10392                        }
10393                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
10394                    }
10395                    if let ExprKind::ArrowDeref {
10396                        expr: arr_expr,
10397                        index,
10398                        kind: DerefKind::Array,
10399                    } = &expr.kind
10400                    {
10401                        if let ExprKind::List(indices) = &index.kind {
10402                            let container = self.eval_arrow_array_base(arr_expr, line)?;
10403                            let mut idxs = Vec::with_capacity(indices.len());
10404                            for ix in indices {
10405                                idxs.push(self.eval_expr(ix)?.to_int());
10406                            }
10407                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
10408                        }
10409                    }
10410                    let val = self.eval_expr(expr)?;
10411                    let new_val = StrykeValue::integer(val.to_int() - 1);
10412                    self.assign_value(expr, new_val.clone())?;
10413                    Ok(new_val)
10414                }
10415                _ => {
10416                    match op {
10417                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
10418                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
10419                                let topic = self.scope.get_scalar("_");
10420                                let rl = expr.line;
10421                                let s = topic.to_string();
10422                                let v =
10423                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
10424                                return Ok(StrykeValue::integer(if v.is_true() { 0 } else { 1 }));
10425                            }
10426                        }
10427                        _ => {}
10428                    }
10429                    let val = self.eval_expr(expr)?;
10430                    match op {
10431                        UnaryOp::Negate => {
10432                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
10433                                return r;
10434                            }
10435                            if let Some(n) = val.as_integer() {
10436                                Ok(StrykeValue::integer(-n))
10437                            } else {
10438                                Ok(StrykeValue::float(-val.to_number()))
10439                            }
10440                        }
10441                        UnaryOp::LogNot => {
10442                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
10443                                let pv = r?;
10444                                return Ok(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
10445                            }
10446                            Ok(StrykeValue::integer(if val.is_true() { 0 } else { 1 }))
10447                        }
10448                        UnaryOp::BitNot => Ok(StrykeValue::integer(!val.to_int())),
10449                        UnaryOp::LogNotWord => {
10450                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
10451                                let pv = r?;
10452                                return Ok(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
10453                            }
10454                            Ok(StrykeValue::integer(if val.is_true() { 0 } else { 1 }))
10455                        }
10456                        UnaryOp::Ref => {
10457                            if let ExprKind::ScalarVar(name) = &expr.kind {
10458                                return Ok(StrykeValue::scalar_binding_ref(name.clone()));
10459                            }
10460                            Ok(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))))
10461                        }
10462                        _ => unreachable!(),
10463                    }
10464                }
10465            },
10466
10467            ExprKind::PostfixOp { expr, op } => {
10468                // For scalar variables, use atomic_mutate_post to hold the lock
10469                // for the entire read-modify-write (critical for mysync).
10470                if let ExprKind::ScalarVar(name) = &expr.kind {
10471                    self.check_strict_scalar_var(name, line)?;
10472                    let n = self.resolved_scalar_storage_name(name);
10473                    let f: fn(&StrykeValue) -> StrykeValue = match op {
10474                        PostfixOp::Increment => |v| perl_inc(v),
10475                        PostfixOp::Decrement => |v| StrykeValue::integer(v.to_int() - 1),
10476                    };
10477                    return Ok(self
10478                        .scope
10479                        .atomic_mutate_post(&n, f)
10480                        .map_err(|e| e.at_line(line))?);
10481                }
10482                if let ExprKind::Deref { kind, .. } = &expr.kind {
10483                    if matches!(kind, Sigil::Array | Sigil::Hash) {
10484                        let is_inc = matches!(op, PostfixOp::Increment);
10485                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
10486                            *kind, false, is_inc, line,
10487                        ));
10488                    }
10489                }
10490                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
10491                    let href = self.eval_expr(container)?;
10492                    let mut key_vals = Vec::with_capacity(keys.len());
10493                    for key_expr in keys {
10494                        key_vals.push(self.eval_expr(key_expr)?);
10495                    }
10496                    let kind_byte = match op {
10497                        PostfixOp::Increment => 2u8,
10498                        PostfixOp::Decrement => 3u8,
10499                    };
10500                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
10501                }
10502                if let ExprKind::ArrowDeref {
10503                    expr: arr_expr,
10504                    index,
10505                    kind: DerefKind::Array,
10506                } = &expr.kind
10507                {
10508                    if let ExprKind::List(indices) = &index.kind {
10509                        let container = self.eval_arrow_array_base(arr_expr, line)?;
10510                        let mut idxs = Vec::with_capacity(indices.len());
10511                        for ix in indices {
10512                            idxs.push(self.eval_expr(ix)?.to_int());
10513                        }
10514                        let kind_byte = match op {
10515                            PostfixOp::Increment => 2u8,
10516                            PostfixOp::Decrement => 3u8,
10517                        };
10518                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
10519                    }
10520                }
10521                let val = self.eval_expr(expr)?;
10522                let old = val.clone();
10523                let new_val = match op {
10524                    PostfixOp::Increment => perl_inc(&val),
10525                    PostfixOp::Decrement => StrykeValue::integer(val.to_int() - 1),
10526                };
10527                self.assign_value(expr, new_val)?;
10528                Ok(old)
10529            }
10530
10531            // Assignment
10532            ExprKind::Assign { target, value } => {
10533                if let ExprKind::Typeglob(lhs) = &target.kind {
10534                    if let ExprKind::Typeglob(rhs) = &value.kind {
10535                        self.copy_typeglob_slots(lhs, rhs, line)?;
10536                        return self.eval_expr(value);
10537                    }
10538                }
10539                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
10540                self.assign_value(target, val.clone())?;
10541                Ok(val)
10542            }
10543            ExprKind::CompoundAssign { target, op, value } => {
10544                // For scalar targets, use atomic_mutate to hold the lock.
10545                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
10546                if let ExprKind::ScalarVar(name) = &target.kind {
10547                    self.check_strict_scalar_var(name, line)?;
10548                    let n = self.resolved_scalar_storage_name(name);
10549                    let op = *op;
10550                    let rhs = match op {
10551                        BinOp::LogOr => {
10552                            let old = self.scope.get_scalar(&n);
10553                            if old.is_true() {
10554                                return Ok(old);
10555                            }
10556                            self.eval_expr(value)?
10557                        }
10558                        BinOp::DefinedOr => {
10559                            let old = self.scope.get_scalar(&n);
10560                            if !old.is_undef() {
10561                                return Ok(old);
10562                            }
10563                            self.eval_expr(value)?
10564                        }
10565                        BinOp::LogAnd => {
10566                            let old = self.scope.get_scalar(&n);
10567                            if !old.is_true() {
10568                                return Ok(old);
10569                            }
10570                            self.eval_expr(value)?
10571                        }
10572                        _ => self.eval_expr(value)?,
10573                    };
10574                    return Ok(self.scalar_compound_assign_scalar_target(&n, op, rhs)?);
10575                }
10576                let rhs = self.eval_expr(value)?;
10577                // For hash element targets: $h{key} += 1
10578                if let ExprKind::HashElement { hash, key } = &target.kind {
10579                    self.check_strict_hash_var(hash, line)?;
10580                    let k = self.eval_expr(key)?.to_string();
10581                    let op = *op;
10582                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
10583                        BinOp::Add => {
10584                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10585                                StrykeValue::integer(a.wrapping_add(b))
10586                            } else {
10587                                StrykeValue::float(old.to_number() + rhs.to_number())
10588                            }
10589                        }
10590                        BinOp::Sub => {
10591                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10592                                StrykeValue::integer(a.wrapping_sub(b))
10593                            } else {
10594                                StrykeValue::float(old.to_number() - rhs.to_number())
10595                            }
10596                        }
10597                        BinOp::Concat => {
10598                            let mut s = old.to_string();
10599                            rhs.append_to(&mut s);
10600                            StrykeValue::string(s)
10601                        }
10602                        _ => StrykeValue::float(old.to_number() + rhs.to_number()),
10603                    })?);
10604                }
10605                // For array element targets: $a[i] += 1
10606                if let ExprKind::ArrayElement { array, index } = &target.kind {
10607                    self.check_strict_array_var(array, line)?;
10608                    let idx = self.eval_expr(index)?.to_int();
10609                    let op = *op;
10610                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
10611                        BinOp::Add => {
10612                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10613                                StrykeValue::integer(a.wrapping_add(b))
10614                            } else {
10615                                StrykeValue::float(old.to_number() + rhs.to_number())
10616                            }
10617                        }
10618                        BinOp::Sub => {
10619                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10620                                StrykeValue::integer(a.wrapping_sub(b))
10621                            } else {
10622                                StrykeValue::float(old.to_number() - rhs.to_number())
10623                            }
10624                        }
10625                        BinOp::Mul => {
10626                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10627                                StrykeValue::integer(a.wrapping_mul(b))
10628                            } else {
10629                                StrykeValue::float(old.to_number() * rhs.to_number())
10630                            }
10631                        }
10632                        BinOp::Div => StrykeValue::float(old.to_number() / rhs.to_number()),
10633                        BinOp::Mod => {
10634                            // Perl `%` is floored-division (sign-of-divisor),
10635                            // not Rust's `%` (sign-of-dividend) nor
10636                            // `rem_euclid` (always non-negative). Truncate
10637                            // float operands to int first, matching Perl 5.
10638                            let a = old.to_int();
10639                            let b = rhs.to_int();
10640                            if b == 0 {
10641                                StrykeValue::integer(0)
10642                            } else {
10643                                StrykeValue::integer(crate::value::perl_mod_i64(a, b))
10644                            }
10645                        }
10646                        BinOp::Concat => {
10647                            let mut s = old.to_string();
10648                            rhs.append_to(&mut s);
10649                            StrykeValue::string(s)
10650                        }
10651                        BinOp::Pow => StrykeValue::float(old.to_number().powf(rhs.to_number())),
10652                        BinOp::BitAnd => StrykeValue::integer(old.to_int() & rhs.to_int()),
10653                        BinOp::BitOr => StrykeValue::integer(old.to_int() | rhs.to_int()),
10654                        BinOp::BitXor => StrykeValue::integer(old.to_int() ^ rhs.to_int()),
10655                        BinOp::ShiftLeft => {
10656                            StrykeValue::integer(perl_shl_i64(old.to_int(), rhs.to_int()))
10657                        }
10658                        BinOp::ShiftRight => {
10659                            StrykeValue::integer(perl_shr_i64(old.to_int(), rhs.to_int()))
10660                        }
10661                        _ => StrykeValue::float(old.to_number() + rhs.to_number()),
10662                    })?);
10663                }
10664                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
10665                    let href = self.eval_expr(container)?;
10666                    let mut key_vals = Vec::with_capacity(keys.len());
10667                    for key_expr in keys {
10668                        key_vals.push(self.eval_expr(key_expr)?);
10669                    }
10670                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
10671                }
10672                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
10673                    if let ExprKind::Deref {
10674                        expr: inner,
10675                        kind: Sigil::Array,
10676                    } = &source.kind
10677                    {
10678                        let container = self.eval_arrow_array_base(inner, line)?;
10679                        let idxs = self.flatten_array_slice_index_specs(indices)?;
10680                        return self
10681                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
10682                    }
10683                }
10684                if let ExprKind::ArrowDeref {
10685                    expr: arr_expr,
10686                    index,
10687                    kind: DerefKind::Array,
10688                } = &target.kind
10689                {
10690                    if let ExprKind::List(indices) = &index.kind {
10691                        let container = self.eval_arrow_array_base(arr_expr, line)?;
10692                        let mut idxs = Vec::with_capacity(indices.len());
10693                        for ix in indices {
10694                            idxs.push(self.eval_expr(ix)?.to_int());
10695                        }
10696                        return self
10697                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
10698                    }
10699                }
10700                let old = self.eval_expr(target)?;
10701                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
10702                self.assign_value(target, new_val.clone())?;
10703                Ok(new_val)
10704            }
10705
10706            // Ternary — propagate wantarray context to both branches so
10707            // `($a, $b) = $c ? (1, 2) : (3, 4)` evaluates the chosen branch
10708            // in list context.
10709            ExprKind::Ternary {
10710                condition,
10711                then_expr,
10712                else_expr,
10713            } => {
10714                if self.eval_boolean_rvalue_condition(condition)? {
10715                    self.eval_expr_ctx(then_expr, ctx)
10716                } else {
10717                    self.eval_expr_ctx(else_expr, ctx)
10718                }
10719            }
10720
10721            // Range
10722            ExprKind::Range {
10723                from,
10724                to,
10725                exclusive,
10726                step,
10727            } => {
10728                if ctx == WantarrayCtx::List {
10729                    let f = self.eval_expr(from)?;
10730                    let t = self.eval_expr(to)?;
10731                    if let Some(s) = step {
10732                        let step_val = self.eval_expr(s)?.to_int();
10733                        let from_i = f.to_int();
10734                        let to_i = t.to_int();
10735                        let list = if step_val == 0 {
10736                            vec![]
10737                        } else if step_val > 0 {
10738                            (from_i..=to_i)
10739                                .step_by(step_val as usize)
10740                                .map(StrykeValue::integer)
10741                                .collect()
10742                        } else {
10743                            std::iter::successors(Some(from_i), |&x| {
10744                                let next = x - step_val.abs();
10745                                if next >= to_i {
10746                                    Some(next)
10747                                } else {
10748                                    None
10749                                }
10750                            })
10751                            .map(StrykeValue::integer)
10752                            .collect()
10753                        };
10754                        Ok(StrykeValue::array(list))
10755                    } else {
10756                        let list = perl_list_range_expand(f, t);
10757                        Ok(StrykeValue::array(list))
10758                    }
10759                } else {
10760                    let key = std::ptr::from_ref(expr) as usize;
10761                    match (&from.kind, &to.kind) {
10762                        (
10763                            ExprKind::Regex(left_pat, left_flags),
10764                            ExprKind::Regex(right_pat, right_flags),
10765                        ) => {
10766                            let dot = self.scalar_flipflop_dot_line();
10767                            let subject = self.scope.get_scalar("_").to_string();
10768                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10769                                |e| match e {
10770                                    FlowOrError::Error(err) => err,
10771                                    FlowOrError::Flow(_) => StrykeError::runtime(
10772                                        "unexpected flow in regex flip-flop",
10773                                        line,
10774                                    ),
10775                                },
10776                            )?;
10777                            let right_re = self
10778                                .compile_regex(right_pat, right_flags, line)
10779                                .map_err(|e| match e {
10780                                    FlowOrError::Error(err) => err,
10781                                    FlowOrError::Flow(_) => StrykeError::runtime(
10782                                        "unexpected flow in regex flip-flop",
10783                                        line,
10784                                    ),
10785                                })?;
10786                            let left_m = left_re.is_match(&subject);
10787                            let right_m = right_re.is_match(&subject);
10788                            let st = self.flip_flop_tree.entry(key).or_default();
10789                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10790                                &mut st.active,
10791                                &mut st.exclusive_left_line,
10792                                *exclusive,
10793                                dot,
10794                                left_m,
10795                                right_m,
10796                            )))
10797                        }
10798                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
10799                            let dot = self.scalar_flipflop_dot_line();
10800                            let subject = self.scope.get_scalar("_").to_string();
10801                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10802                                |e| match e {
10803                                    FlowOrError::Error(err) => err,
10804                                    FlowOrError::Flow(_) => StrykeError::runtime(
10805                                        "unexpected flow in regex/eof flip-flop",
10806                                        line,
10807                                    ),
10808                                },
10809                            )?;
10810                            let left_m = left_re.is_match(&subject);
10811                            let right_m = self.eof_without_arg_is_true();
10812                            let st = self.flip_flop_tree.entry(key).or_default();
10813                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10814                                &mut st.active,
10815                                &mut st.exclusive_left_line,
10816                                *exclusive,
10817                                dot,
10818                                left_m,
10819                                right_m,
10820                            )))
10821                        }
10822                        (
10823                            ExprKind::Regex(left_pat, left_flags),
10824                            ExprKind::Integer(_) | ExprKind::Float(_),
10825                        ) => {
10826                            let dot = self.scalar_flipflop_dot_line();
10827                            let right = self.eval_expr(to)?.to_int();
10828                            let subject = self.scope.get_scalar("_").to_string();
10829                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10830                                |e| match e {
10831                                    FlowOrError::Error(err) => err,
10832                                    FlowOrError::Flow(_) => StrykeError::runtime(
10833                                        "unexpected flow in regex flip-flop",
10834                                        line,
10835                                    ),
10836                                },
10837                            )?;
10838                            let left_m = left_re.is_match(&subject);
10839                            let right_m = dot == right;
10840                            let st = self.flip_flop_tree.entry(key).or_default();
10841                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10842                                &mut st.active,
10843                                &mut st.exclusive_left_line,
10844                                *exclusive,
10845                                dot,
10846                                left_m,
10847                                right_m,
10848                            )))
10849                        }
10850                        (ExprKind::Regex(left_pat, left_flags), _) => {
10851                            if let ExprKind::Eof(Some(_)) = &to.kind {
10852                                return Err(FlowOrError::Error(StrykeError::runtime(
10853                                    "regex flip-flop with eof(HANDLE) is not supported",
10854                                    line,
10855                                )));
10856                            }
10857                            let dot = self.scalar_flipflop_dot_line();
10858                            let subject = self.scope.get_scalar("_").to_string();
10859                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10860                                |e| match e {
10861                                    FlowOrError::Error(err) => err,
10862                                    FlowOrError::Flow(_) => StrykeError::runtime(
10863                                        "unexpected flow in regex flip-flop",
10864                                        line,
10865                                    ),
10866                                },
10867                            )?;
10868                            let left_m = left_re.is_match(&subject);
10869                            let right_m = self.eval_boolean_rvalue_condition(to)?;
10870                            let st = self.flip_flop_tree.entry(key).or_default();
10871                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10872                                &mut st.active,
10873                                &mut st.exclusive_left_line,
10874                                *exclusive,
10875                                dot,
10876                                left_m,
10877                                right_m,
10878                            )))
10879                        }
10880                        _ => {
10881                            let left = self.eval_expr(from)?.to_int();
10882                            let right = self.eval_expr(to)?.to_int();
10883                            let dot = self.scalar_flipflop_dot_line();
10884                            let st = self.flip_flop_tree.entry(key).or_default();
10885                            if !st.active {
10886                                if dot == left {
10887                                    st.active = true;
10888                                    if *exclusive {
10889                                        st.exclusive_left_line = Some(dot);
10890                                    } else {
10891                                        st.exclusive_left_line = None;
10892                                        if dot == right {
10893                                            st.active = false;
10894                                        }
10895                                    }
10896                                    return Ok(StrykeValue::integer(1));
10897                                }
10898                                return Ok(StrykeValue::integer(0));
10899                            }
10900                            if let Some(ll) = st.exclusive_left_line {
10901                                if dot == right && dot > ll {
10902                                    st.active = false;
10903                                    st.exclusive_left_line = None;
10904                                }
10905                            } else if dot == right {
10906                                st.active = false;
10907                            }
10908                            Ok(StrykeValue::integer(1))
10909                        }
10910                    }
10911                }
10912            }
10913
10914            // SliceRange — open-ended Python-style slice expansion. Reachable from the
10915            // tree-walker when slice subscripts are evaluated outside the VM (rare; VM is
10916            // the primary execution engine). Only closed forms (`from:to[:step]`) can be
10917            // expanded here without container length context; open ends require a slice
10918            // op (`Op::ArraySliceRange` / `Op::HashSliceRange`) which knows the container.
10919            ExprKind::SliceRange { from, to, step } => {
10920                let f = match from {
10921                    Some(e) => self.eval_expr(e)?,
10922                    None => {
10923                        return Err(StrykeError::runtime(
10924                            "open-ended slice range cannot be evaluated outside slice subscript",
10925                            line,
10926                        )
10927                        .into());
10928                    }
10929                };
10930                let t = match to {
10931                    Some(e) => self.eval_expr(e)?,
10932                    None => {
10933                        return Err(StrykeError::runtime(
10934                            "open-ended slice range cannot be evaluated outside slice subscript",
10935                            line,
10936                        )
10937                        .into());
10938                    }
10939                };
10940                let list = if let Some(s) = step {
10941                    let sv = self.eval_expr(s)?;
10942                    crate::value::perl_list_range_expand_stepped(f, t, sv)
10943                } else {
10944                    perl_list_range_expand(f, t)
10945                };
10946                Ok(StrykeValue::array(list))
10947            }
10948
10949            // Repeat — see `ast.rs` `ExprKind::Repeat` for the list/scalar split.
10950            ExprKind::Repeat {
10951                expr,
10952                count,
10953                list_repeat,
10954            } => {
10955                let n = self.eval_expr(count)?.to_int().max(0) as usize;
10956                if *list_repeat {
10957                    // `(LIST) x N` — evaluate the LHS in list context, replicate.
10958                    let saved = self.wantarray_kind;
10959                    self.wantarray_kind = WantarrayCtx::List;
10960                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10961                    self.wantarray_kind = saved;
10962                    let items: Vec<StrykeValue> = val.as_array_vec().unwrap_or_else(|| vec![val]);
10963                    let mut result = Vec::with_capacity(items.len() * n);
10964                    for _ in 0..n {
10965                        result.extend(items.iter().cloned());
10966                    }
10967                    Ok(StrykeValue::array(result))
10968                } else {
10969                    // `EXPR x N` — scalar string repetition.
10970                    let val = self.eval_expr(expr)?;
10971                    Ok(StrykeValue::string(val.to_string().repeat(n)))
10972                }
10973            }
10974
10975            // `my $x = …` / `our` / `state` / `local` used as an expression
10976            // (e.g. `if (my $line = readline)`).  Declare each variable in the
10977            // current scope, evaluate the initializer (if any), and return the
10978            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
10979            ExprKind::MyExpr { keyword, decls } => {
10980                // Build a temporary statement and dispatch to the canonical
10981                // statement handler so behavior matches `my $x = …;` exactly.
10982                let stmt_kind = match keyword.as_str() {
10983                    "my" => StmtKind::My(decls.clone()),
10984                    "our" => StmtKind::Our(decls.clone()),
10985                    "state" => StmtKind::State(decls.clone()),
10986                    "local" => StmtKind::Local(decls.clone()),
10987                    _ => StmtKind::My(decls.clone()),
10988                };
10989                let stmt = Statement {
10990                    label: None,
10991                    kind: stmt_kind,
10992                    line,
10993                };
10994                self.exec_statement(&stmt)?;
10995                // Return the value of the (first) declared variable so the
10996                // surrounding expression sees the assigned value, matching
10997                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
10998                let first = decls.first().ok_or_else(|| {
10999                    FlowOrError::Error(StrykeError::runtime("MyExpr: empty decl list", line))
11000                })?;
11001                Ok(match first.sigil {
11002                    Sigil::Scalar => self.scope.get_scalar(&first.name),
11003                    Sigil::Array => StrykeValue::array(self.scope.get_array(&first.name)),
11004                    Sigil::Hash => {
11005                        let h = self.scope.get_hash(&first.name);
11006                        let mut flat: Vec<StrykeValue> = Vec::with_capacity(h.len() * 2);
11007                        for (k, v) in h {
11008                            flat.push(StrykeValue::string(k));
11009                            flat.push(v);
11010                        }
11011                        StrykeValue::array(flat)
11012                    }
11013                    Sigil::Typeglob => StrykeValue::UNDEF,
11014                })
11015            }
11016
11017            // Function calls
11018            ExprKind::FuncCall { name, args } => {
11019                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
11020                // bare-name dispatch so the matches below stay flat.
11021                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
11022                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
11023                if matches!(dispatch_name, "read") && args.len() >= 3 {
11024                    let fh_val = self.eval_expr(&args[0])?;
11025                    let fh = fh_val
11026                        .as_io_handle_name()
11027                        .unwrap_or_else(|| fh_val.to_string());
11028                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
11029                    let offset = if args.len() > 3 {
11030                        self.eval_expr(&args[3])?.to_int().max(0) as usize
11031                    } else {
11032                        0
11033                    };
11034                    // Extract the variable name from the AST
11035                    let var_name = match &args[1].kind {
11036                        ExprKind::ScalarVar(n) => n.clone(),
11037                        _ => self.eval_expr(&args[1])?.to_string(),
11038                    };
11039                    let mut buf = vec![0u8; len];
11040                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
11041                        slot.lock().read(&mut buf).unwrap_or(0)
11042                    } else if fh == "STDIN" {
11043                        std::io::stdin().read(&mut buf).unwrap_or(0)
11044                    } else {
11045                        return Err(StrykeError::runtime(
11046                            format!("read: unopened handle {}", fh),
11047                            line,
11048                        )
11049                        .into());
11050                    };
11051                    buf.truncate(n);
11052                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
11053                    if offset > 0 {
11054                        let mut existing = self.scope.get_scalar(&var_name).to_string();
11055                        while existing.len() < offset {
11056                            existing.push('\0');
11057                        }
11058                        existing.push_str(&read_str);
11059                        let _ = self
11060                            .scope
11061                            .set_scalar(&var_name, StrykeValue::string(existing));
11062                    } else {
11063                        let _ = self
11064                            .scope
11065                            .set_scalar(&var_name, StrykeValue::string(read_str));
11066                    }
11067                    return Ok(StrykeValue::integer(n as i64));
11068                }
11069                if matches!(dispatch_name, "group_by" | "chunk_by") {
11070                    if args.len() != 2 {
11071                        return Err(StrykeError::runtime(
11072                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
11073                            line,
11074                        )
11075                        .into());
11076                    }
11077                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
11078                }
11079                if matches!(dispatch_name, "puniq" | "pfirst" | "pany") {
11080                    let mut arg_vals = Vec::with_capacity(args.len());
11081                    for a in args {
11082                        arg_vals.push(self.eval_expr(a)?);
11083                    }
11084                    let saved_wa = self.wantarray_kind;
11085                    self.wantarray_kind = ctx;
11086                    let r = self.eval_par_list_call(dispatch_name, &arg_vals, ctx, line);
11087                    self.wantarray_kind = saved_wa;
11088                    return r.map_err(Into::into);
11089                }
11090                let arg_vals = if matches!(dispatch_name, "any" | "all" | "none" | "first")
11091                    || matches!(
11092                        dispatch_name,
11093                        "take_while"
11094                            | "drop_while"
11095                            | "skip_while"
11096                            | "reject"
11097                            | "grepv"
11098                            | "tap"
11099                            | "peek"
11100                    )
11101                    || matches!(
11102                        dispatch_name,
11103                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
11104                    ) {
11105                    if args.len() != 2 {
11106                        return Err(StrykeError::runtime(
11107                            format!("{}: expected BLOCK, LIST", name),
11108                            line,
11109                        )
11110                        .into());
11111                    }
11112                    let cr = self.eval_expr(&args[0])?;
11113                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
11114                    let mut v = vec![cr];
11115                    v.extend(list_src.to_list());
11116                    v
11117                } else if matches!(
11118                    dispatch_name,
11119                    "zip"
11120                        | "zip_longest"
11121                        | "zip_shortest"
11122                        | "mesh"
11123                        | "mesh_longest"
11124                        | "mesh_shortest"
11125                ) {
11126                    let mut v = Vec::with_capacity(args.len());
11127                    for a in args {
11128                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
11129                    }
11130                    v
11131                } else if matches!(
11132                    dispatch_name,
11133                    "count" | "size" | "cnt" | "len" | "list_count" | "list_size"
11134                ) {
11135                    // Count-family: preserve the "user wrote 1 syntactic arg" signal.
11136                    // Flattening the lone operand here collapses `count(@empty)` to a
11137                    // zero-arg call, which would then fall back to `$_` topic — wrong.
11138                    // Pass the single evaluated value directly so the builtin's 1-arg
11139                    // path can dispatch on its type (string → chars, array/aref →
11140                    // element count via map_flatten_outputs, hash → key count, …).
11141                    let mut list_out = Vec::new();
11142                    if args.len() == 1 {
11143                        list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
11144                    } else {
11145                        for a in args {
11146                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
11147                        }
11148                    }
11149                    list_out
11150                } else if matches!(
11151                    dispatch_name,
11152                    "uniq"
11153                        | "distinct"
11154                        | "uniqstr"
11155                        | "uniqint"
11156                        | "uniqnum"
11157                        | "flatten"
11158                        | "set"
11159                        | "with_index"
11160                        | "shuffle"
11161                        | "sum"
11162                        | "sum0"
11163                        | "product"
11164                        | "min"
11165                        | "max"
11166                        | "minstr"
11167                        | "maxstr"
11168                        | "mean"
11169                        | "median"
11170                        | "mode"
11171                        | "stddev"
11172                        | "variance"
11173                        | "pairs"
11174                        | "unpairs"
11175                        | "pairkeys"
11176                        | "pairvalues"
11177                ) {
11178                    // Slurpy list `(@)`: one list expr (`uniq @x`) or multiple actuals
11179                    // (`uniq(1, 1, 2)`). Each actual is evaluated in list context so
11180                    // `@a, @b` flattens.
11181                    let mut list_out = Vec::new();
11182                    if args.len() == 1 {
11183                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
11184                    } else {
11185                        for a in args {
11186                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
11187                        }
11188                    }
11189                    list_out
11190                } else if matches!(dispatch_name, "take" | "head" | "tail" | "drop") {
11191                    if args.is_empty() {
11192                        return Err(StrykeError::runtime(
11193                            "take/head/tail/drop: need LIST..., N or unary N",
11194                            line,
11195                        )
11196                        .into());
11197                    }
11198                    let mut arg_vals = Vec::with_capacity(args.len());
11199                    if args.len() == 1 {
11200                        // head @l == head @l, 1 — evaluate in list context
11201                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
11202                    } else {
11203                        for a in &args[..args.len() - 1] {
11204                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
11205                        }
11206                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
11207                    }
11208                    arg_vals
11209                } else if matches!(dispatch_name, "chunked" | "windowed") {
11210                    let mut list_out = Vec::new();
11211                    match args.len() {
11212                        0 => {
11213                            return Err(StrykeError::runtime(
11214                                format!("{name}: expected (LIST, N) or unary N after |>"),
11215                                line,
11216                            )
11217                            .into());
11218                        }
11219                        1 => {
11220                            // chunked @l / windowed @l — evaluate in list context, default size
11221                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
11222                        }
11223                        2 => {
11224                            list_out.extend(
11225                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
11226                            );
11227                            list_out.push(self.eval_expr(&args[1])?);
11228                        }
11229                        _ => {
11230                            return Err(StrykeError::runtime(
11231                                format!(
11232                                    "{name}: expected exactly (LIST, N); use one list expression then size"
11233                                ),
11234                                line,
11235                            )
11236                            .into());
11237                        }
11238                    }
11239                    list_out
11240                } else {
11241                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
11242                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
11243                    let mut arg_vals = Vec::with_capacity(args.len());
11244                    for a in args {
11245                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
11246                        if let Some(items) = v.as_array_vec() {
11247                            arg_vals.extend(items);
11248                        } else {
11249                            arg_vals.push(v);
11250                        }
11251                    }
11252                    arg_vals
11253                };
11254                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
11255                let saved_wa = self.wantarray_kind;
11256                self.wantarray_kind = ctx;
11257                // Builtins first — immune to monkey-patching (matches VM dispatch order).
11258                // In compat mode, user subs shadow builtins (Perl 5 semantics).
11259                if !crate::compat_mode() {
11260                    if matches!(
11261                        dispatch_name,
11262                        "take_while"
11263                            | "drop_while"
11264                            | "skip_while"
11265                            | "reject"
11266                            | "grepv"
11267                            | "tap"
11268                            | "peek"
11269                    ) {
11270                        let r =
11271                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
11272                        self.wantarray_kind = saved_wa;
11273                        return r.map_err(Into::into);
11274                    }
11275                    if let Some(r) =
11276                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
11277                    {
11278                        self.wantarray_kind = saved_wa;
11279                        return r.map_err(Into::into);
11280                    }
11281                }
11282                if let Some(sub) = self.resolve_sub_by_name(name) {
11283                    self.wantarray_kind = saved_wa;
11284                    let args = self.with_topic_default_args(arg_vals);
11285                    let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
11286                    return self.call_sub_with_package(&sub, args, ctx, line, pkg);
11287                }
11288                // Compat mode: check builtins after user subs (Perl 5 semantics).
11289                if crate::compat_mode() {
11290                    if matches!(
11291                        dispatch_name,
11292                        "take_while"
11293                            | "drop_while"
11294                            | "skip_while"
11295                            | "reject"
11296                            | "grepv"
11297                            | "tap"
11298                            | "peek"
11299                    ) {
11300                        let r =
11301                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
11302                        self.wantarray_kind = saved_wa;
11303                        return r.map_err(Into::into);
11304                    }
11305                    if let Some(r) =
11306                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
11307                    {
11308                        self.wantarray_kind = saved_wa;
11309                        return r.map_err(Into::into);
11310                    }
11311                }
11312                self.wantarray_kind = saved_wa;
11313                self.call_named_sub(name, arg_vals, line, ctx)
11314            }
11315            ExprKind::IndirectCall {
11316                target,
11317                args,
11318                ampersand: _,
11319                pass_caller_arglist,
11320            } => {
11321                let tval = self.eval_expr(target)?;
11322                let arg_vals = if *pass_caller_arglist {
11323                    self.scope.get_array("_")
11324                } else {
11325                    // BUG-037: explicit `@array` / `%hash` operands flatten
11326                    // into the call list. Other arg shapes (qw, list exprs,
11327                    // function calls) keep the scalar-context evaluation so
11328                    // threading-style sites (`~> EXPR fn { ... }` desugars to
11329                    // `IndirectCall { target: <fn>, args: [EXPR] }`) pass the
11330                    // LHS as one threaded value rather than as flattened
11331                    // elements.
11332                    let mut v = Vec::with_capacity(args.len());
11333                    for a in args {
11334                        if matches!(a.kind, ExprKind::ArrayVar(_) | ExprKind::HashVar(_)) {
11335                            let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
11336                            if let Some(items) = val.as_array_vec() {
11337                                v.extend(items);
11338                            } else {
11339                                v.push(val);
11340                            }
11341                        } else {
11342                            v.push(self.eval_expr(a)?);
11343                        }
11344                    }
11345                    v
11346                };
11347                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
11348            }
11349            ExprKind::MethodCall {
11350                object,
11351                method,
11352                args,
11353                super_call,
11354            } => {
11355                let obj = self.eval_expr(object)?;
11356                let mut arg_vals = vec![obj.clone()];
11357                for a in args {
11358                    arg_vals.push(self.eval_expr(a)?);
11359                }
11360                if let Some(r) =
11361                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
11362                {
11363                    return r.map_err(Into::into);
11364                }
11365                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
11366                    return r.map_err(Into::into);
11367                }
11368                // Get class name
11369                let class = if let Some(b) = obj.as_blessed_ref() {
11370                    b.class.clone()
11371                } else if let Some(s) = obj.as_str() {
11372                    s // Class->method()
11373                } else {
11374                    return Err(
11375                        StrykeError::runtime("Can't call method on non-object", line).into(),
11376                    );
11377                };
11378                if method == "VERSION" && !*super_call {
11379                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
11380                        return Ok(ver);
11381                    }
11382                }
11383                // UNIVERSAL methods: isa, can, DOES
11384                if !*super_call {
11385                    match method.as_str() {
11386                        "isa" => {
11387                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
11388                            let mro = self.mro_linearize(&class);
11389                            let result = mro.iter().any(|c| c == &target);
11390                            return Ok(StrykeValue::integer(if result { 1 } else { 0 }));
11391                        }
11392                        "can" => {
11393                            let target_method =
11394                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
11395                            let found = self
11396                                .resolve_method_full_name(&class, &target_method, false)
11397                                .and_then(|fq| self.subs.get(&fq))
11398                                .is_some();
11399                            if found {
11400                                return Ok(StrykeValue::code_ref(Arc::new(StrykeSub {
11401                                    name: target_method,
11402                                    params: vec![],
11403                                    body: vec![],
11404                                    closure_env: None,
11405                                    prototype: None,
11406                                    fib_like: None,
11407                                })));
11408                            } else {
11409                                return Ok(StrykeValue::UNDEF);
11410                            }
11411                        }
11412                        "DOES" => {
11413                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
11414                            let mro = self.mro_linearize(&class);
11415                            let result = mro.iter().any(|c| c == &target);
11416                            return Ok(StrykeValue::integer(if result { 1 } else { 0 }));
11417                        }
11418                        _ => {}
11419                    }
11420                }
11421                let full_name = self
11422                    .resolve_method_full_name(&class, method, *super_call)
11423                    .ok_or_else(|| {
11424                        StrykeError::runtime(
11425                            format!(
11426                                "Can't locate method \"{}\" for invocant \"{}\"",
11427                                method, class
11428                            ),
11429                            line,
11430                        )
11431                    })?;
11432                if let Some(sub) = self.subs.get(&full_name).cloned() {
11433                    self.call_sub(&sub, arg_vals, ctx, line)
11434                } else if method == "new" && !*super_call {
11435                    // Default constructor
11436                    self.builtin_new(&class, arg_vals, line)
11437                } else if let Some(r) =
11438                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
11439                {
11440                    r
11441                } else {
11442                    Err(StrykeError::runtime(
11443                        format!(
11444                            "Can't locate method \"{}\" in package \"{}\"",
11445                            method, class
11446                        ),
11447                        line,
11448                    )
11449                    .into())
11450                }
11451            }
11452
11453            // Print/Say/Printf
11454            ExprKind::Print { handle, args } => {
11455                self.exec_print(handle.as_deref(), args, false, line)
11456            }
11457            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
11458            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
11459            ExprKind::Die(args) => {
11460                if args.is_empty() {
11461                    // `die` with no args: re-die with current $@ or "Died"
11462                    let current = self.scope.get_scalar("@");
11463                    let msg = if current.is_undef() || current.to_string().is_empty() {
11464                        let mut m = "Died".to_string();
11465                        m.push_str(&self.die_warn_at_suffix(line));
11466                        m.push('\n');
11467                        m
11468                    } else {
11469                        current.to_string()
11470                    };
11471                    self.fire_pseudosig_die(&msg, line)?;
11472                    return Err(StrykeError::die(msg, line).into());
11473                }
11474                // Single ref argument: store the ref value in $@
11475                if args.len() == 1 {
11476                    let v = self.eval_expr(&args[0])?;
11477                    if v.as_hash_ref().is_some()
11478                        || v.as_blessed_ref().is_some()
11479                        || v.as_array_ref().is_some()
11480                        || v.as_code_ref().is_some()
11481                    {
11482                        let msg = v.to_string();
11483                        self.fire_pseudosig_die(&msg, line)?;
11484                        return Err(StrykeError::die_with_value(v, msg, line).into());
11485                    }
11486                }
11487                let mut msg = String::new();
11488                for a in args {
11489                    let v = self.eval_expr(a)?;
11490                    msg.push_str(&v.to_string());
11491                }
11492                if msg.is_empty() {
11493                    msg = "Died".to_string();
11494                }
11495                if !msg.ends_with('\n') {
11496                    msg.push_str(&self.die_warn_at_suffix(line));
11497                    msg.push('\n');
11498                }
11499                self.fire_pseudosig_die(&msg, line)?;
11500                Err(StrykeError::die(msg, line).into())
11501            }
11502            ExprKind::Warn(args) => {
11503                let mut msg = String::new();
11504                for a in args {
11505                    let v = self.eval_expr(a)?;
11506                    msg.push_str(&v.to_string());
11507                }
11508                if msg.is_empty() {
11509                    msg = "Warning: something's wrong".to_string();
11510                }
11511                if !msg.ends_with('\n') {
11512                    msg.push_str(&self.die_warn_at_suffix(line));
11513                    msg.push('\n');
11514                }
11515                self.fire_pseudosig_warn(&msg, line)?;
11516                Ok(StrykeValue::integer(1))
11517            }
11518
11519            // Regex
11520            ExprKind::Match {
11521                expr,
11522                pattern,
11523                flags,
11524                scalar_g,
11525                delim: _,
11526            } => {
11527                let val = self.eval_expr(expr)?;
11528                if val.is_iterator() {
11529                    let source = crate::map_stream::into_pull_iter(val);
11530                    let re = self.compile_regex(pattern, flags, line)?;
11531                    let global = flags.contains('g');
11532                    if global {
11533                        return Ok(StrykeValue::iterator(std::sync::Arc::new(
11534                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
11535                        )));
11536                    } else {
11537                        return Ok(StrykeValue::iterator(std::sync::Arc::new(
11538                            crate::map_stream::MatchStreamIterator::new(source, re),
11539                        )));
11540                    }
11541                }
11542                let s = val.to_string();
11543                let pos_key = match &expr.kind {
11544                    ExprKind::ScalarVar(n) => n.as_str(),
11545                    _ => "_",
11546                };
11547                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
11548            }
11549            ExprKind::Substitution {
11550                expr,
11551                pattern,
11552                replacement,
11553                flags,
11554                delim: _,
11555            } => {
11556                let val = self.eval_expr(expr)?;
11557                if val.is_iterator() {
11558                    let source = crate::map_stream::into_pull_iter(val);
11559                    let re = self.compile_regex(pattern, flags, line)?;
11560                    let global = flags.contains('g');
11561                    return Ok(StrykeValue::iterator(std::sync::Arc::new(
11562                        crate::map_stream::SubstStreamIterator::new(
11563                            source,
11564                            re,
11565                            normalize_replacement_backrefs(replacement),
11566                            global,
11567                        ),
11568                    )));
11569                }
11570                let s = val.to_string();
11571                self.regex_subst_execute(
11572                    s,
11573                    pattern,
11574                    replacement.as_str(),
11575                    flags.as_str(),
11576                    expr,
11577                    line,
11578                )
11579            }
11580            ExprKind::Transliterate {
11581                expr,
11582                from,
11583                to,
11584                flags,
11585                delim: _,
11586            } => {
11587                let val = self.eval_expr(expr)?;
11588                if val.is_iterator() {
11589                    let source = crate::map_stream::into_pull_iter(val);
11590                    return Ok(StrykeValue::iterator(std::sync::Arc::new(
11591                        crate::map_stream::TransliterateStreamIterator::new(
11592                            source, from, to, flags,
11593                        ),
11594                    )));
11595                }
11596                let s = val.to_string();
11597                self.regex_transliterate_execute(
11598                    s,
11599                    from.as_str(),
11600                    to.as_str(),
11601                    flags.as_str(),
11602                    expr,
11603                    line,
11604                )
11605            }
11606
11607            // List operations
11608            ExprKind::MapExpr {
11609                block,
11610                list,
11611                flatten_array_refs,
11612                stream,
11613            } => {
11614                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11615                if *stream {
11616                    let out =
11617                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
11618                    if ctx == WantarrayCtx::List {
11619                        return Ok(out);
11620                    }
11621                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11622                }
11623                let items = list_val.to_list();
11624                if items.len() == 1 {
11625                    if let Some(p) = items[0].as_pipeline() {
11626                        if *flatten_array_refs {
11627                            return Err(StrykeError::runtime(
11628                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
11629                                line,
11630                            )
11631                            .into());
11632                        }
11633                        let sub = self.anon_coderef_from_block(block);
11634                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
11635                        return Ok(StrykeValue::pipeline(Arc::clone(&p)));
11636                    }
11637                }
11638                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
11639                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
11640                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
11641                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
11642                //
11643                // Save/restore the topic chain around the iter loop so this
11644                // map expression doesn't leak its final `_` (and the chain
11645                // shift it performed via the first set_topic) into the
11646                // enclosing block. Without this, a nested
11647                // `outer |> map { inner |> map { … }; sprintf("%d", _) }`
11648                // sees outer's `_` corrupted to the inner pipe's last iter
11649                // value once the inner completes — the per-iter outer
11650                // block reads of `_` after an inner pipe stage return the
11651                // wrong topic.
11652                let saved_chain = self.scope.save_topic_chain();
11653                let mut result = Vec::new();
11654                for item in items {
11655                    self.scope.set_topic(item);
11656                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
11657                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
11658                }
11659                self.scope.restore_topic_chain(saved_chain);
11660                if ctx == WantarrayCtx::List {
11661                    Ok(StrykeValue::array(result))
11662                } else {
11663                    Ok(StrykeValue::integer(result.len() as i64))
11664                }
11665            }
11666            ExprKind::ForEachExpr { block, list } => {
11667                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11668                // Lazy: consume iterator one-at-a-time without materializing.
11669                if list_val.is_iterator() {
11670                    let iter = list_val.into_iterator();
11671                    let mut count = 0i64;
11672                    while let Some(item) = iter.next_item() {
11673                        count += 1;
11674                        self.scope.set_topic(item);
11675                        self.exec_block(block)?;
11676                    }
11677                    return Ok(StrykeValue::integer(count));
11678                }
11679                let items = list_val.to_list();
11680                let count = items.len();
11681                for item in items {
11682                    self.scope.set_topic(item);
11683                    self.exec_block(block)?;
11684                }
11685                Ok(StrykeValue::integer(count as i64))
11686            }
11687            ExprKind::MapExprComma {
11688                expr,
11689                list,
11690                flatten_array_refs,
11691                stream,
11692            } => {
11693                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11694                if *stream {
11695                    let out =
11696                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
11697                    if ctx == WantarrayCtx::List {
11698                        return Ok(out);
11699                    }
11700                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11701                }
11702                let items = list_val.to_list();
11703                let mut result = Vec::new();
11704                for item in items {
11705                    // EXPR-form: no `{}` block boundary, so don't shift the
11706                    // topic chain or zero slot 1+. Just rebind `$_` / `$_0`.
11707                    // This makes `map _1, @$_` read the surrounding fn's
11708                    // second arg per iter; block-form `map { ... }` still
11709                    // gets a full `set_topic` via its CodeRef call.
11710                    self.scope.set_topic_local(item.clone());
11711                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11712                    // Coderef-in-block-position: `map $f, @l` calls `$f($_)`
11713                    // when `$f` is a code reference. Skipped under `--compat`
11714                    // (Perl semantics: re-evaluate expr per iteration as value).
11715                    let val = if !crate::compat_mode() {
11716                        if let Some(sub) = val.as_code_ref() {
11717                            let sub = sub.clone();
11718                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::List, line)?
11719                        } else {
11720                            val
11721                        }
11722                    } else {
11723                        val
11724                    };
11725                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
11726                }
11727                if ctx == WantarrayCtx::List {
11728                    Ok(StrykeValue::array(result))
11729                } else {
11730                    Ok(StrykeValue::integer(result.len() as i64))
11731                }
11732            }
11733            ExprKind::GrepExpr {
11734                block,
11735                list,
11736                keyword,
11737            } => {
11738                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11739                if keyword.is_stream() {
11740                    let out = self.filter_stream_block_output(list_val, block, line)?;
11741                    if ctx == WantarrayCtx::List {
11742                        return Ok(out);
11743                    }
11744                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11745                }
11746                let items = list_val.to_list();
11747                if items.len() == 1 {
11748                    if let Some(p) = items[0].as_pipeline() {
11749                        let sub = self.anon_coderef_from_block(block);
11750                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
11751                        return Ok(StrykeValue::pipeline(Arc::clone(&p)));
11752                    }
11753                }
11754                let mut result = Vec::new();
11755                for item in items {
11756                    self.scope.set_topic(item.clone());
11757                    let val = self.exec_block(block)?;
11758                    // Bare regex in block → match against $_ (Perl: /pat/ in
11759                    // grep is `$_ =~ /pat/`, not a truthy regex object).
11760                    let keep = if let Some(re) = val.as_regex() {
11761                        re.is_match(&item.to_string())
11762                    } else {
11763                        val.is_true()
11764                    };
11765                    if keep {
11766                        result.push(item);
11767                    }
11768                }
11769                if ctx == WantarrayCtx::List {
11770                    Ok(StrykeValue::array(result))
11771                } else {
11772                    Ok(StrykeValue::integer(result.len() as i64))
11773                }
11774            }
11775            ExprKind::GrepExprComma {
11776                expr,
11777                list,
11778                keyword,
11779            } => {
11780                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11781                if keyword.is_stream() {
11782                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
11783                    if ctx == WantarrayCtx::List {
11784                        return Ok(out);
11785                    }
11786                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11787                }
11788                let items = list_val.to_list();
11789                let mut result = Vec::new();
11790                for item in items {
11791                    // EXPR-form: see comment in MapExprComma above. No block
11792                    // boundary, so no chain shift; `_1` reads the surrounding
11793                    // fn's second arg per iter rather than getting nuked.
11794                    self.scope.set_topic_local(item.clone());
11795                    let val = self.eval_expr(expr)?;
11796                    // Coderef-in-block-position: `grep $f, @l` calls `$f($_)`
11797                    // when `$f` is a code reference, then filters by truthiness
11798                    // of the call result. Skipped under `--compat`.
11799                    let val = if !crate::compat_mode() {
11800                        if let Some(sub) = val.as_code_ref() {
11801                            let sub = sub.clone();
11802                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?
11803                        } else {
11804                            val
11805                        }
11806                    } else {
11807                        val
11808                    };
11809                    let keep = if let Some(re) = val.as_regex() {
11810                        re.is_match(&item.to_string())
11811                    } else {
11812                        val.is_true()
11813                    };
11814                    if keep {
11815                        result.push(item);
11816                    }
11817                }
11818                if ctx == WantarrayCtx::List {
11819                    Ok(StrykeValue::array(result))
11820                } else {
11821                    Ok(StrykeValue::integer(result.len() as i64))
11822                }
11823            }
11824            ExprKind::SortExpr { cmp, list } => {
11825                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11826                let mut items = list_val.to_list();
11827                match cmp {
11828                    Some(SortComparator::Code(code_expr)) => {
11829                        let sub = self.eval_expr(code_expr)?;
11830                        let Some(sub) = sub.as_code_ref() else {
11831                            return Err(StrykeError::runtime(
11832                                "sort: comparator must be a code reference",
11833                                line,
11834                            )
11835                            .into());
11836                        };
11837                        // Save topic chain before sort — set_sort_pair writes to $_
11838                        // which corrupts _< for subsequent pipeline stages.
11839                        let saved_topic = self.scope.save_topic_chain();
11840                        let sub = sub.clone();
11841                        items.sort_by(|a, b| {
11842                            // `set_sort_pair` keeps Perl-style `$a`/`$b` package-
11843                            // global access for `sub cmp { $a <=> $b }`. Passing
11844                            // `(a, b)` as positional args lets stryke lambdas
11845                            // `fn ($a, $b) { $b <=> $a }` receive them via @_.
11846                            self.scope.set_sort_pair(a.clone(), b.clone());
11847                            match self.call_sub(&sub, vec![a.clone(), b.clone()], ctx, line) {
11848                                Ok(v) => {
11849                                    let n = v.to_int();
11850                                    if n < 0 {
11851                                        Ordering::Less
11852                                    } else if n > 0 {
11853                                        Ordering::Greater
11854                                    } else {
11855                                        Ordering::Equal
11856                                    }
11857                                }
11858                                Err(_) => Ordering::Equal,
11859                            }
11860                        });
11861                        self.scope.restore_topic_chain(saved_topic);
11862                    }
11863                    Some(SortComparator::Block(cmp_block)) => {
11864                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
11865                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
11866                        } else {
11867                            // Save topic chain before sort — set_sort_pair writes to $_
11868                            // which corrupts _< for subsequent pipeline stages.
11869                            let saved_topic = self.scope.save_topic_chain();
11870                            let cmp_block = cmp_block.clone();
11871                            items.sort_by(|a, b| {
11872                                self.scope.set_sort_pair(a.clone(), b.clone());
11873                                match self.exec_block(&cmp_block) {
11874                                    Ok(v) => {
11875                                        let n = v.to_int();
11876                                        if n < 0 {
11877                                            Ordering::Less
11878                                        } else if n > 0 {
11879                                            Ordering::Greater
11880                                        } else {
11881                                            Ordering::Equal
11882                                        }
11883                                    }
11884                                    Err(_) => Ordering::Equal,
11885                                }
11886                            });
11887                            self.scope.restore_topic_chain(saved_topic);
11888                        }
11889                    }
11890                    None => {
11891                        items.sort_by_key(|a| a.to_string());
11892                    }
11893                }
11894                Ok(StrykeValue::array(items))
11895            }
11896            ExprKind::Rev(expr) => {
11897                // Eval in scalar context first to preserve set/hash/array ref types
11898                let val = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
11899                if val.is_iterator() {
11900                    return Ok(StrykeValue::iterator(Arc::new(
11901                        crate::value::RevIterator::new(val.into_iterator()),
11902                    )));
11903                }
11904                if let Some(s) = crate::value::set_payload(&val) {
11905                    let mut out = crate::value::PerlSet::new();
11906                    for (k, v) in s.iter().rev() {
11907                        out.insert(k.clone(), v.clone());
11908                    }
11909                    return Ok(StrykeValue::set(Arc::new(out)));
11910                }
11911                if let Some(ar) = val.as_array_ref() {
11912                    let items: Vec<_> = ar.read().iter().rev().cloned().collect();
11913                    return Ok(StrykeValue::array_ref(Arc::new(parking_lot::RwLock::new(
11914                        items,
11915                    ))));
11916                }
11917                if let Some(hr) = val.as_hash_ref() {
11918                    let mut out: indexmap::IndexMap<String, StrykeValue> =
11919                        indexmap::IndexMap::new();
11920                    for (k, v) in hr.read().iter() {
11921                        out.insert(v.to_string(), StrykeValue::string(k.clone()));
11922                    }
11923                    return Ok(StrykeValue::hash_ref(Arc::new(parking_lot::RwLock::new(
11924                        out,
11925                    ))));
11926                }
11927                // Re-eval in list context for bare arrays/hashes
11928                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11929                if let Some(hm) = val.as_hash_map() {
11930                    let mut out: indexmap::IndexMap<String, StrykeValue> =
11931                        indexmap::IndexMap::new();
11932                    for (k, v) in hm.iter() {
11933                        out.insert(v.to_string(), StrykeValue::string(k.clone()));
11934                    }
11935                    return Ok(StrykeValue::hash(out));
11936                }
11937                if val.as_array_vec().is_some() {
11938                    let mut items = val.to_list();
11939                    items.reverse();
11940                    Ok(StrykeValue::array(items))
11941                } else {
11942                    let items = val.to_list();
11943                    if items.len() > 1 {
11944                        let mut items = items;
11945                        items.reverse();
11946                        Ok(StrykeValue::array(items))
11947                    } else {
11948                        let s = val.to_string();
11949                        Ok(StrykeValue::string(s.chars().rev().collect()))
11950                    }
11951                }
11952            }
11953            ExprKind::ReverseExpr(list) => {
11954                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11955                match ctx {
11956                    WantarrayCtx::List => {
11957                        let mut items = val.to_list();
11958                        items.reverse();
11959                        Ok(StrykeValue::array(items))
11960                    }
11961                    _ => {
11962                        let items = val.to_list();
11963                        let s: String = items.iter().map(|v| v.to_string()).collect();
11964                        Ok(StrykeValue::string(s.chars().rev().collect()))
11965                    }
11966                }
11967            }
11968
11969            // ── Parallel operations (rayon-powered) ──
11970            ExprKind::ParLinesExpr {
11971                path,
11972                callback,
11973                progress,
11974            } => self.eval_par_lines_expr(
11975                path.as_ref(),
11976                callback.as_ref(),
11977                progress.as_deref(),
11978                line,
11979            ),
11980            ExprKind::ParWalkExpr {
11981                path,
11982                callback,
11983                progress,
11984            } => {
11985                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
11986            }
11987            ExprKind::PwatchExpr { path, callback } => {
11988                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
11989            }
11990            ExprKind::PMapExpr {
11991                block,
11992                list,
11993                progress,
11994                flat_outputs,
11995                on_cluster,
11996                stream,
11997            } => {
11998                let show_progress = progress
11999                    .as_ref()
12000                    .map(|p| self.eval_expr(p))
12001                    .transpose()?
12002                    .map(|v| v.is_true())
12003                    .unwrap_or(false);
12004                // List context for the operand so `@_` / `@arr` flatten to
12005                // their elements instead of numifying to the array length.
12006                // Scalar context was producing `pmap{_*2}` over `@_` of size
12007                // 13 → one iteration with `_=13` → `[26]` (chunk-size × 2)
12008                // instead of 13 doubled values; same shape inside `~p>` chunk
12009                // workers and at top level.
12010                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12011                if let Some(cluster_e) = on_cluster {
12012                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
12013                    return self.eval_pmap_remote(
12014                        cluster_val,
12015                        list_val,
12016                        show_progress,
12017                        block,
12018                        *flat_outputs,
12019                        line,
12020                    );
12021                }
12022                if *stream {
12023                    let source = crate::map_stream::into_pull_iter(list_val);
12024                    let sub = self.anon_coderef_from_block(block);
12025                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
12026                    return Ok(StrykeValue::iterator(Arc::new(
12027                        crate::map_stream::PMapStreamIterator::new(
12028                            source,
12029                            sub,
12030                            self.subs.clone(),
12031                            capture,
12032                            atomic_arrays,
12033                            atomic_hashes,
12034                            *flat_outputs,
12035                        ),
12036                    )));
12037                }
12038                let items = list_val.to_list();
12039                let block = block.clone();
12040                let subs = self.subs.clone();
12041                let (scope_capture, atomic_arrays, atomic_hashes) =
12042                    self.scope.capture_with_atomics();
12043                let pmap_progress = PmapProgress::new(show_progress, items.len());
12044
12045                if *flat_outputs {
12046                    let mut indexed: Vec<(usize, Vec<StrykeValue>)> = items
12047                        .into_par_iter()
12048                        .enumerate()
12049                        .map(|(i, item)| {
12050                            let mut local_interp = VMHelper::new();
12051                            local_interp.subs = subs.clone();
12052                            local_interp.scope.restore_capture(&scope_capture);
12053                            local_interp
12054                                .scope
12055                                .restore_atomics(&atomic_arrays, &atomic_hashes);
12056                            local_interp.enable_parallel_guard();
12057                            local_interp.scope.set_topic(item);
12058                            let val = match local_interp.exec_block(&block) {
12059                                Ok(val) => val,
12060                                Err(_) => StrykeValue::UNDEF,
12061                            };
12062                            let chunk = val.map_flatten_outputs(true);
12063                            pmap_progress.tick();
12064                            (i, chunk)
12065                        })
12066                        .collect();
12067                    pmap_progress.finish();
12068                    indexed.sort_by_key(|(i, _)| *i);
12069                    let results: Vec<StrykeValue> =
12070                        indexed.into_iter().flat_map(|(_, v)| v).collect();
12071                    Ok(StrykeValue::array(results))
12072                } else {
12073                    let results: Vec<StrykeValue> = items
12074                        .into_par_iter()
12075                        .map(|item| {
12076                            let mut local_interp = VMHelper::new();
12077                            local_interp.subs = subs.clone();
12078                            local_interp.scope.restore_capture(&scope_capture);
12079                            local_interp
12080                                .scope
12081                                .restore_atomics(&atomic_arrays, &atomic_hashes);
12082                            local_interp.enable_parallel_guard();
12083                            local_interp.scope.set_topic(item);
12084                            let val = match local_interp.exec_block(&block) {
12085                                Ok(val) => val,
12086                                Err(_) => StrykeValue::UNDEF,
12087                            };
12088                            pmap_progress.tick();
12089                            val
12090                        })
12091                        .collect();
12092                    pmap_progress.finish();
12093                    Ok(StrykeValue::array(results))
12094                }
12095            }
12096            ExprKind::PMapChunkedExpr {
12097                chunk_size,
12098                block,
12099                list,
12100                progress,
12101            } => {
12102                let show_progress = progress
12103                    .as_ref()
12104                    .map(|p| self.eval_expr(p))
12105                    .transpose()?
12106                    .map(|v| v.is_true())
12107                    .unwrap_or(false);
12108                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
12109                let list_val = self.eval_expr(list)?;
12110                let items = list_val.to_list();
12111                let block = block.clone();
12112                let subs = self.subs.clone();
12113                let (scope_capture, atomic_arrays, atomic_hashes) =
12114                    self.scope.capture_with_atomics();
12115
12116                let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = items
12117                    .chunks(chunk_n)
12118                    .enumerate()
12119                    .map(|(i, c)| (i, c.to_vec()))
12120                    .collect();
12121
12122                let n_chunks = indexed_chunks.len();
12123                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
12124
12125                let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
12126                    .into_par_iter()
12127                    .map(|(chunk_idx, chunk)| {
12128                        let mut local_interp = VMHelper::new();
12129                        local_interp.subs = subs.clone();
12130                        local_interp.scope.restore_capture(&scope_capture);
12131                        local_interp
12132                            .scope
12133                            .restore_atomics(&atomic_arrays, &atomic_hashes);
12134                        local_interp.enable_parallel_guard();
12135                        let mut out = Vec::with_capacity(chunk.len());
12136                        for item in chunk {
12137                            local_interp.scope.set_topic(item);
12138                            match local_interp.exec_block(&block) {
12139                                Ok(val) => out.push(val),
12140                                Err(_) => out.push(StrykeValue::UNDEF),
12141                            }
12142                        }
12143                        pmap_progress.tick();
12144                        (chunk_idx, out)
12145                    })
12146                    .collect();
12147
12148                pmap_progress.finish();
12149                chunk_results.sort_by_key(|(i, _)| *i);
12150                let results: Vec<StrykeValue> =
12151                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
12152                Ok(StrykeValue::array(results))
12153            }
12154            ExprKind::PGrepExpr {
12155                block,
12156                list,
12157                progress,
12158                stream,
12159            } => {
12160                let show_progress = progress
12161                    .as_ref()
12162                    .map(|p| self.eval_expr(p))
12163                    .transpose()?
12164                    .map(|v| v.is_true())
12165                    .unwrap_or(false);
12166                let list_val = self.eval_expr(list)?;
12167                if *stream {
12168                    let source = crate::map_stream::into_pull_iter(list_val);
12169                    let sub = self.anon_coderef_from_block(block);
12170                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
12171                    return Ok(StrykeValue::iterator(Arc::new(
12172                        crate::map_stream::PGrepStreamIterator::new(
12173                            source,
12174                            sub,
12175                            self.subs.clone(),
12176                            capture,
12177                            atomic_arrays,
12178                            atomic_hashes,
12179                        ),
12180                    )));
12181                }
12182                let items = list_val.to_list();
12183                let block = block.clone();
12184                let subs = self.subs.clone();
12185                let (scope_capture, atomic_arrays, atomic_hashes) =
12186                    self.scope.capture_with_atomics();
12187                let pmap_progress = PmapProgress::new(show_progress, items.len());
12188
12189                let results: Vec<StrykeValue> = items
12190                    .into_par_iter()
12191                    .filter_map(|item| {
12192                        let mut local_interp = VMHelper::new();
12193                        local_interp.subs = subs.clone();
12194                        local_interp.scope.restore_capture(&scope_capture);
12195                        local_interp
12196                            .scope
12197                            .restore_atomics(&atomic_arrays, &atomic_hashes);
12198                        local_interp.enable_parallel_guard();
12199                        local_interp.scope.set_topic(item.clone());
12200                        let keep = match local_interp.exec_block(&block) {
12201                            Ok(val) => val.is_true(),
12202                            Err(_) => false,
12203                        };
12204                        pmap_progress.tick();
12205                        if keep {
12206                            Some(item)
12207                        } else {
12208                            None
12209                        }
12210                    })
12211                    .collect();
12212                pmap_progress.finish();
12213                Ok(StrykeValue::array(results))
12214            }
12215            ExprKind::ParExpr { block, list } => {
12216                // Generic parallel-chunk wrapper: split input on a sensible
12217                // boundary (UTF-8 char-aligned for strings, element-aligned
12218                // for arrays), evaluate the block per chunk in parallel
12219                // with `$_` bound to the chunk, then concatenate results.
12220                //
12221                // Chunk count is capped at min(n_threads, 8) because each
12222                // chunk pays a fixed `VMHelper::new()` setup cost (env-var
12223                // parsing, PATH/FPATH split, term-info ioctl, IndexMap
12224                // declarations). On 18-core machines, splitting 18 ways
12225                // makes setup overhead dominate the actual work.
12226                // List context for the operand so `@arr` flattens to its
12227                // elements instead of numifying to the array length —
12228                // matches PMapExpr's eval shape so `par { } @arr` and
12229                // `pmap { } @arr` agree on input semantics.
12230                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12231                let n_threads = rayon::current_num_threads().clamp(1, 8);
12232                let chunks = par_chunk_value(&list_val, n_threads);
12233                if chunks.len() < 2 {
12234                    // Below break-even (small input or unsupported value type):
12235                    // run the block once with the original input as `$_`.
12236                    self.scope.set_topic(list_val);
12237                    let v = self.exec_block(block)?;
12238                    return Ok(v);
12239                }
12240                let block_clone = block.clone();
12241                let subs = self.subs.clone();
12242                let (scope_capture, atomic_arrays, atomic_hashes) =
12243                    self.scope.capture_with_atomics();
12244                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
12245                let err_w = Arc::clone(&first_err);
12246                let per_chunk: Vec<Vec<StrykeValue>> = chunks
12247                    .into_par_iter()
12248                    .map(|chunk| {
12249                        if err_w.lock().is_some() {
12250                            return Vec::new();
12251                        }
12252                        let mut local_interp = VMHelper::new();
12253                        local_interp.subs = subs.clone();
12254                        local_interp.scope.restore_capture(&scope_capture);
12255                        local_interp
12256                            .scope
12257                            .restore_atomics(&atomic_arrays, &atomic_hashes);
12258                        local_interp.enable_parallel_guard();
12259                        local_interp.scope.set_topic(chunk);
12260                        match local_interp.exec_block(&block_clone) {
12261                            Ok(v) => v.map_flatten_outputs(true),
12262                            Err(e) => {
12263                                let mut g = err_w.lock();
12264                                if g.is_none() {
12265                                    *g = Some(format!("par: {:?}", e));
12266                                }
12267                                Vec::new()
12268                            }
12269                        }
12270                    })
12271                    .collect();
12272                if let Some(msg) = first_err.lock().take() {
12273                    return Err(FlowOrError::Error(StrykeError::runtime(msg, line)));
12274                }
12275                let total: usize = per_chunk.iter().map(|v| v.len()).sum();
12276                let mut out = Vec::with_capacity(total);
12277                for v in per_chunk {
12278                    out.extend(v);
12279                }
12280                Ok(StrykeValue::array(out))
12281            }
12282            ExprKind::ParReduceExpr {
12283                extract_block,
12284                reduce_block,
12285                list,
12286            } => {
12287                // Chunk INPUT, run extract per chunk in parallel, then
12288                // reduce pairwise across chunks. With an explicit reduce
12289                // block the user controls merging via `$a` / `$b`. Without
12290                // one, the merger is auto-picked based on the first
12291                // chunk's result type:
12292                //   - hash<num>  → key-wise add (canonical histogram merge)
12293                //   - number     → numeric `+`
12294                //   - array/list → concat
12295                //   - string     → concat
12296                // Use List context so `@a` expands to its elements, not its length.
12297                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12298                let n_threads = rayon::current_num_threads().clamp(1, 8);
12299                let chunks = par_chunk_value(&list_val, n_threads);
12300                if chunks.len() < 2 {
12301                    // Single-chunk fallback: bind the chunk elements to `@_`
12302                    // (not wrapped) so `~p> @a sum` correctly passes all elements
12303                    // to `sum(@_)`. Also set `$_` to the first element for
12304                    // backwards compat with scalar-style blocks. Tail in LIST
12305                    // context so `map { ... } @_` returns the mapped array,
12306                    // not its scalar-context count — `~p> @xs map { _*10 }`
12307                    // must yield `[10,20,30,40,50]`, not `5`.
12308                    let chunk_arr = match list_val.as_array_vec() {
12309                        Some(arr) => arr,
12310                        None => vec![list_val.clone()],
12311                    };
12312                    let first = chunk_arr.first().cloned().unwrap_or(StrykeValue::UNDEF);
12313                    self.scope.declare_array("_", chunk_arr);
12314                    self.scope.set_topic(first);
12315                    return self.exec_block_with_tail(extract_block, WantarrayCtx::List);
12316                }
12317                let extract = extract_block.clone();
12318                let subs = self.subs.clone();
12319                let (scope_capture, atomic_arrays, atomic_hashes) =
12320                    self.scope.capture_with_atomics();
12321                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
12322                let err_w = Arc::clone(&first_err);
12323                let per_chunk: Vec<StrykeValue> = chunks
12324                    .into_par_iter()
12325                    .map(|chunk| {
12326                        if err_w.lock().is_some() {
12327                            return StrykeValue::UNDEF;
12328                        }
12329                        let mut local = VMHelper::new();
12330                        local.subs = subs.clone();
12331                        local.scope.restore_capture(&scope_capture);
12332                        local.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
12333                        local.enable_parallel_guard();
12334                        // Bind the chunk elements to `@_` (not wrapped) so
12335                        // `~p> @a sum` correctly passes all elements to `sum(@_)`.
12336                        // Also set `$_` to the first element for backwards compat.
12337                        let chunk_arr = match chunk.as_array_vec() {
12338                            Some(arr) => arr,
12339                            None => vec![chunk.clone()],
12340                        };
12341                        let first = chunk_arr.first().cloned().unwrap_or(StrykeValue::UNDEF);
12342                        local.scope.declare_array("_", chunk_arr);
12343                        local.scope.set_topic(first);
12344                        match local.exec_block_with_tail(&extract, WantarrayCtx::List) {
12345                            Ok(v) => v,
12346                            Err(e) => {
12347                                let mut g = err_w.lock();
12348                                if g.is_none() {
12349                                    *g = Some(format!("par_reduce: {:?}", e));
12350                                }
12351                                StrykeValue::UNDEF
12352                            }
12353                        }
12354                    })
12355                    .collect();
12356                if let Some(msg) = first_err.lock().take() {
12357                    return Err(FlowOrError::Error(StrykeError::runtime(msg, line)));
12358                }
12359                if per_chunk.is_empty() {
12360                    return Ok(StrykeValue::UNDEF);
12361                }
12362                // Explicit reducer: pairwise via user block with $a/$b bound.
12363                if let Some(rb) = reduce_block {
12364                    let mut acc = per_chunk[0].clone();
12365                    for v in per_chunk.into_iter().skip(1) {
12366                        self.scope.declare_scalar("a", acc.clone());
12367                        self.scope.declare_scalar("b", v);
12368                        acc = self.exec_block(rb)?;
12369                    }
12370                    return Ok(acc);
12371                }
12372                // Auto-merge.
12373                Ok(par_reduce_auto_merge(per_chunk))
12374            }
12375            ExprKind::DistReduceExpr {
12376                cluster,
12377                extract_block,
12378                list,
12379            } => {
12380                // Distributed counterpart of `~p>` — chunk the source list
12381                // locally, ship each chunk as one JSON work-item via the
12382                // existing `cluster::run_cluster` SSH dispatcher (one slot
12383                // per ssh process, session_init once per slot, JOB frames
12384                // over a shared queue, retry budget per chunk).
12385                //
12386                // Each remote worker receives one chunk (a JSON array) as
12387                // `$_[0]`. We prepend `local @_ = @{$_[0]}` to the block so
12388                // the user-supplied stage chain sees `@_` bound to the
12389                // chunk's elements — identical surface to `~p>`'s extract
12390                // block.
12391                let cluster_pv = self.eval_expr(cluster)?;
12392                let Some(remote_cluster) = cluster_pv.as_remote_cluster() else {
12393                    return Err(StrykeError::runtime(
12394                        "~d>: expected cluster(...) value after `on`",
12395                        line,
12396                    )
12397                    .into());
12398                };
12399                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12400                let items_flat = list_val.to_list();
12401                if items_flat.is_empty() {
12402                    return Ok(StrykeValue::array(vec![]));
12403                }
12404                let (scope_capture, atomic_arrays, atomic_hashes) =
12405                    self.scope.capture_with_atomics();
12406                if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
12407                    return Err(StrykeError::runtime(
12408                        "~d>: mysync/atomic capture is not supported for remote workers",
12409                        line,
12410                    )
12411                    .into());
12412                }
12413                // Chunk size mirrors `~p>`'s heuristic: oversubscribe slots
12414                // by 4× so faster slots can pull more work via the shared
12415                // queue. Floor at 1, ceil at items.len() so we always get
12416                // at least one chunk per slot when items < slots.
12417                let n_slots = remote_cluster.slots.len().max(1);
12418                let target_chunks = (n_slots * 4).min(items_flat.len()).max(1);
12419                let chunk_size = items_flat.len().div_ceil(target_chunks);
12420                let mut chunk_items: Vec<serde_json::Value> = Vec::new();
12421                let mut chunk_jsons_buf: Vec<StrykeValue> = Vec::new();
12422                for item in items_flat.into_iter() {
12423                    chunk_jsons_buf.push(item);
12424                    if chunk_jsons_buf.len() >= chunk_size {
12425                        let drained: Vec<StrykeValue> = std::mem::take(&mut chunk_jsons_buf);
12426                        let as_array = StrykeValue::array(drained);
12427                        chunk_items.push(
12428                            crate::remote_wire::perl_to_json_value(&as_array)
12429                                .map_err(|e| StrykeError::runtime(e, line))?,
12430                        );
12431                    }
12432                }
12433                if !chunk_jsons_buf.is_empty() {
12434                    let as_array = StrykeValue::array(chunk_jsons_buf);
12435                    chunk_items.push(
12436                        crate::remote_wire::perl_to_json_value(&as_array)
12437                            .map_err(|e| StrykeError::runtime(e, line))?,
12438                    );
12439                }
12440                // Drop scope entries that can't be JSON-marshalled (the
12441                // `RemoteCluster` value itself, file handles, code refs,
12442                // etc.). The remote worker doesn't need them — they're
12443                // dispatcher-side context only. Any reference the user's
12444                // stage block legitimately needs goes through this filter
12445                // anyway, and a `RemoteCluster` in remote scope would be
12446                // nonsensical (no controller to dispatch through there).
12447                let cap_json: Vec<(String, serde_json::Value)> = scope_capture
12448                    .iter()
12449                    .filter_map(|(k, v)| {
12450                        crate::remote_wire::perl_to_json_value(v)
12451                            .ok()
12452                            .map(|j| (k.clone(), j))
12453                    })
12454                    .collect();
12455                let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
12456                // Remote agent (`remote_wire::run_job_local`) sets `$_`
12457                // to the chunk value (a flat array) but does NOT populate
12458                // `@_`. Two bridges:
12459                //   1. `@_ = $_;` prologue — make the user's `@_`-threaded
12460                //      stage chain see the chunk's elements as an array
12461                //      (matches `~p>` chunk-block surface).
12462                //   2. Wrap the body in `[ ... ]` so the last expression's
12463                //      list result survives the worker's scalar-context
12464                //      block return + JSON round-trip. Without this,
12465                //      `map { ... } @_` collapses to `scalar(@result) = N`
12466                //      before serialization.
12467                let user_block_src = crate::fmt::format_block(extract_block);
12468                let block_src = format!("@_ = $_;\n[ {user_block_src} ]");
12469                let result_values = crate::cluster::run_cluster(
12470                    &remote_cluster,
12471                    subs_prelude,
12472                    block_src,
12473                    cap_json,
12474                    chunk_items,
12475                )
12476                .map_err(|e| StrykeError::runtime(format!("~d> remote: {e}"), line))?;
12477                // Each chunk's extract returns a value; flat-concat across
12478                // chunks to mirror `~p>`'s auto-merge for list-shaped output.
12479                // Scalar-shaped chunk results stay as one element each.
12480                let mut merged: Vec<StrykeValue> = Vec::new();
12481                for v in result_values {
12482                    if let Some(items) = v.as_array_vec() {
12483                        merged.extend(items);
12484                    } else if let Some(ar) = v.as_array_ref() {
12485                        merged.extend(ar.read().iter().cloned());
12486                    } else {
12487                        merged.push(v);
12488                    }
12489                }
12490                Ok(StrykeValue::array(merged))
12491            }
12492            ExprKind::PForExpr {
12493                block,
12494                list,
12495                progress,
12496            } => {
12497                let show_progress = progress
12498                    .as_ref()
12499                    .map(|p| self.eval_expr(p))
12500                    .transpose()?
12501                    .map(|v| v.is_true())
12502                    .unwrap_or(false);
12503                let list_val = self.eval_expr(list)?;
12504                let items = list_val.to_list();
12505                let block = block.clone();
12506                let subs = self.subs.clone();
12507                let (scope_capture, atomic_arrays, atomic_hashes) =
12508                    self.scope.capture_with_atomics();
12509
12510                let pmap_progress = PmapProgress::new(show_progress, items.len());
12511                let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
12512                items.into_par_iter().for_each(|item| {
12513                    if first_err.lock().is_some() {
12514                        return;
12515                    }
12516                    let mut local_interp = VMHelper::new();
12517                    local_interp.subs = subs.clone();
12518                    local_interp.scope.restore_capture(&scope_capture);
12519                    local_interp
12520                        .scope
12521                        .restore_atomics(&atomic_arrays, &atomic_hashes);
12522                    local_interp.enable_parallel_guard();
12523                    local_interp.scope.set_topic(item);
12524                    match local_interp.exec_block(&block) {
12525                        Ok(_) => {}
12526                        Err(e) => {
12527                            let stryke = match e {
12528                                FlowOrError::Error(stryke) => stryke,
12529                                FlowOrError::Flow(_) => StrykeError::runtime(
12530                                    "return/last/next/redo not supported inside pfor block",
12531                                    line,
12532                                ),
12533                            };
12534                            let mut g = first_err.lock();
12535                            if g.is_none() {
12536                                *g = Some(stryke);
12537                            }
12538                        }
12539                    }
12540                    pmap_progress.tick();
12541                });
12542                pmap_progress.finish();
12543                if let Some(e) = first_err.lock().take() {
12544                    return Err(FlowOrError::Error(e));
12545                }
12546                Ok(StrykeValue::UNDEF)
12547            }
12548            ExprKind::FanExpr {
12549                count,
12550                block,
12551                progress,
12552                capture,
12553            } => {
12554                let show_progress = progress
12555                    .as_ref()
12556                    .map(|p| self.eval_expr(p))
12557                    .transpose()?
12558                    .map(|v| v.is_true())
12559                    .unwrap_or(false);
12560                let n = match count {
12561                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
12562                    None => self.parallel_thread_count(),
12563                };
12564                let block = block.clone();
12565                let subs = self.subs.clone();
12566                let (scope_capture, atomic_arrays, atomic_hashes) =
12567                    self.scope.capture_with_atomics();
12568
12569                let fan_progress = FanProgress::new(show_progress, n);
12570                if *capture {
12571                    if n == 0 {
12572                        return Ok(StrykeValue::array(Vec::new()));
12573                    }
12574                    let pairs: Vec<(usize, ExecResult)> = (0..n)
12575                        .into_par_iter()
12576                        .map(|i| {
12577                            fan_progress.start_worker(i);
12578                            let mut local_interp = VMHelper::new();
12579                            local_interp.subs = subs.clone();
12580                            local_interp.suppress_stdout = show_progress;
12581                            local_interp.scope.restore_capture(&scope_capture);
12582                            local_interp
12583                                .scope
12584                                .restore_atomics(&atomic_arrays, &atomic_hashes);
12585                            local_interp.enable_parallel_guard();
12586                            local_interp.scope.set_topic(StrykeValue::integer(i as i64));
12587                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
12588                            let res = local_interp.exec_block(&block);
12589                            crate::parallel_trace::fan_worker_set_index(None);
12590                            fan_progress.finish_worker(i);
12591                            (i, res)
12592                        })
12593                        .collect();
12594                    fan_progress.finish();
12595                    let mut pairs = pairs;
12596                    pairs.sort_by_key(|(i, _)| *i);
12597                    let mut out = Vec::with_capacity(n);
12598                    for (_, r) in pairs {
12599                        match r {
12600                            Ok(v) => out.push(v),
12601                            Err(e) => return Err(e),
12602                        }
12603                    }
12604                    return Ok(StrykeValue::array(out));
12605                }
12606                let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
12607                (0..n).into_par_iter().for_each(|i| {
12608                    if first_err.lock().is_some() {
12609                        return;
12610                    }
12611                    fan_progress.start_worker(i);
12612                    let mut local_interp = VMHelper::new();
12613                    local_interp.subs = subs.clone();
12614                    local_interp.suppress_stdout = show_progress;
12615                    local_interp.scope.restore_capture(&scope_capture);
12616                    local_interp
12617                        .scope
12618                        .restore_atomics(&atomic_arrays, &atomic_hashes);
12619                    local_interp.enable_parallel_guard();
12620                    local_interp.scope.set_topic(StrykeValue::integer(i as i64));
12621                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
12622                    match local_interp.exec_block(&block) {
12623                        Ok(_) => {}
12624                        Err(e) => {
12625                            let stryke = match e {
12626                                FlowOrError::Error(stryke) => stryke,
12627                                FlowOrError::Flow(_) => StrykeError::runtime(
12628                                    "return/last/next/redo not supported inside fan block",
12629                                    line,
12630                                ),
12631                            };
12632                            let mut g = first_err.lock();
12633                            if g.is_none() {
12634                                *g = Some(stryke);
12635                            }
12636                        }
12637                    }
12638                    crate::parallel_trace::fan_worker_set_index(None);
12639                    fan_progress.finish_worker(i);
12640                });
12641                fan_progress.finish();
12642                if let Some(e) = first_err.lock().take() {
12643                    return Err(FlowOrError::Error(e));
12644                }
12645                Ok(StrykeValue::UNDEF)
12646            }
12647            ExprKind::RetryBlock {
12648                body,
12649                times,
12650                backoff,
12651            } => self.eval_retry_block(body, times, *backoff, line),
12652            ExprKind::RateLimitBlock {
12653                slot,
12654                max,
12655                window,
12656                body,
12657            } => self.eval_rate_limit_block(*slot, max, window, body, line),
12658            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
12659            ExprKind::GenBlock { body } => {
12660                let g = Arc::new(PerlGenerator {
12661                    block: body.clone(),
12662                    pc: Mutex::new(0),
12663                    scope_started: Mutex::new(false),
12664                    exhausted: Mutex::new(false),
12665                });
12666                Ok(StrykeValue::generator(g))
12667            }
12668            ExprKind::Yield(e) => {
12669                if !self.in_generator {
12670                    return Err(StrykeError::runtime("yield outside gen block", line).into());
12671                }
12672                let v = self.eval_expr(e)?;
12673                Err(FlowOrError::Flow(Flow::Yield(v)))
12674            }
12675            ExprKind::AlgebraicMatch { subject, arms } => {
12676                self.eval_algebraic_match(subject, arms, line)
12677            }
12678            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
12679                Ok(self.spawn_async_block(body))
12680            }
12681            ExprKind::Trace { body } => {
12682                crate::parallel_trace::trace_enter();
12683                let out = self.exec_block(body);
12684                crate::parallel_trace::trace_leave();
12685                out
12686            }
12687            ExprKind::Spinner { message, body } => {
12688                use std::io::Write as _;
12689                let msg = self.eval_expr(message)?.to_string();
12690                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
12691                let done2 = done.clone();
12692                let handle = std::thread::spawn(move || {
12693                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12694                    let mut i = 0;
12695                    let stderr = std::io::stderr();
12696                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
12697                        {
12698                            let stdout = std::io::stdout();
12699                            let _stdout_lock = stdout.lock();
12700                            let mut err = stderr.lock();
12701                            let _ = write!(
12702                                err,
12703                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
12704                                frames[i % frames.len()],
12705                                msg
12706                            );
12707                            let _ = err.flush();
12708                        }
12709                        std::thread::sleep(std::time::Duration::from_millis(80));
12710                        i += 1;
12711                    }
12712                    let mut err = stderr.lock();
12713                    let _ = write!(err, "\r\x1b[2K");
12714                    let _ = err.flush();
12715                });
12716                let result = self.exec_block(body);
12717                done.store(true, std::sync::atomic::Ordering::Relaxed);
12718                let _ = handle.join();
12719                result
12720            }
12721            ExprKind::Timer { body } => {
12722                let start = std::time::Instant::now();
12723                self.exec_block(body)?;
12724                let ms = start.elapsed().as_secs_f64() * 1000.0;
12725                Ok(StrykeValue::float(ms))
12726            }
12727            ExprKind::Bench { body, times } => {
12728                let n = self.eval_expr(times)?.to_int();
12729                if n < 0 {
12730                    return Err(StrykeError::runtime(
12731                        "bench: iteration count must be non-negative",
12732                        line,
12733                    )
12734                    .into());
12735                }
12736                self.run_bench_block(body, n as usize, line)
12737            }
12738            ExprKind::Await(expr) => {
12739                let v = self.eval_expr(expr)?;
12740                if let Some(t) = v.as_async_task() {
12741                    t.await_result().map_err(FlowOrError::from)
12742                } else {
12743                    Ok(v)
12744                }
12745            }
12746            ExprKind::Slurp(e) => {
12747                let path = self.eval_expr(e)?.to_string();
12748                let path = self.resolve_stryke_path_string(&path);
12749                crate::perl_fs::read_bytes_or_glob(&path)
12750                    .map(StrykeValue::bytes)
12751                    .map_err(|e| {
12752                        FlowOrError::Error(StrykeError::runtime(format!("slurp: {}", e), line))
12753                    })
12754            }
12755            ExprKind::Swallow(e) => {
12756                let path = self.eval_expr(e)?.to_string();
12757                let path = self.resolve_stryke_path_string(&path);
12758                crate::perl_fs::swallow_to_hash(&path).map_err(|e| {
12759                    FlowOrError::Error(StrykeError::runtime(format!("swallow: {}", e), line))
12760                })
12761            }
12762            ExprKind::Ingest(e) => {
12763                let path = self.eval_expr(e)?.to_string();
12764                let path = self.resolve_stryke_path_string(&path);
12765                crate::perl_fs::ingest_iterator(&path).map_err(|e| {
12766                    FlowOrError::Error(StrykeError::runtime(format!("ingest: {}", e), line))
12767                })
12768            }
12769            ExprKind::Burp(e) => {
12770                let v = self.eval_expr(e)?;
12771                crate::perl_fs::burp_hash_to_disk(&v)
12772                    .map(StrykeValue::integer)
12773                    .map_err(|e| {
12774                        FlowOrError::Error(StrykeError::runtime(format!("burp: {}", e), line))
12775                    })
12776            }
12777            ExprKind::God(e) => {
12778                let v = self.eval_expr(e)?;
12779                Ok(StrykeValue::string(crate::god::god_dump(&v)))
12780            }
12781            ExprKind::Capture(e) => {
12782                let cmd = self.eval_expr(e)?.to_string();
12783                let output = Command::new("sh")
12784                    .arg("-c")
12785                    .arg(&cmd)
12786                    .output()
12787                    .map_err(|e| {
12788                        FlowOrError::Error(StrykeError::runtime(format!("capture: {}", e), line))
12789                    })?;
12790                self.record_child_exit_status(output.status);
12791                let exitcode = output.status.code().unwrap_or(-1) as i64;
12792                let stdout = decode_utf8_or_latin1(&output.stdout);
12793                let stderr = decode_utf8_or_latin1(&output.stderr);
12794                Ok(StrykeValue::capture(Arc::new(CaptureResult {
12795                    stdout,
12796                    stderr,
12797                    exitcode,
12798                })))
12799            }
12800            ExprKind::Qx(e) => {
12801                let cmd = self.eval_expr(e)?.to_string();
12802                let raw =
12803                    crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)?;
12804                if ctx == WantarrayCtx::List {
12805                    // Perl `qx` in list context yields one element per line,
12806                    // each terminated by `$/` (default `\n`). The final line
12807                    // keeps whatever termination the command produced.
12808                    let s = raw.to_string();
12809                    if s.is_empty() {
12810                        return Ok(StrykeValue::array(Vec::new()));
12811                    }
12812                    let mut lines = Vec::new();
12813                    let mut buf = String::new();
12814                    for c in s.chars() {
12815                        buf.push(c);
12816                        if c == '\n' {
12817                            lines.push(StrykeValue::string(std::mem::take(&mut buf)));
12818                        }
12819                    }
12820                    if !buf.is_empty() {
12821                        lines.push(StrykeValue::string(buf));
12822                    }
12823                    Ok(StrykeValue::array(lines))
12824                } else {
12825                    Ok(raw)
12826                }
12827            }
12828            ExprKind::FetchUrl(e) => {
12829                let url = self.eval_expr(e)?.to_string();
12830                ureq::get(&url)
12831                    .call()
12832                    .map_err(|e| {
12833                        FlowOrError::Error(StrykeError::runtime(format!("fetch_url: {}", e), line))
12834                    })
12835                    .and_then(|r| {
12836                        r.into_string().map(StrykeValue::string).map_err(|e| {
12837                            FlowOrError::Error(StrykeError::runtime(
12838                                format!("fetch_url: {}", e),
12839                                line,
12840                            ))
12841                        })
12842                    })
12843            }
12844            ExprKind::Pchannel { capacity } => {
12845                if let Some(c) = capacity {
12846                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
12847                    Ok(crate::pchannel::create_bounded_pair(n))
12848                } else {
12849                    Ok(crate::pchannel::create_pair())
12850                }
12851            }
12852            ExprKind::PSortExpr {
12853                cmp,
12854                list,
12855                progress,
12856            } => {
12857                let show_progress = progress
12858                    .as_ref()
12859                    .map(|p| self.eval_expr(p))
12860                    .transpose()?
12861                    .map(|v| v.is_true())
12862                    .unwrap_or(false);
12863                let list_val = self.eval_expr(list)?;
12864                let mut items = list_val.to_list();
12865                let pmap_progress = PmapProgress::new(show_progress, 2);
12866                pmap_progress.tick();
12867                if let Some(cmp_block) = cmp {
12868                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
12869                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
12870                    } else {
12871                        let cmp_block = cmp_block.clone();
12872                        let subs = self.subs.clone();
12873                        let scope_capture = self.scope.capture();
12874                        items.par_sort_by(|a, b| {
12875                            let mut local_interp = VMHelper::new();
12876                            local_interp.subs = subs.clone();
12877                            local_interp.scope.restore_capture(&scope_capture);
12878                            local_interp.scope.set_sort_pair(a.clone(), b.clone());
12879                            match local_interp.exec_block(&cmp_block) {
12880                                Ok(v) => {
12881                                    let n = v.to_int();
12882                                    if n < 0 {
12883                                        std::cmp::Ordering::Less
12884                                    } else if n > 0 {
12885                                        std::cmp::Ordering::Greater
12886                                    } else {
12887                                        std::cmp::Ordering::Equal
12888                                    }
12889                                }
12890                                Err(_) => std::cmp::Ordering::Equal,
12891                            }
12892                        });
12893                    }
12894                } else {
12895                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
12896                }
12897                pmap_progress.tick();
12898                pmap_progress.finish();
12899                Ok(StrykeValue::array(items))
12900            }
12901
12902            ExprKind::ReduceExpr { block, list } => {
12903                let list_val = self.eval_expr(list)?;
12904                let items = list_val.to_list();
12905                if items.is_empty() {
12906                    return Ok(StrykeValue::UNDEF);
12907                }
12908                if items.len() == 1 {
12909                    return Ok(items.into_iter().next().unwrap());
12910                }
12911                let block = block.clone();
12912                let subs = self.subs.clone();
12913                let scope_capture = self.scope.capture();
12914                let mut acc = items[0].clone();
12915                for b in items.into_iter().skip(1) {
12916                    let mut local_interp = VMHelper::new();
12917                    local_interp.subs = subs.clone();
12918                    local_interp.scope.restore_capture(&scope_capture);
12919                    local_interp.scope.set_sort_pair(acc, b);
12920                    acc = match local_interp.exec_block(&block) {
12921                        Ok(val) => val,
12922                        Err(_) => StrykeValue::UNDEF,
12923                    };
12924                }
12925                Ok(acc)
12926            }
12927
12928            ExprKind::PReduceExpr {
12929                block,
12930                list,
12931                progress,
12932            } => {
12933                let show_progress = progress
12934                    .as_ref()
12935                    .map(|p| self.eval_expr(p))
12936                    .transpose()?
12937                    .map(|v| v.is_true())
12938                    .unwrap_or(false);
12939                let list_val = self.eval_expr(list)?;
12940                let items = list_val.to_list();
12941                if items.is_empty() {
12942                    return Ok(StrykeValue::UNDEF);
12943                }
12944                if items.len() == 1 {
12945                    return Ok(items.into_iter().next().unwrap());
12946                }
12947                let block = block.clone();
12948                let subs = self.subs.clone();
12949                let scope_capture = self.scope.capture();
12950                let pmap_progress = PmapProgress::new(show_progress, items.len());
12951
12952                let result = items
12953                    .into_par_iter()
12954                    .map(|x| {
12955                        pmap_progress.tick();
12956                        x
12957                    })
12958                    .reduce_with(|a, b| {
12959                        let mut local_interp = VMHelper::new();
12960                        local_interp.subs = subs.clone();
12961                        local_interp.scope.restore_capture(&scope_capture);
12962                        local_interp.scope.set_sort_pair(a, b);
12963                        match local_interp.exec_block(&block) {
12964                            Ok(val) => val,
12965                            Err(_) => StrykeValue::UNDEF,
12966                        }
12967                    });
12968                pmap_progress.finish();
12969                Ok(result.unwrap_or(StrykeValue::UNDEF))
12970            }
12971
12972            ExprKind::PReduceInitExpr {
12973                init,
12974                block,
12975                list,
12976                progress,
12977            } => {
12978                let show_progress = progress
12979                    .as_ref()
12980                    .map(|p| self.eval_expr(p))
12981                    .transpose()?
12982                    .map(|v| v.is_true())
12983                    .unwrap_or(false);
12984                let init_val = self.eval_expr(init)?;
12985                let list_val = self.eval_expr(list)?;
12986                let items = list_val.to_list();
12987                if items.is_empty() {
12988                    return Ok(init_val);
12989                }
12990                let block = block.clone();
12991                let subs = self.subs.clone();
12992                let scope_capture = self.scope.capture();
12993                let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
12994                if items.len() == 1 {
12995                    return Ok(fold_preduce_init_step(
12996                        &subs,
12997                        cap,
12998                        &block,
12999                        preduce_init_fold_identity(&init_val),
13000                        items.into_iter().next().unwrap(),
13001                    ));
13002                }
13003                let pmap_progress = PmapProgress::new(show_progress, items.len());
13004                let result = items
13005                    .into_par_iter()
13006                    .fold(
13007                        || preduce_init_fold_identity(&init_val),
13008                        |acc, item| {
13009                            pmap_progress.tick();
13010                            fold_preduce_init_step(&subs, cap, &block, acc, item)
13011                        },
13012                    )
13013                    .reduce(
13014                        || preduce_init_fold_identity(&init_val),
13015                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
13016                    );
13017                pmap_progress.finish();
13018                Ok(result)
13019            }
13020
13021            ExprKind::PMapReduceExpr {
13022                map_block,
13023                reduce_block,
13024                list,
13025                progress,
13026            } => {
13027                let show_progress = progress
13028                    .as_ref()
13029                    .map(|p| self.eval_expr(p))
13030                    .transpose()?
13031                    .map(|v| v.is_true())
13032                    .unwrap_or(false);
13033                let list_val = self.eval_expr(list)?;
13034                let items = list_val.to_list();
13035                if items.is_empty() {
13036                    return Ok(StrykeValue::UNDEF);
13037                }
13038                let map_block = map_block.clone();
13039                let reduce_block = reduce_block.clone();
13040                let subs = self.subs.clone();
13041                let scope_capture = self.scope.capture();
13042                if items.len() == 1 {
13043                    let mut local_interp = VMHelper::new();
13044                    local_interp.subs = subs.clone();
13045                    local_interp.scope.restore_capture(&scope_capture);
13046                    local_interp.scope.set_topic(items[0].clone());
13047                    return match local_interp.exec_block_no_scope(&map_block) {
13048                        Ok(v) => Ok(v),
13049                        Err(_) => Ok(StrykeValue::UNDEF),
13050                    };
13051                }
13052                let pmap_progress = PmapProgress::new(show_progress, items.len());
13053                let result = items
13054                    .into_par_iter()
13055                    .map(|item| {
13056                        let mut local_interp = VMHelper::new();
13057                        local_interp.subs = subs.clone();
13058                        local_interp.scope.restore_capture(&scope_capture);
13059                        local_interp.scope.set_topic(item);
13060                        let val = match local_interp.exec_block_no_scope(&map_block) {
13061                            Ok(val) => val,
13062                            Err(_) => StrykeValue::UNDEF,
13063                        };
13064                        pmap_progress.tick();
13065                        val
13066                    })
13067                    .reduce_with(|a, b| {
13068                        let mut local_interp = VMHelper::new();
13069                        local_interp.subs = subs.clone();
13070                        local_interp.scope.restore_capture(&scope_capture);
13071                        local_interp.scope.set_sort_pair(a, b);
13072                        match local_interp.exec_block_no_scope(&reduce_block) {
13073                            Ok(val) => val,
13074                            Err(_) => StrykeValue::UNDEF,
13075                        }
13076                    });
13077                pmap_progress.finish();
13078                Ok(result.unwrap_or(StrykeValue::UNDEF))
13079            }
13080
13081            ExprKind::PcacheExpr {
13082                block,
13083                list,
13084                progress,
13085            } => {
13086                let show_progress = progress
13087                    .as_ref()
13088                    .map(|p| self.eval_expr(p))
13089                    .transpose()?
13090                    .map(|v| v.is_true())
13091                    .unwrap_or(false);
13092                let list_val = self.eval_expr(list)?;
13093                let items = list_val.to_list();
13094                let block = block.clone();
13095                let subs = self.subs.clone();
13096                let scope_capture = self.scope.capture();
13097                let cache = &*crate::pcache::GLOBAL_PCACHE;
13098                let pmap_progress = PmapProgress::new(show_progress, items.len());
13099                let results: Vec<StrykeValue> = items
13100                    .into_par_iter()
13101                    .map(|item| {
13102                        let k = crate::pcache::cache_key(&item);
13103                        if let Some(v) = cache.get(&k) {
13104                            pmap_progress.tick();
13105                            return v.clone();
13106                        }
13107                        let mut local_interp = VMHelper::new();
13108                        local_interp.subs = subs.clone();
13109                        local_interp.scope.restore_capture(&scope_capture);
13110                        local_interp.scope.set_topic(item.clone());
13111                        let val = match local_interp.exec_block_no_scope(&block) {
13112                            Ok(v) => v,
13113                            Err(_) => StrykeValue::UNDEF,
13114                        };
13115                        cache.insert(k, val.clone());
13116                        pmap_progress.tick();
13117                        val
13118                    })
13119                    .collect();
13120                pmap_progress.finish();
13121                Ok(StrykeValue::array(results))
13122            }
13123
13124            ExprKind::PselectExpr { receivers, timeout } => {
13125                let mut rx_vals = Vec::with_capacity(receivers.len());
13126                for r in receivers {
13127                    rx_vals.push(self.eval_expr(r)?);
13128                }
13129                let dur = if let Some(t) = timeout.as_ref() {
13130                    Some(std::time::Duration::from_secs_f64(
13131                        self.eval_expr(t)?.to_number().max(0.0),
13132                    ))
13133                } else {
13134                    None
13135                };
13136                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
13137                    &rx_vals, dur, line,
13138                )?)
13139            }
13140
13141            // Array ops
13142            ExprKind::Push { array, values } => {
13143                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
13144            }
13145            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
13146            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
13147            ExprKind::Unshift { array, values } => {
13148                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
13149            }
13150            ExprKind::Splice {
13151                array,
13152                offset,
13153                length,
13154                replacement,
13155            } => self.eval_splice_expr(
13156                array.as_ref(),
13157                offset.as_deref(),
13158                length.as_deref(),
13159                replacement.as_slice(),
13160                ctx,
13161                line,
13162            ),
13163            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
13164            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
13165            ExprKind::Keys(expr) => {
13166                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
13167                let keys = Self::keys_from_value(val, line)?;
13168                if ctx == WantarrayCtx::List {
13169                    Ok(keys)
13170                } else {
13171                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
13172                    Ok(StrykeValue::integer(n as i64))
13173                }
13174            }
13175            ExprKind::Values(expr) => {
13176                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
13177                let vals = Self::values_from_value(val, line)?;
13178                if ctx == WantarrayCtx::List {
13179                    Ok(vals)
13180                } else {
13181                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
13182                    Ok(StrykeValue::integer(n as i64))
13183                }
13184            }
13185            ExprKind::Each(_) => {
13186                // Simplified: returns empty list (full iterator state would need more work)
13187                Ok(StrykeValue::array(vec![]))
13188            }
13189
13190            // String ops
13191            ExprKind::Chomp(expr) => {
13192                let val = self.eval_expr(expr)?;
13193                self.chomp_inplace_execute(val, expr)
13194            }
13195            ExprKind::Chop(expr) => {
13196                let val = self.eval_expr(expr)?;
13197                self.chop_inplace_execute(val, expr)
13198            }
13199            ExprKind::Length(expr) => {
13200                let val = self.eval_expr(expr)?;
13201                Ok(if let Some(a) = val.as_array_vec() {
13202                    StrykeValue::integer(a.len() as i64)
13203                } else if let Some(h) = val.as_hash_map() {
13204                    StrykeValue::integer(h.len() as i64)
13205                } else if let Some(b) = val.as_bytes_arc() {
13206                    // Raw byte buffer: always byte count, regardless of utf8 pragma.
13207                    StrykeValue::integer(b.len() as i64)
13208                } else {
13209                    let s = val.to_string();
13210                    let n = if self.utf8_pragma {
13211                        s.chars().count()
13212                    } else {
13213                        s.len()
13214                    };
13215                    StrykeValue::integer(n as i64)
13216                })
13217            }
13218            ExprKind::Substr {
13219                string,
13220                offset,
13221                length,
13222                replacement,
13223            } => self.eval_substr_expr(
13224                string.as_ref(),
13225                offset.as_ref(),
13226                length.as_deref(),
13227                replacement.as_deref(),
13228                line,
13229            ),
13230            ExprKind::Index {
13231                string,
13232                substr,
13233                position,
13234            } => {
13235                let s = self.eval_expr(string)?.to_string();
13236                let sub = self.eval_expr(substr)?.to_string();
13237                // Perl: negative POS clamps to 0; POS past end returns -1
13238                // (or, for empty needle, returns POS clamped to len).
13239                let pos = if let Some(p) = position {
13240                    let raw = self.eval_expr(p)?.to_int();
13241                    if raw < 0 {
13242                        0usize
13243                    } else {
13244                        (raw as usize).min(s.len())
13245                    }
13246                } else {
13247                    0
13248                };
13249                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
13250                Ok(StrykeValue::integer(result))
13251            }
13252            ExprKind::Rindex {
13253                string,
13254                substr,
13255                position,
13256            } => {
13257                let s = self.eval_expr(string)?.to_string();
13258                let sub = self.eval_expr(substr)?.to_string();
13259                // Perl: negative POS means "search must end at or before POS";
13260                // any negative POS implies no possible match → -1.
13261                let result = if let Some(p) = position {
13262                    let raw = self.eval_expr(p)?.to_int();
13263                    if raw < 0 {
13264                        -1
13265                    } else {
13266                        let end = (raw as usize).saturating_add(sub.len()).min(s.len());
13267                        s[..end].rfind(&sub).map(|i| i as i64).unwrap_or(-1)
13268                    }
13269                } else {
13270                    s.rfind(&sub).map(|i| i as i64).unwrap_or(-1)
13271                };
13272                Ok(StrykeValue::integer(result))
13273            }
13274            ExprKind::Sprintf { format, args } => {
13275                let fmt = self.eval_expr(format)?.to_string();
13276                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
13277                // builtins into individual format arguments.
13278                let mut arg_vals = Vec::new();
13279                for a in args {
13280                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
13281                    if let Some(items) = v.as_array_vec() {
13282                        arg_vals.extend(items);
13283                    } else {
13284                        arg_vals.push(v);
13285                    }
13286                }
13287                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
13288                Ok(StrykeValue::string(s))
13289            }
13290            ExprKind::JoinExpr { separator, list } => {
13291                let sep = self.eval_expr(separator)?.to_string();
13292                // Like Perl 5, arguments after the separator are evaluated in list context so
13293                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
13294                // expands `localtime` to nine fields.
13295                let items = if let ExprKind::List(exprs) = &list.kind {
13296                    let saved = self.wantarray_kind;
13297                    self.wantarray_kind = WantarrayCtx::List;
13298                    let mut vals = Vec::new();
13299                    for e in exprs {
13300                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
13301                        if let Some(items) = v.as_array_vec() {
13302                            vals.extend(items);
13303                        } else if v.is_iterator() {
13304                            // `join "", rev chars(...)` etc. — drain the
13305                            // lazy iterator into items so it joins the
13306                            // sequence instead of stringifying as "Iterator".
13307                            vals.extend(v.into_iterator().collect_all());
13308                        } else {
13309                            vals.push(v);
13310                        }
13311                    }
13312                    self.wantarray_kind = saved;
13313                    vals
13314                } else {
13315                    let saved = self.wantarray_kind;
13316                    self.wantarray_kind = WantarrayCtx::List;
13317                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
13318                    self.wantarray_kind = saved;
13319                    if let Some(items) = v.as_array_vec() {
13320                        items
13321                    } else if v.is_iterator() {
13322                        // `~> ... rev |> join ""` produces an Iterator from
13323                        // the lazy stages; drain it before joining, otherwise
13324                        // it stringifies as "Iterator".
13325                        v.into_iterator().collect_all()
13326                    } else {
13327                        vec![v]
13328                    }
13329                };
13330                let mut strs = Vec::with_capacity(items.len());
13331                for v in &items {
13332                    strs.push(self.stringify_value(v.clone(), line)?);
13333                }
13334                Ok(StrykeValue::string(strs.join(&sep)))
13335            }
13336            ExprKind::SplitExpr {
13337                pattern,
13338                string,
13339                limit,
13340            } => {
13341                let pat_val = self.eval_expr(pattern)?;
13342                // For a regex value, pull the *source* (not the Display form,
13343                // which wraps an empty regex as `(?:)` and would defeat the
13344                // empty-pattern branch below).  Mirrors the VM's Split path.
13345                let pat = pat_val
13346                    .regex_src_and_flags()
13347                    .map(|(s, _)| s)
13348                    .unwrap_or_else(|| pat_val.to_string());
13349                let s = self.eval_expr(string)?.to_string();
13350                if s.is_empty() {
13351                    return Ok(StrykeValue::array(vec![]));
13352                }
13353                // Perl semantics for the limit field:
13354                //   omitted / 0  → no truncation, *strip* trailing empty fields.
13355                //   > 0          → at most LIMIT fields, keep empties up to limit.
13356                //   < 0          → no truncation, *keep* all empties.
13357                // Stryke previously parsed limit as `usize`, which folded a
13358                // user-supplied -1 into a giant positive number and made the
13359                // strip / keep decision ambiguous. Use `i64` so the sign is
13360                // preserved.
13361                let lim_opt: Option<i64> = limit
13362                    .as_ref()
13363                    .map(|l| self.eval_expr(l).map(|v| v.to_int()))
13364                    .transpose()?;
13365                let re = self.compile_regex(&pat, "", line)?;
13366                let mut parts: Vec<String> = match lim_opt {
13367                    Some(l) if l > 0 => re.splitn_strings(&s, l as usize),
13368                    _ => re.split_strings(&s),
13369                };
13370
13371                // Zero-width patterns (`split //, $s`) are defined by Perl as
13372                // "split between every character" — the regex engine, however,
13373                // also matches the empty string at position 0, producing a
13374                // spurious leading empty field that Perl does not emit. Strip
13375                // it before the trailing-empty rule kicks in.
13376                if pat.is_empty() && parts.first().is_some_and(|p| p.is_empty()) {
13377                    parts.remove(0);
13378                }
13379                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted
13380                // or zero. Positive LIMIT keeps trailing empties; negative
13381                // LIMIT also keeps them.
13382                let strip_trailing = matches!(lim_opt, None | Some(0));
13383                if strip_trailing {
13384                    while parts.last().is_some_and(|p| p.is_empty()) {
13385                        parts.pop();
13386                    }
13387                }
13388
13389                Ok(StrykeValue::array(
13390                    parts.into_iter().map(StrykeValue::string).collect(),
13391                ))
13392            }
13393
13394            // Numeric
13395            ExprKind::Abs(expr) => {
13396                let val = self.eval_expr(expr)?;
13397                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
13398                    return r;
13399                }
13400                Ok(StrykeValue::float(val.to_number().abs()))
13401            }
13402            ExprKind::Int(expr) => {
13403                let val = self.eval_expr(expr)?;
13404                Ok(StrykeValue::integer(val.to_number() as i64))
13405            }
13406            ExprKind::Sqrt(expr) => {
13407                let val = self.eval_expr(expr)?;
13408                Ok(StrykeValue::float(val.to_number().sqrt()))
13409            }
13410            ExprKind::Sin(expr) => {
13411                let val = self.eval_expr(expr)?;
13412                Ok(StrykeValue::float(val.to_number().sin()))
13413            }
13414            ExprKind::Cos(expr) => {
13415                let val = self.eval_expr(expr)?;
13416                Ok(StrykeValue::float(val.to_number().cos()))
13417            }
13418            ExprKind::Atan2 { y, x } => {
13419                let yv = self.eval_expr(y)?.to_number();
13420                let xv = self.eval_expr(x)?.to_number();
13421                Ok(StrykeValue::float(yv.atan2(xv)))
13422            }
13423            ExprKind::Exp(expr) => {
13424                let val = self.eval_expr(expr)?;
13425                Ok(StrykeValue::float(val.to_number().exp()))
13426            }
13427            ExprKind::Log(expr) => {
13428                let val = self.eval_expr(expr)?;
13429                Ok(StrykeValue::float(val.to_number().ln()))
13430            }
13431            ExprKind::Rand(upper) => {
13432                let u = match upper {
13433                    Some(e) => self.eval_expr(e)?.to_number(),
13434                    None => 1.0,
13435                };
13436                Ok(StrykeValue::float(self.perl_rand(u)))
13437            }
13438            ExprKind::Srand(seed) => {
13439                let s = match seed {
13440                    Some(e) => Some(self.eval_expr(e)?.to_number()),
13441                    None => None,
13442                };
13443                Ok(StrykeValue::integer(self.perl_srand(s)))
13444            }
13445            ExprKind::Hex(expr) => {
13446                let val = self.eval_expr(expr)?.to_string();
13447                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
13448                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
13449                Ok(StrykeValue::integer(n))
13450            }
13451            ExprKind::Oct(expr) => {
13452                let val = self.eval_expr(expr)?.to_string();
13453                let s = val.trim();
13454                let n = if s.starts_with("0x") || s.starts_with("0X") {
13455                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
13456                } else if s.starts_with("0b") || s.starts_with("0B") {
13457                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
13458                } else if s.starts_with("0o") || s.starts_with("0O") {
13459                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
13460                } else {
13461                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
13462                };
13463                Ok(StrykeValue::integer(n))
13464            }
13465
13466            // Case
13467            ExprKind::Lc(expr) => Ok(StrykeValue::string(
13468                self.eval_expr(expr)?.to_string().to_lowercase(),
13469            )),
13470            ExprKind::Uc(expr) => Ok(StrykeValue::string(
13471                self.eval_expr(expr)?.to_string().to_uppercase(),
13472            )),
13473            ExprKind::Lcfirst(expr) => {
13474                let s = self.eval_expr(expr)?.to_string();
13475                let mut chars = s.chars();
13476                let result = match chars.next() {
13477                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
13478                    None => String::new(),
13479                };
13480                Ok(StrykeValue::string(result))
13481            }
13482            ExprKind::Ucfirst(expr) => {
13483                let s = self.eval_expr(expr)?.to_string();
13484                let mut chars = s.chars();
13485                let result = match chars.next() {
13486                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
13487                    None => String::new(),
13488                };
13489                Ok(StrykeValue::string(result))
13490            }
13491            ExprKind::Fc(expr) => Ok(StrykeValue::string(default_case_fold_str(
13492                &self.eval_expr(expr)?.to_string(),
13493            ))),
13494            ExprKind::Quotemeta(expr) => Ok(StrykeValue::string(crate::perl_regex::perl_quotemeta(
13495                &self.eval_expr(expr)?.to_string(),
13496            ))),
13497            ExprKind::Crypt { plaintext, salt } => {
13498                let p = self.eval_expr(plaintext)?.to_string();
13499                let sl = self.eval_expr(salt)?.to_string();
13500                Ok(StrykeValue::string(perl_crypt(&p, &sl)))
13501            }
13502            ExprKind::Pos(e) => {
13503                let key = match e {
13504                    None => "_".to_string(),
13505                    Some(expr) => match &expr.kind {
13506                        ExprKind::ScalarVar(n) => n.clone(),
13507                        _ => self.eval_expr(expr)?.to_string(),
13508                    },
13509                };
13510                Ok(self
13511                    .regex_pos
13512                    .get(&key)
13513                    .copied()
13514                    .flatten()
13515                    .map(|p| StrykeValue::integer(p as i64))
13516                    .unwrap_or(StrykeValue::UNDEF))
13517            }
13518            ExprKind::Study(expr) => {
13519                let s = self.eval_expr(expr)?.to_string();
13520                Ok(Self::study_return_value(&s))
13521            }
13522
13523            // Type
13524            ExprKind::Defined(expr) => {
13525                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
13526                if let ExprKind::SubroutineRef(name) = &expr.kind {
13527                    let exists = self.resolve_sub_by_name(name).is_some();
13528                    return Ok(StrykeValue::integer(if exists { 1 } else { 0 }));
13529                }
13530                let val = self.eval_expr(expr)?;
13531                Ok(StrykeValue::integer(if val.is_undef() { 0 } else { 1 }))
13532            }
13533            ExprKind::Ref(expr) => {
13534                let val = self.eval_expr(expr)?;
13535                Ok(val.ref_type())
13536            }
13537            ExprKind::ScalarContext(expr) => {
13538                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
13539                Ok(v.scalar_context())
13540            }
13541
13542            // Char
13543            ExprKind::Chr(expr) => {
13544                let n = self.eval_expr(expr)?.to_int() as u32;
13545                Ok(StrykeValue::string(
13546                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
13547                ))
13548            }
13549            ExprKind::Ord(expr) => {
13550                let s = self.eval_expr(expr)?.to_string();
13551                Ok(StrykeValue::integer(
13552                    s.chars().next().map(|c| c as i64).unwrap_or(0),
13553                ))
13554            }
13555
13556            // I/O
13557            ExprKind::OpenMyHandle { .. } => Err(StrykeError::runtime(
13558                "internal: `open my $fh` handle used outside open()",
13559                line,
13560            )
13561            .into()),
13562            ExprKind::OpendirMyHandle { .. } => Err(StrykeError::runtime(
13563                "internal: `opendir my $dh` handle used outside opendir()",
13564                line,
13565            )
13566            .into()),
13567            ExprKind::Open { handle, mode, file } => {
13568                if let ExprKind::OpenMyHandle { name } = &handle.kind {
13569                    self.scope
13570                        .declare_scalar_frozen(name, StrykeValue::UNDEF, false, None)?;
13571                    self.english_note_lexical_scalar(name);
13572                    let mode_s = self.eval_expr(mode)?.to_string();
13573                    let file_opt = if let Some(f) = file {
13574                        Some(self.eval_expr(f)?.to_string())
13575                    } else {
13576                        None
13577                    };
13578                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
13579                    self.scope.set_scalar(name, ret.clone())?;
13580                    return Ok(ret);
13581                }
13582                let handle_s = self.eval_expr(handle)?.to_string();
13583                let handle_name = self.resolve_io_handle_name(&handle_s);
13584                let mode_s = self.eval_expr(mode)?.to_string();
13585                let file_opt = if let Some(f) = file {
13586                    Some(self.eval_expr(f)?.to_string())
13587                } else {
13588                    None
13589                };
13590                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
13591                    .map_err(Into::into)
13592            }
13593            ExprKind::Close(expr) => {
13594                let s = self.eval_expr(expr)?.to_string();
13595                let name = self.resolve_io_handle_name(&s);
13596                self.close_builtin_execute(name).map_err(Into::into)
13597            }
13598            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
13599                self.readline_builtin_execute_list(handle.as_deref())
13600            } else {
13601                self.readline_builtin_execute(handle.as_deref())
13602            }
13603            .map_err(Into::into),
13604            ExprKind::Eof(expr) => match expr {
13605                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
13606                Some(e) => {
13607                    let name = self.eval_expr(e)?;
13608                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
13609                }
13610            },
13611
13612            ExprKind::Opendir { handle, path } => {
13613                if let ExprKind::OpendirMyHandle { name } = &handle.kind {
13614                    self.scope
13615                        .declare_scalar_frozen(name, StrykeValue::UNDEF, false, None)?;
13616                    self.english_note_lexical_scalar(name);
13617                    let p = self.eval_expr(path)?.to_string();
13618                    let ret = self.opendir_handle(name, &p);
13619                    self.scope.set_scalar(name, ret.clone())?;
13620                    return Ok(ret);
13621                }
13622                let h = self.eval_expr(handle)?.to_string();
13623                let p = self.eval_expr(path)?.to_string();
13624                Ok(self.opendir_handle(&h, &p))
13625            }
13626            ExprKind::Readdir(e) => {
13627                let h = self.eval_expr(e)?.to_string();
13628                Ok(if ctx == WantarrayCtx::List {
13629                    self.readdir_handle_list(&h)
13630                } else {
13631                    self.readdir_handle(&h)
13632                })
13633            }
13634            ExprKind::Closedir(e) => {
13635                let h = self.eval_expr(e)?.to_string();
13636                Ok(self.closedir_handle(&h))
13637            }
13638            ExprKind::Rewinddir(e) => {
13639                let h = self.eval_expr(e)?.to_string();
13640                Ok(self.rewinddir_handle(&h))
13641            }
13642            ExprKind::Telldir(e) => {
13643                let h = self.eval_expr(e)?.to_string();
13644                Ok(self.telldir_handle(&h))
13645            }
13646            ExprKind::Seekdir { handle, position } => {
13647                let h = self.eval_expr(handle)?.to_string();
13648                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
13649                Ok(self.seekdir_handle(&h, pos))
13650            }
13651
13652            // File tests
13653            ExprKind::FileTest { op, expr } => {
13654                let raw = self.eval_expr(expr)?.to_string();
13655                let path = self.resolve_stryke_path_string(&raw);
13656                // -M, -A, -C return fractional days (float), not boolean
13657                if matches!(op, 'M' | 'A' | 'C') {
13658                    #[cfg(unix)]
13659                    {
13660                        return match crate::perl_fs::filetest_age_days(&path, *op) {
13661                            Some(days) => Ok(StrykeValue::float(days)),
13662                            None => Ok(StrykeValue::UNDEF),
13663                        };
13664                    }
13665                    #[cfg(not(unix))]
13666                    return Ok(StrykeValue::UNDEF);
13667                }
13668                // -s returns file size (or undef on error)
13669                if *op == 's' {
13670                    return match std::fs::metadata(&path) {
13671                        Ok(m) => Ok(StrykeValue::integer(m.len() as i64)),
13672                        Err(_) => Ok(StrykeValue::UNDEF),
13673                    };
13674                }
13675                let result = match op {
13676                    'e' => std::path::Path::new(&path).exists(),
13677                    'f' => std::path::Path::new(&path).is_file(),
13678                    'd' => std::path::Path::new(&path).is_dir(),
13679                    'l' => std::path::Path::new(&path).is_symlink(),
13680                    #[cfg(unix)]
13681                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
13682                    #[cfg(not(unix))]
13683                    'r' => std::fs::metadata(&path).is_ok(),
13684                    #[cfg(unix)]
13685                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
13686                    #[cfg(not(unix))]
13687                    'w' => std::fs::metadata(&path).is_ok(),
13688                    #[cfg(unix)]
13689                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
13690                    #[cfg(not(unix))]
13691                    'x' => false,
13692                    #[cfg(unix)]
13693                    'o' => crate::perl_fs::filetest_owned_effective(&path),
13694                    #[cfg(not(unix))]
13695                    'o' => false,
13696                    #[cfg(unix)]
13697                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
13698                    #[cfg(not(unix))]
13699                    'R' => false,
13700                    #[cfg(unix)]
13701                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
13702                    #[cfg(not(unix))]
13703                    'W' => false,
13704                    #[cfg(unix)]
13705                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
13706                    #[cfg(not(unix))]
13707                    'X' => false,
13708                    #[cfg(unix)]
13709                    'O' => crate::perl_fs::filetest_owned_real(&path),
13710                    #[cfg(not(unix))]
13711                    'O' => false,
13712                    'z' => std::fs::metadata(&path)
13713                        .map(|m| m.len() == 0)
13714                        .unwrap_or(true),
13715                    't' => crate::perl_fs::filetest_is_tty(&path),
13716                    #[cfg(unix)]
13717                    'p' => crate::perl_fs::filetest_is_pipe(&path),
13718                    #[cfg(not(unix))]
13719                    'p' => false,
13720                    #[cfg(unix)]
13721                    'S' => crate::perl_fs::filetest_is_socket(&path),
13722                    #[cfg(not(unix))]
13723                    'S' => false,
13724                    #[cfg(unix)]
13725                    'b' => crate::perl_fs::filetest_is_block_device(&path),
13726                    #[cfg(not(unix))]
13727                    'b' => false,
13728                    #[cfg(unix)]
13729                    'c' => crate::perl_fs::filetest_is_char_device(&path),
13730                    #[cfg(not(unix))]
13731                    'c' => false,
13732                    #[cfg(unix)]
13733                    'u' => crate::perl_fs::filetest_is_setuid(&path),
13734                    #[cfg(not(unix))]
13735                    'u' => false,
13736                    #[cfg(unix)]
13737                    'g' => crate::perl_fs::filetest_is_setgid(&path),
13738                    #[cfg(not(unix))]
13739                    'g' => false,
13740                    #[cfg(unix)]
13741                    'k' => crate::perl_fs::filetest_is_sticky(&path),
13742                    #[cfg(not(unix))]
13743                    'k' => false,
13744                    'T' => crate::perl_fs::filetest_is_text(&path),
13745                    'B' => crate::perl_fs::filetest_is_binary(&path),
13746                    _ => false,
13747                };
13748                Ok(StrykeValue::integer(if result { 1 } else { 0 }))
13749            }
13750
13751            // System
13752            ExprKind::System(args) => {
13753                // Perl `system`:
13754                //   - single string  → `sh -c "..."`
13755                //   - 2+ args        → exec program directly with the rest as
13756                //                     argv (skips the shell, so quoting bugs
13757                //                     in user data don't matter).
13758                // Return value is `$?` (status word), not the bare exit code.
13759                let mut cmd_args = Vec::new();
13760                for a in args {
13761                    cmd_args.push(self.eval_expr(a)?.to_string());
13762                }
13763                if cmd_args.is_empty() {
13764                    self.child_exit_status = -1;
13765                    return Ok(StrykeValue::integer(-1));
13766                }
13767                let status = if cmd_args.len() == 1 {
13768                    Command::new("sh").arg("-c").arg(&cmd_args[0]).status()
13769                } else {
13770                    Command::new(&cmd_args[0]).args(&cmd_args[1..]).status()
13771                };
13772                match status {
13773                    Ok(s) => {
13774                        self.record_child_exit_status(s);
13775                        Ok(StrykeValue::integer(self.child_exit_status))
13776                    }
13777                    Err(e) => {
13778                        self.apply_io_error_to_errno(&e);
13779                        self.child_exit_status = -1;
13780                        Ok(StrykeValue::integer(-1))
13781                    }
13782                }
13783            }
13784            ExprKind::Exec(args) => {
13785                let mut cmd_args = Vec::new();
13786                for a in args {
13787                    cmd_args.push(self.eval_expr(a)?.to_string());
13788                }
13789                if cmd_args.is_empty() {
13790                    return Ok(StrykeValue::integer(-1));
13791                }
13792                let status = Command::new("sh")
13793                    .arg("-c")
13794                    .arg(cmd_args.join(" "))
13795                    .status();
13796                match status {
13797                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
13798                    Err(e) => {
13799                        self.apply_io_error_to_errno(&e);
13800                        Ok(StrykeValue::integer(-1))
13801                    }
13802                }
13803            }
13804            ExprKind::Eval(expr) => {
13805                self.eval_nesting += 1;
13806                let out = match &expr.kind {
13807                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
13808                        Ok(v) => {
13809                            self.clear_eval_error();
13810                            Ok(v)
13811                        }
13812                        Err(FlowOrError::Error(e)) => {
13813                            self.set_eval_error_from_perl_error(&e);
13814                            Ok(StrykeValue::UNDEF)
13815                        }
13816                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
13817                    },
13818                    _ => {
13819                        let code = self.eval_expr(expr)?.to_string();
13820                        // Parse and execute the string as Perl code
13821                        match crate::parse_and_run_string(&code, self) {
13822                            Ok(v) => {
13823                                self.clear_eval_error();
13824                                Ok(v)
13825                            }
13826                            Err(e) => {
13827                                self.set_eval_error(e.to_string());
13828                                Ok(StrykeValue::UNDEF)
13829                            }
13830                        }
13831                    }
13832                };
13833                self.eval_nesting -= 1;
13834                out
13835            }
13836            ExprKind::Do(expr) => match &expr.kind {
13837                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
13838                _ => {
13839                    let val = self.eval_expr(expr)?;
13840                    let filename = val.to_string();
13841                    match read_file_text_perl_compat(&filename) {
13842                        Ok(code) => {
13843                            let code = crate::data_section::strip_perl_end_marker(&code);
13844                            match crate::parse_and_run_string_in_file(code, self, &filename) {
13845                                Ok(v) => Ok(v),
13846                                Err(e) => {
13847                                    self.set_eval_error(e.to_string());
13848                                    Ok(StrykeValue::UNDEF)
13849                                }
13850                            }
13851                        }
13852                        Err(e) => {
13853                            self.apply_io_error_to_errno(&e);
13854                            Ok(StrykeValue::UNDEF)
13855                        }
13856                    }
13857                }
13858            },
13859            ExprKind::Require(expr) => {
13860                let spec = self.eval_expr(expr)?.to_string();
13861                self.require_execute(&spec, line)
13862                    .map_err(FlowOrError::Error)
13863            }
13864            ExprKind::Exit(code) => {
13865                let c = if let Some(e) = code {
13866                    self.eval_expr(e)?.to_int() as i32
13867                } else {
13868                    0
13869                };
13870                Err(StrykeError::new(ErrorKind::Exit(c), "", line, &self.file).into())
13871            }
13872            ExprKind::Chdir(expr) => {
13873                let path = self.eval_expr(expr)?.to_string();
13874                match std::env::set_current_dir(&path) {
13875                    Ok(_) => {
13876                        if let Ok(c) = std::env::current_dir() {
13877                            self.stryke_pwd = std::fs::canonicalize(&c).unwrap_or(c);
13878                        }
13879                        Ok(StrykeValue::integer(1))
13880                    }
13881                    Err(e) => {
13882                        self.apply_io_error_to_errno(&e);
13883                        Ok(StrykeValue::integer(0))
13884                    }
13885                }
13886            }
13887            ExprKind::Mkdir { path, mode: _ } => {
13888                let raw = self.eval_expr(path)?.to_string();
13889                let p = self.resolve_stryke_path_string(&raw);
13890                match std::fs::create_dir(&p) {
13891                    Ok(_) => Ok(StrykeValue::integer(1)),
13892                    Err(e) => {
13893                        self.apply_io_error_to_errno(&e);
13894                        Ok(StrykeValue::integer(0))
13895                    }
13896                }
13897            }
13898            ExprKind::Unlink(args) => {
13899                let mut count = 0i64;
13900                for a in args {
13901                    let raw = self.eval_expr(a)?.to_string();
13902                    let path = self.resolve_stryke_path_string(&raw);
13903                    if std::fs::remove_file(&path).is_ok() {
13904                        count += 1;
13905                    }
13906                }
13907                Ok(StrykeValue::integer(count))
13908            }
13909            ExprKind::Rename { old, new } => {
13910                let o_raw = self.eval_expr(old)?.to_string();
13911                let n_raw = self.eval_expr(new)?.to_string();
13912                let o = self.resolve_stryke_path_string(&o_raw);
13913                let n = self.resolve_stryke_path_string(&n_raw);
13914                Ok(crate::perl_fs::rename_paths(&o, &n))
13915            }
13916            ExprKind::Chmod(args) => {
13917                let mode = self.eval_expr(&args[0])?.to_int();
13918                let mut paths = Vec::new();
13919                for a in &args[1..] {
13920                    let raw = self.eval_expr(a)?.to_string();
13921                    paths.push(self.resolve_stryke_path_string(&raw));
13922                }
13923                Ok(StrykeValue::integer(crate::perl_fs::chmod_paths(
13924                    &paths, mode,
13925                )))
13926            }
13927            ExprKind::Chown(args) => {
13928                let uid = self.eval_expr(&args[0])?.to_int();
13929                let gid = self.eval_expr(&args[1])?.to_int();
13930                let mut paths = Vec::new();
13931                for a in &args[2..] {
13932                    let raw = self.eval_expr(a)?.to_string();
13933                    paths.push(self.resolve_stryke_path_string(&raw));
13934                }
13935                Ok(StrykeValue::integer(crate::perl_fs::chown_paths(
13936                    &paths, uid, gid,
13937                )))
13938            }
13939            ExprKind::Stat(e) => {
13940                let raw = self.eval_expr(e)?.to_string();
13941                let path = self.resolve_stryke_path_string(&raw);
13942                Ok(crate::perl_fs::stat_path(&path, false))
13943            }
13944            ExprKind::Lstat(e) => {
13945                let raw = self.eval_expr(e)?.to_string();
13946                let path = self.resolve_stryke_path_string(&raw);
13947                Ok(crate::perl_fs::stat_path(&path, true))
13948            }
13949            ExprKind::Link { old, new } => {
13950                let o_raw = self.eval_expr(old)?.to_string();
13951                let n_raw = self.eval_expr(new)?.to_string();
13952                let o = self.resolve_stryke_path_string(&o_raw);
13953                let n = self.resolve_stryke_path_string(&n_raw);
13954                Ok(crate::perl_fs::link_hard(&o, &n))
13955            }
13956            ExprKind::Symlink { old, new } => {
13957                let o = self.eval_expr(old)?.to_string();
13958                let n_raw = self.eval_expr(new)?.to_string();
13959                let n = self.resolve_stryke_path_string(&n_raw);
13960                Ok(crate::perl_fs::link_sym(&o, &n))
13961            }
13962            ExprKind::Readlink(e) => {
13963                let raw = self.eval_expr(e)?.to_string();
13964                let path = self.resolve_stryke_path_string(&raw);
13965                Ok(crate::perl_fs::read_link(&path))
13966            }
13967            ExprKind::Files(args) => {
13968                let dir_raw = if args.is_empty() {
13969                    ".".to_string()
13970                } else {
13971                    self.eval_expr(&args[0])?.to_string()
13972                };
13973                let dir = self.resolve_stryke_path_string(&dir_raw);
13974                Ok(crate::perl_fs::list_files(&dir))
13975            }
13976            ExprKind::Filesf(args) => {
13977                let dir_raw = if args.is_empty() {
13978                    ".".to_string()
13979                } else {
13980                    self.eval_expr(&args[0])?.to_string()
13981                };
13982                let dir = self.resolve_stryke_path_string(&dir_raw);
13983                Ok(crate::perl_fs::list_filesf(&dir))
13984            }
13985            ExprKind::FilesfRecursive(args) => {
13986                let dir_raw = if args.is_empty() {
13987                    ".".to_string()
13988                } else {
13989                    self.eval_expr(&args[0])?.to_string()
13990                };
13991                let dir = self.resolve_stryke_path_string(&dir_raw);
13992                Ok(StrykeValue::iterator(Arc::new(
13993                    crate::value::FsWalkIterator::new(&dir, true),
13994                )))
13995            }
13996            ExprKind::Dirs(args) => {
13997                let dir_raw = if args.is_empty() {
13998                    ".".to_string()
13999                } else {
14000                    self.eval_expr(&args[0])?.to_string()
14001                };
14002                let dir = self.resolve_stryke_path_string(&dir_raw);
14003                Ok(crate::perl_fs::list_dirs(&dir))
14004            }
14005            ExprKind::DirsRecursive(args) => {
14006                let dir_raw = if args.is_empty() {
14007                    ".".to_string()
14008                } else {
14009                    self.eval_expr(&args[0])?.to_string()
14010                };
14011                let dir = self.resolve_stryke_path_string(&dir_raw);
14012                Ok(StrykeValue::iterator(Arc::new(
14013                    crate::value::FsWalkIterator::new(&dir, false),
14014                )))
14015            }
14016            ExprKind::SymLinks(args) => {
14017                let dir_raw = if args.is_empty() {
14018                    ".".to_string()
14019                } else {
14020                    self.eval_expr(&args[0])?.to_string()
14021                };
14022                let dir = self.resolve_stryke_path_string(&dir_raw);
14023                Ok(crate::perl_fs::list_sym_links(&dir))
14024            }
14025            ExprKind::Sockets(args) => {
14026                let dir_raw = if args.is_empty() {
14027                    ".".to_string()
14028                } else {
14029                    self.eval_expr(&args[0])?.to_string()
14030                };
14031                let dir = self.resolve_stryke_path_string(&dir_raw);
14032                Ok(crate::perl_fs::list_sockets(&dir))
14033            }
14034            ExprKind::Pipes(args) => {
14035                let dir_raw = if args.is_empty() {
14036                    ".".to_string()
14037                } else {
14038                    self.eval_expr(&args[0])?.to_string()
14039                };
14040                let dir = self.resolve_stryke_path_string(&dir_raw);
14041                Ok(crate::perl_fs::list_pipes(&dir))
14042            }
14043            ExprKind::BlockDevices(args) => {
14044                let dir_raw = if args.is_empty() {
14045                    ".".to_string()
14046                } else {
14047                    self.eval_expr(&args[0])?.to_string()
14048                };
14049                let dir = self.resolve_stryke_path_string(&dir_raw);
14050                Ok(crate::perl_fs::list_block_devices(&dir))
14051            }
14052            ExprKind::CharDevices(args) => {
14053                let dir_raw = if args.is_empty() {
14054                    ".".to_string()
14055                } else {
14056                    self.eval_expr(&args[0])?.to_string()
14057                };
14058                let dir = self.resolve_stryke_path_string(&dir_raw);
14059                Ok(crate::perl_fs::list_char_devices(&dir))
14060            }
14061            ExprKind::Executables(args) => {
14062                let dir_raw = if args.is_empty() {
14063                    ".".to_string()
14064                } else {
14065                    self.eval_expr(&args[0])?.to_string()
14066                };
14067                let dir = self.resolve_stryke_path_string(&dir_raw);
14068                Ok(crate::perl_fs::list_executables(&dir))
14069            }
14070            ExprKind::Glob(args) => {
14071                // Pass the user's pattern through unchanged: zsh::glob runs from
14072                // OS cwd, which `chdir` keeps in sync with `stryke_pwd`. Resolving
14073                // relative patterns to absolute paths up front would turn
14074                // `glob("**(/)")` results from "sub" into "/abs/.../sub" — breaking
14075                // the documented contract that relative patterns yield relative
14076                // results (pinned in tests/suite/glob_zsh_qualifiers.rs).
14077                let mut pats = Vec::new();
14078                for a in args {
14079                    pats.push(self.eval_expr(a)?.to_string());
14080                }
14081                Ok(crate::perl_fs::glob_patterns(&pats))
14082            }
14083            ExprKind::GlobPar { args, progress } => {
14084                let mut pats = Vec::new();
14085                for a in args {
14086                    pats.push(self.eval_expr(a)?.to_string());
14087                }
14088                let show_progress = progress
14089                    .as_ref()
14090                    .map(|p| self.eval_expr(p))
14091                    .transpose()?
14092                    .map(|v| v.is_true())
14093                    .unwrap_or(false);
14094                if show_progress {
14095                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
14096                } else {
14097                    Ok(crate::perl_fs::glob_par_patterns(&pats))
14098                }
14099            }
14100            ExprKind::ParSed { args, progress } => {
14101                let has_progress = progress.is_some();
14102                let mut vals: Vec<StrykeValue> = Vec::new();
14103                for a in args {
14104                    vals.push(self.eval_expr(a)?);
14105                }
14106                if let Some(p) = progress {
14107                    vals.push(self.eval_expr(p.as_ref())?);
14108                }
14109                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
14110            }
14111            ExprKind::Bless { ref_expr, class } => {
14112                let val = self.eval_expr(ref_expr)?;
14113                let class_name = if let Some(c) = class {
14114                    self.eval_expr(c)?.to_string()
14115                } else {
14116                    self.scope.get_scalar("__PACKAGE__").to_string()
14117                };
14118                Ok(StrykeValue::blessed(Arc::new(
14119                    crate::value::BlessedRef::new_blessed(class_name, val),
14120                )))
14121            }
14122            ExprKind::Caller(_) => {
14123                // Simplified caller frame: (package, file, line, subname). The
14124                // sub name is the fully-qualified name of the currently
14125                // executing sub (the one that invoked `caller`). Returning it
14126                // unblocks logger / decorator patterns that rely on `caller`
14127                // for "who called me" identification.
14128                let sub_name = self
14129                    .current_sub_stack
14130                    .last()
14131                    .map(|s| StrykeValue::string(s.name.clone()))
14132                    .unwrap_or(StrykeValue::UNDEF);
14133                let pkg = self.current_package();
14134                Ok(StrykeValue::array(vec![
14135                    StrykeValue::string(pkg),
14136                    StrykeValue::string(self.file.clone()),
14137                    StrykeValue::integer(line as i64),
14138                    sub_name,
14139                ]))
14140            }
14141            ExprKind::Wantarray => Ok(match self.wantarray_kind {
14142                WantarrayCtx::Void => StrykeValue::UNDEF,
14143                WantarrayCtx::Scalar => StrykeValue::integer(0),
14144                WantarrayCtx::List => StrykeValue::integer(1),
14145            }),
14146
14147            ExprKind::List(exprs) => {
14148                // In scalar context, the comma operator evaluates to the last element.
14149                if ctx == WantarrayCtx::Scalar {
14150                    if let Some(last) = exprs.last() {
14151                        // Evaluate earlier expressions for side effects
14152                        for e in &exprs[..exprs.len() - 1] {
14153                            self.eval_expr(e)?;
14154                        }
14155                        return self.eval_expr(last);
14156                    } else {
14157                        return Ok(StrykeValue::UNDEF);
14158                    }
14159                }
14160                let mut vals = Vec::new();
14161                for e in exprs {
14162                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
14163                    if let Some(items) = v.as_array_vec() {
14164                        vals.extend(items);
14165                    } else {
14166                        vals.push(v);
14167                    }
14168                }
14169                if vals.len() == 1 {
14170                    Ok(vals.pop().unwrap())
14171                } else {
14172                    Ok(StrykeValue::array(vals))
14173                }
14174            }
14175
14176            // Postfix modifiers
14177            ExprKind::PostfixIf { expr, condition } => {
14178                if self.eval_postfix_condition(condition)? {
14179                    self.eval_expr(expr)
14180                } else {
14181                    Ok(StrykeValue::UNDEF)
14182                }
14183            }
14184            ExprKind::PostfixUnless { expr, condition } => {
14185                if !self.eval_postfix_condition(condition)? {
14186                    self.eval_expr(expr)
14187                } else {
14188                    Ok(StrykeValue::UNDEF)
14189                }
14190            }
14191            ExprKind::PostfixWhile { expr, condition } => {
14192                // `do { ... } while (COND)` — body runs before the first condition check.
14193                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
14194                let is_do_block = matches!(
14195                    &expr.kind,
14196                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
14197                );
14198                let mut last = StrykeValue::UNDEF;
14199                if is_do_block {
14200                    loop {
14201                        last = self.eval_expr(expr)?;
14202                        if !self.eval_postfix_condition(condition)? {
14203                            break;
14204                        }
14205                    }
14206                } else {
14207                    loop {
14208                        if !self.eval_postfix_condition(condition)? {
14209                            break;
14210                        }
14211                        last = self.eval_expr(expr)?;
14212                    }
14213                }
14214                Ok(last)
14215            }
14216            ExprKind::PostfixUntil { expr, condition } => {
14217                let is_do_block = matches!(
14218                    &expr.kind,
14219                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
14220                );
14221                let mut last = StrykeValue::UNDEF;
14222                if is_do_block {
14223                    loop {
14224                        last = self.eval_expr(expr)?;
14225                        if self.eval_postfix_condition(condition)? {
14226                            break;
14227                        }
14228                    }
14229                } else {
14230                    loop {
14231                        if self.eval_postfix_condition(condition)? {
14232                            break;
14233                        }
14234                        last = self.eval_expr(expr)?;
14235                    }
14236                }
14237                Ok(last)
14238            }
14239            ExprKind::PostfixForeach { expr, list } => {
14240                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
14241                let mut last = StrykeValue::UNDEF;
14242                for item in items {
14243                    self.scope.set_topic(item);
14244                    last = self.eval_expr(expr)?;
14245                }
14246                Ok(last)
14247            }
14248        }
14249    }
14250
14251    // ── Helpers ──
14252
14253    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
14254        match op {
14255            BinOp::Add => Some("+"),
14256            BinOp::Sub => Some("-"),
14257            BinOp::Mul => Some("*"),
14258            BinOp::Div => Some("/"),
14259            BinOp::Mod => Some("%"),
14260            BinOp::Pow => Some("**"),
14261            BinOp::Concat => Some("."),
14262            BinOp::StrEq => Some("eq"),
14263            BinOp::NumEq => Some("=="),
14264            BinOp::StrNe => Some("ne"),
14265            BinOp::NumNe => Some("!="),
14266            BinOp::StrLt => Some("lt"),
14267            BinOp::StrGt => Some("gt"),
14268            BinOp::StrLe => Some("le"),
14269            BinOp::StrGe => Some("ge"),
14270            BinOp::NumLt => Some("<"),
14271            BinOp::NumGt => Some(">"),
14272            BinOp::NumLe => Some("<="),
14273            BinOp::NumGe => Some(">="),
14274            BinOp::Spaceship => Some("<=>"),
14275            BinOp::StrCmp => Some("cmp"),
14276            _ => None,
14277        }
14278    }
14279
14280    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
14281    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
14282        map.get("").or_else(|| map.get("\"\""))
14283    }
14284
14285    /// String context for blessed objects with `overload '""'`.
14286    pub(crate) fn stringify_value(
14287        &mut self,
14288        v: StrykeValue,
14289        line: usize,
14290    ) -> Result<String, FlowOrError> {
14291        if let Some(r) = self.try_overload_stringify(&v, line) {
14292            let pv = r?;
14293            return Ok(pv.to_string());
14294        }
14295        Ok(v.to_string())
14296    }
14297
14298    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
14299    pub(crate) fn perl_sprintf_stringify(
14300        &mut self,
14301        fmt: &str,
14302        args: &[StrykeValue],
14303        line: usize,
14304    ) -> Result<String, FlowOrError> {
14305        // Step 1: build the output and collect any `%n` store-targets.
14306        let (out, pending_n) = {
14307            let mut stringify = |v: &StrykeValue| -> Result<String, FlowOrError> {
14308                self.stringify_value(v.clone(), line)
14309            };
14310            perl_sprintf_format_full(fmt, args, &mut stringify)?
14311        };
14312        // Step 2: apply any `%n` writes through the proper scope path.
14313        for (target, count) in pending_n {
14314            self.assign_scalar_ref_deref(target, StrykeValue::integer(count), line)?;
14315        }
14316        Ok(out)
14317    }
14318
14319    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
14320    pub(crate) fn render_format_template(
14321        &mut self,
14322        tmpl: &crate::format::FormatTemplate,
14323        line: usize,
14324    ) -> Result<String, FlowOrError> {
14325        use crate::format::{FormatRecord, PictureSegment};
14326        let mut buf = String::new();
14327        for rec in &tmpl.records {
14328            match rec {
14329                FormatRecord::Literal(s) => {
14330                    buf.push_str(s);
14331                    buf.push('\n');
14332                }
14333                FormatRecord::Picture { segments, exprs } => {
14334                    let mut vals: Vec<String> = Vec::new();
14335                    for e in exprs {
14336                        let v = self.eval_expr(e)?;
14337                        vals.push(self.stringify_value(v, line)?);
14338                    }
14339                    let mut vi = 0usize;
14340                    let mut line_out = String::new();
14341                    for seg in segments {
14342                        match seg {
14343                            PictureSegment::Literal(t) => line_out.push_str(t),
14344                            PictureSegment::Field {
14345                                width,
14346                                align,
14347                                kind: _,
14348                            } => {
14349                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
14350                                vi += 1;
14351                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
14352                            }
14353                        }
14354                    }
14355                    buf.push_str(line_out.trim_end());
14356                    buf.push('\n');
14357                }
14358            }
14359        }
14360        Ok(buf)
14361    }
14362
14363    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
14364    pub(crate) fn resolve_write_output_handle(
14365        &self,
14366        v: &StrykeValue,
14367        line: usize,
14368    ) -> StrykeResult<String> {
14369        if let Some(n) = v.as_io_handle_name() {
14370            let n = self.resolve_io_handle_name(&n);
14371            if self.is_bound_handle(&n) {
14372                return Ok(n);
14373            }
14374        }
14375        if let Some(s) = v.as_str() {
14376            if self.is_bound_handle(&s) {
14377                return Ok(self.resolve_io_handle_name(&s));
14378            }
14379        }
14380        let s = v.to_string();
14381        if self.is_bound_handle(&s) {
14382            return Ok(self.resolve_io_handle_name(&s));
14383        }
14384        Err(StrykeError::runtime(
14385            format!("write: invalid or unopened filehandle {}", s),
14386            line,
14387        ))
14388    }
14389
14390    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
14391    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
14392    /// that handle like `write FH`.
14393    pub(crate) fn write_format_execute(
14394        &mut self,
14395        args: &[StrykeValue],
14396        line: usize,
14397    ) -> StrykeResult<StrykeValue> {
14398        let handle_name = match args.len() {
14399            0 => self.default_print_handle.clone(),
14400            1 => self.resolve_write_output_handle(&args[0], line)?,
14401            _ => {
14402                return Err(StrykeError::runtime("write: too many arguments", line));
14403            }
14404        };
14405        let pkg = self.current_package();
14406        let mut fmt_name = self.scope.get_scalar("~").to_string();
14407        if fmt_name.is_empty() {
14408            fmt_name = "STDOUT".to_string();
14409        }
14410        let key = format!("{}::{}", pkg, fmt_name);
14411        let tmpl = self
14412            .format_templates
14413            .get(&key)
14414            .map(Arc::clone)
14415            .ok_or_else(|| {
14416                StrykeError::runtime(
14417                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
14418                    line,
14419                )
14420            })?;
14421        let out = self
14422            .render_format_template(&tmpl, line)
14423            .map_err(|e| match e {
14424                FlowOrError::Error(e) => e,
14425                FlowOrError::Flow(_) => {
14426                    StrykeError::runtime("write: unexpected control flow", line)
14427                }
14428            })?;
14429        self.write_formatted_print(handle_name.as_str(), &out, line)?;
14430        Ok(StrykeValue::integer(1))
14431    }
14432
14433    pub(crate) fn try_overload_stringify(
14434        &mut self,
14435        v: &StrykeValue,
14436        line: usize,
14437    ) -> Option<ExecResult> {
14438        // Native class instance: look for method named '""' or 'stringify'
14439        if let Some(c) = v.as_class_inst() {
14440            let method_name = c
14441                .def
14442                .method("stringify")
14443                .or_else(|| c.def.method("\"\""))
14444                .filter(|m| m.body.is_some())?;
14445            let body = method_name.body.clone().unwrap();
14446            let params = method_name.params.clone();
14447            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
14448        }
14449        let br = v.as_blessed_ref()?;
14450        let class = br.class.clone();
14451        let map = self.overload_table.get(&class)?;
14452        let sub_short = Self::overload_stringify_method(map)?;
14453        let fq = format!("{}::{}", class, sub_short);
14454        let sub = self.subs.get(&fq)?.clone();
14455        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
14456    }
14457
14458    /// Map overload operator key to native class method name.
14459    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
14460        match key {
14461            "+" => Some("op_add"),
14462            "-" => Some("op_sub"),
14463            "*" => Some("op_mul"),
14464            "/" => Some("op_div"),
14465            "%" => Some("op_mod"),
14466            "**" => Some("op_pow"),
14467            "." => Some("op_concat"),
14468            "==" => Some("op_eq"),
14469            "!=" => Some("op_ne"),
14470            "<" => Some("op_lt"),
14471            ">" => Some("op_gt"),
14472            "<=" => Some("op_le"),
14473            ">=" => Some("op_ge"),
14474            "<=>" => Some("op_spaceship"),
14475            "eq" => Some("op_str_eq"),
14476            "ne" => Some("op_str_ne"),
14477            "lt" => Some("op_str_lt"),
14478            "gt" => Some("op_str_gt"),
14479            "le" => Some("op_str_le"),
14480            "ge" => Some("op_str_ge"),
14481            "cmp" => Some("op_cmp"),
14482            _ => None,
14483        }
14484    }
14485
14486    pub(crate) fn try_overload_binop(
14487        &mut self,
14488        op: BinOp,
14489        lv: &StrykeValue,
14490        rv: &StrykeValue,
14491        line: usize,
14492    ) -> Option<ExecResult> {
14493        let key = Self::overload_key_for_binop(op)?;
14494        // Native class instance overloading
14495        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
14496            (Some(c.def.clone()), lv.clone(), rv.clone())
14497        } else if let Some(c) = rv.as_class_inst() {
14498            (Some(c.def.clone()), rv.clone(), lv.clone())
14499        } else {
14500            (None, lv.clone(), rv.clone())
14501        };
14502        if let Some(ref def) = ci_def {
14503            if let Some(method_name) = Self::overload_method_name_for_key(key) {
14504                if let Some((m, _)) = self.find_class_method(def, method_name) {
14505                    if let Some(ref body) = m.body {
14506                        let params = m.params.clone();
14507                        return Some(self.call_class_method(
14508                            body,
14509                            &params,
14510                            vec![invocant, other],
14511                            line,
14512                        ));
14513                    }
14514                }
14515            }
14516        }
14517        // Blessed ref overloading (existing path)
14518        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
14519            (br.class.clone(), lv.clone(), rv.clone())
14520        } else if let Some(br) = rv.as_blessed_ref() {
14521            (br.class.clone(), rv.clone(), lv.clone())
14522        } else {
14523            return None;
14524        };
14525        let map = self.overload_table.get(&class)?;
14526        let sub_short = if let Some(s) = map.get(key) {
14527            s.clone()
14528        } else if let Some(nm) = map.get("nomethod") {
14529            let fq = format!("{}::{}", class, nm);
14530            let sub = self.subs.get(&fq)?.clone();
14531            return Some(self.call_sub(
14532                &sub,
14533                vec![invocant, other, StrykeValue::string(key.to_string())],
14534                WantarrayCtx::Scalar,
14535                line,
14536            ));
14537        } else {
14538            return None;
14539        };
14540        let fq = format!("{}::{}", class, sub_short);
14541        let sub = self.subs.get(&fq)?.clone();
14542        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
14543    }
14544
14545    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
14546    pub(crate) fn try_overload_unary_dispatch(
14547        &mut self,
14548        op_key: &str,
14549        val: &StrykeValue,
14550        line: usize,
14551    ) -> Option<ExecResult> {
14552        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
14553        if let Some(c) = val.as_class_inst() {
14554            let method_name = match op_key {
14555                "neg" => "op_neg",
14556                "bool" => "op_bool",
14557                "abs" => "op_abs",
14558                "0+" => "op_numify",
14559                _ => return None,
14560            };
14561            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
14562                if let Some(ref body) = m.body {
14563                    let params = m.params.clone();
14564                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
14565                }
14566            }
14567            return None;
14568        }
14569        // Blessed ref path
14570        let br = val.as_blessed_ref()?;
14571        let class = br.class.clone();
14572        let map = self.overload_table.get(&class)?;
14573        if let Some(s) = map.get(op_key) {
14574            let fq = format!("{}::{}", class, s);
14575            let sub = self.subs.get(&fq)?.clone();
14576            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
14577        }
14578        if let Some(nm) = map.get("nomethod") {
14579            let fq = format!("{}::{}", class, nm);
14580            let sub = self.subs.get(&fq)?.clone();
14581            return Some(self.call_sub(
14582                &sub,
14583                vec![val.clone(), StrykeValue::string(op_key.to_string())],
14584                WantarrayCtx::Scalar,
14585                line,
14586            ));
14587        }
14588        None
14589    }
14590
14591    #[inline]
14592    fn eval_binop(
14593        &mut self,
14594        op: BinOp,
14595        lv: &StrykeValue,
14596        rv: &StrykeValue,
14597        _line: usize,
14598    ) -> ExecResult {
14599        Ok(match op {
14600            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
14601            // Perl `+` is numeric addition only; string concatenation is `.`.
14602            BinOp::Add => {
14603                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14604                    StrykeValue::integer(a.wrapping_add(b))
14605                } else {
14606                    StrykeValue::float(lv.to_number() + rv.to_number())
14607                }
14608            }
14609            BinOp::Sub => {
14610                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14611                    StrykeValue::integer(a.wrapping_sub(b))
14612                } else {
14613                    StrykeValue::float(lv.to_number() - rv.to_number())
14614                }
14615            }
14616            BinOp::Mul => {
14617                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14618                    StrykeValue::integer(a.wrapping_mul(b))
14619                } else {
14620                    StrykeValue::float(lv.to_number() * rv.to_number())
14621                }
14622            }
14623            BinOp::Div => {
14624                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14625                    if b == 0 {
14626                        return Err(StrykeError::division_by_zero(
14627                            "Illegal division by zero",
14628                            _line,
14629                        )
14630                        .into());
14631                    }
14632                    if a % b == 0 {
14633                        StrykeValue::integer(a / b)
14634                    } else {
14635                        StrykeValue::float(a as f64 / b as f64)
14636                    }
14637                } else {
14638                    let d = rv.to_number();
14639                    if d == 0.0 {
14640                        return Err(StrykeError::division_by_zero(
14641                            "Illegal division by zero",
14642                            _line,
14643                        )
14644                        .into());
14645                    }
14646                    StrykeValue::float(lv.to_number() / d)
14647                }
14648            }
14649            BinOp::Mod => {
14650                let d = rv.to_int();
14651                if d == 0 {
14652                    return Err(StrykeError::division_by_zero("Illegal modulus zero", _line).into());
14653                }
14654                StrykeValue::integer(crate::value::perl_mod_i64(lv.to_int(), d))
14655            }
14656            BinOp::Pow => {
14657                // Under `--compat` or `use bigint;`, `compat_pow` promotes
14658                // to `BigInt` on overflow; otherwise it falls back to f64
14659                // (matches Perl's default i64-overflow-to-NV behavior).
14660                if crate::compat_mode() || crate::bigint_pragma() {
14661                    crate::value::compat_pow(lv, rv)
14662                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14663                    let int_pow = (b >= 0)
14664                        .then(|| u32::try_from(b).ok())
14665                        .flatten()
14666                        .and_then(|bu| a.checked_pow(bu))
14667                        .map(StrykeValue::integer);
14668                    int_pow
14669                        .unwrap_or_else(|| StrykeValue::float(lv.to_number().powf(rv.to_number())))
14670                } else {
14671                    StrykeValue::float(lv.to_number().powf(rv.to_number()))
14672                }
14673            }
14674            BinOp::Concat => {
14675                let mut s = String::new();
14676                lv.append_to(&mut s);
14677                rv.append_to(&mut s);
14678                StrykeValue::string(s)
14679            }
14680            BinOp::NumEq => {
14681                // Struct equality: compare all fields
14682                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
14683                    if a.def.name != b.def.name {
14684                        StrykeValue::integer(0)
14685                    } else {
14686                        let av = a.get_values();
14687                        let bv = b.get_values();
14688                        let eq = av.len() == bv.len()
14689                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
14690                        StrykeValue::integer(if eq { 1 } else { 0 })
14691                    }
14692                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14693                    StrykeValue::integer(if a == b { 1 } else { 0 })
14694                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
14695                    // Stryke (non-compat) sugar: `==` falls back to string
14696                    // compare when both operands are non-numeric strings, so
14697                    // `"G" == "G"` is true (Perl's `0 == 0` numeric is also
14698                    // true here, but `"G" == "T"` is false in stryke vs
14699                    // also-true in Perl). See `Op::NumEq` in vm.rs.
14700                    StrykeValue::integer(if lv.to_string() == rv.to_string() {
14701                        1
14702                    } else {
14703                        0
14704                    })
14705                } else {
14706                    StrykeValue::integer(if lv.to_number() == rv.to_number() {
14707                        1
14708                    } else {
14709                        0
14710                    })
14711                }
14712            }
14713            BinOp::NumNe => {
14714                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14715                    StrykeValue::integer(if a != b { 1 } else { 0 })
14716                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
14717                    StrykeValue::integer(if lv.to_string() != rv.to_string() {
14718                        1
14719                    } else {
14720                        0
14721                    })
14722                } else {
14723                    StrykeValue::integer(if lv.to_number() != rv.to_number() {
14724                        1
14725                    } else {
14726                        0
14727                    })
14728                }
14729            }
14730            BinOp::NumLt => {
14731                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14732                    StrykeValue::integer(if a < b { 1 } else { 0 })
14733                } else {
14734                    StrykeValue::integer(if lv.to_number() < rv.to_number() {
14735                        1
14736                    } else {
14737                        0
14738                    })
14739                }
14740            }
14741            BinOp::NumGt => {
14742                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14743                    StrykeValue::integer(if a > b { 1 } else { 0 })
14744                } else {
14745                    StrykeValue::integer(if lv.to_number() > rv.to_number() {
14746                        1
14747                    } else {
14748                        0
14749                    })
14750                }
14751            }
14752            BinOp::NumLe => {
14753                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14754                    StrykeValue::integer(if a <= b { 1 } else { 0 })
14755                } else {
14756                    StrykeValue::integer(if lv.to_number() <= rv.to_number() {
14757                        1
14758                    } else {
14759                        0
14760                    })
14761                }
14762            }
14763            BinOp::NumGe => {
14764                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14765                    StrykeValue::integer(if a >= b { 1 } else { 0 })
14766                } else {
14767                    StrykeValue::integer(if lv.to_number() >= rv.to_number() {
14768                        1
14769                    } else {
14770                        0
14771                    })
14772                }
14773            }
14774            BinOp::Spaceship => {
14775                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14776                    StrykeValue::integer(if a < b {
14777                        -1
14778                    } else if a > b {
14779                        1
14780                    } else {
14781                        0
14782                    })
14783                } else {
14784                    let a = lv.to_number();
14785                    let b = rv.to_number();
14786                    StrykeValue::integer(if a < b {
14787                        -1
14788                    } else if a > b {
14789                        1
14790                    } else {
14791                        0
14792                    })
14793                }
14794            }
14795            BinOp::StrEq => StrykeValue::integer(if lv.to_string() == rv.to_string() {
14796                1
14797            } else {
14798                0
14799            }),
14800            BinOp::StrNe => StrykeValue::integer(if lv.to_string() != rv.to_string() {
14801                1
14802            } else {
14803                0
14804            }),
14805            BinOp::StrLt => StrykeValue::integer(if lv.to_string() < rv.to_string() {
14806                1
14807            } else {
14808                0
14809            }),
14810            BinOp::StrGt => StrykeValue::integer(if lv.to_string() > rv.to_string() {
14811                1
14812            } else {
14813                0
14814            }),
14815            BinOp::StrLe => StrykeValue::integer(if lv.to_string() <= rv.to_string() {
14816                1
14817            } else {
14818                0
14819            }),
14820            BinOp::StrGe => StrykeValue::integer(if lv.to_string() >= rv.to_string() {
14821                1
14822            } else {
14823                0
14824            }),
14825            BinOp::StrCmp => {
14826                let cmp = lv.to_string().cmp(&rv.to_string());
14827                StrykeValue::integer(match cmp {
14828                    std::cmp::Ordering::Less => -1,
14829                    std::cmp::Ordering::Greater => 1,
14830                    std::cmp::Ordering::Equal => 0,
14831                })
14832            }
14833            BinOp::BitAnd => {
14834                if let Some(s) = crate::value::set_intersection(lv, rv) {
14835                    s
14836                } else {
14837                    StrykeValue::integer(lv.to_int() & rv.to_int())
14838                }
14839            }
14840            BinOp::BitOr => {
14841                if let Some(s) = crate::value::set_union(lv, rv) {
14842                    s
14843                } else {
14844                    StrykeValue::integer(lv.to_int() | rv.to_int())
14845                }
14846            }
14847            BinOp::BitXor => StrykeValue::integer(lv.to_int() ^ rv.to_int()),
14848            BinOp::ShiftLeft => StrykeValue::integer(perl_shl_i64(lv.to_int(), rv.to_int())),
14849            BinOp::ShiftRight => StrykeValue::integer(perl_shr_i64(lv.to_int(), rv.to_int())),
14850            // These should have been handled by short-circuit above
14851            BinOp::LogAnd
14852            | BinOp::LogOr
14853            | BinOp::DefinedOr
14854            | BinOp::LogAndWord
14855            | BinOp::LogOrWord => unreachable!(),
14856            BinOp::BindMatch | BinOp::BindNotMatch => {
14857                unreachable!("regex bind handled in eval_expr BinOp arm")
14858            }
14859        })
14860    }
14861
14862    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
14863    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
14864    /// length — that was silently wrong vs `perl`.
14865    fn err_modify_symbolic_aggregate_deref_inc_dec(
14866        kind: Sigil,
14867        is_pre: bool,
14868        is_inc: bool,
14869        line: usize,
14870    ) -> FlowOrError {
14871        let agg = match kind {
14872            Sigil::Array => "array",
14873            Sigil::Hash => "hash",
14874            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
14875        };
14876        let op = match (is_pre, is_inc) {
14877            (true, true) => "preincrement (++)",
14878            (true, false) => "predecrement (--)",
14879            (false, true) => "postincrement (++)",
14880            (false, false) => "postdecrement (--)",
14881        };
14882        FlowOrError::Error(StrykeError::runtime(
14883            format!("Can't modify {agg} dereference in {op}"),
14884            line,
14885        ))
14886    }
14887
14888    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
14889    pub(crate) fn symbolic_scalar_ref_postfix(
14890        &mut self,
14891        ref_val: StrykeValue,
14892        decrement: bool,
14893        line: usize,
14894    ) -> Result<StrykeValue, FlowOrError> {
14895        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
14896        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14897        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
14898        Ok(old)
14899    }
14900
14901    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
14902    /// [`Self::assign_value`] and the VM.
14903    pub(crate) fn assign_scalar_ref_deref(
14904        &mut self,
14905        ref_val: StrykeValue,
14906        val: StrykeValue,
14907        line: usize,
14908    ) -> ExecResult {
14909        if let Some(name) = ref_val.as_scalar_binding_name() {
14910            self.set_special_var(&name, &val)
14911                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14912            return Ok(StrykeValue::UNDEF);
14913        }
14914        if let Some(r) = ref_val.as_scalar_ref() {
14915            *r.write() = val;
14916            return Ok(StrykeValue::UNDEF);
14917        }
14918        // Plain primitive scalar value: under no-strict, perl symbolic-derefs
14919        // through the string. With `strict 'refs'`, emit perl's exact diagnostic.
14920        if ref_val.is_integer_like() || ref_val.is_float_like() || ref_val.is_string_like() {
14921            let s = ref_val.to_string();
14922            if self.strict_refs {
14923                return Err(StrykeError::runtime(
14924                    format!(
14925                        "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
14926                        s
14927                    ),
14928                    line,
14929                )
14930                .into());
14931            }
14932            self.set_special_var(&s, &val)
14933                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14934            return Ok(StrykeValue::UNDEF);
14935        }
14936        Err(StrykeError::runtime("Can't assign to non-scalar reference", line).into())
14937    }
14938
14939    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
14940    pub(crate) fn assign_symbolic_array_ref_deref(
14941        &mut self,
14942        ref_val: StrykeValue,
14943        val: StrykeValue,
14944        line: usize,
14945    ) -> ExecResult {
14946        if let Some(a) = ref_val.as_array_ref() {
14947            *a.write() = val.to_list();
14948            return Ok(StrykeValue::UNDEF);
14949        }
14950        if let Some(name) = ref_val.as_array_binding_name() {
14951            self.scope
14952                .set_array(&name, val.to_list())
14953                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14954            return Ok(StrykeValue::UNDEF);
14955        }
14956        if let Some(s) = ref_val.as_str() {
14957            if self.strict_refs {
14958                return Err(StrykeError::runtime(
14959                    format!(
14960                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
14961                        s
14962                    ),
14963                    line,
14964                )
14965                .into());
14966            }
14967            self.scope
14968                .set_array(&s, val.to_list())
14969                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14970            return Ok(StrykeValue::UNDEF);
14971        }
14972        Err(StrykeError::runtime("Can't assign to non-array reference", line).into())
14973    }
14974
14975    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
14976    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
14977    pub(crate) fn assign_symbolic_typeglob_ref_deref(
14978        &mut self,
14979        ref_val: StrykeValue,
14980        val: StrykeValue,
14981        line: usize,
14982    ) -> ExecResult {
14983        let lhs_name = if let Some(s) = ref_val.as_str() {
14984            if self.strict_refs {
14985                return Err(StrykeError::runtime(
14986                    format!(
14987                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
14988                        s
14989                    ),
14990                    line,
14991                )
14992                .into());
14993            }
14994            s.to_string()
14995        } else {
14996            return Err(
14997                StrykeError::runtime("Can't assign to non-glob symbolic reference", line).into(),
14998            );
14999        };
15000        let is_coderef = val.as_code_ref().is_some()
15001            || val
15002                .as_scalar_ref()
15003                .map(|r| r.read().as_code_ref().is_some())
15004                .unwrap_or(false);
15005        if is_coderef {
15006            return self.assign_typeglob_value(&lhs_name, val, line);
15007        }
15008        let rhs_key = val.to_string();
15009        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
15010            .map_err(FlowOrError::Error)?;
15011        Ok(StrykeValue::UNDEF)
15012    }
15013
15014    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
15015    pub(crate) fn assign_symbolic_hash_ref_deref(
15016        &mut self,
15017        ref_val: StrykeValue,
15018        val: StrykeValue,
15019        line: usize,
15020    ) -> ExecResult {
15021        let items = val.to_list();
15022        let mut map = IndexMap::new();
15023        let mut i = 0;
15024        while i + 1 < items.len() {
15025            map.insert(items[i].to_string(), items[i + 1].clone());
15026            i += 2;
15027        }
15028        if let Some(h) = ref_val.as_hash_ref() {
15029            *h.write() = map;
15030            return Ok(StrykeValue::UNDEF);
15031        }
15032        if let Some(name) = ref_val.as_hash_binding_name() {
15033            self.touch_env_hash(&name);
15034            self.scope
15035                .set_hash(&name, map)
15036                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15037            return Ok(StrykeValue::UNDEF);
15038        }
15039        if let Some(s) = ref_val.as_str() {
15040            if self.strict_refs {
15041                return Err(StrykeError::runtime(
15042                    format!(
15043                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
15044                        s
15045                    ),
15046                    line,
15047                )
15048                .into());
15049            }
15050            self.touch_env_hash(&s);
15051            self.scope
15052                .set_hash(&s, map)
15053                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15054            return Ok(StrykeValue::UNDEF);
15055        }
15056        Err(StrykeError::runtime("Can't assign to non-hash reference", line).into())
15057    }
15058
15059    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
15060    pub(crate) fn assign_arrow_hash_deref(
15061        &mut self,
15062        container: StrykeValue,
15063        key: String,
15064        val: StrykeValue,
15065        line: usize,
15066    ) -> ExecResult {
15067        if let Some(b) = container.as_blessed_ref() {
15068            let mut data = b.data.write();
15069            if let Some(r) = data.as_hash_ref() {
15070                r.write().insert(key, val);
15071                return Ok(StrykeValue::UNDEF);
15072            }
15073            if let Some(mut map) = data.as_hash_map() {
15074                map.insert(key, val);
15075                *data = StrykeValue::hash(map);
15076                return Ok(StrykeValue::UNDEF);
15077            }
15078            return Err(
15079                StrykeError::runtime("Can't assign into non-hash blessed ref", line).into(),
15080            );
15081        }
15082        if let Some(r) = container.as_hash_ref() {
15083            r.write().insert(key, val);
15084            return Ok(StrykeValue::UNDEF);
15085        }
15086        if let Some(name) = container.as_hash_binding_name() {
15087            self.touch_env_hash(&name);
15088            self.scope
15089                .set_hash_element(&name, &key, val)
15090                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15091            return Ok(StrykeValue::UNDEF);
15092        }
15093        Err(StrykeError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
15094    }
15095
15096    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
15097    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
15098    pub(crate) fn eval_arrow_array_base(
15099        &mut self,
15100        expr: &Expr,
15101        _line: usize,
15102    ) -> Result<StrykeValue, FlowOrError> {
15103        match &expr.kind {
15104            ExprKind::Deref {
15105                expr: inner,
15106                kind: Sigil::Array | Sigil::Scalar,
15107            } => self.eval_expr(inner),
15108            _ => self.eval_expr(expr),
15109        }
15110    }
15111
15112    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
15113    pub(crate) fn eval_arrow_hash_base(
15114        &mut self,
15115        expr: &Expr,
15116        _line: usize,
15117    ) -> Result<StrykeValue, FlowOrError> {
15118        match &expr.kind {
15119            ExprKind::Deref {
15120                expr: inner,
15121                kind: Sigil::Scalar,
15122            } => self.eval_expr(inner),
15123            _ => self.eval_expr(expr),
15124        }
15125    }
15126
15127    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
15128    pub(crate) fn read_arrow_array_element(
15129        &self,
15130        container: StrykeValue,
15131        idx: i64,
15132        line: usize,
15133    ) -> Result<StrykeValue, FlowOrError> {
15134        if let Some(a) = container.as_array_ref() {
15135            let arr = a.read();
15136            let i = if idx < 0 {
15137                (arr.len() as i64 + idx) as usize
15138            } else {
15139                idx as usize
15140            };
15141            return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
15142        }
15143        if let Some(name) = container.as_array_binding_name() {
15144            return Ok(self.scope.get_array_element(&name, idx));
15145        }
15146        if let Some(arr) = container.as_array_vec() {
15147            let i = if idx < 0 {
15148                (arr.len() as i64 + idx) as usize
15149            } else {
15150                idx as usize
15151            };
15152            return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
15153        }
15154        // Blessed arrayref (e.g. `Pair`) — `pairs` returns blessed `Pair` objects that
15155        // can be indexed via `$_->[0]` / `$_->[1]`.
15156        if let Some(b) = container.as_blessed_ref() {
15157            let inner = b.data.read().clone();
15158            if let Some(a) = inner.as_array_ref() {
15159                let arr = a.read();
15160                let i = if idx < 0 {
15161                    (arr.len() as i64 + idx) as usize
15162                } else {
15163                    idx as usize
15164                };
15165                return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
15166            }
15167        }
15168        Err(StrykeError::runtime("Can't use arrow deref on non-array-ref", line).into())
15169    }
15170
15171    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
15172    pub(crate) fn read_arrow_hash_element(
15173        &mut self,
15174        container: StrykeValue,
15175        key: &str,
15176        line: usize,
15177    ) -> Result<StrykeValue, FlowOrError> {
15178        if let Some(r) = container.as_hash_ref() {
15179            let h = r.read();
15180            return Ok(h.get(key).cloned().unwrap_or(StrykeValue::UNDEF));
15181        }
15182        if let Some(name) = container.as_hash_binding_name() {
15183            self.touch_env_hash(&name);
15184            return Ok(self.scope.get_hash_element(&name, key));
15185        }
15186        if let Some(b) = container.as_blessed_ref() {
15187            let data = b.data.read();
15188            if let Some(v) = data.hash_get(key) {
15189                return Ok(v);
15190            }
15191            if let Some(r) = data.as_hash_ref() {
15192                let h = r.read();
15193                return Ok(h.get(key).cloned().unwrap_or(StrykeValue::UNDEF));
15194            }
15195            return Err(StrykeError::runtime(
15196                "Can't access hash field on non-hash blessed ref",
15197                line,
15198            )
15199            .into());
15200        }
15201        // Struct field access via hash deref syntax: $struct->{field}
15202        if let Some(s) = container.as_struct_inst() {
15203            if let Some(idx) = s.def.field_index(key) {
15204                return Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF));
15205            }
15206            return Err(StrykeError::runtime(
15207                format!("struct {} has no field `{}`", s.def.name, key),
15208                line,
15209            )
15210            .into());
15211        }
15212        // Class instance field access via hash deref: $obj->{field}
15213        if let Some(c) = container.as_class_inst() {
15214            if let Some(idx) = c.def.field_index(key) {
15215                return Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF));
15216            }
15217            return Err(StrykeError::runtime(
15218                format!("class {} has no field `{}`", c.def.name, key),
15219                line,
15220            )
15221            .into());
15222        }
15223        Err(StrykeError::runtime("Can't use arrow deref on non-hash-ref", line).into())
15224    }
15225
15226    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
15227    pub(crate) fn arrow_array_postfix(
15228        &mut self,
15229        container: StrykeValue,
15230        idx: i64,
15231        decrement: bool,
15232        line: usize,
15233    ) -> Result<StrykeValue, FlowOrError> {
15234        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
15235        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
15236        self.assign_arrow_array_deref(container, idx, new_val, line)?;
15237        Ok(old)
15238    }
15239
15240    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
15241    pub(crate) fn arrow_hash_postfix(
15242        &mut self,
15243        container: StrykeValue,
15244        key: String,
15245        decrement: bool,
15246        line: usize,
15247    ) -> Result<StrykeValue, FlowOrError> {
15248        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
15249        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
15250        self.assign_arrow_hash_deref(container, key, new_val, line)?;
15251        Ok(old)
15252    }
15253
15254    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` evaluation. If a nullary
15255    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
15256    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
15257    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
15258    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
15259    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
15260    /// subs" in use`).
15261    pub(crate) fn resolve_bareword_rvalue(
15262        &mut self,
15263        name: &str,
15264        want: WantarrayCtx,
15265        line: usize,
15266    ) -> Result<StrykeValue, FlowOrError> {
15267        if name == "__PACKAGE__" {
15268            return Ok(StrykeValue::string(self.current_package()));
15269        }
15270        if let Some(sub) = self.resolve_sub_by_name(name) {
15271            return self.call_sub(&sub, vec![], want, line);
15272        }
15273        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
15274        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
15275            return r.map_err(Into::into);
15276        }
15277        Ok(StrykeValue::string(name.to_string()))
15278    }
15279
15280    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
15281    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
15282    /// compound / inc-dec / assign helpers below.
15283    pub(crate) fn arrow_array_slice_values(
15284        &mut self,
15285        container: StrykeValue,
15286        indices: &[i64],
15287        line: usize,
15288    ) -> Result<StrykeValue, FlowOrError> {
15289        let mut out = Vec::with_capacity(indices.len());
15290        for &idx in indices {
15291            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
15292            out.push(v);
15293        }
15294        Ok(StrykeValue::array(out))
15295    }
15296
15297    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment for
15298    /// multi-index `ArrowDeref { Array, List }`. Shared by the VM
15299    /// [`crate::bytecode::Op::SetArrowArraySlice`].
15300    pub(crate) fn assign_arrow_array_slice(
15301        &mut self,
15302        container: StrykeValue,
15303        indices: Vec<i64>,
15304        val: StrykeValue,
15305        line: usize,
15306    ) -> Result<StrykeValue, FlowOrError> {
15307        if indices.is_empty() {
15308            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15309        }
15310        let vals = val.to_list();
15311        for (i, idx) in indices.iter().enumerate() {
15312            let v = vals.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
15313            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
15314        }
15315        Ok(StrykeValue::UNDEF)
15316    }
15317
15318    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
15319    pub(crate) fn flatten_array_slice_index_specs(
15320        &mut self,
15321        indices: &[Expr],
15322    ) -> Result<Vec<i64>, FlowOrError> {
15323        let mut out = Vec::new();
15324        for idx_expr in indices {
15325            let v = if matches!(
15326                idx_expr.kind,
15327                ExprKind::Range { .. } | ExprKind::SliceRange { .. }
15328            ) {
15329                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
15330            } else {
15331                self.eval_expr(idx_expr)?
15332            };
15333            if let Some(list) = v.as_array_vec() {
15334                for idx in list {
15335                    out.push(idx.to_int());
15336                }
15337            } else {
15338                out.push(v.to_int());
15339            }
15340        }
15341        Ok(out)
15342    }
15343
15344    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
15345    pub(crate) fn assign_named_array_slice(
15346        &mut self,
15347        stash_array_name: &str,
15348        indices: Vec<i64>,
15349        val: StrykeValue,
15350        line: usize,
15351    ) -> Result<StrykeValue, FlowOrError> {
15352        if indices.is_empty() {
15353            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15354        }
15355        let vals = val.to_list();
15356        for (i, idx) in indices.iter().enumerate() {
15357            let v = vals.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
15358            self.scope
15359                .set_array_element(stash_array_name, *idx, v)
15360                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15361        }
15362        Ok(StrykeValue::UNDEF)
15363    }
15364
15365    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
15366    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
15367    pub(crate) fn compound_assign_arrow_array_slice(
15368        &mut self,
15369        container: StrykeValue,
15370        indices: Vec<i64>,
15371        op: BinOp,
15372        rhs: StrykeValue,
15373        line: usize,
15374    ) -> Result<StrykeValue, FlowOrError> {
15375        if indices.is_empty() {
15376            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15377        }
15378        let last_idx = *indices.last().expect("non-empty indices");
15379        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
15380        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
15381        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
15382        Ok(new_val)
15383    }
15384
15385    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
15386    /// pre forms return the new value, post forms return the old **last** element.
15387    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
15388    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
15389    pub(crate) fn arrow_array_slice_inc_dec(
15390        &mut self,
15391        container: StrykeValue,
15392        indices: Vec<i64>,
15393        kind: u8,
15394        line: usize,
15395    ) -> Result<StrykeValue, FlowOrError> {
15396        if indices.is_empty() {
15397            return Err(StrykeError::runtime(
15398                "array slice increment needs at least one index",
15399                line,
15400            )
15401            .into());
15402        }
15403        let last_idx = *indices.last().expect("non-empty indices");
15404        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
15405        let new_val = if kind & 1 == 0 {
15406            StrykeValue::integer(last_old.to_int() + 1)
15407        } else {
15408            StrykeValue::integer(last_old.to_int() - 1)
15409        };
15410        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
15411        Ok(if kind < 2 { new_val } else { last_old })
15412    }
15413
15414    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
15415    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
15416    pub(crate) fn named_array_slice_inc_dec(
15417        &mut self,
15418        stash_array_name: &str,
15419        indices: Vec<i64>,
15420        kind: u8,
15421        line: usize,
15422    ) -> Result<StrykeValue, FlowOrError> {
15423        let last_idx = *indices.last().ok_or_else(|| {
15424            StrykeError::runtime("array slice increment needs at least one index", line)
15425        })?;
15426        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
15427        let new_val = if kind & 1 == 0 {
15428            StrykeValue::integer(last_old.to_int() + 1)
15429        } else {
15430            StrykeValue::integer(last_old.to_int() - 1)
15431        };
15432        self.scope
15433            .set_array_element(stash_array_name, last_idx, new_val.clone())
15434            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15435        Ok(if kind < 2 { new_val } else { last_old })
15436    }
15437
15438    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
15439    pub(crate) fn compound_assign_named_array_slice(
15440        &mut self,
15441        stash_array_name: &str,
15442        indices: Vec<i64>,
15443        op: BinOp,
15444        rhs: StrykeValue,
15445        line: usize,
15446    ) -> Result<StrykeValue, FlowOrError> {
15447        if indices.is_empty() {
15448            return Err(StrykeError::runtime("assign to empty array slice", line).into());
15449        }
15450        let last_idx = *indices.last().expect("non-empty indices");
15451        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
15452        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
15453        self.scope
15454            .set_array_element(stash_array_name, last_idx, new_val.clone())
15455            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15456        Ok(new_val)
15457    }
15458
15459    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
15460    pub(crate) fn assign_arrow_array_deref(
15461        &mut self,
15462        container: StrykeValue,
15463        idx: i64,
15464        val: StrykeValue,
15465        line: usize,
15466    ) -> ExecResult {
15467        if let Some(a) = container.as_array_ref() {
15468            let mut arr = a.write();
15469            let i = if idx < 0 {
15470                (arr.len() as i64 + idx) as usize
15471            } else {
15472                idx as usize
15473            };
15474            if i >= arr.len() {
15475                arr.resize(i + 1, StrykeValue::UNDEF);
15476            }
15477            arr[i] = val;
15478            return Ok(StrykeValue::UNDEF);
15479        }
15480        if let Some(name) = container.as_array_binding_name() {
15481            self.scope
15482                .set_array_element(&name, idx, val)
15483                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15484            return Ok(StrykeValue::UNDEF);
15485        }
15486        Err(StrykeError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
15487    }
15488
15489    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
15490    pub(crate) fn assign_typeglob_value(
15491        &mut self,
15492        name: &str,
15493        val: StrykeValue,
15494        line: usize,
15495    ) -> ExecResult {
15496        let sub = if let Some(c) = val.as_code_ref() {
15497            Some(c)
15498        } else if let Some(r) = val.as_scalar_ref() {
15499            r.read().as_code_ref().map(|c| Arc::clone(&c))
15500        } else {
15501            None
15502        };
15503        if let Some(sub) = sub {
15504            let lhs_sub = self.qualify_typeglob_sub_key(name);
15505            self.subs.insert(lhs_sub, sub);
15506            return Ok(StrykeValue::UNDEF);
15507        }
15508        Err(StrykeError::runtime(
15509            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
15510            line,
15511        )
15512        .into())
15513    }
15514
15515    fn assign_value(&mut self, target: &Expr, val: StrykeValue) -> ExecResult {
15516        match &target.kind {
15517            // `substr($s, $o, $l) = $rhs` — equivalent to the 4-arg form
15518            // `substr($s, $o, $l, $rhs)`. Evaluate the offset/length
15519            // sub-exprs, splice the new substring into the target, then
15520            // recursively call assign_value to write the modified string
15521            // through the original `string` lvalue.
15522            ExprKind::Substr {
15523                string,
15524                offset,
15525                length,
15526                replacement: None,
15527            } => {
15528                let s = self.eval_expr(string)?.to_string();
15529                let off = self.eval_expr(offset)?.to_int();
15530                let start = if off < 0 {
15531                    (s.len() as i64 + off).max(0) as usize
15532                } else {
15533                    (off as usize).min(s.len())
15534                };
15535                let len = if let Some(l) = length {
15536                    let lv = self.eval_expr(l)?.to_int();
15537                    if lv < 0 {
15538                        let remaining = s.len().saturating_sub(start) as i64;
15539                        (remaining + lv).max(0) as usize
15540                    } else {
15541                        lv as usize
15542                    }
15543                } else {
15544                    s.len().saturating_sub(start)
15545                };
15546                let end = start.saturating_add(len).min(s.len());
15547                let mut new_s = String::with_capacity(s.len());
15548                new_s.push_str(&s[..start]);
15549                new_s.push_str(&val.to_string());
15550                new_s.push_str(&s[end..]);
15551                self.assign_value(string, StrykeValue::string(new_s))?;
15552                Ok(StrykeValue::UNDEF)
15553            }
15554            // `(my $copy = $orig) =~ s/.../.../` — at this point the
15555            // MyExpr has already been evaluated as a side-effect of
15556            // `eval_expr(target)` upstream (so `$copy` has been declared
15557            // and initialized). The substitution / transliteration helpers
15558            // call back here to write the *new* string. Bind through the
15559            // declared name without re-running the initializer.
15560            ExprKind::MyExpr { decls, .. } => {
15561                let first = decls.first().ok_or_else(|| {
15562                    FlowOrError::Error(StrykeError::runtime(
15563                        "assign_value: empty MyExpr decl list",
15564                        target.line,
15565                    ))
15566                })?;
15567                match first.sigil {
15568                    Sigil::Scalar => {
15569                        let stor = self.tree_scalar_storage_name(&first.name);
15570                        self.set_special_var(&stor, &val)
15571                            .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
15572                        Ok(StrykeValue::UNDEF)
15573                    }
15574                    Sigil::Array => {
15575                        self.scope.set_array(&first.name, val.to_list())?;
15576                        Ok(StrykeValue::UNDEF)
15577                    }
15578                    Sigil::Hash => {
15579                        let items = val.to_list();
15580                        let mut map = IndexMap::new();
15581                        let mut i = 0;
15582                        while i + 1 < items.len() {
15583                            map.insert(items[i].to_string(), items[i + 1].clone());
15584                            i += 2;
15585                        }
15586                        self.scope.set_hash(&first.name, map)?;
15587                        Ok(StrykeValue::UNDEF)
15588                    }
15589                    Sigil::Typeglob => Ok(StrykeValue::UNDEF),
15590                }
15591            }
15592            ExprKind::ScalarVar(name) => {
15593                let stor = self.tree_scalar_storage_name(name);
15594                if self.scope.is_scalar_frozen(&stor) {
15595                    return Err(FlowOrError::Error(StrykeError::runtime(
15596                        format!("Modification of a frozen value: ${}", name),
15597                        target.line,
15598                    )));
15599                }
15600                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
15601                    let class = obj
15602                        .as_blessed_ref()
15603                        .map(|b| b.class.clone())
15604                        .unwrap_or_default();
15605                    let full = format!("{}::STORE", class);
15606                    if let Some(sub) = self.subs.get(&full).cloned() {
15607                        let arg_vals = vec![obj, val];
15608                        return match self.call_sub(
15609                            &sub,
15610                            arg_vals,
15611                            WantarrayCtx::Scalar,
15612                            target.line,
15613                        ) {
15614                            Ok(_) => Ok(StrykeValue::UNDEF),
15615                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15616                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15617                        };
15618                    }
15619                }
15620                self.set_special_var(&stor, &val)
15621                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
15622                Ok(StrykeValue::UNDEF)
15623            }
15624            ExprKind::ArrayVar(name) => {
15625                if self.scope.is_array_frozen(name) {
15626                    return Err(StrykeError::runtime(
15627                        format!("Modification of a frozen value: @{}", name),
15628                        target.line,
15629                    )
15630                    .into());
15631                }
15632                if self.strict_vars
15633                    && !name.contains("::")
15634                    && !self.scope.array_binding_exists(name)
15635                {
15636                    return Err(StrykeError::runtime(
15637                        format!(
15638                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
15639                            name, name
15640                        ),
15641                        target.line,
15642                    )
15643                    .into());
15644                }
15645                self.scope.set_array(name, val.to_list())?;
15646                Ok(StrykeValue::UNDEF)
15647            }
15648            ExprKind::HashVar(name) => {
15649                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
15650                {
15651                    return Err(StrykeError::runtime(
15652                        format!(
15653                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15654                            name, name
15655                        ),
15656                        target.line,
15657                    )
15658                    .into());
15659                }
15660                let items = val.to_list();
15661                let mut map = IndexMap::new();
15662                let mut i = 0;
15663                while i + 1 < items.len() {
15664                    map.insert(items[i].to_string(), items[i + 1].clone());
15665                    i += 2;
15666                }
15667                self.scope.set_hash(name, map)?;
15668                Ok(StrykeValue::UNDEF)
15669            }
15670            ExprKind::ArrayElement { array, index } => {
15671                if self.strict_vars
15672                    && !array.contains("::")
15673                    && !self.scope.array_binding_exists(array)
15674                {
15675                    return Err(StrykeError::runtime(
15676                        format!(
15677                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
15678                            array, array
15679                        ),
15680                        target.line,
15681                    )
15682                    .into());
15683                }
15684                if self.scope.is_array_frozen(array) {
15685                    return Err(StrykeError::runtime(
15686                        format!("Modification of a frozen value: @{}", array),
15687                        target.line,
15688                    )
15689                    .into());
15690                }
15691                let idx = self.eval_expr(index)?.to_int();
15692                let aname = self.stash_array_name_for_package(array);
15693                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
15694                    let class = obj
15695                        .as_blessed_ref()
15696                        .map(|b| b.class.clone())
15697                        .unwrap_or_default();
15698                    let full = format!("{}::STORE", class);
15699                    if let Some(sub) = self.subs.get(&full).cloned() {
15700                        let arg_vals = vec![obj, StrykeValue::integer(idx), val];
15701                        return match self.call_sub(
15702                            &sub,
15703                            arg_vals,
15704                            WantarrayCtx::Scalar,
15705                            target.line,
15706                        ) {
15707                            Ok(_) => Ok(StrykeValue::UNDEF),
15708                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15709                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15710                        };
15711                    }
15712                }
15713                self.scope.set_array_element(&aname, idx, val)?;
15714                Ok(StrykeValue::UNDEF)
15715            }
15716            ExprKind::ArraySlice { array, indices } => {
15717                if indices.is_empty() {
15718                    return Err(
15719                        StrykeError::runtime("assign to empty array slice", target.line).into(),
15720                    );
15721                }
15722                self.check_strict_array_var(array, target.line)?;
15723                if self.scope.is_array_frozen(array) {
15724                    return Err(StrykeError::runtime(
15725                        format!("Modification of a frozen value: @{}", array),
15726                        target.line,
15727                    )
15728                    .into());
15729                }
15730                let aname = self.stash_array_name_for_package(array);
15731                let flat = self.flatten_array_slice_index_specs(indices)?;
15732                self.assign_named_array_slice(&aname, flat, val, target.line)
15733            }
15734            ExprKind::HashElement { hash, key } => {
15735                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
15736                {
15737                    return Err(StrykeError::runtime(
15738                        format!(
15739                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15740                            hash, hash
15741                        ),
15742                        target.line,
15743                    )
15744                    .into());
15745                }
15746                if self.scope.is_hash_frozen(hash) {
15747                    return Err(StrykeError::runtime(
15748                        format!("Modification of a frozen value: %%{}", hash),
15749                        target.line,
15750                    )
15751                    .into());
15752                }
15753                let k = self.eval_expr(key)?.to_string();
15754                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
15755                    let class = obj
15756                        .as_blessed_ref()
15757                        .map(|b| b.class.clone())
15758                        .unwrap_or_default();
15759                    let full = format!("{}::STORE", class);
15760                    if let Some(sub) = self.subs.get(&full).cloned() {
15761                        let arg_vals = vec![obj, StrykeValue::string(k), val];
15762                        return match self.call_sub(
15763                            &sub,
15764                            arg_vals,
15765                            WantarrayCtx::Scalar,
15766                            target.line,
15767                        ) {
15768                            Ok(_) => Ok(StrykeValue::UNDEF),
15769                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15770                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15771                        };
15772                    }
15773                }
15774                let hname = self.tree_hash_storage_name(hash);
15775                self.scope.set_hash_element(&hname, &k, val)?;
15776                Ok(StrykeValue::UNDEF)
15777            }
15778            ExprKind::HashSlice { hash, keys } => {
15779                if keys.is_empty() {
15780                    return Err(
15781                        StrykeError::runtime("assign to empty hash slice", target.line).into(),
15782                    );
15783                }
15784                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
15785                {
15786                    return Err(StrykeError::runtime(
15787                        format!(
15788                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15789                            hash, hash
15790                        ),
15791                        target.line,
15792                    )
15793                    .into());
15794                }
15795                if self.scope.is_hash_frozen(hash) {
15796                    return Err(StrykeError::runtime(
15797                        format!("Modification of a frozen value: %%{}", hash),
15798                        target.line,
15799                    )
15800                    .into());
15801                }
15802                let mut key_vals = Vec::with_capacity(keys.len());
15803                for key_expr in keys {
15804                    let v = if matches!(
15805                        key_expr.kind,
15806                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
15807                    ) {
15808                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
15809                    } else {
15810                        self.eval_expr(key_expr)?
15811                    };
15812                    key_vals.push(v);
15813                }
15814                self.assign_named_hash_slice(hash, key_vals, val, target.line)
15815            }
15816            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
15817            ExprKind::TypeglobExpr(e) => {
15818                let name = self.eval_expr(e)?.to_string();
15819                let synthetic = Expr {
15820                    kind: ExprKind::Typeglob(name),
15821                    line: target.line,
15822                };
15823                self.assign_value(&synthetic, val)
15824            }
15825            ExprKind::AnonymousListSlice { source, indices } => {
15826                if let ExprKind::Deref {
15827                    expr: inner,
15828                    kind: Sigil::Array,
15829                } = &source.kind
15830                {
15831                    let container = self.eval_arrow_array_base(inner, target.line)?;
15832                    let vals = val.to_list();
15833                    let n = indices.len().min(vals.len());
15834                    for i in 0..n {
15835                        let idx = self.eval_expr(&indices[i])?.to_int();
15836                        self.assign_arrow_array_deref(
15837                            container.clone(),
15838                            idx,
15839                            vals[i].clone(),
15840                            target.line,
15841                        )?;
15842                    }
15843                    return Ok(StrykeValue::UNDEF);
15844                }
15845                Err(
15846                    StrykeError::runtime("assign to list slice: unsupported base", target.line)
15847                        .into(),
15848                )
15849            }
15850            ExprKind::ArrowDeref {
15851                expr,
15852                index,
15853                kind: DerefKind::Hash,
15854            } => {
15855                let key = self.eval_expr(index)?.to_string();
15856                let container = self.eval_expr(expr)?;
15857                self.assign_arrow_hash_deref(container, key, val, target.line)
15858            }
15859            ExprKind::ArrowDeref {
15860                expr,
15861                index,
15862                kind: DerefKind::Array,
15863            } => {
15864                let container = self.eval_arrow_array_base(expr, target.line)?;
15865                if let ExprKind::List(indices) = &index.kind {
15866                    let vals = val.to_list();
15867                    let n = indices.len().min(vals.len());
15868                    for i in 0..n {
15869                        let idx = self.eval_expr(&indices[i])?.to_int();
15870                        self.assign_arrow_array_deref(
15871                            container.clone(),
15872                            idx,
15873                            vals[i].clone(),
15874                            target.line,
15875                        )?;
15876                    }
15877                    return Ok(StrykeValue::UNDEF);
15878                }
15879                let idx = self.eval_expr(index)?.to_int();
15880                self.assign_arrow_array_deref(container, idx, val, target.line)
15881            }
15882            ExprKind::HashSliceDeref { container, keys } => {
15883                let href = self.eval_expr(container)?;
15884                let mut key_vals = Vec::with_capacity(keys.len());
15885                for key_expr in keys {
15886                    key_vals.push(self.eval_expr(key_expr)?);
15887                }
15888                self.assign_hash_slice_deref(href, key_vals, val, target.line)
15889            }
15890            ExprKind::Deref {
15891                expr,
15892                kind: Sigil::Scalar,
15893            } => {
15894                let ref_val = self.eval_expr(expr)?;
15895                self.assign_scalar_ref_deref(ref_val, val, target.line)
15896            }
15897            ExprKind::Deref {
15898                expr,
15899                kind: Sigil::Array,
15900            } => {
15901                let ref_val = self.eval_expr(expr)?;
15902                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
15903            }
15904            ExprKind::Deref {
15905                expr,
15906                kind: Sigil::Hash,
15907            } => {
15908                let ref_val = self.eval_expr(expr)?;
15909                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
15910            }
15911            ExprKind::Deref {
15912                expr,
15913                kind: Sigil::Typeglob,
15914            } => {
15915                let ref_val = self.eval_expr(expr)?;
15916                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
15917            }
15918            ExprKind::Pos(inner) => {
15919                let key = match inner {
15920                    None => "_".to_string(),
15921                    Some(expr) => match &expr.kind {
15922                        ExprKind::ScalarVar(n) => n.clone(),
15923                        _ => self.eval_expr(expr)?.to_string(),
15924                    },
15925                };
15926                if val.is_undef() {
15927                    self.regex_pos.insert(key, None);
15928                } else {
15929                    let u = val.to_int().max(0) as usize;
15930                    self.regex_pos.insert(key, Some(u));
15931                }
15932                Ok(StrykeValue::UNDEF)
15933            }
15934            // List assignment: `($a, $b, ...) = (val1, val2, ...)`
15935            // RHS is already fully evaluated — distribute elements to targets.
15936            ExprKind::List(targets) => {
15937                let items = val.to_list();
15938                for (i, t) in targets.iter().enumerate() {
15939                    let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
15940                    self.assign_value(t, v)?;
15941                }
15942                Ok(StrykeValue::UNDEF)
15943            }
15944            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
15945            // write the substitution result back to the assignment target.
15946            ExprKind::Assign { target, .. } => self.assign_value(target, val),
15947            _ => Ok(StrykeValue::UNDEF),
15948        }
15949    }
15950
15951    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
15952    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
15953        // Per Perl semantics every punctuation / reserved scalar
15954        // resides in main. `$main::!` ≡ `$!`, `$main::@` ≡ `$@`, etc.
15955        // Canonicalize before matching so the qualified spelling
15956        // routes to the same special-var dispatch as the bare form.
15957        let name = crate::scope::strip_main_prefix(name).unwrap_or(name);
15958        (name.starts_with('#') && name.len() > 1)
15959            || name.starts_with('^')
15960            || matches!(
15961                name,
15962                "$$" | "0"
15963                    | "!"
15964                    | "@"
15965                    | "/"
15966                    | "\\"
15967                    | ","
15968                    | "."
15969                    | "]"
15970                    | ";"
15971                    | "ARGV"
15972                    | "^I"
15973                    | "^D"
15974                    | "^P"
15975                    | "^S"
15976                    | "^W"
15977                    | "^O"
15978                    | "^T"
15979                    | "^V"
15980                    | "^E"
15981                    | "^H"
15982                    | "^WARNING_BITS"
15983                    | "^GLOBAL_PHASE"
15984                    | "^MATCH"
15985                    | "^PREMATCH"
15986                    | "^POSTMATCH"
15987                    | "^LAST_SUBMATCH_RESULT"
15988                    | "<"
15989                    | ">"
15990                    | "("
15991                    | ")"
15992                    | "?"
15993                    | "|"
15994                    | "\""
15995                    | "+"
15996                    | "%"
15997                    | "="
15998                    | "-"
15999                    | ":"
16000                    | "*"
16001                    | "INC"
16002            )
16003            || crate::english::is_known_alias(name)
16004    }
16005
16006    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
16007    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
16008    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
16009    /// [`Self::english_no_match_vars`] is set.
16010    #[inline]
16011    /// English alias resolution + `our`/`oursync` package qualification in one call.
16012    /// Returns the storage key the scope expects: `$ARG` → `_`, then `our $x` → `Pkg::x`.
16013    /// Use this for compound ops (`++`, `--`, `+=`, `||=`, etc.) so atomic-RMW lookups
16014    /// hit the package-qualified cell stored by `oursync`.
16015    pub(crate) fn resolved_scalar_storage_name(&self, name: &str) -> String {
16016        self.tree_scalar_storage_name(self.english_scalar_name(name))
16017    }
16018
16019    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
16020        if !self.english_enabled {
16021            return name;
16022        }
16023        if self
16024            .english_lexical_scalars
16025            .iter()
16026            .any(|s| s.contains(name))
16027        {
16028            return name;
16029        }
16030        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
16031            return short;
16032        }
16033        name
16034    }
16035
16036    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
16037    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
16038        // `$#name = N` resizes `@name` (Perl: setting the last index). The
16039        // bare-set path stores under literal `#name` as a separate scalar
16040        // and silently does nothing useful — match the read-side handling
16041        // by routing through `set_special_var`.
16042        (name.starts_with('#') && name.len() > 1)
16043            || name.starts_with('^')
16044            || matches!(
16045                name,
16046                "0" | "/"
16047                    | "\\"
16048                    | ","
16049                    | ";"
16050                    | "\""
16051                    | "%"
16052                    | "="
16053                    | "-"
16054                    | ":"
16055                    | "*"
16056                    | "INC"
16057                    | "^I"
16058                    | "^D"
16059                    | "^P"
16060                    | "^W"
16061                    | "^H"
16062                    | "^WARNING_BITS"
16063                    | "$$"
16064                    | "]"
16065                    | "^S"
16066                    | "ARGV"
16067                    | "|"
16068                    | "+"
16069                    | "?"
16070                    | "!"
16071                    | "@"
16072                    | "."
16073            )
16074            || crate::english::is_known_alias(name)
16075    }
16076
16077    pub(crate) fn get_special_var(&self, name: &str) -> StrykeValue {
16078        // Per Perl: every punctuation / reserved scalar lives in main.
16079        // Canonicalize `$main::!` → `$!`, `$main::@` → `$@`, etc. so
16080        // the dispatch matches the bare key.
16081        let name = crate::scope::strip_main_prefix(name).unwrap_or(name);
16082        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
16083        let name = if !crate::compat_mode() {
16084            match name {
16085                // awk `NR` is the cumulative record number across all input
16086                // files — served from `nr`, which (unlike `$.` / `line_number`)
16087                // is never reset at a file boundary. `$FNR` (a plain scalar) is
16088                // the per-file counter.
16089                "NR" => {
16090                    return StrykeValue::integer(self.nr);
16091                }
16092                "RS" => "/",
16093                "OFS" => ",",
16094                "ORS" => "\\",
16095                "NF" => {
16096                    let len = self.scope.array_len("F");
16097                    return StrykeValue::integer(len as i64);
16098                }
16099                _ => self.english_scalar_name(name),
16100            }
16101        } else {
16102            self.english_scalar_name(name)
16103        };
16104        match name {
16105            "$$" => StrykeValue::integer(std::process::id() as i64),
16106            "_" => self.scope.get_scalar("_"),
16107            "^MATCH" => StrykeValue::string(self.last_match.clone()),
16108            "^PREMATCH" => StrykeValue::string(self.prematch.clone()),
16109            "^POSTMATCH" => StrykeValue::string(self.postmatch.clone()),
16110            "^LAST_SUBMATCH_RESULT" => StrykeValue::string(self.last_paren_match.clone()),
16111            "0" => StrykeValue::string(self.program_name.clone()),
16112            "!" => StrykeValue::errno_dual(self.errno_code, self.errno.clone()),
16113            "@" => {
16114                if let Some(ref v) = self.eval_error_value {
16115                    v.clone()
16116                } else {
16117                    StrykeValue::errno_dual(self.eval_error_code, self.eval_error.clone())
16118                }
16119            }
16120            "/" => match &self.irs {
16121                Some(s) => StrykeValue::string(s.clone()),
16122                None => StrykeValue::UNDEF,
16123            },
16124            "\\" => StrykeValue::string(self.ors.clone()),
16125            "," => StrykeValue::string(self.ofs.clone()),
16126            "." => {
16127                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
16128                if self.last_readline_handle.is_empty() {
16129                    if self.line_number == 0 {
16130                        StrykeValue::UNDEF
16131                    } else {
16132                        StrykeValue::integer(self.line_number)
16133                    }
16134                } else {
16135                    StrykeValue::integer(
16136                        *self
16137                            .handle_line_numbers
16138                            .get(&self.last_readline_handle)
16139                            .unwrap_or(&0),
16140                    )
16141                }
16142            }
16143            "]" => StrykeValue::float(perl_bracket_version()),
16144            ";" => StrykeValue::string(self.subscript_sep.clone()),
16145            "ARGV" => StrykeValue::string(self.argv_current_file.clone()),
16146            "^I" => StrykeValue::string(self.inplace_edit.clone()),
16147            "^D" => StrykeValue::integer(self.debug_flags),
16148            "^P" => StrykeValue::integer(self.perl_debug_flags),
16149            "^S" => StrykeValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
16150            "^W" => StrykeValue::integer(if self.warnings { 1 } else { 0 }),
16151            "^O" => StrykeValue::string(perl_osname()),
16152            "^T" => StrykeValue::integer(self.script_start_time),
16153            "^V" => StrykeValue::string(perl_version_v_string()),
16154            "^E" => StrykeValue::string(extended_os_error_string()),
16155            "^H" => StrykeValue::integer(self.compile_hints),
16156            "^WARNING_BITS" => StrykeValue::integer(self.warning_bits),
16157            "^GLOBAL_PHASE" => StrykeValue::string(self.global_phase.clone()),
16158            "<" | ">" => StrykeValue::integer(unix_id_for_special(name)),
16159            "(" | ")" => StrykeValue::string(unix_group_list_for_special(name)),
16160            "?" => StrykeValue::integer(self.child_exit_status),
16161            "|" => StrykeValue::integer(if self.output_autoflush { 1 } else { 0 }),
16162            "\"" => StrykeValue::string(self.list_separator.clone()),
16163            "+" => StrykeValue::string(self.last_paren_match.clone()),
16164            "%" => StrykeValue::integer(self.format_page_number),
16165            "=" => StrykeValue::integer(self.format_lines_per_page),
16166            "-" => StrykeValue::integer(self.format_lines_left),
16167            ":" => StrykeValue::string(self.format_line_break_chars.clone()),
16168            "*" => StrykeValue::integer(if self.multiline_match { 1 } else { 0 }),
16169            "^" => StrykeValue::string(self.format_top_name.clone()),
16170            "INC" => StrykeValue::integer(self.inc_hook_index),
16171            "^A" => StrykeValue::string(self.accumulator_format.clone()),
16172            "^C" => StrykeValue::integer(if self.sigint_pending_caret.replace(false) {
16173                1
16174            } else {
16175                0
16176            }),
16177            "^F" => StrykeValue::integer(self.max_system_fd),
16178            "^L" => StrykeValue::string(self.formfeed_string.clone()),
16179            "^M" => StrykeValue::string(self.emergency_memory.clone()),
16180            "^N" => StrykeValue::string(self.last_subpattern_name.clone()),
16181            "^X" => StrykeValue::string(self.executable_path.clone()),
16182            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
16183            "^TAINT" | "^TAINTED" => StrykeValue::integer(0),
16184            "^UNICODE" => StrykeValue::integer(if self.utf8_pragma { 1 } else { 0 }),
16185            "^OPEN" => StrykeValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
16186            "^UTF8LOCALE" => StrykeValue::integer(0),
16187            "^UTF8CACHE" => StrykeValue::integer(-1),
16188            _ if name.starts_with('^') && name.len() > 1 => self
16189                .special_caret_scalars
16190                .get(name)
16191                .cloned()
16192                .unwrap_or(StrykeValue::UNDEF),
16193            _ if name.starts_with('#') && name.len() > 1 => {
16194                let arr = &name[1..];
16195                let aname = self.stash_array_name_for_package(arr);
16196                let len = self.scope.array_len(&aname);
16197                StrykeValue::integer(len as i64 - 1)
16198            }
16199            _ => self.scope.get_scalar(name),
16200        }
16201    }
16202
16203    pub(crate) fn set_special_var(
16204        &mut self,
16205        name: &str,
16206        val: &StrykeValue,
16207    ) -> Result<(), StrykeError> {
16208        let name = self.english_scalar_name(name);
16209        match name {
16210            "!" => {
16211                let code = val.to_int() as i32;
16212                self.errno_code = code;
16213                self.errno = if code == 0 {
16214                    String::new()
16215                } else {
16216                    std::io::Error::from_raw_os_error(code).to_string()
16217                };
16218            }
16219            "@" => {
16220                if let Some((code, msg)) = val.errno_dual_parts() {
16221                    self.eval_error_code = code;
16222                    self.eval_error = msg;
16223                } else {
16224                    self.eval_error = val.to_string();
16225                    let mut code = val.to_int() as i32;
16226                    if code == 0 && !self.eval_error.is_empty() {
16227                        code = 1;
16228                    }
16229                    self.eval_error_code = code;
16230                }
16231            }
16232            "." => {
16233                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
16234                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
16235                let n = val.to_int();
16236                if self.last_readline_handle.is_empty() {
16237                    self.line_number = n;
16238                } else {
16239                    self.handle_line_numbers
16240                        .insert(self.last_readline_handle.clone(), n);
16241                }
16242            }
16243            "0" => self.program_name = val.to_string(),
16244            "/" => {
16245                self.irs = if val.is_undef() {
16246                    None
16247                } else {
16248                    Some(val.to_string())
16249                }
16250            }
16251            "\\" => self.ors = val.to_string(),
16252            "," => self.ofs = val.to_string(),
16253            ";" => self.subscript_sep = val.to_string(),
16254            "\"" => self.list_separator = val.to_string(),
16255            "%" => self.format_page_number = val.to_int(),
16256            "=" => self.format_lines_per_page = val.to_int(),
16257            "-" => self.format_lines_left = val.to_int(),
16258            ":" => self.format_line_break_chars = val.to_string(),
16259            "*" => self.multiline_match = val.to_int() != 0,
16260            "^" => self.format_top_name = val.to_string(),
16261            "INC" => self.inc_hook_index = val.to_int(),
16262            "^A" => self.accumulator_format = val.to_string(),
16263            "^F" => self.max_system_fd = val.to_int(),
16264            "^L" => self.formfeed_string = val.to_string(),
16265            "^M" => self.emergency_memory = val.to_string(),
16266            "^I" => self.inplace_edit = val.to_string(),
16267            "^D" => self.debug_flags = val.to_int(),
16268            "^P" => self.perl_debug_flags = val.to_int(),
16269            "^W" => self.warnings = val.to_int() != 0,
16270            "^H" => self.compile_hints = val.to_int(),
16271            "^WARNING_BITS" => self.warning_bits = val.to_int(),
16272            "|" => {
16273                self.output_autoflush = val.to_int() != 0;
16274                if self.output_autoflush {
16275                    let _ = io::stdout().flush();
16276                }
16277            }
16278            // Read-only or pid-backed
16279            "$$"
16280            | "]"
16281            | "^S"
16282            | "ARGV"
16283            | "?"
16284            | "^O"
16285            | "^T"
16286            | "^V"
16287            | "^E"
16288            | "^GLOBAL_PHASE"
16289            | "^MATCH"
16290            | "^PREMATCH"
16291            | "^POSTMATCH"
16292            | "^LAST_SUBMATCH_RESULT"
16293            | "^C"
16294            | "^N"
16295            | "^X"
16296            | "^TAINT"
16297            | "^TAINTED"
16298            | "^UNICODE"
16299            | "^UTF8LOCALE"
16300            | "^UTF8CACHE"
16301            | "+"
16302            | "<"
16303            | ">"
16304            | "("
16305            | ")" => {}
16306            _ if name.starts_with('^') && name.len() > 1 => {
16307                self.special_caret_scalars
16308                    .insert(name.to_string(), val.clone());
16309            }
16310            _ if name.starts_with('#') && name.len() > 1 => {
16311                // `$#name = N` resizes `@name` to length `N + 1`. Truncates
16312                // when N < current_last_idx, extends with `undef` otherwise.
16313                let arr = &name[1..];
16314                let aname = self.stash_array_name_for_package(arr);
16315                let new_last = val.to_int();
16316                let new_len = if new_last < 0 {
16317                    0
16318                } else {
16319                    (new_last as usize) + 1
16320                };
16321                let mut current = self.scope.get_array(&aname);
16322                current.resize(new_len, StrykeValue::UNDEF);
16323                self.scope.set_array(&aname, current)?;
16324            }
16325            _ => self.scope.set_scalar(name, val.clone())?,
16326        }
16327        Ok(())
16328    }
16329
16330    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
16331        match &expr.kind {
16332            // Route through `tree_array_storage_name` so bare `@servers` inside
16333            // `package Config` resolves to the same `Config::servers` storage
16334            // that the read path (and external `@Config::servers` mutations) see.
16335            ExprKind::ArrayVar(name) => Ok(self.tree_array_storage_name(name)),
16336            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
16337            _ => Err(StrykeError::runtime("Expected array", expr.line).into()),
16338        }
16339    }
16340
16341    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
16342    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
16343        match &expr.kind {
16344            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
16345            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
16346            _ => expr,
16347        }
16348    }
16349
16350    /// `@$aref` / `@{...}` after optional peeling — for `SpliceExpr` / `pop` operations.
16351    fn try_eval_array_deref_container(
16352        &mut self,
16353        expr: &Expr,
16354    ) -> Result<Option<StrykeValue>, FlowOrError> {
16355        let e = Self::peel_array_builtin_operand(expr);
16356        if let ExprKind::Deref {
16357            expr: inner,
16358            kind: Sigil::Array,
16359        } = &e.kind
16360        {
16361            return Ok(Some(self.eval_or_autoviv_array_ref(inner)?));
16362        }
16363        Ok(None)
16364    }
16365
16366    /// Evaluate `inner` and return an array ref, auto-vivifying when the result is undef
16367    /// and `inner` denotes a writable lvalue (scalar var, hash element, array element).
16368    /// Mirrors Perl 5: `push @{$h{k}}, $x` creates `$h{k}` as an arrayref on demand.
16369    fn eval_or_autoviv_array_ref(&mut self, inner: &Expr) -> Result<StrykeValue, FlowOrError> {
16370        let line = inner.line;
16371        let val = self.eval_expr(inner)?;
16372        if !val.is_undef() {
16373            return Ok(val);
16374        }
16375        let new_ref = StrykeValue::array_ref(Arc::new(RwLock::new(Vec::new())));
16376        match &inner.kind {
16377            ExprKind::ScalarVar(name) => {
16378                self.scope
16379                    .set_scalar(name, new_ref.clone())
16380                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
16381                Ok(new_ref)
16382            }
16383            ExprKind::HashElement { hash, key } => {
16384                let k = self.eval_expr(key)?.to_string();
16385                self.scope
16386                    .set_hash_element(hash, &k, new_ref.clone())
16387                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
16388                Ok(new_ref)
16389            }
16390            ExprKind::ArrayElement { array, index } => {
16391                let i = self.eval_expr(index)?.to_int();
16392                self.scope
16393                    .set_array_element(array, i, new_ref.clone())
16394                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
16395                Ok(new_ref)
16396            }
16397            _ => Ok(val),
16398        }
16399    }
16400
16401    /// Current package (`main` when `__PACKAGE__` is unset or empty).
16402    pub(crate) fn current_package(&self) -> String {
16403        let s = self.scope.get_scalar("__PACKAGE__").to_string();
16404        if s.is_empty() {
16405            "main".to_string()
16406        } else {
16407            s
16408        }
16409    }
16410
16411    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
16412    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
16413    pub(crate) fn package_version_scalar(
16414        &mut self,
16415        package: &str,
16416    ) -> StrykeResult<Option<StrykeValue>> {
16417        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
16418        let _ = self
16419            .scope
16420            .set_scalar("__PACKAGE__", StrykeValue::string(package.to_string()));
16421        let ver = self.get_special_var("VERSION");
16422        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
16423        Ok(if ver.is_undef() { None } else { Some(ver) })
16424    }
16425
16426    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
16427    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<StrykeSub>> {
16428        let root = if start_package.is_empty() {
16429            "main"
16430        } else {
16431            start_package
16432        };
16433        for pkg in self.mro_linearize(root) {
16434            let key = if pkg == "main" {
16435                "AUTOLOAD".to_string()
16436            } else {
16437                format!("{}::AUTOLOAD", pkg)
16438            };
16439            if let Some(s) = self.subs.get(&key) {
16440                return Some(s.clone());
16441            }
16442        }
16443        None
16444    }
16445
16446    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
16447    /// qualified missing sub or method name and invoke the handler (same argument list as the
16448    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
16449    /// the package prefix of the missing name (or current package).
16450    pub(crate) fn try_autoload_call(
16451        &mut self,
16452        missing_name: &str,
16453        args: Vec<StrykeValue>,
16454        line: usize,
16455        want: WantarrayCtx,
16456        method_invocant_class: Option<&str>,
16457    ) -> Option<ExecResult> {
16458        let pkg = self.current_package();
16459        let full = if missing_name.contains("::") {
16460            missing_name.to_string()
16461        } else {
16462            format!("{}::{}", pkg, missing_name)
16463        };
16464        let start_pkg = method_invocant_class.unwrap_or_else(|| {
16465            full.rsplit_once("::")
16466                .map(|(p, _)| p)
16467                .filter(|p| !p.is_empty())
16468                .unwrap_or("main")
16469        });
16470        let sub = self.resolve_autoload_sub(start_pkg)?;
16471        if let Err(e) = self
16472            .scope
16473            .set_scalar("AUTOLOAD", StrykeValue::string(full.clone()))
16474        {
16475            return Some(Err(e.into()));
16476        }
16477        Some(self.call_sub(&sub, args, want, line))
16478    }
16479
16480    pub(crate) fn with_topic_default_args(&self, args: Vec<StrykeValue>) -> Vec<StrykeValue> {
16481        if args.is_empty() {
16482            vec![self.scope.get_scalar("_").clone()]
16483        } else {
16484            args
16485        }
16486    }
16487
16488    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
16489    /// and [`crate::bytecode::Op::IndirectCall`].
16490    pub(crate) fn dispatch_indirect_call(
16491        &mut self,
16492        target: StrykeValue,
16493        arg_vals: Vec<StrykeValue>,
16494        want: WantarrayCtx,
16495        line: usize,
16496    ) -> ExecResult {
16497        if let Some(sub) = target.as_code_ref() {
16498            return self.call_sub(&sub, arg_vals, want, line);
16499        }
16500        if let Some(name) = target.as_str() {
16501            return self.call_named_sub(&name, arg_vals, line, want);
16502        }
16503        Err(StrykeError::runtime("Can't use non-code reference as a subroutine", line).into())
16504    }
16505
16506    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
16507    /// Bare-name dispatch for stryke list builtins (`sum`, `min`, `uniq`, `reduce`, `zip`, …).
16508    /// Resolves short aliases (`uq`, `shuf`, `chk`, `win`, `fst`, `rd`, `med`, `std`, `var`, …)
16509    /// and forwards to [`crate::list_builtins::dispatch_by_name`].
16510    pub(crate) fn call_bare_list_builtin(
16511        &mut self,
16512        name: &str,
16513        args: Vec<StrykeValue>,
16514        line: usize,
16515        want: WantarrayCtx,
16516    ) -> ExecResult {
16517        let canonical = match name {
16518            "distinct" | "uq" => "uniq",
16519            "shuf" => "shuffle",
16520            "chk" => "chunked",
16521            "win" => "windowed",
16522            "zp" => "zip",
16523            "fst" => "first",
16524            "rd" => "reduce",
16525            "med" => "median",
16526            "std" => "stddev",
16527            "var" => "variance",
16528            other => other,
16529        };
16530        // List builtins like `sum`, `min`, `uniq` operate on a list — an empty
16531        // input must aggregate to the identity (0/undef), NOT default to $_.
16532        // `sum(@empty_after_grep)` was returning $_ before this; that produced
16533        // surprising results downstream (e.g. `… |> grep {0} |> sum` = topic).
16534        match crate::list_builtins::dispatch_by_name(self, canonical, &args, want) {
16535            Some(r) => r,
16536            None => Err(StrykeError::runtime(
16537                format!("internal: not a stryke list builtin: {name}"),
16538                line,
16539            )
16540            .into()),
16541        }
16542    }
16543
16544    fn call_named_sub(
16545        &mut self,
16546        name: &str,
16547        args: Vec<StrykeValue>,
16548        line: usize,
16549        want: WantarrayCtx,
16550    ) -> ExecResult {
16551        if let Some(sub) = self.resolve_sub_by_name(name) {
16552            let args = self.with_topic_default_args(args);
16553            // The sub's home package is the qualifier from the resolved registry key.
16554            // `StrykeSub.name` itself may be bare; pass an explicit override so call_sub can
16555            // switch `__PACKAGE__` for cross-package `our`/`oursync` qualification.
16556            let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
16557            return self.call_sub_with_package(&sub, args, want, line, pkg);
16558        }
16559        match name {
16560            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
16561            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
16562            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
16563            | "none" | "notall" | "first" | "fst" | "find_index" | "firstidx" | "first_index"
16564            | "reduce" | "rd" | "reductions" | "sum" | "sum0" | "product" | "min" | "max"
16565            | "minstr" | "maxstr" | "mean" | "median" | "med" | "mode" | "stddev" | "std"
16566            | "variance" | "var" | "pairs" | "unpairs" | "pairkeys" | "pairvalues" | "pairgrep"
16567            | "pairmap" | "pairfirst" | "blessed" | "refaddr" | "reftype" | "looks_like_number"
16568            | "weaken" | "unweaken" | "isweak" | "set_subname" | "subname"
16569            | "unicode_to_native" => self.call_bare_list_builtin(name, args, line, want),
16570            "deque" => {
16571                if !args.is_empty() {
16572                    return Err(StrykeError::runtime("deque() takes no arguments", line).into());
16573                }
16574                Ok(StrykeValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
16575            }
16576            "defer__internal" => {
16577                if args.len() != 1 {
16578                    return Err(StrykeError::runtime(
16579                        "defer__internal expects one coderef argument",
16580                        line,
16581                    )
16582                    .into());
16583                }
16584                self.scope.push_defer(args[0].clone());
16585                Ok(StrykeValue::UNDEF)
16586            }
16587            "heap" => {
16588                if args.len() != 1 {
16589                    return Err(
16590                        StrykeError::runtime("heap() expects one comparator sub", line).into(),
16591                    );
16592                }
16593                if let Some(sub) = args[0].as_code_ref() {
16594                    Ok(StrykeValue::heap(Arc::new(Mutex::new(PerlHeap {
16595                        items: Vec::new(),
16596                        cmp: Arc::clone(&sub),
16597                    }))))
16598                } else {
16599                    Err(StrykeError::runtime("heap() requires a code reference", line).into())
16600                }
16601            }
16602            "pipeline" => {
16603                let mut items = Vec::new();
16604                for v in args {
16605                    if let Some(a) = v.as_array_vec() {
16606                        items.extend(a);
16607                    } else {
16608                        items.push(v);
16609                    }
16610                }
16611                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
16612                    source: items,
16613                    ops: Vec::new(),
16614                    has_scalar_terminal: false,
16615                    par_stream: false,
16616                    streaming: false,
16617                    streaming_workers: 0,
16618                    streaming_buffer: 256,
16619                }))))
16620            }
16621            "par_pipeline" => {
16622                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
16623                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
16624                        .map_err(Into::into);
16625                }
16626                Ok(self.builtin_par_pipeline_stream(&args, line)?)
16627            }
16628            "par_pipeline_stream" => {
16629                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
16630                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
16631                        .map_err(Into::into);
16632                }
16633                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
16634            }
16635            "ppool" => {
16636                if args.len() != 1 {
16637                    return Err(StrykeError::runtime(
16638                        "ppool() expects one argument (worker count)",
16639                        line,
16640                    )
16641                    .into());
16642                }
16643                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
16644            }
16645            "barrier" => {
16646                if args.len() != 1 {
16647                    return Err(StrykeError::runtime(
16648                        "barrier() expects one argument (party count)",
16649                        line,
16650                    )
16651                    .into());
16652                }
16653                let n = args[0].to_int().max(1) as usize;
16654                Ok(StrykeValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
16655            }
16656            "cluster" => {
16657                let items = if args.len() == 1 {
16658                    args[0].to_list()
16659                } else {
16660                    args.to_vec()
16661                };
16662                let c = RemoteCluster::from_list_args(&items)
16663                    .map_err(|msg| StrykeError::runtime(msg, line))?;
16664                Ok(StrykeValue::remote_cluster(Arc::new(c)))
16665            }
16666            _ => {
16667                // Late static binding: static::method() resolves to runtime class of $self
16668                if let Some(method_name) = name.strip_prefix("static::") {
16669                    let self_val = self.scope.get_scalar("self");
16670                    if let Some(c) = self_val.as_class_inst() {
16671                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
16672                            if let Some(ref body) = m.body {
16673                                let params = m.params.clone();
16674                                let mut call_args = vec![self_val.clone()];
16675                                call_args.extend(args);
16676                                return match self.call_class_method(body, &params, call_args, line)
16677                                {
16678                                    Ok(v) => Ok(v),
16679                                    Err(FlowOrError::Error(e)) => Err(e.into()),
16680                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16681                                    Err(e) => Err(e),
16682                                };
16683                            }
16684                        }
16685                        return Err(StrykeError::runtime(
16686                            format!(
16687                                "static::{} — method not found on class {}",
16688                                method_name, c.def.name
16689                            ),
16690                            line,
16691                        )
16692                        .into());
16693                    }
16694                    return Err(StrykeError::runtime(
16695                        "static:: can only be used inside a class method",
16696                        line,
16697                    )
16698                    .into());
16699                }
16700                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
16701                if let Some(def) = self.struct_defs.get(name).cloned() {
16702                    return self.struct_construct(&def, args, line);
16703                }
16704                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
16705                if let Some(def) = self.class_defs.get(name).cloned() {
16706                    return self.class_construct(&def, args, line);
16707                }
16708                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
16709                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
16710                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
16711                        return self.enum_construct(&def, variant_name, args, line);
16712                    }
16713                }
16714                // Check for static class method or static field: Math::add(...) / Counter::count()
16715                if let Some((class_name, member_name)) = name.rsplit_once("::") {
16716                    if let Some(def) = self.class_defs.get(class_name).cloned() {
16717                        // Static method
16718                        if let Some(m) = def.method(member_name) {
16719                            if m.is_static {
16720                                if let Some(ref body) = m.body {
16721                                    let params = m.params.clone();
16722                                    return match self.call_static_class_method(
16723                                        body,
16724                                        &params,
16725                                        args.clone(),
16726                                        line,
16727                                    ) {
16728                                        Ok(v) => Ok(v),
16729                                        Err(FlowOrError::Error(e)) => Err(e.into()),
16730                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16731                                        Err(e) => Err(e),
16732                                    };
16733                                }
16734                            }
16735                        }
16736                        // Static field access: getter (0 args) or setter (1 arg)
16737                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
16738                            let key = format!("{}::{}", class_name, member_name);
16739                            match args.len() {
16740                                0 => {
16741                                    let val = self.scope.get_scalar(&key);
16742                                    return Ok(val);
16743                                }
16744                                1 => {
16745                                    let _ = self.scope.set_scalar(&key, args[0].clone());
16746                                    return Ok(args[0].clone());
16747                                }
16748                                _ => {
16749                                    return Err(StrykeError::runtime(
16750                                        format!(
16751                                            "static field `{}::{}` takes 0 or 1 arguments",
16752                                            class_name, member_name
16753                                        ),
16754                                        line,
16755                                    )
16756                                    .into());
16757                                }
16758                            }
16759                        }
16760                    }
16761                }
16762                let args = self.with_topic_default_args(args);
16763                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
16764                    return r;
16765                }
16766                Err(StrykeError::runtime(self.undefined_subroutine_call_message(name), line).into())
16767            }
16768        }
16769    }
16770
16771    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
16772    pub(crate) fn struct_construct(
16773        &mut self,
16774        def: &Arc<StructDef>,
16775        args: Vec<StrykeValue>,
16776        line: usize,
16777    ) -> ExecResult {
16778        // Detect if args are named (key => value pairs) or positional
16779        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
16780        let is_named = args.len() >= 2
16781            && args.len().is_multiple_of(2)
16782            && args.iter().step_by(2).all(|v| {
16783                let s = v.to_string();
16784                def.field_index(&s).is_some()
16785            });
16786
16787        let provided = if is_named {
16788            // Named construction: Point(x => 1, y => 2)
16789            let mut pairs = Vec::new();
16790            let mut i = 0;
16791            while i + 1 < args.len() {
16792                let k = args[i].to_string();
16793                let v = args[i + 1].clone();
16794                pairs.push((k, v));
16795                i += 2;
16796            }
16797            pairs
16798        } else {
16799            // Positional construction: Point(1, 2) fills fields in declaration order
16800            def.fields
16801                .iter()
16802                .zip(args.iter())
16803                .map(|(f, v)| (f.name.clone(), v.clone()))
16804                .collect()
16805        };
16806
16807        // Evaluate default expressions
16808        let mut defaults = Vec::with_capacity(def.fields.len());
16809        for field in &def.fields {
16810            if let Some(ref expr) = field.default {
16811                let val = self.eval_expr(expr)?;
16812                defaults.push(Some(val));
16813            } else {
16814                defaults.push(None);
16815            }
16816        }
16817
16818        Ok(crate::native_data::struct_new_with_defaults(
16819            def, &provided, &defaults, line,
16820        )?)
16821    }
16822
16823    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
16824    pub(crate) fn class_construct(
16825        &mut self,
16826        def: &Arc<ClassDef>,
16827        args: Vec<StrykeValue>,
16828        _line: usize,
16829    ) -> ExecResult {
16830        use crate::value::ClassInstance;
16831
16832        // Prevent instantiation of abstract classes
16833        if def.is_abstract {
16834            return Err(StrykeError::runtime(
16835                format!("cannot instantiate abstract class `{}`", def.name),
16836                _line,
16837            )
16838            .into());
16839        }
16840
16841        // Collect all fields from inheritance chain (parent fields first)
16842        let all_fields = self.collect_class_fields(def);
16843
16844        // Check if args are named
16845        let is_named = args.len() >= 2
16846            && args.len().is_multiple_of(2)
16847            && args.iter().step_by(2).all(|v| {
16848                let s = v.to_string();
16849                all_fields.iter().any(|(name, _, _)| name == &s)
16850            });
16851
16852        let provided: Vec<(String, StrykeValue)> = if is_named {
16853            let mut pairs = Vec::new();
16854            let mut i = 0;
16855            while i + 1 < args.len() {
16856                let k = args[i].to_string();
16857                let v = args[i + 1].clone();
16858                pairs.push((k, v));
16859                i += 2;
16860            }
16861            pairs
16862        } else {
16863            all_fields
16864                .iter()
16865                .zip(args.iter())
16866                .map(|((name, _, _), v)| (name.clone(), v.clone()))
16867                .collect()
16868        };
16869
16870        // Build values array for all fields (inherited + own) with type checking
16871        let mut values = Vec::with_capacity(all_fields.len());
16872        for (name, default, ty) in &all_fields {
16873            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
16874                val.clone()
16875            } else if let Some(ref expr) = default {
16876                self.eval_expr(expr)?
16877            } else {
16878                StrykeValue::UNDEF
16879            };
16880            ty.check_value(&val).map_err(|msg| {
16881                StrykeError::type_error(
16882                    format!("class {} field `{}`: {}", def.name, name, msg),
16883                    _line,
16884                )
16885            })?;
16886            values.push(val);
16887        }
16888
16889        // Compute full ISA chain for type checking
16890        let isa_chain = self.mro_linearize(&def.name);
16891        let instance = StrykeValue::class_inst(Arc::new(ClassInstance::new_with_isa(
16892            Arc::clone(def),
16893            values,
16894            isa_chain,
16895        )));
16896
16897        // Call BUILD hooks: parent BUILD first, then child BUILD
16898        let build_chain = self.collect_build_chain(def);
16899        if !build_chain.is_empty() {
16900            for (body, params) in &build_chain {
16901                let call_args = vec![instance.clone()];
16902                match self.call_class_method(body, params, call_args, _line) {
16903                    Ok(_) => {}
16904                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
16905                    Err(e) => return Err(e),
16906                }
16907            }
16908        }
16909
16910        Ok(instance)
16911    }
16912
16913    /// Collect BUILD methods from parent to child order.
16914    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
16915        let mut chain = Vec::new();
16916        // Parent BUILD first
16917        for parent_name in &def.extends {
16918            if let Some(parent_def) = self.class_defs.get(parent_name) {
16919                chain.extend(self.collect_build_chain(parent_def));
16920            }
16921        }
16922        // Own BUILD
16923        if let Some(m) = def.method("BUILD") {
16924            if let Some(ref body) = m.body {
16925                chain.push((body.clone(), m.params.clone()));
16926            }
16927        }
16928        chain
16929    }
16930
16931    /// Recursively flatten class/struct instances and the hashes/arrays
16932    /// they contain into a plain hashref tree. Atoms (numbers, strings,
16933    /// undef, code refs, regex refs, blessed-non-hash refs, …) round-trip
16934    /// unchanged. Used by `$obj->to_hash_rec` for both class and struct
16935    /// receivers.
16936    pub(crate) fn deep_to_hash_value(&self, v: &StrykeValue) -> StrykeValue {
16937        // Class instance: hashref of fields, recursing into each value.
16938        if let Some(c) = v.as_class_inst() {
16939            let all_fields = self.collect_class_fields_full(&c.def);
16940            let values = c.get_values();
16941            let mut map = IndexMap::new();
16942            for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
16943                if let Some(elem) = values.get(i) {
16944                    map.insert(name.clone(), self.deep_to_hash_value(elem));
16945                }
16946            }
16947            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16948        }
16949        // Struct instance: same shape, declaration order.
16950        if let Some(s) = v.as_struct_inst() {
16951            let values = s.get_values();
16952            let mut map = IndexMap::new();
16953            for (i, field) in s.def.fields.iter().enumerate() {
16954                if let Some(elem) = values.get(i) {
16955                    map.insert(field.name.clone(), self.deep_to_hash_value(elem));
16956                }
16957            }
16958            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16959        }
16960        // Hashref: clone keys, recurse into values.
16961        if let Some(r) = v.as_hash_ref() {
16962            let inner = r.read().clone();
16963            let mut map = IndexMap::new();
16964            for (k, val) in inner.into_iter() {
16965                map.insert(k, self.deep_to_hash_value(&val));
16966            }
16967            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16968        }
16969        // Arrayref: recurse into elements.
16970        if let Some(r) = v.as_array_ref() {
16971            let inner = r.read().clone();
16972            let out: Vec<StrykeValue> = inner.iter().map(|e| self.deep_to_hash_value(e)).collect();
16973            return StrykeValue::array_ref(Arc::new(RwLock::new(out)));
16974        }
16975        // Everything else (scalars, blessed refs, code refs, enums, …)
16976        // round-trips unchanged. Enum instances stringify naturally
16977        // through their existing `Display` so callers see a stable name.
16978        v.clone()
16979    }
16980
16981    /// Collect all fields from a class and its parent hierarchy (parent fields first).
16982    /// Returns (name, default, type, visibility, owning_class_name).
16983    fn collect_class_fields(
16984        &self,
16985        def: &ClassDef,
16986    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
16987        self.collect_class_fields_full(def)
16988            .into_iter()
16989            .map(|(name, default, ty, _, _)| (name, default, ty))
16990            .collect()
16991    }
16992
16993    /// Like collect_class_fields but includes visibility and owning class name.
16994    fn collect_class_fields_full(
16995        &self,
16996        def: &ClassDef,
16997    ) -> Vec<(
16998        String,
16999        Option<Expr>,
17000        crate::ast::PerlTypeName,
17001        crate::ast::Visibility,
17002        String,
17003    )> {
17004        let mut all_fields = Vec::new();
17005
17006        for parent_name in &def.extends {
17007            if let Some(parent_def) = self.class_defs.get(parent_name) {
17008                let parent_fields = self.collect_class_fields_full(parent_def);
17009                all_fields.extend(parent_fields);
17010            }
17011        }
17012
17013        for field in &def.fields {
17014            all_fields.push((
17015                field.name.clone(),
17016                field.default.clone(),
17017                field.ty.clone(),
17018                field.visibility,
17019                def.name.clone(),
17020            ));
17021        }
17022
17023        all_fields
17024    }
17025
17026    /// Collect all method names from class and parents (deduplicates, child overrides parent).
17027    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
17028        // Parent methods first
17029        for parent_name in &def.extends {
17030            if let Some(parent_def) = self.class_defs.get(parent_name) {
17031                self.collect_class_method_names(parent_def, names);
17032            }
17033        }
17034        // Own methods (add if not already present — child overrides parent name)
17035        for m in &def.methods {
17036            if !m.is_static && !names.contains(&m.name) {
17037                names.push(m.name.clone());
17038            }
17039        }
17040    }
17041
17042    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
17043    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
17044        let mut chain = Vec::new();
17045        // Own DESTROY first
17046        if let Some(m) = def.method("DESTROY") {
17047            if let Some(ref body) = m.body {
17048                chain.push((body.clone(), m.params.clone()));
17049            }
17050        }
17051        // Then parent DESTROY
17052        for parent_name in &def.extends {
17053            if let Some(parent_def) = self.class_defs.get(parent_name) {
17054                chain.extend(self.collect_destroy_chain(parent_def));
17055            }
17056        }
17057        chain
17058    }
17059
17060    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
17061    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
17062        if let Some(def) = self.class_defs.get(child) {
17063            for parent in &def.extends {
17064                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
17065                    return true;
17066                }
17067            }
17068        }
17069        false
17070    }
17071
17072    /// Find a method in a class or its parent hierarchy (child methods override parent).
17073    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
17074        // First check the current class
17075        if let Some(m) = def.method(method) {
17076            return Some((m.clone(), def.name.clone()));
17077        }
17078        // Then check parent classes
17079        for parent_name in &def.extends {
17080            if let Some(parent_def) = self.class_defs.get(parent_name) {
17081                if let Some(result) = self.find_class_method(parent_def, method) {
17082                    return Some(result);
17083                }
17084            }
17085        }
17086        None
17087    }
17088
17089    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
17090    pub(crate) fn enum_construct(
17091        &mut self,
17092        def: &Arc<EnumDef>,
17093        variant_name: &str,
17094        args: Vec<StrykeValue>,
17095        line: usize,
17096    ) -> ExecResult {
17097        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
17098            FlowOrError::Error(StrykeError::runtime(
17099                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
17100                line,
17101            ))
17102        })?;
17103        let variant = &def.variants[variant_idx];
17104        let data = if variant.ty.is_some() {
17105            if args.is_empty() {
17106                return Err(StrykeError::runtime(
17107                    format!(
17108                        "enum variant `{}::{}` requires data",
17109                        def.name, variant_name
17110                    ),
17111                    line,
17112                )
17113                .into());
17114            }
17115            if args.len() == 1 {
17116                args.into_iter().next().unwrap()
17117            } else {
17118                StrykeValue::array(args)
17119            }
17120        } else {
17121            if !args.is_empty() {
17122                return Err(StrykeError::runtime(
17123                    format!(
17124                        "enum variant `{}::{}` does not take data",
17125                        def.name, variant_name
17126                    ),
17127                    line,
17128                )
17129                .into());
17130            }
17131            StrykeValue::UNDEF
17132        };
17133        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
17134        Ok(StrykeValue::enum_inst(Arc::new(inst)))
17135    }
17136
17137    /// True if `name` is a registered or standard process-global handle.
17138    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
17139        matches!(name, "STDIN" | "STDOUT" | "STDERR")
17140            || self.input_handles.contains_key(name)
17141            || self.output_handles.contains_key(name)
17142            || self.io_file_slots.contains_key(name)
17143            || self.pipe_children.contains_key(name)
17144    }
17145
17146    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
17147    pub(crate) fn io_handle_method(
17148        &mut self,
17149        name: &str,
17150        method: &str,
17151        args: &[StrykeValue],
17152        line: usize,
17153    ) -> StrykeResult<StrykeValue> {
17154        match method {
17155            "print" => self.io_handle_print(name, args, false, line),
17156            "say" => self.io_handle_print(name, args, true, line),
17157            "printf" => self.io_handle_printf(name, args, line),
17158            "getline" | "readline" => {
17159                if !args.is_empty() {
17160                    return Err(StrykeError::runtime(
17161                        format!("{}: too many arguments", method),
17162                        line,
17163                    ));
17164                }
17165                self.readline_builtin_execute(Some(name))
17166            }
17167            "close" => {
17168                if !args.is_empty() {
17169                    return Err(StrykeError::runtime("close: too many arguments", line));
17170                }
17171                self.close_builtin_execute(name.to_string())
17172            }
17173            "eof" => {
17174                if !args.is_empty() {
17175                    return Err(StrykeError::runtime("eof: too many arguments", line));
17176                }
17177                let at_eof = !self.has_input_handle(name);
17178                Ok(StrykeValue::integer(if at_eof { 1 } else { 0 }))
17179            }
17180            "getc" => {
17181                if !args.is_empty() {
17182                    return Err(StrykeError::runtime("getc: too many arguments", line));
17183                }
17184                match crate::builtins::try_builtin(
17185                    self,
17186                    "getc",
17187                    &[StrykeValue::string(name.to_string())],
17188                    line,
17189                ) {
17190                    Some(r) => r,
17191                    None => Err(StrykeError::runtime("getc: not available", line)),
17192                }
17193            }
17194            "binmode" => match crate::builtins::try_builtin(
17195                self,
17196                "binmode",
17197                &[StrykeValue::string(name.to_string())],
17198                line,
17199            ) {
17200                Some(r) => r,
17201                None => Err(StrykeError::runtime("binmode: not available", line)),
17202            },
17203            "fileno" => match crate::builtins::try_builtin(
17204                self,
17205                "fileno",
17206                &[StrykeValue::string(name.to_string())],
17207                line,
17208            ) {
17209                Some(r) => r,
17210                None => Err(StrykeError::runtime("fileno: not available", line)),
17211            },
17212            "flush" => {
17213                if !args.is_empty() {
17214                    return Err(StrykeError::runtime("flush: too many arguments", line));
17215                }
17216                self.io_handle_flush(name, line)
17217            }
17218            _ => Err(StrykeError::runtime(
17219                format!("Unknown method for filehandle: {}", method),
17220                line,
17221            )),
17222        }
17223    }
17224
17225    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> StrykeResult<StrykeValue> {
17226        match handle_name {
17227            "STDOUT" => {
17228                let _ = IoWrite::flush(&mut io::stdout());
17229            }
17230            "STDERR" => {
17231                let _ = IoWrite::flush(&mut io::stderr());
17232            }
17233            name => {
17234                if let Some(writer) = self.output_handles.get_mut(name) {
17235                    let _ = IoWrite::flush(&mut *writer);
17236                } else {
17237                    return Err(StrykeError::runtime(
17238                        format!("flush on unopened filehandle {}", name),
17239                        line,
17240                    ));
17241                }
17242            }
17243        }
17244        Ok(StrykeValue::integer(1))
17245    }
17246
17247    fn io_handle_print(
17248        &mut self,
17249        handle_name: &str,
17250        args: &[StrykeValue],
17251        newline: bool,
17252        line: usize,
17253    ) -> StrykeResult<StrykeValue> {
17254        if newline && (self.feature_bits & FEAT_SAY) == 0 {
17255            return Err(StrykeError::runtime(
17256                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
17257                line,
17258            ));
17259        }
17260        let mut output = String::new();
17261        if args.is_empty() {
17262            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
17263            output.push_str(&self.scope.get_scalar("_").to_string());
17264        } else {
17265            for (i, val) in args.iter().enumerate() {
17266                if i > 0 && !self.ofs.is_empty() {
17267                    output.push_str(&self.ofs);
17268                }
17269                output.push_str(&val.to_string());
17270            }
17271        }
17272        if newline {
17273            output.push('\n');
17274        }
17275        output.push_str(&self.ors);
17276
17277        self.write_formatted_print(handle_name, &output, line)?;
17278        Ok(StrykeValue::integer(1))
17279    }
17280
17281    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
17282    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
17283    pub(crate) fn write_formatted_print(
17284        &mut self,
17285        handle_name: &str,
17286        output: &str,
17287        line: usize,
17288    ) -> StrykeResult<()> {
17289        match handle_name {
17290            "STDOUT" => {
17291                if !self.suppress_stdout {
17292                    print!("{}", output);
17293                    if self.output_autoflush {
17294                        let _ = io::stdout().flush();
17295                    }
17296                }
17297            }
17298            "STDERR" => {
17299                eprint!("{}", output);
17300                let _ = io::stderr().flush();
17301            }
17302            name => {
17303                if let Some(writer) = self.output_handles.get_mut(name) {
17304                    let _ = writer.write_all(output.as_bytes());
17305                    if self.output_autoflush {
17306                        let _ = writer.flush();
17307                    }
17308                } else {
17309                    return Err(StrykeError::runtime(
17310                        format!("print on unopened filehandle {}", name),
17311                        line,
17312                    ));
17313                }
17314            }
17315        }
17316        Ok(())
17317    }
17318
17319    fn io_handle_printf(
17320        &mut self,
17321        handle_name: &str,
17322        args: &[StrykeValue],
17323        line: usize,
17324    ) -> StrykeResult<StrykeValue> {
17325        let (fmt, rest): (String, &[StrykeValue]) = if args.is_empty() {
17326            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
17327                Ok(s) => s,
17328                Err(FlowOrError::Error(e)) => return Err(e),
17329                Err(FlowOrError::Flow(_)) => {
17330                    return Err(StrykeError::runtime(
17331                        "printf: unexpected control flow in sprintf",
17332                        line,
17333                    ));
17334                }
17335            };
17336            (s, &[])
17337        } else {
17338            (args[0].to_string(), &args[1..])
17339        };
17340        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
17341            Ok(s) => s,
17342            Err(FlowOrError::Error(e)) => return Err(e),
17343            Err(FlowOrError::Flow(_)) => {
17344                return Err(StrykeError::runtime(
17345                    "printf: unexpected control flow in sprintf",
17346                    line,
17347                ));
17348            }
17349        };
17350        match handle_name {
17351            "STDOUT" => {
17352                if !self.suppress_stdout {
17353                    print!("{}", output);
17354                    if self.output_autoflush {
17355                        let _ = IoWrite::flush(&mut io::stdout());
17356                    }
17357                }
17358            }
17359            "STDERR" => {
17360                eprint!("{}", output);
17361                let _ = IoWrite::flush(&mut io::stderr());
17362            }
17363            name => {
17364                if let Some(writer) = self.output_handles.get_mut(name) {
17365                    let _ = writer.write_all(output.as_bytes());
17366                    if self.output_autoflush {
17367                        let _ = writer.flush();
17368                    }
17369                } else {
17370                    return Err(StrykeError::runtime(
17371                        format!("printf on unopened filehandle {}", name),
17372                        line,
17373                    ));
17374                }
17375            }
17376        }
17377        Ok(StrykeValue::integer(1))
17378    }
17379
17380    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
17381    pub(crate) fn try_native_method(
17382        &mut self,
17383        receiver: &StrykeValue,
17384        method: &str,
17385        args: &[StrykeValue],
17386        line: usize,
17387    ) -> Option<StrykeResult<StrykeValue>> {
17388        if let Some(name) = receiver.as_io_handle_name() {
17389            return Some(self.io_handle_method(&name, method, args, line));
17390        }
17391        if let Some(ref s) = receiver.as_str() {
17392            if self.is_bound_handle(s) {
17393                return Some(self.io_handle_method(s, method, args, line));
17394            }
17395        }
17396        if let Some(c) = receiver.as_sqlite_conn() {
17397            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
17398        }
17399        if let Some(s) = receiver.as_struct_inst() {
17400            // Field access: $p->x or $p->x(value)
17401            if let Some(idx) = s.def.field_index(method) {
17402                match args.len() {
17403                    0 => {
17404                        return Some(Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF)));
17405                    }
17406                    1 => {
17407                        let field = &s.def.fields[idx];
17408                        let new_val = args[0].clone();
17409                        if let Err(msg) = field.ty.check_value(&new_val) {
17410                            return Some(Err(StrykeError::type_error(
17411                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
17412                                line,
17413                            )));
17414                        }
17415                        s.set_field(idx, new_val.clone());
17416                        return Some(Ok(new_val));
17417                    }
17418                    _ => {
17419                        return Some(Err(StrykeError::runtime(
17420                            format!(
17421                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
17422                                method,
17423                                args.len()
17424                            ),
17425                            line,
17426                        )));
17427                    }
17428                }
17429            }
17430            // Built-in struct methods
17431            match method {
17432                "with" => {
17433                    // Functional update: $p->with(x => 5) returns new instance with changed field
17434                    let mut new_values = s.get_values();
17435                    let mut i = 0;
17436                    while i + 1 < args.len() {
17437                        let k = args[i].to_string();
17438                        let v = args[i + 1].clone();
17439                        if let Some(idx) = s.def.field_index(&k) {
17440                            let field = &s.def.fields[idx];
17441                            if let Err(msg) = field.ty.check_value(&v) {
17442                                return Some(Err(StrykeError::type_error(
17443                                    format!(
17444                                        "struct {} field `{}`: {}",
17445                                        s.def.name, field.name, msg
17446                                    ),
17447                                    line,
17448                                )));
17449                            }
17450                            new_values[idx] = v;
17451                        } else {
17452                            return Some(Err(StrykeError::runtime(
17453                                format!("struct {}: unknown field `{}`", s.def.name, k),
17454                                line,
17455                            )));
17456                        }
17457                        i += 2;
17458                    }
17459                    return Some(Ok(StrykeValue::struct_inst(Arc::new(
17460                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
17461                    ))));
17462                }
17463                "to_hash" => {
17464                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
17465                    if !args.is_empty() {
17466                        return Some(Err(StrykeError::runtime(
17467                            "struct to_hash takes no arguments",
17468                            line,
17469                        )));
17470                    }
17471                    let mut map = IndexMap::new();
17472                    let values = s.get_values();
17473                    for (i, field) in s.def.fields.iter().enumerate() {
17474                        map.insert(field.name.clone(), values[i].clone());
17475                    }
17476                    return Some(Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map)))));
17477                }
17478                "to_hash_rec" | "to_hash_deep" => {
17479                    // Like to_hash but recurse: nested struct/class/hash/
17480                    // array values become plain hashref/arrayref trees.
17481                    if !args.is_empty() {
17482                        return Some(Err(StrykeError::runtime(
17483                            "struct to_hash_rec takes no arguments",
17484                            line,
17485                        )));
17486                    }
17487                    return Some(Ok(self.deep_to_hash_value(receiver)));
17488                }
17489                "fields" => {
17490                    // Field list: $p->fields returns field names
17491                    if !args.is_empty() {
17492                        return Some(Err(StrykeError::runtime(
17493                            "struct fields takes no arguments",
17494                            line,
17495                        )));
17496                    }
17497                    let names: Vec<StrykeValue> = s
17498                        .def
17499                        .fields
17500                        .iter()
17501                        .map(|f| StrykeValue::string(f.name.clone()))
17502                        .collect();
17503                    return Some(Ok(StrykeValue::array(names)));
17504                }
17505                "clone" => {
17506                    // Clone: $p->clone deep copies
17507                    if !args.is_empty() {
17508                        return Some(Err(StrykeError::runtime(
17509                            "struct clone takes no arguments",
17510                            line,
17511                        )));
17512                    }
17513                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
17514                    return Some(Ok(StrykeValue::struct_inst(Arc::new(
17515                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
17516                    ))));
17517                }
17518                _ => {}
17519            }
17520            // User-defined struct method
17521            if let Some(m) = s.def.method(method) {
17522                let body = m.body.clone();
17523                let params = m.params.clone();
17524                // Build args: $self is the receiver, then the passed args
17525                let mut call_args = vec![receiver.clone()];
17526                call_args.extend(args.iter().cloned());
17527                return Some(
17528                    match self.call_struct_method(&body, &params, call_args, line) {
17529                        Ok(v) => Ok(v),
17530                        Err(FlowOrError::Error(e)) => Err(e),
17531                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17532                        Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
17533                            "unexpected control flow in struct method",
17534                            line,
17535                        )),
17536                    },
17537                );
17538            }
17539            return None;
17540        }
17541        // Class instance method dispatch
17542        if let Some(c) = receiver.as_class_inst() {
17543            // Collect all fields from inheritance chain (with visibility)
17544            let all_fields_full = self.collect_class_fields_full(&c.def);
17545            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
17546                .iter()
17547                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
17548                .collect();
17549
17550            // Field access: $obj->name or $obj->name(value)
17551            if let Some(idx) = all_fields_full
17552                .iter()
17553                .position(|(name, _, _, _, _)| name == method)
17554            {
17555                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
17556
17557                // Enforce field visibility
17558                match vis {
17559                    crate::ast::Visibility::Private => {
17560                        // Only accessible from within the owning class's methods
17561                        let caller_class = self
17562                            .scope
17563                            .get_scalar("self")
17564                            .as_class_inst()
17565                            .map(|ci| ci.def.name.clone());
17566                        if caller_class.as_deref() != Some(owner_class.as_str()) {
17567                            return Some(Err(StrykeError::runtime(
17568                                format!("field `{}` of class {} is private", method, owner_class),
17569                                line,
17570                            )));
17571                        }
17572                    }
17573                    crate::ast::Visibility::Protected => {
17574                        // Accessible from owning class or subclasses
17575                        let caller_class = self
17576                            .scope
17577                            .get_scalar("self")
17578                            .as_class_inst()
17579                            .map(|ci| ci.def.name.clone());
17580                        let allowed = caller_class.as_deref().is_some_and(|caller| {
17581                            caller == owner_class || self.class_inherits_from(caller, owner_class)
17582                        });
17583                        if !allowed {
17584                            return Some(Err(StrykeError::runtime(
17585                                format!("field `{}` of class {} is protected", method, owner_class),
17586                                line,
17587                            )));
17588                        }
17589                    }
17590                    crate::ast::Visibility::Public => {}
17591                }
17592
17593                match args.len() {
17594                    0 => {
17595                        return Some(Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF)));
17596                    }
17597                    1 => {
17598                        let new_val = args[0].clone();
17599                        if let Err(msg) = ty.check_value(&new_val) {
17600                            return Some(Err(StrykeError::type_error(
17601                                format!("class {} field `{}`: {}", c.def.name, method, msg),
17602                                line,
17603                            )));
17604                        }
17605                        c.set_field(idx, new_val.clone());
17606                        return Some(Ok(new_val));
17607                    }
17608                    _ => {
17609                        return Some(Err(StrykeError::runtime(
17610                            format!(
17611                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
17612                                method,
17613                                args.len()
17614                            ),
17615                            line,
17616                        )));
17617                    }
17618                }
17619            }
17620            // Built-in class methods (use all_fields for inheritance)
17621            match method {
17622                "with" => {
17623                    let mut new_values = c.get_values();
17624                    let mut i = 0;
17625                    while i + 1 < args.len() {
17626                        let k = args[i].to_string();
17627                        let v = args[i + 1].clone();
17628                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
17629                            let (_, _, ref ty) = all_fields[idx];
17630                            if let Err(msg) = ty.check_value(&v) {
17631                                return Some(Err(StrykeError::type_error(
17632                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
17633                                    line,
17634                                )));
17635                            }
17636                            new_values[idx] = v;
17637                        } else {
17638                            return Some(Err(StrykeError::runtime(
17639                                format!("class {}: unknown field `{}`", c.def.name, k),
17640                                line,
17641                            )));
17642                        }
17643                        i += 2;
17644                    }
17645                    return Some(Ok(StrykeValue::class_inst(Arc::new(
17646                        crate::value::ClassInstance::new_with_isa(
17647                            Arc::clone(&c.def),
17648                            new_values,
17649                            c.isa_chain.clone(),
17650                        ),
17651                    ))));
17652                }
17653                "to_hash" => {
17654                    if !args.is_empty() {
17655                        return Some(Err(StrykeError::runtime(
17656                            "class to_hash takes no arguments",
17657                            line,
17658                        )));
17659                    }
17660                    let mut map = IndexMap::new();
17661                    let values = c.get_values();
17662                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
17663                        if let Some(v) = values.get(i) {
17664                            map.insert(name.clone(), v.clone());
17665                        }
17666                    }
17667                    return Some(Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map)))));
17668                }
17669                "to_hash_rec" | "to_hash_deep" => {
17670                    // Recursive flatten: nested class/struct/hash/array
17671                    // values become plain hashref/arrayref trees, so the
17672                    // result is JSON-serializable end-to-end without any
17673                    // surviving ClassInstance/StructInstance leaves.
17674                    if !args.is_empty() {
17675                        return Some(Err(StrykeError::runtime(
17676                            "class to_hash_rec takes no arguments",
17677                            line,
17678                        )));
17679                    }
17680                    return Some(Ok(self.deep_to_hash_value(receiver)));
17681                }
17682                "fields" => {
17683                    if !args.is_empty() {
17684                        return Some(Err(StrykeError::runtime(
17685                            "class fields takes no arguments",
17686                            line,
17687                        )));
17688                    }
17689                    let names: Vec<StrykeValue> = all_fields
17690                        .iter()
17691                        .map(|(name, _, _)| StrykeValue::string(name.clone()))
17692                        .collect();
17693                    return Some(Ok(StrykeValue::array(names)));
17694                }
17695                "clone" => {
17696                    if !args.is_empty() {
17697                        return Some(Err(StrykeError::runtime(
17698                            "class clone takes no arguments",
17699                            line,
17700                        )));
17701                    }
17702                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
17703                    return Some(Ok(StrykeValue::class_inst(Arc::new(
17704                        crate::value::ClassInstance::new_with_isa(
17705                            Arc::clone(&c.def),
17706                            new_values,
17707                            c.isa_chain.clone(),
17708                        ),
17709                    ))));
17710                }
17711                "isa" => {
17712                    if args.len() != 1 {
17713                        return Some(Err(StrykeError::runtime("isa requires one argument", line)));
17714                    }
17715                    let class_name = args[0].to_string();
17716                    let is_a = c.isa(&class_name);
17717                    return Some(Ok(if is_a {
17718                        StrykeValue::integer(1)
17719                    } else {
17720                        StrykeValue::string(String::new())
17721                    }));
17722                }
17723                "does" => {
17724                    if args.len() != 1 {
17725                        return Some(Err(StrykeError::runtime(
17726                            "does requires one argument",
17727                            line,
17728                        )));
17729                    }
17730                    let trait_name = args[0].to_string();
17731                    let implements = c.def.implements.contains(&trait_name);
17732                    return Some(Ok(if implements {
17733                        StrykeValue::integer(1)
17734                    } else {
17735                        StrykeValue::string(String::new())
17736                    }));
17737                }
17738                "methods" => {
17739                    if !args.is_empty() {
17740                        return Some(Err(StrykeError::runtime(
17741                            "methods takes no arguments",
17742                            line,
17743                        )));
17744                    }
17745                    let mut names = Vec::new();
17746                    self.collect_class_method_names(&c.def, &mut names);
17747                    let values: Vec<StrykeValue> =
17748                        names.into_iter().map(StrykeValue::string).collect();
17749                    return Some(Ok(StrykeValue::array(values)));
17750                }
17751                "superclass" => {
17752                    if !args.is_empty() {
17753                        return Some(Err(StrykeError::runtime(
17754                            "superclass takes no arguments",
17755                            line,
17756                        )));
17757                    }
17758                    let parents: Vec<StrykeValue> = c
17759                        .def
17760                        .extends
17761                        .iter()
17762                        .map(|s| StrykeValue::string(s.clone()))
17763                        .collect();
17764                    return Some(Ok(StrykeValue::array(parents)));
17765                }
17766                "destroy" => {
17767                    // Explicit destructor call — runs DESTROY chain child-first
17768                    let destroy_chain = self.collect_destroy_chain(&c.def);
17769                    for (body, params) in &destroy_chain {
17770                        let call_args = vec![receiver.clone()];
17771                        match self.call_class_method(body, params, call_args, line) {
17772                            Ok(_) => {}
17773                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
17774                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
17775                            Err(_) => {}
17776                        }
17777                    }
17778                    return Some(Ok(StrykeValue::UNDEF));
17779                }
17780                _ => {}
17781            }
17782            // User-defined class method (search inheritance chain)
17783            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
17784                // Check visibility
17785                match m.visibility {
17786                    crate::ast::Visibility::Private => {
17787                        let caller_class = self
17788                            .scope
17789                            .get_scalar("self")
17790                            .as_class_inst()
17791                            .map(|ci| ci.def.name.clone());
17792                        if caller_class.as_deref() != Some(owner_class.as_str()) {
17793                            return Some(Err(StrykeError::runtime(
17794                                format!("method `{}` of class {} is private", method, owner_class),
17795                                line,
17796                            )));
17797                        }
17798                    }
17799                    crate::ast::Visibility::Protected => {
17800                        let caller_class = self
17801                            .scope
17802                            .get_scalar("self")
17803                            .as_class_inst()
17804                            .map(|ci| ci.def.name.clone());
17805                        let allowed = caller_class.as_deref().is_some_and(|caller| {
17806                            caller == owner_class.as_str()
17807                                || self.class_inherits_from(caller, owner_class)
17808                        });
17809                        if !allowed {
17810                            return Some(Err(StrykeError::runtime(
17811                                format!(
17812                                    "method `{}` of class {} is protected",
17813                                    method, owner_class
17814                                ),
17815                                line,
17816                            )));
17817                        }
17818                    }
17819                    crate::ast::Visibility::Public => {}
17820                }
17821                if let Some(ref body) = m.body {
17822                    let params = m.params.clone();
17823                    let mut call_args = vec![receiver.clone()];
17824                    call_args.extend(args.iter().cloned());
17825                    return Some(
17826                        match self.call_class_method(body, &params, call_args, line) {
17827                            Ok(v) => Ok(v),
17828                            Err(FlowOrError::Error(e)) => Err(e),
17829                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17830                            Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
17831                                "unexpected control flow in class method",
17832                                line,
17833                            )),
17834                        },
17835                    );
17836                }
17837            }
17838            return None;
17839        }
17840        if let Some(d) = receiver.as_dataframe() {
17841            return Some(self.dataframe_method(d, method, args, line));
17842        }
17843        if let Some(s) = crate::value::set_payload(receiver) {
17844            return Some(self.set_method(s, method, args, line));
17845        }
17846        if let Some(d) = receiver.as_deque() {
17847            return Some(self.deque_method(d, method, args, line));
17848        }
17849        if let Some(h) = receiver.as_heap_pq() {
17850            return Some(self.heap_method(h, method, args, line));
17851        }
17852        if let Some(p) = receiver.as_pipeline() {
17853            return Some(self.pipeline_method(p, method, args, line));
17854        }
17855        if let Some(c) = receiver.as_capture() {
17856            return Some(self.capture_method(c, method, args, line));
17857        }
17858        if let Some(p) = receiver.as_ppool() {
17859            return Some(self.ppool_method(p, method, args, line));
17860        }
17861        if let Some(b) = receiver.as_barrier() {
17862            return Some(self.barrier_method(b, method, args, line));
17863        }
17864        if let Some(g) = receiver.as_generator() {
17865            if method == "next" {
17866                if !args.is_empty() {
17867                    return Some(Err(StrykeError::runtime(
17868                        "generator->next takes no arguments",
17869                        line,
17870                    )));
17871                }
17872                return Some(self.generator_next(&g));
17873            }
17874            return None;
17875        }
17876        // `iter->next` mirrors `generator->next` shape: returns
17877        // `[value, more_flag]` where `more_flag` is 1 while items remain and 0
17878        // once the source is exhausted. Lets every `StrykeIterator` (ingest,
17879        // FsWalkIterator, etc.) be driven explicitly without forcing users to
17880        // pick between `for`-loop materialisation and pipe terminals.
17881        if receiver.is_iterator() {
17882            if method == "next" {
17883                if !args.is_empty() {
17884                    return Some(Err(StrykeError::runtime(
17885                        "iterator->next takes no arguments",
17886                        line,
17887                    )));
17888                }
17889                let it = receiver.clone().into_iterator();
17890                let (value, more) = match it.next_item() {
17891                    Some(v) => (v, 1i64),
17892                    None => (StrykeValue::UNDEF, 0i64),
17893                };
17894                let pair = vec![value, StrykeValue::integer(more)];
17895                return Some(Ok(StrykeValue::array_ref(Arc::new(RwLock::new(pair)))));
17896            }
17897            return None;
17898        }
17899        if let Some(arc) = receiver.as_atomic_arc() {
17900            let inner = arc.lock().clone();
17901            if let Some(d) = inner.as_deque() {
17902                return Some(self.deque_method(d, method, args, line));
17903            }
17904            if let Some(h) = inner.as_heap_pq() {
17905                return Some(self.heap_method(h, method, args, line));
17906            }
17907        }
17908        None
17909    }
17910
17911    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
17912    fn dataframe_method(
17913        &mut self,
17914        d: Arc<Mutex<PerlDataFrame>>,
17915        method: &str,
17916        args: &[StrykeValue],
17917        line: usize,
17918    ) -> StrykeResult<StrykeValue> {
17919        match method {
17920            "nrow" | "nrows" => {
17921                if !args.is_empty() {
17922                    return Err(StrykeError::runtime(
17923                        format!("dataframe {} takes no arguments", method),
17924                        line,
17925                    ));
17926                }
17927                Ok(StrykeValue::integer(d.lock().nrows() as i64))
17928            }
17929            "ncol" | "ncols" => {
17930                if !args.is_empty() {
17931                    return Err(StrykeError::runtime(
17932                        format!("dataframe {} takes no arguments", method),
17933                        line,
17934                    ));
17935                }
17936                Ok(StrykeValue::integer(d.lock().ncols() as i64))
17937            }
17938            "filter" => {
17939                if args.len() != 1 {
17940                    return Err(StrykeError::runtime(
17941                        "dataframe filter expects 1 argument (sub)",
17942                        line,
17943                    ));
17944                }
17945                let Some(sub) = args[0].as_code_ref() else {
17946                    return Err(StrykeError::runtime(
17947                        "dataframe filter expects a code reference",
17948                        line,
17949                    ));
17950                };
17951                let df_guard = d.lock();
17952                let n = df_guard.nrows();
17953                let mut keep = vec![false; n];
17954                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
17955                    let row = df_guard.row_hashref(r);
17956                    self.scope_push_hook();
17957                    self.scope.set_topic(row);
17958                    if let Some(ref env) = sub.closure_env {
17959                        self.scope.restore_capture(env);
17960                    }
17961                    let pass = match self.exec_block_no_scope(&sub.body) {
17962                        Ok(v) => v.is_true(),
17963                        Err(_) => false,
17964                    };
17965                    self.scope_pop_hook();
17966                    *row_keep = pass;
17967                }
17968                let columns = df_guard.columns.clone();
17969                let cols: Vec<Vec<StrykeValue>> = (0..df_guard.ncols())
17970                    .map(|i| {
17971                        let mut out = Vec::new();
17972                        for (r, pass_row) in keep.iter().enumerate().take(n) {
17973                            if *pass_row {
17974                                out.push(df_guard.cols[i][r].clone());
17975                            }
17976                        }
17977                        out
17978                    })
17979                    .collect();
17980                let group_by = df_guard.group_by.clone();
17981                drop(df_guard);
17982                let new_df = PerlDataFrame {
17983                    columns,
17984                    cols,
17985                    group_by,
17986                };
17987                Ok(StrykeValue::dataframe(Arc::new(Mutex::new(new_df))))
17988            }
17989            "group_by" => {
17990                if args.len() != 1 {
17991                    return Err(StrykeError::runtime(
17992                        "dataframe group_by expects 1 column name",
17993                        line,
17994                    ));
17995                }
17996                let key = args[0].to_string();
17997                let inner = d.lock();
17998                if inner.col_index(&key).is_none() {
17999                    return Err(StrykeError::runtime(
18000                        format!("dataframe group_by: unknown column \"{}\"", key),
18001                        line,
18002                    ));
18003                }
18004                let new_df = PerlDataFrame {
18005                    columns: inner.columns.clone(),
18006                    cols: inner.cols.clone(),
18007                    group_by: Some(key),
18008                };
18009                Ok(StrykeValue::dataframe(Arc::new(Mutex::new(new_df))))
18010            }
18011            "sum" => {
18012                if args.len() != 1 {
18013                    return Err(StrykeError::runtime(
18014                        "dataframe sum expects 1 column name",
18015                        line,
18016                    ));
18017                }
18018                let col_name = args[0].to_string();
18019                let inner = d.lock();
18020                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
18021                    StrykeError::runtime(
18022                        format!("dataframe sum: unknown column \"{}\"", col_name),
18023                        line,
18024                    )
18025                })?;
18026                match &inner.group_by {
18027                    Some(gcol) => {
18028                        let gi = inner.col_index(gcol).ok_or_else(|| {
18029                            StrykeError::runtime(
18030                                format!("dataframe sum: unknown group column \"{}\"", gcol),
18031                                line,
18032                            )
18033                        })?;
18034                        let mut acc: IndexMap<String, f64> = IndexMap::new();
18035                        for r in 0..inner.nrows() {
18036                            let k = inner.cols[gi][r].to_string();
18037                            let v = inner.cols[val_idx][r].to_number();
18038                            *acc.entry(k).or_insert(0.0) += v;
18039                        }
18040                        let keys: Vec<String> = acc.keys().cloned().collect();
18041                        let sums: Vec<f64> = acc.values().copied().collect();
18042                        let cols = vec![
18043                            keys.into_iter().map(StrykeValue::string).collect(),
18044                            sums.into_iter().map(StrykeValue::float).collect(),
18045                        ];
18046                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
18047                        let out = PerlDataFrame {
18048                            columns,
18049                            cols,
18050                            group_by: None,
18051                        };
18052                        Ok(StrykeValue::dataframe(Arc::new(Mutex::new(out))))
18053                    }
18054                    None => {
18055                        let total: f64 = (0..inner.nrows())
18056                            .map(|r| inner.cols[val_idx][r].to_number())
18057                            .sum();
18058                        Ok(StrykeValue::float(total))
18059                    }
18060                }
18061            }
18062            _ => Err(StrykeError::runtime(
18063                format!("Unknown method for dataframe: {}", method),
18064                line,
18065            )),
18066        }
18067    }
18068
18069    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
18070    fn set_method(
18071        &self,
18072        s: Arc<crate::value::PerlSet>,
18073        method: &str,
18074        args: &[StrykeValue],
18075        line: usize,
18076    ) -> StrykeResult<StrykeValue> {
18077        match method {
18078            "has" | "contains" | "member" => {
18079                if args.len() != 1 {
18080                    return Err(StrykeError::runtime(
18081                        "set->has expects one argument (element)",
18082                        line,
18083                    ));
18084                }
18085                let k = crate::value::set_member_key(&args[0]);
18086                Ok(StrykeValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
18087            }
18088            "size" | "len" | "count" => {
18089                if !args.is_empty() {
18090                    return Err(StrykeError::runtime("set->size takes no arguments", line));
18091                }
18092                Ok(StrykeValue::integer(s.len() as i64))
18093            }
18094            "values" | "list" | "elements" => {
18095                if !args.is_empty() {
18096                    return Err(StrykeError::runtime("set->values takes no arguments", line));
18097                }
18098                Ok(StrykeValue::array(s.values().cloned().collect()))
18099            }
18100            _ => Err(StrykeError::runtime(
18101                format!("Unknown method for set: {}", method),
18102                line,
18103            )),
18104        }
18105    }
18106
18107    fn deque_method(
18108        &mut self,
18109        d: Arc<Mutex<VecDeque<StrykeValue>>>,
18110        method: &str,
18111        args: &[StrykeValue],
18112        line: usize,
18113    ) -> StrykeResult<StrykeValue> {
18114        match method {
18115            "push_back" => {
18116                if args.len() != 1 {
18117                    return Err(StrykeError::runtime("push_back expects 1 argument", line));
18118                }
18119                d.lock().push_back(args[0].clone());
18120                Ok(StrykeValue::integer(d.lock().len() as i64))
18121            }
18122            "push_front" => {
18123                if args.len() != 1 {
18124                    return Err(StrykeError::runtime("push_front expects 1 argument", line));
18125                }
18126                d.lock().push_front(args[0].clone());
18127                Ok(StrykeValue::integer(d.lock().len() as i64))
18128            }
18129            "pop_back" => Ok(d.lock().pop_back().unwrap_or(StrykeValue::UNDEF)),
18130            "pop_front" => Ok(d.lock().pop_front().unwrap_or(StrykeValue::UNDEF)),
18131            "size" | "len" => Ok(StrykeValue::integer(d.lock().len() as i64)),
18132            _ => Err(StrykeError::runtime(
18133                format!("Unknown method for deque: {}", method),
18134                line,
18135            )),
18136        }
18137    }
18138
18139    fn heap_method(
18140        &mut self,
18141        h: Arc<Mutex<PerlHeap>>,
18142        method: &str,
18143        args: &[StrykeValue],
18144        line: usize,
18145    ) -> StrykeResult<StrykeValue> {
18146        match method {
18147            "push" => {
18148                if args.len() != 1 {
18149                    return Err(StrykeError::runtime("heap push expects 1 argument", line));
18150                }
18151                let mut g = h.lock();
18152                let n = g.items.len();
18153                g.items.push(args[0].clone());
18154                let cmp = g.cmp.clone();
18155                drop(g);
18156                let mut g = h.lock();
18157                self.heap_sift_up(&mut g.items, &cmp, n);
18158                Ok(StrykeValue::integer(g.items.len() as i64))
18159            }
18160            "pop" => {
18161                let mut g = h.lock();
18162                if g.items.is_empty() {
18163                    return Ok(StrykeValue::UNDEF);
18164                }
18165                let cmp = g.cmp.clone();
18166                let n = g.items.len();
18167                g.items.swap(0, n - 1);
18168                let v = g.items.pop().unwrap();
18169                if !g.items.is_empty() {
18170                    self.heap_sift_down(&mut g.items, &cmp, 0);
18171                }
18172                Ok(v)
18173            }
18174            "peek" => Ok(h
18175                .lock()
18176                .items
18177                .first()
18178                .cloned()
18179                .unwrap_or(StrykeValue::UNDEF)),
18180            _ => Err(StrykeError::runtime(
18181                format!("Unknown method for heap: {}", method),
18182                line,
18183            )),
18184        }
18185    }
18186
18187    fn ppool_method(
18188        &mut self,
18189        pool: PerlPpool,
18190        method: &str,
18191        args: &[StrykeValue],
18192        line: usize,
18193    ) -> StrykeResult<StrykeValue> {
18194        match method {
18195            "submit" => pool.submit(self, args, line),
18196            "collect" => {
18197                if !args.is_empty() {
18198                    return Err(StrykeError::runtime("collect() takes no arguments", line));
18199                }
18200                pool.collect(line)
18201            }
18202            _ => Err(StrykeError::runtime(
18203                format!("Unknown method for ppool: {}", method),
18204                line,
18205            )),
18206        }
18207    }
18208
18209    fn barrier_method(
18210        &self,
18211        barrier: PerlBarrier,
18212        method: &str,
18213        args: &[StrykeValue],
18214        line: usize,
18215    ) -> StrykeResult<StrykeValue> {
18216        match method {
18217            "wait" => {
18218                if !args.is_empty() {
18219                    return Err(StrykeError::runtime("wait() takes no arguments", line));
18220                }
18221                let _ = barrier.0.wait();
18222                Ok(StrykeValue::integer(1))
18223            }
18224            _ => Err(StrykeError::runtime(
18225                format!("Unknown method for barrier: {}", method),
18226                line,
18227            )),
18228        }
18229    }
18230
18231    fn capture_method(
18232        &self,
18233        c: Arc<CaptureResult>,
18234        method: &str,
18235        args: &[StrykeValue],
18236        line: usize,
18237    ) -> StrykeResult<StrykeValue> {
18238        if !args.is_empty() {
18239            return Err(StrykeError::runtime(
18240                format!("capture: {} takes no arguments", method),
18241                line,
18242            ));
18243        }
18244        match method {
18245            "stdout" => Ok(StrykeValue::string(c.stdout.clone())),
18246            "stderr" => Ok(StrykeValue::string(c.stderr.clone())),
18247            "exitcode" => Ok(StrykeValue::integer(c.exitcode)),
18248            "failed" => Ok(StrykeValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
18249            _ => Err(StrykeError::runtime(
18250                format!("Unknown method for capture: {}", method),
18251                line,
18252            )),
18253        }
18254    }
18255
18256    pub(crate) fn builtin_par_pipeline_stream(
18257        &mut self,
18258        args: &[StrykeValue],
18259        _line: usize,
18260    ) -> StrykeResult<StrykeValue> {
18261        let mut items = Vec::new();
18262        for v in args {
18263            if let Some(a) = v.as_array_vec() {
18264                items.extend(a);
18265            } else {
18266                items.push(v.clone());
18267            }
18268        }
18269        Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
18270            source: items,
18271            ops: Vec::new(),
18272            has_scalar_terminal: false,
18273            par_stream: true,
18274            streaming: false,
18275            streaming_workers: 0,
18276            streaming_buffer: 256,
18277        }))))
18278    }
18279
18280    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
18281    /// that wires ops through bounded channels on `collect()`.
18282    pub(crate) fn builtin_par_pipeline_stream_new(
18283        &mut self,
18284        args: &[StrykeValue],
18285        _line: usize,
18286    ) -> StrykeResult<StrykeValue> {
18287        let mut items = Vec::new();
18288        let mut workers: usize = 0;
18289        let mut buffer: usize = 256;
18290        // Separate list items from keyword args (workers => N, buffer => N).
18291        let mut i = 0;
18292        while i < args.len() {
18293            let s = args[i].to_string();
18294            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
18295                let val = args[i + 1].to_int().max(1) as usize;
18296                if s == "workers" {
18297                    workers = val;
18298                } else {
18299                    buffer = val;
18300                }
18301                i += 2;
18302            } else if let Some(a) = args[i].as_array_vec() {
18303                items.extend(a);
18304                i += 1;
18305            } else {
18306                items.push(args[i].clone());
18307                i += 1;
18308            }
18309        }
18310        Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
18311            source: items,
18312            ops: Vec::new(),
18313            has_scalar_terminal: false,
18314            par_stream: false,
18315            streaming: true,
18316            streaming_workers: workers,
18317            streaming_buffer: buffer,
18318        }))))
18319    }
18320
18321    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
18322    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<StrykeSub> {
18323        let line = 1usize;
18324        let body = vec![Statement {
18325            label: None,
18326            kind: StmtKind::Expression(Expr {
18327                kind: ExprKind::BinOp {
18328                    left: Box::new(Expr {
18329                        kind: ExprKind::ScalarVar("_".into()),
18330                        line,
18331                    }),
18332                    op: BinOp::Mul,
18333                    right: Box::new(Expr {
18334                        kind: ExprKind::Integer(k),
18335                        line,
18336                    }),
18337                },
18338                line,
18339            }),
18340            line,
18341        }];
18342        Arc::new(StrykeSub {
18343            name: "__pipeline_int_mul__".into(),
18344            params: vec![],
18345            body,
18346            closure_env: None,
18347            prototype: None,
18348            fib_like: None,
18349        })
18350    }
18351
18352    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<StrykeSub> {
18353        let captured = self.scope.capture();
18354        Arc::new(StrykeSub {
18355            name: "__ANON__".into(),
18356            params: vec![],
18357            body: block.clone(),
18358            closure_env: Some(captured),
18359            prototype: None,
18360            fib_like: None,
18361        })
18362    }
18363
18364    pub(crate) fn builtin_collect_execute(
18365        &mut self,
18366        args: &[StrykeValue],
18367        line: usize,
18368    ) -> StrykeResult<StrykeValue> {
18369        if args.is_empty() {
18370            return Err(StrykeError::runtime(
18371                "collect() expects at least one argument",
18372                line,
18373            ));
18374        }
18375        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
18376        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
18377        if args.len() == 1 {
18378            if let Some(p) = args[0].as_pipeline() {
18379                return self.pipeline_collect(&p, line);
18380            }
18381            return Ok(StrykeValue::array(args[0].to_list()));
18382        }
18383        Ok(StrykeValue::array(args.to_vec()))
18384    }
18385
18386    pub(crate) fn pipeline_push(
18387        &self,
18388        p: &Arc<Mutex<PipelineInner>>,
18389        op: PipelineOp,
18390        line: usize,
18391    ) -> StrykeResult<()> {
18392        let mut g = p.lock();
18393        if g.has_scalar_terminal {
18394            return Err(StrykeError::runtime(
18395                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
18396                line,
18397            ));
18398        }
18399        if matches!(
18400            &op,
18401            PipelineOp::PReduce { .. }
18402                | PipelineOp::PReduceInit { .. }
18403                | PipelineOp::PMapReduce { .. }
18404        ) {
18405            g.has_scalar_terminal = true;
18406        }
18407        g.ops.push(op);
18408        Ok(())
18409    }
18410
18411    fn pipeline_parse_sub_progress(
18412        args: &[StrykeValue],
18413        line: usize,
18414        name: &str,
18415    ) -> StrykeResult<(Arc<StrykeSub>, bool)> {
18416        if args.is_empty() {
18417            return Err(StrykeError::runtime(
18418                format!("pipeline {}: expects at least 1 argument (code ref)", name),
18419                line,
18420            ));
18421        }
18422        let Some(sub) = args[0].as_code_ref() else {
18423            return Err(StrykeError::runtime(
18424                format!("pipeline {}: first argument must be a code reference", name),
18425                line,
18426            ));
18427        };
18428        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
18429        if args.len() > 2 {
18430            return Err(StrykeError::runtime(
18431                format!(
18432                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
18433                    name
18434                ),
18435                line,
18436            ));
18437        }
18438        Ok((sub, progress))
18439    }
18440
18441    pub(crate) fn pipeline_method(
18442        &mut self,
18443        p: Arc<Mutex<PipelineInner>>,
18444        method: &str,
18445        args: &[StrykeValue],
18446        line: usize,
18447    ) -> StrykeResult<StrykeValue> {
18448        match method {
18449            "filter" | "f" | "grep" => {
18450                if args.len() != 1 {
18451                    return Err(StrykeError::runtime(
18452                        "pipeline filter/grep expects 1 argument (sub)",
18453                        line,
18454                    ));
18455                }
18456                let Some(sub) = args[0].as_code_ref() else {
18457                    return Err(StrykeError::runtime(
18458                        "pipeline filter/grep expects a code reference",
18459                        line,
18460                    ));
18461                };
18462                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
18463                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18464            }
18465            "map" => {
18466                if args.len() != 1 {
18467                    return Err(StrykeError::runtime(
18468                        "pipeline map expects 1 argument (sub)",
18469                        line,
18470                    ));
18471                }
18472                let Some(sub) = args[0].as_code_ref() else {
18473                    return Err(StrykeError::runtime(
18474                        "pipeline map expects a code reference",
18475                        line,
18476                    ));
18477                };
18478                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
18479                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18480            }
18481            "tap" | "peek" => {
18482                if args.len() != 1 {
18483                    return Err(StrykeError::runtime(
18484                        "pipeline tap/peek expects 1 argument (sub)",
18485                        line,
18486                    ));
18487                }
18488                let Some(sub) = args[0].as_code_ref() else {
18489                    return Err(StrykeError::runtime(
18490                        "pipeline tap/peek expects a code reference",
18491                        line,
18492                    ));
18493                };
18494                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
18495                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18496            }
18497            "take" => {
18498                if args.len() != 1 {
18499                    return Err(StrykeError::runtime(
18500                        "pipeline take expects 1 argument",
18501                        line,
18502                    ));
18503                }
18504                let n = args[0].to_int();
18505                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
18506                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18507            }
18508            "pmap" => {
18509                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
18510                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
18511                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18512            }
18513            "pgrep" => {
18514                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
18515                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
18516                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18517            }
18518            "pfor" => {
18519                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
18520                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
18521                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18522            }
18523            "pmap_chunked" => {
18524                if args.len() < 2 {
18525                    return Err(StrykeError::runtime(
18526                        "pipeline pmap_chunked expects chunk size and a code reference",
18527                        line,
18528                    ));
18529                }
18530                let chunk = args[0].to_int().max(1);
18531                let Some(sub) = args[1].as_code_ref() else {
18532                    return Err(StrykeError::runtime(
18533                        "pipeline pmap_chunked: second argument must be a code reference",
18534                        line,
18535                    ));
18536                };
18537                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
18538                if args.len() > 3 {
18539                    return Err(StrykeError::runtime(
18540                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
18541                        line,
18542                    ));
18543                }
18544                self.pipeline_push(
18545                    &p,
18546                    PipelineOp::PMapChunked {
18547                        chunk,
18548                        sub,
18549                        progress,
18550                    },
18551                    line,
18552                )?;
18553                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18554            }
18555            "psort" => {
18556                let (cmp, progress) = match args.len() {
18557                    0 => (None, false),
18558                    1 => {
18559                        if let Some(s) = args[0].as_code_ref() {
18560                            (Some(s), false)
18561                        } else {
18562                            (None, args[0].is_true())
18563                        }
18564                    }
18565                    2 => {
18566                        let Some(s) = args[0].as_code_ref() else {
18567                            return Err(StrykeError::runtime(
18568                                "pipeline psort: with two arguments, the first must be a comparator sub",
18569                                line,
18570                            ));
18571                        };
18572                        (Some(s), args[1].is_true())
18573                    }
18574                    _ => {
18575                        return Err(StrykeError::runtime(
18576                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
18577                            line,
18578                        ));
18579                    }
18580                };
18581                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
18582                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18583            }
18584            "pcache" => {
18585                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
18586                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
18587                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18588            }
18589            "preduce" => {
18590                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
18591                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
18592                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18593            }
18594            "preduce_init" => {
18595                if args.len() < 2 {
18596                    return Err(StrykeError::runtime(
18597                        "pipeline preduce_init expects init value and a code reference",
18598                        line,
18599                    ));
18600                }
18601                let init = args[0].clone();
18602                let Some(sub) = args[1].as_code_ref() else {
18603                    return Err(StrykeError::runtime(
18604                        "pipeline preduce_init: second argument must be a code reference",
18605                        line,
18606                    ));
18607                };
18608                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
18609                if args.len() > 3 {
18610                    return Err(StrykeError::runtime(
18611                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
18612                        line,
18613                    ));
18614                }
18615                self.pipeline_push(
18616                    &p,
18617                    PipelineOp::PReduceInit {
18618                        init,
18619                        sub,
18620                        progress,
18621                    },
18622                    line,
18623                )?;
18624                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18625            }
18626            "pmap_reduce" => {
18627                if args.len() < 2 {
18628                    return Err(StrykeError::runtime(
18629                        "pipeline pmap_reduce expects map sub and reduce sub",
18630                        line,
18631                    ));
18632                }
18633                let Some(map) = args[0].as_code_ref() else {
18634                    return Err(StrykeError::runtime(
18635                        "pipeline pmap_reduce: first argument must be a code reference (map)",
18636                        line,
18637                    ));
18638                };
18639                let Some(reduce) = args[1].as_code_ref() else {
18640                    return Err(StrykeError::runtime(
18641                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
18642                        line,
18643                    ));
18644                };
18645                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
18646                if args.len() > 3 {
18647                    return Err(StrykeError::runtime(
18648                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
18649                        line,
18650                    ));
18651                }
18652                self.pipeline_push(
18653                    &p,
18654                    PipelineOp::PMapReduce {
18655                        map,
18656                        reduce,
18657                        progress,
18658                    },
18659                    line,
18660                )?;
18661                Ok(StrykeValue::pipeline(Arc::clone(&p)))
18662            }
18663            "collect" => {
18664                if !args.is_empty() {
18665                    return Err(StrykeError::runtime(
18666                        "pipeline collect takes no arguments",
18667                        line,
18668                    ));
18669                }
18670                self.pipeline_collect(&p, line)
18671            }
18672            _ => {
18673                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
18674                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
18675                if let Some(sub) = self.resolve_sub_by_name(method) {
18676                    if !args.is_empty() {
18677                        return Err(StrykeError::runtime(
18678                            format!(
18679                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
18680                                method
18681                            ),
18682                            line,
18683                        ));
18684                    }
18685                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
18686                    Ok(StrykeValue::pipeline(Arc::clone(&p)))
18687                } else {
18688                    Err(StrykeError::runtime(
18689                        format!("Unknown method for pipeline: {}", method),
18690                        line,
18691                    ))
18692                }
18693            }
18694        }
18695    }
18696
18697    fn pipeline_parallel_map(
18698        &mut self,
18699        items: Vec<StrykeValue>,
18700        sub: &Arc<StrykeSub>,
18701        progress: bool,
18702    ) -> Vec<StrykeValue> {
18703        let subs = self.subs.clone();
18704        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18705        let pmap_progress = PmapProgress::new(progress, items.len());
18706        let results: Vec<StrykeValue> = items
18707            .into_par_iter()
18708            .map(|item| {
18709                let mut local_interp = VMHelper::new();
18710                local_interp.subs = subs.clone();
18711                local_interp.scope.restore_capture(&scope_capture);
18712                local_interp
18713                    .scope
18714                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18715                local_interp.enable_parallel_guard();
18716                local_interp.scope.set_topic(item);
18717                local_interp.scope_push_hook();
18718                let val = match local_interp.exec_block_no_scope(&sub.body) {
18719                    Ok(val) => val,
18720                    Err(_) => StrykeValue::UNDEF,
18721                };
18722                local_interp.scope_pop_hook();
18723                pmap_progress.tick();
18724                val
18725            })
18726            .collect();
18727        pmap_progress.finish();
18728        results
18729    }
18730
18731    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
18732    fn pipeline_par_stream_filter(
18733        &mut self,
18734        items: Vec<StrykeValue>,
18735        sub: &Arc<StrykeSub>,
18736    ) -> Vec<StrykeValue> {
18737        if items.is_empty() {
18738            return items;
18739        }
18740        let subs = self.subs.clone();
18741        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18742        let indexed: Vec<(usize, StrykeValue)> = items.into_iter().enumerate().collect();
18743        let mut kept: Vec<(usize, StrykeValue)> = indexed
18744            .into_par_iter()
18745            .filter_map(|(i, item)| {
18746                let mut local_interp = VMHelper::new();
18747                local_interp.subs = subs.clone();
18748                local_interp.scope.restore_capture(&scope_capture);
18749                local_interp
18750                    .scope
18751                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18752                local_interp.enable_parallel_guard();
18753                local_interp.scope.set_topic(item.clone());
18754                local_interp.scope_push_hook();
18755                let keep = match local_interp.exec_block_no_scope(&sub.body) {
18756                    Ok(val) => val.is_true(),
18757                    Err(_) => false,
18758                };
18759                local_interp.scope_pop_hook();
18760                if keep {
18761                    Some((i, item))
18762                } else {
18763                    None
18764                }
18765            })
18766            .collect();
18767        kept.sort_by_key(|(i, _)| *i);
18768        kept.into_iter().map(|(_, x)| x).collect()
18769    }
18770
18771    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
18772    fn pipeline_par_stream_map(
18773        &mut self,
18774        items: Vec<StrykeValue>,
18775        sub: &Arc<StrykeSub>,
18776    ) -> Vec<StrykeValue> {
18777        if items.is_empty() {
18778            return items;
18779        }
18780        let subs = self.subs.clone();
18781        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18782        let indexed: Vec<(usize, StrykeValue)> = items.into_iter().enumerate().collect();
18783        let mut mapped: Vec<(usize, StrykeValue)> = indexed
18784            .into_par_iter()
18785            .map(|(i, item)| {
18786                let mut local_interp = VMHelper::new();
18787                local_interp.subs = subs.clone();
18788                local_interp.scope.restore_capture(&scope_capture);
18789                local_interp
18790                    .scope
18791                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18792                local_interp.enable_parallel_guard();
18793                local_interp.scope.set_topic(item);
18794                local_interp.scope_push_hook();
18795                let val = match local_interp.exec_block_no_scope(&sub.body) {
18796                    Ok(val) => val,
18797                    Err(_) => StrykeValue::UNDEF,
18798                };
18799                local_interp.scope_pop_hook();
18800                (i, val)
18801            })
18802            .collect();
18803        mapped.sort_by_key(|(i, _)| *i);
18804        mapped.into_iter().map(|(_, x)| x).collect()
18805    }
18806
18807    fn pipeline_collect(
18808        &mut self,
18809        p: &Arc<Mutex<PipelineInner>>,
18810        line: usize,
18811    ) -> StrykeResult<StrykeValue> {
18812        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
18813            let g = p.lock();
18814            (
18815                g.source.clone(),
18816                g.ops.clone(),
18817                g.par_stream,
18818                g.streaming,
18819                g.streaming_workers,
18820                g.streaming_buffer,
18821            )
18822        };
18823        if streaming {
18824            return self.pipeline_collect_streaming(
18825                v,
18826                &ops,
18827                streaming_workers,
18828                streaming_buffer,
18829                line,
18830            );
18831        }
18832        for op in ops {
18833            match op {
18834                PipelineOp::Filter(sub) => {
18835                    if par_stream {
18836                        v = self.pipeline_par_stream_filter(v, &sub);
18837                    } else {
18838                        let mut out = Vec::new();
18839                        for item in v {
18840                            self.scope_push_hook();
18841                            self.scope.set_topic(item.clone());
18842                            if let Some(ref env) = sub.closure_env {
18843                                self.scope.restore_capture(env);
18844                            }
18845                            let keep = match self.exec_block_no_scope(&sub.body) {
18846                                Ok(val) => val.is_true(),
18847                                Err(_) => false,
18848                            };
18849                            self.scope_pop_hook();
18850                            if keep {
18851                                out.push(item);
18852                            }
18853                        }
18854                        v = out;
18855                    }
18856                }
18857                PipelineOp::Map(sub) => {
18858                    if par_stream {
18859                        v = self.pipeline_par_stream_map(v, &sub);
18860                    } else {
18861                        let mut out = Vec::new();
18862                        for item in v {
18863                            self.scope_push_hook();
18864                            self.scope.set_topic(item);
18865                            if let Some(ref env) = sub.closure_env {
18866                                self.scope.restore_capture(env);
18867                            }
18868                            let mapped = match self.exec_block_no_scope(&sub.body) {
18869                                Ok(val) => val,
18870                                Err(_) => StrykeValue::UNDEF,
18871                            };
18872                            self.scope_pop_hook();
18873                            out.push(mapped);
18874                        }
18875                        v = out;
18876                    }
18877                }
18878                PipelineOp::Tap(sub) => {
18879                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
18880                        Ok(_) => {}
18881                        Err(FlowOrError::Error(e)) => return Err(e),
18882                        Err(FlowOrError::Flow(_)) => {
18883                            return Err(StrykeError::runtime(
18884                                "tap: unsupported control flow in block",
18885                                line,
18886                            ));
18887                        }
18888                    }
18889                }
18890                PipelineOp::Take(n) => {
18891                    let n = n.max(0) as usize;
18892                    if v.len() > n {
18893                        v.truncate(n);
18894                    }
18895                }
18896                PipelineOp::PMap { sub, progress } => {
18897                    v = self.pipeline_parallel_map(v, &sub, progress);
18898                }
18899                PipelineOp::PGrep { sub, progress } => {
18900                    let subs = self.subs.clone();
18901                    let (scope_capture, atomic_arrays, atomic_hashes) =
18902                        self.scope.capture_with_atomics();
18903                    let pmap_progress = PmapProgress::new(progress, v.len());
18904                    v = v
18905                        .into_par_iter()
18906                        .filter_map(|item| {
18907                            let mut local_interp = VMHelper::new();
18908                            local_interp.subs = subs.clone();
18909                            local_interp.scope.restore_capture(&scope_capture);
18910                            local_interp
18911                                .scope
18912                                .restore_atomics(&atomic_arrays, &atomic_hashes);
18913                            local_interp.enable_parallel_guard();
18914                            local_interp.scope.set_topic(item.clone());
18915                            local_interp.scope_push_hook();
18916                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
18917                                Ok(val) => val.is_true(),
18918                                Err(_) => false,
18919                            };
18920                            local_interp.scope_pop_hook();
18921                            pmap_progress.tick();
18922                            if keep {
18923                                Some(item)
18924                            } else {
18925                                None
18926                            }
18927                        })
18928                        .collect();
18929                    pmap_progress.finish();
18930                }
18931                PipelineOp::PFor { sub, progress } => {
18932                    let subs = self.subs.clone();
18933                    let (scope_capture, atomic_arrays, atomic_hashes) =
18934                        self.scope.capture_with_atomics();
18935                    let pmap_progress = PmapProgress::new(progress, v.len());
18936                    let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
18937                    v.clone().into_par_iter().for_each(|item| {
18938                        if first_err.lock().is_some() {
18939                            return;
18940                        }
18941                        let mut local_interp = VMHelper::new();
18942                        local_interp.subs = subs.clone();
18943                        local_interp.scope.restore_capture(&scope_capture);
18944                        local_interp
18945                            .scope
18946                            .restore_atomics(&atomic_arrays, &atomic_hashes);
18947                        local_interp.enable_parallel_guard();
18948                        local_interp.scope.set_topic(item);
18949                        local_interp.scope_push_hook();
18950                        match local_interp.exec_block_no_scope(&sub.body) {
18951                            Ok(_) => {}
18952                            Err(e) => {
18953                                let stryke = match e {
18954                                    FlowOrError::Error(stryke) => stryke,
18955                                    FlowOrError::Flow(_) => StrykeError::runtime(
18956                                        "return/last/next/redo not supported inside pipeline pfor block",
18957                                        line,
18958                                    ),
18959                                };
18960                                let mut g = first_err.lock();
18961                                if g.is_none() {
18962                                    *g = Some(stryke);
18963                                }
18964                            }
18965                        }
18966                        local_interp.scope_pop_hook();
18967                        pmap_progress.tick();
18968                    });
18969                    pmap_progress.finish();
18970                    let pfor_err = first_err.lock().take();
18971                    if let Some(e) = pfor_err {
18972                        return Err(e);
18973                    }
18974                }
18975                PipelineOp::PMapChunked {
18976                    chunk,
18977                    sub,
18978                    progress,
18979                } => {
18980                    let chunk_n = chunk.max(1) as usize;
18981                    let subs = self.subs.clone();
18982                    let (scope_capture, atomic_arrays, atomic_hashes) =
18983                        self.scope.capture_with_atomics();
18984                    let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = v
18985                        .chunks(chunk_n)
18986                        .enumerate()
18987                        .map(|(i, c)| (i, c.to_vec()))
18988                        .collect();
18989                    let n_chunks = indexed_chunks.len();
18990                    let pmap_progress = PmapProgress::new(progress, n_chunks);
18991                    let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
18992                        .into_par_iter()
18993                        .map(|(chunk_idx, chunk)| {
18994                            let mut local_interp = VMHelper::new();
18995                            local_interp.subs = subs.clone();
18996                            local_interp.scope.restore_capture(&scope_capture);
18997                            local_interp
18998                                .scope
18999                                .restore_atomics(&atomic_arrays, &atomic_hashes);
19000                            local_interp.enable_parallel_guard();
19001                            let mut out = Vec::with_capacity(chunk.len());
19002                            for item in chunk {
19003                                local_interp.scope.set_topic(item);
19004                                local_interp.scope_push_hook();
19005                                match local_interp.exec_block_no_scope(&sub.body) {
19006                                    Ok(val) => {
19007                                        local_interp.scope_pop_hook();
19008                                        out.push(val);
19009                                    }
19010                                    Err(_) => {
19011                                        local_interp.scope_pop_hook();
19012                                        out.push(StrykeValue::UNDEF);
19013                                    }
19014                                }
19015                            }
19016                            pmap_progress.tick();
19017                            (chunk_idx, out)
19018                        })
19019                        .collect();
19020                    pmap_progress.finish();
19021                    chunk_results.sort_by_key(|(i, _)| *i);
19022                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
19023                }
19024                PipelineOp::PSort { cmp, progress } => {
19025                    let pmap_progress = PmapProgress::new(progress, 2);
19026                    pmap_progress.tick();
19027                    match cmp {
19028                        Some(cmp_block) => {
19029                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
19030                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
19031                            } else {
19032                                let subs = self.subs.clone();
19033                                let scope_capture = self.scope.capture();
19034                                v.par_sort_by(|a, b| {
19035                                    let mut local_interp = VMHelper::new();
19036                                    local_interp.subs = subs.clone();
19037                                    local_interp.scope.restore_capture(&scope_capture);
19038                                    local_interp.enable_parallel_guard();
19039                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
19040                                    local_interp.scope_push_hook();
19041                                    let ord =
19042                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
19043                                            Ok(v) => {
19044                                                let n = v.to_int();
19045                                                if n < 0 {
19046                                                    std::cmp::Ordering::Less
19047                                                } else if n > 0 {
19048                                                    std::cmp::Ordering::Greater
19049                                                } else {
19050                                                    std::cmp::Ordering::Equal
19051                                                }
19052                                            }
19053                                            Err(_) => std::cmp::Ordering::Equal,
19054                                        };
19055                                    local_interp.scope_pop_hook();
19056                                    ord
19057                                });
19058                            }
19059                        }
19060                        None => {
19061                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
19062                        }
19063                    }
19064                    pmap_progress.tick();
19065                    pmap_progress.finish();
19066                }
19067                PipelineOp::PCache { sub, progress } => {
19068                    let subs = self.subs.clone();
19069                    let scope_capture = self.scope.capture();
19070                    let cache = &*crate::pcache::GLOBAL_PCACHE;
19071                    let pmap_progress = PmapProgress::new(progress, v.len());
19072                    v = v
19073                        .into_par_iter()
19074                        .map(|item| {
19075                            let k = crate::pcache::cache_key(&item);
19076                            if let Some(cached) = cache.get(&k) {
19077                                pmap_progress.tick();
19078                                return cached.clone();
19079                            }
19080                            let mut local_interp = VMHelper::new();
19081                            local_interp.subs = subs.clone();
19082                            local_interp.scope.restore_capture(&scope_capture);
19083                            local_interp.enable_parallel_guard();
19084                            local_interp.scope.set_topic(item.clone());
19085                            local_interp.scope_push_hook();
19086                            let val = match local_interp.exec_block_no_scope(&sub.body) {
19087                                Ok(v) => v,
19088                                Err(_) => StrykeValue::UNDEF,
19089                            };
19090                            local_interp.scope_pop_hook();
19091                            cache.insert(k, val.clone());
19092                            pmap_progress.tick();
19093                            val
19094                        })
19095                        .collect();
19096                    pmap_progress.finish();
19097                }
19098                PipelineOp::PReduce { sub, progress } => {
19099                    if v.is_empty() {
19100                        return Ok(StrykeValue::UNDEF);
19101                    }
19102                    if v.len() == 1 {
19103                        return Ok(v.into_iter().next().unwrap());
19104                    }
19105                    let block = sub.body.clone();
19106                    let subs = self.subs.clone();
19107                    let scope_capture = self.scope.capture();
19108                    let pmap_progress = PmapProgress::new(progress, v.len());
19109                    let result = v
19110                        .into_par_iter()
19111                        .map(|x| {
19112                            pmap_progress.tick();
19113                            x
19114                        })
19115                        .reduce_with(|a, b| {
19116                            let mut local_interp = VMHelper::new();
19117                            local_interp.subs = subs.clone();
19118                            local_interp.scope.restore_capture(&scope_capture);
19119                            local_interp.enable_parallel_guard();
19120                            local_interp.scope.set_sort_pair(a, b);
19121                            match local_interp.exec_block(&block) {
19122                                Ok(val) => val,
19123                                Err(_) => StrykeValue::UNDEF,
19124                            }
19125                        });
19126                    pmap_progress.finish();
19127                    return Ok(result.unwrap_or(StrykeValue::UNDEF));
19128                }
19129                PipelineOp::PReduceInit {
19130                    init,
19131                    sub,
19132                    progress,
19133                } => {
19134                    if v.is_empty() {
19135                        return Ok(init);
19136                    }
19137                    let block = sub.body.clone();
19138                    let subs = self.subs.clone();
19139                    let scope_capture = self.scope.capture();
19140                    let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
19141                    if v.len() == 1 {
19142                        return Ok(fold_preduce_init_step(
19143                            &subs,
19144                            cap,
19145                            &block,
19146                            preduce_init_fold_identity(&init),
19147                            v.into_iter().next().unwrap(),
19148                        ));
19149                    }
19150                    let pmap_progress = PmapProgress::new(progress, v.len());
19151                    let result = v
19152                        .into_par_iter()
19153                        .fold(
19154                            || preduce_init_fold_identity(&init),
19155                            |acc, item| {
19156                                pmap_progress.tick();
19157                                fold_preduce_init_step(&subs, cap, &block, acc, item)
19158                            },
19159                        )
19160                        .reduce(
19161                            || preduce_init_fold_identity(&init),
19162                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
19163                        );
19164                    pmap_progress.finish();
19165                    return Ok(result);
19166                }
19167                PipelineOp::PMapReduce {
19168                    map,
19169                    reduce,
19170                    progress,
19171                } => {
19172                    if v.is_empty() {
19173                        return Ok(StrykeValue::UNDEF);
19174                    }
19175                    let map_block = map.body.clone();
19176                    let reduce_block = reduce.body.clone();
19177                    let subs = self.subs.clone();
19178                    let scope_capture = self.scope.capture();
19179                    if v.len() == 1 {
19180                        let mut local_interp = VMHelper::new();
19181                        local_interp.subs = subs.clone();
19182                        local_interp.scope.restore_capture(&scope_capture);
19183                        local_interp.scope.set_topic(v[0].clone());
19184                        return match local_interp.exec_block_no_scope(&map_block) {
19185                            Ok(val) => Ok(val),
19186                            Err(_) => Ok(StrykeValue::UNDEF),
19187                        };
19188                    }
19189                    let pmap_progress = PmapProgress::new(progress, v.len());
19190                    let result = v
19191                        .into_par_iter()
19192                        .map(|item| {
19193                            let mut local_interp = VMHelper::new();
19194                            local_interp.subs = subs.clone();
19195                            local_interp.scope.restore_capture(&scope_capture);
19196                            local_interp.scope.set_topic(item);
19197                            let val = match local_interp.exec_block_no_scope(&map_block) {
19198                                Ok(val) => val,
19199                                Err(_) => StrykeValue::UNDEF,
19200                            };
19201                            pmap_progress.tick();
19202                            val
19203                        })
19204                        .reduce_with(|a, b| {
19205                            let mut local_interp = VMHelper::new();
19206                            local_interp.subs = subs.clone();
19207                            local_interp.scope.restore_capture(&scope_capture);
19208                            local_interp.scope.set_sort_pair(a, b);
19209                            match local_interp.exec_block_no_scope(&reduce_block) {
19210                                Ok(val) => val,
19211                                Err(_) => StrykeValue::UNDEF,
19212                            }
19213                        });
19214                    pmap_progress.finish();
19215                    return Ok(result.unwrap_or(StrykeValue::UNDEF));
19216                }
19217            }
19218        }
19219        Ok(StrykeValue::array(v))
19220    }
19221
19222    /// Streaming collect: wire pipeline ops through bounded channels so items flow
19223    /// between stages concurrently.  Order is **not** preserved.
19224    fn pipeline_collect_streaming(
19225        &mut self,
19226        source: Vec<StrykeValue>,
19227        ops: &[PipelineOp],
19228        workers_per_stage: usize,
19229        buffer: usize,
19230        line: usize,
19231    ) -> StrykeResult<StrykeValue> {
19232        use crossbeam::channel::{bounded, Receiver, Sender};
19233
19234        // Validate: reject ops that require all items (can't stream).
19235        for op in ops {
19236            match op {
19237                PipelineOp::PSort { .. }
19238                | PipelineOp::PReduce { .. }
19239                | PipelineOp::PReduceInit { .. }
19240                | PipelineOp::PMapReduce { .. }
19241                | PipelineOp::PMapChunked { .. } => {
19242                    return Err(StrykeError::runtime(
19243                        format!(
19244                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
19245                            std::mem::discriminant(op)
19246                        ),
19247                        line,
19248                    ));
19249                }
19250                _ => {}
19251            }
19252        }
19253
19254        // Filter out non-streamable ops and collect streamable ones.
19255        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
19256        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
19257        if streamable_ops.is_empty() {
19258            return Ok(StrykeValue::array(source));
19259        }
19260
19261        let n_stages = streamable_ops.len();
19262        let wn = if workers_per_stage > 0 {
19263            workers_per_stage
19264        } else {
19265            self.parallel_thread_count()
19266        };
19267        let subs = self.subs.clone();
19268        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
19269
19270        // Build channels: one between each pair of stages, plus one for output.
19271        // channel[0]: source → stage 0
19272        // channel[i]: stage i-1 → stage i
19273        // channel[n_stages]: stage n_stages-1 → collector
19274        let mut channels: Vec<(Sender<StrykeValue>, Receiver<StrykeValue>)> =
19275            (0..=n_stages).map(|_| bounded(buffer)).collect();
19276
19277        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
19278        let take_done: Arc<std::sync::atomic::AtomicBool> =
19279            Arc::new(std::sync::atomic::AtomicBool::new(false));
19280
19281        // Collect senders/receivers for each stage.
19282        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
19283        let source_tx = channels[0].0.clone();
19284        let result_rx = channels[n_stages].1.clone();
19285        let results: Arc<Mutex<Vec<StrykeValue>>> = Arc::new(Mutex::new(Vec::new()));
19286
19287        std::thread::scope(|scope| {
19288            // Collector thread: drain results concurrently to avoid deadlock
19289            // when bounded channels fill up.
19290            let result_rx_c = result_rx.clone();
19291            let results_c = Arc::clone(&results);
19292            scope.spawn(move || {
19293                while let Ok(item) = result_rx_c.recv() {
19294                    results_c.lock().push(item);
19295                }
19296            });
19297
19298            // Source feeder thread.
19299            let err_s = Arc::clone(&err);
19300            let take_done_s = Arc::clone(&take_done);
19301            scope.spawn(move || {
19302                for item in source {
19303                    if err_s.lock().is_some()
19304                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
19305                    {
19306                        break;
19307                    }
19308                    if source_tx.send(item).is_err() {
19309                        break;
19310                    }
19311                }
19312            });
19313
19314            // Spawn workers for each stage.
19315            for (stage_idx, op) in streamable_ops.iter().enumerate() {
19316                let rx = channels[stage_idx].1.clone();
19317                let tx = channels[stage_idx + 1].0.clone();
19318
19319                for _ in 0..wn {
19320                    let rx = rx.clone();
19321                    let tx = tx.clone();
19322                    let subs = subs.clone();
19323                    let capture = capture.clone();
19324                    let atomic_arrays = atomic_arrays.clone();
19325                    let atomic_hashes = atomic_hashes.clone();
19326                    let err_w = Arc::clone(&err);
19327                    let take_done_w = Arc::clone(&take_done);
19328
19329                    match *op {
19330                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
19331                            let sub = Arc::clone(sub);
19332                            scope.spawn(move || {
19333                                while let Ok(item) = rx.recv() {
19334                                    if err_w.lock().is_some() {
19335                                        break;
19336                                    }
19337                                    let mut interp = VMHelper::new();
19338                                    interp.subs = subs.clone();
19339                                    interp.scope.restore_capture(&capture);
19340                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
19341                                    interp.enable_parallel_guard();
19342                                    interp.scope.set_topic(item.clone());
19343                                    interp.scope_push_hook();
19344                                    let keep = match interp.exec_block_no_scope(&sub.body) {
19345                                        Ok(val) => val.is_true(),
19346                                        Err(_) => false,
19347                                    };
19348                                    interp.scope_pop_hook();
19349                                    if keep && tx.send(item).is_err() {
19350                                        break;
19351                                    }
19352                                }
19353                            });
19354                        }
19355                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
19356                            let sub = Arc::clone(sub);
19357                            scope.spawn(move || {
19358                                while let Ok(item) = rx.recv() {
19359                                    if err_w.lock().is_some() {
19360                                        break;
19361                                    }
19362                                    let mut interp = VMHelper::new();
19363                                    interp.subs = subs.clone();
19364                                    interp.scope.restore_capture(&capture);
19365                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
19366                                    interp.enable_parallel_guard();
19367                                    interp.scope.set_topic(item);
19368                                    interp.scope_push_hook();
19369                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
19370                                        Ok(val) => val,
19371                                        Err(_) => StrykeValue::UNDEF,
19372                                    };
19373                                    interp.scope_pop_hook();
19374                                    if tx.send(mapped).is_err() {
19375                                        break;
19376                                    }
19377                                }
19378                            });
19379                        }
19380                        PipelineOp::Take(n) => {
19381                            let limit = (*n).max(0) as usize;
19382                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
19383                            let count_w = Arc::clone(&count);
19384                            scope.spawn(move || {
19385                                while let Ok(item) = rx.recv() {
19386                                    let prev =
19387                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
19388                                    if prev >= limit {
19389                                        take_done_w
19390                                            .store(true, std::sync::atomic::Ordering::Relaxed);
19391                                        break;
19392                                    }
19393                                    if tx.send(item).is_err() {
19394                                        break;
19395                                    }
19396                                }
19397                            });
19398                            // Take only needs 1 worker; skip remaining worker spawns.
19399                            break;
19400                        }
19401                        PipelineOp::PFor { ref sub, .. } => {
19402                            let sub = Arc::clone(sub);
19403                            scope.spawn(move || {
19404                                while let Ok(item) = rx.recv() {
19405                                    if err_w.lock().is_some() {
19406                                        break;
19407                                    }
19408                                    let mut interp = VMHelper::new();
19409                                    interp.subs = subs.clone();
19410                                    interp.scope.restore_capture(&capture);
19411                                    interp
19412                                        .scope
19413                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
19414                                    interp.enable_parallel_guard();
19415                                    interp.scope.set_topic(item.clone());
19416                                    interp.scope_push_hook();
19417                                    match interp.exec_block_no_scope(&sub.body) {
19418                                        Ok(_) => {}
19419                                        Err(e) => {
19420                                            let msg = match e {
19421                                                FlowOrError::Error(stryke) => stryke.to_string(),
19422                                                FlowOrError::Flow(_) => {
19423                                                    "unexpected control flow in par_pipeline_stream pfor".into()
19424                                                }
19425                                            };
19426                                            let mut g = err_w.lock();
19427                                            if g.is_none() {
19428                                                *g = Some(msg);
19429                                            }
19430                                            interp.scope_pop_hook();
19431                                            break;
19432                                        }
19433                                    }
19434                                    interp.scope_pop_hook();
19435                                    if tx.send(item).is_err() {
19436                                        break;
19437                                    }
19438                                }
19439                            });
19440                        }
19441                        PipelineOp::Tap(ref sub) => {
19442                            let sub = Arc::clone(sub);
19443                            scope.spawn(move || {
19444                                while let Ok(item) = rx.recv() {
19445                                    if err_w.lock().is_some() {
19446                                        break;
19447                                    }
19448                                    let mut interp = VMHelper::new();
19449                                    interp.subs = subs.clone();
19450                                    interp.scope.restore_capture(&capture);
19451                                    interp
19452                                        .scope
19453                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
19454                                    interp.enable_parallel_guard();
19455                                    match interp.call_sub(
19456                                        &sub,
19457                                        vec![item.clone()],
19458                                        WantarrayCtx::Void,
19459                                        line,
19460                                    )
19461                                    {
19462                                        Ok(_) => {}
19463                                        Err(e) => {
19464                                            let msg = match e {
19465                                                FlowOrError::Error(stryke) => stryke.to_string(),
19466                                                FlowOrError::Flow(_) => {
19467                                                    "unexpected control flow in par_pipeline_stream tap"
19468                                                        .into()
19469                                                }
19470                                            };
19471                                            let mut g = err_w.lock();
19472                                            if g.is_none() {
19473                                                *g = Some(msg);
19474                                            }
19475                                            break;
19476                                        }
19477                                    }
19478                                    if tx.send(item).is_err() {
19479                                        break;
19480                                    }
19481                                }
19482                            });
19483                        }
19484                        PipelineOp::PCache { ref sub, .. } => {
19485                            let sub = Arc::clone(sub);
19486                            scope.spawn(move || {
19487                                while let Ok(item) = rx.recv() {
19488                                    if err_w.lock().is_some() {
19489                                        break;
19490                                    }
19491                                    let k = crate::pcache::cache_key(&item);
19492                                    let val = if let Some(cached) =
19493                                        crate::pcache::GLOBAL_PCACHE.get(&k)
19494                                    {
19495                                        cached.clone()
19496                                    } else {
19497                                        let mut interp = VMHelper::new();
19498                                        interp.subs = subs.clone();
19499                                        interp.scope.restore_capture(&capture);
19500                                        interp
19501                                            .scope
19502                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
19503                                        interp.enable_parallel_guard();
19504                                        interp.scope.set_topic(item);
19505                                        interp.scope_push_hook();
19506                                        let v = match interp.exec_block_no_scope(&sub.body) {
19507                                            Ok(v) => v,
19508                                            Err(_) => StrykeValue::UNDEF,
19509                                        };
19510                                        interp.scope_pop_hook();
19511                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
19512                                        v
19513                                    };
19514                                    if tx.send(val).is_err() {
19515                                        break;
19516                                    }
19517                                }
19518                            });
19519                        }
19520                        // Non-streaming ops already rejected above.
19521                        _ => unreachable!(),
19522                    }
19523                }
19524            }
19525
19526            // Drop our copies of intermediate senders/receivers so channels disconnect
19527            // when workers finish.  Also drop result_rx so the collector thread exits
19528            // once all stage workers are done.
19529            channels.clear();
19530            drop(result_rx);
19531        });
19532
19533        if let Some(msg) = err.lock().take() {
19534            return Err(StrykeError::runtime(msg, line));
19535        }
19536
19537        let results = std::mem::take(&mut *results.lock());
19538        Ok(StrykeValue::array(results))
19539    }
19540
19541    fn heap_compare(&mut self, cmp: &Arc<StrykeSub>, a: &StrykeValue, b: &StrykeValue) -> Ordering {
19542        self.scope_push_hook();
19543        if let Some(ref env) = cmp.closure_env {
19544            self.scope.restore_capture(env);
19545        }
19546        self.scope.set_sort_pair(a.clone(), b.clone());
19547        let ord = match self.exec_block_no_scope(&cmp.body) {
19548            Ok(v) => {
19549                let n = v.to_int();
19550                if n < 0 {
19551                    Ordering::Less
19552                } else if n > 0 {
19553                    Ordering::Greater
19554                } else {
19555                    Ordering::Equal
19556                }
19557            }
19558            Err(_) => Ordering::Equal,
19559        };
19560        self.scope_pop_hook();
19561        ord
19562    }
19563
19564    fn heap_sift_up(&mut self, items: &mut [StrykeValue], cmp: &Arc<StrykeSub>, mut i: usize) {
19565        while i > 0 {
19566            let p = (i - 1) / 2;
19567            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
19568                break;
19569            }
19570            items.swap(i, p);
19571            i = p;
19572        }
19573    }
19574
19575    fn heap_sift_down(&mut self, items: &mut [StrykeValue], cmp: &Arc<StrykeSub>, mut i: usize) {
19576        let n = items.len();
19577        loop {
19578            let mut sm = i;
19579            let l = 2 * i + 1;
19580            let r = 2 * i + 2;
19581            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
19582                sm = l;
19583            }
19584            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
19585                sm = r;
19586            }
19587            if sm == i {
19588                break;
19589            }
19590            items.swap(i, sm);
19591            i = sm;
19592        }
19593    }
19594
19595    fn hash_for_signature_destruct(
19596        &mut self,
19597        v: &StrykeValue,
19598        line: usize,
19599    ) -> StrykeResult<IndexMap<String, StrykeValue>> {
19600        let Some(m) = self.match_subject_as_hash(v) else {
19601            return Err(StrykeError::runtime(
19602                format!(
19603                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
19604                    v.ref_type()
19605                ),
19606                line,
19607            ));
19608        };
19609        Ok(m)
19610    }
19611
19612    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
19613    pub(crate) fn apply_sub_signature(
19614        &mut self,
19615        sub: &StrykeSub,
19616        argv: &[StrykeValue],
19617        line: usize,
19618    ) -> StrykeResult<()> {
19619        if sub.params.is_empty() {
19620            return Ok(());
19621        }
19622        let mut i = 0usize;
19623        for p in &sub.params {
19624            match p {
19625                SubSigParam::Scalar(name, ty, default) => {
19626                    let val = if i < argv.len() {
19627                        argv[i].clone()
19628                    } else if let Some(default_expr) = default {
19629                        match self.eval_expr(default_expr) {
19630                            Ok(v) => v,
19631                            Err(FlowOrError::Error(e)) => return Err(e),
19632                            Err(FlowOrError::Flow(_)) => {
19633                                return Err(StrykeError::runtime(
19634                                    "unexpected control flow in parameter default",
19635                                    line,
19636                                ))
19637                            }
19638                        }
19639                    } else {
19640                        StrykeValue::UNDEF
19641                    };
19642                    i += 1;
19643                    if let Some(t) = ty {
19644                        if let Err(e) = t.check_value(&val) {
19645                            return Err(StrykeError::runtime(
19646                                format!("sub parameter ${}: {}", name, e),
19647                                line,
19648                            ));
19649                        }
19650                    }
19651                    let n = self.english_scalar_name(name);
19652                    self.scope.declare_scalar(n, val);
19653                }
19654                SubSigParam::Array(name, default) => {
19655                    let rest: Vec<StrykeValue> = if i < argv.len() {
19656                        let r = argv[i..].to_vec();
19657                        i = argv.len();
19658                        r
19659                    } else if let Some(default_expr) = default {
19660                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19661                            Ok(v) => v,
19662                            Err(FlowOrError::Error(e)) => return Err(e),
19663                            Err(FlowOrError::Flow(_)) => {
19664                                return Err(StrykeError::runtime(
19665                                    "unexpected control flow in parameter default",
19666                                    line,
19667                                ))
19668                            }
19669                        };
19670                        val.to_list()
19671                    } else {
19672                        vec![]
19673                    };
19674                    let aname = self.stash_array_name_for_package(name);
19675                    self.scope.declare_array(&aname, rest);
19676                }
19677                SubSigParam::Hash(name, default) => {
19678                    let rest: Vec<StrykeValue> = if i < argv.len() {
19679                        let r = argv[i..].to_vec();
19680                        i = argv.len();
19681                        r
19682                    } else if let Some(default_expr) = default {
19683                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19684                            Ok(v) => v,
19685                            Err(FlowOrError::Error(e)) => return Err(e),
19686                            Err(FlowOrError::Flow(_)) => {
19687                                return Err(StrykeError::runtime(
19688                                    "unexpected control flow in parameter default",
19689                                    line,
19690                                ))
19691                            }
19692                        };
19693                        val.to_list()
19694                    } else {
19695                        vec![]
19696                    };
19697                    let mut map = IndexMap::new();
19698                    let mut j = 0;
19699                    while j + 1 < rest.len() {
19700                        map.insert(rest[j].to_string(), rest[j + 1].clone());
19701                        j += 2;
19702                    }
19703                    self.scope.declare_hash(name, map);
19704                }
19705                SubSigParam::ArrayDestruct(elems) => {
19706                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19707                    i += 1;
19708                    let Some(arr) = self.match_subject_as_array(&arg) else {
19709                        return Err(StrykeError::runtime(
19710                            format!(
19711                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
19712                                arg.ref_type()
19713                            ),
19714                            line,
19715                        ));
19716                    };
19717                    let binds = self
19718                        .match_array_pattern_elems(&arr, elems, line)
19719                        .map_err(|e| match e {
19720                            FlowOrError::Error(stryke) => stryke,
19721                            FlowOrError::Flow(_) => StrykeError::runtime(
19722                                "unexpected flow in sub signature array destruct",
19723                                line,
19724                            ),
19725                        })?;
19726                    let Some(binds) = binds else {
19727                        return Err(StrykeError::runtime(
19728                            "sub signature array destruct: length or element mismatch",
19729                            line,
19730                        ));
19731                    };
19732                    for b in binds {
19733                        match b {
19734                            PatternBinding::Scalar(name, v) => {
19735                                let n = self.english_scalar_name(&name);
19736                                self.scope.declare_scalar(n, v);
19737                            }
19738                            PatternBinding::Array(name, elems) => {
19739                                self.scope.declare_array(&name, elems);
19740                            }
19741                        }
19742                    }
19743                }
19744                SubSigParam::HashDestruct(pairs) => {
19745                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19746                    i += 1;
19747                    let map = self.hash_for_signature_destruct(&arg, line)?;
19748                    for (key, varname) in pairs {
19749                        let v = map.get(key).cloned().unwrap_or(StrykeValue::UNDEF);
19750                        let n = self.english_scalar_name(varname);
19751                        self.scope.declare_scalar(n, v);
19752                    }
19753                }
19754            }
19755        }
19756        Ok(())
19757    }
19758
19759    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
19760    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
19761    /// These are `StrykeSub`s with empty bodies and magic keys in `closure_env`.
19762    pub(crate) fn try_hof_dispatch(
19763        &mut self,
19764        sub: &StrykeSub,
19765        args: &[StrykeValue],
19766        want: WantarrayCtx,
19767        line: usize,
19768    ) -> Option<ExecResult> {
19769        let env = sub.closure_env.as_ref()?;
19770        fn env_get<'a>(env: &'a [(String, StrykeValue)], key: &str) -> Option<&'a StrykeValue> {
19771            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
19772        }
19773
19774        match sub.name.as_str() {
19775            // ── compose: right-to-left function application ──
19776            "__comp__" => {
19777                let fns = env_get(env, "__comp_fns__")?.to_list();
19778                let mut val = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
19779                for f in fns.iter().rev() {
19780                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
19781                        Ok(v) => val = v,
19782                        Err(e) => return Some(Err(e)),
19783                    }
19784                }
19785                Some(Ok(val))
19786            }
19787            // ── constantly: always return the captured value ──
19788            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
19789            // ── juxt: call each fn with same args, collect results ──
19790            "__juxt__" => {
19791                let fns = env_get(env, "__juxt_fns__")?.to_list();
19792                let mut results = Vec::with_capacity(fns.len());
19793                for f in &fns {
19794                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
19795                        Ok(v) => results.push(v),
19796                        Err(e) => return Some(Err(e)),
19797                    }
19798                }
19799                Some(Ok(StrykeValue::array(results)))
19800            }
19801            // ── partial: prepend bound args ──
19802            "__partial__" => {
19803                let fn_val = env_get(env, "__partial_fn__")?.clone();
19804                let bound = env_get(env, "__partial_args__")?.to_list();
19805                let mut all_args = bound;
19806                all_args.extend_from_slice(args);
19807                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
19808            }
19809            // ── complement: negate the result ──
19810            "__complement__" => {
19811                let fn_val = env_get(env, "__complement_fn__")?.clone();
19812                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19813                    Ok(v) => Some(Ok(StrykeValue::integer(if v.is_true() { 0 } else { 1 }))),
19814                    Err(e) => Some(Err(e)),
19815                }
19816            }
19817            // ── fnil: replace undef args with defaults ──
19818            "__fnil__" => {
19819                let fn_val = env_get(env, "__fnil_fn__")?.clone();
19820                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
19821                let mut patched = args.to_vec();
19822                for (i, d) in defaults.iter().enumerate() {
19823                    if i < patched.len() {
19824                        if patched[i].is_undef() {
19825                            patched[i] = d.clone();
19826                        }
19827                    } else {
19828                        patched.push(d.clone());
19829                    }
19830                }
19831                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
19832            }
19833            // ── memoize: cache by stringified args ──
19834            "__memoize__" => {
19835                let fn_val = env_get(env, "__memoize_fn__")?.clone();
19836                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
19837                let key = args
19838                    .iter()
19839                    .map(|a| a.to_string())
19840                    .collect::<Vec<_>>()
19841                    .join("\x00");
19842                if let Some(href) = cache_ref.as_hash_ref() {
19843                    if let Some(cached) = href.read().get(&key) {
19844                        return Some(Ok(cached.clone()));
19845                    }
19846                }
19847                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19848                    Ok(v) => {
19849                        if let Some(href) = cache_ref.as_hash_ref() {
19850                            href.write().insert(key, v.clone());
19851                        }
19852                        Some(Ok(v))
19853                    }
19854                    Err(e) => Some(Err(e)),
19855                }
19856            }
19857            // ── curry: accumulate args until arity reached ──
19858            "__curry__" => {
19859                let fn_val = env_get(env, "__curry_fn__")?.clone();
19860                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
19861                let bound = env_get(env, "__curry_bound__")?.to_list();
19862                let mut all = bound;
19863                all.extend_from_slice(args);
19864                if all.len() >= arity {
19865                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
19866                } else {
19867                    let curry_sub = StrykeSub {
19868                        name: "__curry__".to_string(),
19869                        params: vec![],
19870                        body: vec![],
19871                        closure_env: Some(vec![
19872                            ("__curry_fn__".to_string(), fn_val),
19873                            (
19874                                "__curry_arity__".to_string(),
19875                                StrykeValue::integer(arity as i64),
19876                            ),
19877                            ("__curry_bound__".to_string(), StrykeValue::array(all)),
19878                        ]),
19879                        prototype: None,
19880                        fib_like: None,
19881                    };
19882                    Some(Ok(StrykeValue::code_ref(Arc::new(curry_sub))))
19883                }
19884            }
19885            // ── once: call once, cache forever ──
19886            "__once__" => {
19887                let cache_ref = env_get(env, "__once_cache__")?.clone();
19888                if let Some(href) = cache_ref.as_hash_ref() {
19889                    let r = href.read();
19890                    if r.contains_key("done") {
19891                        return Some(Ok(r.get("val").cloned().unwrap_or(StrykeValue::UNDEF)));
19892                    }
19893                }
19894                let fn_val = env_get(env, "__once_fn__")?.clone();
19895                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19896                    Ok(v) => {
19897                        if let Some(href) = cache_ref.as_hash_ref() {
19898                            let mut w = href.write();
19899                            w.insert("done".to_string(), StrykeValue::integer(1));
19900                            w.insert("val".to_string(), v.clone());
19901                        }
19902                        Some(Ok(v))
19903                    }
19904                    Err(e) => Some(Err(e)),
19905                }
19906            }
19907            _ => None,
19908        }
19909    }
19910
19911    pub(crate) fn call_sub(
19912        &mut self,
19913        sub: &StrykeSub,
19914        args: Vec<StrykeValue>,
19915        want: WantarrayCtx,
19916        line: usize,
19917    ) -> ExecResult {
19918        // Default path: derive the package from `sub.name` if it is qualified. Bare-named
19919        // subs (registered without a `Pkg::` prefix) leave `__PACKAGE__` untouched.
19920        let pkg = sub.name.rsplit_once("::").map(|(p, _)| p.to_string());
19921        self.call_sub_with_package(sub, args, want, line, pkg)
19922    }
19923
19924    /// Internal helper: like [`Self::call_sub`] but takes an explicit home-package override
19925    /// (used by [`Self::call_named_sub`], which knows the qualified registry key even when
19926    /// the cached `StrykeSub.name` is bare).
19927    fn call_sub_with_package(
19928        &mut self,
19929        sub: &StrykeSub,
19930        args: Vec<StrykeValue>,
19931        want: WantarrayCtx,
19932        _line: usize,
19933        home_package: Option<String>,
19934    ) -> ExecResult {
19935        // Push current sub for __SUB__ access
19936        self.current_sub_stack.push(Arc::new(sub.clone()));
19937
19938        // Single frame for both @_ and the block's local variables —
19939        // avoids the double push_frame/pop_frame overhead per call.
19940        self.scope_push_hook();
19941        self.scope.declare_array("_", args.clone());
19942        if let Some(ref env) = sub.closure_env {
19943            self.scope.restore_capture(env);
19944        }
19945        // Switch `__PACKAGE__` to the sub's home package so cross-package `our`/`oursync`
19946        // qualifies correctly inside the body. Bytecode VM rewrites at compile time so it
19947        // never needed this; the tree walker (used by parallel workers) does need it.
19948        // Goes AFTER restore_capture so the closure's captured `__PACKAGE__` doesn't
19949        // overwrite our home-package switch.
19950        if let Some(pkg) = home_package {
19951            self.scope
19952                .declare_scalar("__PACKAGE__", StrykeValue::string(pkg));
19953        }
19954        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
19955        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
19956        // Must be AFTER restore_capture so we don't get shadowed by captured $_
19957        self.scope.set_closure_args(&args);
19958        // Move `@_` out so `fib_like` / hof dispatch take `&[StrykeValue]` without cloning.
19959        let argv = self.scope.take_sub_underscore().unwrap_or_default();
19960        self.apply_sub_signature(sub, &argv, _line)?;
19961        let saved = self.wantarray_kind;
19962        self.wantarray_kind = want;
19963        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
19964            self.wantarray_kind = saved;
19965            self.scope_pop_hook();
19966            self.current_sub_stack.pop();
19967            return match r {
19968                Ok(v) => Ok(v),
19969                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19970                Err(e) => Err(e),
19971            };
19972        }
19973        if let Some(pat) = sub.fib_like.as_ref() {
19974            if argv.len() == 1 {
19975                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
19976                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
19977                    if let Some(p) = &mut self.profiler {
19978                        p.enter_sub(&sub.name);
19979                    }
19980                    self.debugger_enter_sub(&sub.name);
19981                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
19982                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
19983                        p.exit_sub(t0.elapsed());
19984                    }
19985                    self.debugger_leave_sub();
19986                    self.wantarray_kind = saved;
19987                    self.scope_pop_hook();
19988                    self.current_sub_stack.pop();
19989                    return Ok(StrykeValue::integer(n));
19990                }
19991            }
19992        }
19993        self.scope.declare_array("_", argv.clone());
19994        // Note: set_closure_args was already called at line 15077; don't call it again
19995        // as that would incorrectly shift the outer topic stack a second time.
19996        let t0 = self.profiler.is_some().then(std::time::Instant::now);
19997        if let Some(p) = &mut self.profiler {
19998            p.enter_sub(&sub.name);
19999        }
20000        self.debugger_enter_sub(&sub.name);
20001        // Always evaluate the function body's last expression in List context so
20002        // `@array` returns the array contents, not the count. The caller adapts the
20003        // return value to their own wantarray context after receiving it.
20004        let result = self.exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List);
20005        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
20006            p.exit_sub(t0.elapsed());
20007        }
20008        self.debugger_leave_sub();
20009        // For goto &sub, capture @_ before popping the frame
20010        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
20011            Some(self.scope.get_array("_"))
20012        } else {
20013            None
20014        };
20015        self.wantarray_kind = saved;
20016        self.scope_pop_hook();
20017        self.current_sub_stack.pop();
20018        match result {
20019            Ok(v) => Ok(v),
20020            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
20021            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
20022                // goto &sub — tail call: look up target and call with same @_
20023                let goto_args = goto_args.unwrap_or_default();
20024                let fqn = if target_name.contains("::") {
20025                    target_name.clone()
20026                } else {
20027                    format!("{}::{}", self.current_package(), target_name)
20028                };
20029                if let Some(target_sub) = self
20030                    .subs
20031                    .get(&fqn)
20032                    .cloned()
20033                    .or_else(|| self.subs.get(&target_name).cloned())
20034                {
20035                    self.call_sub(&target_sub, goto_args, want, _line)
20036                } else {
20037                    Err(StrykeError::runtime(
20038                        format!("Undefined subroutine &{}", target_name),
20039                        _line,
20040                    )
20041                    .into())
20042                }
20043            }
20044            Err(FlowOrError::Flow(Flow::Yield(_))) => {
20045                Err(StrykeError::runtime("yield is only valid inside gen { }", 0).into())
20046            }
20047            Err(e) => Err(e),
20048        }
20049    }
20050
20051    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
20052    fn call_struct_method(
20053        &mut self,
20054        body: &Block,
20055        params: &[SubSigParam],
20056        args: Vec<StrykeValue>,
20057        line: usize,
20058    ) -> ExecResult {
20059        self.scope_push_hook();
20060        self.scope.declare_array("_", args.clone());
20061        // Bind $self to first arg (the receiver)
20062        if let Some(self_val) = args.first() {
20063            self.scope.declare_scalar("self", self_val.clone());
20064        }
20065        // Set $_0, $_1, etc. for the EXPLICIT args (skip $self at args[0]) so
20066        // `fn tom { _ * 2 }; obj->tom(99)` works identically to the standalone
20067        // `fn tom { _ * 2 }; tom(99)` — both treat the first EXPLICIT arg as
20068        // the topic. `$self` stays accessible via the dedicated `$self` binding
20069        // above and via `$_[0]` (full `@_` retained).
20070        if args.len() > 1 {
20071            self.scope.set_closure_args(&args[1..]);
20072        }
20073        // Apply signature if provided - skip the first arg ($self) for user params
20074        let user_args: Vec<StrykeValue> = args.iter().skip(1).cloned().collect();
20075        self.apply_params_to_argv(params, &user_args, line)?;
20076        let result = self.exec_block_no_scope(body);
20077        self.scope_pop_hook();
20078        match result {
20079            Ok(v) => Ok(v),
20080            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
20081            Err(e) => Err(e),
20082        }
20083    }
20084
20085    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
20086    pub(crate) fn call_class_method(
20087        &mut self,
20088        body: &Block,
20089        params: &[SubSigParam],
20090        args: Vec<StrykeValue>,
20091        line: usize,
20092    ) -> ExecResult {
20093        self.call_class_method_inner(body, params, args, line, false)
20094    }
20095
20096    /// Call a static class method: `Math::add(...)`.
20097    pub(crate) fn call_static_class_method(
20098        &mut self,
20099        body: &Block,
20100        params: &[SubSigParam],
20101        args: Vec<StrykeValue>,
20102        line: usize,
20103    ) -> ExecResult {
20104        self.call_class_method_inner(body, params, args, line, true)
20105    }
20106
20107    fn call_class_method_inner(
20108        &mut self,
20109        body: &Block,
20110        params: &[SubSigParam],
20111        args: Vec<StrykeValue>,
20112        line: usize,
20113        is_static: bool,
20114    ) -> ExecResult {
20115        self.scope_push_hook();
20116        self.scope.declare_array("_", args.clone());
20117        if !is_static {
20118            // Bind $self to first arg (the receiver) for instance methods
20119            if let Some(self_val) = args.first() {
20120                self.scope.declare_scalar("self", self_val.clone());
20121            }
20122        }
20123        // Set $_0, $_1, etc. for the EXPLICIT args. For instance methods skip
20124        // args[0] (which is $self) so `fn tom { _ * 2 }; obj->tom(99)` behaves
20125        // the same as `fn tom { _ * 2 }; tom(99)` — both treat the first
20126        // EXPLICIT arg as the topic. `$self` is still accessible via the
20127        // dedicated `$self` binding above and via `$_[0]` (full `@_` retained).
20128        // Static methods have no `$self`, so the full args list IS the topic.
20129        if is_static {
20130            self.scope.set_closure_args(&args);
20131        } else if args.len() > 1 {
20132            self.scope.set_closure_args(&args[1..]);
20133        }
20134        // Apply signature: skip first arg ($self) only for instance methods
20135        let user_args: Vec<StrykeValue> = if is_static {
20136            args.clone()
20137        } else {
20138            args.iter().skip(1).cloned().collect()
20139        };
20140        self.apply_params_to_argv(params, &user_args, line)?;
20141        let result = self.exec_block_no_scope(body);
20142        self.scope_pop_hook();
20143        match result {
20144            Ok(v) => Ok(v),
20145            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
20146            Err(e) => Err(e),
20147        }
20148    }
20149
20150    /// Apply SubSigParam bindings without the full StrykeSub machinery.
20151    fn apply_params_to_argv(
20152        &mut self,
20153        params: &[SubSigParam],
20154        argv: &[StrykeValue],
20155        line: usize,
20156    ) -> StrykeResult<()> {
20157        let mut i = 0;
20158        for param in params {
20159            match param {
20160                SubSigParam::Scalar(name, ty_opt, default) => {
20161                    let v = if i < argv.len() {
20162                        argv[i].clone()
20163                    } else if let Some(default_expr) = default {
20164                        match self.eval_expr(default_expr) {
20165                            Ok(v) => v,
20166                            Err(FlowOrError::Error(e)) => return Err(e),
20167                            Err(FlowOrError::Flow(_)) => {
20168                                return Err(StrykeError::runtime(
20169                                    "unexpected control flow in parameter default",
20170                                    line,
20171                                ))
20172                            }
20173                        }
20174                    } else {
20175                        StrykeValue::UNDEF
20176                    };
20177                    i += 1;
20178                    if let Some(ty) = ty_opt {
20179                        ty.check_value(&v).map_err(|msg| {
20180                            StrykeError::type_error(
20181                                format!("method parameter ${}: {}", name, msg),
20182                                line,
20183                            )
20184                        })?;
20185                    }
20186                    let n = self.english_scalar_name(name);
20187                    self.scope.declare_scalar(n, v);
20188                }
20189                SubSigParam::Array(name, default) => {
20190                    let rest: Vec<StrykeValue> = if i < argv.len() {
20191                        let r = argv[i..].to_vec();
20192                        i = argv.len();
20193                        r
20194                    } else if let Some(default_expr) = default {
20195                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
20196                            Ok(v) => v,
20197                            Err(FlowOrError::Error(e)) => return Err(e),
20198                            Err(FlowOrError::Flow(_)) => {
20199                                return Err(StrykeError::runtime(
20200                                    "unexpected control flow in parameter default",
20201                                    line,
20202                                ))
20203                            }
20204                        };
20205                        val.to_list()
20206                    } else {
20207                        vec![]
20208                    };
20209                    let aname = self.stash_array_name_for_package(name);
20210                    self.scope.declare_array(&aname, rest);
20211                }
20212                SubSigParam::Hash(name, default) => {
20213                    let rest: Vec<StrykeValue> = if i < argv.len() {
20214                        let r = argv[i..].to_vec();
20215                        i = argv.len();
20216                        r
20217                    } else if let Some(default_expr) = default {
20218                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
20219                            Ok(v) => v,
20220                            Err(FlowOrError::Error(e)) => return Err(e),
20221                            Err(FlowOrError::Flow(_)) => {
20222                                return Err(StrykeError::runtime(
20223                                    "unexpected control flow in parameter default",
20224                                    line,
20225                                ))
20226                            }
20227                        };
20228                        val.to_list()
20229                    } else {
20230                        vec![]
20231                    };
20232                    let mut map = IndexMap::new();
20233                    let mut j = 0;
20234                    while j + 1 < rest.len() {
20235                        map.insert(rest[j].to_string(), rest[j + 1].clone());
20236                        j += 2;
20237                    }
20238                    self.scope.declare_hash(name, map);
20239                }
20240                SubSigParam::ArrayDestruct(elems) => {
20241                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
20242                    i += 1;
20243                    let Some(arr) = self.match_subject_as_array(&arg) else {
20244                        return Err(StrykeError::runtime(
20245                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
20246                            line,
20247                        ));
20248                    };
20249                    let binds = self
20250                        .match_array_pattern_elems(&arr, elems, line)
20251                        .map_err(|e| match e {
20252                            FlowOrError::Error(stryke) => stryke,
20253                            FlowOrError::Flow(_) => StrykeError::runtime(
20254                                "unexpected flow in method array destruct",
20255                                line,
20256                            ),
20257                        })?;
20258                    let Some(binds) = binds else {
20259                        return Err(StrykeError::runtime(
20260                            format!(
20261                                "method parameter: array destructure failed at position {}",
20262                                i
20263                            ),
20264                            line,
20265                        ));
20266                    };
20267                    for b in binds {
20268                        match b {
20269                            PatternBinding::Scalar(name, v) => {
20270                                let n = self.english_scalar_name(&name);
20271                                self.scope.declare_scalar(n, v);
20272                            }
20273                            PatternBinding::Array(name, elems) => {
20274                                self.scope.declare_array(&name, elems);
20275                            }
20276                        }
20277                    }
20278                }
20279                SubSigParam::HashDestruct(pairs) => {
20280                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
20281                    i += 1;
20282                    let map = self.hash_for_signature_destruct(&arg, line)?;
20283                    for (key, varname) in pairs {
20284                        let v = map.get(key).cloned().unwrap_or(StrykeValue::UNDEF);
20285                        let n = self.english_scalar_name(varname);
20286                        self.scope.declare_scalar(n, v);
20287                    }
20288                }
20289            }
20290        }
20291        Ok(())
20292    }
20293
20294    fn builtin_new(&mut self, class: &str, args: Vec<StrykeValue>, line: usize) -> ExecResult {
20295        if class == "Set" {
20296            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
20297        }
20298        if let Some(def) = self.struct_defs.get(class).cloned() {
20299            let mut provided = Vec::new();
20300            let mut i = 1;
20301            while i + 1 < args.len() {
20302                let k = args[i].to_string();
20303                let v = args[i + 1].clone();
20304                provided.push((k, v));
20305                i += 2;
20306            }
20307            let mut defaults = Vec::with_capacity(def.fields.len());
20308            for field in &def.fields {
20309                if let Some(ref expr) = field.default {
20310                    let val = self.eval_expr(expr)?;
20311                    defaults.push(Some(val));
20312                } else {
20313                    defaults.push(None);
20314                }
20315            }
20316            return Ok(crate::native_data::struct_new_with_defaults(
20317                &def, &provided, &defaults, line,
20318            )?);
20319        }
20320        // Stryke `class` declarations route through `class_construct` so the
20321        // result is a real `ClassInstance` (typed-my checks, isa walk, BUILD
20322        // hooks, etc.). Without this, `Class->new` for a registered class
20323        // fell through to the default Perl-style blessed-hashref path,
20324        // breaking `typed my $x : Class = Class->new` even though the
20325        // runtime check for `Struct(name)` was already in place. Skip
20326        // `args[0]` (the class-name receiver) since `class_construct`
20327        // expects user args only.
20328        if let Some(def) = self.class_defs.get(class).cloned() {
20329            let user_args: Vec<StrykeValue> = args.into_iter().skip(1).collect();
20330            return self.class_construct(&def, user_args, line);
20331        }
20332        // Default OO constructor: Class->new(%args) → bless {%args}, class
20333        let mut map = IndexMap::new();
20334        let mut i = 1; // skip $self (first arg is class name)
20335        while i + 1 < args.len() {
20336            let k = args[i].to_string();
20337            let v = args[i + 1].clone();
20338            map.insert(k, v);
20339            i += 2;
20340        }
20341        Ok(StrykeValue::blessed(Arc::new(
20342            crate::value::BlessedRef::new_blessed(class.to_string(), StrykeValue::hash(map)),
20343        )))
20344    }
20345
20346    fn exec_print(
20347        &mut self,
20348        handle: Option<&str>,
20349        args: &[Expr],
20350        newline: bool,
20351        line: usize,
20352    ) -> ExecResult {
20353        if newline && (self.feature_bits & FEAT_SAY) == 0 {
20354            return Err(StrykeError::runtime(
20355                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
20356                line,
20357            )
20358            .into());
20359        }
20360        let mut output = String::new();
20361        if args.is_empty() {
20362            // Perl: print with no LIST prints $_ (same for say).
20363            let topic = self.scope.get_scalar("_").clone();
20364            let s = self.stringify_value(topic, line)?;
20365            output.push_str(&s);
20366        } else {
20367            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
20368            // between those top-level expressions only (not between elements of an expanded `@arr`).
20369            for (i, a) in args.iter().enumerate() {
20370                if i > 0 {
20371                    output.push_str(&self.ofs);
20372                }
20373                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
20374                for item in val.to_list() {
20375                    let s = self.stringify_value(item, line)?;
20376                    output.push_str(&s);
20377                }
20378            }
20379        }
20380        if newline {
20381            output.push('\n');
20382        }
20383        output.push_str(&self.ors);
20384
20385        let handle_name =
20386            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
20387        self.write_formatted_print(handle_name.as_str(), &output, line)?;
20388        Ok(StrykeValue::integer(1))
20389    }
20390
20391    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
20392        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
20393            // Perl: printf with no args uses $_ as the format string.
20394            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
20395            (s, &[])
20396        } else {
20397            (self.eval_expr(&args[0])?.to_string(), &args[1..])
20398        };
20399        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
20400        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
20401        // ranges to flip-flop values, so go through list-context eval and splat.
20402        let mut arg_vals = Vec::new();
20403        for a in rest {
20404            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
20405            if let Some(items) = v.as_array_vec() {
20406                arg_vals.extend(items);
20407            } else {
20408                arg_vals.push(v);
20409            }
20410        }
20411        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
20412        let handle_name =
20413            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
20414        match handle_name.as_str() {
20415            "STDOUT" => {
20416                if !self.suppress_stdout {
20417                    print!("{}", output);
20418                    if self.output_autoflush {
20419                        let _ = io::stdout().flush();
20420                    }
20421                }
20422            }
20423            "STDERR" => {
20424                eprint!("{}", output);
20425                let _ = io::stderr().flush();
20426            }
20427            name => {
20428                if let Some(writer) = self.output_handles.get_mut(name) {
20429                    let _ = writer.write_all(output.as_bytes());
20430                    if self.output_autoflush {
20431                        let _ = writer.flush();
20432                    }
20433                }
20434            }
20435        }
20436        Ok(StrykeValue::integer(1))
20437    }
20438
20439    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
20440    pub(crate) fn eval_substr_expr(
20441        &mut self,
20442        string: &Expr,
20443        offset: &Expr,
20444        length: Option<&Expr>,
20445        replacement: Option<&Expr>,
20446        _line: usize,
20447    ) -> Result<StrykeValue, FlowOrError> {
20448        let s = self.eval_expr(string)?.to_string();
20449        let off = self.eval_expr(offset)?.to_int();
20450        let start = if off < 0 {
20451            (s.len() as i64 + off).max(0) as usize
20452        } else {
20453            off as usize
20454        };
20455        let len = if let Some(l) = length {
20456            let len_val = self.eval_expr(l)?.to_int();
20457            if len_val < 0 {
20458                // Negative length: count from end of string
20459                let remaining = s.len().saturating_sub(start) as i64;
20460                (remaining + len_val).max(0) as usize
20461            } else {
20462                len_val as usize
20463            }
20464        } else {
20465            s.len().saturating_sub(start)
20466        };
20467        let end = start.saturating_add(len).min(s.len());
20468        let result = s.get(start..end).unwrap_or("").to_string();
20469        if let Some(rep) = replacement {
20470            let rep_s = self.eval_expr(rep)?.to_string();
20471            let mut new_s = String::new();
20472            new_s.push_str(&s[..start]);
20473            new_s.push_str(&rep_s);
20474            new_s.push_str(&s[end..]);
20475            self.assign_value(string, StrykeValue::string(new_s))?;
20476        }
20477        Ok(StrykeValue::string(result))
20478    }
20479
20480    pub(crate) fn eval_push_expr(
20481        &mut self,
20482        array: &Expr,
20483        values: &[Expr],
20484        line: usize,
20485    ) -> Result<StrykeValue, FlowOrError> {
20486        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20487            for v in values {
20488                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20489                self.push_array_deref_value(aref.clone(), val, line)?;
20490            }
20491            let len = self.array_deref_len(aref, line)?;
20492            return Ok(StrykeValue::integer(len));
20493        }
20494        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20495        if self.scope.is_array_frozen(&arr_name) {
20496            return Err(StrykeError::runtime(
20497                format!("Modification of a frozen value: @{}", arr_name),
20498                line,
20499            )
20500            .into());
20501        }
20502        for v in values {
20503            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20504            if let Some(items) = val.as_array_vec() {
20505                for item in items {
20506                    self.scope
20507                        .push_to_array(&arr_name, item)
20508                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20509                }
20510            } else {
20511                self.scope
20512                    .push_to_array(&arr_name, val)
20513                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20514            }
20515        }
20516        let len = self.scope.array_len(&arr_name);
20517        Ok(StrykeValue::integer(len as i64))
20518    }
20519
20520    pub(crate) fn eval_pop_expr(
20521        &mut self,
20522        array: &Expr,
20523        line: usize,
20524    ) -> Result<StrykeValue, FlowOrError> {
20525        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20526            return self.pop_array_deref(aref, line);
20527        }
20528        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20529        self.scope
20530            .pop_from_array(&arr_name)
20531            .map_err(|e| FlowOrError::Error(e.at_line(line)))
20532    }
20533
20534    pub(crate) fn eval_shift_expr(
20535        &mut self,
20536        array: &Expr,
20537        line: usize,
20538    ) -> Result<StrykeValue, FlowOrError> {
20539        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20540            return self.shift_array_deref(aref, line);
20541        }
20542        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20543        self.scope
20544            .shift_from_array(&arr_name)
20545            .map_err(|e| FlowOrError::Error(e.at_line(line)))
20546    }
20547
20548    pub(crate) fn eval_unshift_expr(
20549        &mut self,
20550        array: &Expr,
20551        values: &[Expr],
20552        line: usize,
20553    ) -> Result<StrykeValue, FlowOrError> {
20554        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20555            let mut vals = Vec::new();
20556            for v in values {
20557                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20558                if let Some(items) = val.as_array_vec() {
20559                    vals.extend(items);
20560                } else {
20561                    vals.push(val);
20562                }
20563            }
20564            let len = self.unshift_array_deref_multi(aref, vals, line)?;
20565            return Ok(StrykeValue::integer(len));
20566        }
20567        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20568        let mut vals = Vec::new();
20569        for v in values {
20570            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
20571            if let Some(items) = val.as_array_vec() {
20572                vals.extend(items);
20573            } else {
20574                vals.push(val);
20575            }
20576        }
20577        let arr = self
20578            .scope
20579            .get_array_mut(&arr_name)
20580            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20581        for (i, v) in vals.into_iter().enumerate() {
20582            arr.insert(i, v);
20583        }
20584        let len = arr.len();
20585        Ok(StrykeValue::integer(len as i64))
20586    }
20587
20588    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
20589    pub(crate) fn push_array_deref_value(
20590        &mut self,
20591        arr_ref: StrykeValue,
20592        val: StrykeValue,
20593        line: usize,
20594    ) -> Result<(), FlowOrError> {
20595        // Resolve binding refs in the value being stored so they snapshot
20596        // the current scope data and survive scope pop.
20597        let val = self.scope.resolve_container_binding_ref(val);
20598        if let Some(r) = arr_ref.as_array_ref() {
20599            let mut w = r.write();
20600            if let Some(items) = val.as_array_vec() {
20601                w.extend(items.iter().cloned());
20602            } else {
20603                w.push(val);
20604            }
20605            return Ok(());
20606        }
20607        if let Some(name) = arr_ref.as_array_binding_name() {
20608            if let Some(items) = val.as_array_vec() {
20609                for item in items {
20610                    self.scope
20611                        .push_to_array(&name, item)
20612                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20613                }
20614            } else {
20615                self.scope
20616                    .push_to_array(&name, val)
20617                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20618            }
20619            return Ok(());
20620        }
20621        if let Some(s) = arr_ref.as_str() {
20622            if self.strict_refs {
20623                return Err(StrykeError::runtime(
20624                    format!(
20625                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20626                        s
20627                    ),
20628                    line,
20629                )
20630                .into());
20631            }
20632            let name = s.to_string();
20633            if let Some(items) = val.as_array_vec() {
20634                for item in items {
20635                    self.scope
20636                        .push_to_array(&name, item)
20637                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20638                }
20639            } else {
20640                self.scope
20641                    .push_to_array(&name, val)
20642                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20643            }
20644            return Ok(());
20645        }
20646        Err(StrykeError::runtime("push argument is not an ARRAY reference", line).into())
20647    }
20648
20649    pub(crate) fn array_deref_len(
20650        &self,
20651        arr_ref: StrykeValue,
20652        line: usize,
20653    ) -> Result<i64, FlowOrError> {
20654        if let Some(r) = arr_ref.as_array_ref() {
20655            return Ok(r.read().len() as i64);
20656        }
20657        if let Some(name) = arr_ref.as_array_binding_name() {
20658            return Ok(self.scope.array_len(&name) as i64);
20659        }
20660        if let Some(s) = arr_ref.as_str() {
20661            if self.strict_refs {
20662                return Err(StrykeError::runtime(
20663                    format!(
20664                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20665                        s
20666                    ),
20667                    line,
20668                )
20669                .into());
20670            }
20671            return Ok(self.scope.array_len(&s) as i64);
20672        }
20673        Err(StrykeError::runtime("argument is not an ARRAY reference", line).into())
20674    }
20675
20676    pub(crate) fn pop_array_deref(
20677        &mut self,
20678        arr_ref: StrykeValue,
20679        line: usize,
20680    ) -> Result<StrykeValue, FlowOrError> {
20681        if let Some(r) = arr_ref.as_array_ref() {
20682            let mut w = r.write();
20683            return Ok(w.pop().unwrap_or(StrykeValue::UNDEF));
20684        }
20685        if let Some(name) = arr_ref.as_array_binding_name() {
20686            return self
20687                .scope
20688                .pop_from_array(&name)
20689                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20690        }
20691        if let Some(s) = arr_ref.as_str() {
20692            if self.strict_refs {
20693                return Err(StrykeError::runtime(
20694                    format!(
20695                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20696                        s
20697                    ),
20698                    line,
20699                )
20700                .into());
20701            }
20702            return self
20703                .scope
20704                .pop_from_array(&s)
20705                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20706        }
20707        Err(StrykeError::runtime("pop argument is not an ARRAY reference", line).into())
20708    }
20709
20710    pub(crate) fn shift_array_deref(
20711        &mut self,
20712        arr_ref: StrykeValue,
20713        line: usize,
20714    ) -> Result<StrykeValue, FlowOrError> {
20715        if let Some(r) = arr_ref.as_array_ref() {
20716            let mut w = r.write();
20717            return Ok(if w.is_empty() {
20718                StrykeValue::UNDEF
20719            } else {
20720                w.remove(0)
20721            });
20722        }
20723        if let Some(name) = arr_ref.as_array_binding_name() {
20724            return self
20725                .scope
20726                .shift_from_array(&name)
20727                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20728        }
20729        if let Some(s) = arr_ref.as_str() {
20730            if self.strict_refs {
20731                return Err(StrykeError::runtime(
20732                    format!(
20733                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20734                        s
20735                    ),
20736                    line,
20737                )
20738                .into());
20739            }
20740            return self
20741                .scope
20742                .shift_from_array(&s)
20743                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20744        }
20745        Err(StrykeError::runtime("shift argument is not an ARRAY reference", line).into())
20746    }
20747
20748    pub(crate) fn unshift_array_deref_multi(
20749        &mut self,
20750        arr_ref: StrykeValue,
20751        vals: Vec<StrykeValue>,
20752        line: usize,
20753    ) -> Result<i64, FlowOrError> {
20754        let mut flat: Vec<StrykeValue> = Vec::new();
20755        for v in vals {
20756            if let Some(items) = v.as_array_vec() {
20757                flat.extend(items);
20758            } else {
20759                flat.push(v);
20760            }
20761        }
20762        if let Some(r) = arr_ref.as_array_ref() {
20763            let mut w = r.write();
20764            for (i, v) in flat.into_iter().enumerate() {
20765                w.insert(i, v);
20766            }
20767            return Ok(w.len() as i64);
20768        }
20769        if let Some(name) = arr_ref.as_array_binding_name() {
20770            let arr = self
20771                .scope
20772                .get_array_mut(&name)
20773                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20774            for (i, v) in flat.into_iter().enumerate() {
20775                arr.insert(i, v);
20776            }
20777            return Ok(arr.len() as i64);
20778        }
20779        if let Some(s) = arr_ref.as_str() {
20780            if self.strict_refs {
20781                return Err(StrykeError::runtime(
20782                    format!(
20783                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20784                        s
20785                    ),
20786                    line,
20787                )
20788                .into());
20789            }
20790            let name = s.to_string();
20791            let arr = self
20792                .scope
20793                .get_array_mut(&name)
20794                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20795            for (i, v) in flat.into_iter().enumerate() {
20796                arr.insert(i, v);
20797            }
20798            return Ok(arr.len() as i64);
20799        }
20800        Err(StrykeError::runtime("unshift argument is not an ARRAY reference", line).into())
20801    }
20802
20803    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
20804    /// / compiler wraps `splice` like other context-sensitive builtins).
20805    pub(crate) fn splice_array_deref(
20806        &mut self,
20807        aref: StrykeValue,
20808        offset_val: StrykeValue,
20809        length_val: StrykeValue,
20810        rep_vals: Vec<StrykeValue>,
20811        line: usize,
20812    ) -> Result<StrykeValue, FlowOrError> {
20813        let ctx = self.wantarray_kind;
20814        if let Some(r) = aref.as_array_ref() {
20815            let arr_len = r.read().len();
20816            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20817            let mut w = r.write();
20818            let removed: Vec<StrykeValue> = w.drain(off..end).collect();
20819            for (i, v) in rep_vals.into_iter().enumerate() {
20820                w.insert(off + i, v);
20821            }
20822            return Ok(match ctx {
20823                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20824                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20825            });
20826        }
20827        if let Some(name) = aref.as_array_binding_name() {
20828            let arr_len = self.scope.array_len(&name);
20829            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20830            let removed = self
20831                .scope
20832                .splice_in_place(&name, off, end, rep_vals)
20833                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20834            return Ok(match ctx {
20835                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20836                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20837            });
20838        }
20839        if let Some(s) = aref.as_str() {
20840            if self.strict_refs {
20841                return Err(StrykeError::runtime(
20842                    format!(
20843                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20844                        s
20845                    ),
20846                    line,
20847                )
20848                .into());
20849            }
20850            let arr_len = self.scope.array_len(&s);
20851            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20852            let removed = self
20853                .scope
20854                .splice_in_place(&s, off, end, rep_vals)
20855                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20856            return Ok(match ctx {
20857                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20858                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20859            });
20860        }
20861        Err(StrykeError::runtime("splice argument is not an ARRAY reference", line).into())
20862    }
20863
20864    /// Splice's LIST argument is Perl list context — any `@arr` / range /
20865    /// list-returning expression is flattened into the inserted values
20866    /// instead of being scalarized to its element count. Mirrors how
20867    /// `push`/`unshift` evaluate trailing args.
20868    fn eval_splice_replacement(
20869        &mut self,
20870        replacement: &[Expr],
20871    ) -> Result<Vec<StrykeValue>, FlowOrError> {
20872        let saved = self.wantarray_kind;
20873        self.wantarray_kind = WantarrayCtx::List;
20874        let mut out = Vec::new();
20875        for r in replacement {
20876            let v = self.eval_expr_ctx(r, WantarrayCtx::List)?;
20877            if let Some(items) = v.as_array_vec() {
20878                out.extend(items);
20879            } else if v.is_iterator() {
20880                out.extend(v.into_iterator().collect_all());
20881            } else {
20882                out.push(v);
20883            }
20884        }
20885        self.wantarray_kind = saved;
20886        Ok(out)
20887    }
20888
20889    pub(crate) fn eval_splice_expr(
20890        &mut self,
20891        array: &Expr,
20892        offset: Option<&Expr>,
20893        length: Option<&Expr>,
20894        replacement: &[Expr],
20895        ctx: WantarrayCtx,
20896        line: usize,
20897    ) -> Result<StrykeValue, FlowOrError> {
20898        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20899            let offset_val = if let Some(o) = offset {
20900                self.eval_expr(o)?
20901            } else {
20902                StrykeValue::integer(0)
20903            };
20904            let length_val = if let Some(l) = length {
20905                self.eval_expr(l)?
20906            } else {
20907                StrykeValue::UNDEF
20908            };
20909            let rep_vals = self.eval_splice_replacement(replacement)?;
20910            let saved = self.wantarray_kind;
20911            self.wantarray_kind = ctx;
20912            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
20913            self.wantarray_kind = saved;
20914            return out;
20915        }
20916        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20917        let arr_len = self.scope.array_len(&arr_name);
20918        let offset_val = if let Some(o) = offset {
20919            self.eval_expr(o)?
20920        } else {
20921            StrykeValue::integer(0)
20922        };
20923        let length_val = if let Some(l) = length {
20924            self.eval_expr(l)?
20925        } else {
20926            StrykeValue::UNDEF
20927        };
20928        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20929        let rep_vals = self.eval_splice_replacement(replacement)?;
20930        let removed = self
20931            .scope
20932            .splice_in_place(&arr_name, off, end, rep_vals)
20933            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20934        Ok(match ctx {
20935            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20936            WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20937        })
20938    }
20939
20940    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
20941    pub(crate) fn keys_from_value(
20942        val: StrykeValue,
20943        line: usize,
20944    ) -> Result<StrykeValue, FlowOrError> {
20945        if let Some(h) = val.as_hash_map() {
20946            Ok(StrykeValue::array(
20947                h.keys().map(|k| StrykeValue::string(k.clone())).collect(),
20948            ))
20949        } else if let Some(r) = val.as_hash_ref() {
20950            Ok(StrykeValue::array(
20951                r.read()
20952                    .keys()
20953                    .map(|k| StrykeValue::string(k.clone()))
20954                    .collect(),
20955            ))
20956        } else {
20957            Err(StrykeError::runtime("keys requires hash", line).into())
20958        }
20959    }
20960
20961    pub(crate) fn eval_keys_expr(
20962        &mut self,
20963        expr: &Expr,
20964        line: usize,
20965    ) -> Result<StrykeValue, FlowOrError> {
20966        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
20967        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
20968        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
20969        Self::keys_from_value(val, line)
20970    }
20971
20972    /// Result of `values EXPR` after `EXPR` has been evaluated.
20973    pub(crate) fn values_from_value(
20974        val: StrykeValue,
20975        line: usize,
20976    ) -> Result<StrykeValue, FlowOrError> {
20977        if let Some(h) = val.as_hash_map() {
20978            Ok(StrykeValue::array(h.values().cloned().collect()))
20979        } else if let Some(r) = val.as_hash_ref() {
20980            Ok(StrykeValue::array(r.read().values().cloned().collect()))
20981        } else {
20982            Err(StrykeError::runtime("values requires hash", line).into())
20983        }
20984    }
20985
20986    pub(crate) fn eval_values_expr(
20987        &mut self,
20988        expr: &Expr,
20989        line: usize,
20990    ) -> Result<StrykeValue, FlowOrError> {
20991        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
20992        Self::values_from_value(val, line)
20993    }
20994
20995    pub(crate) fn eval_delete_operand(
20996        &mut self,
20997        expr: &Expr,
20998        line: usize,
20999    ) -> Result<StrykeValue, FlowOrError> {
21000        match &expr.kind {
21001            ExprKind::HashElement { hash, key } => {
21002                let k = self.eval_expr(key)?.to_string();
21003                self.touch_env_hash(hash);
21004                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
21005                    let class = obj
21006                        .as_blessed_ref()
21007                        .map(|b| b.class.clone())
21008                        .unwrap_or_default();
21009                    let full = format!("{}::DELETE", class);
21010                    if let Some(sub) = self.subs.get(&full).cloned() {
21011                        return self.call_sub(
21012                            &sub,
21013                            vec![obj, StrykeValue::string(k)],
21014                            WantarrayCtx::Scalar,
21015                            line,
21016                        );
21017                    }
21018                }
21019                self.scope
21020                    .delete_hash_element(hash, &k)
21021                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
21022            }
21023            ExprKind::ArrayElement { array, index } => {
21024                self.check_strict_array_var(array, line)?;
21025                let idx = self.eval_expr(index)?.to_int();
21026                let aname = self.stash_array_name_for_package(array);
21027                self.scope
21028                    .delete_array_element(&aname, idx)
21029                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
21030            }
21031            ExprKind::ArrowDeref {
21032                expr: inner,
21033                index,
21034                kind: DerefKind::Hash,
21035            } => {
21036                let k = self.eval_expr(index)?.to_string();
21037                let container = self.eval_expr(inner)?;
21038                self.delete_arrow_hash_element(container, &k, line)
21039                    .map_err(Into::into)
21040            }
21041            ExprKind::ArrowDeref {
21042                expr: inner,
21043                index,
21044                kind: DerefKind::Array,
21045            } => {
21046                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
21047                    return Err(StrykeError::runtime(
21048                        "delete on array element needs scalar subscript",
21049                        line,
21050                    )
21051                    .into());
21052                }
21053                let container = self.eval_expr(inner)?;
21054                let idx = self.eval_expr(index)?.to_int();
21055                self.delete_arrow_array_element(container, idx, line)
21056                    .map_err(Into::into)
21057            }
21058            // `delete @h{KEYS}` — Perl slice form. Drains every named key,
21059            // returning the list of deleted values (undef when absent).
21060            ExprKind::HashSlice { hash, keys } => {
21061                self.touch_env_hash(hash);
21062                let mut all_keys: Vec<String> = Vec::new();
21063                for key_expr in keys {
21064                    all_keys.extend(self.eval_hash_slice_key_components(key_expr)?);
21065                }
21066                let mut deleted = Vec::with_capacity(all_keys.len());
21067                for k in &all_keys {
21068                    let v = self
21069                        .scope
21070                        .delete_hash_element(hash, k)
21071                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
21072                    deleted.push(v);
21073                }
21074                Ok(StrykeValue::array(deleted))
21075            }
21076            // `delete @a[INDICES]` — Perl array-slice form.
21077            ExprKind::ArraySlice { array, indices } => {
21078                self.check_strict_array_var(array, line)?;
21079                let aname = self.stash_array_name_for_package(array);
21080                let mut all_idx: Vec<i64> = Vec::new();
21081                for idx_expr in indices {
21082                    let v = self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?;
21083                    if let Some(items) = v.as_array_vec() {
21084                        all_idx.extend(items.iter().map(|x| x.to_int()));
21085                    } else if let Some(r) = v.as_array_ref() {
21086                        all_idx.extend(r.read().iter().map(|x| x.to_int()));
21087                    } else if v.is_iterator() {
21088                        all_idx.extend(v.into_iterator().collect_all().iter().map(|x| x.to_int()));
21089                    } else {
21090                        all_idx.push(v.to_int());
21091                    }
21092                }
21093                let mut deleted = Vec::with_capacity(all_idx.len());
21094                for idx in &all_idx {
21095                    let v = self
21096                        .scope
21097                        .delete_array_element(&aname, *idx)
21098                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
21099                    deleted.push(v);
21100                }
21101                Ok(StrykeValue::array(deleted))
21102            }
21103            _ => Err(StrykeError::runtime("delete requires hash or array element", line).into()),
21104        }
21105    }
21106
21107    /// Evaluate a deref-chain in "exists mode" — like [`Self::eval_expr`] but
21108    /// recursively walks `ArrowDeref` chains and turns undef-intermediate
21109    /// derefs into undef (instead of erroring). Used by
21110    /// [`Self::eval_exists_operand`] so `exists $h{x}{y}{z}` returns 0 for
21111    /// any missing level. (BUG-009)
21112    fn eval_expr_exists_mode(&mut self, expr: &Expr) -> Result<StrykeValue, FlowOrError> {
21113        match &expr.kind {
21114            ExprKind::ArrowDeref {
21115                expr: inner,
21116                index,
21117                kind: DerefKind::Hash,
21118            } => {
21119                let inner_val = self.eval_expr_exists_mode(inner)?;
21120                if inner_val.is_undef() {
21121                    return Ok(StrykeValue::UNDEF);
21122                }
21123                if let Some(r) = inner_val.as_hash_ref() {
21124                    let k = self.eval_expr(index)?.to_string();
21125                    return Ok(r.read().get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
21126                }
21127                if let Some(b) = inner_val.as_blessed_ref() {
21128                    let data = b.data.read();
21129                    if let Some(r) = data.as_hash_ref() {
21130                        let k = self.eval_expr(index)?.to_string();
21131                        return Ok(r.read().get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
21132                    }
21133                }
21134                // Struct / class instance — look up the field by name and
21135                // return its value. Without this, `exists $struct->{f}->{k}`
21136                // soft-fails to false even when the field is a real hashref.
21137                if let Some(s) = inner_val.as_struct_inst() {
21138                    let k = self.eval_expr(index)?.to_string();
21139                    if let Some(idx) = s.def.field_index(&k) {
21140                        return Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF));
21141                    }
21142                    return Ok(StrykeValue::UNDEF);
21143                }
21144                if let Some(c) = inner_val.as_class_inst() {
21145                    let k = self.eval_expr(index)?.to_string();
21146                    if let Some(idx) = c.def.field_index(&k) {
21147                        return Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF));
21148                    }
21149                    return Ok(StrykeValue::UNDEF);
21150                }
21151                Ok(StrykeValue::UNDEF)
21152            }
21153            ExprKind::ArrowDeref {
21154                expr: inner,
21155                index,
21156                kind: DerefKind::Array,
21157            } => {
21158                let inner_val = self.eval_expr_exists_mode(inner)?;
21159                if inner_val.is_undef() {
21160                    return Ok(StrykeValue::UNDEF);
21161                }
21162                if let Some(r) = inner_val.as_array_ref() {
21163                    let idx = self.eval_expr(index)?.to_int();
21164                    let arr = r.read();
21165                    let i = if idx < 0 {
21166                        (arr.len() as i64 + idx).max(0) as usize
21167                    } else {
21168                        idx as usize
21169                    };
21170                    return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
21171                }
21172                Ok(StrykeValue::UNDEF)
21173            }
21174            _ => self.eval_expr(expr),
21175        }
21176    }
21177
21178    pub(crate) fn eval_exists_operand(
21179        &mut self,
21180        expr: &Expr,
21181        line: usize,
21182    ) -> Result<StrykeValue, FlowOrError> {
21183        match &expr.kind {
21184            ExprKind::HashElement { hash, key } => {
21185                let k = self.eval_expr(key)?.to_string();
21186                self.touch_env_hash(hash);
21187                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
21188                    let class = obj
21189                        .as_blessed_ref()
21190                        .map(|b| b.class.clone())
21191                        .unwrap_or_default();
21192                    let full = format!("{}::EXISTS", class);
21193                    if let Some(sub) = self.subs.get(&full).cloned() {
21194                        return self.call_sub(
21195                            &sub,
21196                            vec![obj, StrykeValue::string(k)],
21197                            WantarrayCtx::Scalar,
21198                            line,
21199                        );
21200                    }
21201                }
21202                Ok(StrykeValue::integer(
21203                    if self.scope.exists_hash_element(hash, &k) {
21204                        1
21205                    } else {
21206                        0
21207                    },
21208                ))
21209            }
21210            ExprKind::ArrayElement { array, index } => {
21211                self.check_strict_array_var(array, line)?;
21212                let idx = self.eval_expr(index)?.to_int();
21213                let aname = self.stash_array_name_for_package(array);
21214                Ok(StrykeValue::integer(
21215                    if self.scope.exists_array_element(&aname, idx) {
21216                        1
21217                    } else {
21218                        0
21219                    },
21220                ))
21221            }
21222            ExprKind::ArrowDeref {
21223                expr: inner,
21224                index,
21225                kind: DerefKind::Hash,
21226            } => {
21227                let k = self.eval_expr(index)?.to_string();
21228                // Evaluate the chain in "exists mode" — undef intermediates
21229                // propagate as undef instead of erroring on missing-key
21230                // deref, matching Perl's `exists $h{x}{y}{z}` returning 0
21231                // for any missing level. (BUG-009)
21232                let container = match self.eval_expr_exists_mode(inner) {
21233                    Ok(v) => v,
21234                    Err(_) => return Ok(StrykeValue::integer(0)),
21235                };
21236                if container.is_undef() {
21237                    return Ok(StrykeValue::integer(0));
21238                }
21239                let yes = self.exists_arrow_hash_element(container, &k, line)?;
21240                Ok(StrykeValue::integer(if yes { 1 } else { 0 }))
21241            }
21242            ExprKind::ArrowDeref {
21243                expr: inner,
21244                index,
21245                kind: DerefKind::Array,
21246            } => {
21247                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
21248                    return Err(StrykeError::runtime(
21249                        "exists on array element needs scalar subscript",
21250                        line,
21251                    )
21252                    .into());
21253                }
21254                let container = match self.eval_expr_exists_mode(inner) {
21255                    Ok(v) => v,
21256                    Err(_) => return Ok(StrykeValue::integer(0)),
21257                };
21258                if container.is_undef() {
21259                    return Ok(StrykeValue::integer(0));
21260                }
21261                let idx = self.eval_expr(index)?.to_int();
21262                let yes = self.exists_arrow_array_element(container, idx, line)?;
21263                Ok(StrykeValue::integer(if yes { 1 } else { 0 }))
21264            }
21265            ExprKind::SubroutineRef(name) => {
21266                // `exists &name` / `exists &Pkg::name` — true when the
21267                // subroutine has been declared (whether or not it's
21268                // defined, mirroring Perl's `exists &subname`).
21269                let resolved = self.resolve_sub_by_name(name);
21270                Ok(StrykeValue::integer(if resolved.is_some() { 1 } else { 0 }))
21271            }
21272            _ => Err(StrykeError::runtime("exists requires hash or array element", line).into()),
21273        }
21274    }
21275
21276    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
21277    ///
21278    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
21279    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
21280    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
21281    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
21282    /// the new path amortizes the handshake across the whole map.
21283    pub(crate) fn eval_pmap_remote(
21284        &mut self,
21285        cluster_pv: StrykeValue,
21286        list_pv: StrykeValue,
21287        show_progress: bool,
21288        block: &Block,
21289        flat_outputs: bool,
21290        line: usize,
21291    ) -> Result<StrykeValue, FlowOrError> {
21292        let Some(cluster) = cluster_pv.as_remote_cluster() else {
21293            return Err(StrykeError::runtime("pmap_on: expected cluster(...) value", line).into());
21294        };
21295        let items = list_pv.to_list();
21296        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
21297        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
21298            return Err(StrykeError::runtime(
21299                "pmap_on: mysync/atomic capture is not supported for remote workers",
21300                line,
21301            )
21302            .into());
21303        }
21304        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
21305            .map_err(|e| StrykeError::runtime(e, line))?;
21306        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
21307        let block_src = crate::fmt::format_block(block);
21308        let item_jsons = crate::cluster::perl_items_to_json(&items)
21309            .map_err(|e| StrykeError::runtime(e, line))?;
21310
21311        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
21312        // synchronous from the caller's POV, so we drive the bar before/after the call.
21313        let pmap_progress = PmapProgress::new(show_progress, items.len());
21314        let result_values =
21315            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
21316                .map_err(|e| StrykeError::runtime(format!("pmap_on remote: {e}"), line))?;
21317        for _ in 0..result_values.len() {
21318            pmap_progress.tick();
21319        }
21320        pmap_progress.finish();
21321
21322        if flat_outputs {
21323            let flattened: Vec<StrykeValue> = result_values
21324                .into_iter()
21325                .flat_map(|v| v.map_flatten_outputs(true))
21326                .collect();
21327            Ok(StrykeValue::array(flattened))
21328        } else {
21329            Ok(StrykeValue::array(result_values))
21330        }
21331    }
21332
21333    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
21334    pub(crate) fn eval_par_lines_expr(
21335        &mut self,
21336        path: &Expr,
21337        callback: &Expr,
21338        progress: Option<&Expr>,
21339        line: usize,
21340    ) -> Result<StrykeValue, FlowOrError> {
21341        let show_progress = progress
21342            .map(|p| self.eval_expr(p))
21343            .transpose()?
21344            .map(|v| v.is_true())
21345            .unwrap_or(false);
21346        let raw = self.eval_expr(path)?.to_string();
21347        let path_s = self.resolve_stryke_path_string(&raw);
21348        let cb_val = self.eval_expr(callback)?;
21349        let sub = if let Some(s) = cb_val.as_code_ref() {
21350            s
21351        } else {
21352            return Err(StrykeError::runtime(
21353                "par_lines: second argument must be a code reference",
21354                line,
21355            )
21356            .into());
21357        };
21358        let subs = self.subs.clone();
21359        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
21360        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
21361            FlowOrError::Error(StrykeError::runtime(format!("par_lines: {}", e), line))
21362        })?;
21363        let mmap = unsafe {
21364            memmap2::Mmap::map(&file).map_err(|e| {
21365                FlowOrError::Error(StrykeError::runtime(
21366                    format!("par_lines: mmap: {}", e),
21367                    line,
21368                ))
21369            })?
21370        };
21371        let data: &[u8] = &mmap;
21372        if data.is_empty() {
21373            return Ok(StrykeValue::UNDEF);
21374        }
21375        let line_total = crate::par_lines::line_count_bytes(data);
21376        let pmap_progress = PmapProgress::new(show_progress, line_total);
21377        if self.num_threads == 0 {
21378            self.num_threads = rayon::current_num_threads();
21379        }
21380        let num_chunks = self.num_threads.saturating_mul(8).max(1);
21381        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
21382        chunks.into_par_iter().try_for_each(|(start, end)| {
21383            let slice = &data[start..end];
21384            let mut s = 0usize;
21385            while s < slice.len() {
21386                let e = slice[s..]
21387                    .iter()
21388                    .position(|&b| b == b'\n')
21389                    .map(|p| s + p)
21390                    .unwrap_or(slice.len());
21391                let line_bytes = &slice[s..e];
21392                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
21393                let mut local_interp = VMHelper::new();
21394                local_interp.subs = subs.clone();
21395                local_interp.scope.restore_capture(&scope_capture);
21396                local_interp
21397                    .scope
21398                    .restore_atomics(&atomic_arrays, &atomic_hashes);
21399                local_interp.enable_parallel_guard();
21400                local_interp.scope.set_topic(StrykeValue::string(line_str));
21401                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
21402                    Ok(_) => {}
21403                    Err(e) => return Err(e),
21404                }
21405                pmap_progress.tick();
21406                if e >= slice.len() {
21407                    break;
21408                }
21409                s = e + 1;
21410            }
21411            Ok(())
21412        })?;
21413        pmap_progress.finish();
21414        Ok(StrykeValue::UNDEF)
21415    }
21416
21417    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
21418    pub(crate) fn eval_par_walk_expr(
21419        &mut self,
21420        path: &Expr,
21421        callback: &Expr,
21422        progress: Option<&Expr>,
21423        line: usize,
21424    ) -> Result<StrykeValue, FlowOrError> {
21425        let show_progress = progress
21426            .map(|p| self.eval_expr(p))
21427            .transpose()?
21428            .map(|v| v.is_true())
21429            .unwrap_or(false);
21430        let path_val = self.eval_expr(path)?;
21431        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
21432            arr.into_iter()
21433                .map(|v| PathBuf::from(v.to_string()))
21434                .collect()
21435        } else {
21436            vec![PathBuf::from(path_val.to_string())]
21437        };
21438        let cb_val = self.eval_expr(callback)?;
21439        let sub = if let Some(s) = cb_val.as_code_ref() {
21440            s
21441        } else {
21442            return Err(StrykeError::runtime(
21443                "par_walk: second argument must be a code reference",
21444                line,
21445            )
21446            .into());
21447        };
21448        let subs = self.subs.clone();
21449        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
21450
21451        if show_progress {
21452            let paths = crate::par_walk::collect_paths(&roots);
21453            let pmap_progress = PmapProgress::new(true, paths.len());
21454            paths.into_par_iter().try_for_each(|p| {
21455                let s = p.to_string_lossy().into_owned();
21456                let mut local_interp = VMHelper::new();
21457                local_interp.subs = subs.clone();
21458                local_interp.scope.restore_capture(&scope_capture);
21459                local_interp
21460                    .scope
21461                    .restore_atomics(&atomic_arrays, &atomic_hashes);
21462                local_interp.enable_parallel_guard();
21463                local_interp.scope.set_topic(StrykeValue::string(s));
21464                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
21465                    Ok(_) => {}
21466                    Err(e) => return Err(e),
21467                }
21468                pmap_progress.tick();
21469                Ok(())
21470            })?;
21471            pmap_progress.finish();
21472        } else {
21473            for r in &roots {
21474                par_walk_recursive(
21475                    r.as_path(),
21476                    &sub,
21477                    &subs,
21478                    &scope_capture,
21479                    &atomic_arrays,
21480                    &atomic_hashes,
21481                    line,
21482                )?;
21483            }
21484        }
21485        Ok(StrykeValue::UNDEF)
21486    }
21487
21488    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
21489    pub(crate) fn builtin_par_sed(
21490        &mut self,
21491        args: &[StrykeValue],
21492        line: usize,
21493        has_progress: bool,
21494    ) -> StrykeResult<StrykeValue> {
21495        let show_progress = if has_progress {
21496            args.last().map(|v| v.is_true()).unwrap_or(false)
21497        } else {
21498            false
21499        };
21500        let slice = if has_progress {
21501            &args[..args.len().saturating_sub(1)]
21502        } else {
21503            args
21504        };
21505        if slice.len() < 3 {
21506            return Err(StrykeError::runtime(
21507                "par_sed: need pattern, replacement, and at least one file path",
21508                line,
21509            ));
21510        }
21511        let pat_val = &slice[0];
21512        let repl = slice[1].to_string();
21513        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
21514
21515        let re = if let Some(rx) = pat_val.as_regex() {
21516            rx
21517        } else {
21518            let pattern = pat_val.to_string();
21519            match self.compile_regex(&pattern, "g", line) {
21520                Ok(r) => r,
21521                Err(FlowOrError::Error(e)) => return Err(e),
21522                Err(FlowOrError::Flow(f)) => {
21523                    return Err(StrykeError::runtime(format!("par_sed: {:?}", f), line))
21524                }
21525            }
21526        };
21527
21528        let pmap = PmapProgress::new(show_progress, files.len());
21529        let touched = AtomicUsize::new(0);
21530        files.par_iter().try_for_each(|path| {
21531            let content = read_file_text_perl_compat(path)
21532                .map_err(|e| StrykeError::runtime(format!("par_sed {}: {}", path, e), line))?;
21533            let new_s = re.replace_all(&content, &repl);
21534            if new_s != content {
21535                std::fs::write(path, new_s.as_bytes())
21536                    .map_err(|e| StrykeError::runtime(format!("par_sed {}: {}", path, e), line))?;
21537                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
21538            }
21539            pmap.tick();
21540            Ok(())
21541        })?;
21542        pmap.finish();
21543        Ok(StrykeValue::integer(
21544            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
21545        ))
21546    }
21547
21548    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
21549    pub(crate) fn eval_pwatch_expr(
21550        &mut self,
21551        path: &Expr,
21552        callback: &Expr,
21553        line: usize,
21554    ) -> Result<StrykeValue, FlowOrError> {
21555        let pattern_s = self.eval_expr(path)?.to_string();
21556        let cb_val = self.eval_expr(callback)?;
21557        let sub = if let Some(s) = cb_val.as_code_ref() {
21558            s
21559        } else {
21560            return Err(StrykeError::runtime(
21561                "pwatch: second argument must be a code reference",
21562                line,
21563            )
21564            .into());
21565        };
21566        let subs = self.subs.clone();
21567        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
21568        crate::pwatch::run_pwatch(
21569            &pattern_s,
21570            sub,
21571            subs,
21572            scope_capture,
21573            atomic_arrays,
21574            atomic_hashes,
21575            line,
21576        )
21577        .map_err(FlowOrError::Error)
21578    }
21579
21580    /// Interpolate `$var` in s/// replacement strings, preserving numeric backrefs ($1, $2, etc.).
21581    fn interpolate_replacement_string(&self, replacement: &str) -> String {
21582        let mut out = String::with_capacity(replacement.len());
21583        let chars: Vec<char> = replacement.chars().collect();
21584        let mut i = 0;
21585        while i < chars.len() {
21586            if chars[i] == '\\' && i + 1 < chars.len() {
21587                out.push(chars[i]);
21588                out.push(chars[i + 1]);
21589                i += 2;
21590                continue;
21591            }
21592            if chars[i] == '$' && i + 1 < chars.len() {
21593                let start = i;
21594                i += 1;
21595                if chars[i].is_ascii_digit() {
21596                    out.push('$');
21597                    while i < chars.len() && chars[i].is_ascii_digit() {
21598                        out.push(chars[i]);
21599                        i += 1;
21600                    }
21601                    continue;
21602                }
21603                if chars[i] == '&' || chars[i] == '`' || chars[i] == '\'' {
21604                    out.push('$');
21605                    out.push(chars[i]);
21606                    i += 1;
21607                    continue;
21608                }
21609                if !chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{' {
21610                    out.push('$');
21611                    continue;
21612                }
21613                let mut name = String::new();
21614                if chars[i] == '{' {
21615                    i += 1;
21616                    while i < chars.len() && chars[i] != '}' {
21617                        name.push(chars[i]);
21618                        i += 1;
21619                    }
21620                    if i < chars.len() {
21621                        i += 1;
21622                    }
21623                } else {
21624                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
21625                        name.push(chars[i]);
21626                        i += 1;
21627                    }
21628                }
21629                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
21630                    let val = self.scope.get_scalar(&name);
21631                    out.push_str(&val.to_string());
21632                } else if !name.is_empty() {
21633                    out.push_str(&replacement[start..i]);
21634                } else {
21635                    out.push('$');
21636                }
21637                continue;
21638            }
21639            out.push(chars[i]);
21640            i += 1;
21641        }
21642        out
21643    }
21644
21645    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
21646    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
21647        let mut out = String::with_capacity(pattern.len());
21648        let chars: Vec<char> = pattern.chars().collect();
21649        let mut i = 0;
21650        while i < chars.len() {
21651            if chars[i] == '\\' && i + 1 < chars.len() {
21652                // Preserve escape sequences (including \$ which is literal $)
21653                out.push(chars[i]);
21654                out.push(chars[i + 1]);
21655                i += 2;
21656                continue;
21657            }
21658            if chars[i] == '$' && i + 1 < chars.len() {
21659                i += 1;
21660                // `$` at end of pattern is an anchor, not a variable
21661                if i >= chars.len()
21662                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
21663                {
21664                    out.push('$');
21665                    continue;
21666                }
21667                let mut name = String::new();
21668                if chars[i] == '{' {
21669                    i += 1;
21670                    while i < chars.len() && chars[i] != '}' {
21671                        name.push(chars[i]);
21672                        i += 1;
21673                    }
21674                    if i < chars.len() {
21675                        i += 1;
21676                    } // skip }
21677                } else {
21678                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
21679                        name.push(chars[i]);
21680                        i += 1;
21681                    }
21682                }
21683                if !name.is_empty() {
21684                    let val = self.scope.get_scalar(&name);
21685                    out.push_str(&val.to_string());
21686                } else {
21687                    out.push('$');
21688                }
21689                continue;
21690            }
21691            out.push(chars[i]);
21692            i += 1;
21693        }
21694        out
21695    }
21696
21697    pub(crate) fn compile_regex(
21698        &mut self,
21699        pattern: &str,
21700        flags: &str,
21701        line: usize,
21702    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
21703        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
21704        let pattern = if pattern.contains('$') || pattern.contains('@') {
21705            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
21706        } else {
21707            std::borrow::Cow::Borrowed(pattern)
21708        };
21709        let pattern = pattern.as_ref();
21710        // Fast path: same regex as last call (common in loops).
21711        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
21712        let multiline = self.multiline_match;
21713        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
21714            if lp == pattern && lf == flags && *lm == multiline {
21715                return Ok(lr.clone());
21716            }
21717        }
21718        // Slow path: HashMap lookup
21719        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
21720        if let Some(cached) = self.regex_cache.get(&key) {
21721            self.regex_last = Some((
21722                pattern.to_string(),
21723                flags.to_string(),
21724                multiline,
21725                cached.clone(),
21726            ));
21727            return Ok(cached.clone());
21728        }
21729        let expanded = expand_perl_regex_quotemeta(pattern);
21730        let expanded = expand_perl_regex_octal_escapes(&expanded);
21731        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
21732        let mut re_str = String::new();
21733        if flags.contains('i') {
21734            re_str.push_str("(?i)");
21735        }
21736        if flags.contains('s') {
21737            re_str.push_str("(?s)");
21738        }
21739        if flags.contains('m') {
21740            re_str.push_str("(?m)");
21741        }
21742        if flags.contains('x') {
21743            re_str.push_str("(?x)");
21744        }
21745        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
21746        if multiline {
21747            re_str.push_str("(?s)");
21748        }
21749        re_str.push_str(&expanded);
21750        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
21751            FlowOrError::Error(StrykeError::runtime(
21752                format!("Invalid regex /{}/: {}", pattern, e),
21753                line,
21754            ))
21755        })?;
21756        let arc = re;
21757        self.regex_last = Some((
21758            pattern.to_string(),
21759            flags.to_string(),
21760            multiline,
21761            arc.clone(),
21762        ));
21763        self.regex_cache.insert(key, arc.clone());
21764        Ok(arc)
21765    }
21766
21767    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
21768    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
21769        if self.last_readline_handle.is_empty() {
21770            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
21771        }
21772        let n = *self
21773            .handle_line_numbers
21774            .get(&self.last_readline_handle)
21775            .unwrap_or(&0);
21776        if n <= 0 {
21777            return None;
21778        }
21779        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
21780        {
21781            return Some(("<>".to_string(), n));
21782        }
21783        if self.last_readline_handle == "STDIN" {
21784            return Some((self.last_stdin_die_bracket.clone(), n));
21785        }
21786        Some((format!("<{}>", self.last_readline_handle), n))
21787    }
21788
21789    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
21790    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
21791        let mut s = format!(" at {} line {}", self.file, source_line);
21792        if let Some((bracket, n)) = self.die_warn_io_annotation() {
21793            s.push_str(&format!(", {} line {}.", bracket, n));
21794        } else {
21795            s.push('.');
21796        }
21797        s
21798    }
21799
21800    /// Process a line in -n/-p mode via the VM.
21801    ///
21802    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
21803    /// file so `eof` with no arguments matches Perl behavior on that line.
21804    pub fn process_line(
21805        &mut self,
21806        line_str: &str,
21807        _program: &Program,
21808        is_last_input_line: bool,
21809    ) -> StrykeResult<Option<String>> {
21810        // Per input line, run the main body. The compiled chunk ends the main body with a
21811        // `Halt`; in line mode any `END` region is placed AFTER that `Halt`, so per-line
21812        // execution stops at the main body and never runs `END` (which fires once after the
21813        // loop via `run_end_blocks`).
21814        let chunk = self
21815            .line_mode_chunk
21816            .as_ref()
21817            .expect("process_line called without compiled chunk — execute() must run first")
21818            .clone();
21819        crate::run_line_body(&chunk, self, line_str, is_last_input_line)
21820    }
21821}
21822
21823/// Mirrors `vm.rs::both_non_numeric_strings`. Used by the tree-walker's
21824/// `==` / `!=` to decide whether to fall back to string compare in
21825/// stryke non-compat mode.
21826fn both_non_numeric_strings_iv(a: &StrykeValue, b: &StrykeValue) -> bool {
21827    if !a.is_string_like() || !b.is_string_like() {
21828        return false;
21829    }
21830    let sa = a.to_string();
21831    let sb = b.to_string();
21832    let looks = |s: &str| {
21833        let t = s.trim();
21834        !t.is_empty() && t.parse::<f64>().is_ok()
21835    };
21836    !looks(&sa) && !looks(&sb)
21837}
21838
21839fn par_walk_invoke_entry(
21840    path: &Path,
21841    sub: &Arc<StrykeSub>,
21842    subs: &HashMap<String, Arc<StrykeSub>>,
21843    scope_capture: &[(String, StrykeValue)],
21844    atomic_arrays: &[(String, crate::scope::AtomicArray)],
21845    atomic_hashes: &[(String, crate::scope::AtomicHash)],
21846    line: usize,
21847) -> Result<(), FlowOrError> {
21848    let s = path.to_string_lossy().into_owned();
21849    let mut local_interp = VMHelper::new();
21850    local_interp.subs = subs.clone();
21851    local_interp.scope.restore_capture(scope_capture);
21852    local_interp
21853        .scope
21854        .restore_atomics(atomic_arrays, atomic_hashes);
21855    local_interp.enable_parallel_guard();
21856    local_interp.scope.set_topic(StrykeValue::string(s));
21857    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
21858    Ok(())
21859}
21860
21861fn par_walk_recursive(
21862    path: &Path,
21863    sub: &Arc<StrykeSub>,
21864    subs: &HashMap<String, Arc<StrykeSub>>,
21865    scope_capture: &[(String, StrykeValue)],
21866    atomic_arrays: &[(String, crate::scope::AtomicArray)],
21867    atomic_hashes: &[(String, crate::scope::AtomicHash)],
21868    line: usize,
21869) -> Result<(), FlowOrError> {
21870    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
21871        return par_walk_invoke_entry(
21872            path,
21873            sub,
21874            subs,
21875            scope_capture,
21876            atomic_arrays,
21877            atomic_hashes,
21878            line,
21879        );
21880    }
21881    if !path.is_dir() {
21882        return Ok(());
21883    }
21884    par_walk_invoke_entry(
21885        path,
21886        sub,
21887        subs,
21888        scope_capture,
21889        atomic_arrays,
21890        atomic_hashes,
21891        line,
21892    )?;
21893    let read = match std::fs::read_dir(path) {
21894        Ok(r) => r,
21895        Err(_) => return Ok(()),
21896    };
21897    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
21898    entries.par_iter().try_for_each(|e| {
21899        par_walk_recursive(
21900            &e.path(),
21901            sub,
21902            subs,
21903            scope_capture,
21904            atomic_arrays,
21905            atomic_hashes,
21906            line,
21907        )
21908    })?;
21909    Ok(())
21910}
21911
21912/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
21913/// Reformat Rust's `{:e}` / `{:E}` exponent style (`1.234568e4`) to the
21914/// Perl/C convention (`1.234568e+04`). Adds a sign character to the
21915/// exponent and zero-pads it to at least two digits.
21916/// Perl-style magical string increment.
21917///
21918/// Returns `Some(new)` when `s` matches `^[A-Za-z]+[0-9]*$` (i.e. some
21919/// letters, optionally followed by digits, ending at the end of string)
21920/// or is the empty string (which becomes `"1"`). Returns `None` for any
21921/// other shape — pure digits, leading whitespace, mixed letters/digits,
21922/// embedded punctuation, etc. — so the caller can fall back to a plain
21923/// numeric increment.
21924///
21925/// Carry rules:
21926/// - In the digit suffix, `9 -> 0` carries left.
21927/// - In the letter prefix, `z -> a` and `Z -> A` carry left.
21928/// - When a carry exits the leftmost letter, a fresh `a` or `A` is
21929///   prepended (case-matched to the first character of the original).
21930///
21931/// Split a `StrykeValue` into approximately `n_threads` chunks for the
21932/// `par { BLOCK }` runtime. Strings are partitioned on UTF-8 char-aligned
21933/// byte boundaries; arrays/lists on element boundaries. Other scalar
21934/// types (int, float, undef, ref) return a single-chunk Vec containing
21935/// the value unchanged — the caller should handle this fallback.
21936///
21937/// Returned chunks are themselves `StrykeValue` so the worker can bind
21938/// each to `$_` and invoke the user's block.
21939fn par_chunk_value(v: &StrykeValue, n_threads: usize) -> Vec<StrykeValue> {
21940    let n = n_threads.max(1);
21941    // String input: split on char boundaries.
21942    if let Some(s) = v.as_str() {
21943        let bytes = s.as_bytes();
21944        if bytes.len() < 16_384 || n < 2 {
21945            return vec![StrykeValue::string(s)];
21946        }
21947        let target = bytes.len().div_ceil(n);
21948        let mut splits = vec![0usize];
21949        let mut cursor = target;
21950        while cursor < bytes.len() {
21951            // Walk forward until we hit a UTF-8 leading byte (`0xxxxxxx` or `11xxxxxx`).
21952            while cursor < bytes.len() && (bytes[cursor] & 0xC0) == 0x80 {
21953                cursor += 1;
21954            }
21955            splits.push(cursor);
21956            cursor += target;
21957        }
21958        splits.push(bytes.len());
21959        return splits
21960            .windows(2)
21961            .map(|w| {
21962                let chunk = std::str::from_utf8(&bytes[w[0]..w[1]]).unwrap_or("");
21963                StrykeValue::string(chunk.to_string())
21964            })
21965            .collect();
21966    }
21967    // Array / list input: split on element boundaries.
21968    if let Some(arr) = v.as_array_vec() {
21969        if arr.len() < 32 || n < 2 {
21970            return vec![StrykeValue::array(arr)];
21971        }
21972        let target = arr.len().div_ceil(n);
21973        let mut chunks = Vec::with_capacity(n);
21974        for slice in arr.chunks(target) {
21975            chunks.push(StrykeValue::array(slice.to_vec()));
21976        }
21977        return chunks;
21978    }
21979    if let Some(arr_ref) = v.as_array_ref() {
21980        let arr = arr_ref.read().clone();
21981        if arr.len() < 32 || n < 2 {
21982            return vec![StrykeValue::array(arr)];
21983        }
21984        let target = arr.len().div_ceil(n);
21985        let mut chunks = Vec::with_capacity(n);
21986        for slice in arr.chunks(target) {
21987            chunks.push(StrykeValue::array(slice.to_vec()));
21988        }
21989        return chunks;
21990    }
21991    // Fallback: single chunk holding the original value.
21992    vec![v.clone()]
21993}
21994
21995/// Auto-merge a list of `par_reduce` per-chunk results when no explicit
21996/// reduce block is supplied. Picks the merger by inspecting the first
21997/// chunk's value type:
21998///
21999/// - **Hash with numeric values** → key-wise add (canonical histogram merge)
22000/// - **Number** → numeric `+`
22001/// - **Array / list** → concat
22002/// - **String** → concat
22003/// - **Anything else** → return chunks as a flat array (caller can post-process)
22004fn par_reduce_auto_merge(chunks: Vec<StrykeValue>) -> StrykeValue {
22005    if chunks.is_empty() {
22006        return StrykeValue::UNDEF;
22007    }
22008    let first = &chunks[0];
22009    // Hash<number> add-merge.
22010    if let Some(_h) = first.as_hash_ref() {
22011        let mut out: indexmap::IndexMap<String, f64> = indexmap::IndexMap::new();
22012        for chunk in &chunks {
22013            if let Some(hr) = chunk.as_hash_ref() {
22014                for (k, v) in hr.read().iter() {
22015                    *out.entry(k.clone()).or_insert(0.0) += v.to_number();
22016                }
22017            }
22018        }
22019        // Round-trip integer values back to integers so `freq`-style
22020        // hashes stay integer-typed downstream.
22021        let mut indexmap_out: indexmap::IndexMap<String, StrykeValue> = indexmap::IndexMap::new();
22022        for (k, v) in out {
22023            let pv = if v == v.trunc() && v.abs() < 1e15 {
22024                StrykeValue::integer(v as i64)
22025            } else {
22026                StrykeValue::float(v)
22027            };
22028            indexmap_out.insert(k, pv);
22029        }
22030        return StrykeValue::hash_ref(Arc::new(parking_lot::RwLock::new(indexmap_out)));
22031    }
22032    // Numeric add-merge (int or float).
22033    if first.is_integer_like() || first.is_float_like() {
22034        let s: f64 = chunks.iter().map(|v| v.to_number()).sum();
22035        if s == s.trunc() && s.abs() < 1e15 {
22036            return StrykeValue::integer(s as i64);
22037        }
22038        return StrykeValue::float(s);
22039    }
22040    // Array concat.
22041    if first.as_array_vec().is_some() || first.as_array_ref().is_some() {
22042        let mut out = Vec::new();
22043        for v in &chunks {
22044            out.extend(v.map_flatten_outputs(true));
22045        }
22046        return StrykeValue::array(out);
22047    }
22048    // String concat.
22049    if first.is_string_like() {
22050        let mut out = String::new();
22051        for v in &chunks {
22052            out.push_str(&v.to_string());
22053        }
22054        return StrykeValue::string(out);
22055    }
22056    // Fallback: flat list of chunk results.
22057    StrykeValue::array(chunks)
22058}
22059
22060/// Decrement has no magic counterpart in Perl 5; this helper is for `++`
22061/// only.
22062fn perl_magic_str_inc(s: &str) -> Option<String> {
22063    if s.is_empty() {
22064        return Some("1".to_string());
22065    }
22066    let bytes = s.as_bytes();
22067    let mut i = 0;
22068    while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
22069        i += 1;
22070    }
22071    let letters_end = i;
22072    while i < bytes.len() && bytes[i].is_ascii_digit() {
22073        i += 1;
22074    }
22075    if i != bytes.len() {
22076        return None;
22077    }
22078    if letters_end == 0 {
22079        // Pure digits: Perl handles these as plain numbers, so defer.
22080        return None;
22081    }
22082
22083    let mut result: Vec<u8> = bytes.to_vec();
22084    let mut carry = true;
22085    let mut idx = result.len();
22086
22087    // Phase 1: digits, right to left.
22088    while carry && idx > letters_end {
22089        idx -= 1;
22090        if result[idx] == b'9' {
22091            result[idx] = b'0';
22092            // carry stays true
22093        } else {
22094            result[idx] += 1;
22095            carry = false;
22096        }
22097    }
22098
22099    // Phase 2: letters, right to left.
22100    while carry && idx > 0 {
22101        idx -= 1;
22102        let c = result[idx];
22103        if c == b'z' {
22104            result[idx] = b'a';
22105        } else if c == b'Z' {
22106            result[idx] = b'A';
22107        } else {
22108            result[idx] += 1;
22109            carry = false;
22110        }
22111    }
22112
22113    // Phase 3: prepend a fresh letter if the carry escaped.
22114    if carry {
22115        let prepend = if bytes[0].is_ascii_uppercase() {
22116            b'A'
22117        } else {
22118            b'a'
22119        };
22120        let mut grown = Vec::with_capacity(result.len() + 1);
22121        grown.push(prepend);
22122        grown.extend_from_slice(&result);
22123        return String::from_utf8(grown).ok();
22124    }
22125
22126    String::from_utf8(result).ok()
22127}
22128
22129/// `++$x` semantics: try magic string increment first when the value is
22130/// already a string; fall back to a numeric +1 for everything else
22131/// (integers, floats, undef, plain numeric strings).
22132pub(crate) fn perl_inc(v: &StrykeValue) -> StrykeValue {
22133    if let Some(s) = v.as_str() {
22134        if let Some(new_s) = perl_magic_str_inc(&s) {
22135            return StrykeValue::string(new_s);
22136        }
22137    }
22138    StrykeValue::integer(v.to_int() + 1)
22139}
22140
22141fn perl_exponent_form(rust_repr: &str, upper: bool) -> String {
22142    let marker = if upper { 'E' } else { 'e' };
22143    if let Some(pos) = rust_repr.find(marker) {
22144        let (mantissa, after) = rust_repr.split_at(pos);
22145        let exp_part = &after[1..]; // skip the 'e' / 'E'
22146        let (sign, digits) = match exp_part.chars().next() {
22147            Some('+') => ("+", &exp_part[1..]),
22148            Some('-') => ("-", &exp_part[1..]),
22149            _ => ("+", exp_part),
22150        };
22151        let padded = if digits.len() < 2 {
22152            format!("0{}", digits)
22153        } else {
22154            digits.to_string()
22155        };
22156        return format!("{}{}{}{}", mantissa, marker, sign, padded);
22157    }
22158    rust_repr.to_string()
22159}
22160
22161/// Hex-float format (`%a` / `%A`). Produces strings like `0x1.8p+0` for
22162/// 1.5 — sign, normalized hex mantissa, then `p[+-]N` decimal exponent of
22163/// the radix-2 form. Matches C99 / POSIX `%a`.
22164fn perl_hex_float(n: f64, upper: bool) -> String {
22165    if n.is_nan() {
22166        return if upper { "NAN" } else { "nan" }.to_string();
22167    }
22168    if n.is_infinite() {
22169        let sign = if n.is_sign_negative() { "-" } else { "" };
22170        let body = if upper { "INF" } else { "inf" };
22171        return format!("{}{}", sign, body);
22172    }
22173    let prefix = if upper { "0X" } else { "0x" };
22174    let p_letter = if upper { 'P' } else { 'p' };
22175    let bits = n.to_bits();
22176    let sign_bit = bits >> 63;
22177    let exp_bits = (bits >> 52) & 0x7FF;
22178    let mant_bits = bits & 0x000F_FFFF_FFFF_FFFF;
22179    let sign_str = if sign_bit == 1 { "-" } else { "" };
22180    if exp_bits == 0 && mant_bits == 0 {
22181        return format!("{}{}{}{}{}", sign_str, prefix, "0", p_letter, "+0");
22182    }
22183    let (lead_digit, exp_unbiased): (u64, i32) = if exp_bits == 0 {
22184        // Subnormal: implicit leading 0, exponent fixed at -1022.
22185        (0, -1022)
22186    } else {
22187        (1, (exp_bits as i32) - 1023)
22188    };
22189    let exp_sign = if exp_unbiased >= 0 { "+" } else { "-" };
22190    let exp_abs = exp_unbiased.unsigned_abs();
22191    if mant_bits == 0 {
22192        return format!(
22193            "{}{}{}{}{}{}",
22194            sign_str, prefix, lead_digit, p_letter, exp_sign, exp_abs
22195        );
22196    }
22197    // 52 mantissa bits = 13 hex digits.
22198    let mant_hex = format!("{:013x}", mant_bits);
22199    let trimmed = mant_hex.trim_end_matches('0');
22200    let mant_str = if upper {
22201        trimmed.to_uppercase()
22202    } else {
22203        trimmed.to_string()
22204    };
22205    format!(
22206        "{}{}{}.{}{}{}{}",
22207        sign_str, prefix, lead_digit, mant_str, p_letter, exp_sign, exp_abs
22208    )
22209}
22210
22211/// Format a value with `%g`-style "shortest of %e or %f, strip trailing
22212/// zeros". Precision is the number of *significant* digits (default 6).
22213fn perl_g_form(n: f64, prec: usize, upper: bool) -> String {
22214    let prec = prec.max(1);
22215    if !n.is_finite() {
22216        return if upper {
22217            format!("{}", n).to_uppercase()
22218        } else {
22219            format!("{}", n)
22220        };
22221    }
22222    // Compute base-10 exponent.
22223    let abs = n.abs();
22224    let x = if abs == 0.0 {
22225        0i32
22226    } else {
22227        abs.log10().floor() as i32
22228    };
22229    // %g rule: use exponential form if x < -4 OR x >= prec.
22230    let use_e = x < -4 || x >= prec as i32;
22231    // Always work in lowercase-`e` form internally so the trim logic has a
22232    // single shape; upcase the marker letter at the end for `%G`.
22233    let formatted = if use_e {
22234        let raw = format!("{:.*e}", prec - 1, n);
22235        perl_exponent_form(&raw, false)
22236    } else {
22237        let f_prec = (prec as i32 - 1 - x).max(0) as usize;
22238        format!("{:.*}", f_prec, n)
22239    };
22240    // Strip trailing zeros from the fractional part (and a trailing '.'),
22241    // but only on the mantissa side — leave the exponent untouched.
22242    let (mant, exp) = if let Some(pos) = formatted.find('e') {
22243        (formatted[..pos].to_string(), formatted[pos..].to_string())
22244    } else {
22245        (formatted.clone(), String::new())
22246    };
22247    let trimmed = if mant.contains('.') {
22248        let t = mant.trim_end_matches('0');
22249        let t = t.trim_end_matches('.');
22250        t.to_string()
22251    } else {
22252        mant
22253    };
22254    let combined = format!("{}{}", trimmed, exp);
22255    if upper {
22256        combined.replace('e', "E")
22257    } else {
22258        combined
22259    }
22260}
22261
22262/// Public sprintf entry point. Returns the formatted string plus the list
22263/// of `%n` store-targets and counts that the caller should apply via
22264/// [`VMHelper::assign_scalar_ref_deref`]. Callers that don't use `%n`
22265/// can ignore the second tuple element.
22266pub(crate) fn perl_sprintf_format_full<F>(
22267    fmt: &str,
22268    args: &[StrykeValue],
22269    string_for_s: &mut F,
22270) -> Result<(String, Vec<(StrykeValue, i64)>), FlowOrError>
22271where
22272    F: FnMut(&StrykeValue) -> Result<String, FlowOrError>,
22273{
22274    let mut pending_n: Vec<(StrykeValue, i64)> = Vec::new();
22275    let mut result = String::new();
22276    let mut arg_idx = 0;
22277    let chars: Vec<char> = fmt.chars().collect();
22278    let mut i = 0;
22279
22280    // Helper to consume the next arg as an i64 (used for `*` width / precision).
22281    let take_arg_int = |args: &[StrykeValue], idx: &mut usize| -> i64 {
22282        let v = args.get(*idx).cloned().unwrap_or(StrykeValue::UNDEF);
22283        *idx += 1;
22284        v.to_int()
22285    };
22286
22287    while i < chars.len() {
22288        if chars[i] == '%' {
22289            i += 1;
22290            if i >= chars.len() {
22291                break;
22292            }
22293            if chars[i] == '%' {
22294                result.push('%');
22295                i += 1;
22296                continue;
22297            }
22298
22299            // Positional `%N$...`: take this conversion's value from args[N-1]
22300            // instead of advancing the sequential cursor. Must be the very
22301            // first thing after `%`. We peek for `digits$` and rewind if the
22302            // `$` isn't there (the digits could just be a width).
22303            let mut positional: Option<usize> = None;
22304            {
22305                let saved = i;
22306                let mut digits = String::new();
22307                let mut j = i;
22308                while j < chars.len() && chars[j].is_ascii_digit() {
22309                    digits.push(chars[j]);
22310                    j += 1;
22311                }
22312                if j < chars.len() && chars[j] == '$' && !digits.is_empty() {
22313                    if let Ok(n) = digits.parse::<usize>() {
22314                        if n >= 1 {
22315                            positional = Some(n - 1);
22316                            i = j + 1; // consume the digits and the '$'
22317                        }
22318                    }
22319                }
22320                if positional.is_none() {
22321                    i = saved;
22322                }
22323            }
22324
22325            // Parse format specifier
22326            let mut flags = String::new();
22327            while i < chars.len() && "-+ #0".contains(chars[i]) {
22328                flags.push(chars[i]);
22329                i += 1;
22330            }
22331            // Vector flag: `v` (separator = ".") or `*v` (separator = next arg).
22332            // When set, the conversion runs once per byte of the value's
22333            // string form, joining results with the separator.
22334            let mut vector_sep: Option<String> = None;
22335            if i < chars.len() && chars[i] == 'v' {
22336                vector_sep = Some(".".to_string());
22337                i += 1;
22338            } else if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == 'v' {
22339                let sep_arg = args.get(arg_idx).cloned().unwrap_or(StrykeValue::UNDEF);
22340                arg_idx += 1;
22341                vector_sep = Some(sep_arg.to_string());
22342                i += 2;
22343            }
22344            // Width: either `*` (consume an arg) or run of digits.
22345            let mut width = String::new();
22346            let mut left_align = flags.contains('-');
22347            if i < chars.len() && chars[i] == '*' {
22348                let n = take_arg_int(args, &mut arg_idx);
22349                if n < 0 {
22350                    // Negative width means left-align with |n|.
22351                    left_align = true;
22352                    width = (-n).to_string();
22353                } else {
22354                    width = n.to_string();
22355                }
22356                i += 1;
22357            } else {
22358                while i < chars.len() && chars[i].is_ascii_digit() {
22359                    width.push(chars[i]);
22360                    i += 1;
22361                }
22362            }
22363            // Precision: `.*` or `.<digits>` (or nothing).
22364            let mut precision = String::new();
22365            if i < chars.len() && chars[i] == '.' {
22366                i += 1;
22367                if i < chars.len() && chars[i] == '*' {
22368                    let n = take_arg_int(args, &mut arg_idx);
22369                    precision = n.max(0).to_string();
22370                    i += 1;
22371                } else {
22372                    while i < chars.len() && chars[i].is_ascii_digit() {
22373                        precision.push(chars[i]);
22374                        i += 1;
22375                    }
22376                    // ".<no digits>" means precision 0 (Perl/C convention).
22377                    if precision.is_empty() {
22378                        precision = "0".to_string();
22379                    }
22380                }
22381            }
22382            if i >= chars.len() {
22383                break;
22384            }
22385            let spec = chars[i];
22386            i += 1;
22387
22388            // For vector conversions the conversion's value-arg is the
22389            // string whose bytes we'll iterate; for non-vector, it's the
22390            // value we format. Either way the index resolution is the
22391            // same: positional or sequential.
22392            let arg = if let Some(idx) = positional {
22393                args.get(idx).cloned().unwrap_or(StrykeValue::UNDEF)
22394            } else {
22395                let v = args.get(arg_idx).cloned().unwrap_or(StrykeValue::UNDEF);
22396                arg_idx += 1;
22397                v
22398            };
22399
22400            let w: usize = width.parse().unwrap_or(0);
22401            let p: usize = precision.parse().unwrap_or(6);
22402
22403            let zero_pad = flags.contains('0') && !left_align;
22404            let plus = flags.contains('+');
22405            let space = flags.contains(' ');
22406            let hash = flags.contains('#');
22407
22408            // Apply width + alignment to a body string. Honors zero-pad for
22409            // numerics (caller passes the raw signed body so we can splice
22410            // zeros after the sign).
22411            let pad_align = |body: &str, width: usize, left: bool, zero: bool| -> String {
22412                if width == 0 || body.len() >= width {
22413                    return body.to_string();
22414                }
22415                if zero && !left {
22416                    if let Some(rest) = body.strip_prefix('-') {
22417                        return format!("-{:0>width$}", rest, width = width - 1);
22418                    }
22419                    if let Some(rest) = body.strip_prefix('+') {
22420                        return format!("+{:0>width$}", rest, width = width - 1);
22421                    }
22422                    return format!("{:0>width$}", body, width = width);
22423                }
22424                if left {
22425                    format!("{:<width$}", body, width = width)
22426                } else {
22427                    format!("{:>width$}", body, width = width)
22428                }
22429            };
22430
22431            // Format a single integer with the inner spec for `%v...`. No
22432            // width/precision is applied here — those are deferred to the
22433            // joined result. Supports the common int-shape conversions.
22434            let format_int_for_vector = |n: i64, spec: char| -> String {
22435                match spec {
22436                    'd' | 'i' => format!("{}", n),
22437                    'u' => format!("{}", n as u64),
22438                    'x' => {
22439                        if hash && n != 0 {
22440                            format!("0x{:x}", n)
22441                        } else {
22442                            format!("{:x}", n)
22443                        }
22444                    }
22445                    'X' => {
22446                        if hash && n != 0 {
22447                            format!("0X{:X}", n)
22448                        } else {
22449                            format!("{:X}", n)
22450                        }
22451                    }
22452                    'o' => {
22453                        if hash && n != 0 {
22454                            format!("0{:o}", n)
22455                        } else {
22456                            format!("{:o}", n)
22457                        }
22458                    }
22459                    'b' => {
22460                        if hash && n != 0 {
22461                            format!("0b{:b}", n)
22462                        } else {
22463                            format!("{:b}", n)
22464                        }
22465                    }
22466                    'c' => char::from_u32(n as u32)
22467                        .map(|c| c.to_string())
22468                        .unwrap_or_default(),
22469                    _ => format!("{}", n),
22470                }
22471            };
22472
22473            // `%v` short-circuit: format each byte of the arg's string form
22474            // with the inner spec, join with `vector_sep`, then pad/align
22475            // the joined string. Skips the regular per-spec match below.
22476            if let Some(ref sep) = vector_sep {
22477                let s = arg.to_string();
22478                let parts: Vec<String> = s
22479                    .bytes()
22480                    .map(|b| format_int_for_vector(b as i64, spec))
22481                    .collect();
22482                let body = parts.join(sep);
22483                let final_body = if width.is_empty() {
22484                    body
22485                } else if left_align {
22486                    format!("{:<width$}", body, width = w)
22487                } else {
22488                    format!("{:>width$}", body, width = w)
22489                };
22490                result.push_str(&final_body);
22491                continue;
22492            }
22493
22494            let formatted = match spec {
22495                'd' | 'i' => {
22496                    let v = arg.to_int();
22497                    let body = if plus && v >= 0 {
22498                        format!("+{}", v)
22499                    } else if space && v >= 0 {
22500                        format!(" {}", v)
22501                    } else {
22502                        format!("{}", v)
22503                    };
22504                    pad_align(&body, w, left_align, zero_pad)
22505                }
22506                'u' => {
22507                    let v = arg.to_int() as u64;
22508                    pad_align(&format!("{}", v), w, left_align, zero_pad)
22509                }
22510                'f' => {
22511                    let n = arg.to_number();
22512                    let body = if plus && n.is_sign_positive() {
22513                        format!("+{:.*}", p, n)
22514                    } else if space && n.is_sign_positive() {
22515                        format!(" {:.*}", p, n)
22516                    } else {
22517                        format!("{:.*}", p, n)
22518                    };
22519                    pad_align(&body, w, left_align, zero_pad)
22520                }
22521                'e' => {
22522                    let n = arg.to_number();
22523                    let raw = format!("{:.*e}", p, n);
22524                    let body0 = perl_exponent_form(&raw, false);
22525                    let body = if plus && n.is_sign_positive() {
22526                        format!("+{}", body0)
22527                    } else if space && n.is_sign_positive() {
22528                        format!(" {}", body0)
22529                    } else {
22530                        body0
22531                    };
22532                    pad_align(&body, w, left_align, zero_pad)
22533                }
22534                'E' => {
22535                    let n = arg.to_number();
22536                    let raw = format!("{:.*E}", p, n);
22537                    let body0 = perl_exponent_form(&raw, true);
22538                    let body = if plus && n.is_sign_positive() {
22539                        format!("+{}", body0)
22540                    } else if space && n.is_sign_positive() {
22541                        format!(" {}", body0)
22542                    } else {
22543                        body0
22544                    };
22545                    pad_align(&body, w, left_align, zero_pad)
22546                }
22547                'g' => {
22548                    let n = arg.to_number();
22549                    // For %g, precision means "significant digits" (default 6).
22550                    let prec_g = if precision.is_empty() { 6 } else { p };
22551                    let body0 = perl_g_form(n, prec_g, false);
22552                    let body = if plus && n.is_sign_positive() {
22553                        format!("+{}", body0)
22554                    } else if space && n.is_sign_positive() {
22555                        format!(" {}", body0)
22556                    } else {
22557                        body0
22558                    };
22559                    pad_align(&body, w, left_align, zero_pad)
22560                }
22561                'G' => {
22562                    let n = arg.to_number();
22563                    let prec_g = if precision.is_empty() { 6 } else { p };
22564                    let body0 = perl_g_form(n, prec_g, true);
22565                    let body = if plus && n.is_sign_positive() {
22566                        format!("+{}", body0)
22567                    } else if space && n.is_sign_positive() {
22568                        format!(" {}", body0)
22569                    } else {
22570                        body0
22571                    };
22572                    pad_align(&body, w, left_align, zero_pad)
22573                }
22574                's' => {
22575                    let s = string_for_s(&arg)?;
22576                    let body = if !precision.is_empty() {
22577                        s.chars().take(p).collect::<String>()
22578                    } else {
22579                        s
22580                    };
22581                    if left_align {
22582                        format!("{:<width$}", body, width = w)
22583                    } else {
22584                        format!("{:>width$}", body, width = w)
22585                    }
22586                }
22587                'x' => {
22588                    let v = arg.to_int();
22589                    let body = if hash && v != 0 {
22590                        format!("0x{:x}", v)
22591                    } else {
22592                        format!("{:x}", v)
22593                    };
22594                    pad_align(&body, w, left_align, zero_pad)
22595                }
22596                'X' => {
22597                    let v = arg.to_int();
22598                    let body = if hash && v != 0 {
22599                        format!("0X{:X}", v)
22600                    } else {
22601                        format!("{:X}", v)
22602                    };
22603                    pad_align(&body, w, left_align, zero_pad)
22604                }
22605                'o' => {
22606                    let v = arg.to_int();
22607                    let body = if hash && v != 0 {
22608                        format!("0{:o}", v)
22609                    } else {
22610                        format!("{:o}", v)
22611                    };
22612                    pad_align(&body, w, left_align, zero_pad)
22613                }
22614                'b' => {
22615                    let v = arg.to_int();
22616                    let body = if hash && v != 0 {
22617                        format!("0b{:b}", v)
22618                    } else {
22619                        format!("{:b}", v)
22620                    };
22621                    pad_align(&body, w, left_align, zero_pad)
22622                }
22623                'c' => char::from_u32(arg.to_int() as u32)
22624                    .map(|c| c.to_string())
22625                    .unwrap_or_default(),
22626                'a' | 'A' => {
22627                    let upper = spec == 'A';
22628                    let body0 = perl_hex_float(arg.to_number(), upper);
22629                    let body = if plus && !body0.starts_with('-') {
22630                        format!("+{}", body0)
22631                    } else if space && !body0.starts_with('-') {
22632                        format!(" {}", body0)
22633                    } else {
22634                        body0
22635                    };
22636                    pad_align(&body, w, left_align, zero_pad)
22637                }
22638                'p' => {
22639                    // Stryke uses placeholder addresses for refs; emit the
22640                    // same `0x...` form here so output stays deterministic
22641                    // and machine-comparable across runs.
22642                    pad_align("0x...", w, left_align, false)
22643                }
22644                'n' => {
22645                    // Write the number of bytes emitted so far into the
22646                    // referent of the arg (must be a scalar ref, e.g.
22647                    // `\$count`). `%n` does NOT consume an output slot, so
22648                    // the formatted body is empty. The store is queued and
22649                    // applied by the caller after formatting finishes —
22650                    // works for both `HeapObject::ScalarRef` and the
22651                    // `ScalarBindingRef` shape that `\$my_var` produces.
22652                    pending_n.push((arg.clone(), result.len() as i64));
22653                    String::new()
22654                }
22655                _ => arg.to_string(),
22656            };
22657
22658            result.push_str(&formatted);
22659        } else {
22660            result.push(chars[i]);
22661            i += 1;
22662        }
22663    }
22664    Ok((result, pending_n))
22665}
22666
22667#[cfg(test)]
22668mod regex_expand_tests {
22669    use super::VMHelper;
22670
22671    #[test]
22672    fn compile_regex_quotemeta_qe_matches_literal() {
22673        let mut i = VMHelper::new();
22674        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
22675        assert!(re.is_match("a.c"));
22676        assert!(!re.is_match("abc"));
22677    }
22678
22679    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
22680    /// stay literal (not rewritten to `(?:\n?\z)`).
22681    #[test]
22682    fn compile_regex_char_class_leading_close_bracket_is_literal() {
22683        let mut i = VMHelper::new();
22684        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
22685        assert!(re.is_match("$"));
22686        assert!(re.is_match("]"));
22687        assert!(!re.is_match("x"));
22688    }
22689}
22690
22691#[cfg(test)]
22692mod special_scalar_name_tests {
22693    use super::VMHelper;
22694
22695    #[test]
22696    fn special_scalar_name_for_get_matches_magic_globals() {
22697        assert!(VMHelper::is_special_scalar_name_for_get("0"));
22698        assert!(VMHelper::is_special_scalar_name_for_get("!"));
22699        assert!(VMHelper::is_special_scalar_name_for_get("^W"));
22700        assert!(VMHelper::is_special_scalar_name_for_get("^O"));
22701        assert!(VMHelper::is_special_scalar_name_for_get("^MATCH"));
22702        assert!(VMHelper::is_special_scalar_name_for_get("<"));
22703        assert!(VMHelper::is_special_scalar_name_for_get("?"));
22704        assert!(VMHelper::is_special_scalar_name_for_get("|"));
22705        assert!(VMHelper::is_special_scalar_name_for_get("^UNICODE"));
22706        assert!(VMHelper::is_special_scalar_name_for_get("\""));
22707        assert!(!VMHelper::is_special_scalar_name_for_get("foo"));
22708        assert!(!VMHelper::is_special_scalar_name_for_get("plainvar"));
22709    }
22710
22711    #[test]
22712    fn special_scalar_name_for_set_matches_set_special_var_arms() {
22713        assert!(VMHelper::is_special_scalar_name_for_set("0"));
22714        assert!(VMHelper::is_special_scalar_name_for_set("^D"));
22715        assert!(VMHelper::is_special_scalar_name_for_set("^H"));
22716        assert!(VMHelper::is_special_scalar_name_for_set("^WARNING_BITS"));
22717        assert!(VMHelper::is_special_scalar_name_for_set("ARGV"));
22718        assert!(VMHelper::is_special_scalar_name_for_set("|"));
22719        assert!(VMHelper::is_special_scalar_name_for_set("?"));
22720        assert!(VMHelper::is_special_scalar_name_for_set("^UNICODE"));
22721        assert!(VMHelper::is_special_scalar_name_for_set("."));
22722        assert!(!VMHelper::is_special_scalar_name_for_set("foo"));
22723        assert!(!VMHelper::is_special_scalar_name_for_set("__PACKAGE__"));
22724    }
22725
22726    #[test]
22727    fn caret_and_id_specials_roundtrip_get() {
22728        let i = VMHelper::new();
22729        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
22730        assert_eq!(
22731            i.get_special_var("^V").to_string(),
22732            format!("v{}", env!("CARGO_PKG_VERSION"))
22733        );
22734        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
22735        assert!(i.get_special_var("^T").to_int() >= 0);
22736        #[cfg(unix)]
22737        {
22738            assert!(i.get_special_var("<").to_int() >= 0);
22739        }
22740    }
22741
22742    #[test]
22743    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
22744        let mut i = VMHelper::new();
22745        i.last_readline_handle.clear();
22746        i.line_number = 3;
22747        i.prepare_flip_flop_vm_slots(1);
22748        assert_eq!(
22749            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
22750            1
22751        );
22752        assert!(i.flip_flop_active[0]);
22753        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
22754        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
22755        assert_eq!(
22756            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
22757            1
22758        );
22759        assert!(i.flip_flop_active[0]);
22760    }
22761
22762    #[test]
22763    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
22764        let mut i = VMHelper::new();
22765        i.last_readline_handle.clear();
22766        i.line_number = 2;
22767        i.prepare_flip_flop_vm_slots(1);
22768        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
22769        assert!(i.flip_flop_active[0]);
22770        i.line_number = 3;
22771        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
22772        assert!(!i.flip_flop_active[0]);
22773        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
22774    }
22775}