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, CaptureResult, PerlBarrier, PerlDataFrame, PerlGenerator, PerlHeap,
37    PerlPpool, PipelineInner, PipelineOp, RemoteCluster, StrykeAsyncTask, StrykeSub, StrykeValue,
38};
39
40/// Merge two counting-hash accumulators (parallel `preduce_init` partials).
41/// Returns a hashref so arrow deref (`$acc->{k}`) stays valid after parallel merge.
42pub(crate) fn preduce_init_merge_maps(
43    mut acc: IndexMap<String, StrykeValue>,
44    b: IndexMap<String, StrykeValue>,
45) -> StrykeValue {
46    for (k, v2) in b {
47        acc.entry(k)
48            .and_modify(|v1| *v1 = StrykeValue::float(v1.to_number() + v2.to_number()))
49            .or_insert(v2);
50    }
51    StrykeValue::hash_ref(Arc::new(RwLock::new(acc)))
52}
53
54/// `(off, end)` for `splice` / `arr.drain(off..end)` — Perl negative OFFSET/LENGTH; clamps offset to array length.
55#[inline]
56fn splice_compute_range(
57    arr_len: usize,
58    offset_val: &StrykeValue,
59    length_val: &StrykeValue,
60) -> (usize, usize) {
61    let off_i = offset_val.to_int();
62    let off = if off_i < 0 {
63        arr_len.saturating_sub((-off_i) as usize)
64    } else {
65        (off_i as usize).min(arr_len)
66    };
67    let rest = arr_len.saturating_sub(off);
68    let take = if length_val.is_undef() {
69        rest
70    } else {
71        let l = length_val.to_int();
72        if l < 0 {
73            rest.saturating_sub((-l) as usize)
74        } else {
75            (l as usize).min(rest)
76        }
77    };
78    let end = (off + take).min(arr_len);
79    (off, end)
80}
81
82/// Combine two partial results from `preduce_init`: hash/hashref maps add per-key counts; otherwise
83/// the fold block is invoked with `$a` / `$b` as the two partial accumulators (associative combine).
84pub(crate) fn merge_preduce_init_partials(
85    a: StrykeValue,
86    b: StrykeValue,
87    block: &Block,
88    subs: &HashMap<String, Arc<StrykeSub>>,
89    scope_capture: &[(String, StrykeValue)],
90) -> StrykeValue {
91    if let (Some(m1), Some(m2)) = (a.as_hash_map(), b.as_hash_map()) {
92        return preduce_init_merge_maps(m1, m2);
93    }
94    if let (Some(r1), Some(r2)) = (a.as_hash_ref(), b.as_hash_ref()) {
95        let m1 = r1.read().clone();
96        let m2 = r2.read().clone();
97        return preduce_init_merge_maps(m1, m2);
98    }
99    if let Some(m1) = a.as_hash_map() {
100        if let Some(r2) = b.as_hash_ref() {
101            let m2 = r2.read().clone();
102            return preduce_init_merge_maps(m1, m2);
103        }
104    }
105    if let Some(r1) = a.as_hash_ref() {
106        if let Some(m2) = b.as_hash_map() {
107            let m1 = r1.read().clone();
108            return preduce_init_merge_maps(m1, m2);
109        }
110    }
111    let mut local_interp = VMHelper::new();
112    local_interp.subs = subs.clone();
113    local_interp.scope.restore_capture(scope_capture);
114    local_interp.enable_parallel_guard();
115    local_interp
116        .scope
117        .declare_array("_", vec![a.clone(), b.clone()]);
118    local_interp.scope.set_sort_pair(a, b);
119    match local_interp.exec_block(block) {
120        Ok(val) => val,
121        Err(_) => StrykeValue::UNDEF,
122    }
123}
124
125/// Seed each parallel chunk from `init` without sharing mutable hashref storage (plain `clone` on
126/// `HashRef` reuses the same `Arc<RwLock<…>>`).
127pub(crate) fn preduce_init_fold_identity(init: &StrykeValue) -> StrykeValue {
128    if let Some(m) = init.as_hash_map() {
129        return StrykeValue::hash(m.clone());
130    }
131    if let Some(r) = init.as_hash_ref() {
132        return StrykeValue::hash_ref(Arc::new(RwLock::new(r.read().clone())));
133    }
134    init.clone()
135}
136
137pub(crate) fn fold_preduce_init_step(
138    subs: &HashMap<String, Arc<StrykeSub>>,
139    scope_capture: &[(String, StrykeValue)],
140    block: &Block,
141    acc: StrykeValue,
142    item: StrykeValue,
143) -> StrykeValue {
144    let mut local_interp = VMHelper::new();
145    local_interp.subs = subs.clone();
146    local_interp.scope.restore_capture(scope_capture);
147    local_interp.enable_parallel_guard();
148    local_interp
149        .scope
150        .declare_array("_", vec![acc.clone(), item.clone()]);
151    local_interp.scope.set_sort_pair(acc, item);
152    match local_interp.exec_block(block) {
153        Ok(val) => val,
154        Err(_) => StrykeValue::UNDEF,
155    }
156}
157
158/// `use feature 'say'`
159pub const FEAT_SAY: u64 = 1 << 0;
160/// `use feature 'state'`
161pub const FEAT_STATE: u64 = 1 << 1;
162/// `use feature 'switch'` (given/when when fully wired)
163pub const FEAT_SWITCH: u64 = 1 << 2;
164/// `use feature 'unicode_strings'`
165pub const FEAT_UNICODE_STRINGS: u64 = 1 << 3;
166
167/// Flow control signals propagated via Result.
168#[derive(Debug)]
169pub(crate) enum Flow {
170    Return(StrykeValue),
171    Last(Option<String>),
172    Next(Option<String>),
173    Redo(Option<String>),
174    Yield(StrykeValue),
175    /// `goto &sub` — tail-call: replace current sub with the named one, keeping @_.
176    GotoSub(String),
177}
178
179pub(crate) type ExecResult = Result<StrykeValue, FlowOrError>;
180
181#[derive(Debug)]
182pub(crate) enum FlowOrError {
183    Flow(Flow),
184    Error(StrykeError),
185}
186
187impl From<StrykeError> for FlowOrError {
188    fn from(e: StrykeError) -> Self {
189        FlowOrError::Error(e)
190    }
191}
192
193impl From<Flow> for FlowOrError {
194    fn from(f: Flow) -> Self {
195        FlowOrError::Flow(f)
196    }
197}
198
199/// Bindings introduced by a successful algebraic [`MatchPattern`] (scalar vs array).
200enum PatternBinding {
201    Scalar(String, StrykeValue),
202    Array(String, Vec<StrykeValue>),
203}
204
205/// Perl `$]` — numeric language level (`5 + minor/1000 + patch/1_000_000`).
206/// Emulated Perl 5.x level (not the `stryke` crate semver).
207pub fn perl_bracket_version() -> f64 {
208    const PERL_EMUL_MINOR: u32 = 38;
209    const PERL_EMUL_PATCH: u32 = 0;
210    5.0 + (PERL_EMUL_MINOR as f64) / 1000.0 + (PERL_EMUL_PATCH as f64) / 1_000_000.0
211}
212
213/// Cheap seed for [`StdRng`] at startup (avoids `getentropy` / blocking sources).
214#[inline]
215fn fast_rng_seed() -> u64 {
216    let local: u8 = 0;
217    let addr = &local as *const u8 as u64;
218    (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ addr
219}
220
221/// `$^X` — cache `current_exe()` once per process (tiny win on repeated `Interpreter::new`).
222fn cached_executable_path() -> String {
223    static CACHED: OnceLock<String> = OnceLock::new();
224    CACHED
225        .get_or_init(|| {
226            std::env::current_exe()
227                .map(|p| p.to_string_lossy().into_owned())
228                .unwrap_or_else(|_| "stryke".to_string())
229        })
230        .clone()
231}
232
233fn build_term_hash() -> IndexMap<String, StrykeValue> {
234    let mut m = IndexMap::new();
235    m.insert(
236        "TERM".into(),
237        StrykeValue::string(std::env::var("TERM").unwrap_or_default()),
238    );
239    m.insert(
240        "COLORTERM".into(),
241        StrykeValue::string(std::env::var("COLORTERM").unwrap_or_default()),
242    );
243
244    let (rows, cols) = term_size();
245    m.insert("rows".into(), StrykeValue::integer(rows));
246    m.insert("cols".into(), StrykeValue::integer(cols));
247
248    #[cfg(unix)]
249    let is_tty = unsafe { libc::isatty(1) != 0 };
250    #[cfg(not(unix))]
251    let is_tty = false;
252    m.insert(
253        "is_tty".into(),
254        StrykeValue::integer(if is_tty { 1 } else { 0 }),
255    );
256
257    m
258}
259
260fn term_size() -> (i64, i64) {
261    #[cfg(unix)]
262    {
263        unsafe {
264            let mut ws: libc::winsize = std::mem::zeroed();
265            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 {
266                return (ws.ws_row as i64, ws.ws_col as i64);
267            }
268        }
269    }
270    let rows = std::env::var("LINES")
271        .ok()
272        .and_then(|s| s.parse().ok())
273        .unwrap_or(24);
274    let cols = std::env::var("COLUMNS")
275        .ok()
276        .and_then(|s| s.parse().ok())
277        .unwrap_or(80);
278    (rows, cols)
279}
280
281#[cfg(unix)]
282fn build_uname_hash() -> IndexMap<String, StrykeValue> {
283    fn uts_field(slice: &[libc::c_char]) -> String {
284        let n = slice.iter().take_while(|&&c| c != 0).count();
285        let bytes: Vec<u8> = slice[..n].iter().map(|&c| c as u8).collect();
286        String::from_utf8_lossy(&bytes).into_owned()
287    }
288    let mut m = IndexMap::new();
289    let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
290    if unsafe { libc::uname(&mut uts) } == 0 {
291        m.insert(
292            "sysname".into(),
293            StrykeValue::string(uts_field(uts.sysname.as_slice())),
294        );
295        m.insert(
296            "nodename".into(),
297            StrykeValue::string(uts_field(uts.nodename.as_slice())),
298        );
299        m.insert(
300            "release".into(),
301            StrykeValue::string(uts_field(uts.release.as_slice())),
302        );
303        m.insert(
304            "version".into(),
305            StrykeValue::string(uts_field(uts.version.as_slice())),
306        );
307        m.insert(
308            "machine".into(),
309            StrykeValue::string(uts_field(uts.machine.as_slice())),
310        );
311    }
312    m
313}
314
315#[cfg(unix)]
316fn build_limits_hash() -> IndexMap<String, StrykeValue> {
317    use libc::{getrlimit, rlimit, RLIM_INFINITY};
318    #[cfg(target_os = "linux")]
319    type RlimitResource = libc::__rlimit_resource_t;
320    #[cfg(not(target_os = "linux"))]
321    type RlimitResource = libc::c_int;
322    fn get_limit(resource: RlimitResource) -> (i64, i64) {
323        let mut rlim = rlimit {
324            rlim_cur: 0,
325            rlim_max: 0,
326        };
327        if unsafe { getrlimit(resource, &mut rlim) } == 0 {
328            let cur = if rlim.rlim_cur == RLIM_INFINITY {
329                -1
330            } else {
331                rlim.rlim_cur as i64
332            };
333            let max = if rlim.rlim_max == RLIM_INFINITY {
334                -1
335            } else {
336                rlim.rlim_max as i64
337            };
338            (cur, max)
339        } else {
340            (-1, -1)
341        }
342    }
343    let mut m = IndexMap::new();
344    let (cur, max) = get_limit(libc::RLIMIT_NOFILE);
345    m.insert("nofile".into(), StrykeValue::integer(cur));
346    m.insert("nofile_max".into(), StrykeValue::integer(max));
347    let (cur, max) = get_limit(libc::RLIMIT_STACK);
348    m.insert("stack".into(), StrykeValue::integer(cur));
349    m.insert("stack_max".into(), StrykeValue::integer(max));
350    let (cur, max) = get_limit(libc::RLIMIT_AS);
351    m.insert("as".into(), StrykeValue::integer(cur));
352    m.insert("as_max".into(), StrykeValue::integer(max));
353    let (cur, max) = get_limit(libc::RLIMIT_DATA);
354    m.insert("data".into(), StrykeValue::integer(cur));
355    m.insert("data_max".into(), StrykeValue::integer(max));
356    let (cur, max) = get_limit(libc::RLIMIT_FSIZE);
357    m.insert("fsize".into(), StrykeValue::integer(cur));
358    m.insert("fsize_max".into(), StrykeValue::integer(max));
359    let (cur, max) = get_limit(libc::RLIMIT_CORE);
360    m.insert("core".into(), StrykeValue::integer(cur));
361    m.insert("core_max".into(), StrykeValue::integer(max));
362    let (cur, max) = get_limit(libc::RLIMIT_CPU);
363    m.insert("cpu".into(), StrykeValue::integer(cur));
364    m.insert("cpu_max".into(), StrykeValue::integer(max));
365    let (cur, max) = get_limit(libc::RLIMIT_NPROC);
366    m.insert("nproc".into(), StrykeValue::integer(cur));
367    m.insert("nproc_max".into(), StrykeValue::integer(max));
368    #[cfg(target_os = "linux")]
369    {
370        let (cur, max) = get_limit(libc::RLIMIT_MEMLOCK);
371        m.insert("memlock".into(), StrykeValue::integer(cur));
372        m.insert("memlock_max".into(), StrykeValue::integer(max));
373    }
374    m
375}
376
377/// Context of the **current** subroutine call (`wantarray`).
378#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
379pub(crate) enum WantarrayCtx {
380    #[default]
381    Scalar,
382    List,
383    Void,
384}
385
386impl WantarrayCtx {
387    #[inline]
388    pub(crate) fn from_byte(b: u8) -> Self {
389        match b {
390            1 => Self::List,
391            2 => Self::Void,
392            _ => Self::Scalar,
393        }
394    }
395
396    #[inline]
397    pub(crate) fn as_byte(self) -> u8 {
398        match self {
399            Self::Scalar => 0,
400            Self::List => 1,
401            Self::Void => 2,
402        }
403    }
404}
405
406/// Minimum log level filter for `log_*` / `log_json` (trace = most verbose).
407#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
408pub(crate) enum LogLevelFilter {
409    Trace,
410    Debug,
411    Info,
412    Warn,
413    Error,
414}
415
416impl LogLevelFilter {
417    pub(crate) fn parse(s: &str) -> Option<Self> {
418        match s.trim().to_ascii_lowercase().as_str() {
419            "trace" => Some(Self::Trace),
420            "debug" => Some(Self::Debug),
421            "info" => Some(Self::Info),
422            "warn" | "warning" => Some(Self::Warn),
423            "error" => Some(Self::Error),
424            _ => None,
425        }
426    }
427
428    pub(crate) fn as_str(self) -> &'static str {
429        match self {
430            Self::Trace => "trace",
431            Self::Debug => "debug",
432            Self::Info => "info",
433            Self::Warn => "warn",
434            Self::Error => "error",
435        }
436    }
437}
438
439/// True when `@$aref->[IX]` / `IX` needs **list** context on the RHS of `=` (multi-slot slice).
440fn arrow_deref_array_assign_rhs_list_ctx(index: &Expr) -> bool {
441    match &index.kind {
442        ExprKind::Range { .. } | ExprKind::SliceRange { .. } => true,
443        ExprKind::QW(ws) => ws.len() > 1,
444        ExprKind::List(el) => {
445            if el.len() > 1 {
446                true
447            } else if el.len() == 1 {
448                arrow_deref_array_assign_rhs_list_ctx(&el[0])
449            } else {
450                false
451            }
452        }
453        _ => false,
454    }
455}
456
457/// Wantarray for the RHS of a plain `=` assignment — must match [`crate::compiler::Compiler`] lowering
458/// so `<>` / `readline` list-slurp matches Perl for `@a = <>` (not only `my`/`our`/`local` initializers).
459pub(crate) fn assign_rhs_wantarray(target: &Expr) -> WantarrayCtx {
460    match &target.kind {
461        ExprKind::ArrayVar(_) | ExprKind::HashVar(_) => WantarrayCtx::List,
462        ExprKind::ScalarVar(_) | ExprKind::ArrayElement { .. } | ExprKind::HashElement { .. } => {
463            WantarrayCtx::Scalar
464        }
465        ExprKind::Deref { kind, .. } => match kind {
466            Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
467            Sigil::Array | Sigil::Hash => WantarrayCtx::List,
468        },
469        ExprKind::ArrowDeref {
470            index,
471            kind: DerefKind::Array,
472            ..
473        } => {
474            if arrow_deref_array_assign_rhs_list_ctx(index) {
475                WantarrayCtx::List
476            } else {
477                WantarrayCtx::Scalar
478            }
479        }
480        ExprKind::ArrowDeref {
481            kind: DerefKind::Hash,
482            ..
483        }
484        | ExprKind::ArrowDeref {
485            kind: DerefKind::Call,
486            ..
487        } => WantarrayCtx::Scalar,
488        ExprKind::HashSliceDeref { .. }
489        | ExprKind::HashSlice { .. }
490        | ExprKind::HashKvSlice { .. } => WantarrayCtx::List,
491        ExprKind::ArraySlice { indices, .. } => {
492            if indices.len() > 1 {
493                WantarrayCtx::List
494            } else if indices.len() == 1 {
495                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
496                    WantarrayCtx::List
497                } else {
498                    WantarrayCtx::Scalar
499                }
500            } else {
501                WantarrayCtx::Scalar
502            }
503        }
504        ExprKind::AnonymousListSlice { indices, .. } => {
505            if indices.len() > 1 {
506                WantarrayCtx::List
507            } else if indices.len() == 1 {
508                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
509                    WantarrayCtx::List
510                } else {
511                    WantarrayCtx::Scalar
512                }
513            } else {
514                WantarrayCtx::Scalar
515            }
516        }
517        ExprKind::Typeglob(_) | ExprKind::TypeglobExpr(_) => WantarrayCtx::Scalar,
518        ExprKind::List(_) => WantarrayCtx::List,
519        _ => WantarrayCtx::Scalar,
520    }
521}
522
523/// Memoized inputs + result for a non-`g` `regex_match_execute` call. Populated on every
524/// successful match and consulted at the top of the next call; on exact-match (same pattern,
525/// flags, multiline, and haystack content) we skip regex execution + capture-var scope population
526/// entirely, replaying the stored `StrykeValue` result. See [`VMHelper::regex_match_memo`].
527#[derive(Clone)]
528pub(crate) struct RegexMatchMemo {
529    pub pattern: String,
530    pub flags: String,
531    pub multiline: bool,
532    pub haystack: String,
533    pub result: StrykeValue,
534}
535
536/// State for scalar `..` / `...` (key: `Expr` address).
537#[derive(Clone, Copy, Default)]
538struct FlipFlopTreeState {
539    active: bool,
540    /// Exclusive `...`: `$.` line where the left bound matched — right is only tested when `$.` is
541    /// strictly greater (Perl: do not test the right operand until the next evaluation; for numeric
542    /// `$.` that defers past the left-match line, including multiple evals on that line).
543    exclusive_left_line: Option<i64>,
544}
545
546/// `BufReader` / `print` / `sysread` / `tell` on the same handle share this [`File`] cursor.
547#[derive(Clone)]
548pub(crate) struct IoSharedFile(pub Arc<Mutex<File>>);
549
550impl Read for IoSharedFile {
551    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
552        self.0.lock().read(buf)
553    }
554}
555
556pub(crate) struct IoSharedFileWrite(pub Arc<Mutex<File>>);
557
558impl IoWrite for IoSharedFileWrite {
559    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
560        self.0.lock().write(buf)
561    }
562
563    fn flush(&mut self) -> io::Result<()> {
564        self.0.lock().flush()
565    }
566}
567
568/// There is no Tree walking Interpreter, this is Just a Virtual Machine helper struct
569pub struct VMHelper {
570    pub scope: Scope,
571    pub(crate) subs: HashMap<String, Arc<StrykeSub>>,
572    /// AOP advice registry — populated by `Op::RegisterAdvice` from `before|after|around` decls.
573    pub(crate) intercepts: Vec<crate::aop::Intercept>,
574    /// Auto-incremented for the next registered intercept id (1-based; matches zshrs).
575    pub(crate) next_intercept_id: u32,
576    /// Stack of active around-advice contexts; `proceed()` reads the top frame.
577    pub(crate) intercept_ctx_stack: Vec<crate::aop::InterceptCtx>,
578    /// Re-entrancy guard: while running advice for a name, calling that same name from inside
579    /// the body skips advice and runs the original directly. Prevents infinite recursion when
580    /// an advice body uses the same sub it advises.
581    pub(crate) intercept_active_names: Vec<String>,
582    pub(crate) file: String,
583    /// File handles: name → writer
584    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
585    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
586    /// Output separator ($,)
587    pub ofs: String,
588    /// Output record separator ($\)
589    pub ors: String,
590    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
591    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
592    pub irs: Option<String>,
593    /// $! — last OS error
594    pub errno: String,
595    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
596    pub errno_code: i32,
597    /// $@ — last eval error (string)
598    pub eval_error: String,
599    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
600    pub eval_error_code: i32,
601    /// When `die` is called with a ref argument, the ref value is preserved here.
602    pub eval_error_value: Option<StrykeValue>,
603    /// @ARGV
604    pub argv: Vec<String>,
605    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
606    pub env: IndexMap<String, StrykeValue>,
607    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
608    pub env_materialized: bool,
609    /// $0
610    pub program_name: String,
611    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle)
612    pub line_number: i64,
613    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
614    pub last_readline_handle: String,
615    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
616    pub(crate) last_stdin_die_bracket: String,
617    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
618    pub handle_line_numbers: HashMap<String, i64>,
619    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
620    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
621    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
622    pub(crate) flip_flop_active: Vec<bool>,
623    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
624    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
625    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
626    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
627    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
628    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
629    pub(crate) flip_flop_sequence: Vec<i64>,
630    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
631    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
632    /// range on one line return the same sequence number).
633    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
634    /// Scalar `..` / `...` flip-flop state (key: `Expr` address).
635    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
636    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
637    pub sigint_pending_caret: Cell<bool>,
638    /// Auto-split mode (-a)
639    pub auto_split: bool,
640    /// Field separator for -F
641    pub field_separator: Option<String>,
642    /// BEGIN blocks
643    begin_blocks: Vec<Block>,
644    /// `UNITCHECK` blocks (LIFO at run)
645    unit_check_blocks: Vec<Block>,
646    /// `CHECK` blocks (LIFO at run)
647    check_blocks: Vec<Block>,
648    /// `INIT` blocks (FIFO at run)
649    init_blocks: Vec<Block>,
650    /// END blocks
651    end_blocks: Vec<Block>,
652    /// -w warnings / `use warnings` / `$^W`
653    pub warnings: bool,
654    /// Output autoflush (`$|`).
655    pub output_autoflush: bool,
656    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
657    pub default_print_handle: String,
658    /// Suppress stdout output (fan workers with progress bars).
659    pub suppress_stdout: bool,
660    /// Per-instance test counters for `assert_*` / `test_run` / `test_skip` (stryke
661    /// `.stk` test framework). Atomics so the immutable-ref builtin signature
662    /// (`fn(&VMHelper, ...)`) can mutate without changing every call site. Replaces
663    /// the previous `AtomicUsize` process-globals which leaked counts across runs
664    /// in a single process — embedders running multiple `.stk` programs in one
665    /// `VMHelper` would see the previous run's counts contaminate the next.
666    pub test_pass_count: std::sync::atomic::AtomicUsize,
667    pub test_fail_count: std::sync::atomic::AtomicUsize,
668    pub test_skip_count: std::sync::atomic::AtomicUsize,
669    /// Cumulative-across-the-whole-run counters. The `test_pass_count` /
670    /// `test_fail_count` / `test_skip_count` triplet above is reset by
671    /// `test_run` after it prints its per-block summary, so external
672    /// embedders (the worker-pool test runner reading counts after
673    /// `execute()` returns) see 0 if any test in the file already
674    /// finished a `test_run` block. The `_total` counters are summed-in
675    /// by `test_run` *before* the reset, never themselves reset, and
676    /// give the harness an honest "how many assertions did this run
677    /// actually do" number.
678    pub test_pass_total: std::sync::atomic::AtomicUsize,
679    pub test_fail_total: std::sync::atomic::AtomicUsize,
680    pub test_skip_total: std::sync::atomic::AtomicUsize,
681    /// Set to `true` by `test_run` when any assertion failed during the run.
682    /// CLI driver (`main.rs`) reads this after `execute` returns and exits with
683    /// code 1. Replaces the previous in-VM `std::process::exit(1)` which made
684    /// embedding (running a `.stk` program from a Rust harness) impossible —
685    /// any failing test would kill the host process.
686    pub test_run_failed: std::sync::atomic::AtomicBool,
687    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
688    pub child_exit_status: i64,
689    /// Last successful match (`$&`, `${^MATCH}`).
690    pub last_match: String,
691    /// Before match (`` $` ``, `${^PREMATCH}`).
692    pub prematch: String,
693    /// After match (`$'`, `${^POSTMATCH}`).
694    pub postmatch: String,
695    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
696    pub last_paren_match: String,
697    /// List separator for array stringification in concatenation / interpolation (`$"`).
698    pub list_separator: String,
699    /// Script start time (`$^T`) — seconds since Unix epoch.
700    pub script_start_time: i64,
701    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
702    pub compile_hints: i64,
703    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
704    pub warning_bits: i64,
705    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
706    pub global_phase: String,
707    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
708    pub subscript_sep: String,
709    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
710    /// The `stryke` driver sets this from `-i` / `-i.ext`.
711    pub inplace_edit: String,
712    /// `$^D` — debugging flags (integer; mostly ignored).
713    pub debug_flags: i64,
714    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
715    pub perl_debug_flags: i64,
716    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
717    pub eval_nesting: u32,
718    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
719    pub argv_current_file: String,
720    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
721    pub(crate) diamond_next_idx: usize,
722    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
723    pub(crate) diamond_reader: Option<BufReader<File>>,
724    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
725    pub strict_refs: bool,
726    pub strict_subs: bool,
727    pub strict_vars: bool,
728    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
729    pub utf8_pragma: bool,
730    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
731    pub open_pragma_utf8: bool,
732    /// `use feature` — bit flags (`FEAT_*`).
733    pub feature_bits: u64,
734    /// Number of parallel threads
735    pub num_threads: usize,
736    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
737    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
738    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
739    /// Third flag: `$*` multiline (prepends `(?s)` when true).
740    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
741    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
742    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
743    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
744    /// scope population entirely on cache hit.
745    ///
746    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
747    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
748    /// capture-var side-effect replay is forced on the next hit.
749    regex_match_memo: Option<RegexMatchMemo>,
750    /// False when the user (or some non-regex code path) has written to one of the capture
751    /// variables since the last `apply_regex_captures` call. The memoized match result is still
752    /// valid, but the scope side effects need to be reapplied on the next hit.
753    regex_capture_scope_fresh: bool,
754    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
755    pub(crate) regex_pos: HashMap<String, Option<usize>>,
756    /// Persistent storage for `state` variables, keyed by "line:name".
757    pub(crate) state_vars: HashMap<String, StrykeValue>,
758    /// Per-frame tracking of state variable bindings: (var_name, state_key).
759    pub(crate) state_bindings_stack: Vec<Vec<(String, String)>>,
760    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
761    pub(crate) rand_rng: StdRng,
762    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
763    pub(crate) dir_handles: HashMap<String, DirHandleState>,
764    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
765    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
766    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
767    pub(crate) pipe_children: HashMap<String, Child>,
768    /// Sockets from `socket` / `accept` / `connect`.
769    pub(crate) socket_handles: HashMap<String, StrykeSocket>,
770    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
771    pub(crate) wantarray_kind: WantarrayCtx,
772    /// `struct Name { ... }` definitions (merged from VM chunks).
773    pub struct_defs: HashMap<String, Arc<StructDef>>,
774    /// `enum Name { ... }` definitions (merged from VM chunks).
775    pub enum_defs: HashMap<String, Arc<EnumDef>>,
776    /// `class Name extends ... impl ... { ... }` definitions.
777    pub class_defs: HashMap<String, Arc<ClassDef>>,
778    /// `trait Name { ... }` definitions.
779    pub trait_defs: HashMap<String, Arc<TraitDef>>,
780    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
781    /// call/return (JIT disabled); per-statement lines and subs.
782    pub profiler: Option<Profiler>,
783    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
784    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
785    /// Virtual modules: path → source (for AOT bundles). Checked before filesystem in `require`.
786    pub(crate) virtual_modules: HashMap<String, String>,
787    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
788    pub(crate) tied_hashes: HashMap<String, StrykeValue>,
789    /// `tie $name` — TIESCALAR object for FETCH/STORE.
790    pub(crate) tied_scalars: HashMap<String, StrykeValue>,
791    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
792    pub(crate) tied_arrays: HashMap<String, StrykeValue>,
793    /// `use overload` — class → Perl overload key → short method name in that package.
794    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
795    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
796    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
797    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
798    pub(crate) special_caret_scalars: HashMap<String, StrykeValue>,
799    /// `$%` — format output page number.
800    pub format_page_number: i64,
801    /// `$=` — format lines per page.
802    pub format_lines_per_page: i64,
803    /// `$-` — lines remaining on format page.
804    pub format_lines_left: i64,
805    /// `$:` — characters to break format lines (Perl default `\n`).
806    pub format_line_break_chars: String,
807    /// `$^` — top-of-form format name.
808    pub format_top_name: String,
809    /// `$^A` — format write accumulator.
810    pub accumulator_format: String,
811    /// `$^F` — max system file descriptor (Perl default 2).
812    pub max_system_fd: i64,
813    /// `$^M` — emergency memory buffer (no-op pool in stryke).
814    pub emergency_memory: String,
815    /// `$^N` — last opened named regexp capture name.
816    pub last_subpattern_name: String,
817    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
818    pub inc_hook_index: i64,
819    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
820    pub multiline_match: bool,
821    /// `$^X` — path to this executable (cached).
822    pub executable_path: String,
823    /// `$^L` — formfeed string for formats (Perl default `\f`).
824    pub formfeed_string: String,
825    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
826    pub(crate) glob_handle_alias: HashMap<String, String>,
827    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
828    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
829    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
830    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
831    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
832    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
833    pub(crate) special_var_restore_frames: Vec<Vec<(String, StrykeValue)>>,
834    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
835    /// Lazy-init flag: reflection hashes (`%b`, `%stryke::builtins`, etc.)
836    /// are only built on first access to avoid startup cost.
837    pub(crate) reflection_hashes_ready: bool,
838    pub(crate) english_enabled: bool,
839    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
840    pub(crate) english_no_match_vars: bool,
841    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
842    /// available for the rest of the program — Perl exports them into the caller's namespace
843    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
844    pub(crate) english_match_vars_ever_enabled: bool,
845    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
846    english_lexical_scalars: Vec<HashSet<String>>,
847    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
848    our_lexical_scalars: Vec<HashSet<String>>,
849    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
850    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
851    pub vm_jit_enabled: bool,
852    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
853    pub disasm_bytecode: bool,
854    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from a bytecode cache hit. When
855    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
856    /// (`.take()`) on first read so re-entry compiles normally.
857    pub cached_chunk: Option<crate::bytecode::Chunk>,
858    /// Sideband: script path for bytecode cache save after compilation (mtime-based).
859    pub cache_script_path: Option<std::path::PathBuf>,
860    /// Interpreter base for relative filesystem paths (`cd` updates this; OS `chdir` does not).
861    pub(crate) stryke_pwd: PathBuf,
862    /// Set while stepping a `gen { }` body (`yield`).
863    pub(crate) in_generator: bool,
864    /// `-n`/`-p` driver: prelude only; body runs per line in [`Self::process_line_vm`].
865    pub line_mode_skip_main: bool,
866    /// Pre-compiled chunk for `-n`/`-p` line mode. Stored after the prelude `execute()` call
867    /// so `process_line_vm` can re-execute the body portion per input line.
868    pub line_mode_chunk: Option<crate::bytecode::Chunk>,
869    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
870    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
871    /// matches Perl (true on the last line of that source).
872    pub(crate) line_mode_eof_pending: bool,
873    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
874    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
875    pub line_mode_stdin_pending: VecDeque<String>,
876    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
877    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
878    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
879    pub(crate) log_level_override: Option<LogLevelFilter>,
880    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
881    /// Pushed on `call_sub` entry, popped on exit.
882    pub(crate) current_sub_stack: Vec<Arc<StrykeSub>>,
883    /// Interactive debugger state (`-d` flag).
884    pub debugger: Option<crate::debugger::Debugger>,
885    /// Call stack for debugger: (sub_name, call_line).
886    pub(crate) debug_call_stack: Vec<(String, usize)>,
887}
888
889/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
890#[derive(Debug, Clone, Default)]
891pub struct ReplCompletionSnapshot {
892    pub subs: Vec<String>,
893    pub blessed_scalars: HashMap<String, String>,
894    pub isa_for_class: HashMap<String, Vec<String>>,
895}
896
897impl ReplCompletionSnapshot {
898    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
899    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
900        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
901        let mro = linearize_c3(class, &parents, 0);
902        let mut names = HashSet::new();
903        for pkg in &mro {
904            if pkg == "UNIVERSAL" {
905                continue;
906            }
907            let prefix = format!("{}::", pkg);
908            for k in &self.subs {
909                if k.starts_with(&prefix) {
910                    let rest = &k[prefix.len()..];
911                    if !rest.contains("::") {
912                        names.insert(rest.to_string());
913                    }
914                }
915            }
916        }
917        for k in &self.subs {
918            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
919                if !rest.contains("::") {
920                    names.insert(rest.to_string());
921                }
922            }
923        }
924        let mut v: Vec<String> = names.into_iter().collect();
925        v.sort();
926        v
927    }
928}
929
930fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
931    let left = left.trim_end();
932    if left.is_empty() {
933        return None;
934    }
935    if let Some(i) = left.rfind('$') {
936        let name = left[i + 1..].trim();
937        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
938            return state.blessed_scalars.get(name).cloned();
939        }
940    }
941    let tok = left.split_whitespace().last()?;
942    if tok.contains("::") {
943        return Some(tok.to_string());
944    }
945    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
946        return Some(tok.to_string());
947    }
948    None
949}
950
951/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
952pub fn repl_arrow_method_completions(
953    state: &ReplCompletionSnapshot,
954    line: &str,
955    pos: usize,
956) -> Option<(usize, Vec<String>)> {
957    let pos = pos.min(line.len());
958    let before = &line[..pos];
959    let arrow_idx = before.rfind("->")?;
960    let after_arrow = &before[arrow_idx + 2..];
961    let rest = after_arrow.trim_start();
962    let ws_len = after_arrow.len() - rest.len();
963    let method_start = arrow_idx + 2 + ws_len;
964    let method_prefix = &line[method_start..pos];
965    if !method_prefix
966        .chars()
967        .all(|c| c.is_alphanumeric() || c == '_')
968    {
969        return None;
970    }
971    let left = line[..arrow_idx].trim_end();
972    let class = repl_resolve_class_for_arrow(state, left)?;
973    let mut methods = state.methods_for_class(&class);
974    methods.retain(|m| m.starts_with(method_prefix));
975    Some((method_start, methods))
976}
977
978/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
979#[derive(Debug, Clone, Default)]
980pub(crate) struct ModuleExportLists {
981    /// Default imports for `use Module` with no list.
982    pub export: Vec<String>,
983    /// Extra symbols allowed in `use Module qw(name)`.
984    pub export_ok: Vec<String>,
985}
986
987/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
988fn piped_shell_command(cmd: &str) -> Command {
989    if cfg!(windows) {
990        let mut c = Command::new("cmd");
991        c.arg("/C").arg(cmd);
992        c
993    } else {
994        let mut c = Command::new("sh");
995        c.arg("-c").arg(cmd);
996        c
997    }
998}
999
1000/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
1001/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
1002/// so the Rust `regex` crate can match them.
1003/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
1004/// so the Rust regex crate can match NUL and other octal-specified bytes.
1005/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
1006fn expand_perl_regex_octal_escapes(pat: &str) -> String {
1007    let mut out = String::with_capacity(pat.len());
1008    let mut it = pat.chars().peekable();
1009    while let Some(c) = it.next() {
1010        if c == '\\' {
1011            if let Some(&'0') = it.peek() {
1012                // Collect up to 3 octal digits starting with '0'
1013                let mut oct = String::new();
1014                while oct.len() < 3 {
1015                    if let Some(&d) = it.peek() {
1016                        if ('0'..='7').contains(&d) {
1017                            oct.push(d);
1018                            it.next();
1019                        } else {
1020                            break;
1021                        }
1022                    } else {
1023                        break;
1024                    }
1025                }
1026                if let Ok(val) = u8::from_str_radix(&oct, 8) {
1027                    out.push_str(&format!("\\x{:02x}", val));
1028                } else {
1029                    out.push('\\');
1030                    out.push_str(&oct);
1031                }
1032                continue;
1033            }
1034        }
1035        out.push(c);
1036    }
1037    out
1038}
1039
1040fn expand_perl_regex_quotemeta(pat: &str) -> String {
1041    let mut out = String::with_capacity(pat.len().saturating_mul(2));
1042    let mut it = pat.chars().peekable();
1043    let mut in_q = false;
1044    while let Some(c) = it.next() {
1045        if in_q {
1046            if c == '\\' && it.peek() == Some(&'E') {
1047                it.next();
1048                in_q = false;
1049                continue;
1050            }
1051            out.push_str(&perl_quotemeta(&c.to_string()));
1052            continue;
1053        }
1054        if c == '\\' && it.peek() == Some(&'Q') {
1055            it.next();
1056            in_q = true;
1057            continue;
1058        }
1059        out.push(c);
1060    }
1061    out
1062}
1063
1064/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
1065///
1066/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
1067/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
1068///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
1069pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
1070    let mut out = String::with_capacity(replacement.len() + 8);
1071    let mut it = replacement.chars().peekable();
1072    while let Some(c) = it.next() {
1073        if c == '\\' {
1074            match it.peek() {
1075                Some(&d) if d.is_ascii_digit() => {
1076                    it.next();
1077                    out.push_str("${");
1078                    out.push(d);
1079                    while let Some(&d2) = it.peek() {
1080                        if !d2.is_ascii_digit() {
1081                            break;
1082                        }
1083                        it.next();
1084                        out.push(d2);
1085                    }
1086                    out.push('}');
1087                }
1088                Some(&'\\') => {
1089                    it.next();
1090                    out.push('\\');
1091                }
1092                _ => out.push('\\'),
1093            }
1094        } else if c == '$' {
1095            match it.peek() {
1096                Some(&d) if d.is_ascii_digit() => {
1097                    it.next();
1098                    out.push_str("${");
1099                    out.push(d);
1100                    while let Some(&d2) = it.peek() {
1101                        if !d2.is_ascii_digit() {
1102                            break;
1103                        }
1104                        it.next();
1105                        out.push(d2);
1106                    }
1107                    out.push('}');
1108                }
1109                Some(&'{') => {
1110                    // already braced — pass through as-is
1111                    out.push('$');
1112                }
1113                _ => out.push('$'),
1114            }
1115        } else {
1116            out.push(c);
1117        }
1118    }
1119    out
1120}
1121
1122/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
1123/// past the closing `]`.
1124fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
1125    debug_assert_eq!(chars.get(i), Some(&'['));
1126    out.push('[');
1127    i += 1;
1128    if i < chars.len() && chars[i] == '^' {
1129        out.push('^');
1130        i += 1;
1131    }
1132    if i >= chars.len() {
1133        return i;
1134    }
1135    // `]` as the first class character is literal iff another unescaped `]` closes the class
1136    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
1137    // this `]`.
1138    if chars[i] == ']' {
1139        if i + 1 < chars.len() && chars[i + 1] == ']' {
1140            // `[]]` / `[^]]`: literal `]` then the closing `]`.
1141            out.push(']');
1142            i += 1;
1143        } else {
1144            let mut scan = i + 1;
1145            let mut found_closing = false;
1146            while scan < chars.len() {
1147                if chars[scan] == '\\' && scan + 1 < chars.len() {
1148                    scan += 2;
1149                    continue;
1150                }
1151                if chars[scan] == ']' {
1152                    found_closing = true;
1153                    break;
1154                }
1155                scan += 1;
1156            }
1157            if found_closing {
1158                out.push(']');
1159                i += 1;
1160            } else {
1161                out.push(']');
1162                return i + 1;
1163            }
1164        }
1165    }
1166    while i < chars.len() && chars[i] != ']' {
1167        if chars[i] == '\\' && i + 1 < chars.len() {
1168            out.push(chars[i]);
1169            out.push(chars[i + 1]);
1170            i += 2;
1171            continue;
1172        }
1173        out.push(chars[i]);
1174        i += 1;
1175    }
1176    if i < chars.len() {
1177        out.push(']');
1178        i += 1;
1179    }
1180    i
1181}
1182
1183/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
1184/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
1185/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1186/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1187fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1188    if multiline_flag {
1189        return pat.to_string();
1190    }
1191    let chars: Vec<char> = pat.chars().collect();
1192    let mut out = String::with_capacity(pat.len().saturating_add(16));
1193    let mut i = 0usize;
1194    while i < chars.len() {
1195        let c = chars[i];
1196        if c == '\\' && i + 1 < chars.len() {
1197            out.push(c);
1198            out.push(chars[i + 1]);
1199            i += 2;
1200            continue;
1201        }
1202        if c == '[' {
1203            i = copy_regex_char_class(&chars, i, &mut out);
1204            continue;
1205        }
1206        if c == '$' {
1207            if let Some(&next) = chars.get(i + 1) {
1208                if next.is_ascii_digit() {
1209                    out.push(c);
1210                    i += 1;
1211                    continue;
1212                }
1213                if next == '{' {
1214                    out.push(c);
1215                    i += 1;
1216                    continue;
1217                }
1218                if next.is_ascii_alphanumeric() || next == '_' {
1219                    out.push(c);
1220                    i += 1;
1221                    continue;
1222                }
1223            }
1224            out.push_str("(?=\\n?\\z)");
1225            i += 1;
1226            continue;
1227        }
1228        out.push(c);
1229        i += 1;
1230    }
1231    out
1232}
1233
1234/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1235#[derive(Debug, Clone)]
1236pub(crate) struct DirHandleState {
1237    pub entries: Vec<String>,
1238    pub pos: usize,
1239}
1240
1241/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1242pub(crate) fn perl_osname() -> String {
1243    match std::env::consts::OS {
1244        "linux" => "linux".to_string(),
1245        "macos" => "darwin".to_string(),
1246        "windows" => "MSWin32".to_string(),
1247        other => other.to_string(),
1248    }
1249}
1250
1251fn perl_version_v_string() -> String {
1252    format!("v{}", env!("CARGO_PKG_VERSION"))
1253}
1254
1255fn extended_os_error_string() -> String {
1256    std::io::Error::last_os_error().to_string()
1257}
1258
1259#[cfg(unix)]
1260fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1261    unsafe {
1262        (
1263            libc::getuid() as i64,
1264            libc::geteuid() as i64,
1265            libc::getgid() as i64,
1266            libc::getegid() as i64,
1267        )
1268    }
1269}
1270
1271#[cfg(not(unix))]
1272fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1273    (0, 0, 0, 0)
1274}
1275
1276fn unix_id_for_special(name: &str) -> i64 {
1277    let (r, e, _, _) = unix_real_effective_ids();
1278    match name {
1279        "<" => r,
1280        ">" => e,
1281        _ => 0,
1282    }
1283}
1284
1285#[cfg(unix)]
1286fn unix_group_list_string(primary: libc::gid_t) -> String {
1287    let mut buf = vec![0 as libc::gid_t; 256];
1288    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1289    if n <= 0 {
1290        return format!("{}", primary);
1291    }
1292    let mut parts = vec![format!("{}", primary)];
1293    for g in buf.iter().take(n as usize) {
1294        parts.push(format!("{}", g));
1295    }
1296    parts.join(" ")
1297}
1298
1299/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1300#[cfg(unix)]
1301fn unix_group_list_for_special(name: &str) -> String {
1302    let (_, _, gid, egid) = unix_real_effective_ids();
1303    match name {
1304        "(" => unix_group_list_string(gid as libc::gid_t),
1305        ")" => unix_group_list_string(egid as libc::gid_t),
1306        _ => String::new(),
1307    }
1308}
1309
1310#[cfg(not(unix))]
1311fn unix_group_list_for_special(_name: &str) -> String {
1312    String::new()
1313}
1314
1315/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1316/// `~/.ssh/config` and keys).
1317#[cfg(unix)]
1318fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1319    use libc::{getpwuid_r, getuid};
1320    use std::ffi::CStr;
1321    use std::os::unix::ffi::OsStringExt;
1322    let uid = unsafe { getuid() };
1323    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1324    let mut result: *mut libc::passwd = std::ptr::null_mut();
1325    let mut buf = vec![0u8; 16_384];
1326    let rc = unsafe {
1327        getpwuid_r(
1328            uid,
1329            &mut pw,
1330            buf.as_mut_ptr().cast::<libc::c_char>(),
1331            buf.len(),
1332            &mut result,
1333        )
1334    };
1335    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1336        return None;
1337    }
1338    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1339    if bytes.is_empty() {
1340        return None;
1341    }
1342    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1343}
1344
1345/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1346#[cfg(unix)]
1347fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1348    use libc::getpwnam_r;
1349    use std::ffi::{CStr, CString};
1350    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1351    let bytes = login.as_bytes();
1352    if bytes.is_empty() || bytes.contains(&0) {
1353        return None;
1354    }
1355    let cname = CString::new(bytes).ok()?;
1356    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1357    let mut result: *mut libc::passwd = std::ptr::null_mut();
1358    let mut buf = vec![0u8; 16_384];
1359    let rc = unsafe {
1360        getpwnam_r(
1361            cname.as_ptr(),
1362            &mut pw,
1363            buf.as_mut_ptr().cast::<libc::c_char>(),
1364            buf.len(),
1365            &mut result,
1366        )
1367    };
1368    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1369        return None;
1370    }
1371    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1372    if dir_bytes.is_empty() {
1373        return None;
1374    }
1375    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1376}
1377
1378impl Default for VMHelper {
1379    fn default() -> Self {
1380        Self::new()
1381    }
1382}
1383
1384/// How [`VMHelper::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1385#[derive(Clone, Copy)]
1386pub(crate) enum CaptureAllMode {
1387    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1388    Empty,
1389    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1390    Append,
1391    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1392    Skip,
1393}
1394
1395impl VMHelper {
1396    pub fn new() -> Self {
1397        let mut scope = Scope::new();
1398        scope.declare_array("INC", vec![StrykeValue::string(".".to_string())]);
1399        scope.declare_hash("INC", IndexMap::new());
1400        scope.declare_array("ARGV", vec![]);
1401        scope.declare_array("_", vec![]);
1402
1403        // @path / @p — $PATH split by OS path separator, frozen (immutable)
1404        let path_vec: Vec<StrykeValue> = std::env::var("PATH")
1405            .unwrap_or_default()
1406            .split(if cfg!(windows) { ';' } else { ':' })
1407            .filter(|s| !s.is_empty())
1408            .map(|p| StrykeValue::string(p.to_string()))
1409            .collect();
1410        scope.declare_array_frozen("path", path_vec.clone(), true);
1411        scope.declare_array_frozen("p", path_vec, true);
1412
1413        // @fpath / @f — $FPATH (zsh function path) split by ':', frozen
1414        let fpath_vec: Vec<StrykeValue> = std::env::var("FPATH")
1415            .unwrap_or_default()
1416            .split(':')
1417            .filter(|s| !s.is_empty())
1418            .map(|p| StrykeValue::string(p.to_string()))
1419            .collect();
1420        scope.declare_array_frozen("fpath", fpath_vec.clone(), true);
1421        scope.declare_array_frozen("f", fpath_vec, true);
1422        scope.declare_hash("ENV", IndexMap::new());
1423        scope.declare_hash("SIG", IndexMap::new());
1424
1425        // %term — terminal info (frozen)
1426        let term_map = build_term_hash();
1427        scope.declare_hash_global_frozen("term", term_map);
1428
1429        // %uname — system identification (frozen, Unix only)
1430        #[cfg(unix)]
1431        {
1432            let uname_map = build_uname_hash();
1433            scope.declare_hash_global_frozen("uname", uname_map);
1434        }
1435        #[cfg(not(unix))]
1436        {
1437            scope.declare_hash_global_frozen("uname", IndexMap::new());
1438        }
1439
1440        // %limits — resource limits (frozen, Unix only)
1441        #[cfg(unix)]
1442        {
1443            let limits_map = build_limits_hash();
1444            scope.declare_hash_global_frozen("limits", limits_map);
1445        }
1446        #[cfg(not(unix))]
1447        {
1448            scope.declare_hash_global_frozen("limits", IndexMap::new());
1449        }
1450
1451        // Reflection hashes — populated from `build.rs`-generated tables so
1452        // they track the real parser/dispatcher/LSP without hand-maintenance.
1453        // Eleven hashes; all lookups are O(1). Forward maps:
1454        //   %b   / %stryke::builtins      — callable name → category ("parallel", "string", …)
1455        //   %k   / %stryke::keywords      — language keyword → category ("control", "decl", …)
1456        //   %o   / %stryke::operators     — symbol operator → category ("arith", "pipeline", …)
1457        //   %v   / %stryke::special_vars  — special var spelling (sigil included) → category
1458        //   %pc  / %stryke::perl_compats  — subset: Perl 5 core only
1459        //   %e   / %stryke::extensions    — subset: stryke-only
1460        //   %a   / %stryke::aliases       — alias → primary
1461        //   %d   / %stryke::descriptions  — name → LSP one-liner (sparse)
1462        //   %all / %stryke::all           — primaries + aliases + keywords (union)
1463        // Inverted indexes for constant-time reverse queries:
1464        //   %c   / %stryke::categories    — category → arrayref of names
1465        //   %p   / %stryke::primaries     — primary → arrayref of aliases
1466        //
1467        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1468        // together they cover `keys %builtins`. Short aliases use the
1469        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1470        // Reflection hashes are lazily initialized on first access
1471        // (see `ensure_reflection_hashes`). Only declare the version scalar
1472        // eagerly since it's trivial.
1473        scope.declare_scalar(
1474            "stryke::VERSION",
1475            StrykeValue::string(env!("CARGO_PKG_VERSION").to_string()),
1476        );
1477        scope.declare_array("-", vec![]);
1478        scope.declare_array("+", vec![]);
1479        scope.declare_array("^CAPTURE", vec![]);
1480        scope.declare_array("^CAPTURE_ALL", vec![]);
1481        scope.declare_hash("^HOOK", IndexMap::new());
1482        scope.declare_scalar("~", StrykeValue::string("STDOUT".to_string()));
1483
1484        let script_start_time = std::time::SystemTime::now()
1485            .duration_since(std::time::UNIX_EPOCH)
1486            .map(|d| d.as_secs() as i64)
1487            .unwrap_or(0);
1488
1489        let executable_path = cached_executable_path();
1490
1491        let stryke_pwd_init = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
1492        let stryke_pwd = std::fs::canonicalize(&stryke_pwd_init).unwrap_or(stryke_pwd_init);
1493
1494        let mut special_caret_scalars: HashMap<String, StrykeValue> = HashMap::new();
1495        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1496            special_caret_scalars.insert(format!("^{}", name), StrykeValue::UNDEF);
1497        }
1498
1499        let mut s = Self {
1500            scope,
1501            subs: HashMap::new(),
1502            intercepts: Vec::new(),
1503            next_intercept_id: 1,
1504            intercept_ctx_stack: Vec::new(),
1505            intercept_active_names: Vec::new(),
1506            struct_defs: HashMap::new(),
1507            enum_defs: HashMap::new(),
1508            class_defs: HashMap::new(),
1509            trait_defs: HashMap::new(),
1510            file: "-e".to_string(),
1511            output_handles: HashMap::new(),
1512            input_handles: HashMap::new(),
1513            ofs: String::new(),
1514            ors: String::new(),
1515            irs: Some("\n".to_string()),
1516            errno: String::new(),
1517            errno_code: 0,
1518            eval_error: String::new(),
1519            eval_error_code: 0,
1520            eval_error_value: None,
1521            argv: Vec::new(),
1522            env: IndexMap::new(),
1523            env_materialized: false,
1524            program_name: "stryke".to_string(),
1525            line_number: 0,
1526            last_readline_handle: String::new(),
1527            last_stdin_die_bracket: "<STDIN>".to_string(),
1528            handle_line_numbers: HashMap::new(),
1529            flip_flop_active: Vec::new(),
1530            flip_flop_exclusive_left_line: Vec::new(),
1531            flip_flop_sequence: Vec::new(),
1532            flip_flop_last_dot: Vec::new(),
1533            flip_flop_tree: HashMap::new(),
1534            sigint_pending_caret: Cell::new(false),
1535            auto_split: false,
1536            field_separator: None,
1537            begin_blocks: Vec::new(),
1538            unit_check_blocks: Vec::new(),
1539            check_blocks: Vec::new(),
1540            init_blocks: Vec::new(),
1541            end_blocks: Vec::new(),
1542            warnings: false,
1543            output_autoflush: false,
1544            default_print_handle: "STDOUT".to_string(),
1545            suppress_stdout: false,
1546            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1547            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1548            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1549            test_pass_total: std::sync::atomic::AtomicUsize::new(0),
1550            test_fail_total: std::sync::atomic::AtomicUsize::new(0),
1551            test_skip_total: std::sync::atomic::AtomicUsize::new(0),
1552            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1553            child_exit_status: 0,
1554            last_match: String::new(),
1555            prematch: String::new(),
1556            postmatch: String::new(),
1557            last_paren_match: String::new(),
1558            list_separator: " ".to_string(),
1559            script_start_time,
1560            compile_hints: 0,
1561            warning_bits: 0,
1562            global_phase: "RUN".to_string(),
1563            subscript_sep: "\x1c".to_string(),
1564            inplace_edit: String::new(),
1565            debug_flags: 0,
1566            perl_debug_flags: 0,
1567            eval_nesting: 0,
1568            argv_current_file: String::new(),
1569            diamond_next_idx: 0,
1570            diamond_reader: None,
1571            strict_refs: false,
1572            strict_subs: false,
1573            strict_vars: false,
1574            utf8_pragma: false,
1575            open_pragma_utf8: false,
1576            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1577            feature_bits: FEAT_SAY,
1578            num_threads: 0, // lazily read from rayon on first parallel op
1579            regex_cache: HashMap::new(),
1580            regex_last: None,
1581            regex_match_memo: None,
1582            regex_capture_scope_fresh: false,
1583            regex_pos: HashMap::new(),
1584            state_vars: HashMap::new(),
1585            state_bindings_stack: Vec::new(),
1586            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1587            dir_handles: HashMap::new(),
1588            io_file_slots: HashMap::new(),
1589            pipe_children: HashMap::new(),
1590            socket_handles: HashMap::new(),
1591            wantarray_kind: WantarrayCtx::Scalar,
1592            profiler: None,
1593            module_export_lists: HashMap::new(),
1594            virtual_modules: HashMap::new(),
1595            tied_hashes: HashMap::new(),
1596            tied_scalars: HashMap::new(),
1597            tied_arrays: HashMap::new(),
1598            overload_table: HashMap::new(),
1599            format_templates: HashMap::new(),
1600            special_caret_scalars,
1601            format_page_number: 0,
1602            format_lines_per_page: 60,
1603            format_lines_left: 0,
1604            format_line_break_chars: "\n".to_string(),
1605            format_top_name: String::new(),
1606            accumulator_format: String::new(),
1607            max_system_fd: 2,
1608            emergency_memory: String::new(),
1609            last_subpattern_name: String::new(),
1610            inc_hook_index: 0,
1611            multiline_match: false,
1612            executable_path,
1613            formfeed_string: "\x0c".to_string(),
1614            glob_handle_alias: HashMap::new(),
1615            glob_restore_frames: vec![Vec::new()],
1616            special_var_restore_frames: vec![Vec::new()],
1617            reflection_hashes_ready: false,
1618            english_enabled: false,
1619            english_no_match_vars: false,
1620            english_match_vars_ever_enabled: false,
1621            english_lexical_scalars: vec![HashSet::new()],
1622            our_lexical_scalars: vec![HashSet::new()],
1623            vm_jit_enabled: !matches!(
1624                std::env::var("STRYKE_NO_JIT"),
1625                Ok(v)
1626                    if v == "1"
1627                        || v.eq_ignore_ascii_case("true")
1628                        || v.eq_ignore_ascii_case("yes")
1629            ),
1630            disasm_bytecode: false,
1631            cached_chunk: None,
1632            cache_script_path: None,
1633            stryke_pwd,
1634            in_generator: false,
1635            line_mode_skip_main: false,
1636            line_mode_chunk: None,
1637            line_mode_eof_pending: false,
1638            line_mode_stdin_pending: VecDeque::new(),
1639            rate_limit_slots: Vec::new(),
1640            log_level_override: None,
1641            current_sub_stack: Vec::new(),
1642            debugger: None,
1643            debug_call_stack: Vec::new(),
1644        };
1645        s.install_overload_pragma_stubs();
1646        s
1647    }
1648
1649    /// Lazily populate the reflection hashes (`%b`, `%stryke::builtins`, etc.)
1650    /// on first access. This avoids building ~12k hash entries on startup for
1651    /// one-liners that never touch introspection.
1652    pub(crate) fn ensure_reflection_hashes(&mut self) {
1653        if self.reflection_hashes_ready {
1654            return;
1655        }
1656        self.reflection_hashes_ready = true;
1657        // Package stashes (`%main::` / `%Pkg::`) are Perl-spec, install in
1658        // every mode — `--compat` does not turn off the symbol table.
1659        self.refresh_package_stashes();
1660        // Everything below is stryke-only. `--compat` skips the entire block
1661        // so a Perl 5 script sees no extension hashes and can use `%all` /
1662        // `%b` / `%parameters` / `%stryke::*` etc. as ordinary user hashes.
1663        if crate::compat_mode() {
1664            return;
1665        }
1666        let builtins_map = crate::builtins::builtins_hash_map();
1667        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1668        let extensions_map = crate::builtins::extensions_hash_map();
1669        let aliases_map = crate::builtins::aliases_hash_map();
1670        let descriptions_map = crate::builtins::descriptions_hash_map();
1671        let categories_map = crate::builtins::categories_hash_map();
1672        let primaries_map = crate::builtins::primaries_hash_map();
1673        let keywords_map = crate::builtins::keywords_hash_map();
1674        let operators_map = crate::builtins::operators_hash_map();
1675        let special_vars_map = crate::builtins::special_vars_hash_map();
1676        let all_map = crate::builtins::all_hash_map();
1677        self.scope
1678            .declare_hash_global_frozen("stryke::builtins", builtins_map.clone());
1679        self.scope
1680            .declare_hash_global_frozen("stryke::perl_compats", perl_compats_map.clone());
1681        self.scope
1682            .declare_hash_global_frozen("stryke::extensions", extensions_map.clone());
1683        self.scope
1684            .declare_hash_global_frozen("stryke::aliases", aliases_map.clone());
1685        self.scope
1686            .declare_hash_global_frozen("stryke::descriptions", descriptions_map.clone());
1687        self.scope
1688            .declare_hash_global_frozen("stryke::categories", categories_map.clone());
1689        self.scope
1690            .declare_hash_global_frozen("stryke::primaries", primaries_map.clone());
1691        self.scope
1692            .declare_hash_global_frozen("stryke::keywords", keywords_map.clone());
1693        self.scope
1694            .declare_hash_global_frozen("stryke::operators", operators_map.clone());
1695        self.scope
1696            .declare_hash_global_frozen("stryke::special_vars", special_vars_map.clone());
1697        self.scope
1698            .declare_hash_global_frozen("stryke::all", all_map.clone());
1699        // Short aliases: only declare if no user-declared hash with that name
1700        // exists, to avoid overwriting `my %e` etc.
1701        for (name, val) in [
1702            ("b", builtins_map),
1703            ("pc", perl_compats_map),
1704            ("e", extensions_map),
1705            ("a", aliases_map),
1706            ("d", descriptions_map),
1707            ("c", categories_map),
1708            ("p", primaries_map),
1709            ("k", keywords_map),
1710            ("o", operators_map),
1711            ("v", special_vars_map),
1712            ("all", all_map),
1713        ] {
1714            if !self.scope.any_frame_has_hash(name) {
1715                self.scope.declare_hash_global_frozen(name, val);
1716            }
1717        }
1718        // Initial install of `%parameters` (zsh-`$parameters` analogue).
1719        // Refreshed automatically on every read via `touch_env_hash`.
1720        if !self.scope.has_lexical_hash("parameters") {
1721            self.refresh_parameters_hash();
1722        }
1723    }
1724
1725    /// Rebuild `%parameters` (zsh-`$parameters` analogue) from the current
1726    /// scope. Maps every live sigil-prefixed name (`$x`, `@a`, `%h`, …) to its
1727    /// kind string (`"scalar"`, `"array"`, `"hash"`, `"atomic_array"`,
1728    /// `"atomic_hash"`, `"shared_array"`, `"shared_hash"`). Installed as a
1729    /// frozen global hash so user code can read it but not assign into it
1730    /// (parallel to `%all` / `%b` / `%stryke::*`). Refreshed automatically on
1731    /// every `%parameters` read via the `touch_env_hash` hook, so the snapshot
1732    /// is always current — the user never needs to call this directly.
1733    pub fn refresh_parameters_hash(&mut self) {
1734        let pairs = self.scope.parameters_pairs();
1735        let mut h: indexmap::IndexMap<String, StrykeValue> =
1736            indexmap::IndexMap::with_capacity(pairs.len());
1737        for (name, kind) in pairs {
1738            h.insert(name, StrykeValue::string(kind.to_string()));
1739        }
1740        // declare_hash_global_frozen overwrites unconditionally, so each
1741        // refresh replaces the prior snapshot.
1742        self.scope.declare_hash_global_frozen("parameters", h);
1743    }
1744
1745    /// Populate `%main::` / `%Foo::` package stashes with current symbol-table
1746    /// state so `keys %main::` and `keys %Foo::` enumerate live names. Maps
1747    /// each unqualified name → its kind string (`"scalar"`, `"array"`,
1748    /// `"hash"`, `"sub"`). Stryke has no real Perl typeglob layer; the kind
1749    /// string is the most useful per-symbol value we can offer.
1750    ///
1751    /// Callable repeatedly — overwrites prior stashes — so the REPL refreshes
1752    /// after every line and scripts can call it explicitly via the
1753    /// `refresh_stashes()` builtin if they want post-eval visibility.
1754    pub fn refresh_package_stashes(&mut self) {
1755        use indexmap::IndexMap;
1756
1757        let mut by_pkg: std::collections::HashMap<String, IndexMap<String, StrykeValue>> =
1758            std::collections::HashMap::new();
1759
1760        let record = |pkg: &str,
1761                      sym: &str,
1762                      kind: &str,
1763                      map: &mut std::collections::HashMap<
1764            String,
1765            IndexMap<String, StrykeValue>,
1766        >| {
1767            map.entry(pkg.to_string())
1768                .or_default()
1769                .insert(sym.to_string(), StrykeValue::string(kind.to_string()));
1770        };
1771
1772        // Subs: keys like "main::foo" / "Foo::Bar::baz".
1773        for key in self.subs.keys() {
1774            if let Some(idx) = key.rfind("::") {
1775                let (pkg, rest) = key.split_at(idx);
1776                let sym = &rest[2..];
1777                if pkg.is_empty() || sym.is_empty() {
1778                    continue;
1779                }
1780                record(pkg, sym, "sub", &mut by_pkg);
1781            } else {
1782                // Bare-name sub (no package qualifier) lives in main::.
1783                record("main", key, "sub", &mut by_pkg);
1784            }
1785        }
1786
1787        // Package-qualified scalars / arrays / hashes from every frame.
1788        for frame in self.scope.frames_for_introspection() {
1789            let (scalars, arrays, hashes) = frame;
1790            for name in scalars {
1791                if let Some(idx) = name.rfind("::") {
1792                    let (pkg, rest) = name.split_at(idx);
1793                    let sym = &rest[2..];
1794                    if !pkg.is_empty() && !sym.is_empty() {
1795                        record(pkg, sym, "scalar", &mut by_pkg);
1796                    }
1797                }
1798            }
1799            for name in arrays {
1800                if let Some(idx) = name.rfind("::") {
1801                    let (pkg, rest) = name.split_at(idx);
1802                    let sym = &rest[2..];
1803                    if !pkg.is_empty() && !sym.is_empty() {
1804                        record(pkg, sym, "array", &mut by_pkg);
1805                    }
1806                }
1807            }
1808            for name in hashes {
1809                if let Some(idx) = name.rfind("::") {
1810                    let (pkg, rest) = name.split_at(idx);
1811                    let sym = &rest[2..];
1812                    if !pkg.is_empty() && !sym.is_empty() {
1813                        record(pkg, sym, "hash", &mut by_pkg);
1814                    }
1815                }
1816            }
1817        }
1818
1819        // Install each `%Pkg::` in the global frame. Lexer emits the trailing
1820        // `::` as part of the name, so the stash hash lives under that exact
1821        // key. `declare_hash_global_frozen` overwrites any prior copy.
1822        for (pkg, mut entries) in by_pkg {
1823            entries.sort_keys();
1824            let key = format!("{}::", pkg);
1825            self.scope.declare_hash_global_frozen(&key, entries);
1826        }
1827    }
1828
1829    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1830    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1831    /// strict subs and to satisfy `use overload ();` call sites.
1832    fn install_overload_pragma_stubs(&mut self) {
1833        let empty: Block = vec![];
1834        for key in ["overload::import", "overload::unimport"] {
1835            let name = key.to_string();
1836            self.subs.insert(
1837                name.clone(),
1838                Arc::new(StrykeSub {
1839                    name,
1840                    params: vec![],
1841                    body: empty.clone(),
1842                    prototype: None,
1843                    closure_env: None,
1844                    fib_like: None,
1845                }),
1846            );
1847        }
1848    }
1849
1850    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1851    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1852    pub fn line_mode_worker_clone(&self) -> VMHelper {
1853        VMHelper {
1854            scope: self.scope.clone(),
1855            subs: self.subs.clone(),
1856            intercepts: self.intercepts.clone(),
1857            next_intercept_id: self.next_intercept_id,
1858            intercept_ctx_stack: self.intercept_ctx_stack.clone(),
1859            intercept_active_names: self.intercept_active_names.clone(),
1860            struct_defs: self.struct_defs.clone(),
1861            enum_defs: self.enum_defs.clone(),
1862            class_defs: self.class_defs.clone(),
1863            trait_defs: self.trait_defs.clone(),
1864            file: self.file.clone(),
1865            output_handles: HashMap::new(),
1866            input_handles: HashMap::new(),
1867            ofs: self.ofs.clone(),
1868            ors: self.ors.clone(),
1869            irs: self.irs.clone(),
1870            errno: self.errno.clone(),
1871            errno_code: self.errno_code,
1872            eval_error: self.eval_error.clone(),
1873            eval_error_code: self.eval_error_code,
1874            eval_error_value: self.eval_error_value.clone(),
1875            argv: self.argv.clone(),
1876            env: self.env.clone(),
1877            env_materialized: self.env_materialized,
1878            program_name: self.program_name.clone(),
1879            line_number: 0,
1880            last_readline_handle: String::new(),
1881            last_stdin_die_bracket: "<STDIN>".to_string(),
1882            handle_line_numbers: HashMap::new(),
1883            flip_flop_active: Vec::new(),
1884            flip_flop_exclusive_left_line: Vec::new(),
1885            flip_flop_sequence: Vec::new(),
1886            flip_flop_last_dot: Vec::new(),
1887            flip_flop_tree: HashMap::new(),
1888            sigint_pending_caret: Cell::new(false),
1889            auto_split: self.auto_split,
1890            field_separator: self.field_separator.clone(),
1891            begin_blocks: self.begin_blocks.clone(),
1892            unit_check_blocks: self.unit_check_blocks.clone(),
1893            check_blocks: self.check_blocks.clone(),
1894            init_blocks: self.init_blocks.clone(),
1895            end_blocks: self.end_blocks.clone(),
1896            warnings: self.warnings,
1897            output_autoflush: self.output_autoflush,
1898            default_print_handle: self.default_print_handle.clone(),
1899            suppress_stdout: self.suppress_stdout,
1900            // Workers start with fresh test counters — they don't share with the
1901            // parent. The parent is responsible for aggregating across workers if
1902            // it cares (none of the current parallel callers do).
1903            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1904            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1905            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1906            test_pass_total: std::sync::atomic::AtomicUsize::new(0),
1907            test_fail_total: std::sync::atomic::AtomicUsize::new(0),
1908            test_skip_total: std::sync::atomic::AtomicUsize::new(0),
1909            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1910            child_exit_status: self.child_exit_status,
1911            last_match: self.last_match.clone(),
1912            prematch: self.prematch.clone(),
1913            postmatch: self.postmatch.clone(),
1914            last_paren_match: self.last_paren_match.clone(),
1915            list_separator: self.list_separator.clone(),
1916            script_start_time: self.script_start_time,
1917            compile_hints: self.compile_hints,
1918            warning_bits: self.warning_bits,
1919            global_phase: self.global_phase.clone(),
1920            subscript_sep: self.subscript_sep.clone(),
1921            inplace_edit: self.inplace_edit.clone(),
1922            debug_flags: self.debug_flags,
1923            perl_debug_flags: self.perl_debug_flags,
1924            eval_nesting: self.eval_nesting,
1925            argv_current_file: String::new(),
1926            diamond_next_idx: 0,
1927            diamond_reader: None,
1928            strict_refs: self.strict_refs,
1929            strict_subs: self.strict_subs,
1930            strict_vars: self.strict_vars,
1931            utf8_pragma: self.utf8_pragma,
1932            open_pragma_utf8: self.open_pragma_utf8,
1933            feature_bits: self.feature_bits,
1934            num_threads: 0,
1935            regex_cache: self.regex_cache.clone(),
1936            regex_last: self.regex_last.clone(),
1937            regex_match_memo: self.regex_match_memo.clone(),
1938            regex_capture_scope_fresh: false,
1939            regex_pos: self.regex_pos.clone(),
1940            state_vars: self.state_vars.clone(),
1941            state_bindings_stack: Vec::new(),
1942            rand_rng: self.rand_rng.clone(),
1943            dir_handles: HashMap::new(),
1944            io_file_slots: HashMap::new(),
1945            pipe_children: HashMap::new(),
1946            socket_handles: HashMap::new(),
1947            wantarray_kind: self.wantarray_kind,
1948            profiler: None,
1949            module_export_lists: self.module_export_lists.clone(),
1950            virtual_modules: self.virtual_modules.clone(),
1951            tied_hashes: self.tied_hashes.clone(),
1952            tied_scalars: self.tied_scalars.clone(),
1953            tied_arrays: self.tied_arrays.clone(),
1954            overload_table: self.overload_table.clone(),
1955            format_templates: self.format_templates.clone(),
1956            special_caret_scalars: self.special_caret_scalars.clone(),
1957            format_page_number: self.format_page_number,
1958            format_lines_per_page: self.format_lines_per_page,
1959            format_lines_left: self.format_lines_left,
1960            format_line_break_chars: self.format_line_break_chars.clone(),
1961            format_top_name: self.format_top_name.clone(),
1962            accumulator_format: self.accumulator_format.clone(),
1963            max_system_fd: self.max_system_fd,
1964            emergency_memory: self.emergency_memory.clone(),
1965            last_subpattern_name: self.last_subpattern_name.clone(),
1966            inc_hook_index: self.inc_hook_index,
1967            multiline_match: self.multiline_match,
1968            executable_path: self.executable_path.clone(),
1969            formfeed_string: self.formfeed_string.clone(),
1970            glob_handle_alias: self.glob_handle_alias.clone(),
1971            glob_restore_frames: self.glob_restore_frames.clone(),
1972            special_var_restore_frames: self.special_var_restore_frames.clone(),
1973            reflection_hashes_ready: self.reflection_hashes_ready,
1974            english_enabled: self.english_enabled,
1975            english_no_match_vars: self.english_no_match_vars,
1976            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
1977            english_lexical_scalars: self.english_lexical_scalars.clone(),
1978            our_lexical_scalars: self.our_lexical_scalars.clone(),
1979            vm_jit_enabled: self.vm_jit_enabled,
1980            disasm_bytecode: self.disasm_bytecode,
1981            // Sideband cache fields belong to the top-level driver, not line-mode workers.
1982            cached_chunk: None,
1983            cache_script_path: None,
1984            stryke_pwd: self.stryke_pwd.clone(),
1985            in_generator: false,
1986            line_mode_skip_main: false,
1987            line_mode_chunk: self.line_mode_chunk.clone(),
1988            line_mode_eof_pending: false,
1989            line_mode_stdin_pending: VecDeque::new(),
1990            rate_limit_slots: Vec::new(),
1991            log_level_override: self.log_level_override,
1992            current_sub_stack: Vec::new(),
1993            debugger: None,
1994            debug_call_stack: Vec::new(),
1995        }
1996    }
1997
1998    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
1999    pub(crate) fn parallel_thread_count(&mut self) -> usize {
2000        if self.num_threads == 0 {
2001            self.num_threads = rayon::current_num_threads();
2002        }
2003        self.num_threads
2004    }
2005
2006    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
2007    pub(crate) fn eval_par_list_call(
2008        &mut self,
2009        name: &str,
2010        args: &[StrykeValue],
2011        ctx: WantarrayCtx,
2012        line: usize,
2013    ) -> StrykeResult<StrykeValue> {
2014        match name {
2015            "puniq" => {
2016                let (list_src, show_prog) = match args.len() {
2017                    0 => return Err(StrykeError::runtime("puniq: expected LIST", line)),
2018                    1 => (&args[0], false),
2019                    2 => (&args[0], args[1].is_true()),
2020                    _ => {
2021                        return Err(StrykeError::runtime(
2022                            "puniq: expected LIST [, progress => EXPR]",
2023                            line,
2024                        ));
2025                    }
2026                };
2027                let list = list_src.to_list();
2028                let n_threads = self.parallel_thread_count();
2029                let pmap_progress = PmapProgress::new(show_prog, list.len());
2030                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
2031                pmap_progress.finish();
2032                if ctx == WantarrayCtx::List {
2033                    Ok(StrykeValue::array(out))
2034                } else {
2035                    Ok(StrykeValue::integer(out.len() as i64))
2036                }
2037            }
2038            "pfirst" => {
2039                let (code_val, list_src, show_prog) = match args.len() {
2040                    2 => (&args[0], &args[1], false),
2041                    3 => (&args[0], &args[1], args[2].is_true()),
2042                    _ => {
2043                        return Err(StrykeError::runtime(
2044                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
2045                            line,
2046                        ));
2047                    }
2048                };
2049                let Some(sub) = code_val.as_code_ref() else {
2050                    return Err(StrykeError::runtime(
2051                        "pfirst: first argument must be a code reference",
2052                        line,
2053                    ));
2054                };
2055                let sub = sub.clone();
2056                let list = list_src.to_list();
2057                if list.is_empty() {
2058                    return Ok(StrykeValue::UNDEF);
2059                }
2060                let pmap_progress = PmapProgress::new(show_prog, list.len());
2061                let subs = self.subs.clone();
2062                let (scope_capture, atomic_arrays, atomic_hashes) =
2063                    self.scope.capture_with_atomics();
2064                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
2065                    let mut local_interp = VMHelper::new();
2066                    local_interp.subs = subs.clone();
2067                    local_interp.scope.restore_capture(&scope_capture);
2068                    local_interp
2069                        .scope
2070                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2071                    local_interp.enable_parallel_guard();
2072                    local_interp.scope.set_topic(item);
2073                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2074                        Ok(v) => v.is_true(),
2075                        Err(_) => false,
2076                    }
2077                });
2078                pmap_progress.finish();
2079                Ok(out.unwrap_or(StrykeValue::UNDEF))
2080            }
2081            "pany" => {
2082                let (code_val, list_src, show_prog) = match args.len() {
2083                    2 => (&args[0], &args[1], false),
2084                    3 => (&args[0], &args[1], args[2].is_true()),
2085                    _ => {
2086                        return Err(StrykeError::runtime(
2087                            "pany: expected BLOCK, LIST [, progress => EXPR]",
2088                            line,
2089                        ));
2090                    }
2091                };
2092                let Some(sub) = code_val.as_code_ref() else {
2093                    return Err(StrykeError::runtime(
2094                        "pany: first argument must be a code reference",
2095                        line,
2096                    ));
2097                };
2098                let sub = sub.clone();
2099                let list = list_src.to_list();
2100                let pmap_progress = PmapProgress::new(show_prog, list.len());
2101                let subs = self.subs.clone();
2102                let (scope_capture, atomic_arrays, atomic_hashes) =
2103                    self.scope.capture_with_atomics();
2104                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
2105                    let mut local_interp = VMHelper::new();
2106                    local_interp.subs = subs.clone();
2107                    local_interp.scope.restore_capture(&scope_capture);
2108                    local_interp
2109                        .scope
2110                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2111                    local_interp.enable_parallel_guard();
2112                    local_interp.scope.set_topic(item);
2113                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2114                        Ok(v) => v.is_true(),
2115                        Err(_) => false,
2116                    }
2117                });
2118                pmap_progress.finish();
2119                Ok(StrykeValue::integer(if b { 1 } else { 0 }))
2120            }
2121            _ => Err(StrykeError::runtime(
2122                format!("internal: unknown par_list builtin {name}"),
2123                line,
2124            )),
2125        }
2126    }
2127
2128    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
2129        #[cfg(unix)]
2130        if let Some(sig) = s.signal() {
2131            return sig as i64 & 0x7f;
2132        }
2133        let code = s.code().unwrap_or(0) as i64;
2134        code << 8
2135    }
2136
2137    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
2138        self.child_exit_status = self.encode_exit_status(s);
2139    }
2140
2141    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
2142    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
2143        // Perl's $! is the bare description ("No such file or directory"),
2144        // not Rust's "<desc> (os error N)" form. Strip the trailing parenthetical.
2145        let s = e.to_string();
2146        let stripped = s
2147            .rfind(" (os error ")
2148            .map(|i| s[..i].to_string())
2149            .unwrap_or(s);
2150        self.errno = stripped;
2151        self.errno_code = e.raw_os_error().unwrap_or(0);
2152    }
2153
2154    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
2155    ///
2156    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
2157    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
2158    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
2159    ///
2160    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
2161    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
2162    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
2163    /// `~/.ssh/config` and keys apply.
2164    pub(crate) fn ssh_builtin_execute(
2165        &mut self,
2166        args: &[StrykeValue],
2167    ) -> StrykeResult<StrykeValue> {
2168        use std::process::Command;
2169        let mut cmd = Command::new("ssh");
2170        #[cfg(unix)]
2171        {
2172            use libc::geteuid;
2173            let home_for_ssh = if unsafe { geteuid() } == 0 {
2174                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
2175            } else {
2176                None
2177            };
2178            if let Some(h) = home_for_ssh {
2179                cmd.env("HOME", h);
2180            } else if std::env::var_os("HOME").is_none() {
2181                if let Some(h) = pw_home_dir_for_current_uid() {
2182                    cmd.env("HOME", h);
2183                }
2184            }
2185        }
2186        for a in args {
2187            cmd.arg(a.to_string());
2188        }
2189        match cmd.status() {
2190            Ok(s) => {
2191                self.record_child_exit_status(s);
2192                Ok(StrykeValue::integer(s.code().unwrap_or(-1) as i64))
2193            }
2194            Err(e) => {
2195                self.apply_io_error_to_errno(&e);
2196                Ok(StrykeValue::integer(-1))
2197            }
2198        }
2199    }
2200
2201    /// Set `$@` message; numeric side is `0` if empty, else `1`.
2202    pub(crate) fn set_eval_error(&mut self, msg: String) {
2203        self.eval_error = msg;
2204        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2205        self.eval_error_value = None;
2206    }
2207
2208    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &StrykeError) {
2209        self.eval_error = e.to_string();
2210        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2211        self.eval_error_value = e.die_value.clone();
2212    }
2213
2214    pub(crate) fn clear_eval_error(&mut self) {
2215        self.eval_error = String::new();
2216        self.eval_error_code = 0;
2217        self.eval_error_value = None;
2218    }
2219
2220    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
2221    fn bump_line_for_handle(&mut self, handle_key: &str) {
2222        self.last_readline_handle = handle_key.to_string();
2223        *self
2224            .handle_line_numbers
2225            .entry(handle_key.to_string())
2226            .or_insert(0) += 1;
2227    }
2228
2229    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
2230    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
2231        if name.starts_with('^') {
2232            return name.to_string();
2233        }
2234        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
2235            let pkg = self.current_package();
2236            if !pkg.is_empty() && pkg != "main" {
2237                return format!("{}::{}", pkg, name);
2238            }
2239        }
2240        name.to_string()
2241    }
2242
2243    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
2244    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
2245        if name.contains("::") {
2246            return name.to_string();
2247        }
2248        let pkg = self.current_package();
2249        if pkg.is_empty() || pkg == "main" {
2250            format!("main::{}", name)
2251        } else {
2252            format!("{}::{}", pkg, name)
2253        }
2254    }
2255
2256    /// Bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
2257    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
2258        if name.contains("::") {
2259            return name.to_string();
2260        }
2261        for (lex, our) in self
2262            .english_lexical_scalars
2263            .iter()
2264            .zip(self.our_lexical_scalars.iter())
2265            .rev()
2266        {
2267            if lex.contains(name) {
2268                if our.contains(name) {
2269                    return self.stash_scalar_name_for_package(name);
2270                }
2271                return name.to_string();
2272            }
2273        }
2274        name.to_string()
2275    }
2276
2277    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
2278    pub(crate) fn tie_execute(
2279        &mut self,
2280        target_kind: u8,
2281        target_name: &str,
2282        class_and_args: Vec<StrykeValue>,
2283        line: usize,
2284    ) -> StrykeResult<StrykeValue> {
2285        let mut it = class_and_args.into_iter();
2286        let class = it.next().unwrap_or(StrykeValue::UNDEF);
2287        let pkg = class.to_string();
2288        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
2289        let tie_ctor = match target_kind {
2290            0 => "TIESCALAR",
2291            1 => "TIEARRAY",
2292            2 => "TIEHASH",
2293            _ => return Err(StrykeError::runtime("tie: invalid target kind", line)),
2294        };
2295        let tie_fn = format!("{}::{}", pkg, tie_ctor);
2296        let sub =
2297            self.subs.get(&tie_fn).cloned().ok_or_else(|| {
2298                StrykeError::runtime(format!("tie: cannot find &{}", tie_fn), line)
2299            })?;
2300        let mut call_args = vec![StrykeValue::string(pkg.clone())];
2301        call_args.extend(it);
2302        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
2303            Ok(v) => v,
2304            Err(FlowOrError::Flow(_)) => StrykeValue::UNDEF,
2305            Err(FlowOrError::Error(e)) => return Err(e),
2306        };
2307        match target_kind {
2308            0 => {
2309                self.tied_scalars.insert(target_name.to_string(), obj);
2310            }
2311            1 => {
2312                let key = self.stash_array_name_for_package(target_name);
2313                self.tied_arrays.insert(key, obj);
2314            }
2315            2 => {
2316                self.tied_hashes.insert(target_name.to_string(), obj);
2317            }
2318            _ => return Err(StrykeError::runtime("tie: invalid target kind", line)),
2319        }
2320        Ok(StrykeValue::UNDEF)
2321    }
2322
2323    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
2324    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
2325        let key = format!("{}::ISA", class);
2326        self.scope
2327            .get_array(&key)
2328            .into_iter()
2329            .map(|v| v.to_string())
2330            .collect()
2331    }
2332
2333    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
2334        let p = |c: &str| self.parents_of_class(c);
2335        linearize_c3(class, &p, 0)
2336    }
2337
2338    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
2339    pub(crate) fn resolve_method_full_name(
2340        &self,
2341        invocant_class: &str,
2342        method: &str,
2343        super_mode: bool,
2344    ) -> Option<String> {
2345        let mro = self.mro_linearize(invocant_class);
2346        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
2347        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
2348        // even when running `C::meth`.
2349        let start = if super_mode {
2350            mro.iter()
2351                .position(|p| p == invocant_class)
2352                .map(|i| i + 1)
2353                // If the class string does not appear in MRO (should be rare), skip the first
2354                // entry so we still search parents before giving up.
2355                .unwrap_or(1)
2356        } else {
2357            0
2358        };
2359        for pkg in mro.iter().skip(start) {
2360            if pkg == "UNIVERSAL" {
2361                continue;
2362            }
2363            let fq = format!("{}::{}", pkg, method);
2364            if self.subs.contains_key(&fq) {
2365                return Some(fq);
2366            }
2367        }
2368        mro.iter()
2369            .skip(start)
2370            .find(|p| *p != "UNIVERSAL")
2371            .map(|pkg| format!("{}::{}", pkg, method))
2372    }
2373
2374    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
2375        if let Some(alias) = self.glob_handle_alias.get(name) {
2376            return alias.clone();
2377        }
2378        // `print $fh …` stores the handle as "$varname"; resolve it by
2379        // reading the scalar variable which holds the IO handle name.
2380        if let Some(var_name) = name.strip_prefix('$') {
2381            let val = self.scope.get_scalar(var_name);
2382            let s = val.to_string();
2383            if !s.is_empty() {
2384                return self.resolve_io_handle_name(&s);
2385            }
2386        }
2387        name.to_string()
2388    }
2389
2390    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
2391    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
2392        if name.contains("::") {
2393            name.to_string()
2394        } else {
2395            self.qualify_sub_key(name)
2396        }
2397    }
2398
2399    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
2400    pub(crate) fn copy_typeglob_slots(
2401        &mut self,
2402        lhs: &str,
2403        rhs: &str,
2404        line: usize,
2405    ) -> StrykeResult<()> {
2406        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
2407        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
2408        match self.subs.get(&rhs_sub).cloned() {
2409            Some(s) => {
2410                self.subs.insert(lhs_sub, s);
2411            }
2412            None => {
2413                self.subs.remove(&lhs_sub);
2414            }
2415        }
2416        let sv = self.scope.get_scalar(rhs);
2417        self.scope
2418            .set_scalar(lhs, sv.clone())
2419            .map_err(|e| e.at_line(line))?;
2420        let lhs_an = self.stash_array_name_for_package(lhs);
2421        let rhs_an = self.stash_array_name_for_package(rhs);
2422        let av = self.scope.get_array(&rhs_an);
2423        self.scope
2424            .set_array(&lhs_an, av.clone())
2425            .map_err(|e| e.at_line(line))?;
2426        let hv = self.scope.get_hash(rhs);
2427        self.scope
2428            .set_hash(lhs, hv.clone())
2429            .map_err(|e| e.at_line(line))?;
2430        match self.glob_handle_alias.get(rhs).cloned() {
2431            Some(t) => {
2432                self.glob_handle_alias.insert(lhs.to_string(), t);
2433            }
2434            None => {
2435                self.glob_handle_alias.remove(lhs);
2436            }
2437        }
2438        Ok(())
2439    }
2440
2441    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2442    pub(crate) fn install_format_decl(
2443        &mut self,
2444        basename: &str,
2445        lines: &[String],
2446        line: usize,
2447    ) -> StrykeResult<()> {
2448        let pkg = self.current_package();
2449        let key = format!("{}::{}", pkg, basename);
2450        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2451        self.format_templates.insert(key, Arc::new(tmpl));
2452        Ok(())
2453    }
2454
2455    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2456    /// Anonymous overload handlers are emitted by the parser as a synthetic
2457    /// `__overload_anon_N` SubDecl at the top of the program (registered under
2458    /// `main::`); re-bind a clone under the current package so the dispatch
2459    /// `Pkg::__overload_anon_N` lookup at runtime resolves. (PARITY-012)
2460    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2461        let pkg = self.current_package();
2462        for (_, v) in pairs {
2463            if v.starts_with("__overload_anon_") {
2464                // Synthetic anon-overload subs are emitted at the top of the
2465                // program, before any user `package N` statement, so they're
2466                // registered under the bare name (qualify_sub_key returns the
2467                // unqualified form for the `main` package). Re-bind a clone
2468                // under `Pkg::name` so the dispatch lookup `Pkg::sub_short`
2469                // resolves.
2470                let pkg_key = format!("{}::{}", pkg, v);
2471                if !self.subs.contains_key(&pkg_key) {
2472                    let src = if let Some(s) = self.subs.get(v) {
2473                        Some(s.clone())
2474                    } else {
2475                        self.subs.get(&format!("main::{}", v)).cloned()
2476                    };
2477                    if let Some(sub) = src {
2478                        self.subs.insert(pkg_key, sub);
2479                    }
2480                }
2481            }
2482        }
2483        let ent = self.overload_table.entry(pkg).or_default();
2484        for (k, v) in pairs {
2485            ent.insert(k.clone(), v.clone());
2486        }
2487    }
2488
2489    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2490    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2491    pub(crate) fn local_declare_typeglob(
2492        &mut self,
2493        lhs: &str,
2494        rhs: Option<&str>,
2495        line: usize,
2496    ) -> StrykeResult<()> {
2497        let old = self.glob_handle_alias.remove(lhs);
2498        let Some(frame) = self.glob_restore_frames.last_mut() else {
2499            return Err(StrykeError::runtime(
2500                "internal: no glob restore frame for local *GLOB",
2501                line,
2502            ));
2503        };
2504        frame.push((lhs.to_string(), old));
2505        if let Some(r) = rhs {
2506            self.glob_handle_alias
2507                .insert(lhs.to_string(), r.to_string());
2508        }
2509        Ok(())
2510    }
2511
2512    pub(crate) fn scope_push_hook(&mut self) {
2513        self.scope.push_frame();
2514        self.glob_restore_frames.push(Vec::new());
2515        self.special_var_restore_frames.push(Vec::new());
2516        self.english_lexical_scalars.push(HashSet::new());
2517        self.our_lexical_scalars.push(HashSet::new());
2518        self.state_bindings_stack.push(Vec::new());
2519    }
2520
2521    #[inline]
2522    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2523        if let Some(s) = self.english_lexical_scalars.last_mut() {
2524            s.insert(name.to_string());
2525        }
2526    }
2527
2528    /// Snapshot the `english_lexical_scalars` stack for parallel worker spawn (rayon
2529    /// closures need owned `Vec<HashSet<String>>` they can `clone()` per-worker).
2530    #[inline]
2531    pub(crate) fn english_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2532        self.english_lexical_scalars.clone()
2533    }
2534
2535    /// Snapshot the `our_lexical_scalars` stack — companion to
2536    /// [`Self::english_lexical_scalars_clone`].
2537    #[inline]
2538    pub(crate) fn our_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2539        self.our_lexical_scalars.clone()
2540    }
2541
2542    /// Replace `english_lexical_scalars` wholesale (parallel-worker setup).
2543    #[inline]
2544    pub(crate) fn set_english_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2545        self.english_lexical_scalars = v;
2546    }
2547
2548    /// Replace `our_lexical_scalars` wholesale (parallel-worker setup).
2549    #[inline]
2550    pub(crate) fn set_our_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2551        self.our_lexical_scalars = v;
2552    }
2553
2554    #[inline]
2555    fn note_our_scalar(&mut self, bare_name: &str) {
2556        if let Some(s) = self.our_lexical_scalars.last_mut() {
2557            s.insert(bare_name.to_string());
2558        }
2559    }
2560
2561    /// Public wrapper for [`Self::english_note_lexical_scalar`] — used by bytecode
2562    /// `Op::DeclareOurSync*` to register bare names so worker `tree_scalar_storage_name`
2563    /// reads rewrite to `Pkg::x`.
2564    #[inline]
2565    pub(crate) fn english_note_lexical_scalar_pub(&mut self, name: &str) {
2566        self.english_note_lexical_scalar(name);
2567    }
2568
2569    /// Public wrapper for [`Self::note_our_scalar`] — see [`Self::english_note_lexical_scalar_pub`].
2570    #[inline]
2571    pub(crate) fn note_our_scalar_pub(&mut self, bare_name: &str) {
2572        self.note_our_scalar(bare_name);
2573    }
2574
2575    pub(crate) fn scope_pop_hook(&mut self) {
2576        if !self.scope.can_pop_frame() {
2577            return;
2578        }
2579        // Execute deferred blocks in LIFO order before popping the frame.
2580        // Important: defer blocks run in the CURRENT scope (not a new frame),
2581        // so they can modify variables in the enclosing scope.
2582        let defers = self.scope.take_defers();
2583        for coderef in defers {
2584            if let Some(sub) = coderef.as_code_ref() {
2585                // Execute the defer block body directly in the current scope,
2586                // without creating a new frame or restoring closure captures.
2587                // This allows defer { $x = 100 } to modify the outer $x.
2588                let saved_wa = self.wantarray_kind;
2589                self.wantarray_kind = WantarrayCtx::Void;
2590                let _ = self.exec_block_no_scope(&sub.body);
2591                self.wantarray_kind = saved_wa;
2592            }
2593        }
2594        // Save state variable values back before popping the frame
2595        if let Some(bindings) = self.state_bindings_stack.pop() {
2596            for (var_name, state_key) in &bindings {
2597                let val = self.scope.get_scalar(var_name).clone();
2598                self.state_vars.insert(state_key.clone(), val);
2599            }
2600        }
2601        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2602        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2603        if let Some(entries) = self.special_var_restore_frames.pop() {
2604            for (name, old) in entries.into_iter().rev() {
2605                let _ = self.set_special_var(&name, &old);
2606            }
2607        }
2608        if let Some(entries) = self.glob_restore_frames.pop() {
2609            for (name, old) in entries.into_iter().rev() {
2610                match old {
2611                    Some(s) => {
2612                        self.glob_handle_alias.insert(name, s);
2613                    }
2614                    None => {
2615                        self.glob_handle_alias.remove(&name);
2616                    }
2617                }
2618            }
2619        }
2620        self.scope.pop_frame();
2621        let _ = self.english_lexical_scalars.pop();
2622        let _ = self.our_lexical_scalars.pop();
2623    }
2624
2625    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2626    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2627    #[inline]
2628    pub(crate) fn enable_parallel_guard(&mut self) {
2629        self.scope.set_parallel_guard(true);
2630    }
2631
2632    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues after VM compilation.
2633    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2634        self.begin_blocks.clear();
2635        self.unit_check_blocks.clear();
2636        self.check_blocks.clear();
2637        self.init_blocks.clear();
2638        self.end_blocks.clear();
2639    }
2640
2641    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2642    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2643    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2644    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2645    /// there because it only calls [`Scope::pop_frame`].
2646    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2647        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2648            self.scope_pop_hook();
2649        }
2650    }
2651
2652    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2653    ///
2654    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2655    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2656    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2657    /// workers that call `perl_signal::poll`).
2658    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> StrykeResult<()> {
2659        self.touch_env_hash("SIG");
2660        let v = self.scope.get_hash_element("SIG", sig);
2661        if v.is_undef() {
2662            return Self::default_sig_action(sig);
2663        }
2664        if let Some(s) = v.as_str() {
2665            if s == "IGNORE" {
2666                return Ok(());
2667            }
2668            if s == "DEFAULT" {
2669                return Self::default_sig_action(sig);
2670            }
2671        }
2672        if let Some(sub) = v.as_code_ref() {
2673            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2674                Ok(_) => Ok(()),
2675                Err(FlowOrError::Flow(_)) => Ok(()),
2676                Err(FlowOrError::Error(e)) => Err(e),
2677            }
2678        } else {
2679            Self::default_sig_action(sig)
2680        }
2681    }
2682
2683    /// Dispatch `$SIG{__WARN__}` if a coderef is installed; fall back to stderr.
2684    /// Recursion is guarded by temporarily clearing the slot during dispatch so
2685    /// a `__WARN__` handler that itself calls `warn` does not loop.
2686    pub(crate) fn fire_pseudosig_warn(&mut self, msg: &str, line: usize) -> StrykeResult<()> {
2687        self.touch_env_hash("SIG");
2688        let slot = self.scope.get_hash_element("SIG", "__WARN__");
2689        if let Some(sub) = slot.as_code_ref() {
2690            let prev = slot;
2691            let _ = self
2692                .scope
2693                .set_hash_element("SIG", "__WARN__", StrykeValue::UNDEF);
2694            let arg = StrykeValue::string(msg.to_string());
2695            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2696            let _ = self.scope.set_hash_element("SIG", "__WARN__", prev);
2697            return match r {
2698                Ok(_) => Ok(()),
2699                Err(FlowOrError::Flow(_)) => Ok(()),
2700                Err(FlowOrError::Error(e)) => Err(e),
2701            };
2702        }
2703        eprint!("{}", msg);
2704        Ok(())
2705    }
2706
2707    /// Dispatch `$SIG{__DIE__}` if a coderef is installed. Perl semantics:
2708    /// the handler runs even when the die is going to be caught by an `eval`,
2709    /// and the die still propagates afterwards. If the handler itself dies,
2710    /// that error replaces the original. Recursion is guarded by temporarily
2711    /// clearing the slot during dispatch.
2712    pub(crate) fn fire_pseudosig_die(&mut self, msg: &str, line: usize) -> StrykeResult<()> {
2713        self.touch_env_hash("SIG");
2714        let slot = self.scope.get_hash_element("SIG", "__DIE__");
2715        if let Some(sub) = slot.as_code_ref() {
2716            let prev = slot;
2717            let _ = self
2718                .scope
2719                .set_hash_element("SIG", "__DIE__", StrykeValue::UNDEF);
2720            let arg = StrykeValue::string(msg.to_string());
2721            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2722            let _ = self.scope.set_hash_element("SIG", "__DIE__", prev);
2723            return match r {
2724                Ok(_) => Ok(()),
2725                Err(FlowOrError::Flow(_)) => Ok(()),
2726                Err(FlowOrError::Error(e)) => Err(e),
2727            };
2728        }
2729        Ok(())
2730    }
2731
2732    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2733    #[inline]
2734    fn default_sig_action(sig: &str) -> StrykeResult<()> {
2735        match sig {
2736            // 128 + signal number (common shell convention)
2737            "INT" => std::process::exit(130),
2738            "TERM" => std::process::exit(143),
2739            "ALRM" => std::process::exit(142),
2740            // Default for SIGCHLD is ignore
2741            "CHLD" => Ok(()),
2742            _ => Ok(()),
2743        }
2744    }
2745
2746    /// Notify the debugger that a user sub call is about to happen. No-op
2747    /// when no debugger is attached. Paired with [`Self::debugger_leave_sub`]
2748    /// after the call returns — the depth counter is what makes step-over
2749    /// skip past UDFs instead of stepping into them.
2750    #[inline]
2751    pub fn debugger_enter_sub(&mut self, name: &str) {
2752        if let Some(dbg) = &mut self.debugger {
2753            dbg.enter_sub(name);
2754        }
2755    }
2756
2757    /// Pair to [`Self::debugger_enter_sub`].
2758    #[inline]
2759    pub fn debugger_leave_sub(&mut self) {
2760        if let Some(dbg) = &mut self.debugger {
2761            dbg.leave_sub();
2762        }
2763    }
2764
2765    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2766    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2767    pub fn materialize_env_if_needed(&mut self) {
2768        if self.env_materialized {
2769            return;
2770        }
2771        self.env = std::env::vars()
2772            .map(|(k, v)| (k, StrykeValue::string(v)))
2773            .collect();
2774        self.scope
2775            .set_hash("ENV", self.env.clone())
2776            .expect("set %ENV");
2777        self.env_materialized = true;
2778    }
2779
2780    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2781    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2782        self.materialize_env_if_needed();
2783        if let Some(x) = self.log_level_override {
2784            return x;
2785        }
2786        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2787        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2788    }
2789
2790    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2791    pub(crate) fn no_color_effective(&mut self) -> bool {
2792        self.materialize_env_if_needed();
2793        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2794        if v.is_undef() {
2795            return false;
2796        }
2797        !v.to_string().is_empty()
2798    }
2799
2800    #[inline]
2801    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2802        // `%main::ENV` ≡ `%ENV`, `%main::parameters` ≡ `%parameters`,
2803        // `%main::a` ≡ `%a`, etc. Strip the `main::` qualifier so the
2804        // lazy-materialize / reflection-hash branches fire on the
2805        // canonical bare name. Without this, `exists $main::ENV{PATH}`
2806        // returns 0 on a fresh interpreter because ENV never gets
2807        // materialized.
2808        let hash_name: &str = crate::scope::strip_main_prefix(hash_name).unwrap_or(hash_name);
2809        if hash_name == "ENV" {
2810            self.materialize_env_if_needed();
2811        } else if hash_name == "parameters"
2812            && !crate::compat_mode()
2813            && !self.scope.has_lexical_hash("parameters")
2814        {
2815            // `%parameters` (zsh `$parameters` analogue) — rebuild on every
2816            // read so it always reflects current scope state. Frozen install,
2817            // so user code can read but not assign into it. Stryke-only;
2818            // `--compat` skips the auto-refresh so Perl 5 scripts that use
2819            // `%parameters` for their own purposes are unaffected.
2820            self.ensure_reflection_hashes();
2821            self.refresh_parameters_hash();
2822        } else if hash_name.ends_with("::") && hash_name.len() > 2 {
2823            // `%main::` / `%Foo::` — repopulate from current symbol table on
2824            // every read so newly-defined subs / `our` vars become visible
2825            // without an explicit `refresh_stashes()` call. Cheap: walks
2826            // `subs` keys + frame name lists, no value cloning.
2827            self.refresh_package_stashes();
2828        } else if !self.reflection_hashes_ready && !self.scope.has_lexical_hash(hash_name) {
2829            match hash_name {
2830                "b"
2831                | "pc"
2832                | "e"
2833                | "a"
2834                | "d"
2835                | "c"
2836                | "p"
2837                | "k"
2838                | "o"
2839                | "v"
2840                | "all"
2841                | "stryke::builtins"
2842                | "stryke::perl_compats"
2843                | "stryke::extensions"
2844                | "stryke::aliases"
2845                | "stryke::descriptions"
2846                | "stryke::categories"
2847                | "stryke::primaries"
2848                | "stryke::keywords"
2849                | "stryke::operators"
2850                | "stryke::special_vars"
2851                | "stryke::all" => {
2852                    self.ensure_reflection_hashes();
2853                }
2854                _ => {}
2855            }
2856        }
2857    }
2858
2859    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2860    pub(crate) fn exists_arrow_hash_element(
2861        &self,
2862        container: StrykeValue,
2863        key: &str,
2864        line: usize,
2865    ) -> StrykeResult<bool> {
2866        if let Some(r) = container.as_hash_ref() {
2867            return Ok(r.read().contains_key(key));
2868        }
2869        if let Some(b) = container.as_blessed_ref() {
2870            let data = b.data.read();
2871            if let Some(r) = data.as_hash_ref() {
2872                return Ok(r.read().contains_key(key));
2873            }
2874            if let Some(hm) = data.as_hash_map() {
2875                return Ok(hm.contains_key(key));
2876            }
2877            return Err(StrykeError::runtime(
2878                "exists argument is not a HASH reference",
2879                line,
2880            ));
2881        }
2882        // `exists $h{x}{y}` when `$h{x}` is undef OR a non-hash scalar: Perl
2883        // returns false for the deepest test without erroring. Stryke
2884        // previously errored on the intermediate. Match Perl. (BUG-009)
2885        let _ = line;
2886        Ok(false)
2887    }
2888
2889    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
2890    pub(crate) fn delete_arrow_hash_element(
2891        &self,
2892        container: StrykeValue,
2893        key: &str,
2894        line: usize,
2895    ) -> StrykeResult<StrykeValue> {
2896        if let Some(r) = container.as_hash_ref() {
2897            return Ok(r.write().shift_remove(key).unwrap_or(StrykeValue::UNDEF));
2898        }
2899        if let Some(b) = container.as_blessed_ref() {
2900            let mut data = b.data.write();
2901            if let Some(r) = data.as_hash_ref() {
2902                return Ok(r.write().shift_remove(key).unwrap_or(StrykeValue::UNDEF));
2903            }
2904            if let Some(mut map) = data.as_hash_map() {
2905                let v = map.shift_remove(key).unwrap_or(StrykeValue::UNDEF);
2906                *data = StrykeValue::hash(map);
2907                return Ok(v);
2908            }
2909            return Err(StrykeError::runtime(
2910                "delete argument is not a HASH reference",
2911                line,
2912            ));
2913        }
2914        Err(StrykeError::runtime(
2915            "delete argument is not a HASH reference",
2916            line,
2917        ))
2918    }
2919
2920    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
2921    pub(crate) fn exists_arrow_array_element(
2922        &self,
2923        container: StrykeValue,
2924        idx: i64,
2925        line: usize,
2926    ) -> StrykeResult<bool> {
2927        if let Some(a) = container.as_array_ref() {
2928            let arr = a.read();
2929            let i = if idx < 0 {
2930                (arr.len() as i64 + idx) as usize
2931            } else {
2932                idx as usize
2933            };
2934            return Ok(i < arr.len());
2935        }
2936        // `exists $a[5][0]` when `$a[5]` is missing OR a non-array scalar:
2937        // Perl returns false at the deepest test without erroring. (BUG-009)
2938        let _ = line;
2939        Ok(false)
2940    }
2941
2942    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
2943    pub(crate) fn delete_arrow_array_element(
2944        &self,
2945        container: StrykeValue,
2946        idx: i64,
2947        line: usize,
2948    ) -> StrykeResult<StrykeValue> {
2949        if let Some(a) = container.as_array_ref() {
2950            let mut arr = a.write();
2951            let i = if idx < 0 {
2952                (arr.len() as i64 + idx) as usize
2953            } else {
2954                idx as usize
2955            };
2956            if i >= arr.len() {
2957                return Ok(StrykeValue::UNDEF);
2958            }
2959            let old = arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
2960            arr[i] = StrykeValue::UNDEF;
2961            return Ok(old);
2962        }
2963        Err(StrykeError::runtime(
2964            "delete argument is not an ARRAY reference",
2965            line,
2966        ))
2967    }
2968
2969    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
2970    pub(crate) fn inc_directories(&self) -> Vec<String> {
2971        let mut v: Vec<String> = self
2972            .scope
2973            .get_array("INC")
2974            .into_iter()
2975            .map(|x| x.to_string())
2976            .filter(|s| !s.is_empty())
2977            .collect();
2978        if v.is_empty() {
2979            v.push(".".to_string());
2980        }
2981        v
2982    }
2983
2984    #[inline]
2985    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
2986        matches!(
2987            name,
2988            "_" | "0"
2989                | "!"
2990                | "@"
2991                | "/"
2992                | "\\"
2993                | ","
2994                | "."
2995                | "__PACKAGE__"
2996                | "$$"
2997                | "|"
2998                | "?"
2999                | "\""
3000                | "&"
3001                | "`"
3002                | "'"
3003                | "+"
3004                | "<"
3005                | ">"
3006                | "("
3007                | ")"
3008                | "]"
3009                | ";"
3010                | "ARGV"
3011                | "%"
3012                | "="
3013                | "-"
3014                | ":"
3015                | "*"
3016                | "INC"
3017                // sort/reduce comparator slots — predefined package globals
3018                // ($main::a, $main::b). Perl exempts them globally, not just
3019                // inside sort blocks, so any reference compiles cleanly.
3020                | "a"
3021                | "b"
3022        ) || name.chars().all(|c| c.is_ascii_digit())
3023            || name.starts_with('^')
3024            || (name.starts_with('#') && name.len() > 1)
3025            // Stryke implicit closure-param slots (`$_0`, `$_1`, …, `$_99`).
3026            // These are auto-bound inside any block that takes positional
3027            // arguments (sort comparators, reduce blocks, sub bodies, map/
3028            // grep blocks). Treat them like the digit-only match groups —
3029            // exempt globally so a strict-vars check inside a `preduce {
3030            // $_0 + $_1 }` block doesn't reject them as undeclared.
3031            || (name.starts_with('_')
3032                && name.len() > 1
3033                && name[1..].chars().all(|c| c.is_ascii_digit()))
3034    }
3035
3036    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3037        if !self.strict_vars
3038            || Self::strict_scalar_exempt(name)
3039            || name.contains("::")
3040            || self.scope.scalar_binding_exists(name)
3041        {
3042            return Ok(());
3043        }
3044        Err(StrykeError::runtime(
3045            format!(
3046                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
3047                name, name
3048            ),
3049            line,
3050        )
3051        .into())
3052    }
3053
3054    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3055        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
3056            return Ok(());
3057        }
3058        Err(StrykeError::runtime(
3059            format!(
3060                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
3061                name, name
3062            ),
3063            line,
3064        )
3065        .into())
3066    }
3067
3068    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3069        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
3070        if !self.strict_vars
3071            || name.contains("::")
3072            || self.scope.hash_binding_exists(name)
3073            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
3074        {
3075            return Ok(());
3076        }
3077        Err(StrykeError::runtime(
3078            format!(
3079                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
3080                name, name
3081            ),
3082            line,
3083        )
3084        .into())
3085    }
3086
3087    fn looks_like_version_only(spec: &str) -> bool {
3088        let t = spec.trim();
3089        !t.is_empty()
3090            && !t.contains('/')
3091            && !t.contains('\\')
3092            && !t.contains("::")
3093            && t.chars()
3094                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
3095            && t.chars().any(|c| c.is_ascii_digit())
3096    }
3097
3098    fn module_spec_to_relpath(spec: &str) -> String {
3099        let t = spec.trim();
3100        if t.contains("::") {
3101            format!("{}.pm", t.replace("::", "/"))
3102        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
3103            t.replace('\\', "/")
3104        } else {
3105            format!("{}.pm", t)
3106        }
3107    }
3108
3109    /// Lockfile-driven module resolution (RFC §"Module Resolution"). Walks up from
3110    /// `cwd` for `stryke.toml`, then asks [`crate::pkg::commands::resolve_module`]
3111    /// to find the module either in `lib/` or in the lockfile-pinned store. The
3112    /// `relpath` arg is the `@INC`-style path (`Foo/Bar.pm`) used elsewhere in
3113    /// `require`; it is converted to a logical name (`Foo::Bar`) for the resolver.
3114    /// Both `.pm` and `.stk` variants are tried — stryke source uses `.stk`.
3115    fn try_resolve_via_lockfile(relpath: &str) -> Option<std::path::PathBuf> {
3116        let cwd = std::env::current_dir().ok()?;
3117        let project_root = crate::pkg::commands::find_project_root(&cwd)?;
3118
3119        // Convert "Foo/Bar.pm" → "Foo::Bar". Drop the trailing extension so
3120        // `resolve_module` (which appends `.stk`) builds the right path.
3121        let stem = relpath
3122            .strip_suffix(".pm")
3123            .or_else(|| relpath.strip_suffix(".pl"))
3124            .or_else(|| relpath.strip_suffix(".stk"))
3125            .unwrap_or(relpath);
3126        let logical = stem.replace('/', "::");
3127
3128        crate::pkg::commands::resolve_module(&project_root, &logical).unwrap_or_default()
3129    }
3130
3131    /// `sub name` in `package P` → stash key `P::name`. `sub Q::name { }` is already fully
3132    /// qualified — do not prepend the current package. Unqualified names in `main` are stored
3133    /// **bare** (`name`), matching the compiler's `Op::Call` interning so the VM's
3134    /// `sub_for_closure_restore` lookup hits in one step.
3135    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
3136        if name.contains("::") {
3137            return name.to_string();
3138        }
3139        let pkg = self.current_package();
3140        if pkg.is_empty() || pkg == "main" {
3141            name.to_string()
3142        } else {
3143            format!("{}::{}", pkg, name)
3144        }
3145    }
3146
3147    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
3148    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
3149        let mut msg = format!("Undefined subroutine &{}", name);
3150        if self.strict_subs {
3151            msg.push_str(
3152                " (strict subs: declare the sub or use a fully qualified name before calling)",
3153            );
3154        }
3155        msg
3156    }
3157
3158    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
3159    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
3160        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
3161        if self.strict_subs {
3162            msg.push_str(
3163                " (strict subs: declare the sub or use a fully qualified name before calling)",
3164            );
3165        }
3166        msg
3167    }
3168
3169    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
3170    fn import_alias_key(&self, short: &str) -> String {
3171        self.qualify_sub_key(short)
3172    }
3173
3174    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
3175    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
3176        if imports.len() == 1 {
3177            match &imports[0].kind {
3178                ExprKind::QW(ws) => return ws.is_empty(),
3179                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
3180                ExprKind::List(xs) => return xs.is_empty(),
3181                _ => {}
3182            }
3183        }
3184        false
3185    }
3186
3187    /// After `require`, copy `Module::export` → caller stash per `use` list.
3188    fn apply_module_import(
3189        &mut self,
3190        module: &str,
3191        imports: &[Expr],
3192        line: usize,
3193    ) -> StrykeResult<()> {
3194        if imports.is_empty() {
3195            return self.import_all_from_module(module, line);
3196        }
3197        if Self::is_explicit_empty_import_list(imports) {
3198            return Ok(());
3199        }
3200        let names = Self::pragma_import_strings(imports, line)?;
3201        if names.is_empty() {
3202            return Ok(());
3203        }
3204        for name in names {
3205            self.import_one_symbol(module, &name, line)?;
3206        }
3207        Ok(())
3208    }
3209
3210    fn import_all_from_module(&mut self, module: &str, line: usize) -> StrykeResult<()> {
3211        if let Some(lists) = self.module_export_lists.get(module) {
3212            let export: Vec<String> = lists.export.clone();
3213            for short in export {
3214                self.import_named_sub(module, &short, line)?;
3215            }
3216            return Ok(());
3217        }
3218        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
3219        let prefix = format!("{}::", module);
3220        let keys: Vec<String> = self
3221            .subs
3222            .keys()
3223            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
3224            .cloned()
3225            .collect();
3226        for k in keys {
3227            let short = k[prefix.len()..].to_string();
3228            if let Some(sub) = self.subs.get(&k).cloned() {
3229                let alias = self.import_alias_key(&short);
3230                self.subs.insert(alias, sub);
3231            }
3232        }
3233        Ok(())
3234    }
3235
3236    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
3237    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> StrykeResult<()> {
3238        let qual = format!("{}::{}", module, short);
3239        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
3240            StrykeError::runtime(
3241                format!(
3242                    "`{}` is not defined in module `{}` (expected `{}`)",
3243                    short, module, qual
3244                ),
3245                line,
3246            )
3247        })?;
3248        let alias = self.import_alias_key(short);
3249        self.subs.insert(alias, sub);
3250        Ok(())
3251    }
3252
3253    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> StrykeResult<()> {
3254        if let Some(lists) = self.module_export_lists.get(module) {
3255            let allowed: HashSet<&str> = lists
3256                .export
3257                .iter()
3258                .map(|s| s.as_str())
3259                .chain(lists.export_ok.iter().map(|s| s.as_str()))
3260                .collect();
3261            if !allowed.contains(export) {
3262                return Err(StrykeError::runtime(
3263                    format!(
3264                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
3265                        export, module
3266                    ),
3267                    line,
3268                ));
3269            }
3270        }
3271        self.import_named_sub(module, export, line)
3272    }
3273
3274    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
3275    fn record_exporter_our_array_name(&mut self, name: &str, items: &[StrykeValue]) {
3276        if name != "EXPORT" && name != "EXPORT_OK" {
3277            return;
3278        }
3279        let pkg = self.current_package();
3280        if pkg.is_empty() || pkg == "main" {
3281            return;
3282        }
3283        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
3284        let ent = self.module_export_lists.entry(pkg).or_default();
3285        if name == "EXPORT" {
3286            ent.export = names;
3287        } else {
3288            ent.export_ok = names;
3289        }
3290    }
3291
3292    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
3293    /// Refresh [`StrykeSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
3294    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
3295    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
3296        let key = self.qualify_sub_key(name);
3297        let Some(sub) = self.subs.get(&key).cloned() else {
3298            return;
3299        };
3300        let captured = self.scope.capture();
3301        let closure_env = if captured.is_empty() {
3302            None
3303        } else {
3304            Some(captured)
3305        };
3306        let mut new_sub = (*sub).clone();
3307        new_sub.closure_env = closure_env;
3308        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
3309        self.subs.insert(key, Arc::new(new_sub));
3310    }
3311
3312    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<StrykeSub>> {
3313        if let Some(s) = self.subs.get(name) {
3314            return Some(s.clone());
3315        }
3316        if !name.contains("::") {
3317            // Non-`main` packages store subs at `Pkg::name`; resolve bare callers there.
3318            let pkg = self.current_package();
3319            if !pkg.is_empty() && pkg != "main" {
3320                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
3321                q.push_str(&pkg);
3322                q.push_str("::");
3323                q.push_str(name);
3324                return self.subs.get(&q).cloned();
3325            }
3326            return None;
3327        }
3328        // `\&main::greet` / `defined &main::greet`: subs in `main` are stored bare so the
3329        // compiler's `Op::Call("greet", ...)` and the runtime stash lookup share a key.
3330        // Strip the `main::` qualifier and try the bare form so explicit qualified callers
3331        // still resolve to the same sub.
3332        if let Some(rest) = name.strip_prefix("main::") {
3333            if !rest.contains("::") {
3334                return self.subs.get(rest).cloned();
3335            }
3336        }
3337        None
3338    }
3339
3340    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
3341    /// before calling `import`).
3342    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
3343        if let Some(first) = imports.first() {
3344            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
3345                return &imports[1..];
3346            }
3347        }
3348        imports
3349    }
3350
3351    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
3352    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> StrykeResult<Vec<String>> {
3353        let mut out = Vec::new();
3354        for e in imports {
3355            match &e.kind {
3356                ExprKind::String(s) => out.push(s.clone()),
3357                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
3358                ExprKind::Integer(n) => out.push(n.to_string()),
3359                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
3360                // a single interpolated variable.  Reconstruct the sigil+name form.
3361                ExprKind::InterpolatedString(parts) => {
3362                    let mut s = String::new();
3363                    for p in parts {
3364                        match p {
3365                            StringPart::Literal(l) => s.push_str(l),
3366                            StringPart::ScalarVar(v) => {
3367                                s.push('$');
3368                                s.push_str(v);
3369                            }
3370                            StringPart::ArrayVar(v) => {
3371                                s.push('@');
3372                                s.push_str(v);
3373                            }
3374                            _ => {
3375                                return Err(StrykeError::runtime(
3376                                    "pragma import must be a compile-time string, qw(), or integer",
3377                                    e.line.max(default_line),
3378                                ));
3379                            }
3380                        }
3381                    }
3382                    out.push(s);
3383                }
3384                _ => {
3385                    return Err(StrykeError::runtime(
3386                        "pragma import must be a compile-time string, qw(), or integer",
3387                        e.line.max(default_line),
3388                    ));
3389                }
3390            }
3391        }
3392        Ok(out)
3393    }
3394
3395    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3396        if imports.is_empty() {
3397            self.strict_refs = true;
3398            self.strict_subs = true;
3399            self.strict_vars = true;
3400            return Ok(());
3401        }
3402        let names = Self::pragma_import_strings(imports, line)?;
3403        for name in names {
3404            match name.as_str() {
3405                "refs" => self.strict_refs = true,
3406                "subs" => self.strict_subs = true,
3407                "vars" => self.strict_vars = true,
3408                _ => {
3409                    return Err(StrykeError::runtime(
3410                        format!("Unknown strict mode `{}`", name),
3411                        line,
3412                    ));
3413                }
3414            }
3415        }
3416        Ok(())
3417    }
3418
3419    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3420        if imports.is_empty() {
3421            self.strict_refs = false;
3422            self.strict_subs = false;
3423            self.strict_vars = false;
3424            return Ok(());
3425        }
3426        let names = Self::pragma_import_strings(imports, line)?;
3427        for name in names {
3428            match name.as_str() {
3429                "refs" => self.strict_refs = false,
3430                "subs" => self.strict_subs = false,
3431                "vars" => self.strict_vars = false,
3432                _ => {
3433                    return Err(StrykeError::runtime(
3434                        format!("Unknown strict mode `{}`", name),
3435                        line,
3436                    ));
3437                }
3438            }
3439        }
3440        Ok(())
3441    }
3442
3443    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3444        let items = Self::pragma_import_strings(imports, line)?;
3445        if items.is_empty() {
3446            return Err(StrykeError::runtime(
3447                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
3448                line,
3449            ));
3450        }
3451        for item in items {
3452            let s = item.trim();
3453            if let Some(rest) = s.strip_prefix(':') {
3454                self.apply_feature_bundle(rest, line)?;
3455            } else {
3456                self.apply_feature_name(s, true, line)?;
3457            }
3458        }
3459        Ok(())
3460    }
3461
3462    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3463        if imports.is_empty() {
3464            self.feature_bits = 0;
3465            return Ok(());
3466        }
3467        let items = Self::pragma_import_strings(imports, line)?;
3468        for item in items {
3469            let s = item.trim();
3470            if let Some(rest) = s.strip_prefix(':') {
3471                self.clear_feature_bundle(rest);
3472            } else {
3473                self.apply_feature_name(s, false, line)?;
3474            }
3475        }
3476        Ok(())
3477    }
3478
3479    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> StrykeResult<()> {
3480        let key = v.trim();
3481        match key {
3482            "5.10" | "5.010" | "5.10.0" => {
3483                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3484            }
3485            "5.12" | "5.012" | "5.12.0" => {
3486                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3487            }
3488            _ => {
3489                return Err(StrykeError::runtime(
3490                    format!("unsupported feature bundle :{}", key),
3491                    line,
3492                ));
3493            }
3494        }
3495        Ok(())
3496    }
3497
3498    fn clear_feature_bundle(&mut self, v: &str) {
3499        let key = v.trim();
3500        if matches!(
3501            key,
3502            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
3503        ) {
3504            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
3505        }
3506    }
3507
3508    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> StrykeResult<()> {
3509        let bit = match name {
3510            "say" => FEAT_SAY,
3511            "state" => FEAT_STATE,
3512            "switch" => FEAT_SWITCH,
3513            "unicode_strings" => FEAT_UNICODE_STRINGS,
3514            // Features that stryke accepts as known but tracks no separate bit for —
3515            // either always-on, always-off, or syntactic sugar already enabled.
3516            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
3517            "postderef"
3518            | "postderef_qq"
3519            | "evalbytes"
3520            | "current_sub"
3521            | "fc"
3522            | "lexical_subs"
3523            | "signatures"
3524            | "refaliasing"
3525            | "bitwise"
3526            | "isa"
3527            | "indirect"
3528            | "multidimensional"
3529            | "bareword_filehandles"
3530            | "try"
3531            | "defer"
3532            | "extra_paired_delimiters"
3533            | "module_true"
3534            | "class"
3535            | "array_base" => return Ok(()),
3536            _ => {
3537                return Err(StrykeError::runtime(
3538                    format!("unknown feature `{}`", name),
3539                    line,
3540                ));
3541            }
3542        };
3543        if enable {
3544            self.feature_bits |= bit;
3545        } else {
3546            self.feature_bits &= !bit;
3547        }
3548        Ok(())
3549    }
3550
3551    /// `require EXPR` — load once, record `%INC`, return `1` on success.
3552    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> StrykeResult<StrykeValue> {
3553        let t = spec.trim();
3554        if t.is_empty() {
3555            return Err(StrykeError::runtime("require: empty argument", line));
3556        }
3557        match t {
3558            "strict" => {
3559                self.apply_use_strict(&[], line)?;
3560                return Ok(StrykeValue::integer(1));
3561            }
3562            "utf8" => {
3563                self.utf8_pragma = true;
3564                return Ok(StrykeValue::integer(1));
3565            }
3566            "feature" | "v5" => {
3567                return Ok(StrykeValue::integer(1));
3568            }
3569            "warnings" => {
3570                self.warnings = true;
3571                return Ok(StrykeValue::integer(1));
3572            }
3573            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
3574                return Ok(StrykeValue::integer(1));
3575            }
3576            _ => {}
3577        }
3578        let p = Path::new(t);
3579        if p.is_absolute() {
3580            return self.require_absolute_path(p, line);
3581        }
3582        if t.starts_with("./") || t.starts_with("../") {
3583            return self.require_relative_path(p, line);
3584        }
3585        if Self::looks_like_version_only(t) {
3586            return Ok(StrykeValue::integer(1));
3587        }
3588        let relpath = Self::module_spec_to_relpath(t);
3589        self.require_from_inc(&relpath, line)
3590    }
3591
3592    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
3593    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> StrykeResult<()> {
3594        let v = self.scope.get_hash_element("^HOOK", key);
3595        if v.is_undef() {
3596            return Ok(());
3597        }
3598        let Some(sub) = v.as_code_ref() else {
3599            return Ok(());
3600        };
3601        let r = self.call_sub(
3602            sub.as_ref(),
3603            vec![StrykeValue::string(path.to_string())],
3604            WantarrayCtx::Scalar,
3605            line,
3606        );
3607        match r {
3608            Ok(_) => Ok(()),
3609            Err(FlowOrError::Error(e)) => Err(e),
3610            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
3611            Err(FlowOrError::Flow(other)) => Err(StrykeError::runtime(
3612                format!(
3613                    "require hook {:?} returned unexpected control flow: {:?}",
3614                    key, other
3615                ),
3616                line,
3617            )),
3618        }
3619    }
3620
3621    fn require_absolute_path(&mut self, path: &Path, line: usize) -> StrykeResult<StrykeValue> {
3622        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
3623        let key = canon.to_string_lossy().into_owned();
3624        if self.scope.exists_hash_element("INC", &key) {
3625            return Ok(StrykeValue::integer(1));
3626        }
3627        self.invoke_require_hook("require__before", &key, line)?;
3628        let code = read_file_text_perl_compat(&canon).map_err(|e| {
3629            StrykeError::runtime(
3630                format!("Can't open {} for reading: {}", canon.display(), e),
3631                line,
3632            )
3633        })?;
3634        let code = crate::data_section::strip_perl_end_marker(&code);
3635        self.scope
3636            .set_hash_element("INC", &key, StrykeValue::string(key.clone()))?;
3637        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3638        let r = crate::parse_and_run_module_in_file(code, self, &key);
3639        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3640        r?;
3641        self.invoke_require_hook("require__after", &key, line)?;
3642        Ok(StrykeValue::integer(1))
3643    }
3644
3645    fn require_relative_path(&mut self, path: &Path, line: usize) -> StrykeResult<StrykeValue> {
3646        if !path.exists() {
3647            return Err(StrykeError::runtime(
3648                format!(
3649                    "Can't locate {} (relative path does not exist)",
3650                    path.display()
3651                ),
3652                line,
3653            ));
3654        }
3655        self.require_absolute_path(path, line)
3656    }
3657
3658    fn require_from_inc(&mut self, relpath: &str, line: usize) -> StrykeResult<StrykeValue> {
3659        if self.scope.exists_hash_element("INC", relpath) {
3660            return Ok(StrykeValue::integer(1));
3661        }
3662        self.invoke_require_hook("require__before", relpath, line)?;
3663
3664        // Lockfile-driven module resolution. When the cwd is inside a stryke
3665        // project (`stryke.toml` reachable), `use Foo::Bar` first looks at
3666        // `lib/Foo/Bar.stk` and then at lockfile-pinned store entries before
3667        // falling through to `@INC`. See docs/PACKAGE_REGISTRY.md §"Module
3668        // Resolution".
3669        if let Some(found) = Self::try_resolve_via_lockfile(relpath) {
3670            let code = read_file_text_perl_compat(&found).map_err(|e| {
3671                StrykeError::runtime(
3672                    format!("Can't open {} for reading: {}", found.display(), e),
3673                    line,
3674                )
3675            })?;
3676            let code = crate::data_section::strip_perl_end_marker(&code);
3677            let abs = found.canonicalize().unwrap_or(found);
3678            let abs_s = abs.to_string_lossy().into_owned();
3679            self.scope
3680                .set_hash_element("INC", relpath, StrykeValue::string(abs_s.clone()))?;
3681            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3682            let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3683            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3684            r?;
3685            self.invoke_require_hook("require__after", relpath, line)?;
3686            return Ok(StrykeValue::integer(1));
3687        }
3688
3689        // Check virtual modules first (AOT bundles).
3690        if let Some(code) = self.virtual_modules.get(relpath).cloned() {
3691            let code = crate::data_section::strip_perl_end_marker(&code);
3692            self.scope.set_hash_element(
3693                "INC",
3694                relpath,
3695                StrykeValue::string(format!("(virtual)/{}", relpath)),
3696            )?;
3697            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3698            let r = crate::parse_and_run_module_in_file(code, self, relpath);
3699            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3700            r?;
3701            self.invoke_require_hook("require__after", relpath, line)?;
3702            return Ok(StrykeValue::integer(1));
3703        }
3704
3705        for dir in self.inc_directories() {
3706            let full = Path::new(&dir).join(relpath);
3707            if full.is_file() {
3708                let code = read_file_text_perl_compat(&full).map_err(|e| {
3709                    StrykeError::runtime(
3710                        format!("Can't open {} for reading: {}", full.display(), e),
3711                        line,
3712                    )
3713                })?;
3714                let code = crate::data_section::strip_perl_end_marker(&code);
3715                let abs = full.canonicalize().unwrap_or(full);
3716                let abs_s = abs.to_string_lossy().into_owned();
3717                self.scope
3718                    .set_hash_element("INC", relpath, StrykeValue::string(abs_s.clone()))?;
3719                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3720                let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3721                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3722                r?;
3723                self.invoke_require_hook("require__after", relpath, line)?;
3724                return Ok(StrykeValue::integer(1));
3725            }
3726        }
3727        Err(StrykeError::runtime(
3728            format!(
3729                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
3730                relpath
3731            ),
3732            line,
3733        ))
3734    }
3735
3736    /// Register a virtual module (for AOT bundles). Path should be relative like "lib/foo.stk".
3737    pub fn register_virtual_module(&mut self, path: String, source: String) {
3738        self.virtual_modules.insert(path, source);
3739    }
3740
3741    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
3742    pub(crate) fn exec_use_stmt(
3743        &mut self,
3744        module: &str,
3745        imports: &[Expr],
3746        line: usize,
3747    ) -> StrykeResult<()> {
3748        match module {
3749            "strict" => self.apply_use_strict(imports, line),
3750            "utf8" => {
3751                if !imports.is_empty() {
3752                    return Err(StrykeError::runtime("use utf8 takes no arguments", line));
3753                }
3754                self.utf8_pragma = true;
3755                Ok(())
3756            }
3757            "feature" => self.apply_use_feature(imports, line),
3758            "v5" => Ok(()),
3759            "warnings" => {
3760                self.warnings = true;
3761                Ok(())
3762            }
3763            "English" => {
3764                self.english_enabled = true;
3765                let args = Self::pragma_import_strings(imports, line)?;
3766                let no_match = args.iter().any(|a| a == "-no_match_vars");
3767                // Once match vars are exported (use English without -no_match_vars),
3768                // they stay available for the rest of the program — Perl exports them
3769                // into the caller's namespace and later pragmas cannot un-export them.
3770                if !no_match {
3771                    self.english_match_vars_ever_enabled = true;
3772                }
3773                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
3774                Ok(())
3775            }
3776            "Env" => self.apply_use_env(imports, line),
3777            "open" => self.apply_use_open(imports, line),
3778            "constant" => self.apply_use_constant(imports, line),
3779            "bigint" | "bignum" | "bigrat" => {
3780                // Activate BigInt promotion for `**` (and any other op
3781                // that consults the bigint pragma). `bignum` and
3782                // `bigrat` are routed here too — stryke doesn't yet
3783                // distinguish them from `bigint` for arithmetic, but
3784                // accepting them prevents the default-load path from
3785                // searching @INC for a CPAN module that won't parse.
3786                crate::set_bigint_pragma(true);
3787                Ok(())
3788            }
3789            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3790            _ => {
3791                self.require_execute(module, line)?;
3792                let imports = Self::imports_after_leading_use_version(imports);
3793                self.apply_module_import(module, imports, line)?;
3794                Ok(())
3795            }
3796        }
3797    }
3798
3799    /// `no strict 'refs'`, `no warnings`, `no feature`, …
3800    pub(crate) fn exec_no_stmt(
3801        &mut self,
3802        module: &str,
3803        imports: &[Expr],
3804        line: usize,
3805    ) -> StrykeResult<()> {
3806        match module {
3807            "strict" => self.apply_no_strict(imports, line),
3808            "utf8" => {
3809                if !imports.is_empty() {
3810                    return Err(StrykeError::runtime("no utf8 takes no arguments", line));
3811                }
3812                self.utf8_pragma = false;
3813                Ok(())
3814            }
3815            "feature" => self.apply_no_feature(imports, line),
3816            "v5" => Ok(()),
3817            "warnings" => {
3818                self.warnings = false;
3819                Ok(())
3820            }
3821            "English" => {
3822                self.english_enabled = false;
3823                // Don't reset no_match_vars here — if match vars were ever enabled,
3824                // they persist (Perl's export cannot be un-exported).
3825                if !self.english_match_vars_ever_enabled {
3826                    self.english_no_match_vars = false;
3827                }
3828                Ok(())
3829            }
3830            "open" => {
3831                self.open_pragma_utf8 = false;
3832                Ok(())
3833            }
3834            "bigint" | "bignum" | "bigrat" => {
3835                crate::set_bigint_pragma(false);
3836                Ok(())
3837            }
3838            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3839            _ => Ok(()),
3840        }
3841    }
3842
3843    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
3844    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3845        let names = Self::pragma_import_strings(imports, line)?;
3846        for n in names {
3847            let key = n.trim_start_matches('@');
3848            if key.eq_ignore_ascii_case("PATH") {
3849                let path_env = std::env::var("PATH").unwrap_or_default();
3850                let path_vec: Vec<StrykeValue> = std::env::split_paths(&path_env)
3851                    .map(|p| StrykeValue::string(p.to_string_lossy().into_owned()))
3852                    .collect();
3853                let aname = self.stash_array_name_for_package("PATH");
3854                self.scope.declare_array(&aname, path_vec);
3855            }
3856        }
3857        Ok(())
3858    }
3859
3860    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
3861    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3862        let items = Self::pragma_import_strings(imports, line)?;
3863        for item in items {
3864            let s = item.trim();
3865            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
3866                self.open_pragma_utf8 = true;
3867                continue;
3868            }
3869            if let Some(rest) = s.strip_prefix(":encoding(") {
3870                if let Some(inner) = rest.strip_suffix(')') {
3871                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
3872                        self.open_pragma_utf8 = true;
3873                    }
3874                }
3875            }
3876        }
3877        Ok(())
3878    }
3879
3880    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
3881    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> StrykeResult<()> {
3882        if imports.is_empty() {
3883            return Ok(());
3884        }
3885        // `use constant 1.03;` — version check only (ignored here).
3886        if imports.len() == 1 {
3887            match &imports[0].kind {
3888                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
3889                _ => {}
3890            }
3891        }
3892        for imp in imports {
3893            match &imp.kind {
3894                ExprKind::List(items) => {
3895                    if items.len() % 2 != 0 {
3896                        return Err(StrykeError::runtime(
3897                            format!(
3898                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
3899                                items.len()
3900                            ),
3901                            line,
3902                        ));
3903                    }
3904                    let mut i = 0;
3905                    while i < items.len() {
3906                        let name = match &items[i].kind {
3907                            ExprKind::String(s) => s.clone(),
3908                            _ => {
3909                                return Err(StrykeError::runtime(
3910                                    "use constant: constant name must be a string literal",
3911                                    line,
3912                                ));
3913                            }
3914                        };
3915                        let val = match self.eval_expr(&items[i + 1]) {
3916                            Ok(v) => v,
3917                            Err(FlowOrError::Error(e)) => return Err(e),
3918                            Err(FlowOrError::Flow(_)) => {
3919                                return Err(StrykeError::runtime(
3920                                    "use constant: unexpected control flow in initializer",
3921                                    line,
3922                                ));
3923                            }
3924                        };
3925                        self.install_constant_sub(&name, &val, line)?;
3926                        i += 2;
3927                    }
3928                }
3929                _ => {
3930                    return Err(StrykeError::runtime(
3931                        "use constant: expected list of NAME => VALUE pairs",
3932                        line,
3933                    ));
3934                }
3935            }
3936        }
3937        Ok(())
3938    }
3939
3940    fn install_constant_sub(
3941        &mut self,
3942        name: &str,
3943        val: &StrykeValue,
3944        line: usize,
3945    ) -> StrykeResult<()> {
3946        let key = self.qualify_sub_key(name);
3947        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
3948        let body = vec![Statement {
3949            label: None,
3950            kind: StmtKind::Return(Some(ret_expr)),
3951            line,
3952        }];
3953        self.subs.insert(
3954            key.clone(),
3955            Arc::new(StrykeSub {
3956                name: key,
3957                params: vec![],
3958                body,
3959                prototype: None,
3960                closure_env: None,
3961                fib_like: None,
3962            }),
3963        );
3964        Ok(())
3965    }
3966
3967    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
3968    fn perl_value_to_const_literal_expr(&self, v: &StrykeValue, line: usize) -> StrykeResult<Expr> {
3969        if v.is_undef() {
3970            return Ok(Expr {
3971                kind: ExprKind::Undef,
3972                line,
3973            });
3974        }
3975        if let Some(n) = v.as_integer() {
3976            return Ok(Expr {
3977                kind: ExprKind::Integer(n),
3978                line,
3979            });
3980        }
3981        if let Some(f) = v.as_float() {
3982            return Ok(Expr {
3983                kind: ExprKind::Float(f),
3984                line,
3985            });
3986        }
3987        if let Some(s) = v.as_str() {
3988            return Ok(Expr {
3989                kind: ExprKind::String(s),
3990                line,
3991            });
3992        }
3993        if let Some(arr) = v.as_array_vec() {
3994            let mut elems = Vec::with_capacity(arr.len());
3995            for e in &arr {
3996                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3997            }
3998            return Ok(Expr {
3999                kind: ExprKind::ArrayRef(elems),
4000                line,
4001            });
4002        }
4003        if let Some(h) = v.as_hash_map() {
4004            let mut pairs = Vec::with_capacity(h.len());
4005            for (k, vv) in h.iter() {
4006                pairs.push((
4007                    Expr {
4008                        kind: ExprKind::String(k.clone()),
4009                        line,
4010                    },
4011                    self.perl_value_to_const_literal_expr(vv, line)?,
4012                ));
4013            }
4014            return Ok(Expr {
4015                kind: ExprKind::HashRef(pairs),
4016                line,
4017            });
4018        }
4019        if let Some(aref) = v.as_array_ref() {
4020            let arr = aref.read();
4021            let mut elems = Vec::with_capacity(arr.len());
4022            for e in arr.iter() {
4023                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
4024            }
4025            return Ok(Expr {
4026                kind: ExprKind::ArrayRef(elems),
4027                line,
4028            });
4029        }
4030        if let Some(href) = v.as_hash_ref() {
4031            let h = href.read();
4032            let mut pairs = Vec::with_capacity(h.len());
4033            for (k, vv) in h.iter() {
4034                pairs.push((
4035                    Expr {
4036                        kind: ExprKind::String(k.clone()),
4037                        line,
4038                    },
4039                    self.perl_value_to_const_literal_expr(vv, line)?,
4040                ));
4041            }
4042            return Ok(Expr {
4043                kind: ExprKind::HashRef(pairs),
4044                line,
4045            });
4046        }
4047        Err(StrykeError::runtime(
4048            format!("use constant: unsupported value type ({v:?})"),
4049            line,
4050        ))
4051    }
4052
4053    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
4054    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> StrykeResult<()> {
4055        // Reset per-interpreter pragma flags. Each new program scan starts
4056        // clean; pragmas activate only when the program contains `use utf8;`
4057        // / `use bigint;` etc. (Globals like `BIGINT_PRAGMA` stay sticky
4058        // across runs in the same process — bigint tests that need
4059        // isolation use subprocess invocation.)
4060        self.utf8_pragma = false;
4061        for stmt in &program.statements {
4062            match &stmt.kind {
4063                StmtKind::Package { name } => {
4064                    let _ = self
4065                        .scope
4066                        .set_scalar("__PACKAGE__", StrykeValue::string(name.clone()));
4067                }
4068                StmtKind::SubDecl {
4069                    name,
4070                    params,
4071                    body,
4072                    prototype,
4073                } => {
4074                    let key = self.qualify_sub_key(name);
4075                    let mut sub = StrykeSub {
4076                        name: name.clone(),
4077                        params: params.clone(),
4078                        body: body.clone(),
4079                        closure_env: None,
4080                        prototype: prototype.clone(),
4081                        fib_like: None,
4082                    };
4083                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
4084                    self.subs.insert(key, Arc::new(sub));
4085                }
4086                StmtKind::UsePerlVersion { .. } => {}
4087                StmtKind::Use { module, imports } => {
4088                    self.exec_use_stmt(module, imports, stmt.line)?;
4089                }
4090                StmtKind::UseOverload { pairs } => {
4091                    self.install_use_overload_pairs(pairs);
4092                }
4093                StmtKind::FormatDecl { name, lines } => {
4094                    self.install_format_decl(name, lines, stmt.line)?;
4095                }
4096                StmtKind::No { module, imports } => {
4097                    self.exec_no_stmt(module, imports, stmt.line)?;
4098                }
4099                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
4100                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
4101                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
4102                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
4103                StmtKind::End(block) => self.end_blocks.push(block.clone()),
4104                _ => {}
4105            }
4106        }
4107        Ok(())
4108    }
4109
4110    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
4111    pub fn install_data_handle(&mut self, data: Vec<u8>) {
4112        self.input_handles.insert(
4113            "DATA".to_string(),
4114            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
4115        );
4116    }
4117
4118    /// Resolve `path` against [`Self::stryke_pwd`] when relative; absolute paths unchanged.
4119    #[inline]
4120    pub(crate) fn resolve_stryke_path(&self, path: &str) -> PathBuf {
4121        if path.is_empty() {
4122            return self.stryke_pwd.clone();
4123        }
4124        let p = Path::new(path);
4125        if p.is_absolute() {
4126            PathBuf::from(path)
4127        } else {
4128            self.stryke_pwd.join(path)
4129        }
4130    }
4131
4132    pub(crate) fn resolve_stryke_path_string(&self, path: &str) -> String {
4133        self.resolve_stryke_path(path)
4134            .to_string_lossy()
4135            .into_owned()
4136    }
4137
4138    /// `cd DIR` / `cd()` — set the interpreter working directory for relative path builtins
4139    /// (does not call OS `chdir`; use `chdir` for that). Returns `1` on success, `0` on failure
4140    /// and sets `$!` / errno. With no arguments, changes to `$HOME` / `%USERPROFILE%` when set.
4141    pub(crate) fn builtin_cd_execute(
4142        &mut self,
4143        args: &[StrykeValue],
4144        _line: usize,
4145    ) -> StrykeResult<StrykeValue> {
4146        let dest: PathBuf = if args.is_empty() {
4147            let home = std::env::var_os("HOME")
4148                .or_else(|| std::env::var_os("USERPROFILE"))
4149                .map(PathBuf::from);
4150            let Some(h) = home else {
4151                return Ok(StrykeValue::integer(0));
4152            };
4153            h
4154        } else {
4155            let raw = args[0].to_string();
4156            if raw.is_empty() {
4157                return Ok(StrykeValue::integer(0));
4158            }
4159            self.resolve_stryke_path(&raw)
4160        };
4161        match std::fs::metadata(&dest) {
4162            Ok(m) if m.is_dir() => {
4163                self.stryke_pwd = std::fs::canonicalize(&dest).unwrap_or(dest);
4164                Ok(StrykeValue::integer(1))
4165            }
4166            Ok(_) => Ok(StrykeValue::integer(0)),
4167            Err(e) => {
4168                self.apply_io_error_to_errno(&e);
4169                Ok(StrykeValue::integer(0))
4170            }
4171        }
4172    }
4173
4174    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
4175    ///
4176    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
4177    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
4178    /// [`piped_shell_command`]).
4179    pub(crate) fn open_builtin_execute(
4180        &mut self,
4181        handle_name: String,
4182        mode_s: String,
4183        file_opt: Option<String>,
4184        line: usize,
4185    ) -> StrykeResult<StrykeValue> {
4186        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
4187        // - leading `|`  → pipe to command (write to child's stdin)
4188        // - trailing `|` → pipe from command (read child's stdout)
4189        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
4190        let (actual_mode, path) = if let Some(f) = file_opt {
4191            (mode_s, f)
4192        } else {
4193            let trimmed = mode_s.trim();
4194            if let Some(rest) = trimmed.strip_prefix('|') {
4195                ("|-".to_string(), rest.trim_start().to_string())
4196            } else if trimmed.ends_with('|') {
4197                let mut cmd = trimmed.to_string();
4198                cmd.pop(); // trailing `|` that selects pipe-from-command
4199                ("-|".to_string(), cmd.trim_end().to_string())
4200            } else if let Some(rest) = trimmed.strip_prefix(">>") {
4201                (">>".to_string(), rest.trim().to_string())
4202            } else if let Some(rest) = trimmed.strip_prefix('>') {
4203                (">".to_string(), rest.trim().to_string())
4204            } else if let Some(rest) = trimmed.strip_prefix('<') {
4205                ("<".to_string(), rest.trim().to_string())
4206            } else {
4207                ("<".to_string(), trimmed.to_string())
4208            }
4209        };
4210        let handle_return = handle_name.clone();
4211        let file_path = match actual_mode.as_str() {
4212            "<" | ">" | ">>" => self.resolve_stryke_path_string(&path),
4213            _ => path.clone(),
4214        };
4215        match actual_mode.as_str() {
4216            "-|" => {
4217                let mut cmd = piped_shell_command(&path);
4218                cmd.stdout(Stdio::piped());
4219                let mut child = cmd.spawn().map_err(|e| {
4220                    self.apply_io_error_to_errno(&e);
4221                    StrykeError::runtime(format!("Can't open pipe from command: {}", e), line)
4222                })?;
4223                let stdout = child
4224                    .stdout
4225                    .take()
4226                    .ok_or_else(|| StrykeError::runtime("pipe: child has no stdout", line))?;
4227                self.input_handles
4228                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
4229                self.pipe_children.insert(handle_name, child);
4230            }
4231            "|-" => {
4232                let mut cmd = piped_shell_command(&path);
4233                cmd.stdin(Stdio::piped());
4234                let mut child = cmd.spawn().map_err(|e| {
4235                    self.apply_io_error_to_errno(&e);
4236                    StrykeError::runtime(format!("Can't open pipe to command: {}", e), line)
4237                })?;
4238                let stdin = child
4239                    .stdin
4240                    .take()
4241                    .ok_or_else(|| StrykeError::runtime("pipe: child has no stdin", line))?;
4242                self.output_handles
4243                    .insert(handle_name.clone(), Box::new(stdin));
4244                self.pipe_children.insert(handle_name, child);
4245            }
4246            "<" => {
4247                let file = match std::fs::File::open(&file_path) {
4248                    Ok(f) => f,
4249                    Err(e) => {
4250                        self.apply_io_error_to_errno(&e);
4251                        return Ok(StrykeValue::integer(0));
4252                    }
4253                };
4254                let shared = Arc::new(Mutex::new(file));
4255                self.io_file_slots
4256                    .insert(handle_name.clone(), Arc::clone(&shared));
4257                self.input_handles.insert(
4258                    handle_name.clone(),
4259                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
4260                );
4261            }
4262            ">" => {
4263                let file = match std::fs::File::create(&file_path) {
4264                    Ok(f) => f,
4265                    Err(e) => {
4266                        self.apply_io_error_to_errno(&e);
4267                        return Ok(StrykeValue::integer(0));
4268                    }
4269                };
4270                let shared = Arc::new(Mutex::new(file));
4271                self.io_file_slots
4272                    .insert(handle_name.clone(), Arc::clone(&shared));
4273                self.output_handles.insert(
4274                    handle_name.clone(),
4275                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4276                );
4277            }
4278            ">>" => {
4279                let file = match std::fs::OpenOptions::new()
4280                    .append(true)
4281                    .create(true)
4282                    .open(&file_path)
4283                {
4284                    Ok(f) => f,
4285                    Err(e) => {
4286                        self.apply_io_error_to_errno(&e);
4287                        return Ok(StrykeValue::integer(0));
4288                    }
4289                };
4290                let shared = Arc::new(Mutex::new(file));
4291                self.io_file_slots
4292                    .insert(handle_name.clone(), Arc::clone(&shared));
4293                self.output_handles.insert(
4294                    handle_name.clone(),
4295                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4296                );
4297            }
4298            _ => {
4299                return Err(StrykeError::runtime(
4300                    format!("Unknown open mode '{}'", actual_mode),
4301                    line,
4302                ));
4303            }
4304        }
4305        Ok(StrykeValue::io_handle(handle_return))
4306    }
4307
4308    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
4309    /// matches the previous key under [`StrykeValue::str_eq`]. Returns a list of arrayrefs
4310    /// (same outer shape as `chunked`).
4311    pub(crate) fn eval_chunk_by_builtin(
4312        &mut self,
4313        key_spec: &Expr,
4314        list_expr: &Expr,
4315        ctx: WantarrayCtx,
4316        line: usize,
4317    ) -> ExecResult {
4318        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
4319        let chunks = match &key_spec.kind {
4320            ExprKind::CodeRef { .. } => {
4321                let cr = self.eval_expr(key_spec)?;
4322                let Some(sub) = cr.as_code_ref() else {
4323                    return Err(StrykeError::runtime(
4324                        "group_by/chunk_by: first argument must be { BLOCK }",
4325                        line,
4326                    )
4327                    .into());
4328                };
4329                let sub = sub.clone();
4330                let mut chunks: Vec<StrykeValue> = Vec::new();
4331                let mut run: Vec<StrykeValue> = Vec::new();
4332                let mut prev_key: Option<StrykeValue> = None;
4333                for item in list {
4334                    self.scope.set_topic(item.clone());
4335                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
4336                        Ok(k) => k,
4337                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
4338                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
4339                        Err(_) => StrykeValue::UNDEF,
4340                    };
4341                    match &prev_key {
4342                        None => {
4343                            run.push(item);
4344                            prev_key = Some(key);
4345                        }
4346                        Some(pk) => {
4347                            if key.str_eq(pk) {
4348                                run.push(item);
4349                            } else {
4350                                chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
4351                                    std::mem::take(&mut run),
4352                                ))));
4353                                run.push(item);
4354                                prev_key = Some(key);
4355                            }
4356                        }
4357                    }
4358                }
4359                if !run.is_empty() {
4360                    chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
4361                }
4362                chunks
4363            }
4364            _ => {
4365                let mut chunks: Vec<StrykeValue> = Vec::new();
4366                let mut run: Vec<StrykeValue> = Vec::new();
4367                let mut prev_key: Option<StrykeValue> = None;
4368                for item in list {
4369                    self.scope.set_topic(item.clone());
4370                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
4371                    match &prev_key {
4372                        None => {
4373                            run.push(item);
4374                            prev_key = Some(key);
4375                        }
4376                        Some(pk) => {
4377                            if key.str_eq(pk) {
4378                                run.push(item);
4379                            } else {
4380                                chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(
4381                                    std::mem::take(&mut run),
4382                                ))));
4383                                run.push(item);
4384                                prev_key = Some(key);
4385                            }
4386                        }
4387                    }
4388                }
4389                if !run.is_empty() {
4390                    chunks.push(StrykeValue::array_ref(Arc::new(RwLock::new(run))));
4391                }
4392                chunks
4393            }
4394        };
4395        Ok(match ctx {
4396            WantarrayCtx::List => StrykeValue::array(chunks),
4397            WantarrayCtx::Scalar => StrykeValue::integer(chunks.len() as i64),
4398            WantarrayCtx::Void => StrykeValue::UNDEF,
4399        })
4400    }
4401
4402    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
4403    pub(crate) fn list_higher_order_block_builtin(
4404        &mut self,
4405        name: &str,
4406        args: &[StrykeValue],
4407        line: usize,
4408    ) -> StrykeResult<StrykeValue> {
4409        match self.list_higher_order_block_builtin_exec(name, args, line) {
4410            Ok(v) => Ok(v),
4411            Err(FlowOrError::Error(e)) => Err(e),
4412            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
4413            Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
4414                format!("{name}: unsupported control flow in block"),
4415                line,
4416            )),
4417        }
4418    }
4419
4420    fn list_higher_order_block_builtin_exec(
4421        &mut self,
4422        name: &str,
4423        args: &[StrykeValue],
4424        line: usize,
4425    ) -> ExecResult {
4426        if args.is_empty() {
4427            return Err(
4428                StrykeError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
4429            );
4430        }
4431        let Some(sub) = args[0].as_code_ref() else {
4432            return Err(StrykeError::runtime(
4433                format!("{name}: first argument must be {{ BLOCK }}"),
4434                line,
4435            )
4436            .into());
4437        };
4438        let sub = sub.clone();
4439        let items: Vec<StrykeValue> = args[1..].to_vec();
4440        if matches!(name, "tap" | "peek") && items.len() == 1 {
4441            if let Some(p) = items[0].as_pipeline() {
4442                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
4443                return Ok(StrykeValue::pipeline(Arc::clone(&p)));
4444            }
4445            let v = &items[0];
4446            if v.is_iterator() || v.as_array_vec().is_some() {
4447                let source = crate::map_stream::into_pull_iter(v.clone());
4448                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
4449                return Ok(StrykeValue::iterator(Arc::new(
4450                    crate::map_stream::TapIterator::new(
4451                        source,
4452                        sub,
4453                        self.subs.clone(),
4454                        capture,
4455                        atomic_arrays,
4456                        atomic_hashes,
4457                    ),
4458                )));
4459            }
4460        }
4461        // Streaming optimization disabled for these functions because the pre-captured
4462        // coderef from args[0] has its closure_env populated at parse time, which causes
4463        // $_ to get stale values on subsequent calls. These functions work correctly in
4464        // the non-streaming eager path below.
4465        let wa = self.wantarray_kind;
4466        match name {
4467            "take_while" => {
4468                let mut out = Vec::new();
4469                for item in items {
4470                    // `call_sub` binds the item to @_/positional params (so
4471                    // stryke lambdas work) and to `$_` via `set_closure_args`
4472                    // (so `_`-using blocks work). Replaces the old
4473                    // `exec_block(&sub.body)` path which only set the topic.
4474                    self.scope.set_topic(item.clone());
4475                    let pred =
4476                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4477                    if !pred.is_true() {
4478                        break;
4479                    }
4480                    out.push(item);
4481                }
4482                Ok(match wa {
4483                    WantarrayCtx::List => StrykeValue::array(out),
4484                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4485                    WantarrayCtx::Void => StrykeValue::UNDEF,
4486                })
4487            }
4488            "drop_while" | "skip_while" => {
4489                let mut i = 0usize;
4490                while i < items.len() {
4491                    let it = items[i].clone();
4492                    self.scope.set_topic(it.clone());
4493                    let pred = self.call_sub(&sub, vec![it], WantarrayCtx::Scalar, line)?;
4494                    if !pred.is_true() {
4495                        break;
4496                    }
4497                    i += 1;
4498                }
4499                let rest = items[i..].to_vec();
4500                Ok(match wa {
4501                    WantarrayCtx::List => StrykeValue::array(rest),
4502                    WantarrayCtx::Scalar => StrykeValue::integer(rest.len() as i64),
4503                    WantarrayCtx::Void => StrykeValue::UNDEF,
4504                })
4505            }
4506            "reject" | "grepv" => {
4507                let mut out = Vec::new();
4508                for item in items {
4509                    self.scope.set_topic(item.clone());
4510                    let pred =
4511                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4512                    if !pred.is_true() {
4513                        out.push(item);
4514                    }
4515                }
4516                Ok(match wa {
4517                    WantarrayCtx::List => StrykeValue::array(out),
4518                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4519                    WantarrayCtx::Void => StrykeValue::UNDEF,
4520                })
4521            }
4522            "tap" | "peek" => {
4523                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
4524                Ok(match wa {
4525                    WantarrayCtx::List => StrykeValue::array(items),
4526                    WantarrayCtx::Scalar => StrykeValue::integer(items.len() as i64),
4527                    WantarrayCtx::Void => StrykeValue::UNDEF,
4528                })
4529            }
4530            "partition" => {
4531                let mut yes = Vec::new();
4532                let mut no = Vec::new();
4533                for item in items {
4534                    self.scope.set_topic(item.clone());
4535                    let pred =
4536                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4537                    if pred.is_true() {
4538                        yes.push(item);
4539                    } else {
4540                        no.push(item);
4541                    }
4542                }
4543                let yes_ref = StrykeValue::array_ref(Arc::new(RwLock::new(yes)));
4544                let no_ref = StrykeValue::array_ref(Arc::new(RwLock::new(no)));
4545                Ok(match wa {
4546                    WantarrayCtx::List => StrykeValue::array(vec![yes_ref, no_ref]),
4547                    WantarrayCtx::Scalar => StrykeValue::integer(2),
4548                    WantarrayCtx::Void => StrykeValue::UNDEF,
4549                })
4550            }
4551            "min_by" => {
4552                let mut best: Option<(StrykeValue, StrykeValue)> = None;
4553                for item in items {
4554                    self.scope.set_topic(item.clone());
4555                    let key =
4556                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4557                    best = Some(match best {
4558                        None => (item, key),
4559                        Some((bv, bk)) => {
4560                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
4561                                (item, key)
4562                            } else {
4563                                (bv, bk)
4564                            }
4565                        }
4566                    });
4567                }
4568                Ok(best.map(|(v, _)| v).unwrap_or(StrykeValue::UNDEF))
4569            }
4570            "max_by" => {
4571                let mut best: Option<(StrykeValue, StrykeValue)> = None;
4572                for item in items {
4573                    self.scope.set_topic(item.clone());
4574                    let key =
4575                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4576                    best = Some(match best {
4577                        None => (item, key),
4578                        Some((bv, bk)) => {
4579                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
4580                                (item, key)
4581                            } else {
4582                                (bv, bk)
4583                            }
4584                        }
4585                    });
4586                }
4587                Ok(best.map(|(v, _)| v).unwrap_or(StrykeValue::UNDEF))
4588            }
4589            "zip_with" => {
4590                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
4591                // Flatten items, then treat each array ref/binding as a separate list.
4592                let flat: Vec<StrykeValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
4593                let refs: Vec<Vec<StrykeValue>> = flat
4594                    .iter()
4595                    .map(|el| {
4596                        if let Some(ar) = el.as_array_ref() {
4597                            ar.read().clone()
4598                        } else if let Some(name) = el.as_array_binding_name() {
4599                            self.scope.get_array(&name)
4600                        } else {
4601                            vec![el.clone()]
4602                        }
4603                    })
4604                    .collect();
4605                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
4606                let mut out = Vec::with_capacity(max_len);
4607                for i in 0..max_len {
4608                    let pair: Vec<StrykeValue> = refs
4609                        .iter()
4610                        .map(|l| l.get(i).cloned().unwrap_or(StrykeValue::UNDEF))
4611                        .collect();
4612                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
4613                    out.push(result);
4614                }
4615                Ok(match wa {
4616                    WantarrayCtx::List => StrykeValue::array(out),
4617                    WantarrayCtx::Scalar => StrykeValue::integer(out.len() as i64),
4618                    WantarrayCtx::Void => StrykeValue::UNDEF,
4619                })
4620            }
4621            "count_by" => {
4622                let mut counts = indexmap::IndexMap::new();
4623                for item in items {
4624                    self.scope.set_topic(item.clone());
4625                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4626                    let k = key.to_string();
4627                    let entry = counts.entry(k).or_insert(StrykeValue::integer(0));
4628                    *entry = StrykeValue::integer(entry.to_int() + 1);
4629                }
4630                Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(counts))))
4631            }
4632            _ => Err(StrykeError::runtime(
4633                format!("internal: unknown list block builtin `{name}`"),
4634                line,
4635            )
4636            .into()),
4637        }
4638    }
4639
4640    /// `rmdir LIST` — remove empty directories; returns count removed.
4641    pub(crate) fn builtin_rmdir_execute(
4642        &mut self,
4643        args: &[StrykeValue],
4644        _line: usize,
4645    ) -> StrykeResult<StrykeValue> {
4646        let mut count = 0i64;
4647        for a in args {
4648            let p = a.to_string();
4649            if p.is_empty() {
4650                continue;
4651            }
4652            let p = self.resolve_stryke_path_string(&p);
4653            if std::fs::remove_dir(&p).is_ok() {
4654                count += 1;
4655            }
4656        }
4657        Ok(StrykeValue::integer(count))
4658    }
4659
4660    /// `touch FILE, ...` — create if absent, update timestamps to now.
4661    pub(crate) fn builtin_touch_execute(
4662        &mut self,
4663        args: &[StrykeValue],
4664        _line: usize,
4665    ) -> StrykeResult<StrykeValue> {
4666        let paths: Vec<String> = args
4667            .iter()
4668            .map(|v| self.resolve_stryke_path_string(&v.to_string()))
4669            .collect();
4670        Ok(StrykeValue::integer(crate::perl_fs::touch_paths(&paths)))
4671    }
4672
4673    /// `utime ATIME, MTIME, LIST`
4674    pub(crate) fn builtin_utime_execute(
4675        &mut self,
4676        args: &[StrykeValue],
4677        line: usize,
4678    ) -> StrykeResult<StrykeValue> {
4679        if args.len() < 3 {
4680            return Err(StrykeError::runtime(
4681                "utime requires at least three arguments (atime, mtime, files...)",
4682                line,
4683            ));
4684        }
4685        let at = args[0].to_int();
4686        let mt = args[1].to_int();
4687        let paths: Vec<String> = args
4688            .iter()
4689            .skip(2)
4690            .map(|v| self.resolve_stryke_path_string(&v.to_string()))
4691            .collect();
4692        let n = crate::perl_fs::utime_paths(at, mt, &paths);
4693        #[cfg(not(unix))]
4694        if !paths.is_empty() && n == 0 {
4695            return Err(StrykeError::runtime(
4696                "utime is not supported on this platform",
4697                line,
4698            ));
4699        }
4700        Ok(StrykeValue::integer(n))
4701    }
4702
4703    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
4704    pub(crate) fn builtin_umask_execute(
4705        &mut self,
4706        args: &[StrykeValue],
4707        line: usize,
4708    ) -> StrykeResult<StrykeValue> {
4709        #[cfg(unix)]
4710        {
4711            let _ = line;
4712            if args.is_empty() {
4713                let cur = unsafe { libc::umask(0) };
4714                unsafe { libc::umask(cur) };
4715                return Ok(StrykeValue::integer(cur as i64));
4716            }
4717            let new_m = args[0].to_int() as libc::mode_t;
4718            let old = unsafe { libc::umask(new_m) };
4719            Ok(StrykeValue::integer(old as i64))
4720        }
4721        #[cfg(not(unix))]
4722        {
4723            let _ = args;
4724            Err(StrykeError::runtime(
4725                "umask is not supported on this platform",
4726                line,
4727            ))
4728        }
4729    }
4730
4731    /// `getcwd` — current directory or undef on failure.
4732    pub(crate) fn builtin_getcwd_execute(
4733        &mut self,
4734        args: &[StrykeValue],
4735        line: usize,
4736    ) -> StrykeResult<StrykeValue> {
4737        if !args.is_empty() {
4738            return Err(StrykeError::runtime("getcwd takes no arguments", line));
4739        }
4740        match std::env::current_dir() {
4741            Ok(p) => Ok(StrykeValue::string(p.to_string_lossy().into_owned())),
4742            Err(e) => {
4743                self.apply_io_error_to_errno(&e);
4744                Ok(StrykeValue::UNDEF)
4745            }
4746        }
4747    }
4748
4749    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
4750    pub(crate) fn builtin_realpath_execute(
4751        &mut self,
4752        args: &[StrykeValue],
4753        line: usize,
4754    ) -> StrykeResult<StrykeValue> {
4755        let path = args
4756            .first()
4757            .ok_or_else(|| StrykeError::runtime("realpath: need path", line))?
4758            .to_string();
4759        if path.is_empty() {
4760            return Err(StrykeError::runtime("realpath: need path", line));
4761        }
4762        let path = self.resolve_stryke_path_string(&path);
4763        match crate::perl_fs::realpath_resolved(&path) {
4764            Ok(s) => Ok(StrykeValue::string(s)),
4765            Err(e) => {
4766                self.apply_io_error_to_errno(&e);
4767                Ok(StrykeValue::UNDEF)
4768            }
4769        }
4770    }
4771
4772    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
4773    pub(crate) fn builtin_pipe_execute(
4774        &mut self,
4775        args: &[StrykeValue],
4776        line: usize,
4777    ) -> StrykeResult<StrykeValue> {
4778        if args.len() != 2 {
4779            return Err(StrykeError::runtime(
4780                "pipe requires exactly two arguments",
4781                line,
4782            ));
4783        }
4784        #[cfg(unix)]
4785        {
4786            use std::fs::File;
4787            use std::os::unix::io::FromRawFd;
4788
4789            let read_name = args[0].to_string();
4790            let write_name = args[1].to_string();
4791            if read_name.is_empty() || write_name.is_empty() {
4792                return Err(StrykeError::runtime("pipe: invalid handle name", line));
4793            }
4794            let mut fds = [0i32; 2];
4795            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
4796                let e = std::io::Error::last_os_error();
4797                self.apply_io_error_to_errno(&e);
4798                return Ok(StrykeValue::integer(0));
4799            }
4800            let read_file = unsafe { File::from_raw_fd(fds[0]) };
4801            let write_file = unsafe { File::from_raw_fd(fds[1]) };
4802
4803            let read_shared = Arc::new(Mutex::new(read_file));
4804            let write_shared = Arc::new(Mutex::new(write_file));
4805
4806            self.close_builtin_execute(read_name.clone()).ok();
4807            self.close_builtin_execute(write_name.clone()).ok();
4808
4809            self.io_file_slots
4810                .insert(read_name.clone(), Arc::clone(&read_shared));
4811            self.input_handles.insert(
4812                read_name,
4813                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
4814            );
4815
4816            self.io_file_slots
4817                .insert(write_name.clone(), Arc::clone(&write_shared));
4818            self.output_handles
4819                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
4820
4821            Ok(StrykeValue::integer(1))
4822        }
4823        #[cfg(not(unix))]
4824        {
4825            let _ = args;
4826            Err(StrykeError::runtime(
4827                "pipe is not supported on this platform",
4828                line,
4829            ))
4830        }
4831    }
4832
4833    pub(crate) fn close_builtin_execute(&mut self, name: String) -> StrykeResult<StrykeValue> {
4834        self.output_handles.remove(&name);
4835        self.input_handles.remove(&name);
4836        self.io_file_slots.remove(&name);
4837        if let Some(mut child) = self.pipe_children.remove(&name) {
4838            if let Ok(st) = child.wait() {
4839                self.record_child_exit_status(st);
4840            }
4841        }
4842        Ok(StrykeValue::integer(1))
4843    }
4844
4845    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
4846        self.input_handles.contains_key(name)
4847    }
4848
4849    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
4850    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
4851    /// readline-level EOF tracking exists.
4852    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
4853        self.line_mode_eof_pending
4854    }
4855
4856    /// `eof` / `eof()` / `eof FH` — shared by [`crate::vm::VM`] and
4857    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
4858    /// not [`ExprKind::Eof`]).
4859    pub(crate) fn eof_builtin_execute(
4860        &self,
4861        args: &[StrykeValue],
4862        line: usize,
4863    ) -> StrykeResult<StrykeValue> {
4864        match args.len() {
4865            0 => Ok(StrykeValue::integer(if self.eof_without_arg_is_true() {
4866                1
4867            } else {
4868                0
4869            })),
4870            1 => {
4871                let name = args[0].to_string();
4872                let at_eof = !self.has_input_handle(&name);
4873                Ok(StrykeValue::integer(if at_eof { 1 } else { 0 }))
4874            }
4875            _ => Err(StrykeError::runtime("eof: too many arguments", line)),
4876        }
4877    }
4878
4879    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
4880    /// `0`, stringifies to `""`) for `""`.
4881    pub(crate) fn study_return_value(s: &str) -> StrykeValue {
4882        if s.is_empty() {
4883            StrykeValue::string(String::new())
4884        } else {
4885            StrykeValue::integer(1)
4886        }
4887    }
4888
4889    pub(crate) fn readline_builtin_execute(
4890        &mut self,
4891        handle: Option<&str>,
4892    ) -> StrykeResult<StrykeValue> {
4893        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
4894        if handle.is_none() {
4895            let argv = self.scope.get_array("ARGV");
4896            if !argv.is_empty() {
4897                loop {
4898                    if self.diamond_reader.is_none() {
4899                        while self.diamond_next_idx < argv.len() {
4900                            let path = self.resolve_stryke_path_string(
4901                                &argv[self.diamond_next_idx].to_string(),
4902                            );
4903                            self.diamond_next_idx += 1;
4904                            match File::open(&path) {
4905                                Ok(f) => {
4906                                    self.argv_current_file = path;
4907                                    self.diamond_reader = Some(BufReader::new(f));
4908                                    break;
4909                                }
4910                                Err(e) => {
4911                                    self.apply_io_error_to_errno(&e);
4912                                }
4913                            }
4914                        }
4915                        if self.diamond_reader.is_none() {
4916                            return Ok(StrykeValue::UNDEF);
4917                        }
4918                    }
4919                    let mut line_str = String::new();
4920                    let read_result: Result<usize, io::Error> =
4921                        if let Some(reader) = self.diamond_reader.as_mut() {
4922                            if self.open_pragma_utf8 {
4923                                let mut buf = Vec::new();
4924                                reader.read_until(b'\n', &mut buf).inspect(|n| {
4925                                    if *n > 0 {
4926                                        line_str = String::from_utf8_lossy(&buf).into_owned();
4927                                    }
4928                                })
4929                            } else {
4930                                let mut buf = Vec::new();
4931                                match reader.read_until(b'\n', &mut buf) {
4932                                    Ok(n) => {
4933                                        if n > 0 {
4934                                            line_str =
4935                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
4936                                                &buf,
4937                                            );
4938                                        }
4939                                        Ok(n)
4940                                    }
4941                                    Err(e) => Err(e),
4942                                }
4943                            }
4944                        } else {
4945                            unreachable!()
4946                        };
4947                    match read_result {
4948                        Ok(0) => {
4949                            self.diamond_reader = None;
4950                            continue;
4951                        }
4952                        Ok(_) => {
4953                            self.bump_line_for_handle(&self.argv_current_file.clone());
4954                            return Ok(StrykeValue::string(line_str));
4955                        }
4956                        Err(e) => {
4957                            self.apply_io_error_to_errno(&e);
4958                            self.diamond_reader = None;
4959                            continue;
4960                        }
4961                    }
4962                }
4963            } else {
4964                self.argv_current_file.clear();
4965            }
4966        }
4967
4968        let handle_name = handle.unwrap_or("STDIN");
4969        let mut line_str = String::new();
4970        if handle_name == "STDIN" {
4971            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
4972                self.last_stdin_die_bracket = if handle.is_none() {
4973                    "<>".to_string()
4974                } else {
4975                    "<STDIN>".to_string()
4976                };
4977                self.bump_line_for_handle("STDIN");
4978                return Ok(StrykeValue::string(queued));
4979            }
4980            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4981                let mut buf = Vec::new();
4982                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
4983                    if *n > 0 {
4984                        line_str = String::from_utf8_lossy(&buf).into_owned();
4985                    }
4986                })
4987            } else {
4988                let mut buf = Vec::new();
4989                let mut lock = io::stdin().lock();
4990                match lock.read_until(b'\n', &mut buf) {
4991                    Ok(n) => {
4992                        if n > 0 {
4993                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4994                        }
4995                        Ok(n)
4996                    }
4997                    Err(e) => Err(e),
4998                }
4999            };
5000            match r {
5001                Ok(0) => Ok(StrykeValue::UNDEF),
5002                Ok(_) => {
5003                    self.last_stdin_die_bracket = if handle.is_none() {
5004                        "<>".to_string()
5005                    } else {
5006                        "<STDIN>".to_string()
5007                    };
5008                    self.bump_line_for_handle("STDIN");
5009                    Ok(StrykeValue::string(line_str))
5010                }
5011                Err(e) => {
5012                    self.apply_io_error_to_errno(&e);
5013                    Ok(StrykeValue::UNDEF)
5014                }
5015            }
5016        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
5017            // Check $/ for slurp mode (None/undef = read entire file)
5018            let slurp_mode = self.irs.is_none();
5019            let r: Result<usize, io::Error> = if slurp_mode {
5020                // Slurp mode: read entire remaining content
5021                let mut buf = Vec::new();
5022                match reader.read_to_end(&mut buf) {
5023                    Ok(n) => {
5024                        if n > 0 {
5025                            line_str = if self.open_pragma_utf8 {
5026                                String::from_utf8_lossy(&buf).into_owned()
5027                            } else {
5028                                crate::perl_decode::decode_utf8_or_latin1_read_until(&buf)
5029                            };
5030                        }
5031                        Ok(n)
5032                    }
5033                    Err(e) => Err(e),
5034                }
5035            } else if self.open_pragma_utf8 {
5036                let mut buf = Vec::new();
5037                reader.read_until(b'\n', &mut buf).inspect(|n| {
5038                    if *n > 0 {
5039                        line_str = String::from_utf8_lossy(&buf).into_owned();
5040                    }
5041                })
5042            } else {
5043                let mut buf = Vec::new();
5044                match reader.read_until(b'\n', &mut buf) {
5045                    Ok(n) => {
5046                        if n > 0 {
5047                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
5048                        }
5049                        Ok(n)
5050                    }
5051                    Err(e) => Err(e),
5052                }
5053            };
5054            match r {
5055                Ok(0) => Ok(StrykeValue::UNDEF),
5056                Ok(_) => {
5057                    self.bump_line_for_handle(handle_name);
5058                    Ok(StrykeValue::string(line_str))
5059                }
5060                Err(e) => {
5061                    self.apply_io_error_to_errno(&e);
5062                    Ok(StrykeValue::UNDEF)
5063                }
5064            }
5065        } else {
5066            Ok(StrykeValue::UNDEF)
5067        }
5068    }
5069
5070    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
5071    pub(crate) fn readline_builtin_execute_list(
5072        &mut self,
5073        handle: Option<&str>,
5074    ) -> StrykeResult<StrykeValue> {
5075        let mut lines = Vec::new();
5076        loop {
5077            let v = self.readline_builtin_execute(handle)?;
5078            if v.is_undef() {
5079                break;
5080            }
5081            lines.push(v);
5082        }
5083        Ok(StrykeValue::array(lines))
5084    }
5085
5086    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> StrykeValue {
5087        let path = self.resolve_stryke_path_string(path);
5088        match std::fs::read_dir(&path) {
5089            Ok(rd) => {
5090                let entries: Vec<String> = rd
5091                    .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
5092                    .collect();
5093                self.dir_handles
5094                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
5095                StrykeValue::integer(1)
5096            }
5097            Err(e) => {
5098                self.apply_io_error_to_errno(&e);
5099                StrykeValue::integer(0)
5100            }
5101        }
5102    }
5103
5104    pub(crate) fn readdir_handle(&mut self, handle: &str) -> StrykeValue {
5105        if let Some(dh) = self.dir_handles.get_mut(handle) {
5106            if dh.pos < dh.entries.len() {
5107                let s = dh.entries[dh.pos].clone();
5108                dh.pos += 1;
5109                StrykeValue::string(s)
5110            } else {
5111                StrykeValue::UNDEF
5112            }
5113        } else {
5114            StrykeValue::UNDEF
5115        }
5116    }
5117
5118    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
5119    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> StrykeValue {
5120        if let Some(dh) = self.dir_handles.get_mut(handle) {
5121            let rest: Vec<StrykeValue> = dh.entries[dh.pos..]
5122                .iter()
5123                .cloned()
5124                .map(StrykeValue::string)
5125                .collect();
5126            dh.pos = dh.entries.len();
5127            StrykeValue::array(rest)
5128        } else {
5129            StrykeValue::array(Vec::new())
5130        }
5131    }
5132
5133    pub(crate) fn closedir_handle(&mut self, handle: &str) -> StrykeValue {
5134        StrykeValue::integer(if self.dir_handles.remove(handle).is_some() {
5135            1
5136        } else {
5137            0
5138        })
5139    }
5140
5141    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> StrykeValue {
5142        if let Some(dh) = self.dir_handles.get_mut(handle) {
5143            dh.pos = 0;
5144            StrykeValue::integer(1)
5145        } else {
5146            StrykeValue::integer(0)
5147        }
5148    }
5149
5150    pub(crate) fn telldir_handle(&mut self, handle: &str) -> StrykeValue {
5151        self.dir_handles
5152            .get(handle)
5153            .map(|dh| StrykeValue::integer(dh.pos as i64))
5154            .unwrap_or(StrykeValue::UNDEF)
5155    }
5156
5157    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> StrykeValue {
5158        if let Some(dh) = self.dir_handles.get_mut(handle) {
5159            dh.pos = pos.min(dh.entries.len());
5160            StrykeValue::integer(1)
5161        } else {
5162            StrykeValue::integer(0)
5163        }
5164    }
5165
5166    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
5167    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
5168    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
5169    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
5170    #[inline]
5171    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
5172        crate::special_vars::is_regex_match_scalar_name(name)
5173    }
5174
5175    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
5176    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
5177    /// `apply_regex_captures` instead of short-circuiting.
5178    #[inline]
5179    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
5180        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
5181            self.regex_capture_scope_fresh = false;
5182        }
5183    }
5184
5185    pub(crate) fn apply_regex_captures(
5186        &mut self,
5187        haystack: &str,
5188        offset: usize,
5189        re: &PerlCompiledRegex,
5190        caps: &PerlCaptures<'_>,
5191        capture_all: CaptureAllMode,
5192    ) -> Result<(), FlowOrError> {
5193        let m0 = caps.get(0).expect("regex capture 0");
5194        let s0 = offset + m0.start;
5195        let e0 = offset + m0.end;
5196        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
5197        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
5198        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
5199        let mut last_paren = String::new();
5200        for i in 1..caps.len() {
5201            if let Some(m) = caps.get(i) {
5202                last_paren = m.text.to_string();
5203            }
5204        }
5205        self.last_paren_match = last_paren;
5206        self.last_subpattern_name = String::new();
5207        for n in re.capture_names().flatten() {
5208            if caps.name(n).is_some() {
5209                self.last_subpattern_name = n.to_string();
5210            }
5211        }
5212        self.scope
5213            .set_scalar("&", StrykeValue::string(self.last_match.clone()))?;
5214        self.scope
5215            .set_scalar("`", StrykeValue::string(self.prematch.clone()))?;
5216        self.scope
5217            .set_scalar("'", StrykeValue::string(self.postmatch.clone()))?;
5218        self.scope
5219            .set_scalar("+", StrykeValue::string(self.last_paren_match.clone()))?;
5220        for i in 1..caps.len() {
5221            if let Some(m) = caps.get(i) {
5222                self.scope
5223                    .set_scalar(&i.to_string(), StrykeValue::string(m.text.to_string()))?;
5224            }
5225        }
5226        let mut start_arr = vec![StrykeValue::integer(s0 as i64)];
5227        let mut end_arr = vec![StrykeValue::integer(e0 as i64)];
5228        for i in 1..caps.len() {
5229            if let Some(m) = caps.get(i) {
5230                start_arr.push(StrykeValue::integer((offset + m.start) as i64));
5231                end_arr.push(StrykeValue::integer((offset + m.end) as i64));
5232            } else {
5233                start_arr.push(StrykeValue::integer(-1));
5234                end_arr.push(StrykeValue::integer(-1));
5235            }
5236        }
5237        self.scope.set_array("-", start_arr)?;
5238        self.scope.set_array("+", end_arr)?;
5239        let mut named = IndexMap::new();
5240        for name in re.capture_names().flatten() {
5241            if let Some(m) = caps.name(name) {
5242                named.insert(name.to_string(), StrykeValue::string(m.text.to_string()));
5243            }
5244        }
5245        self.scope.set_hash("+", named.clone())?;
5246        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
5247        let mut named_minus = IndexMap::new();
5248        for (name, val) in &named {
5249            named_minus.insert(
5250                name.clone(),
5251                StrykeValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
5252            );
5253        }
5254        self.scope.set_hash("-", named_minus)?;
5255        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
5256        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
5257        match capture_all {
5258            CaptureAllMode::Empty => {
5259                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5260            }
5261            CaptureAllMode::Append => {
5262                let mut rows = self.scope.get_array("^CAPTURE_ALL");
5263                rows.push(StrykeValue::array(cap_flat));
5264                self.scope.set_array("^CAPTURE_ALL", rows)?;
5265            }
5266            CaptureAllMode::Skip => {}
5267        }
5268        Ok(())
5269    }
5270
5271    pub(crate) fn clear_flip_flop_state(&mut self) {
5272        self.flip_flop_active.clear();
5273        self.flip_flop_exclusive_left_line.clear();
5274        self.flip_flop_sequence.clear();
5275        self.flip_flop_last_dot.clear();
5276        self.flip_flop_tree.clear();
5277    }
5278
5279    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
5280        self.flip_flop_active.resize(slots as usize, false);
5281        self.flip_flop_active.fill(false);
5282        self.flip_flop_exclusive_left_line
5283            .resize(slots as usize, None);
5284        self.flip_flop_exclusive_left_line.fill(None);
5285        self.flip_flop_sequence.resize(slots as usize, 0);
5286        self.flip_flop_sequence.fill(0);
5287        self.flip_flop_last_dot.resize(slots as usize, None);
5288        self.flip_flop_last_dot.fill(None);
5289    }
5290
5291    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
5292    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
5293    /// [`Self::handle_line_numbers`]).
5294    #[inline]
5295    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
5296        if self.last_readline_handle.is_empty() {
5297            self.line_number
5298        } else {
5299            *self
5300                .handle_line_numbers
5301                .get(&self.last_readline_handle)
5302                .unwrap_or(&0)
5303        }
5304    }
5305
5306    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
5307    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
5308    ///
5309    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
5310    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
5311    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
5312    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
5313    /// get `0` / `N` from [`StrykeValue::to_int`].
5314    pub(crate) fn scalar_flip_flop_eval(
5315        &mut self,
5316        left: i64,
5317        right: i64,
5318        slot: usize,
5319        exclusive: bool,
5320    ) -> StrykeResult<StrykeValue> {
5321        if self.flip_flop_active.len() <= slot {
5322            self.flip_flop_active.resize(slot + 1, false);
5323        }
5324        if self.flip_flop_exclusive_left_line.len() <= slot {
5325            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5326        }
5327        if self.flip_flop_sequence.len() <= slot {
5328            self.flip_flop_sequence.resize(slot + 1, 0);
5329        }
5330        if self.flip_flop_last_dot.len() <= slot {
5331            self.flip_flop_last_dot.resize(slot + 1, None);
5332        }
5333        let dot = self.scalar_flipflop_dot_line();
5334        let active = &mut self.flip_flop_active[slot];
5335        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5336        let seq = &mut self.flip_flop_sequence[slot];
5337        let last_dot = &mut self.flip_flop_last_dot[slot];
5338        if !*active {
5339            if dot == left {
5340                *active = true;
5341                *seq = 1;
5342                *last_dot = Some(dot);
5343                if exclusive {
5344                    *excl_left = Some(dot);
5345                } else {
5346                    *excl_left = None;
5347                    if dot == right {
5348                        *active = false;
5349                        return Ok(StrykeValue::string(format!("{}E0", *seq)));
5350                    }
5351                }
5352                return Ok(StrykeValue::string(seq.to_string()));
5353            }
5354            *last_dot = Some(dot);
5355            return Ok(StrykeValue::string(String::new()));
5356        }
5357        // Already active: increment the sequence once per new `$.`, so a second evaluation on
5358        // the same line reads the same number (matches Perl `pp_flop`).
5359        if *last_dot != Some(dot) {
5360            *seq += 1;
5361            *last_dot = Some(dot);
5362        }
5363        let cur_seq = *seq;
5364        if let Some(ll) = *excl_left {
5365            if dot == right && dot > ll {
5366                *active = false;
5367                *excl_left = None;
5368                *seq = 0;
5369                return Ok(StrykeValue::string(format!("{}E0", cur_seq)));
5370            }
5371        } else if dot == right {
5372            *active = false;
5373            *seq = 0;
5374            return Ok(StrykeValue::string(format!("{}E0", cur_seq)));
5375        }
5376        Ok(StrykeValue::string(cur_seq.to_string()))
5377    }
5378
5379    fn regex_flip_flop_transition(
5380        active: &mut bool,
5381        excl_left: &mut Option<i64>,
5382        exclusive: bool,
5383        dot: i64,
5384        left_m: bool,
5385        right_m: bool,
5386    ) -> i64 {
5387        if !*active {
5388            if left_m {
5389                *active = true;
5390                if exclusive {
5391                    *excl_left = Some(dot);
5392                } else {
5393                    *excl_left = None;
5394                    if right_m {
5395                        *active = false;
5396                    }
5397                }
5398                return 1;
5399            }
5400            return 0;
5401        }
5402        if let Some(ll) = *excl_left {
5403            if right_m && dot > ll {
5404                *active = false;
5405                *excl_left = None;
5406            }
5407        } else if right_m {
5408            *active = false;
5409        }
5410        1
5411    }
5412
5413    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
5414    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
5415    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
5416    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
5417    pub(crate) fn regex_flip_flop_eval(
5418        &mut self,
5419        left_pat: &str,
5420        left_flags: &str,
5421        right_pat: &str,
5422        right_flags: &str,
5423        slot: usize,
5424        exclusive: bool,
5425        line: usize,
5426    ) -> StrykeResult<StrykeValue> {
5427        let dot = self.scalar_flipflop_dot_line();
5428        let subject = self.scope.get_scalar("_").to_string();
5429        let left_re = self
5430            .compile_regex(left_pat, left_flags, line)
5431            .map_err(|e| match e {
5432                FlowOrError::Error(err) => err,
5433                FlowOrError::Flow(_) => {
5434                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5435                }
5436            })?;
5437        let right_re = self
5438            .compile_regex(right_pat, right_flags, line)
5439            .map_err(|e| match e {
5440                FlowOrError::Error(err) => err,
5441                FlowOrError::Flow(_) => {
5442                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5443                }
5444            })?;
5445        let left_m = left_re.is_match(&subject);
5446        let right_m = right_re.is_match(&subject);
5447        if self.flip_flop_active.len() <= slot {
5448            self.flip_flop_active.resize(slot + 1, false);
5449        }
5450        if self.flip_flop_exclusive_left_line.len() <= slot {
5451            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5452        }
5453        let active = &mut self.flip_flop_active[slot];
5454        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5455        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5456            active, excl_left, exclusive, dot, left_m, right_m,
5457        )))
5458    }
5459
5460    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
5461    pub(crate) fn regex_flip_flop_eval_dynamic_right(
5462        &mut self,
5463        left_pat: &str,
5464        left_flags: &str,
5465        slot: usize,
5466        exclusive: bool,
5467        line: usize,
5468        right_m: bool,
5469    ) -> StrykeResult<StrykeValue> {
5470        let dot = self.scalar_flipflop_dot_line();
5471        let subject = self.scope.get_scalar("_").to_string();
5472        let left_re = self
5473            .compile_regex(left_pat, left_flags, line)
5474            .map_err(|e| match e {
5475                FlowOrError::Error(err) => err,
5476                FlowOrError::Flow(_) => {
5477                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5478                }
5479            })?;
5480        let left_m = left_re.is_match(&subject);
5481        if self.flip_flop_active.len() <= slot {
5482            self.flip_flop_active.resize(slot + 1, false);
5483        }
5484        if self.flip_flop_exclusive_left_line.len() <= slot {
5485            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5486        }
5487        let active = &mut self.flip_flop_active[slot];
5488        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5489        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5490            active, excl_left, exclusive, dot, left_m, right_m,
5491        )))
5492    }
5493
5494    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
5495    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
5496        &mut self,
5497        left_pat: &str,
5498        left_flags: &str,
5499        slot: usize,
5500        exclusive: bool,
5501        line: usize,
5502        rhs_line: i64,
5503    ) -> StrykeResult<StrykeValue> {
5504        let dot = self.scalar_flipflop_dot_line();
5505        let subject = self.scope.get_scalar("_").to_string();
5506        let left_re = self
5507            .compile_regex(left_pat, left_flags, line)
5508            .map_err(|e| match e {
5509                FlowOrError::Error(err) => err,
5510                FlowOrError::Flow(_) => {
5511                    StrykeError::runtime("unexpected flow in regex flip-flop", line)
5512                }
5513            })?;
5514        let left_m = left_re.is_match(&subject);
5515        let right_m = dot == rhs_line;
5516        if self.flip_flop_active.len() <= slot {
5517            self.flip_flop_active.resize(slot + 1, false);
5518        }
5519        if self.flip_flop_exclusive_left_line.len() <= slot {
5520            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5521        }
5522        let active = &mut self.flip_flop_active[slot];
5523        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5524        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5525            active, excl_left, exclusive, dot, left_m, right_m,
5526        )))
5527    }
5528
5529    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
5530    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
5531    /// right test until `$.` is strictly past the line where the left regex matched (same as
5532    /// [`Self::regex_flip_flop_eval`]).
5533    pub(crate) fn regex_eof_flip_flop_eval(
5534        &mut self,
5535        left_pat: &str,
5536        left_flags: &str,
5537        slot: usize,
5538        exclusive: bool,
5539        line: usize,
5540    ) -> StrykeResult<StrykeValue> {
5541        let dot = self.scalar_flipflop_dot_line();
5542        let subject = self.scope.get_scalar("_").to_string();
5543        let left_re = self
5544            .compile_regex(left_pat, left_flags, line)
5545            .map_err(|e| match e {
5546                FlowOrError::Error(err) => err,
5547                FlowOrError::Flow(_) => {
5548                    StrykeError::runtime("unexpected flow in regex/eof flip-flop", line)
5549                }
5550            })?;
5551        let left_m = left_re.is_match(&subject);
5552        let right_m = self.eof_without_arg_is_true();
5553        if self.flip_flop_active.len() <= slot {
5554            self.flip_flop_active.resize(slot + 1, false);
5555        }
5556        if self.flip_flop_exclusive_left_line.len() <= slot {
5557            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5558        }
5559        let active = &mut self.flip_flop_active[slot];
5560        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5561        Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
5562            active, excl_left, exclusive, dot, left_m, right_m,
5563        )))
5564    }
5565
5566    /// Shared `chomp` implementation (mutates `target`).
5567    /// `read(FH, $buf, LEN)` — read from filehandle into named variable.
5568    /// Returns bytes read count (or error). Called from VM's ReadIntoVar op.
5569    pub(crate) fn builtin_read_into(
5570        &mut self,
5571        fh_val: StrykeValue,
5572        var_name: &str,
5573        length: usize,
5574        line: usize,
5575    ) -> ExecResult {
5576        use std::io::Read;
5577        let fh = fh_val
5578            .as_io_handle_name()
5579            .unwrap_or_else(|| fh_val.to_string());
5580        let mut buf = vec![0u8; length];
5581        let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
5582            slot.lock().read(&mut buf).unwrap_or(0)
5583        } else if fh == "STDIN" {
5584            std::io::stdin().read(&mut buf).unwrap_or(0)
5585        } else {
5586            return Err(StrykeError::runtime(format!("read: unopened handle {}", fh), line).into());
5587        };
5588        buf.truncate(n);
5589        let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
5590        let _ = self
5591            .scope
5592            .set_scalar(var_name, StrykeValue::string(read_str));
5593        Ok(StrykeValue::integer(n as i64))
5594    }
5595
5596    pub(crate) fn chomp_inplace_execute(&mut self, val: StrykeValue, target: &Expr) -> ExecResult {
5597        // Perl's `chomp` on `@arr` / `%hash` iterates and chomps every
5598        // element in place, returning the *total count* of newlines
5599        // removed. Pre-fix this collapsed the array/hash to its
5600        // stringified form, chomped that, and reassigned a scalar
5601        // back — silently destroying the container.
5602        match &target.kind {
5603            ExprKind::ArrayVar(name) => {
5604                let arr = self.scope.get_array(name);
5605                let mut total = 0i64;
5606                let mut new_arr = Vec::with_capacity(arr.len());
5607                for v in arr {
5608                    let mut s = v.to_string();
5609                    if s.ends_with('\n') {
5610                        s.pop();
5611                        total += 1;
5612                    }
5613                    new_arr.push(StrykeValue::string(s));
5614                }
5615                self.scope
5616                    .set_array(name, new_arr)
5617                    .map_err(FlowOrError::Error)?;
5618                return Ok(StrykeValue::integer(total));
5619            }
5620            ExprKind::HashVar(name) => {
5621                let h = self.scope.get_hash(name);
5622                let mut total = 0i64;
5623                let mut new_h: indexmap::IndexMap<String, StrykeValue> =
5624                    indexmap::IndexMap::with_capacity(h.len());
5625                for (k, v) in h {
5626                    let mut s = v.to_string();
5627                    if s.ends_with('\n') {
5628                        s.pop();
5629                        total += 1;
5630                    }
5631                    new_h.insert(k, StrykeValue::string(s));
5632                }
5633                self.scope
5634                    .set_hash(name, new_h)
5635                    .map_err(FlowOrError::Error)?;
5636                return Ok(StrykeValue::integer(total));
5637            }
5638            _ => {}
5639        }
5640        let mut s = val.to_string();
5641        let removed = if s.ends_with('\n') {
5642            s.pop();
5643            1i64
5644        } else {
5645            0i64
5646        };
5647        self.assign_value(target, StrykeValue::string(s))?;
5648        Ok(StrykeValue::integer(removed))
5649    }
5650
5651    /// Shared `chop` implementation (mutates `target`).
5652    pub(crate) fn chop_inplace_execute(&mut self, val: StrykeValue, target: &Expr) -> ExecResult {
5653        // Perl's `chop @arr` / `chop %hash` chops every element in
5654        // place and returns the *last character chopped*. Without
5655        // this branch the call stringified the whole container,
5656        // chopped one byte off the joined form, and reassigned a
5657        // scalar back — destroying the array.
5658        match &target.kind {
5659            ExprKind::ArrayVar(name) => {
5660                let arr = self.scope.get_array(name);
5661                let mut last = StrykeValue::UNDEF;
5662                let mut new_arr = Vec::with_capacity(arr.len());
5663                for v in arr {
5664                    let mut s = v.to_string();
5665                    if let Some(c) = s.pop() {
5666                        last = StrykeValue::string(c.to_string());
5667                    }
5668                    new_arr.push(StrykeValue::string(s));
5669                }
5670                self.scope
5671                    .set_array(name, new_arr)
5672                    .map_err(FlowOrError::Error)?;
5673                return Ok(last);
5674            }
5675            ExprKind::HashVar(name) => {
5676                let h = self.scope.get_hash(name);
5677                let mut last = StrykeValue::UNDEF;
5678                let mut new_h: indexmap::IndexMap<String, StrykeValue> =
5679                    indexmap::IndexMap::with_capacity(h.len());
5680                for (k, v) in h {
5681                    let mut s = v.to_string();
5682                    if let Some(c) = s.pop() {
5683                        last = StrykeValue::string(c.to_string());
5684                    }
5685                    new_h.insert(k, StrykeValue::string(s));
5686                }
5687                self.scope
5688                    .set_hash(name, new_h)
5689                    .map_err(FlowOrError::Error)?;
5690                return Ok(last);
5691            }
5692            _ => {}
5693        }
5694        let mut s = val.to_string();
5695        let chopped = s
5696            .pop()
5697            .map(|c| StrykeValue::string(c.to_string()))
5698            .unwrap_or(StrykeValue::UNDEF);
5699        self.assign_value(target, StrykeValue::string(s))?;
5700        Ok(chopped)
5701    }
5702
5703    /// Shared regex match implementation (`pos` is updated for scalar `/g`).
5704    pub(crate) fn regex_match_execute(
5705        &mut self,
5706        s: String,
5707        pattern: &str,
5708        flags: &str,
5709        scalar_g: bool,
5710        pos_key: &str,
5711        line: usize,
5712    ) -> ExecResult {
5713        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
5714        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
5715        // also keep per-pattern `pos()` state that the memo doesn't track.
5716        //
5717        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
5718        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
5719        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
5720        if !flags.contains('g') && !scalar_g {
5721            let memo_hit = {
5722                if let Some(ref mem) = self.regex_match_memo {
5723                    mem.pattern == pattern
5724                        && mem.flags == flags
5725                        && mem.multiline == self.multiline_match
5726                        && mem.haystack == s
5727                } else {
5728                    false
5729                }
5730            };
5731            if memo_hit {
5732                if self.regex_capture_scope_fresh {
5733                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
5734                }
5735                // Memo hit but scope side effects were invalidated. Re-apply captures
5736                // from the memoized haystack + a fresh compiled regex.
5737                let (memo_s, memo_result) = {
5738                    let mem = self.regex_match_memo.as_ref().expect("memo");
5739                    (mem.haystack.clone(), mem.result.clone())
5740                };
5741                let re = self.compile_regex(pattern, flags, line)?;
5742                if let Some(caps) = re.captures(&memo_s) {
5743                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
5744                }
5745                self.regex_capture_scope_fresh = true;
5746                return Ok(memo_result);
5747            }
5748        }
5749        let re = self.compile_regex(pattern, flags, line)?;
5750        if flags.contains('g') && scalar_g {
5751            let key = pos_key.to_string();
5752            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
5753            if start == 0 {
5754                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5755            }
5756            if start > s.len() {
5757                self.regex_pos.insert(key, None);
5758                return Ok(StrykeValue::integer(0));
5759            }
5760            let sub = s.get(start..).unwrap_or("");
5761            if let Some(caps) = re.captures(sub) {
5762                let overall = caps.get(0).expect("capture 0");
5763                let abs_end = start + overall.end;
5764                self.regex_pos.insert(key, Some(abs_end));
5765                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
5766                Ok(StrykeValue::integer(1))
5767            } else {
5768                self.regex_pos.insert(key, None);
5769                Ok(StrykeValue::integer(0))
5770            }
5771        } else if flags.contains('g') {
5772            let mut rows = Vec::new();
5773            let mut last_caps: Option<PerlCaptures<'_>> = None;
5774            for caps in re.captures_iter(&s) {
5775                rows.push(StrykeValue::array(
5776                    crate::perl_regex::numbered_capture_flat(&caps),
5777                ));
5778                last_caps = Some(caps);
5779            }
5780            self.scope.set_array("^CAPTURE_ALL", rows)?;
5781            let matches: Vec<StrykeValue> = match &*re {
5782                PerlCompiledRegex::Rust(r) => r
5783                    .find_iter(&s)
5784                    .map(|m| StrykeValue::string(m.as_str().to_string()))
5785                    .collect(),
5786                PerlCompiledRegex::Fancy(r) => r
5787                    .find_iter(&s)
5788                    .filter_map(|m| m.ok())
5789                    .map(|m| StrykeValue::string(m.as_str().to_string()))
5790                    .collect(),
5791                PerlCompiledRegex::Pcre2(r) => r
5792                    .find_iter(s.as_bytes())
5793                    .filter_map(|m| m.ok())
5794                    .map(|m| {
5795                        let t = s.get(m.start()..m.end()).unwrap_or("");
5796                        StrykeValue::string(t.to_string())
5797                    })
5798                    .collect(),
5799            };
5800            if matches.is_empty() {
5801                Ok(StrykeValue::integer(0))
5802            } else {
5803                if let Some(caps) = last_caps {
5804                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
5805                }
5806                Ok(StrykeValue::array(matches))
5807            }
5808        } else if let Some(caps) = re.captures(&s) {
5809            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
5810            let result = StrykeValue::integer(1);
5811            self.regex_match_memo = Some(RegexMatchMemo {
5812                pattern: pattern.to_string(),
5813                flags: flags.to_string(),
5814                multiline: self.multiline_match,
5815                haystack: s,
5816                result: result.clone(),
5817            });
5818            self.regex_capture_scope_fresh = true;
5819            Ok(result)
5820        } else {
5821            let result = StrykeValue::integer(0);
5822            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
5823            self.regex_match_memo = Some(RegexMatchMemo {
5824                pattern: pattern.to_string(),
5825                flags: flags.to_string(),
5826                multiline: self.multiline_match,
5827                haystack: s,
5828                result: result.clone(),
5829            });
5830            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
5831            // the last successful match (if any) set them to. Don't flip the flag.
5832            Ok(result)
5833        }
5834    }
5835
5836    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
5837    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
5838    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
5839    pub(crate) fn expand_env_braces_in_subst(
5840        &mut self,
5841        raw: &str,
5842        line: usize,
5843    ) -> StrykeResult<String> {
5844        self.materialize_env_if_needed();
5845        let mut out = String::new();
5846        let mut rest = raw;
5847        while let Some(idx) = rest.find("$ENV{") {
5848            out.push_str(&rest[..idx]);
5849            let after = &rest[idx + 5..];
5850            let end = after
5851                .find('}')
5852                .ok_or_else(|| StrykeError::runtime("Unclosed $ENV{...} in s///", line))?;
5853            let key = &after[..end];
5854            let val = self.scope.get_hash_element("ENV", key);
5855            out.push_str(&val.to_string());
5856            rest = &after[end + 1..];
5857        }
5858        out.push_str(rest);
5859        Ok(out)
5860    }
5861
5862    /// Shared `s///` implementation.
5863    ///
5864    /// Perl replacement strings accept both `\1` and `$1` for back-references.
5865    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
5866    /// understand `$N`, so we normalise here.
5867    pub(crate) fn regex_subst_execute(
5868        &mut self,
5869        s: String,
5870        pattern: &str,
5871        replacement: &str,
5872        flags: &str,
5873        target: &Expr,
5874        line: usize,
5875    ) -> ExecResult {
5876        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
5877        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
5878        let re = self.compile_regex(&pattern, &re_flags, line)?;
5879        if flags.contains('e') {
5880            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
5881        }
5882        let replacement = self.expand_env_braces_in_subst(replacement, line)?;
5883        let replacement = self.interpolate_replacement_string(&replacement);
5884        let replacement = normalize_replacement_backrefs(&replacement);
5885        let last_caps = if flags.contains('g') {
5886            let mut rows = Vec::new();
5887            let mut last = None;
5888            for caps in re.captures_iter(&s) {
5889                rows.push(StrykeValue::array(
5890                    crate::perl_regex::numbered_capture_flat(&caps),
5891                ));
5892                last = Some(caps);
5893            }
5894            self.scope.set_array("^CAPTURE_ALL", rows)?;
5895            last
5896        } else {
5897            re.captures(&s)
5898        };
5899        if let Some(caps) = last_caps {
5900            let mode = if flags.contains('g') {
5901                CaptureAllMode::Skip
5902            } else {
5903                CaptureAllMode::Empty
5904            };
5905            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
5906        }
5907        let (new_s, count) = if flags.contains('g') {
5908            let count = re.find_iter_count(&s);
5909            (re.replace_all(&s, replacement.as_str()), count)
5910        } else {
5911            let count = if re.is_match(&s) { 1 } else { 0 };
5912            (re.replace(&s, replacement.as_str()), count)
5913        };
5914        if flags.contains('r') {
5915            // /r — non-destructive: return the modified string, leave target unchanged
5916            Ok(StrykeValue::string(new_s))
5917        } else {
5918            self.assign_value(target, StrykeValue::string(new_s))?;
5919            Ok(StrykeValue::integer(count as i64))
5920        }
5921    }
5922
5923    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
5924    /// and executes the string; the next round uses [`StrykeValue::to_string`] of the prior value).
5925    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
5926        let prep_source = |raw: &str| -> String {
5927            let mut code = raw.trim().to_string();
5928            if !code.ends_with(';') {
5929                code.push(';');
5930            }
5931            code
5932        };
5933        let mut cur = prep_source(replacement);
5934        let mut last = StrykeValue::UNDEF;
5935        for round in 0..e_count {
5936            last = crate::parse_and_run_string(&cur, self)?;
5937            if round + 1 < e_count {
5938                cur = prep_source(&last.to_string());
5939            }
5940        }
5941        Ok(last)
5942    }
5943
5944    fn regex_subst_execute_eval(
5945        &mut self,
5946        s: String,
5947        re: &PerlCompiledRegex,
5948        replacement: &str,
5949        flags: &str,
5950        target: &Expr,
5951        line: usize,
5952    ) -> ExecResult {
5953        let e_count = flags.chars().filter(|c| *c == 'e').count();
5954        if e_count == 0 {
5955            return Err(StrykeError::runtime("s///e: internal error (no e flag)", line).into());
5956        }
5957
5958        if flags.contains('g') {
5959            let mut rows = Vec::new();
5960            let mut out = String::new();
5961            let mut last = 0usize;
5962            let mut count = 0usize;
5963            for caps in re.captures_iter(&s) {
5964                let m0 = caps.get(0).expect("regex capture 0");
5965                out.push_str(&s[last..m0.start]);
5966                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5967                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5968                out.push_str(&repl_val.to_string());
5969                last = m0.end;
5970                count += 1;
5971                rows.push(StrykeValue::array(
5972                    crate::perl_regex::numbered_capture_flat(&caps),
5973                ));
5974            }
5975            self.scope.set_array("^CAPTURE_ALL", rows)?;
5976            out.push_str(&s[last..]);
5977            if flags.contains('r') {
5978                return Ok(StrykeValue::string(out));
5979            }
5980            self.assign_value(target, StrykeValue::string(out))?;
5981            return Ok(StrykeValue::integer(count as i64));
5982        }
5983        if let Some(caps) = re.captures(&s) {
5984            let m0 = caps.get(0).expect("regex capture 0");
5985            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5986            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5987            let mut out = String::new();
5988            out.push_str(&s[..m0.start]);
5989            out.push_str(&repl_val.to_string());
5990            out.push_str(&s[m0.end..]);
5991            if flags.contains('r') {
5992                return Ok(StrykeValue::string(out));
5993            }
5994            self.assign_value(target, StrykeValue::string(out))?;
5995            return Ok(StrykeValue::integer(1));
5996        }
5997        if flags.contains('r') {
5998            return Ok(StrykeValue::string(s));
5999        }
6000        self.assign_value(target, StrykeValue::string(s))?;
6001        Ok(StrykeValue::integer(0))
6002    }
6003
6004    /// Shared `tr///` implementation.
6005    pub(crate) fn regex_transliterate_execute(
6006        &mut self,
6007        s: String,
6008        from: &str,
6009        to: &str,
6010        flags: &str,
6011        target: &Expr,
6012        line: usize,
6013    ) -> ExecResult {
6014        let _ = line;
6015        let from_chars = Self::tr_expand_ranges(from);
6016        let to_chars = Self::tr_expand_ranges(to);
6017        let delete_mode = flags.contains('d');
6018        let mut count = 0i64;
6019        let new_s: String = s
6020            .chars()
6021            .filter_map(|c| {
6022                if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
6023                    count += 1;
6024                    if delete_mode {
6025                        // /d — delete characters that match but have no replacement
6026                        if pos < to_chars.len() {
6027                            Some(to_chars[pos])
6028                        } else {
6029                            None // delete this character
6030                        }
6031                    } else {
6032                        // Normal mode: use last char in to_chars if pos exceeds, or keep original
6033                        Some(to_chars.get(pos).or(to_chars.last()).copied().unwrap_or(c))
6034                    }
6035                } else {
6036                    Some(c)
6037                }
6038            })
6039            .collect();
6040        if flags.contains('r') {
6041            // /r — non-destructive: return the modified string, leave target unchanged
6042            Ok(StrykeValue::string(new_s))
6043        } else {
6044            self.assign_value(target, StrykeValue::string(new_s))?;
6045            Ok(StrykeValue::integer(count))
6046        }
6047    }
6048
6049    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
6050    /// A literal `-` at the start or end of the spec is kept as-is.
6051    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
6052        let raw: Vec<char> = spec.chars().collect();
6053        let mut out = Vec::with_capacity(raw.len());
6054        let mut i = 0;
6055        while i < raw.len() {
6056            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
6057                let start = raw[i] as u32;
6058                let end = raw[i + 2] as u32;
6059                for code in start..=end {
6060                    if let Some(c) = char::from_u32(code) {
6061                        out.push(c);
6062                    }
6063                }
6064                i += 3;
6065            } else {
6066                out.push(raw[i]);
6067                i += 1;
6068            }
6069        }
6070        out
6071    }
6072
6073    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
6074    pub(crate) fn splice_builtin_execute(
6075        &mut self,
6076        args: &[StrykeValue],
6077        line: usize,
6078    ) -> StrykeResult<StrykeValue> {
6079        if args.is_empty() {
6080            return Err(StrykeError::runtime("splice: missing array", line));
6081        }
6082        let arr_name = args[0].to_string();
6083        let arr_len = self.scope.array_len(&arr_name);
6084        let offset_val = args
6085            .get(1)
6086            .cloned()
6087            .unwrap_or_else(|| StrykeValue::integer(0));
6088        let length_val = match args.get(2) {
6089            None => StrykeValue::UNDEF,
6090            Some(v) => v.clone(),
6091        };
6092        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
6093        let rep_vals: Vec<StrykeValue> = args.iter().skip(3).cloned().collect();
6094        let removed = self.scope.splice_in_place(&arr_name, off, end, rep_vals)?;
6095        Ok(match self.wantarray_kind {
6096            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
6097            WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
6098        })
6099    }
6100
6101    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
6102    pub(crate) fn unshift_builtin_execute(
6103        &mut self,
6104        args: &[StrykeValue],
6105        line: usize,
6106    ) -> StrykeResult<StrykeValue> {
6107        if args.is_empty() {
6108            return Err(StrykeError::runtime("unshift: missing array", line));
6109        }
6110        let arr_name = args[0].to_string();
6111        let mut flat_vals: Vec<StrykeValue> = Vec::new();
6112        for a in args.iter().skip(1) {
6113            if let Some(items) = a.as_array_vec() {
6114                flat_vals.extend(items);
6115            } else {
6116                flat_vals.push(a.clone());
6117            }
6118        }
6119        let arr = self.scope.get_array_mut(&arr_name)?;
6120        for (i, v) in flat_vals.into_iter().enumerate() {
6121            arr.insert(i, v);
6122        }
6123        Ok(StrykeValue::integer(arr.len() as i64))
6124    }
6125
6126    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
6127    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
6128    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
6129        if upper == 0.0 {
6130            self.rand_rng.gen_range(0.0..1.0)
6131        } else if upper > 0.0 {
6132            self.rand_rng.gen_range(0.0..upper)
6133        } else {
6134            self.rand_rng.gen_range(upper..0.0)
6135        }
6136    }
6137
6138    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
6139    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
6140        let n = if let Some(s) = seed {
6141            s as i64
6142        } else {
6143            std::time::SystemTime::now()
6144                .duration_since(std::time::UNIX_EPOCH)
6145                .map(|d| d.as_secs() as i64)
6146                .unwrap_or(1)
6147        };
6148        let mag = n.unsigned_abs();
6149        self.rand_rng = StdRng::seed_from_u64(mag);
6150        n.abs()
6151    }
6152
6153    pub fn set_file(&mut self, file: &str) {
6154        self.file = file.to_string();
6155    }
6156
6157    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
6158    pub fn repl_completion_names(&self) -> Vec<String> {
6159        let mut v = self.scope.repl_binding_names();
6160        v.extend(self.subs.keys().cloned());
6161        v.sort();
6162        v.dedup();
6163        v
6164    }
6165
6166    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
6167    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
6168        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
6169        subs.sort();
6170        let mut classes: HashSet<String> = HashSet::new();
6171        for k in &subs {
6172            if let Some((pkg, rest)) = k.split_once("::") {
6173                if !rest.contains("::") {
6174                    classes.insert(pkg.to_string());
6175                }
6176            }
6177        }
6178        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
6179        for bn in self.scope.repl_binding_names() {
6180            if let Some(r) = bn.strip_prefix('$') {
6181                let v = self.scope.get_scalar(r);
6182                if let Some(b) = v.as_blessed_ref() {
6183                    blessed_scalars.insert(r.to_string(), b.class.clone());
6184                    classes.insert(b.class.clone());
6185                }
6186            }
6187        }
6188        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
6189        for c in classes {
6190            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
6191        }
6192        ReplCompletionSnapshot {
6193            subs,
6194            blessed_scalars,
6195            isa_for_class,
6196        }
6197    }
6198
6199    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
6200        if n == 0 {
6201            return Err(FlowOrError::Error(StrykeError::runtime(
6202                "bench: iteration count must be positive",
6203                line,
6204            )));
6205        }
6206        let mut samples = Vec::with_capacity(n);
6207        for _ in 0..n {
6208            let start = std::time::Instant::now();
6209            self.exec_block(body)?;
6210            samples.push(start.elapsed().as_secs_f64() * 1000.0);
6211        }
6212        let mut sorted = samples.clone();
6213        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6214        let min_ms = sorted[0];
6215        let mean = samples.iter().sum::<f64>() / n as f64;
6216        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
6217            .saturating_sub(1)
6218            .min(n - 1);
6219        let p99_ms = sorted[p99_idx];
6220        Ok(StrykeValue::string(format!(
6221            "bench: n={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
6222            n, min_ms, mean, p99_ms
6223        )))
6224    }
6225
6226    pub fn execute(&mut self, program: &Program) -> StrykeResult<StrykeValue> {
6227        // Snapshot the (possibly empty) class registry into the
6228        // thread-local that the free-function serializers consult, so
6229        // that `to_json($obj)` can resolve inheritance fields without
6230        // taking a `&VMHelper`. Done unconditionally — cheap clone of
6231        // an Arc<HashMap>-shaped structure.
6232        crate::serialize_normalize::install_class_defs(self.class_defs.clone());
6233        // `-n`/`-p`: compile and run only the prelude, store chunk for per-line re-execution.
6234        if self.line_mode_skip_main {
6235            crate::compile_and_run_prelude(program, self)?;
6236            return Ok(StrykeValue::UNDEF);
6237        }
6238        crate::try_vm_execute(program, self)
6239            .expect("VM compilation must succeed — all execution is VM-only")
6240    }
6241
6242    /// Run `END` blocks (after `-n`/`-p` line loop when prelude used [`Self::line_mode_skip_main`]).
6243    pub fn run_end_blocks(&mut self) -> StrykeResult<()> {
6244        self.global_phase = "END".to_string();
6245        let ends = std::mem::take(&mut self.end_blocks);
6246        for block in &ends {
6247            self.exec_block(block).map_err(|e| match e {
6248                FlowOrError::Error(e) => e,
6249                FlowOrError::Flow(_) => StrykeError::runtime("Unexpected flow control in END", 0),
6250            })?;
6251        }
6252        Ok(())
6253    }
6254
6255    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
6256    /// and drain remaining `DESTROY` callbacks.
6257    pub fn run_global_teardown(&mut self) -> StrykeResult<()> {
6258        self.global_phase = "DESTRUCT".to_string();
6259        self.drain_pending_destroys(0)
6260    }
6261
6262    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
6263    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> StrykeResult<()> {
6264        loop {
6265            let batch = crate::pending_destroy::take_queue();
6266            if batch.is_empty() {
6267                break;
6268            }
6269            for (class, payload) in batch {
6270                let fq = format!("{}::DESTROY", class);
6271                let Some(sub) = self.subs.get(&fq).cloned() else {
6272                    continue;
6273                };
6274                let inv = StrykeValue::blessed(Arc::new(
6275                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
6276                ));
6277                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
6278                    Ok(_) => {}
6279                    Err(FlowOrError::Error(e)) => return Err(e),
6280                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
6281                    Err(FlowOrError::Flow(other)) => {
6282                        return Err(StrykeError::runtime(
6283                            format!("DESTROY: unexpected control flow ({other:?})"),
6284                            line,
6285                        ));
6286                    }
6287                }
6288            }
6289        }
6290        Ok(())
6291    }
6292
6293    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
6294        self.exec_block_with_tail(block, WantarrayCtx::Void)
6295    }
6296
6297    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
6298    /// Non-final statements stay void context.
6299    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6300        let uses_goto = block
6301            .iter()
6302            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
6303        if uses_goto {
6304            self.scope_push_hook();
6305            let r = self.exec_block_with_goto_tail(block, tail);
6306            self.scope_pop_hook();
6307            r
6308        } else {
6309            self.scope_push_hook();
6310            let result = self.exec_block_no_scope_with_tail(block, tail);
6311            self.scope_pop_hook();
6312            result
6313        }
6314    }
6315
6316    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6317        let mut map: HashMap<String, usize> = HashMap::new();
6318        for (i, s) in block.iter().enumerate() {
6319            if let Some(l) = &s.label {
6320                map.insert(l.clone(), i);
6321            }
6322        }
6323        let mut pc = 0usize;
6324        let mut last = StrykeValue::UNDEF;
6325        let last_idx = block.len().saturating_sub(1);
6326        while pc < block.len() {
6327            if let StmtKind::Goto { target } = &block[pc].kind {
6328                let line = block[pc].line;
6329                let name = self.eval_expr(target)?.to_string();
6330                pc = *map.get(&name).ok_or_else(|| {
6331                    FlowOrError::Error(StrykeError::runtime(
6332                        format!("goto: unknown label {}", name),
6333                        line,
6334                    ))
6335                })?;
6336                continue;
6337            }
6338            let v = if pc == last_idx {
6339                match &block[pc].kind {
6340                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
6341                    _ => self.exec_statement(&block[pc])?,
6342                }
6343            } else {
6344                self.exec_statement(&block[pc])?
6345            };
6346            last = v;
6347            pc += 1;
6348        }
6349        Ok(last)
6350    }
6351
6352    /// Execute block statements without pushing/popping a scope frame.
6353    /// Used internally by loops and the VM for sub calls.
6354    #[inline]
6355    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
6356        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
6357    }
6358
6359    pub(crate) fn exec_block_no_scope_with_tail(
6360        &mut self,
6361        block: &Block,
6362        tail: WantarrayCtx,
6363    ) -> ExecResult {
6364        if block.is_empty() {
6365            return Ok(StrykeValue::UNDEF);
6366        }
6367        let last_i = block.len() - 1;
6368        for (i, stmt) in block.iter().enumerate() {
6369            if i < last_i {
6370                self.exec_statement(stmt)?;
6371            } else {
6372                return match &stmt.kind {
6373                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
6374                    _ => self.exec_statement(stmt),
6375                };
6376            }
6377        }
6378        Ok(StrykeValue::UNDEF)
6379    }
6380
6381    /// Spawn `block` on a worker thread; returns an [`StrykeValue::AsyncTask`] handle (`async { }` / `spawn { }`).
6382    pub(crate) fn spawn_async_block(&self, block: &Block) -> StrykeValue {
6383        use parking_lot::Mutex as ParkMutex;
6384
6385        let block = block.clone();
6386        let subs = self.subs.clone();
6387        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6388        let result = Arc::new(ParkMutex::new(None));
6389        let join = Arc::new(ParkMutex::new(None));
6390        let result2 = result.clone();
6391        let h = std::thread::spawn(move || {
6392            let mut interp = VMHelper::new();
6393            interp.subs = subs;
6394            interp.scope.restore_capture(&scalars);
6395            interp.scope.restore_atomics(&aar, &ahash);
6396            interp.enable_parallel_guard();
6397            let r = match interp.exec_block(&block) {
6398                Ok(v) => Ok(v),
6399                Err(FlowOrError::Error(e)) => Err(e),
6400                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6401                    Err(StrykeError::runtime("yield inside async/spawn block", 0))
6402                }
6403                Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
6404            };
6405            *result2.lock() = Some(r);
6406        });
6407        *join.lock() = Some(h);
6408        StrykeValue::async_task(Arc::new(StrykeAsyncTask { result, join }))
6409    }
6410
6411    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
6412    pub(crate) fn eval_timeout_block(
6413        &mut self,
6414        body: &Block,
6415        secs: f64,
6416        line: usize,
6417    ) -> ExecResult {
6418        use std::sync::mpsc::channel;
6419        use std::time::Duration;
6420
6421        let block = body.clone();
6422        let subs = self.subs.clone();
6423        let struct_defs = self.struct_defs.clone();
6424        let enum_defs = self.enum_defs.clone();
6425        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6426        self.materialize_env_if_needed();
6427        let env = self.env.clone();
6428        let argv = self.argv.clone();
6429        let inc = self.scope.get_array("INC");
6430        let (tx, rx) = channel::<StrykeResult<StrykeValue>>();
6431        let _handle = std::thread::spawn(move || {
6432            let mut interp = VMHelper::new();
6433            interp.subs = subs;
6434            interp.struct_defs = struct_defs;
6435            interp.enum_defs = enum_defs;
6436            interp.env = env.clone();
6437            interp.argv = argv.clone();
6438            interp.scope.declare_array(
6439                "ARGV",
6440                argv.iter()
6441                    .map(|s| StrykeValue::string(s.clone()))
6442                    .collect(),
6443            );
6444            for (k, v) in env {
6445                interp
6446                    .scope
6447                    .set_hash_element("ENV", &k, v)
6448                    .expect("set ENV in timeout thread");
6449            }
6450            interp.scope.declare_array("INC", inc);
6451            interp.scope.restore_capture(&scalars);
6452            interp.scope.restore_atomics(&aar, &ahash);
6453            interp.enable_parallel_guard();
6454            let out: StrykeResult<StrykeValue> = match interp.exec_block(&block) {
6455                Ok(v) => Ok(v),
6456                Err(FlowOrError::Error(e)) => Err(e),
6457                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6458                    Err(StrykeError::runtime("yield inside eval_timeout block", 0))
6459                }
6460                Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
6461            };
6462            let _ = tx.send(out);
6463        });
6464        let dur = Duration::from_secs_f64(secs.max(0.0));
6465        match rx.recv_timeout(dur) {
6466            Ok(Ok(v)) => Ok(v),
6467            Ok(Err(e)) => Err(FlowOrError::Error(e)),
6468            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(StrykeError::runtime(
6469                format!(
6470                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
6471                    secs
6472                ),
6473                line,
6474            )
6475            .into()),
6476            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(StrykeError::runtime(
6477                "eval_timeout: worker thread panicked or disconnected",
6478                line,
6479            )
6480            .into()),
6481        }
6482    }
6483
6484    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
6485        let mut last = StrykeValue::UNDEF;
6486        for stmt in body {
6487            match &stmt.kind {
6488                StmtKind::When { cond, body: wb } => {
6489                    if self.when_matches(cond)? {
6490                        return self.exec_block_smart(wb);
6491                    }
6492                }
6493                StmtKind::DefaultCase { body: db } => {
6494                    return self.exec_block_smart(db);
6495                }
6496                _ => {
6497                    last = self.exec_statement(stmt)?;
6498                }
6499            }
6500        }
6501        Ok(last)
6502    }
6503
6504    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
6505    pub(crate) fn exec_given_with_topic_value(
6506        &mut self,
6507        topic: StrykeValue,
6508        body: &Block,
6509    ) -> ExecResult {
6510        self.scope_push_hook();
6511        self.scope.declare_scalar("_", topic);
6512        self.english_note_lexical_scalar("_");
6513        let r = self.exec_given_body(body);
6514        self.scope_pop_hook();
6515        r
6516    }
6517
6518    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
6519        let t = self.eval_expr(topic)?;
6520        self.exec_given_with_topic_value(t, body)
6521    }
6522
6523    /// `when (COND)` — topic is `$_` (set by `given`).
6524    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6525        let topic = self.scope.get_scalar("_");
6526        let line = cond.line;
6527        match &cond.kind {
6528            ExprKind::Regex(pattern, flags) => {
6529                let re = self.compile_regex(pattern, flags, line)?;
6530                let s = topic.to_string();
6531                Ok(re.is_match(&s))
6532            }
6533            ExprKind::String(s) => Ok(topic.to_string() == *s),
6534            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
6535            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
6536            _ => {
6537                let c = self.eval_expr(cond)?;
6538                Ok(self.smartmatch_when(&topic, &c))
6539            }
6540        }
6541    }
6542
6543    fn smartmatch_when(&self, topic: &StrykeValue, c: &StrykeValue) -> bool {
6544        if let Some(re) = c.as_regex() {
6545            return re.is_match(&topic.to_string());
6546        }
6547        // ARRAY / array-ref RHS: smartmatch is "any element matches the topic"
6548        // (`$x ~~ @list` → `grep { $x ~~ $_ } @list` per `perlop`).
6549        // Without this branch, `when ([2, 3, 5, 7])` always falls through to
6550        // `default` because the array stringified ("23 5 7"-ish) won't equal
6551        // the scalar. Recurse so nested arrays / regexes inside the array
6552        // still work.
6553        if let Some(arr) = c.as_array_ref() {
6554            let arr = arr.read();
6555            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
6556        }
6557        if let Some(arr) = c.as_array_vec() {
6558            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
6559        }
6560        // HASH / hash-ref RHS: "topic is a key" (`$x ~~ %h` → `exists $h{$x}`).
6561        if let Some(href) = c.as_hash_ref() {
6562            return href.read().contains_key(&topic.to_string());
6563        }
6564        if let Some(h) = c.as_hash_map() {
6565            return h.contains_key(&topic.to_string());
6566        }
6567        // Coderef RHS: call it with the topic, treat truthy result as match.
6568        if let Some(sub) = c.as_code_ref() {
6569            // smartmatch_when is `&self`; we can't `call_sub` (needs `&mut`).
6570            // For now, fall through to string equality. Future: hoist
6571            // when_matches to use `&mut self` so coderef RHS can fire.
6572            let _ = sub;
6573        }
6574        // Numeric equality if both sides parse as numbers.
6575        if let (Some(a), Some(b)) = (topic.as_integer(), c.as_integer()) {
6576            return a == b;
6577        }
6578        topic.to_string() == c.to_string()
6579    }
6580
6581    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
6582    pub(crate) fn eval_boolean_rvalue_condition(
6583        &mut self,
6584        cond: &Expr,
6585    ) -> Result<bool, FlowOrError> {
6586        match &cond.kind {
6587            ExprKind::Regex(pattern, flags) => {
6588                let topic = self.scope.get_scalar("_");
6589                let line = cond.line;
6590                let s = topic.to_string();
6591                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
6592                Ok(v.is_true())
6593            }
6594            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
6595            ExprKind::ReadLine(_) => {
6596                let v = self.eval_expr(cond)?;
6597                self.scope.set_topic(v.clone());
6598                Ok(!v.is_undef())
6599            }
6600            _ => {
6601                let v = self.eval_expr(cond)?;
6602                Ok(v.is_true())
6603            }
6604        }
6605    }
6606
6607    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
6608    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6609        self.eval_boolean_rvalue_condition(cond)
6610    }
6611
6612    pub(crate) fn eval_algebraic_match(
6613        &mut self,
6614        subject: &Expr,
6615        arms: &[MatchArm],
6616        line: usize,
6617    ) -> ExecResult {
6618        let val = self.eval_algebraic_match_subject(subject, line)?;
6619        self.eval_algebraic_match_with_subject_value(val, arms, line)
6620    }
6621
6622    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
6623    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
6624        match &subject.kind {
6625            ExprKind::ArrayVar(name) => {
6626                self.check_strict_array_var(name, line)?;
6627                let aname = self.stash_array_name_for_package(name);
6628                Ok(StrykeValue::array_binding_ref(aname))
6629            }
6630            ExprKind::HashVar(name) => {
6631                self.check_strict_hash_var(name, line)?;
6632                self.touch_env_hash(name);
6633                Ok(StrykeValue::hash_binding_ref(name.clone()))
6634            }
6635            _ => self.eval_expr(subject),
6636        }
6637    }
6638
6639    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
6640    pub(crate) fn eval_algebraic_match_with_subject_value(
6641        &mut self,
6642        val: StrykeValue,
6643        arms: &[MatchArm],
6644        line: usize,
6645    ) -> ExecResult {
6646        // Exhaustive enum match: check variant coverage before matching
6647        if let Some(e) = val.as_enum_inst() {
6648            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
6649            if !has_catchall {
6650                let covered: Vec<String> = arms
6651                    .iter()
6652                    .filter_map(|a| {
6653                        if let MatchPattern::Value(expr) = &a.pattern {
6654                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
6655                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
6656                            }
6657                        }
6658                        None
6659                    })
6660                    .collect();
6661                let missing: Vec<&str> = e
6662                    .def
6663                    .variants
6664                    .iter()
6665                    .filter(|v| !covered.contains(&v.name))
6666                    .map(|v| v.name.as_str())
6667                    .collect();
6668                if !missing.is_empty() {
6669                    return Err(StrykeError::runtime(
6670                        format!(
6671                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
6672                            e.def.name,
6673                            missing.join(", ")
6674                        ),
6675                        line,
6676                    )
6677                    .into());
6678                }
6679            }
6680        }
6681        for arm in arms {
6682            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
6683                let re = self.compile_regex(pattern, flags, line)?;
6684                let s = val.to_string();
6685                if let Some(caps) = re.captures(&s) {
6686                    self.scope_push_hook();
6687                    self.scope.declare_scalar("_", val.clone());
6688                    self.english_note_lexical_scalar("_");
6689                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
6690                    let guard_ok = if let Some(g) = &arm.guard {
6691                        self.eval_expr(g)?.is_true()
6692                    } else {
6693                        true
6694                    };
6695                    if !guard_ok {
6696                        self.scope_pop_hook();
6697                        continue;
6698                    }
6699                    let out = self.eval_expr(&arm.body);
6700                    self.scope_pop_hook();
6701                    return out;
6702                }
6703                continue;
6704            }
6705            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
6706                self.scope_push_hook();
6707                self.scope.declare_scalar("_", val.clone());
6708                self.english_note_lexical_scalar("_");
6709                for b in bindings {
6710                    match b {
6711                        PatternBinding::Scalar(name, v) => {
6712                            self.scope.declare_scalar(&name, v);
6713                            self.english_note_lexical_scalar(&name);
6714                        }
6715                        PatternBinding::Array(name, elems) => {
6716                            self.scope.declare_array(&name, elems);
6717                        }
6718                    }
6719                }
6720                let guard_ok = if let Some(g) = &arm.guard {
6721                    self.eval_expr(g)?.is_true()
6722                } else {
6723                    true
6724                };
6725                if !guard_ok {
6726                    self.scope_pop_hook();
6727                    continue;
6728                }
6729                let out = self.eval_expr(&arm.body);
6730                self.scope_pop_hook();
6731                return out;
6732            }
6733        }
6734        Err(StrykeError::runtime(
6735            "match: no arm matched the value (add a `_` catch-all)",
6736            line,
6737        )
6738        .into())
6739    }
6740
6741    fn parse_duration_seconds(pv: &StrykeValue) -> Option<f64> {
6742        let s = pv.to_string();
6743        let s = s.trim();
6744        if let Some(rest) = s.strip_suffix("ms") {
6745            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
6746        }
6747        if let Some(rest) = s.strip_suffix('s') {
6748            return rest.trim().parse::<f64>().ok();
6749        }
6750        if let Some(rest) = s.strip_suffix('m') {
6751            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
6752        }
6753        s.parse::<f64>().ok()
6754    }
6755
6756    fn eval_retry_block(
6757        &mut self,
6758        body: &Block,
6759        times: &Expr,
6760        backoff: RetryBackoff,
6761        _line: usize,
6762    ) -> ExecResult {
6763        let max = self.eval_expr(times)?.to_int().max(1) as usize;
6764        let base_ms: u64 = 10;
6765        let mut attempt = 0usize;
6766        loop {
6767            attempt += 1;
6768            match self.exec_block(body) {
6769                Ok(v) => return Ok(v),
6770                Err(FlowOrError::Error(e)) => {
6771                    if attempt >= max {
6772                        return Err(FlowOrError::Error(e));
6773                    }
6774                    let delay_ms = match backoff {
6775                        RetryBackoff::None => 0,
6776                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
6777                        RetryBackoff::Exponential => {
6778                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
6779                        }
6780                    };
6781                    if delay_ms > 0 {
6782                        std::thread::sleep(Duration::from_millis(delay_ms));
6783                    }
6784                }
6785                Err(e) => return Err(e),
6786            }
6787        }
6788    }
6789
6790    fn eval_rate_limit_block(
6791        &mut self,
6792        slot: u32,
6793        max: &Expr,
6794        window: &Expr,
6795        body: &Block,
6796        _line: usize,
6797    ) -> ExecResult {
6798        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
6799        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
6800            .filter(|s| *s > 0.0)
6801            .unwrap_or(1.0);
6802        let window_d = Duration::from_secs_f64(window_sec);
6803        let slot = slot as usize;
6804        while self.rate_limit_slots.len() <= slot {
6805            self.rate_limit_slots.push(VecDeque::new());
6806        }
6807        {
6808            let dq = &mut self.rate_limit_slots[slot];
6809            loop {
6810                let now = Instant::now();
6811                while let Some(t0) = dq.front().copied() {
6812                    if now.duration_since(t0) >= window_d {
6813                        dq.pop_front();
6814                    } else {
6815                        break;
6816                    }
6817                }
6818                if dq.len() < max_n || max_n == 0 {
6819                    break;
6820                }
6821                let t0 = dq.front().copied().unwrap();
6822                let wait = window_d.saturating_sub(now.duration_since(t0));
6823                if wait.is_zero() {
6824                    dq.pop_front();
6825                    continue;
6826                }
6827                std::thread::sleep(wait);
6828            }
6829            dq.push_back(Instant::now());
6830        }
6831        self.exec_block(body)
6832    }
6833
6834    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
6835        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
6836            .filter(|s| *s > 0.0)
6837            .unwrap_or(1.0);
6838        loop {
6839            match self.exec_block(body) {
6840                Ok(_) => {}
6841                Err(e) => return Err(e),
6842            }
6843            std::thread::sleep(Duration::from_secs_f64(sec));
6844        }
6845    }
6846
6847    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
6848    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> StrykeResult<StrykeValue> {
6849        let pair = |value: StrykeValue, more: i64| {
6850            StrykeValue::array_ref(Arc::new(RwLock::new(vec![
6851                value,
6852                StrykeValue::integer(more),
6853            ])))
6854        };
6855        let mut exhausted = gen.exhausted.lock();
6856        if *exhausted {
6857            return Ok(pair(StrykeValue::UNDEF, 0));
6858        }
6859        let mut pc = gen.pc.lock();
6860        let mut scope_started = gen.scope_started.lock();
6861        if *pc >= gen.block.len() {
6862            if *scope_started {
6863                self.scope_pop_hook();
6864                *scope_started = false;
6865            }
6866            *exhausted = true;
6867            return Ok(pair(StrykeValue::UNDEF, 0));
6868        }
6869        if !*scope_started {
6870            self.scope_push_hook();
6871            *scope_started = true;
6872        }
6873        self.in_generator = true;
6874        while *pc < gen.block.len() {
6875            let stmt = &gen.block[*pc];
6876            match self.exec_statement(stmt) {
6877                Ok(_) => {
6878                    *pc += 1;
6879                }
6880                Err(FlowOrError::Flow(Flow::Yield(v))) => {
6881                    *pc += 1;
6882                    self.in_generator = false;
6883                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
6884                    // binds in the caller block, not inside a frame left across yield.
6885                    if *scope_started {
6886                        self.scope_pop_hook();
6887                        *scope_started = false;
6888                    }
6889                    return Ok(pair(v, 1));
6890                }
6891                Err(e) => {
6892                    self.in_generator = false;
6893                    if *scope_started {
6894                        self.scope_pop_hook();
6895                        *scope_started = false;
6896                    }
6897                    return Err(match e {
6898                        FlowOrError::Error(ee) => ee,
6899                        FlowOrError::Flow(Flow::Yield(_)) => {
6900                            unreachable!("yield handled above")
6901                        }
6902                        FlowOrError::Flow(flow) => StrykeError::runtime(
6903                            format!("unexpected control flow in generator: {:?}", flow),
6904                            0,
6905                        ),
6906                    });
6907                }
6908            }
6909        }
6910        self.in_generator = false;
6911        if *scope_started {
6912            self.scope_pop_hook();
6913            *scope_started = false;
6914        }
6915        *exhausted = true;
6916        Ok(pair(StrykeValue::UNDEF, 0))
6917    }
6918
6919    fn match_pattern_try(
6920        &mut self,
6921        subject: &StrykeValue,
6922        pattern: &MatchPattern,
6923        line: usize,
6924    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6925        match pattern {
6926            MatchPattern::Any => Ok(Some(vec![])),
6927            MatchPattern::Regex { .. } => {
6928                unreachable!("regex arms are handled in eval_algebraic_match")
6929            }
6930            MatchPattern::Value(expr) => {
6931                if self.match_pattern_value_alternation(subject, expr, line)? {
6932                    Ok(Some(vec![]))
6933                } else {
6934                    Ok(None)
6935                }
6936            }
6937            MatchPattern::Array(elems) => {
6938                let Some(arr) = self.match_subject_as_array(subject) else {
6939                    return Ok(None);
6940                };
6941                self.match_array_pattern_elems(&arr, elems, line)
6942            }
6943            MatchPattern::Hash(pairs) => {
6944                let Some(h) = self.match_subject_as_hash(subject) else {
6945                    return Ok(None);
6946                };
6947                self.match_hash_pattern_pairs(&h, pairs, line)
6948            }
6949            MatchPattern::OptionSome(name) => {
6950                let Some(arr) = self.match_subject_as_array(subject) else {
6951                    return Ok(None);
6952                };
6953                if arr.len() < 2 {
6954                    return Ok(None);
6955                }
6956                if !arr[1].is_true() {
6957                    return Ok(None);
6958                }
6959                Ok(Some(vec![PatternBinding::Scalar(
6960                    name.clone(),
6961                    arr[0].clone(),
6962                )]))
6963            }
6964        }
6965    }
6966
6967    /// Handle pattern alternation (e.g., `"foo" | "bar" | "baz"`) in match patterns.
6968    /// If the expression is a BitOr chain, recursively check if subject matches any alternative.
6969    fn match_pattern_value_alternation(
6970        &mut self,
6971        subject: &StrykeValue,
6972        expr: &Expr,
6973        _line: usize,
6974    ) -> Result<bool, FlowOrError> {
6975        if let ExprKind::BinOp {
6976            left,
6977            op: BinOp::BitOr,
6978            right,
6979        } = &expr.kind
6980        {
6981            if self.match_pattern_value_alternation(subject, left, _line)? {
6982                return Ok(true);
6983            }
6984            return self.match_pattern_value_alternation(subject, right, _line);
6985        }
6986        let pv = self.eval_expr(expr)?;
6987        Ok(self.smartmatch_when(subject, &pv))
6988    }
6989
6990    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
6991    fn match_subject_as_array(&self, v: &StrykeValue) -> Option<Vec<StrykeValue>> {
6992        if let Some(a) = v.as_array_vec() {
6993            return Some(a);
6994        }
6995        if let Some(r) = v.as_array_ref() {
6996            return Some(r.read().clone());
6997        }
6998        if let Some(name) = v.as_array_binding_name() {
6999            return Some(self.scope.get_array(&name));
7000        }
7001        None
7002    }
7003
7004    fn match_subject_as_hash(&mut self, v: &StrykeValue) -> Option<IndexMap<String, StrykeValue>> {
7005        if let Some(h) = v.as_hash_map() {
7006            return Some(h);
7007        }
7008        if let Some(r) = v.as_hash_ref() {
7009            return Some(r.read().clone());
7010        }
7011        if let Some(name) = v.as_hash_binding_name() {
7012            self.touch_env_hash(&name);
7013            return Some(self.scope.get_hash(&name));
7014        }
7015        None
7016    }
7017
7018    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
7019    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
7020    pub(crate) fn hash_slice_deref_values(
7021        &mut self,
7022        container: &StrykeValue,
7023        key_values: &[StrykeValue],
7024        line: usize,
7025    ) -> Result<StrykeValue, FlowOrError> {
7026        let h = if let Some(m) = self.match_subject_as_hash(container) {
7027            m
7028        } else {
7029            return Err(StrykeError::runtime(
7030                "Hash slice dereference needs a hash or hash reference value",
7031                line,
7032            )
7033            .into());
7034        };
7035        let mut result = Vec::new();
7036        for kv in key_values {
7037            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
7038                vv.iter().map(|x| x.to_string()).collect()
7039            } else {
7040                vec![kv.to_string()]
7041            };
7042            for k in key_strings {
7043                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
7044            }
7045        }
7046        Ok(StrykeValue::array(result))
7047    }
7048
7049    /// Single-key write for a hash slice container (hash ref or package hash name).
7050    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
7051    pub(crate) fn assign_hash_slice_one_key(
7052        &mut self,
7053        container: StrykeValue,
7054        key: &str,
7055        val: StrykeValue,
7056        line: usize,
7057    ) -> Result<StrykeValue, FlowOrError> {
7058        if let Some(r) = container.as_hash_ref() {
7059            r.write().insert(key.to_string(), val);
7060            return Ok(StrykeValue::UNDEF);
7061        }
7062        if let Some(name) = container.as_hash_binding_name() {
7063            self.touch_env_hash(&name);
7064            self.scope
7065                .set_hash_element(&name, key, val)
7066                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7067            return Ok(StrykeValue::UNDEF);
7068        }
7069        if let Some(s) = container.as_str() {
7070            self.touch_env_hash(&s);
7071            if self.strict_refs {
7072                return Err(StrykeError::runtime(
7073                    format!(
7074                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7075                        s
7076                    ),
7077                    line,
7078                )
7079                .into());
7080            }
7081            self.scope
7082                .set_hash_element(&s, key, val)
7083                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7084            return Ok(StrykeValue::UNDEF);
7085        }
7086        Err(StrykeError::runtime(
7087            "Hash slice assignment needs a hash or hash reference value",
7088            line,
7089        )
7090        .into())
7091    }
7092
7093    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
7094    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
7095    pub(crate) fn assign_named_hash_slice(
7096        &mut self,
7097        hash: &str,
7098        key_values: Vec<StrykeValue>,
7099        val: StrykeValue,
7100        line: usize,
7101    ) -> Result<StrykeValue, FlowOrError> {
7102        self.touch_env_hash(hash);
7103        let mut ks: Vec<String> = Vec::new();
7104        for kv in key_values {
7105            if let Some(vv) = kv.as_array_vec() {
7106                ks.extend(vv.iter().map(|x| x.to_string()));
7107            } else {
7108                ks.push(kv.to_string());
7109            }
7110        }
7111        if ks.is_empty() {
7112            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7113        }
7114        let items = val.to_list();
7115        for (i, k) in ks.iter().enumerate() {
7116            let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7117            self.scope
7118                .set_hash_element(hash, k, v)
7119                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7120        }
7121        Ok(StrykeValue::UNDEF)
7122    }
7123
7124    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
7125    pub(crate) fn assign_hash_slice_deref(
7126        &mut self,
7127        container: StrykeValue,
7128        key_values: Vec<StrykeValue>,
7129        val: StrykeValue,
7130        line: usize,
7131    ) -> Result<StrykeValue, FlowOrError> {
7132        let mut ks: Vec<String> = Vec::new();
7133        for kv in key_values {
7134            if let Some(vv) = kv.as_array_vec() {
7135                ks.extend(vv.iter().map(|x| x.to_string()));
7136            } else {
7137                ks.push(kv.to_string());
7138            }
7139        }
7140        if ks.is_empty() {
7141            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7142        }
7143        let items = val.to_list();
7144        if let Some(r) = container.as_hash_ref() {
7145            let mut h = r.write();
7146            for (i, k) in ks.iter().enumerate() {
7147                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7148                h.insert(k.clone(), v);
7149            }
7150            return Ok(StrykeValue::UNDEF);
7151        }
7152        if let Some(name) = container.as_hash_binding_name() {
7153            self.touch_env_hash(&name);
7154            for (i, k) in ks.iter().enumerate() {
7155                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7156                self.scope
7157                    .set_hash_element(&name, k, v)
7158                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7159            }
7160            return Ok(StrykeValue::UNDEF);
7161        }
7162        if let Some(s) = container.as_str() {
7163            if self.strict_refs {
7164                return Err(StrykeError::runtime(
7165                    format!(
7166                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7167                        s
7168                    ),
7169                    line,
7170                )
7171                .into());
7172            }
7173            self.touch_env_hash(&s);
7174            for (i, k) in ks.iter().enumerate() {
7175                let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
7176                self.scope
7177                    .set_hash_element(&s, k, v)
7178                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7179            }
7180            return Ok(StrykeValue::UNDEF);
7181        }
7182        Err(StrykeError::runtime(
7183            "Hash slice assignment needs a hash or hash reference value",
7184            line,
7185        )
7186        .into())
7187    }
7188
7189    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
7190    /// Perl 5 applies the compound op only to the **last** slice element.
7191    pub(crate) fn compound_assign_hash_slice_deref(
7192        &mut self,
7193        container: StrykeValue,
7194        key_values: Vec<StrykeValue>,
7195        op: BinOp,
7196        rhs: StrykeValue,
7197        line: usize,
7198    ) -> Result<StrykeValue, FlowOrError> {
7199        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7200        let last_old = old_list
7201            .to_list()
7202            .last()
7203            .cloned()
7204            .unwrap_or(StrykeValue::UNDEF);
7205        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7206        let mut ks: Vec<String> = Vec::new();
7207        for kv in &key_values {
7208            if let Some(vv) = kv.as_array_vec() {
7209                ks.extend(vv.iter().map(|x| x.to_string()));
7210            } else {
7211                ks.push(kv.to_string());
7212            }
7213        }
7214        if ks.is_empty() {
7215            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7216        }
7217        let last_key = ks.last().expect("non-empty ks");
7218        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7219        Ok(new_val)
7220    }
7221
7222    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
7223    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
7224    /// the **old** value of that last element.
7225    ///
7226    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
7227    pub(crate) fn hash_slice_deref_inc_dec(
7228        &mut self,
7229        container: StrykeValue,
7230        key_values: Vec<StrykeValue>,
7231        kind: u8,
7232        line: usize,
7233    ) -> Result<StrykeValue, FlowOrError> {
7234        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7235        let last_old = old_list
7236            .to_list()
7237            .last()
7238            .cloned()
7239            .unwrap_or(StrykeValue::UNDEF);
7240        let new_val = if kind & 1 == 0 {
7241            StrykeValue::integer(last_old.to_int() + 1)
7242        } else {
7243            StrykeValue::integer(last_old.to_int() - 1)
7244        };
7245        let mut ks: Vec<String> = Vec::new();
7246        for kv in &key_values {
7247            if let Some(vv) = kv.as_array_vec() {
7248                ks.extend(vv.iter().map(|x| x.to_string()));
7249            } else {
7250                ks.push(kv.to_string());
7251            }
7252        }
7253        let last_key = ks.last().ok_or_else(|| {
7254            StrykeError::runtime("Hash slice increment needs at least one key", line)
7255        })?;
7256        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7257        Ok(if kind < 2 { new_val } else { last_old })
7258    }
7259
7260    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[StrykeValue]) -> StrykeValue {
7261        self.touch_env_hash(hash);
7262        let h = self.scope.get_hash(hash);
7263        let mut result = Vec::new();
7264        for kv in key_values {
7265            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
7266                vv.iter().map(|x| x.to_string()).collect()
7267            } else {
7268                vec![kv.to_string()]
7269            };
7270            for k in key_strings {
7271                result.push(h.get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
7272            }
7273        }
7274        StrykeValue::array(result)
7275    }
7276
7277    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
7278    pub(crate) fn compound_assign_named_hash_slice(
7279        &mut self,
7280        hash: &str,
7281        key_values: Vec<StrykeValue>,
7282        op: BinOp,
7283        rhs: StrykeValue,
7284        line: usize,
7285    ) -> Result<StrykeValue, FlowOrError> {
7286        let old_list = self.hash_slice_named_values(hash, &key_values);
7287        let last_old = old_list
7288            .to_list()
7289            .last()
7290            .cloned()
7291            .unwrap_or(StrykeValue::UNDEF);
7292        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7293        let mut ks: Vec<String> = Vec::new();
7294        for kv in &key_values {
7295            if let Some(vv) = kv.as_array_vec() {
7296                ks.extend(vv.iter().map(|x| x.to_string()));
7297            } else {
7298                ks.push(kv.to_string());
7299            }
7300        }
7301        if ks.is_empty() {
7302            return Err(StrykeError::runtime("assign to empty hash slice", line).into());
7303        }
7304        let last_key = ks.last().expect("non-empty ks");
7305        let container = StrykeValue::string(hash.to_string());
7306        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7307        Ok(new_val)
7308    }
7309
7310    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
7311    pub(crate) fn named_hash_slice_inc_dec(
7312        &mut self,
7313        hash: &str,
7314        key_values: Vec<StrykeValue>,
7315        kind: u8,
7316        line: usize,
7317    ) -> Result<StrykeValue, FlowOrError> {
7318        let old_list = self.hash_slice_named_values(hash, &key_values);
7319        let last_old = old_list
7320            .to_list()
7321            .last()
7322            .cloned()
7323            .unwrap_or(StrykeValue::UNDEF);
7324        let new_val = if kind & 1 == 0 {
7325            StrykeValue::integer(last_old.to_int() + 1)
7326        } else {
7327            StrykeValue::integer(last_old.to_int() - 1)
7328        };
7329        let mut ks: Vec<String> = Vec::new();
7330        for kv in &key_values {
7331            if let Some(vv) = kv.as_array_vec() {
7332                ks.extend(vv.iter().map(|x| x.to_string()));
7333            } else {
7334                ks.push(kv.to_string());
7335            }
7336        }
7337        let last_key = ks.last().ok_or_else(|| {
7338            StrykeError::runtime("Hash slice increment needs at least one key", line)
7339        })?;
7340        let container = StrykeValue::string(hash.to_string());
7341        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7342        Ok(if kind < 2 { new_val } else { last_old })
7343    }
7344
7345    fn match_array_pattern_elems(
7346        &mut self,
7347        arr: &[StrykeValue],
7348        elems: &[MatchArrayElem],
7349        line: usize,
7350    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7351        let has_rest = elems
7352            .iter()
7353            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
7354        let mut binds: Vec<PatternBinding> = Vec::new();
7355        let mut idx = 0usize;
7356        for (i, elem) in elems.iter().enumerate() {
7357            match elem {
7358                MatchArrayElem::Rest => {
7359                    if i != elems.len() - 1 {
7360                        return Err(StrykeError::runtime(
7361                            "internal: `*` must be last in array match pattern",
7362                            line,
7363                        )
7364                        .into());
7365                    }
7366                    return Ok(Some(binds));
7367                }
7368                MatchArrayElem::RestBind(name) => {
7369                    if i != elems.len() - 1 {
7370                        return Err(StrykeError::runtime(
7371                            "internal: `@name` rest bind must be last in array match pattern",
7372                            line,
7373                        )
7374                        .into());
7375                    }
7376                    let tail = arr[idx..].to_vec();
7377                    binds.push(PatternBinding::Array(name.clone(), tail));
7378                    return Ok(Some(binds));
7379                }
7380                MatchArrayElem::CaptureScalar(name) => {
7381                    if idx >= arr.len() {
7382                        return Ok(None);
7383                    }
7384                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
7385                    idx += 1;
7386                }
7387                MatchArrayElem::Expr(e) => {
7388                    if idx >= arr.len() {
7389                        return Ok(None);
7390                    }
7391                    let expected = self.eval_expr(e)?;
7392                    if !self.smartmatch_when(&arr[idx], &expected) {
7393                        return Ok(None);
7394                    }
7395                    idx += 1;
7396                }
7397            }
7398        }
7399        if !has_rest && idx != arr.len() {
7400            return Ok(None);
7401        }
7402        Ok(Some(binds))
7403    }
7404
7405    fn match_hash_pattern_pairs(
7406        &mut self,
7407        h: &IndexMap<String, StrykeValue>,
7408        pairs: &[MatchHashPair],
7409        _line: usize,
7410    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7411        let mut binds = Vec::new();
7412        for pair in pairs {
7413            match pair {
7414                MatchHashPair::KeyOnly { key } => {
7415                    let ks = self.eval_expr(key)?.to_string();
7416                    if !h.contains_key(&ks) {
7417                        return Ok(None);
7418                    }
7419                }
7420                MatchHashPair::Capture { key, name } => {
7421                    let ks = self.eval_expr(key)?.to_string();
7422                    let Some(v) = h.get(&ks) else {
7423                        return Ok(None);
7424                    };
7425                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
7426                }
7427            }
7428        }
7429        Ok(Some(binds))
7430    }
7431
7432    /// Check if a block declares variables (needs its own scope frame).
7433    #[inline]
7434    fn block_needs_scope(block: &Block) -> bool {
7435        block.iter().any(|s| match &s.kind {
7436            StmtKind::My(_)
7437            | StmtKind::Our(_)
7438            | StmtKind::Local(_)
7439            | StmtKind::State(_)
7440            | StmtKind::LocalExpr { .. } => true,
7441            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
7442            _ => false,
7443        })
7444    }
7445
7446    /// Execute block, only pushing a scope frame if needed.
7447    #[inline]
7448    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
7449        if Self::block_needs_scope(block) {
7450            self.exec_block(block)
7451        } else {
7452            self.exec_block_no_scope(block)
7453        }
7454    }
7455
7456    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
7457        let t0 = self.profiler.is_some().then(std::time::Instant::now);
7458        let r = self.exec_statement_inner(stmt);
7459        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
7460            prof.on_line(&self.file, stmt.line, t0.elapsed());
7461        }
7462        r
7463    }
7464
7465    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
7466        if let Err(e) = crate::perl_signal::poll(self) {
7467            return Err(FlowOrError::Error(e));
7468        }
7469        if let Err(e) = self.drain_pending_destroys(stmt.line) {
7470            return Err(FlowOrError::Error(e));
7471        }
7472        match &stmt.kind {
7473            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
7474            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
7475            StmtKind::If {
7476                condition,
7477                body,
7478                elsifs,
7479                else_block,
7480            } => {
7481                if self.eval_boolean_rvalue_condition(condition)? {
7482                    return self.exec_block(body);
7483                }
7484                for (c, b) in elsifs {
7485                    if self.eval_boolean_rvalue_condition(c)? {
7486                        return self.exec_block(b);
7487                    }
7488                }
7489                if let Some(eb) = else_block {
7490                    return self.exec_block(eb);
7491                }
7492                Ok(StrykeValue::UNDEF)
7493            }
7494            StmtKind::Unless {
7495                condition,
7496                body,
7497                else_block,
7498            } => {
7499                if !self.eval_boolean_rvalue_condition(condition)? {
7500                    return self.exec_block(body);
7501                }
7502                if let Some(eb) = else_block {
7503                    return self.exec_block(eb);
7504                }
7505                Ok(StrykeValue::UNDEF)
7506            }
7507            StmtKind::While {
7508                condition,
7509                body,
7510                label,
7511                continue_block,
7512            } => {
7513                'outer: loop {
7514                    if !self.eval_boolean_rvalue_condition(condition)? {
7515                        break;
7516                    }
7517                    'inner: loop {
7518                        match self.exec_block_smart(body) {
7519                            Ok(_) => break 'inner,
7520                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7521                                if l == label || l.is_none() =>
7522                            {
7523                                break 'outer;
7524                            }
7525                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7526                                if l == label || l.is_none() =>
7527                            {
7528                                if let Some(cb) = continue_block {
7529                                    let _ = self.exec_block_smart(cb);
7530                                }
7531                                continue 'outer;
7532                            }
7533                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7534                                if l == label || l.is_none() =>
7535                            {
7536                                continue 'inner;
7537                            }
7538                            Err(e) => return Err(e),
7539                        }
7540                    }
7541                    if let Some(cb) = continue_block {
7542                        let _ = self.exec_block_smart(cb);
7543                    }
7544                }
7545                Ok(StrykeValue::UNDEF)
7546            }
7547            StmtKind::Until {
7548                condition,
7549                body,
7550                label,
7551                continue_block,
7552            } => {
7553                'outer: loop {
7554                    if self.eval_boolean_rvalue_condition(condition)? {
7555                        break;
7556                    }
7557                    'inner: loop {
7558                        match self.exec_block(body) {
7559                            Ok(_) => break 'inner,
7560                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7561                                if l == label || l.is_none() =>
7562                            {
7563                                break 'outer;
7564                            }
7565                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7566                                if l == label || l.is_none() =>
7567                            {
7568                                if let Some(cb) = continue_block {
7569                                    let _ = self.exec_block_smart(cb);
7570                                }
7571                                continue 'outer;
7572                            }
7573                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7574                                if l == label || l.is_none() =>
7575                            {
7576                                continue 'inner;
7577                            }
7578                            Err(e) => return Err(e),
7579                        }
7580                    }
7581                    if let Some(cb) = continue_block {
7582                        let _ = self.exec_block_smart(cb);
7583                    }
7584                }
7585                Ok(StrykeValue::UNDEF)
7586            }
7587            StmtKind::DoWhile { body, condition } => {
7588                loop {
7589                    self.exec_block(body)?;
7590                    if !self.eval_boolean_rvalue_condition(condition)? {
7591                        break;
7592                    }
7593                }
7594                Ok(StrykeValue::UNDEF)
7595            }
7596            StmtKind::For {
7597                init,
7598                condition,
7599                step,
7600                body,
7601                label,
7602                continue_block,
7603            } => {
7604                self.scope_push_hook();
7605                if let Some(init) = init {
7606                    self.exec_statement(init)?;
7607                }
7608                'outer: loop {
7609                    if let Some(cond) = condition {
7610                        if !self.eval_boolean_rvalue_condition(cond)? {
7611                            break;
7612                        }
7613                    }
7614                    'inner: loop {
7615                        match self.exec_block_smart(body) {
7616                            Ok(_) => break 'inner,
7617                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7618                                if l == label || l.is_none() =>
7619                            {
7620                                break 'outer;
7621                            }
7622                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7623                                if l == label || l.is_none() =>
7624                            {
7625                                if let Some(cb) = continue_block {
7626                                    let _ = self.exec_block_smart(cb);
7627                                }
7628                                if let Some(step) = step {
7629                                    self.eval_expr(step)?;
7630                                }
7631                                continue 'outer;
7632                            }
7633                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7634                                if l == label || l.is_none() =>
7635                            {
7636                                continue 'inner;
7637                            }
7638                            Err(e) => {
7639                                self.scope_pop_hook();
7640                                return Err(e);
7641                            }
7642                        }
7643                    }
7644                    if let Some(cb) = continue_block {
7645                        let _ = self.exec_block_smart(cb);
7646                    }
7647                    if let Some(step) = step {
7648                        self.eval_expr(step)?;
7649                    }
7650                }
7651                self.scope_pop_hook();
7652                Ok(StrykeValue::UNDEF)
7653            }
7654            StmtKind::Foreach {
7655                var,
7656                list,
7657                body,
7658                label,
7659                continue_block,
7660            } => {
7661                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
7662                let items = list_val.to_list();
7663                self.scope_push_hook();
7664                self.scope.declare_scalar(var, StrykeValue::UNDEF);
7665                self.english_note_lexical_scalar(var);
7666                let mut i = 0usize;
7667                'outer: while i < items.len() {
7668                    // For the implicit topic loop (`for (@list) { ... }`,
7669                    // var=="_"), use `set_topic` so `$_`, `$_0`, `_`, `_0`
7670                    // all alias the iter value. Plain `set_scalar("_", ...)`
7671                    // only updates `$_` and leaves `$_0` undef, violating
7672                    // the four-way aliasing invariant. Explicit named loops
7673                    // (`for my $x (@list)`) keep the simple scalar binding.
7674                    if var == "_" {
7675                        self.scope.set_topic(items[i].clone());
7676                    } else {
7677                        self.scope
7678                            .set_scalar(var, items[i].clone())
7679                            .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
7680                    }
7681                    'inner: loop {
7682                        match self.exec_block_smart(body) {
7683                            Ok(_) => break 'inner,
7684                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7685                                if l == label || l.is_none() =>
7686                            {
7687                                break 'outer;
7688                            }
7689                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7690                                if l == label || l.is_none() =>
7691                            {
7692                                if let Some(cb) = continue_block {
7693                                    let _ = self.exec_block_smart(cb);
7694                                }
7695                                i += 1;
7696                                continue 'outer;
7697                            }
7698                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7699                                if l == label || l.is_none() =>
7700                            {
7701                                continue 'inner;
7702                            }
7703                            Err(e) => {
7704                                self.scope_pop_hook();
7705                                return Err(e);
7706                            }
7707                        }
7708                    }
7709                    if let Some(cb) = continue_block {
7710                        let _ = self.exec_block_smart(cb);
7711                    }
7712                    i += 1;
7713                }
7714                self.scope_pop_hook();
7715                Ok(StrykeValue::UNDEF)
7716            }
7717            StmtKind::SubDecl {
7718                name,
7719                params,
7720                body,
7721                prototype,
7722            } => {
7723                let key = self.qualify_sub_key(name);
7724                let captured = self.scope.capture();
7725                let closure_env = if captured.is_empty() {
7726                    None
7727                } else {
7728                    Some(captured)
7729                };
7730                let mut sub = StrykeSub {
7731                    name: name.clone(),
7732                    params: params.clone(),
7733                    body: body.clone(),
7734                    closure_env,
7735                    prototype: prototype.clone(),
7736                    fib_like: None,
7737                };
7738                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
7739                self.subs.insert(key, Arc::new(sub));
7740                Ok(StrykeValue::UNDEF)
7741            }
7742            StmtKind::StructDecl { def } => {
7743                if self.struct_defs.contains_key(&def.name) {
7744                    return Err(StrykeError::runtime(
7745                        format!("duplicate struct `{}`", def.name),
7746                        stmt.line,
7747                    )
7748                    .into());
7749                }
7750                self.struct_defs
7751                    .insert(def.name.clone(), Arc::new(def.clone()));
7752                Ok(StrykeValue::UNDEF)
7753            }
7754            StmtKind::EnumDecl { def } => {
7755                if self.enum_defs.contains_key(&def.name) {
7756                    return Err(StrykeError::runtime(
7757                        format!("duplicate enum `{}`", def.name),
7758                        stmt.line,
7759                    )
7760                    .into());
7761                }
7762                self.enum_defs
7763                    .insert(def.name.clone(), Arc::new(def.clone()));
7764                Ok(StrykeValue::UNDEF)
7765            }
7766            StmtKind::ClassDecl { def } => {
7767                if self.class_defs.contains_key(&def.name) {
7768                    return Err(StrykeError::runtime(
7769                        format!("duplicate class `{}`", def.name),
7770                        stmt.line,
7771                    )
7772                    .into());
7773                }
7774                // Final class enforcement: prevent subclassing
7775                for parent_name in &def.extends {
7776                    if let Some(parent_def) = self.class_defs.get(parent_name) {
7777                        if parent_def.is_final {
7778                            return Err(StrykeError::runtime(
7779                                format!("cannot extend final class `{}`", parent_name),
7780                                stmt.line,
7781                            )
7782                            .into());
7783                        }
7784                        // Final method enforcement: prevent overriding
7785                        for m in &def.methods {
7786                            if let Some(parent_method) = parent_def.method(&m.name) {
7787                                if parent_method.is_final {
7788                                    return Err(StrykeError::runtime(
7789                                        format!(
7790                                            "cannot override final method `{}` from class `{}`",
7791                                            m.name, parent_name
7792                                        ),
7793                                        stmt.line,
7794                                    )
7795                                    .into());
7796                                }
7797                            }
7798                        }
7799                    }
7800                }
7801                // Trait contract enforcement + default method inheritance
7802                let mut def = def.clone();
7803                for trait_name in &def.implements.clone() {
7804                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
7805                        for required in trait_def.required_methods() {
7806                            let has_method = def.methods.iter().any(|m| m.name == required.name);
7807                            if !has_method {
7808                                return Err(StrykeError::runtime(
7809                                    format!(
7810                                        "class `{}` implements trait `{}` but does not define required method `{}`",
7811                                        def.name, trait_name, required.name
7812                                    ),
7813                                    stmt.line,
7814                                )
7815                                .into());
7816                            }
7817                        }
7818                        // Inherit default methods from trait (methods with bodies)
7819                        for tm in &trait_def.methods {
7820                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
7821                                def.methods.push(tm.clone());
7822                            }
7823                        }
7824                    }
7825                }
7826                // Abstract method enforcement: concrete subclasses must implement
7827                // all abstract methods (body-less methods) from abstract parents
7828                if !def.is_abstract {
7829                    for parent_name in &def.extends.clone() {
7830                        if let Some(parent_def) = self.class_defs.get(parent_name) {
7831                            if parent_def.is_abstract {
7832                                for m in &parent_def.methods {
7833                                    if m.body.is_none()
7834                                        && !def.methods.iter().any(|dm| dm.name == m.name)
7835                                    {
7836                                        return Err(StrykeError::runtime(
7837                                            format!(
7838                                                "class `{}` must implement abstract method `{}` from `{}`",
7839                                                def.name, m.name, parent_name
7840                                            ),
7841                                            stmt.line,
7842                                        )
7843                                        .into());
7844                                    }
7845                                }
7846                            }
7847                        }
7848                    }
7849                }
7850                // Initialize static fields
7851                for sf in &def.static_fields {
7852                    let val = if let Some(ref expr) = sf.default {
7853                        self.eval_expr(expr)?
7854                    } else {
7855                        StrykeValue::UNDEF
7856                    };
7857                    let key = format!("{}::{}", def.name, sf.name);
7858                    self.scope.declare_scalar(&key, val);
7859                }
7860                // Register class methods into self.subs so method dispatch finds them.
7861                for m in &def.methods {
7862                    if let Some(ref body) = m.body {
7863                        let fq = format!("{}::{}", def.name, m.name);
7864                        let sub = Arc::new(StrykeSub {
7865                            name: fq.clone(),
7866                            params: m.params.clone(),
7867                            body: body.clone(),
7868                            closure_env: None,
7869                            prototype: None,
7870                            fib_like: None,
7871                        });
7872                        self.subs.insert(fq, sub);
7873                    }
7874                }
7875                // Set @ClassName::ISA so MRO/isa resolution works.
7876                if !def.extends.is_empty() {
7877                    let isa_key = format!("{}::ISA", def.name);
7878                    let parents: Vec<StrykeValue> = def
7879                        .extends
7880                        .iter()
7881                        .map(|p| StrykeValue::string(p.clone()))
7882                        .collect();
7883                    self.scope.declare_array(&isa_key, parents);
7884                }
7885                let arc_def = Arc::new(def);
7886                self.class_defs
7887                    .insert(arc_def.name.clone(), Arc::clone(&arc_def));
7888                // Mirror the new class into the serializer-visible
7889                // thread-local registry so `to_json($obj)` etc. can walk
7890                // its inheritance chain.
7891                crate::serialize_normalize::register_class_def(arc_def);
7892                Ok(StrykeValue::UNDEF)
7893            }
7894            StmtKind::TraitDecl { def } => {
7895                if self.trait_defs.contains_key(&def.name) {
7896                    return Err(StrykeError::runtime(
7897                        format!("duplicate trait `{}`", def.name),
7898                        stmt.line,
7899                    )
7900                    .into());
7901                }
7902                self.trait_defs
7903                    .insert(def.name.clone(), Arc::new(def.clone()));
7904                Ok(StrykeValue::UNDEF)
7905            }
7906            StmtKind::My(decls) | StmtKind::Our(decls) => {
7907                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
7908                // For list assignment my ($a, $b) = (10, 20), distribute elements.
7909                // All decls share the same initializer in the AST (parser clones it).
7910                if decls.len() > 1 && decls[0].initializer.is_some() {
7911                    let val = self.eval_expr_ctx(
7912                        decls[0].initializer.as_ref().unwrap(),
7913                        WantarrayCtx::List,
7914                    )?;
7915                    let items = val.to_list();
7916                    let mut idx = 0;
7917                    for decl in decls {
7918                        match decl.sigil {
7919                            Sigil::Scalar => {
7920                                let v = items.get(idx).cloned().unwrap_or(StrykeValue::UNDEF);
7921                                let skey = if is_our {
7922                                    self.stash_scalar_name_for_package(&decl.name)
7923                                } else {
7924                                    decl.name.clone()
7925                                };
7926                                self.scope.declare_scalar_frozen(
7927                                    &skey,
7928                                    v,
7929                                    decl.frozen,
7930                                    decl.type_annotation.clone(),
7931                                )?;
7932                                self.english_note_lexical_scalar(&decl.name);
7933                                if is_our {
7934                                    self.note_our_scalar(&decl.name);
7935                                }
7936                                idx += 1;
7937                            }
7938                            Sigil::Array => {
7939                                // Array slurps remaining elements
7940                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
7941                                idx = items.len();
7942                                if is_our {
7943                                    self.record_exporter_our_array_name(&decl.name, &rest);
7944                                }
7945                                let aname = self.stash_array_name_for_package(&decl.name);
7946                                self.scope.declare_array(&aname, rest);
7947                            }
7948                            Sigil::Hash => {
7949                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
7950                                idx = items.len();
7951                                let mut map = IndexMap::new();
7952                                let mut i = 0;
7953                                while i + 1 < rest.len() {
7954                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7955                                    i += 2;
7956                                }
7957                                self.scope.declare_hash(&decl.name, map);
7958                            }
7959                            Sigil::Typeglob => {
7960                                return Err(StrykeError::runtime(
7961                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
7962                                    stmt.line,
7963                                )
7964                                .into());
7965                            }
7966                        }
7967                    }
7968                } else {
7969                    // Single decl or no initializer
7970                    for decl in decls {
7971                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
7972                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
7973                        // compound op reads the lhs (see system Exporter.pm).
7974                        let compound_init = decl
7975                            .initializer
7976                            .as_ref()
7977                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
7978
7979                        if compound_init {
7980                            match decl.sigil {
7981                                Sigil::Typeglob => {
7982                                    return Err(StrykeError::runtime(
7983                                        "compound assignment on typeglob declaration is not supported",
7984                                        stmt.line,
7985                                    )
7986                                    .into());
7987                                }
7988                                Sigil::Scalar => {
7989                                    let skey = if is_our {
7990                                        self.stash_scalar_name_for_package(&decl.name)
7991                                    } else {
7992                                        decl.name.clone()
7993                                    };
7994                                    self.scope.declare_scalar_frozen(
7995                                        &skey,
7996                                        StrykeValue::UNDEF,
7997                                        decl.frozen,
7998                                        decl.type_annotation.clone(),
7999                                    )?;
8000                                    self.english_note_lexical_scalar(&decl.name);
8001                                    if is_our {
8002                                        self.note_our_scalar(&decl.name);
8003                                    }
8004                                    let init = decl.initializer.as_ref().unwrap();
8005                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8006                                }
8007                                Sigil::Array => {
8008                                    let aname = self.stash_array_name_for_package(&decl.name);
8009                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
8010                                    let init = decl.initializer.as_ref().unwrap();
8011                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8012                                    if is_our {
8013                                        let items = self.scope.get_array(&aname);
8014                                        self.record_exporter_our_array_name(&decl.name, &items);
8015                                    }
8016                                }
8017                                Sigil::Hash => {
8018                                    self.scope.declare_hash_frozen(
8019                                        &decl.name,
8020                                        IndexMap::new(),
8021                                        decl.frozen,
8022                                    );
8023                                    let init = decl.initializer.as_ref().unwrap();
8024                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
8025                                }
8026                            }
8027                            continue;
8028                        }
8029
8030                        let val = if let Some(init) = &decl.initializer {
8031                            let ctx = match decl.sigil {
8032                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
8033                                Sigil::Scalar if decl.list_context => WantarrayCtx::List,
8034                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
8035                            };
8036                            let v = self.eval_expr_ctx(init, ctx)?;
8037                            // my ($x) = @arr → extract first element from list
8038                            if decl.sigil == Sigil::Scalar && decl.list_context {
8039                                v.to_list().first().cloned().unwrap_or(StrykeValue::UNDEF)
8040                            } else {
8041                                v
8042                            }
8043                        } else {
8044                            StrykeValue::UNDEF
8045                        };
8046                        match decl.sigil {
8047                            Sigil::Typeglob => {
8048                                return Err(StrykeError::runtime(
8049                                    "`my *FH` / typeglob declaration is not supported",
8050                                    stmt.line,
8051                                )
8052                                .into());
8053                            }
8054                            Sigil::Scalar => {
8055                                let skey = if is_our {
8056                                    self.stash_scalar_name_for_package(&decl.name)
8057                                } else {
8058                                    decl.name.clone()
8059                                };
8060                                self.scope.declare_scalar_frozen(
8061                                    &skey,
8062                                    val,
8063                                    decl.frozen,
8064                                    decl.type_annotation.clone(),
8065                                )?;
8066                                self.english_note_lexical_scalar(&decl.name);
8067                                if is_our {
8068                                    self.note_our_scalar(&decl.name);
8069                                }
8070                            }
8071                            Sigil::Array => {
8072                                let items = val.to_list();
8073                                if is_our {
8074                                    self.record_exporter_our_array_name(&decl.name, &items);
8075                                }
8076                                let aname = self.stash_array_name_for_package(&decl.name);
8077                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
8078                            }
8079                            Sigil::Hash => {
8080                                let items = val.to_list();
8081                                let mut map = IndexMap::new();
8082                                let mut i = 0;
8083                                while i + 1 < items.len() {
8084                                    let k = items[i].to_string();
8085                                    let v = items[i + 1].clone();
8086                                    map.insert(k, v);
8087                                    i += 2;
8088                                }
8089                                self.scope.declare_hash_frozen(&decl.name, map, decl.frozen);
8090                            }
8091                        }
8092                    }
8093                }
8094                Ok(StrykeValue::UNDEF)
8095            }
8096            StmtKind::State(decls) => {
8097                // `state` variables persist across subroutine calls.
8098                // Key by source line + name for uniqueness.
8099                for decl in decls {
8100                    let state_key = format!("{}:{}", stmt.line, decl.name);
8101                    match decl.sigil {
8102                        Sigil::Scalar => {
8103                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
8104                                // Already initialized — declare with persisted value
8105                                self.scope.declare_scalar(&decl.name, prev);
8106                            } else {
8107                                // First encounter — evaluate initializer
8108                                let val = if let Some(init) = &decl.initializer {
8109                                    self.eval_expr(init)?
8110                                } else {
8111                                    StrykeValue::UNDEF
8112                                };
8113                                self.state_vars.insert(state_key.clone(), val.clone());
8114                                self.scope.declare_scalar(&decl.name, val);
8115                            }
8116                            // Register for save-back when scope pops
8117                            if let Some(frame) = self.state_bindings_stack.last_mut() {
8118                                frame.push((decl.name.clone(), state_key));
8119                            }
8120                        }
8121                        _ => {
8122                            // For arrays/hashes, fall back to simple my-like behavior
8123                            let val = if let Some(init) = &decl.initializer {
8124                                self.eval_expr(init)?
8125                            } else {
8126                                StrykeValue::UNDEF
8127                            };
8128                            match decl.sigil {
8129                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
8130                                Sigil::Hash => {
8131                                    let items = val.to_list();
8132                                    let mut map = IndexMap::new();
8133                                    let mut i = 0;
8134                                    while i + 1 < items.len() {
8135                                        map.insert(items[i].to_string(), items[i + 1].clone());
8136                                        i += 2;
8137                                    }
8138                                    self.scope.declare_hash(&decl.name, map);
8139                                }
8140                                _ => {}
8141                            }
8142                        }
8143                    }
8144                }
8145                Ok(StrykeValue::UNDEF)
8146            }
8147            StmtKind::Local(decls) => {
8148                if decls.len() > 1 && decls[0].initializer.is_some() {
8149                    let val = self.eval_expr_ctx(
8150                        decls[0].initializer.as_ref().unwrap(),
8151                        WantarrayCtx::List,
8152                    )?;
8153                    let items = val.to_list();
8154                    let mut idx = 0;
8155                    for decl in decls {
8156                        match decl.sigil {
8157                            Sigil::Scalar => {
8158                                let v = items.get(idx).cloned().unwrap_or(StrykeValue::UNDEF);
8159                                idx += 1;
8160                                self.scope.local_set_scalar(&decl.name, v)?;
8161                            }
8162                            Sigil::Array => {
8163                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8164                                idx = items.len();
8165                                self.scope.local_set_array(&decl.name, rest)?;
8166                            }
8167                            Sigil::Hash => {
8168                                let rest: Vec<StrykeValue> = items[idx..].to_vec();
8169                                idx = items.len();
8170                                if decl.name == "ENV" {
8171                                    self.materialize_env_if_needed();
8172                                }
8173                                let mut map = IndexMap::new();
8174                                let mut i = 0;
8175                                while i + 1 < rest.len() {
8176                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
8177                                    i += 2;
8178                                }
8179                                self.scope.local_set_hash(&decl.name, map)?;
8180                            }
8181                            Sigil::Typeglob => {
8182                                return Err(StrykeError::runtime(
8183                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
8184                                    stmt.line,
8185                                )
8186                                .into());
8187                            }
8188                        }
8189                    }
8190                    Ok(val)
8191                } else {
8192                    let mut last_val = StrykeValue::UNDEF;
8193                    for decl in decls {
8194                        let val = if let Some(init) = &decl.initializer {
8195                            let ctx = match decl.sigil {
8196                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
8197                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
8198                            };
8199                            self.eval_expr_ctx(init, ctx)?
8200                        } else {
8201                            StrykeValue::UNDEF
8202                        };
8203                        last_val = val.clone();
8204                        match decl.sigil {
8205                            Sigil::Typeglob => {
8206                                let old = self.glob_handle_alias.remove(&decl.name);
8207                                if let Some(frame) = self.glob_restore_frames.last_mut() {
8208                                    frame.push((decl.name.clone(), old));
8209                                }
8210                                if let Some(init) = &decl.initializer {
8211                                    if let ExprKind::Typeglob(rhs) = &init.kind {
8212                                        self.glob_handle_alias
8213                                            .insert(decl.name.clone(), rhs.clone());
8214                                    } else {
8215                                        return Err(StrykeError::runtime(
8216                                            "local *GLOB = *OTHER — right side must be a typeglob",
8217                                            stmt.line,
8218                                        )
8219                                        .into());
8220                                    }
8221                                }
8222                            }
8223                            Sigil::Scalar => {
8224                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
8225                                // must update the interpreter's backing field too — these are
8226                                // not stored in `Scope`. Save the prior value for restoration
8227                                // on `scope_pop_hook` so the block-exit restore is visible to
8228                                // print/I/O code.
8229                                if Self::is_special_scalar_name_for_set(&decl.name) {
8230                                    let old = self.get_special_var(&decl.name);
8231                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
8232                                    {
8233                                        frame.push((decl.name.clone(), old));
8234                                    }
8235                                    self.set_special_var(&decl.name, &val)
8236                                        .map_err(|e| e.at_line(stmt.line))?;
8237                                }
8238                                self.scope.local_set_scalar(&decl.name, val)?;
8239                            }
8240                            Sigil::Array => {
8241                                self.scope.local_set_array(&decl.name, val.to_list())?;
8242                            }
8243                            Sigil::Hash => {
8244                                if decl.name == "ENV" {
8245                                    self.materialize_env_if_needed();
8246                                }
8247                                let items = val.to_list();
8248                                let mut map = IndexMap::new();
8249                                let mut i = 0;
8250                                while i + 1 < items.len() {
8251                                    let k = items[i].to_string();
8252                                    let v = items[i + 1].clone();
8253                                    map.insert(k, v);
8254                                    i += 2;
8255                                }
8256                                self.scope.local_set_hash(&decl.name, map)?;
8257                            }
8258                        }
8259                    }
8260                    Ok(last_val)
8261                }
8262            }
8263            StmtKind::LocalExpr {
8264                target,
8265                initializer,
8266            } => {
8267                let rhs_name = |init: &Expr| -> StrykeResult<Option<String>> {
8268                    match &init.kind {
8269                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
8270                        _ => Err(StrykeError::runtime(
8271                            "local *GLOB = *OTHER — right side must be a typeglob",
8272                            stmt.line,
8273                        )),
8274                    }
8275                };
8276                match &target.kind {
8277                    ExprKind::Typeglob(name) => {
8278                        let rhs = if let Some(init) = initializer {
8279                            rhs_name(init)?
8280                        } else {
8281                            None
8282                        };
8283                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
8284                        return Ok(StrykeValue::UNDEF);
8285                    }
8286                    ExprKind::Deref {
8287                        expr,
8288                        kind: Sigil::Typeglob,
8289                    } => {
8290                        let lhs = self.eval_expr(expr)?.to_string();
8291                        let rhs = if let Some(init) = initializer {
8292                            rhs_name(init)?
8293                        } else {
8294                            None
8295                        };
8296                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8297                        return Ok(StrykeValue::UNDEF);
8298                    }
8299                    ExprKind::TypeglobExpr(e) => {
8300                        let lhs = self.eval_expr(e)?.to_string();
8301                        let rhs = if let Some(init) = initializer {
8302                            rhs_name(init)?
8303                        } else {
8304                            None
8305                        };
8306                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8307                        return Ok(StrykeValue::UNDEF);
8308                    }
8309                    _ => {}
8310                }
8311                let val = if let Some(init) = initializer {
8312                    let ctx = match &target.kind {
8313                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
8314                        _ => WantarrayCtx::Scalar,
8315                    };
8316                    self.eval_expr_ctx(init, ctx)?
8317                } else {
8318                    StrykeValue::UNDEF
8319                };
8320                match &target.kind {
8321                    ExprKind::ScalarVar(name) => {
8322                        // `local $X = …` on a special var — see twin block in
8323                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
8324                        if Self::is_special_scalar_name_for_set(name) {
8325                            let old = self.get_special_var(name);
8326                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
8327                                frame.push((name.clone(), old));
8328                            }
8329                            self.set_special_var(name, &val)
8330                                .map_err(|e| e.at_line(stmt.line))?;
8331                        }
8332                        self.scope.local_set_scalar(name, val.clone())?;
8333                    }
8334                    ExprKind::ArrayVar(name) => {
8335                        self.scope.local_set_array(name, val.to_list())?;
8336                    }
8337                    ExprKind::HashVar(name) => {
8338                        if name == "ENV" {
8339                            self.materialize_env_if_needed();
8340                        }
8341                        let items = val.to_list();
8342                        let mut map = IndexMap::new();
8343                        let mut i = 0;
8344                        while i + 1 < items.len() {
8345                            map.insert(items[i].to_string(), items[i + 1].clone());
8346                            i += 2;
8347                        }
8348                        self.scope.local_set_hash(name, map)?;
8349                    }
8350                    ExprKind::HashElement { hash, key } => {
8351                        let ks = self.eval_expr(key)?.to_string();
8352                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
8353                    }
8354                    ExprKind::ArrayElement { array, index } => {
8355                        self.check_strict_array_var(array, stmt.line)?;
8356                        let aname = self.stash_array_name_for_package(array);
8357                        let idx = self.eval_expr(index)?.to_int();
8358                        self.scope
8359                            .local_set_array_element(&aname, idx, val.clone())?;
8360                    }
8361                    _ => {
8362                        return Err(StrykeError::runtime(
8363                            format!(
8364                                "local on this lvalue is not supported yet ({:?})",
8365                                target.kind
8366                            ),
8367                            stmt.line,
8368                        )
8369                        .into());
8370                    }
8371                }
8372                Ok(val)
8373            }
8374            StmtKind::MySync(decls) => {
8375                for decl in decls {
8376                    let val = if let Some(init) = &decl.initializer {
8377                        self.eval_expr(init)?
8378                    } else {
8379                        StrykeValue::UNDEF
8380                    };
8381                    match decl.sigil {
8382                        Sigil::Typeglob => {
8383                            return Err(StrykeError::runtime(
8384                                "`mysync` does not support typeglob variables",
8385                                stmt.line,
8386                            )
8387                            .into());
8388                        }
8389                        Sigil::Scalar => {
8390                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
8391                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
8392                            let stored = if val.is_mysync_deque_or_heap() {
8393                                val
8394                            } else {
8395                                StrykeValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(
8396                                    val,
8397                                )))
8398                            };
8399                            self.scope.declare_scalar(&decl.name, stored);
8400                        }
8401                        Sigil::Array => {
8402                            self.scope.declare_atomic_array(&decl.name, val.to_list());
8403                        }
8404                        Sigil::Hash => {
8405                            let items = val.to_list();
8406                            let mut map = IndexMap::new();
8407                            let mut i = 0;
8408                            while i + 1 < items.len() {
8409                                map.insert(items[i].to_string(), items[i + 1].clone());
8410                                i += 2;
8411                            }
8412                            self.scope.declare_atomic_hash(&decl.name, map);
8413                        }
8414                    }
8415                }
8416                Ok(StrykeValue::UNDEF)
8417            }
8418            StmtKind::OurSync(decls) => {
8419                // The fan/pmap/pfor workers execute closure bodies via this tree-walker
8420                // (`exec_block_no_scope`), not the bytecode VM — so `oursync` MUST register
8421                // each declared name in `english_lexical_scalars` + `our_lexical_scalars`
8422                // for `tree_scalar_storage_name` to rewrite later `$x` reads to `Pkg::x`.
8423                // Without this, worker `$x` reads see UNDEF (the qualified key isn't
8424                // queried) even though capture/restore brought the cell across.
8425                for decl in decls {
8426                    let val = if let Some(init) = &decl.initializer {
8427                        self.eval_expr(init)?
8428                    } else {
8429                        StrykeValue::UNDEF
8430                    };
8431                    match decl.sigil {
8432                        Sigil::Typeglob => {
8433                            return Err(StrykeError::runtime(
8434                                "`oursync` does not support typeglob variables",
8435                                stmt.line,
8436                            )
8437                            .into());
8438                        }
8439                        Sigil::Scalar => {
8440                            let stash = self.stash_scalar_name_for_package(&decl.name);
8441                            let stored = if val.is_mysync_deque_or_heap() {
8442                                val
8443                            } else {
8444                                StrykeValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(
8445                                    val,
8446                                )))
8447                            };
8448                            self.scope.declare_scalar(&stash, stored);
8449                            self.english_note_lexical_scalar(&decl.name);
8450                            self.note_our_scalar(&decl.name);
8451                        }
8452                        Sigil::Array => {
8453                            let stash = self.stash_array_name_for_package(&decl.name);
8454                            self.scope.declare_atomic_array(&stash, val.to_list());
8455                            self.english_note_lexical_scalar(&decl.name);
8456                            self.note_our_scalar(&decl.name);
8457                        }
8458                        Sigil::Hash => {
8459                            let items = val.to_list();
8460                            let mut map = IndexMap::new();
8461                            let mut i = 0;
8462                            while i + 1 < items.len() {
8463                                map.insert(items[i].to_string(), items[i + 1].clone());
8464                                i += 2;
8465                            }
8466                            // Match `our %h` convention: bare hash name (existing
8467                            // cross-package quirk).
8468                            self.scope.declare_atomic_hash(&decl.name, map);
8469                            self.english_note_lexical_scalar(&decl.name);
8470                            self.note_our_scalar(&decl.name);
8471                        }
8472                    }
8473                }
8474                Ok(StrykeValue::UNDEF)
8475            }
8476            StmtKind::Package { name } => {
8477                // Minimal package support — just set a variable
8478                let _ = self
8479                    .scope
8480                    .set_scalar("__PACKAGE__", StrykeValue::string(name.clone()));
8481                Ok(StrykeValue::UNDEF)
8482            }
8483            StmtKind::UsePerlVersion { .. } => Ok(StrykeValue::UNDEF),
8484            StmtKind::Use { .. } => {
8485                // Handled in `prepare_program_top_level` before BEGIN / main.
8486                Ok(StrykeValue::UNDEF)
8487            }
8488            StmtKind::UseOverload { pairs } => {
8489                self.install_use_overload_pairs(pairs);
8490                Ok(StrykeValue::UNDEF)
8491            }
8492            StmtKind::No { .. } => {
8493                // Handled in `prepare_program_top_level` (same phase as `use`).
8494                Ok(StrykeValue::UNDEF)
8495            }
8496            StmtKind::Return(val) => {
8497                let v = if let Some(e) = val {
8498                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
8499                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
8500                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
8501                    self.eval_expr_ctx(e, self.wantarray_kind)?
8502                } else {
8503                    StrykeValue::UNDEF
8504                };
8505                Err(Flow::Return(v).into())
8506            }
8507            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
8508            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
8509            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
8510            StmtKind::Block(block) => self.exec_block(block),
8511            StmtKind::Begin(_)
8512            | StmtKind::UnitCheck(_)
8513            | StmtKind::Check(_)
8514            | StmtKind::Init(_)
8515            | StmtKind::End(_) => Ok(StrykeValue::UNDEF),
8516            StmtKind::Empty => Ok(StrykeValue::UNDEF),
8517            StmtKind::Goto { target } => {
8518                // goto &sub — tail call
8519                if let ExprKind::SubroutineRef(name) = &target.kind {
8520                    return Err(Flow::GotoSub(name.clone()).into());
8521                }
8522                Err(StrykeError::runtime("goto reached outside goto-aware block", stmt.line).into())
8523            }
8524            StmtKind::EvalTimeout { timeout, body } => {
8525                let secs = self.eval_expr(timeout)?.to_number();
8526                self.eval_timeout_block(body, secs, stmt.line)
8527            }
8528            StmtKind::Tie {
8529                target,
8530                class,
8531                args,
8532            } => {
8533                let kind = match &target {
8534                    TieTarget::Scalar(_) => 0u8,
8535                    TieTarget::Array(_) => 1u8,
8536                    TieTarget::Hash(_) => 2u8,
8537                };
8538                let name = match &target {
8539                    TieTarget::Scalar(s) => s.as_str(),
8540                    TieTarget::Array(a) => a.as_str(),
8541                    TieTarget::Hash(h) => h.as_str(),
8542                };
8543                let mut vals = vec![self.eval_expr(class)?];
8544                for a in args {
8545                    vals.push(self.eval_expr(a)?);
8546                }
8547                self.tie_execute(kind, name, vals, stmt.line)
8548                    .map_err(Into::into)
8549            }
8550            StmtKind::TryCatch {
8551                try_block,
8552                catch_var,
8553                catch_block,
8554                finally_block,
8555            } => match self.exec_block(try_block) {
8556                Ok(v) => {
8557                    if let Some(fb) = finally_block {
8558                        self.exec_block(fb)?;
8559                    }
8560                    Ok(v)
8561                }
8562                Err(FlowOrError::Error(e)) => {
8563                    if matches!(e.kind, ErrorKind::Exit(_)) {
8564                        return Err(FlowOrError::Error(e));
8565                    }
8566                    self.scope_push_hook();
8567                    self.scope
8568                        .declare_scalar(catch_var, StrykeValue::string(e.to_string()));
8569                    self.english_note_lexical_scalar(catch_var);
8570                    let r = self.exec_block(catch_block);
8571                    self.scope_pop_hook();
8572                    if let Some(fb) = finally_block {
8573                        self.exec_block(fb)?;
8574                    }
8575                    r
8576                }
8577                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
8578            },
8579            StmtKind::Given { topic, body } => self.exec_given(topic, body),
8580            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(StrykeError::runtime(
8581                "when/default may only appear inside a given block",
8582                stmt.line,
8583            )
8584            .into()),
8585            StmtKind::FormatDecl { .. } => {
8586                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
8587                Ok(StrykeValue::UNDEF)
8588            }
8589            StmtKind::AdviceDecl {
8590                kind,
8591                pattern,
8592                body,
8593            } => {
8594                // Tree-walker registration path: only reached if bytecode compilation
8595                // bailed out (extremely rare — production programs always run through
8596                // the VM). The body has no compiled bytecode region, so we tag it with
8597                // `u16::MAX` and `dispatch_with_advice` will refuse to fire it rather
8598                // than silently fall back to the AST tree-walker.
8599                let id = self.next_intercept_id;
8600                self.next_intercept_id = id.saturating_add(1);
8601                self.intercepts.push(crate::aop::Intercept {
8602                    id,
8603                    kind: *kind,
8604                    pattern: pattern.clone(),
8605                    body: body.clone(),
8606                    body_block_idx: u16::MAX,
8607                });
8608                Ok(StrykeValue::UNDEF)
8609            }
8610            StmtKind::Continue(block) => self.exec_block_smart(block),
8611        }
8612    }
8613
8614    #[inline]
8615    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
8616        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
8617    }
8618
8619    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
8620    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
8621    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
8622    pub(crate) fn scalar_compound_assign_scalar_target(
8623        &mut self,
8624        name: &str,
8625        op: BinOp,
8626        rhs: StrykeValue,
8627    ) -> Result<StrykeValue, StrykeError> {
8628        if op == BinOp::Concat {
8629            return self.scope.scalar_concat_inplace(name, &rhs);
8630        }
8631        self.scope
8632            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs))
8633    }
8634
8635    fn compound_scalar_binop(old: &StrykeValue, op: BinOp, rhs: &StrykeValue) -> StrykeValue {
8636        match op {
8637            BinOp::Add => {
8638                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8639                    StrykeValue::integer(a.wrapping_add(b))
8640                } else {
8641                    StrykeValue::float(old.to_number() + rhs.to_number())
8642                }
8643            }
8644            BinOp::Sub => {
8645                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8646                    StrykeValue::integer(a.wrapping_sub(b))
8647                } else {
8648                    StrykeValue::float(old.to_number() - rhs.to_number())
8649                }
8650            }
8651            BinOp::Mul => {
8652                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8653                    StrykeValue::integer(a.wrapping_mul(b))
8654                } else {
8655                    StrykeValue::float(old.to_number() * rhs.to_number())
8656                }
8657            }
8658            BinOp::BitAnd => {
8659                if let Some(s) = crate::value::set_intersection(old, rhs) {
8660                    s
8661                } else {
8662                    StrykeValue::integer(old.to_int() & rhs.to_int())
8663                }
8664            }
8665            BinOp::BitOr => {
8666                if let Some(s) = crate::value::set_union(old, rhs) {
8667                    s
8668                } else {
8669                    StrykeValue::integer(old.to_int() | rhs.to_int())
8670                }
8671            }
8672            BinOp::BitXor => StrykeValue::integer(old.to_int() ^ rhs.to_int()),
8673            BinOp::ShiftLeft => StrykeValue::integer(old.to_int() << rhs.to_int()),
8674            BinOp::ShiftRight => StrykeValue::integer(old.to_int() >> rhs.to_int()),
8675            BinOp::Div => StrykeValue::float(old.to_number() / rhs.to_number()),
8676            BinOp::Mod => {
8677                // Return 0 on b==0 silently — this helper is the
8678                // `$x OP= rhs` atomic-mutate path which can't propagate
8679                // errors. The non-compound `%` path (eval_binop) raises
8680                // `ErrorKind::DivisionByZero`.
8681                let b = rhs.to_int();
8682                if b == 0 {
8683                    StrykeValue::integer(0)
8684                } else {
8685                    StrykeValue::integer(crate::value::perl_mod_i64(old.to_int(), b))
8686                }
8687            }
8688            BinOp::Pow => StrykeValue::float(old.to_number().powf(rhs.to_number())),
8689            BinOp::LogOr => {
8690                if old.is_true() {
8691                    old.clone()
8692                } else {
8693                    rhs.clone()
8694                }
8695            }
8696            BinOp::DefinedOr => {
8697                if !old.is_undef() {
8698                    old.clone()
8699                } else {
8700                    rhs.clone()
8701                }
8702            }
8703            BinOp::LogAnd => {
8704                if old.is_true() {
8705                    rhs.clone()
8706                } else {
8707                    old.clone()
8708                }
8709            }
8710            _ => StrykeValue::float(old.to_number() + rhs.to_number()),
8711        }
8712    }
8713
8714    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
8715    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
8716    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
8717    fn eval_hash_slice_key_components(
8718        &mut self,
8719        key_expr: &Expr,
8720    ) -> Result<Vec<String>, FlowOrError> {
8721        let v = if matches!(
8722            key_expr.kind,
8723            ExprKind::Range { .. } | ExprKind::SliceRange { .. }
8724        ) {
8725            self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
8726        } else {
8727            self.eval_expr(key_expr)?
8728        };
8729        if let Some(vv) = v.as_array_vec() {
8730            Ok(vv.iter().map(|x| x.to_string()).collect())
8731        } else {
8732            Ok(vec![v.to_string()])
8733        }
8734    }
8735
8736    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
8737    pub(crate) fn symbolic_deref(
8738        &mut self,
8739        val: StrykeValue,
8740        kind: Sigil,
8741        line: usize,
8742    ) -> ExecResult {
8743        match kind {
8744            Sigil::Scalar => {
8745                if let Some(name) = val.as_scalar_binding_name() {
8746                    return Ok(self.get_special_var(&name));
8747                }
8748                if let Some(r) = val.as_scalar_ref() {
8749                    return Ok(r.read().clone());
8750                }
8751                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
8752                if let Some(r) = val.as_array_ref() {
8753                    return Ok(StrykeValue::array(r.read().clone()));
8754                }
8755                if let Some(name) = val.as_array_binding_name() {
8756                    return Ok(StrykeValue::array(self.scope.get_array(&name)));
8757                }
8758                if let Some(r) = val.as_hash_ref() {
8759                    return Ok(StrykeValue::hash(r.read().clone()));
8760                }
8761                if let Some(name) = val.as_hash_binding_name() {
8762                    self.touch_env_hash(&name);
8763                    return Ok(StrykeValue::hash(self.scope.get_hash(&name)));
8764                }
8765                if let Some(s) = val.as_str() {
8766                    if self.strict_refs {
8767                        return Err(StrykeError::runtime(
8768                            format!(
8769                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
8770                                s
8771                            ),
8772                            line,
8773                        )
8774                        .into());
8775                    }
8776                    return Ok(self.get_special_var(&s));
8777                }
8778                Err(StrykeError::runtime("Can't dereference non-reference as scalar", line).into())
8779            }
8780            Sigil::Array => {
8781                if let Some(r) = val.as_array_ref() {
8782                    return Ok(StrykeValue::array(r.read().clone()));
8783                }
8784                if let Some(name) = val.as_array_binding_name() {
8785                    return Ok(StrykeValue::array(self.scope.get_array(&name)));
8786                }
8787                if val.is_undef() {
8788                    if self.strict_refs {
8789                        return Err(StrykeError::runtime(
8790                            "Can't use an undefined value as an ARRAY reference",
8791                            line,
8792                        )
8793                        .into());
8794                    }
8795                    return Ok(StrykeValue::array(vec![]));
8796                }
8797                // Plain primitive scalar (int, float, string): under no-strict, perl
8798                // treats this as a symbolic ref `@{$val_as_string}` and silently
8799                // returns the (likely empty) named array. Under strict refs, error.
8800                // Heap objects (Pair, Generator, blessed-non-ref) fall through to
8801                // the dereference-error so we don't silently swallow real bugs.
8802                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
8803                    let s = val.to_string();
8804                    if self.strict_refs {
8805                        return Err(StrykeError::runtime(
8806                            format!(
8807                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8808                                s
8809                            ),
8810                            line,
8811                        )
8812                        .into());
8813                    }
8814                    return Ok(StrykeValue::array(self.scope.get_array(&s)));
8815                }
8816                Err(StrykeError::runtime("Can't dereference non-reference as array", line).into())
8817            }
8818            Sigil::Hash => {
8819                if let Some(r) = val.as_hash_ref() {
8820                    return Ok(StrykeValue::hash(r.read().clone()));
8821                }
8822                if let Some(name) = val.as_hash_binding_name() {
8823                    self.touch_env_hash(&name);
8824                    return Ok(StrykeValue::hash(self.scope.get_hash(&name)));
8825                }
8826                // Stryke `class C { ... }` instances answer to `%$obj` by
8827                // flattening their field name/value pairs — the same shape
8828                // a Perl-style blessed hashref produces. This keeps the
8829                // canonical introspection idiom (`keys %$obj`, `values
8830                // %$obj`) working for stryke-native OO too. Order matches
8831                // the inheritance-collected field order from
8832                // `collect_class_fields_full`.
8833                if let Some(c) = val.as_class_inst() {
8834                    let all_fields = self.collect_class_fields_full(&c.def);
8835                    let values = c.get_values();
8836                    let mut map = IndexMap::new();
8837                    for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
8838                        if let Some(v) = values.get(i) {
8839                            map.insert(name.clone(), v.clone());
8840                        }
8841                    }
8842                    return Ok(StrykeValue::hash(map));
8843                }
8844                // Same for stryke `struct S { ... }` instances — keep them
8845                // introspectable through the Perl-style hash-deref idiom.
8846                if let Some(s) = val.as_struct_inst() {
8847                    let values = s.get_values();
8848                    let mut map = IndexMap::new();
8849                    for (i, field) in s.def.fields.iter().enumerate() {
8850                        if let Some(v) = values.get(i) {
8851                            map.insert(field.name.clone(), v.clone());
8852                        }
8853                    }
8854                    return Ok(StrykeValue::hash(map));
8855                }
8856                // Blessed-ref escape hatch: when the inner data is a hash,
8857                // unwrap and treat the deref as if it targeted the inner
8858                // hash. Old Perl OO code that wrote `%$self` on a blessed
8859                // hashref keeps working without an extra unbless step.
8860                if let Some(b) = val.as_blessed_ref() {
8861                    let inner = b.data.read().clone();
8862                    if let Some(r) = inner.as_hash_ref() {
8863                        return Ok(StrykeValue::hash(r.read().clone()));
8864                    }
8865                    if let Some(h) = inner.as_hash_map() {
8866                        return Ok(StrykeValue::hash(h));
8867                    }
8868                }
8869                if val.is_undef() {
8870                    if self.strict_refs {
8871                        return Err(StrykeError::runtime(
8872                            "Can't use an undefined value as a HASH reference",
8873                            line,
8874                        )
8875                        .into());
8876                    }
8877                    return Ok(StrykeValue::hash(IndexMap::new()));
8878                }
8879                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
8880                    let s = val.to_string();
8881                    if self.strict_refs {
8882                        return Err(StrykeError::runtime(
8883                            format!(
8884                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8885                                s
8886                            ),
8887                            line,
8888                        )
8889                        .into());
8890                    }
8891                    self.touch_env_hash(&s);
8892                    return Ok(StrykeValue::hash(self.scope.get_hash(&s)));
8893                }
8894                Err(StrykeError::runtime("Can't dereference non-reference as hash", line).into())
8895            }
8896            Sigil::Typeglob => {
8897                if let Some(s) = val.as_str() {
8898                    return Ok(StrykeValue::string(self.resolve_io_handle_name(&s)));
8899                }
8900                Err(
8901                    StrykeError::runtime("Can't dereference non-reference as typeglob", line)
8902                        .into(),
8903                )
8904            }
8905        }
8906    }
8907
8908    /// `qq` list join expects a plain array; if a bare [`StrykeValue::array_ref`] reaches join, peel
8909    /// one level so elements stringify like Perl (`"@$r"`).
8910    #[inline]
8911    pub(crate) fn peel_array_ref_for_list_join(&self, v: StrykeValue) -> StrykeValue {
8912        if let Some(r) = v.as_array_ref() {
8913            return StrykeValue::array(r.read().clone());
8914        }
8915        v
8916    }
8917
8918    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
8919    pub(crate) fn make_array_ref_alias(&self, val: StrykeValue, line: usize) -> ExecResult {
8920        if let Some(a) = val.as_array_ref() {
8921            return Ok(StrykeValue::array_ref(Arc::clone(&a)));
8922        }
8923        if let Some(name) = val.as_array_binding_name() {
8924            return Ok(StrykeValue::array_binding_ref(name));
8925        }
8926        if let Some(s) = val.as_str() {
8927            if self.strict_refs {
8928                return Err(StrykeError::runtime(
8929                    format!(
8930                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8931                        s
8932                    ),
8933                    line,
8934                )
8935                .into());
8936            }
8937            return Ok(StrykeValue::array_binding_ref(s.to_string()));
8938        }
8939        if let Some(r) = val.as_scalar_ref() {
8940            let inner = r.read().clone();
8941            return self.make_array_ref_alias(inner, line);
8942        }
8943        Err(StrykeError::runtime("Can't make array reference from value", line).into())
8944    }
8945
8946    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
8947    pub(crate) fn make_hash_ref_alias(&self, val: StrykeValue, line: usize) -> ExecResult {
8948        if let Some(h) = val.as_hash_ref() {
8949            return Ok(StrykeValue::hash_ref(Arc::clone(&h)));
8950        }
8951        if let Some(name) = val.as_hash_binding_name() {
8952            return Ok(StrykeValue::hash_binding_ref(name));
8953        }
8954        if let Some(s) = val.as_str() {
8955            if self.strict_refs {
8956                return Err(StrykeError::runtime(
8957                    format!(
8958                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8959                        s
8960                    ),
8961                    line,
8962                )
8963                .into());
8964            }
8965            return Ok(StrykeValue::hash_binding_ref(s.to_string()));
8966        }
8967        if let Some(r) = val.as_scalar_ref() {
8968            let inner = r.read().clone();
8969            return self.make_hash_ref_alias(inner, line);
8970        }
8971        Err(StrykeError::runtime("Can't make hash reference from value", line).into())
8972    }
8973
8974    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
8975    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
8976    pub(crate) fn process_case_escapes(s: &str) -> String {
8977        // Quick check: if no backslash, nothing to do
8978        if !s.contains('\\') {
8979            return s.to_string();
8980        }
8981        let mut result = String::with_capacity(s.len());
8982        let mut chars = s.chars().peekable();
8983        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
8984        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
8985
8986        while let Some(c) = chars.next() {
8987            if c == '\\' {
8988                match chars.peek() {
8989                    Some(&'U') => {
8990                        chars.next();
8991                        mode = Some('U');
8992                        continue;
8993                    }
8994                    Some(&'L') => {
8995                        chars.next();
8996                        mode = Some('L');
8997                        continue;
8998                    }
8999                    Some(&'Q') => {
9000                        chars.next();
9001                        mode = Some('Q');
9002                        continue;
9003                    }
9004                    Some(&'E') => {
9005                        chars.next();
9006                        mode = None;
9007                        next_char_mod = None;
9008                        continue;
9009                    }
9010                    Some(&'u') => {
9011                        chars.next();
9012                        next_char_mod = Some('u');
9013                        continue;
9014                    }
9015                    Some(&'l') => {
9016                        chars.next();
9017                        next_char_mod = Some('l');
9018                        continue;
9019                    }
9020                    _ => {}
9021                }
9022            }
9023
9024            let ch = c;
9025
9026            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
9027            if let Some(m) = next_char_mod.take() {
9028                let transformed = match m {
9029                    'u' => ch.to_uppercase().next().unwrap_or(ch),
9030                    'l' => ch.to_lowercase().next().unwrap_or(ch),
9031                    _ => ch,
9032                };
9033                result.push(transformed);
9034            } else {
9035                // Apply ongoing mode
9036                match mode {
9037                    Some('U') => {
9038                        for uc in ch.to_uppercase() {
9039                            result.push(uc);
9040                        }
9041                    }
9042                    Some('L') => {
9043                        for lc in ch.to_lowercase() {
9044                            result.push(lc);
9045                        }
9046                    }
9047                    Some('Q') => {
9048                        if !ch.is_ascii_alphanumeric() && ch != '_' {
9049                            result.push('\\');
9050                        }
9051                        result.push(ch);
9052                    }
9053                    None | Some(_) => {
9054                        result.push(ch);
9055                    }
9056                }
9057            }
9058        }
9059        result
9060    }
9061
9062    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
9063        let line = expr.line;
9064        match &expr.kind {
9065            ExprKind::Integer(n) => Ok(StrykeValue::integer(*n)),
9066            ExprKind::Float(f) => Ok(StrykeValue::float(*f)),
9067            ExprKind::String(s) => {
9068                let processed = Self::process_case_escapes(s);
9069                Ok(StrykeValue::string(processed))
9070            }
9071            ExprKind::Bareword(s) => {
9072                if s == "__PACKAGE__" {
9073                    return Ok(StrykeValue::string(self.current_package()));
9074                }
9075                if let Some(sub) = self.resolve_sub_by_name(s) {
9076                    return self.call_sub(&sub, vec![], ctx, line);
9077                }
9078                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
9079                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
9080                    return r.map_err(Into::into);
9081                }
9082                Ok(StrykeValue::string(s.clone()))
9083            }
9084            ExprKind::Undef => Ok(StrykeValue::UNDEF),
9085            ExprKind::MagicConst(MagicConstKind::File) => {
9086                Ok(StrykeValue::string(self.file.clone()))
9087            }
9088            ExprKind::MagicConst(MagicConstKind::Line) => {
9089                Ok(StrykeValue::integer(expr.line as i64))
9090            }
9091            ExprKind::MagicConst(MagicConstKind::Sub) => {
9092                if let Some(sub) = self.current_sub_stack.last().cloned() {
9093                    Ok(StrykeValue::code_ref(sub))
9094                } else {
9095                    Ok(StrykeValue::UNDEF)
9096                }
9097            }
9098            ExprKind::Regex(pattern, flags) => {
9099                if ctx == WantarrayCtx::Void {
9100                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
9101                    let topic = self.scope.get_scalar("_");
9102                    let s = topic.to_string();
9103                    self.regex_match_execute(s, pattern, flags, false, "_", line)
9104                } else {
9105                    let re = self.compile_regex(pattern, flags, line)?;
9106                    Ok(StrykeValue::regex(re, pattern.clone(), flags.clone()))
9107                }
9108            }
9109            ExprKind::QW(words) => Ok(StrykeValue::array(
9110                words
9111                    .iter()
9112                    .map(|w| StrykeValue::string(w.clone()))
9113                    .collect(),
9114            )),
9115
9116            // Interpolated strings
9117            ExprKind::InterpolatedString(parts) => {
9118                let mut raw_result = String::new();
9119                for part in parts {
9120                    match part {
9121                        StringPart::Literal(s) => raw_result.push_str(s),
9122                        StringPart::ScalarVar(name) => {
9123                            self.check_strict_scalar_var(name, line)?;
9124                            let val = self.get_special_var(name);
9125                            let s = self.stringify_value(val, line)?;
9126                            raw_result.push_str(&s);
9127                        }
9128                        StringPart::ArrayVar(name) => {
9129                            self.check_strict_array_var(name, line)?;
9130                            let aname = self.stash_array_name_for_package(name);
9131                            let arr = self.scope.get_array(&aname);
9132                            let mut parts = Vec::with_capacity(arr.len());
9133                            for v in &arr {
9134                                parts.push(self.stringify_value(v.clone(), line)?);
9135                            }
9136                            let sep = self.list_separator.clone();
9137                            raw_result.push_str(&parts.join(&sep));
9138                        }
9139                        StringPart::Expr(e) => {
9140                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
9141                                self.check_strict_array_var(array, line)?;
9142                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9143                                let val = self.peel_array_ref_for_list_join(val);
9144                                let list = val.to_list();
9145                                let sep = self.list_separator.clone();
9146                                let mut parts = Vec::with_capacity(list.len());
9147                                for v in list {
9148                                    parts.push(self.stringify_value(v, line)?);
9149                                }
9150                                raw_result.push_str(&parts.join(&sep));
9151                            } else if let ExprKind::Deref {
9152                                kind: Sigil::Array, ..
9153                            } = &e.kind
9154                            {
9155                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9156                                let val = self.peel_array_ref_for_list_join(val);
9157                                let list = val.to_list();
9158                                let sep = self.list_separator.clone();
9159                                let mut parts = Vec::with_capacity(list.len());
9160                                for v in list {
9161                                    parts.push(self.stringify_value(v, line)?);
9162                                }
9163                                raw_result.push_str(&parts.join(&sep));
9164                            } else {
9165                                let val = self.eval_expr(e)?;
9166                                let s = self.stringify_value(val, line)?;
9167                                raw_result.push_str(&s);
9168                            }
9169                        }
9170                    }
9171                }
9172                let result = Self::process_case_escapes(&raw_result);
9173                Ok(StrykeValue::string(result))
9174            }
9175
9176            // Variables
9177            ExprKind::ScalarVar(name) => {
9178                self.check_strict_scalar_var(name, line)?;
9179                let stor = self.tree_scalar_storage_name(name);
9180                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
9181                    let class = obj
9182                        .as_blessed_ref()
9183                        .map(|b| b.class.clone())
9184                        .unwrap_or_default();
9185                    let full = format!("{}::FETCH", class);
9186                    if let Some(sub) = self.subs.get(&full).cloned() {
9187                        return self.call_sub(&sub, vec![obj], ctx, line);
9188                    }
9189                }
9190                Ok(self.get_special_var(&stor))
9191            }
9192            ExprKind::ArrayVar(name) => {
9193                self.check_strict_array_var(name, line)?;
9194                let aname = self.stash_array_name_for_package(name);
9195                let arr = self.scope.get_array(&aname);
9196                if ctx == WantarrayCtx::List {
9197                    Ok(StrykeValue::array(arr))
9198                } else {
9199                    Ok(StrykeValue::integer(arr.len() as i64))
9200                }
9201            }
9202            ExprKind::HashVar(name) => {
9203                self.check_strict_hash_var(name, line)?;
9204                self.touch_env_hash(name);
9205                let h = self.scope.get_hash(name);
9206                let pv = StrykeValue::hash(h);
9207                if ctx == WantarrayCtx::List {
9208                    Ok(pv)
9209                } else {
9210                    Ok(pv.scalar_context())
9211                }
9212            }
9213            ExprKind::Typeglob(name) => {
9214                let n = self.resolve_io_handle_name(name);
9215                Ok(StrykeValue::string(n))
9216            }
9217            ExprKind::TypeglobExpr(e) => {
9218                let name = self.eval_expr(e)?.to_string();
9219                let n = self.resolve_io_handle_name(&name);
9220                Ok(StrykeValue::string(n))
9221            }
9222            ExprKind::ArrayElement { array, index } => {
9223                // Stryke string-index sugar: bareword `_[N]` parses to an
9224                // ArrayElement with a `__topicstr__N` synthetic name. Strip
9225                // the prefix and treat as substr-of-topic. Differs from
9226                // `$_[N]` (sigil form) which keeps Perl's @_-access.
9227                if let Some(real) = array.strip_prefix("__topicstr__") {
9228                    let s = self.scope.get_scalar(real).to_string();
9229                    if let ExprKind::Range {
9230                        from,
9231                        to,
9232                        exclusive,
9233                        step,
9234                    } = &index.kind
9235                    {
9236                        let n = s.chars().count() as i64;
9237                        let mut from_i = self.eval_expr(from)?.to_int();
9238                        let mut to_i = self.eval_expr(to)?.to_int();
9239                        let step_i = match step {
9240                            Some(e) => self.eval_expr(e)?.to_int(),
9241                            None => 1,
9242                        };
9243                        if from_i < 0 {
9244                            from_i += n
9245                        }
9246                        if to_i < 0 {
9247                            to_i += n
9248                        }
9249                        if *exclusive {
9250                            to_i -= 1
9251                        }
9252                        let chars: Vec<char> = s.chars().collect();
9253                        let mut out = String::new();
9254                        if step_i > 0 {
9255                            let mut i = from_i;
9256                            while i <= to_i && i < n {
9257                                if i >= 0 {
9258                                    out.push(chars[i as usize]);
9259                                }
9260                                i += step_i;
9261                            }
9262                        } else if step_i < 0 {
9263                            let mut i = from_i;
9264                            while i >= to_i && i >= 0 {
9265                                if i < n {
9266                                    out.push(chars[i as usize]);
9267                                }
9268                                i += step_i;
9269                            }
9270                        }
9271                        return Ok(StrykeValue::string(out));
9272                    }
9273                    let idx = self.eval_expr(index)?.to_int();
9274                    let n = s.chars().count() as i64;
9275                    let i = if idx < 0 { idx + n } else { idx };
9276                    return Ok(if i >= 0 && i < n {
9277                        s.chars()
9278                            .nth(i as usize)
9279                            .map(|c| StrykeValue::string(c.to_string()))
9280                            .unwrap_or(StrykeValue::UNDEF)
9281                    } else {
9282                        StrykeValue::UNDEF
9283                    });
9284                }
9285                self.check_strict_array_var(array, line)?;
9286                // Stryke (non-compat) string-slice sugar: when the index is
9287                // a `from:to[:step]` range AND the target is a string, return
9288                // a substring with optional step. Mirrors Python `s[1:10:2]`.
9289                // Detect this BEFORE collapsing the range to an int.
9290                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9291                    if let ExprKind::Range {
9292                        from,
9293                        to,
9294                        exclusive,
9295                        step,
9296                    } = &index.kind
9297                    {
9298                        let aname_check = self.stash_array_name_for_package(array);
9299                        let prefer_scalar =
9300                            array == "_" || self.scope.get_array(&aname_check).is_empty();
9301                        if prefer_scalar {
9302                            let s = self.scope.get_scalar(array).to_string();
9303                            if !s.is_empty() {
9304                                let n = s.chars().count() as i64;
9305                                let mut from_i = self.eval_expr(from)?.to_int();
9306                                let mut to_i = self.eval_expr(to)?.to_int();
9307                                let step_i = match step {
9308                                    Some(e) => self.eval_expr(e)?.to_int(),
9309                                    None => 1,
9310                                };
9311                                if from_i < 0 {
9312                                    from_i += n
9313                                }
9314                                if to_i < 0 {
9315                                    to_i += n
9316                                }
9317                                if *exclusive {
9318                                    to_i -= 1
9319                                }
9320                                let chars: Vec<char> = s.chars().collect();
9321                                let mut out = String::new();
9322                                if step_i > 0 {
9323                                    let mut i = from_i;
9324                                    while i <= to_i && i < n {
9325                                        if i >= 0 {
9326                                            out.push(chars[i as usize]);
9327                                        }
9328                                        i += step_i;
9329                                    }
9330                                } else if step_i < 0 {
9331                                    let mut i = from_i;
9332                                    while i >= to_i && i >= 0 {
9333                                        if i < n {
9334                                            out.push(chars[i as usize]);
9335                                        }
9336                                        i += step_i;
9337                                    }
9338                                }
9339                                return Ok(StrykeValue::string(out));
9340                            }
9341                        }
9342                    }
9343                }
9344                let idx = self.eval_expr(index)?.to_int();
9345                let aname = self.stash_array_name_for_package(array);
9346                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
9347                    let class = obj
9348                        .as_blessed_ref()
9349                        .map(|b| b.class.clone())
9350                        .unwrap_or_default();
9351                    let full = format!("{}::FETCH", class);
9352                    if let Some(sub) = self.subs.get(&full).cloned() {
9353                        let arg_vals = vec![obj, StrykeValue::integer(idx)];
9354                        return self.call_sub(&sub, arg_vals, ctx, line);
9355                    }
9356                }
9357                // Stryke (non-compat) sugar: `$name[i]` indexes by Unicode
9358                // char when `@name` is missing/empty but `$name` is a
9359                // non-empty string. So `$s[0]` is the first grapheme of
9360                // `$s`. NB: `$_[0]` keeps Perl's `@_`-access semantics
9361                // because `@_` is populated inside any sub call; the
9362                // bareword `_[0]` parses to the same AST node, so both
9363                // forms behave alike — use `substr(_, 0, 1)` for char-of-
9364                // topic when inside a sub. Compat mode = Perl semantics.
9365                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9366                    let prefer_scalar = self.scope.get_array(&aname).is_empty();
9367                    if prefer_scalar {
9368                        let s = self.scope.get_scalar(array).to_string();
9369                        if !s.is_empty() {
9370                            let n = s.chars().count() as i64;
9371                            let i = if idx < 0 { idx + n } else { idx };
9372                            if i >= 0 && i < n {
9373                                if let Some(c) = s.chars().nth(i as usize) {
9374                                    return Ok(StrykeValue::string(c.to_string()));
9375                                }
9376                            }
9377                            return Ok(StrykeValue::UNDEF);
9378                        }
9379                    }
9380                }
9381                Ok(self.scope.get_array_element(&aname, idx))
9382            }
9383            ExprKind::HashElement { hash, key } => {
9384                self.check_strict_hash_var(hash, line)?;
9385                let k = self.eval_expr(key)?.to_string();
9386                self.touch_env_hash(hash);
9387                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
9388                    let class = obj
9389                        .as_blessed_ref()
9390                        .map(|b| b.class.clone())
9391                        .unwrap_or_default();
9392                    let full = format!("{}::FETCH", class);
9393                    if let Some(sub) = self.subs.get(&full).cloned() {
9394                        let arg_vals = vec![obj, StrykeValue::string(k)];
9395                        return self.call_sub(&sub, arg_vals, ctx, line);
9396                    }
9397                }
9398                Ok(self.scope.get_hash_element(hash, &k))
9399            }
9400            ExprKind::ArraySlice { array, indices } => {
9401                self.check_strict_array_var(array, line)?;
9402                let aname = self.stash_array_name_for_package(array);
9403                let flat = self.flatten_array_slice_index_specs(indices)?;
9404                let mut result = Vec::with_capacity(flat.len());
9405                for idx in flat {
9406                    result.push(self.scope.get_array_element(&aname, idx));
9407                }
9408                Ok(StrykeValue::array(result))
9409            }
9410            ExprKind::HashSlice { hash, keys } => {
9411                self.check_strict_hash_var(hash, line)?;
9412                self.touch_env_hash(hash);
9413                let mut result = Vec::new();
9414                for key_expr in keys {
9415                    for k in self.eval_hash_slice_key_components(key_expr)? {
9416                        result.push(self.scope.get_hash_element(hash, &k));
9417                    }
9418                }
9419                Ok(StrykeValue::array(result))
9420            }
9421            ExprKind::HashKvSlice { hash, keys } => {
9422                // `%h{KEYS}` — Perl 5.20+ key-value slice. Returns a flat
9423                // (key, value, key, value, ...) list. (BUG-008)
9424                self.check_strict_hash_var(hash, line)?;
9425                self.touch_env_hash(hash);
9426                let mut result = Vec::new();
9427                for key_expr in keys {
9428                    for k in self.eval_hash_slice_key_components(key_expr)? {
9429                        let v = self.scope.get_hash_element(hash, &k);
9430                        result.push(StrykeValue::string(k));
9431                        result.push(v);
9432                    }
9433                }
9434                Ok(StrykeValue::array(result))
9435            }
9436            ExprKind::HashSliceDeref { container, keys } => {
9437                let hv = self.eval_expr(container)?;
9438                let mut key_vals = Vec::with_capacity(keys.len());
9439                for key_expr in keys {
9440                    let v = if matches!(
9441                        key_expr.kind,
9442                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
9443                    ) {
9444                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
9445                    } else {
9446                        self.eval_expr(key_expr)?
9447                    };
9448                    key_vals.push(v);
9449                }
9450                self.hash_slice_deref_values(&hv, &key_vals, line)
9451            }
9452            ExprKind::AnonymousListSlice { source, indices } => {
9453                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
9454                let items = list_val.to_list();
9455                let flat = self.flatten_array_slice_index_specs(indices)?;
9456                let mut out = Vec::with_capacity(flat.len());
9457                for idx in flat {
9458                    let i = if idx < 0 {
9459                        (items.len() as i64 + idx) as usize
9460                    } else {
9461                        idx as usize
9462                    };
9463                    out.push(items.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
9464                }
9465                let arr = StrykeValue::array(out);
9466                if ctx != WantarrayCtx::List {
9467                    let v = arr.to_list();
9468                    Ok(v.last().cloned().unwrap_or(StrykeValue::UNDEF))
9469                } else {
9470                    Ok(arr)
9471                }
9472            }
9473
9474            // References
9475            ExprKind::ScalarRef(inner) => match &inner.kind {
9476                ExprKind::ScalarVar(name) => Ok(StrykeValue::scalar_binding_ref(name.clone())),
9477                ExprKind::ArrayVar(name) => {
9478                    self.check_strict_array_var(name, line)?;
9479                    let aname = self.stash_array_name_for_package(name);
9480                    // Promote the scope's array to shared Arc-backed storage.
9481                    // Both the scope and the returned ref share the same Arc.
9482                    let arc = self.scope.promote_array_to_shared(&aname);
9483                    Ok(StrykeValue::array_ref(arc))
9484                }
9485                ExprKind::HashVar(name) => {
9486                    self.check_strict_hash_var(name, line)?;
9487                    let arc = self.scope.promote_hash_to_shared(name);
9488                    Ok(StrykeValue::hash_ref(arc))
9489                }
9490                ExprKind::Deref {
9491                    expr: e,
9492                    kind: Sigil::Array,
9493                } => {
9494                    let v = self.eval_expr(e)?;
9495                    self.make_array_ref_alias(v, line)
9496                }
9497                ExprKind::Deref {
9498                    expr: e,
9499                    kind: Sigil::Hash,
9500                } => {
9501                    let v = self.eval_expr(e)?;
9502                    self.make_hash_ref_alias(v, line)
9503                }
9504                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
9505                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
9506                    Ok(StrykeValue::array_ref(Arc::new(RwLock::new(
9507                        list.to_list(),
9508                    ))))
9509                }
9510                ExprKind::HashSliceDeref { .. } => {
9511                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
9512                    Ok(StrykeValue::array_ref(Arc::new(RwLock::new(
9513                        list.to_list(),
9514                    ))))
9515                }
9516                _ => {
9517                    let val = self.eval_expr(inner)?;
9518                    Ok(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))))
9519                }
9520            },
9521            ExprKind::ArrayRef(elems) => {
9522                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
9523                // variables flatten into the ref rather than collapsing to a scalar count /
9524                // flip-flop value.
9525                let mut arr = Vec::with_capacity(elems.len());
9526                for e in elems {
9527                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9528                    let v = self.scope.resolve_container_binding_ref(v);
9529                    if let Some(vec) = v.as_array_vec() {
9530                        arr.extend(vec);
9531                    } else {
9532                        arr.push(v);
9533                    }
9534                }
9535                Ok(StrykeValue::array_ref(Arc::new(RwLock::new(arr))))
9536            }
9537            ExprKind::HashRef(pairs) => {
9538                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
9539                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
9540                let mut map = IndexMap::new();
9541                for (k, v) in pairs {
9542                    let key_str = self.eval_expr(k)?.to_string();
9543                    if key_str == "__HASH_SPREAD__" {
9544                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
9545                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
9546                        let items = spread.to_list();
9547                        let mut i = 0;
9548                        while i + 1 < items.len() {
9549                            map.insert(items[i].to_string(), items[i + 1].clone());
9550                            i += 2;
9551                        }
9552                    } else {
9553                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
9554                        map.insert(key_str, val);
9555                    }
9556                }
9557                Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map))))
9558            }
9559            ExprKind::CodeRef { params, body } => {
9560                let captured = self.scope.capture();
9561                Ok(StrykeValue::code_ref(Arc::new(StrykeSub {
9562                    name: "__ANON__".to_string(),
9563                    params: params.clone(),
9564                    body: body.clone(),
9565                    closure_env: Some(captured),
9566                    prototype: None,
9567                    fib_like: None,
9568                })))
9569            }
9570            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
9571            ExprKind::SubroutineCodeRef(name) => {
9572                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
9573                    StrykeError::runtime(self.undefined_subroutine_resolve_message(name), line)
9574                })?;
9575                Ok(StrykeValue::code_ref(sub))
9576            }
9577            ExprKind::DynamicSubCodeRef(expr) => {
9578                let name = self.eval_expr(expr)?.to_string();
9579                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
9580                    StrykeError::runtime(self.undefined_subroutine_resolve_message(&name), line)
9581                })?;
9582                Ok(StrykeValue::code_ref(sub))
9583            }
9584            ExprKind::Deref { expr, kind } => {
9585                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
9586                    let val = self.eval_expr(expr)?;
9587                    let n = self.array_deref_len(val, line)?;
9588                    return Ok(StrykeValue::integer(n));
9589                }
9590                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
9591                    let val = self.eval_expr(expr)?;
9592                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
9593                    return Ok(h.scalar_context());
9594                }
9595                let val = self.eval_expr(expr)?;
9596                self.symbolic_deref(val, *kind, line)
9597            }
9598            ExprKind::ArrowDeref { expr, index, kind } => {
9599                match kind {
9600                    DerefKind::Array => {
9601                        let container = self.eval_arrow_array_base(expr, line)?;
9602                        if let ExprKind::List(indices) = &index.kind {
9603                            let mut out = Vec::with_capacity(indices.len());
9604                            for ix in indices {
9605                                let idx = self.eval_expr(ix)?.to_int();
9606                                out.push(self.read_arrow_array_element(
9607                                    container.clone(),
9608                                    idx,
9609                                    line,
9610                                )?);
9611                            }
9612                            let arr = StrykeValue::array(out);
9613                            if ctx != WantarrayCtx::List {
9614                                let v = arr.to_list();
9615                                return Ok(v.last().cloned().unwrap_or(StrykeValue::UNDEF));
9616                            }
9617                            return Ok(arr);
9618                        }
9619                        let idx = self.eval_expr(index)?.to_int();
9620                        self.read_arrow_array_element(container, idx, line)
9621                    }
9622                    DerefKind::Hash => {
9623                        let val = self.eval_arrow_hash_base(expr, line)?;
9624                        let key = self.eval_expr(index)?.to_string();
9625                        self.read_arrow_hash_element(val, key.as_str(), line)
9626                    }
9627                    DerefKind::Call => {
9628                        // $coderef->(args). BUG-037: explicit `@array` / `%hash`
9629                        // arguments flatten into the call list (mirrors Perl's
9630                        // call list semantics so `$f->(@_)` does not pass
9631                        // `scalar(@_)`). Other arg shapes — `qw(...)`, list
9632                        // expressions, function calls — keep the original
9633                        // scalar-context evaluation so threading-style call
9634                        // sites (`~> qw(a b c d) fn { ... }`) pass the LHS as
9635                        // one threaded value rather than as flattened elements.
9636                        let val = self.eval_expr(expr)?;
9637                        if let ExprKind::List(ref arg_exprs) = index.kind {
9638                            let mut args = Vec::with_capacity(arg_exprs.len());
9639                            for a in arg_exprs {
9640                                if matches!(a.kind, ExprKind::ArrayVar(_) | ExprKind::HashVar(_)) {
9641                                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
9642                                    if let Some(items) = v.as_array_vec() {
9643                                        args.extend(items);
9644                                    } else {
9645                                        args.push(v);
9646                                    }
9647                                } else {
9648                                    args.push(self.eval_expr(a)?);
9649                                }
9650                            }
9651                            // Auto-deref ScalarRef for closure self-reference: $f->()
9652                            let callable = if let Some(inner) = val.as_scalar_ref() {
9653                                inner.read().clone()
9654                            } else {
9655                                val
9656                            };
9657                            if let Some(sub) = callable.as_code_ref() {
9658                                return self.call_sub(&sub, args, ctx, line);
9659                            }
9660                            Err(StrykeError::runtime("Not a code reference", line).into())
9661                        } else {
9662                            Err(StrykeError::runtime("Invalid call deref", line).into())
9663                        }
9664                    }
9665                }
9666            }
9667
9668            // Binary operators
9669            ExprKind::BinOp { left, op, right } => {
9670                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
9671                match op {
9672                    BinOp::BindMatch => {
9673                        let lv = self.eval_expr(left)?;
9674                        let rv = self.eval_expr(right)?;
9675                        let s = lv.to_string();
9676                        let pat = rv.to_string();
9677                        return self.regex_match_execute(s, &pat, "", false, "_", line);
9678                    }
9679                    BinOp::BindNotMatch => {
9680                        let lv = self.eval_expr(left)?;
9681                        let rv = self.eval_expr(right)?;
9682                        let s = lv.to_string();
9683                        let pat = rv.to_string();
9684                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
9685                        return Ok(StrykeValue::integer(if m.is_true() { 0 } else { 1 }));
9686                    }
9687                    BinOp::LogAnd | BinOp::LogAndWord => {
9688                        match &left.kind {
9689                            ExprKind::Regex(_, _) => {
9690                                if !self.eval_boolean_rvalue_condition(left)? {
9691                                    return Ok(StrykeValue::string(String::new()));
9692                                }
9693                            }
9694                            _ => {
9695                                let lv = self.eval_expr(left)?;
9696                                if !lv.is_true() {
9697                                    return Ok(lv);
9698                                }
9699                            }
9700                        }
9701                        return match &right.kind {
9702                            ExprKind::Regex(_, _) => Ok(StrykeValue::integer(
9703                                if self.eval_boolean_rvalue_condition(right)? {
9704                                    1
9705                                } else {
9706                                    0
9707                                },
9708                            )),
9709                            _ => self.eval_expr(right),
9710                        };
9711                    }
9712                    BinOp::LogOr | BinOp::LogOrWord => {
9713                        match &left.kind {
9714                            ExprKind::Regex(_, _) => {
9715                                if self.eval_boolean_rvalue_condition(left)? {
9716                                    return Ok(StrykeValue::integer(1));
9717                                }
9718                            }
9719                            _ => {
9720                                let lv = self.eval_expr(left)?;
9721                                if lv.is_true() {
9722                                    return Ok(lv);
9723                                }
9724                            }
9725                        }
9726                        return match &right.kind {
9727                            ExprKind::Regex(_, _) => Ok(StrykeValue::integer(
9728                                if self.eval_boolean_rvalue_condition(right)? {
9729                                    1
9730                                } else {
9731                                    0
9732                                },
9733                            )),
9734                            _ => self.eval_expr(right),
9735                        };
9736                    }
9737                    BinOp::DefinedOr => {
9738                        let lv = self.eval_expr(left)?;
9739                        if !lv.is_undef() {
9740                            return Ok(lv);
9741                        }
9742                        return self.eval_expr(right);
9743                    }
9744                    _ => {}
9745                }
9746                let lv = self.eval_expr(left)?;
9747                let rv = self.eval_expr(right)?;
9748                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
9749                    return r;
9750                }
9751                self.eval_binop(*op, &lv, &rv, line)
9752            }
9753
9754            // Unary
9755            ExprKind::UnaryOp { op, expr } => match op {
9756                UnaryOp::PreIncrement => {
9757                    if let ExprKind::ScalarVar(name) = &expr.kind {
9758                        self.check_strict_scalar_var(name, line)?;
9759                        let n = self.resolved_scalar_storage_name(name);
9760                        return Ok(self
9761                            .scope
9762                            .atomic_mutate(&n, perl_inc)
9763                            .map_err(|e| e.at_line(line))?);
9764                    }
9765                    if let ExprKind::Deref { kind, .. } = &expr.kind {
9766                        if matches!(kind, Sigil::Array | Sigil::Hash) {
9767                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
9768                                *kind, true, true, line,
9769                            ));
9770                        }
9771                    }
9772                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
9773                        let href = self.eval_expr(container)?;
9774                        let mut key_vals = Vec::with_capacity(keys.len());
9775                        for key_expr in keys {
9776                            key_vals.push(self.eval_expr(key_expr)?);
9777                        }
9778                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
9779                    }
9780                    if let ExprKind::ArrowDeref {
9781                        expr: arr_expr,
9782                        index,
9783                        kind: DerefKind::Array,
9784                    } = &expr.kind
9785                    {
9786                        if let ExprKind::List(indices) = &index.kind {
9787                            let container = self.eval_arrow_array_base(arr_expr, line)?;
9788                            let mut idxs = Vec::with_capacity(indices.len());
9789                            for ix in indices {
9790                                idxs.push(self.eval_expr(ix)?.to_int());
9791                            }
9792                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
9793                        }
9794                    }
9795                    let val = self.eval_expr(expr)?;
9796                    let new_val = perl_inc(&val);
9797                    self.assign_value(expr, new_val.clone())?;
9798                    Ok(new_val)
9799                }
9800                UnaryOp::PreDecrement => {
9801                    if let ExprKind::ScalarVar(name) = &expr.kind {
9802                        self.check_strict_scalar_var(name, line)?;
9803                        let n = self.resolved_scalar_storage_name(name);
9804                        return Ok(self
9805                            .scope
9806                            .atomic_mutate(&n, |v| StrykeValue::integer(v.to_int() - 1))
9807                            .map_err(|e| e.at_line(line))?);
9808                    }
9809                    if let ExprKind::Deref { kind, .. } = &expr.kind {
9810                        if matches!(kind, Sigil::Array | Sigil::Hash) {
9811                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
9812                                *kind, true, false, line,
9813                            ));
9814                        }
9815                    }
9816                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
9817                        let href = self.eval_expr(container)?;
9818                        let mut key_vals = Vec::with_capacity(keys.len());
9819                        for key_expr in keys {
9820                            key_vals.push(self.eval_expr(key_expr)?);
9821                        }
9822                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
9823                    }
9824                    if let ExprKind::ArrowDeref {
9825                        expr: arr_expr,
9826                        index,
9827                        kind: DerefKind::Array,
9828                    } = &expr.kind
9829                    {
9830                        if let ExprKind::List(indices) = &index.kind {
9831                            let container = self.eval_arrow_array_base(arr_expr, line)?;
9832                            let mut idxs = Vec::with_capacity(indices.len());
9833                            for ix in indices {
9834                                idxs.push(self.eval_expr(ix)?.to_int());
9835                            }
9836                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
9837                        }
9838                    }
9839                    let val = self.eval_expr(expr)?;
9840                    let new_val = StrykeValue::integer(val.to_int() - 1);
9841                    self.assign_value(expr, new_val.clone())?;
9842                    Ok(new_val)
9843                }
9844                _ => {
9845                    match op {
9846                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
9847                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
9848                                let topic = self.scope.get_scalar("_");
9849                                let rl = expr.line;
9850                                let s = topic.to_string();
9851                                let v =
9852                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
9853                                return Ok(StrykeValue::integer(if v.is_true() { 0 } else { 1 }));
9854                            }
9855                        }
9856                        _ => {}
9857                    }
9858                    let val = self.eval_expr(expr)?;
9859                    match op {
9860                        UnaryOp::Negate => {
9861                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
9862                                return r;
9863                            }
9864                            if let Some(n) = val.as_integer() {
9865                                Ok(StrykeValue::integer(-n))
9866                            } else {
9867                                Ok(StrykeValue::float(-val.to_number()))
9868                            }
9869                        }
9870                        UnaryOp::LogNot => {
9871                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
9872                                let pv = r?;
9873                                return Ok(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
9874                            }
9875                            Ok(StrykeValue::integer(if val.is_true() { 0 } else { 1 }))
9876                        }
9877                        UnaryOp::BitNot => Ok(StrykeValue::integer(!val.to_int())),
9878                        UnaryOp::LogNotWord => {
9879                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
9880                                let pv = r?;
9881                                return Ok(StrykeValue::integer(if pv.is_true() { 0 } else { 1 }));
9882                            }
9883                            Ok(StrykeValue::integer(if val.is_true() { 0 } else { 1 }))
9884                        }
9885                        UnaryOp::Ref => {
9886                            if let ExprKind::ScalarVar(name) = &expr.kind {
9887                                return Ok(StrykeValue::scalar_binding_ref(name.clone()));
9888                            }
9889                            Ok(StrykeValue::scalar_ref(Arc::new(RwLock::new(val))))
9890                        }
9891                        _ => unreachable!(),
9892                    }
9893                }
9894            },
9895
9896            ExprKind::PostfixOp { expr, op } => {
9897                // For scalar variables, use atomic_mutate_post to hold the lock
9898                // for the entire read-modify-write (critical for mysync).
9899                if let ExprKind::ScalarVar(name) = &expr.kind {
9900                    self.check_strict_scalar_var(name, line)?;
9901                    let n = self.resolved_scalar_storage_name(name);
9902                    let f: fn(&StrykeValue) -> StrykeValue = match op {
9903                        PostfixOp::Increment => |v| perl_inc(v),
9904                        PostfixOp::Decrement => |v| StrykeValue::integer(v.to_int() - 1),
9905                    };
9906                    return Ok(self
9907                        .scope
9908                        .atomic_mutate_post(&n, f)
9909                        .map_err(|e| e.at_line(line))?);
9910                }
9911                if let ExprKind::Deref { kind, .. } = &expr.kind {
9912                    if matches!(kind, Sigil::Array | Sigil::Hash) {
9913                        let is_inc = matches!(op, PostfixOp::Increment);
9914                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
9915                            *kind, false, is_inc, line,
9916                        ));
9917                    }
9918                }
9919                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
9920                    let href = self.eval_expr(container)?;
9921                    let mut key_vals = Vec::with_capacity(keys.len());
9922                    for key_expr in keys {
9923                        key_vals.push(self.eval_expr(key_expr)?);
9924                    }
9925                    let kind_byte = match op {
9926                        PostfixOp::Increment => 2u8,
9927                        PostfixOp::Decrement => 3u8,
9928                    };
9929                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
9930                }
9931                if let ExprKind::ArrowDeref {
9932                    expr: arr_expr,
9933                    index,
9934                    kind: DerefKind::Array,
9935                } = &expr.kind
9936                {
9937                    if let ExprKind::List(indices) = &index.kind {
9938                        let container = self.eval_arrow_array_base(arr_expr, line)?;
9939                        let mut idxs = Vec::with_capacity(indices.len());
9940                        for ix in indices {
9941                            idxs.push(self.eval_expr(ix)?.to_int());
9942                        }
9943                        let kind_byte = match op {
9944                            PostfixOp::Increment => 2u8,
9945                            PostfixOp::Decrement => 3u8,
9946                        };
9947                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
9948                    }
9949                }
9950                let val = self.eval_expr(expr)?;
9951                let old = val.clone();
9952                let new_val = match op {
9953                    PostfixOp::Increment => perl_inc(&val),
9954                    PostfixOp::Decrement => StrykeValue::integer(val.to_int() - 1),
9955                };
9956                self.assign_value(expr, new_val)?;
9957                Ok(old)
9958            }
9959
9960            // Assignment
9961            ExprKind::Assign { target, value } => {
9962                if let ExprKind::Typeglob(lhs) = &target.kind {
9963                    if let ExprKind::Typeglob(rhs) = &value.kind {
9964                        self.copy_typeglob_slots(lhs, rhs, line)?;
9965                        return self.eval_expr(value);
9966                    }
9967                }
9968                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
9969                self.assign_value(target, val.clone())?;
9970                Ok(val)
9971            }
9972            ExprKind::CompoundAssign { target, op, value } => {
9973                // For scalar targets, use atomic_mutate to hold the lock.
9974                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
9975                if let ExprKind::ScalarVar(name) = &target.kind {
9976                    self.check_strict_scalar_var(name, line)?;
9977                    let n = self.resolved_scalar_storage_name(name);
9978                    let op = *op;
9979                    let rhs = match op {
9980                        BinOp::LogOr => {
9981                            let old = self.scope.get_scalar(&n);
9982                            if old.is_true() {
9983                                return Ok(old);
9984                            }
9985                            self.eval_expr(value)?
9986                        }
9987                        BinOp::DefinedOr => {
9988                            let old = self.scope.get_scalar(&n);
9989                            if !old.is_undef() {
9990                                return Ok(old);
9991                            }
9992                            self.eval_expr(value)?
9993                        }
9994                        BinOp::LogAnd => {
9995                            let old = self.scope.get_scalar(&n);
9996                            if !old.is_true() {
9997                                return Ok(old);
9998                            }
9999                            self.eval_expr(value)?
10000                        }
10001                        _ => self.eval_expr(value)?,
10002                    };
10003                    return Ok(self.scalar_compound_assign_scalar_target(&n, op, rhs)?);
10004                }
10005                let rhs = self.eval_expr(value)?;
10006                // For hash element targets: $h{key} += 1
10007                if let ExprKind::HashElement { hash, key } = &target.kind {
10008                    self.check_strict_hash_var(hash, line)?;
10009                    let k = self.eval_expr(key)?.to_string();
10010                    let op = *op;
10011                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
10012                        BinOp::Add => {
10013                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10014                                StrykeValue::integer(a.wrapping_add(b))
10015                            } else {
10016                                StrykeValue::float(old.to_number() + rhs.to_number())
10017                            }
10018                        }
10019                        BinOp::Sub => {
10020                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10021                                StrykeValue::integer(a.wrapping_sub(b))
10022                            } else {
10023                                StrykeValue::float(old.to_number() - rhs.to_number())
10024                            }
10025                        }
10026                        BinOp::Concat => {
10027                            let mut s = old.to_string();
10028                            rhs.append_to(&mut s);
10029                            StrykeValue::string(s)
10030                        }
10031                        _ => StrykeValue::float(old.to_number() + rhs.to_number()),
10032                    })?);
10033                }
10034                // For array element targets: $a[i] += 1
10035                if let ExprKind::ArrayElement { array, index } = &target.kind {
10036                    self.check_strict_array_var(array, line)?;
10037                    let idx = self.eval_expr(index)?.to_int();
10038                    let op = *op;
10039                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
10040                        BinOp::Add => {
10041                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10042                                StrykeValue::integer(a.wrapping_add(b))
10043                            } else {
10044                                StrykeValue::float(old.to_number() + rhs.to_number())
10045                            }
10046                        }
10047                        BinOp::Sub => {
10048                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10049                                StrykeValue::integer(a.wrapping_sub(b))
10050                            } else {
10051                                StrykeValue::float(old.to_number() - rhs.to_number())
10052                            }
10053                        }
10054                        BinOp::Mul => {
10055                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
10056                                StrykeValue::integer(a.wrapping_mul(b))
10057                            } else {
10058                                StrykeValue::float(old.to_number() * rhs.to_number())
10059                            }
10060                        }
10061                        BinOp::Div => StrykeValue::float(old.to_number() / rhs.to_number()),
10062                        BinOp::Mod => {
10063                            // Perl `%` is floored-division (sign-of-divisor),
10064                            // not Rust's `%` (sign-of-dividend) nor
10065                            // `rem_euclid` (always non-negative). Truncate
10066                            // float operands to int first, matching Perl 5.
10067                            let a = old.to_int();
10068                            let b = rhs.to_int();
10069                            if b == 0 {
10070                                StrykeValue::integer(0)
10071                            } else {
10072                                StrykeValue::integer(crate::value::perl_mod_i64(a, b))
10073                            }
10074                        }
10075                        BinOp::Concat => {
10076                            let mut s = old.to_string();
10077                            rhs.append_to(&mut s);
10078                            StrykeValue::string(s)
10079                        }
10080                        BinOp::Pow => StrykeValue::float(old.to_number().powf(rhs.to_number())),
10081                        BinOp::BitAnd => StrykeValue::integer(old.to_int() & rhs.to_int()),
10082                        BinOp::BitOr => StrykeValue::integer(old.to_int() | rhs.to_int()),
10083                        BinOp::BitXor => StrykeValue::integer(old.to_int() ^ rhs.to_int()),
10084                        BinOp::ShiftLeft => StrykeValue::integer(old.to_int() << rhs.to_int()),
10085                        BinOp::ShiftRight => StrykeValue::integer(old.to_int() >> rhs.to_int()),
10086                        _ => StrykeValue::float(old.to_number() + rhs.to_number()),
10087                    })?);
10088                }
10089                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
10090                    let href = self.eval_expr(container)?;
10091                    let mut key_vals = Vec::with_capacity(keys.len());
10092                    for key_expr in keys {
10093                        key_vals.push(self.eval_expr(key_expr)?);
10094                    }
10095                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
10096                }
10097                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
10098                    if let ExprKind::Deref {
10099                        expr: inner,
10100                        kind: Sigil::Array,
10101                    } = &source.kind
10102                    {
10103                        let container = self.eval_arrow_array_base(inner, line)?;
10104                        let idxs = self.flatten_array_slice_index_specs(indices)?;
10105                        return self
10106                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
10107                    }
10108                }
10109                if let ExprKind::ArrowDeref {
10110                    expr: arr_expr,
10111                    index,
10112                    kind: DerefKind::Array,
10113                } = &target.kind
10114                {
10115                    if let ExprKind::List(indices) = &index.kind {
10116                        let container = self.eval_arrow_array_base(arr_expr, line)?;
10117                        let mut idxs = Vec::with_capacity(indices.len());
10118                        for ix in indices {
10119                            idxs.push(self.eval_expr(ix)?.to_int());
10120                        }
10121                        return self
10122                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
10123                    }
10124                }
10125                let old = self.eval_expr(target)?;
10126                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
10127                self.assign_value(target, new_val.clone())?;
10128                Ok(new_val)
10129            }
10130
10131            // Ternary — propagate wantarray context to both branches so
10132            // `($a, $b) = $c ? (1, 2) : (3, 4)` evaluates the chosen branch
10133            // in list context.
10134            ExprKind::Ternary {
10135                condition,
10136                then_expr,
10137                else_expr,
10138            } => {
10139                if self.eval_boolean_rvalue_condition(condition)? {
10140                    self.eval_expr_ctx(then_expr, ctx)
10141                } else {
10142                    self.eval_expr_ctx(else_expr, ctx)
10143                }
10144            }
10145
10146            // Range
10147            ExprKind::Range {
10148                from,
10149                to,
10150                exclusive,
10151                step,
10152            } => {
10153                if ctx == WantarrayCtx::List {
10154                    let f = self.eval_expr(from)?;
10155                    let t = self.eval_expr(to)?;
10156                    if let Some(s) = step {
10157                        let step_val = self.eval_expr(s)?.to_int();
10158                        let from_i = f.to_int();
10159                        let to_i = t.to_int();
10160                        let list = if step_val == 0 {
10161                            vec![]
10162                        } else if step_val > 0 {
10163                            (from_i..=to_i)
10164                                .step_by(step_val as usize)
10165                                .map(StrykeValue::integer)
10166                                .collect()
10167                        } else {
10168                            std::iter::successors(Some(from_i), |&x| {
10169                                let next = x - step_val.abs();
10170                                if next >= to_i {
10171                                    Some(next)
10172                                } else {
10173                                    None
10174                                }
10175                            })
10176                            .map(StrykeValue::integer)
10177                            .collect()
10178                        };
10179                        Ok(StrykeValue::array(list))
10180                    } else {
10181                        let list = perl_list_range_expand(f, t);
10182                        Ok(StrykeValue::array(list))
10183                    }
10184                } else {
10185                    let key = std::ptr::from_ref(expr) as usize;
10186                    match (&from.kind, &to.kind) {
10187                        (
10188                            ExprKind::Regex(left_pat, left_flags),
10189                            ExprKind::Regex(right_pat, right_flags),
10190                        ) => {
10191                            let dot = self.scalar_flipflop_dot_line();
10192                            let subject = self.scope.get_scalar("_").to_string();
10193                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10194                                |e| match e {
10195                                    FlowOrError::Error(err) => err,
10196                                    FlowOrError::Flow(_) => StrykeError::runtime(
10197                                        "unexpected flow in regex flip-flop",
10198                                        line,
10199                                    ),
10200                                },
10201                            )?;
10202                            let right_re = self
10203                                .compile_regex(right_pat, right_flags, line)
10204                                .map_err(|e| match e {
10205                                    FlowOrError::Error(err) => err,
10206                                    FlowOrError::Flow(_) => StrykeError::runtime(
10207                                        "unexpected flow in regex flip-flop",
10208                                        line,
10209                                    ),
10210                                })?;
10211                            let left_m = left_re.is_match(&subject);
10212                            let right_m = right_re.is_match(&subject);
10213                            let st = self.flip_flop_tree.entry(key).or_default();
10214                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10215                                &mut st.active,
10216                                &mut st.exclusive_left_line,
10217                                *exclusive,
10218                                dot,
10219                                left_m,
10220                                right_m,
10221                            )))
10222                        }
10223                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
10224                            let dot = self.scalar_flipflop_dot_line();
10225                            let subject = self.scope.get_scalar("_").to_string();
10226                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10227                                |e| match e {
10228                                    FlowOrError::Error(err) => err,
10229                                    FlowOrError::Flow(_) => StrykeError::runtime(
10230                                        "unexpected flow in regex/eof flip-flop",
10231                                        line,
10232                                    ),
10233                                },
10234                            )?;
10235                            let left_m = left_re.is_match(&subject);
10236                            let right_m = self.eof_without_arg_is_true();
10237                            let st = self.flip_flop_tree.entry(key).or_default();
10238                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10239                                &mut st.active,
10240                                &mut st.exclusive_left_line,
10241                                *exclusive,
10242                                dot,
10243                                left_m,
10244                                right_m,
10245                            )))
10246                        }
10247                        (
10248                            ExprKind::Regex(left_pat, left_flags),
10249                            ExprKind::Integer(_) | ExprKind::Float(_),
10250                        ) => {
10251                            let dot = self.scalar_flipflop_dot_line();
10252                            let right = self.eval_expr(to)?.to_int();
10253                            let subject = self.scope.get_scalar("_").to_string();
10254                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10255                                |e| match e {
10256                                    FlowOrError::Error(err) => err,
10257                                    FlowOrError::Flow(_) => StrykeError::runtime(
10258                                        "unexpected flow in regex flip-flop",
10259                                        line,
10260                                    ),
10261                                },
10262                            )?;
10263                            let left_m = left_re.is_match(&subject);
10264                            let right_m = dot == right;
10265                            let st = self.flip_flop_tree.entry(key).or_default();
10266                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10267                                &mut st.active,
10268                                &mut st.exclusive_left_line,
10269                                *exclusive,
10270                                dot,
10271                                left_m,
10272                                right_m,
10273                            )))
10274                        }
10275                        (ExprKind::Regex(left_pat, left_flags), _) => {
10276                            if let ExprKind::Eof(Some(_)) = &to.kind {
10277                                return Err(FlowOrError::Error(StrykeError::runtime(
10278                                    "regex flip-flop with eof(HANDLE) is not supported",
10279                                    line,
10280                                )));
10281                            }
10282                            let dot = self.scalar_flipflop_dot_line();
10283                            let subject = self.scope.get_scalar("_").to_string();
10284                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10285                                |e| match e {
10286                                    FlowOrError::Error(err) => err,
10287                                    FlowOrError::Flow(_) => StrykeError::runtime(
10288                                        "unexpected flow in regex flip-flop",
10289                                        line,
10290                                    ),
10291                                },
10292                            )?;
10293                            let left_m = left_re.is_match(&subject);
10294                            let right_m = self.eval_boolean_rvalue_condition(to)?;
10295                            let st = self.flip_flop_tree.entry(key).or_default();
10296                            Ok(StrykeValue::integer(Self::regex_flip_flop_transition(
10297                                &mut st.active,
10298                                &mut st.exclusive_left_line,
10299                                *exclusive,
10300                                dot,
10301                                left_m,
10302                                right_m,
10303                            )))
10304                        }
10305                        _ => {
10306                            let left = self.eval_expr(from)?.to_int();
10307                            let right = self.eval_expr(to)?.to_int();
10308                            let dot = self.scalar_flipflop_dot_line();
10309                            let st = self.flip_flop_tree.entry(key).or_default();
10310                            if !st.active {
10311                                if dot == left {
10312                                    st.active = true;
10313                                    if *exclusive {
10314                                        st.exclusive_left_line = Some(dot);
10315                                    } else {
10316                                        st.exclusive_left_line = None;
10317                                        if dot == right {
10318                                            st.active = false;
10319                                        }
10320                                    }
10321                                    return Ok(StrykeValue::integer(1));
10322                                }
10323                                return Ok(StrykeValue::integer(0));
10324                            }
10325                            if let Some(ll) = st.exclusive_left_line {
10326                                if dot == right && dot > ll {
10327                                    st.active = false;
10328                                    st.exclusive_left_line = None;
10329                                }
10330                            } else if dot == right {
10331                                st.active = false;
10332                            }
10333                            Ok(StrykeValue::integer(1))
10334                        }
10335                    }
10336                }
10337            }
10338
10339            // SliceRange — open-ended Python-style slice expansion. Reachable from the
10340            // tree-walker when slice subscripts are evaluated outside the VM (rare; VM is
10341            // the primary execution engine). Only closed forms (`from:to[:step]`) can be
10342            // expanded here without container length context; open ends require a slice
10343            // op (`Op::ArraySliceRange` / `Op::HashSliceRange`) which knows the container.
10344            ExprKind::SliceRange { from, to, step } => {
10345                let f = match from {
10346                    Some(e) => self.eval_expr(e)?,
10347                    None => {
10348                        return Err(StrykeError::runtime(
10349                            "open-ended slice range cannot be evaluated outside slice subscript",
10350                            line,
10351                        )
10352                        .into());
10353                    }
10354                };
10355                let t = match to {
10356                    Some(e) => self.eval_expr(e)?,
10357                    None => {
10358                        return Err(StrykeError::runtime(
10359                            "open-ended slice range cannot be evaluated outside slice subscript",
10360                            line,
10361                        )
10362                        .into());
10363                    }
10364                };
10365                let list = if let Some(s) = step {
10366                    let sv = self.eval_expr(s)?;
10367                    crate::value::perl_list_range_expand_stepped(f, t, sv)
10368                } else {
10369                    perl_list_range_expand(f, t)
10370                };
10371                Ok(StrykeValue::array(list))
10372            }
10373
10374            // Repeat — see `ast.rs` `ExprKind::Repeat` for the list/scalar split.
10375            ExprKind::Repeat {
10376                expr,
10377                count,
10378                list_repeat,
10379            } => {
10380                let n = self.eval_expr(count)?.to_int().max(0) as usize;
10381                if *list_repeat {
10382                    // `(LIST) x N` — evaluate the LHS in list context, replicate.
10383                    let saved = self.wantarray_kind;
10384                    self.wantarray_kind = WantarrayCtx::List;
10385                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10386                    self.wantarray_kind = saved;
10387                    let items: Vec<StrykeValue> = val.as_array_vec().unwrap_or_else(|| vec![val]);
10388                    let mut result = Vec::with_capacity(items.len() * n);
10389                    for _ in 0..n {
10390                        result.extend(items.iter().cloned());
10391                    }
10392                    Ok(StrykeValue::array(result))
10393                } else {
10394                    // `EXPR x N` — scalar string repetition.
10395                    let val = self.eval_expr(expr)?;
10396                    Ok(StrykeValue::string(val.to_string().repeat(n)))
10397                }
10398            }
10399
10400            // `my $x = …` / `our` / `state` / `local` used as an expression
10401            // (e.g. `if (my $line = readline)`).  Declare each variable in the
10402            // current scope, evaluate the initializer (if any), and return the
10403            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
10404            ExprKind::MyExpr { keyword, decls } => {
10405                // Build a temporary statement and dispatch to the canonical
10406                // statement handler so behavior matches `my $x = …;` exactly.
10407                let stmt_kind = match keyword.as_str() {
10408                    "my" => StmtKind::My(decls.clone()),
10409                    "our" => StmtKind::Our(decls.clone()),
10410                    "state" => StmtKind::State(decls.clone()),
10411                    "local" => StmtKind::Local(decls.clone()),
10412                    _ => StmtKind::My(decls.clone()),
10413                };
10414                let stmt = Statement {
10415                    label: None,
10416                    kind: stmt_kind,
10417                    line,
10418                };
10419                self.exec_statement(&stmt)?;
10420                // Return the value of the (first) declared variable so the
10421                // surrounding expression sees the assigned value, matching
10422                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
10423                let first = decls.first().ok_or_else(|| {
10424                    FlowOrError::Error(StrykeError::runtime("MyExpr: empty decl list", line))
10425                })?;
10426                Ok(match first.sigil {
10427                    Sigil::Scalar => self.scope.get_scalar(&first.name),
10428                    Sigil::Array => StrykeValue::array(self.scope.get_array(&first.name)),
10429                    Sigil::Hash => {
10430                        let h = self.scope.get_hash(&first.name);
10431                        let mut flat: Vec<StrykeValue> = Vec::with_capacity(h.len() * 2);
10432                        for (k, v) in h {
10433                            flat.push(StrykeValue::string(k));
10434                            flat.push(v);
10435                        }
10436                        StrykeValue::array(flat)
10437                    }
10438                    Sigil::Typeglob => StrykeValue::UNDEF,
10439                })
10440            }
10441
10442            // Function calls
10443            ExprKind::FuncCall { name, args } => {
10444                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
10445                // bare-name dispatch so the matches below stay flat.
10446                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
10447                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
10448                if matches!(dispatch_name, "read") && args.len() >= 3 {
10449                    let fh_val = self.eval_expr(&args[0])?;
10450                    let fh = fh_val
10451                        .as_io_handle_name()
10452                        .unwrap_or_else(|| fh_val.to_string());
10453                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
10454                    let offset = if args.len() > 3 {
10455                        self.eval_expr(&args[3])?.to_int().max(0) as usize
10456                    } else {
10457                        0
10458                    };
10459                    // Extract the variable name from the AST
10460                    let var_name = match &args[1].kind {
10461                        ExprKind::ScalarVar(n) => n.clone(),
10462                        _ => self.eval_expr(&args[1])?.to_string(),
10463                    };
10464                    let mut buf = vec![0u8; len];
10465                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
10466                        slot.lock().read(&mut buf).unwrap_or(0)
10467                    } else if fh == "STDIN" {
10468                        std::io::stdin().read(&mut buf).unwrap_or(0)
10469                    } else {
10470                        return Err(StrykeError::runtime(
10471                            format!("read: unopened handle {}", fh),
10472                            line,
10473                        )
10474                        .into());
10475                    };
10476                    buf.truncate(n);
10477                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
10478                    if offset > 0 {
10479                        let mut existing = self.scope.get_scalar(&var_name).to_string();
10480                        while existing.len() < offset {
10481                            existing.push('\0');
10482                        }
10483                        existing.push_str(&read_str);
10484                        let _ = self
10485                            .scope
10486                            .set_scalar(&var_name, StrykeValue::string(existing));
10487                    } else {
10488                        let _ = self
10489                            .scope
10490                            .set_scalar(&var_name, StrykeValue::string(read_str));
10491                    }
10492                    return Ok(StrykeValue::integer(n as i64));
10493                }
10494                if matches!(dispatch_name, "group_by" | "chunk_by") {
10495                    if args.len() != 2 {
10496                        return Err(StrykeError::runtime(
10497                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
10498                            line,
10499                        )
10500                        .into());
10501                    }
10502                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
10503                }
10504                if matches!(dispatch_name, "puniq" | "pfirst" | "pany") {
10505                    let mut arg_vals = Vec::with_capacity(args.len());
10506                    for a in args {
10507                        arg_vals.push(self.eval_expr(a)?);
10508                    }
10509                    let saved_wa = self.wantarray_kind;
10510                    self.wantarray_kind = ctx;
10511                    let r = self.eval_par_list_call(dispatch_name, &arg_vals, ctx, line);
10512                    self.wantarray_kind = saved_wa;
10513                    return r.map_err(Into::into);
10514                }
10515                let arg_vals = if matches!(dispatch_name, "any" | "all" | "none" | "first")
10516                    || matches!(
10517                        dispatch_name,
10518                        "take_while"
10519                            | "drop_while"
10520                            | "skip_while"
10521                            | "reject"
10522                            | "grepv"
10523                            | "tap"
10524                            | "peek"
10525                    )
10526                    || matches!(
10527                        dispatch_name,
10528                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
10529                    ) {
10530                    if args.len() != 2 {
10531                        return Err(StrykeError::runtime(
10532                            format!("{}: expected BLOCK, LIST", name),
10533                            line,
10534                        )
10535                        .into());
10536                    }
10537                    let cr = self.eval_expr(&args[0])?;
10538                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
10539                    let mut v = vec![cr];
10540                    v.extend(list_src.to_list());
10541                    v
10542                } else if matches!(
10543                    dispatch_name,
10544                    "zip"
10545                        | "zip_longest"
10546                        | "zip_shortest"
10547                        | "mesh"
10548                        | "mesh_longest"
10549                        | "mesh_shortest"
10550                ) {
10551                    let mut v = Vec::with_capacity(args.len());
10552                    for a in args {
10553                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
10554                    }
10555                    v
10556                } else if matches!(
10557                    dispatch_name,
10558                    "count" | "size" | "cnt" | "len" | "list_count" | "list_size"
10559                ) {
10560                    // Count-family: preserve the "user wrote 1 syntactic arg" signal.
10561                    // Flattening the lone operand here collapses `count(@empty)` to a
10562                    // zero-arg call, which would then fall back to `$_` topic — wrong.
10563                    // Pass the single evaluated value directly so the builtin's 1-arg
10564                    // path can dispatch on its type (string → chars, array/aref →
10565                    // element count via map_flatten_outputs, hash → key count, …).
10566                    let mut list_out = Vec::new();
10567                    if args.len() == 1 {
10568                        list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10569                    } else {
10570                        for a in args {
10571                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
10572                        }
10573                    }
10574                    list_out
10575                } else if matches!(
10576                    dispatch_name,
10577                    "uniq"
10578                        | "distinct"
10579                        | "uniqstr"
10580                        | "uniqint"
10581                        | "uniqnum"
10582                        | "flatten"
10583                        | "set"
10584                        | "with_index"
10585                        | "shuffle"
10586                        | "sum"
10587                        | "sum0"
10588                        | "product"
10589                        | "min"
10590                        | "max"
10591                        | "minstr"
10592                        | "maxstr"
10593                        | "mean"
10594                        | "median"
10595                        | "mode"
10596                        | "stddev"
10597                        | "variance"
10598                        | "pairs"
10599                        | "unpairs"
10600                        | "pairkeys"
10601                        | "pairvalues"
10602                ) {
10603                    // Slurpy list `(@)`: one list expr (`uniq @x`) or multiple actuals
10604                    // (`uniq(1, 1, 2)`). Each actual is evaluated in list context so
10605                    // `@a, @b` flattens.
10606                    let mut list_out = Vec::new();
10607                    if args.len() == 1 {
10608                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
10609                    } else {
10610                        for a in args {
10611                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
10612                        }
10613                    }
10614                    list_out
10615                } else if matches!(dispatch_name, "take" | "head" | "tail" | "drop") {
10616                    if args.is_empty() {
10617                        return Err(StrykeError::runtime(
10618                            "take/head/tail/drop: need LIST..., N or unary N",
10619                            line,
10620                        )
10621                        .into());
10622                    }
10623                    let mut arg_vals = Vec::with_capacity(args.len());
10624                    if args.len() == 1 {
10625                        // head @l == head @l, 1 — evaluate in list context
10626                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10627                    } else {
10628                        for a in &args[..args.len() - 1] {
10629                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
10630                        }
10631                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
10632                    }
10633                    arg_vals
10634                } else if matches!(dispatch_name, "chunked" | "windowed") {
10635                    let mut list_out = Vec::new();
10636                    match args.len() {
10637                        0 => {
10638                            return Err(StrykeError::runtime(
10639                                format!("{name}: expected (LIST, N) or unary N after |>"),
10640                                line,
10641                            )
10642                            .into());
10643                        }
10644                        1 => {
10645                            // chunked @l / windowed @l — evaluate in list context, default size
10646                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10647                        }
10648                        2 => {
10649                            list_out.extend(
10650                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
10651                            );
10652                            list_out.push(self.eval_expr(&args[1])?);
10653                        }
10654                        _ => {
10655                            return Err(StrykeError::runtime(
10656                                format!(
10657                                    "{name}: expected exactly (LIST, N); use one list expression then size"
10658                                ),
10659                                line,
10660                            )
10661                            .into());
10662                        }
10663                    }
10664                    list_out
10665                } else {
10666                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
10667                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
10668                    let mut arg_vals = Vec::with_capacity(args.len());
10669                    for a in args {
10670                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
10671                        if let Some(items) = v.as_array_vec() {
10672                            arg_vals.extend(items);
10673                        } else {
10674                            arg_vals.push(v);
10675                        }
10676                    }
10677                    arg_vals
10678                };
10679                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
10680                let saved_wa = self.wantarray_kind;
10681                self.wantarray_kind = ctx;
10682                // Builtins first — immune to monkey-patching (matches VM dispatch order).
10683                // In compat mode, user subs shadow builtins (Perl 5 semantics).
10684                if !crate::compat_mode() {
10685                    if matches!(
10686                        dispatch_name,
10687                        "take_while"
10688                            | "drop_while"
10689                            | "skip_while"
10690                            | "reject"
10691                            | "grepv"
10692                            | "tap"
10693                            | "peek"
10694                    ) {
10695                        let r =
10696                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
10697                        self.wantarray_kind = saved_wa;
10698                        return r.map_err(Into::into);
10699                    }
10700                    if let Some(r) =
10701                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
10702                    {
10703                        self.wantarray_kind = saved_wa;
10704                        return r.map_err(Into::into);
10705                    }
10706                }
10707                if let Some(sub) = self.resolve_sub_by_name(name) {
10708                    self.wantarray_kind = saved_wa;
10709                    let args = self.with_topic_default_args(arg_vals);
10710                    let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
10711                    return self.call_sub_with_package(&sub, args, ctx, line, pkg);
10712                }
10713                // Compat mode: check builtins after user subs (Perl 5 semantics).
10714                if crate::compat_mode() {
10715                    if matches!(
10716                        dispatch_name,
10717                        "take_while"
10718                            | "drop_while"
10719                            | "skip_while"
10720                            | "reject"
10721                            | "grepv"
10722                            | "tap"
10723                            | "peek"
10724                    ) {
10725                        let r =
10726                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
10727                        self.wantarray_kind = saved_wa;
10728                        return r.map_err(Into::into);
10729                    }
10730                    if let Some(r) =
10731                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
10732                    {
10733                        self.wantarray_kind = saved_wa;
10734                        return r.map_err(Into::into);
10735                    }
10736                }
10737                self.wantarray_kind = saved_wa;
10738                self.call_named_sub(name, arg_vals, line, ctx)
10739            }
10740            ExprKind::IndirectCall {
10741                target,
10742                args,
10743                ampersand: _,
10744                pass_caller_arglist,
10745            } => {
10746                let tval = self.eval_expr(target)?;
10747                let arg_vals = if *pass_caller_arglist {
10748                    self.scope.get_array("_")
10749                } else {
10750                    // BUG-037: explicit `@array` / `%hash` operands flatten
10751                    // into the call list. Other arg shapes (qw, list exprs,
10752                    // function calls) keep the scalar-context evaluation so
10753                    // threading-style sites (`~> EXPR fn { ... }` desugars to
10754                    // `IndirectCall { target: <fn>, args: [EXPR] }`) pass the
10755                    // LHS as one threaded value rather than as flattened
10756                    // elements.
10757                    let mut v = Vec::with_capacity(args.len());
10758                    for a in args {
10759                        if matches!(a.kind, ExprKind::ArrayVar(_) | ExprKind::HashVar(_)) {
10760                            let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
10761                            if let Some(items) = val.as_array_vec() {
10762                                v.extend(items);
10763                            } else {
10764                                v.push(val);
10765                            }
10766                        } else {
10767                            v.push(self.eval_expr(a)?);
10768                        }
10769                    }
10770                    v
10771                };
10772                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
10773            }
10774            ExprKind::MethodCall {
10775                object,
10776                method,
10777                args,
10778                super_call,
10779            } => {
10780                let obj = self.eval_expr(object)?;
10781                let mut arg_vals = vec![obj.clone()];
10782                for a in args {
10783                    arg_vals.push(self.eval_expr(a)?);
10784                }
10785                if let Some(r) =
10786                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
10787                {
10788                    return r.map_err(Into::into);
10789                }
10790                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
10791                    return r.map_err(Into::into);
10792                }
10793                // Get class name
10794                let class = if let Some(b) = obj.as_blessed_ref() {
10795                    b.class.clone()
10796                } else if let Some(s) = obj.as_str() {
10797                    s // Class->method()
10798                } else {
10799                    return Err(
10800                        StrykeError::runtime("Can't call method on non-object", line).into(),
10801                    );
10802                };
10803                if method == "VERSION" && !*super_call {
10804                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
10805                        return Ok(ver);
10806                    }
10807                }
10808                // UNIVERSAL methods: isa, can, DOES
10809                if !*super_call {
10810                    match method.as_str() {
10811                        "isa" => {
10812                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
10813                            let mro = self.mro_linearize(&class);
10814                            let result = mro.iter().any(|c| c == &target);
10815                            return Ok(StrykeValue::integer(if result { 1 } else { 0 }));
10816                        }
10817                        "can" => {
10818                            let target_method =
10819                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
10820                            let found = self
10821                                .resolve_method_full_name(&class, &target_method, false)
10822                                .and_then(|fq| self.subs.get(&fq))
10823                                .is_some();
10824                            if found {
10825                                return Ok(StrykeValue::code_ref(Arc::new(StrykeSub {
10826                                    name: target_method,
10827                                    params: vec![],
10828                                    body: vec![],
10829                                    closure_env: None,
10830                                    prototype: None,
10831                                    fib_like: None,
10832                                })));
10833                            } else {
10834                                return Ok(StrykeValue::UNDEF);
10835                            }
10836                        }
10837                        "DOES" => {
10838                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
10839                            let mro = self.mro_linearize(&class);
10840                            let result = mro.iter().any(|c| c == &target);
10841                            return Ok(StrykeValue::integer(if result { 1 } else { 0 }));
10842                        }
10843                        _ => {}
10844                    }
10845                }
10846                let full_name = self
10847                    .resolve_method_full_name(&class, method, *super_call)
10848                    .ok_or_else(|| {
10849                        StrykeError::runtime(
10850                            format!(
10851                                "Can't locate method \"{}\" for invocant \"{}\"",
10852                                method, class
10853                            ),
10854                            line,
10855                        )
10856                    })?;
10857                if let Some(sub) = self.subs.get(&full_name).cloned() {
10858                    self.call_sub(&sub, arg_vals, ctx, line)
10859                } else if method == "new" && !*super_call {
10860                    // Default constructor
10861                    self.builtin_new(&class, arg_vals, line)
10862                } else if let Some(r) =
10863                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
10864                {
10865                    r
10866                } else {
10867                    Err(StrykeError::runtime(
10868                        format!(
10869                            "Can't locate method \"{}\" in package \"{}\"",
10870                            method, class
10871                        ),
10872                        line,
10873                    )
10874                    .into())
10875                }
10876            }
10877
10878            // Print/Say/Printf
10879            ExprKind::Print { handle, args } => {
10880                self.exec_print(handle.as_deref(), args, false, line)
10881            }
10882            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
10883            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
10884            ExprKind::Die(args) => {
10885                if args.is_empty() {
10886                    // `die` with no args: re-die with current $@ or "Died"
10887                    let current = self.scope.get_scalar("@");
10888                    let msg = if current.is_undef() || current.to_string().is_empty() {
10889                        let mut m = "Died".to_string();
10890                        m.push_str(&self.die_warn_at_suffix(line));
10891                        m.push('\n');
10892                        m
10893                    } else {
10894                        current.to_string()
10895                    };
10896                    self.fire_pseudosig_die(&msg, line)?;
10897                    return Err(StrykeError::die(msg, line).into());
10898                }
10899                // Single ref argument: store the ref value in $@
10900                if args.len() == 1 {
10901                    let v = self.eval_expr(&args[0])?;
10902                    if v.as_hash_ref().is_some()
10903                        || v.as_blessed_ref().is_some()
10904                        || v.as_array_ref().is_some()
10905                        || v.as_code_ref().is_some()
10906                    {
10907                        let msg = v.to_string();
10908                        self.fire_pseudosig_die(&msg, line)?;
10909                        return Err(StrykeError::die_with_value(v, msg, line).into());
10910                    }
10911                }
10912                let mut msg = String::new();
10913                for a in args {
10914                    let v = self.eval_expr(a)?;
10915                    msg.push_str(&v.to_string());
10916                }
10917                if msg.is_empty() {
10918                    msg = "Died".to_string();
10919                }
10920                if !msg.ends_with('\n') {
10921                    msg.push_str(&self.die_warn_at_suffix(line));
10922                    msg.push('\n');
10923                }
10924                self.fire_pseudosig_die(&msg, line)?;
10925                Err(StrykeError::die(msg, line).into())
10926            }
10927            ExprKind::Warn(args) => {
10928                let mut msg = String::new();
10929                for a in args {
10930                    let v = self.eval_expr(a)?;
10931                    msg.push_str(&v.to_string());
10932                }
10933                if msg.is_empty() {
10934                    msg = "Warning: something's wrong".to_string();
10935                }
10936                if !msg.ends_with('\n') {
10937                    msg.push_str(&self.die_warn_at_suffix(line));
10938                    msg.push('\n');
10939                }
10940                self.fire_pseudosig_warn(&msg, line)?;
10941                Ok(StrykeValue::integer(1))
10942            }
10943
10944            // Regex
10945            ExprKind::Match {
10946                expr,
10947                pattern,
10948                flags,
10949                scalar_g,
10950                delim: _,
10951            } => {
10952                let val = self.eval_expr(expr)?;
10953                if val.is_iterator() {
10954                    let source = crate::map_stream::into_pull_iter(val);
10955                    let re = self.compile_regex(pattern, flags, line)?;
10956                    let global = flags.contains('g');
10957                    if global {
10958                        return Ok(StrykeValue::iterator(std::sync::Arc::new(
10959                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
10960                        )));
10961                    } else {
10962                        return Ok(StrykeValue::iterator(std::sync::Arc::new(
10963                            crate::map_stream::MatchStreamIterator::new(source, re),
10964                        )));
10965                    }
10966                }
10967                let s = val.to_string();
10968                let pos_key = match &expr.kind {
10969                    ExprKind::ScalarVar(n) => n.as_str(),
10970                    _ => "_",
10971                };
10972                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
10973            }
10974            ExprKind::Substitution {
10975                expr,
10976                pattern,
10977                replacement,
10978                flags,
10979                delim: _,
10980            } => {
10981                let val = self.eval_expr(expr)?;
10982                if val.is_iterator() {
10983                    let source = crate::map_stream::into_pull_iter(val);
10984                    let re = self.compile_regex(pattern, flags, line)?;
10985                    let global = flags.contains('g');
10986                    return Ok(StrykeValue::iterator(std::sync::Arc::new(
10987                        crate::map_stream::SubstStreamIterator::new(
10988                            source,
10989                            re,
10990                            normalize_replacement_backrefs(replacement),
10991                            global,
10992                        ),
10993                    )));
10994                }
10995                let s = val.to_string();
10996                self.regex_subst_execute(
10997                    s,
10998                    pattern,
10999                    replacement.as_str(),
11000                    flags.as_str(),
11001                    expr,
11002                    line,
11003                )
11004            }
11005            ExprKind::Transliterate {
11006                expr,
11007                from,
11008                to,
11009                flags,
11010                delim: _,
11011            } => {
11012                let val = self.eval_expr(expr)?;
11013                if val.is_iterator() {
11014                    let source = crate::map_stream::into_pull_iter(val);
11015                    return Ok(StrykeValue::iterator(std::sync::Arc::new(
11016                        crate::map_stream::TransliterateStreamIterator::new(
11017                            source, from, to, flags,
11018                        ),
11019                    )));
11020                }
11021                let s = val.to_string();
11022                self.regex_transliterate_execute(
11023                    s,
11024                    from.as_str(),
11025                    to.as_str(),
11026                    flags.as_str(),
11027                    expr,
11028                    line,
11029                )
11030            }
11031
11032            // List operations
11033            ExprKind::MapExpr {
11034                block,
11035                list,
11036                flatten_array_refs,
11037                stream,
11038            } => {
11039                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11040                if *stream {
11041                    let out =
11042                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
11043                    if ctx == WantarrayCtx::List {
11044                        return Ok(out);
11045                    }
11046                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11047                }
11048                let items = list_val.to_list();
11049                if items.len() == 1 {
11050                    if let Some(p) = items[0].as_pipeline() {
11051                        if *flatten_array_refs {
11052                            return Err(StrykeError::runtime(
11053                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
11054                                line,
11055                            )
11056                            .into());
11057                        }
11058                        let sub = self.anon_coderef_from_block(block);
11059                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
11060                        return Ok(StrykeValue::pipeline(Arc::clone(&p)));
11061                    }
11062                }
11063                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
11064                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
11065                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
11066                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
11067                let mut result = Vec::new();
11068                for item in items {
11069                    self.scope.set_topic(item);
11070                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
11071                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
11072                }
11073                if ctx == WantarrayCtx::List {
11074                    Ok(StrykeValue::array(result))
11075                } else {
11076                    Ok(StrykeValue::integer(result.len() as i64))
11077                }
11078            }
11079            ExprKind::ForEachExpr { block, list } => {
11080                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11081                // Lazy: consume iterator one-at-a-time without materializing.
11082                if list_val.is_iterator() {
11083                    let iter = list_val.into_iterator();
11084                    let mut count = 0i64;
11085                    while let Some(item) = iter.next_item() {
11086                        count += 1;
11087                        self.scope.set_topic(item);
11088                        self.exec_block(block)?;
11089                    }
11090                    return Ok(StrykeValue::integer(count));
11091                }
11092                let items = list_val.to_list();
11093                let count = items.len();
11094                for item in items {
11095                    self.scope.set_topic(item);
11096                    self.exec_block(block)?;
11097                }
11098                Ok(StrykeValue::integer(count as i64))
11099            }
11100            ExprKind::MapExprComma {
11101                expr,
11102                list,
11103                flatten_array_refs,
11104                stream,
11105            } => {
11106                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11107                if *stream {
11108                    let out =
11109                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
11110                    if ctx == WantarrayCtx::List {
11111                        return Ok(out);
11112                    }
11113                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11114                }
11115                let items = list_val.to_list();
11116                let mut result = Vec::new();
11117                for item in items {
11118                    // EXPR-form: no `{}` block boundary, so don't shift the
11119                    // topic chain or zero slot 1+. Just rebind `$_` / `$_0`.
11120                    // This makes `map _1, @$_` read the surrounding fn's
11121                    // second arg per iter; block-form `map { ... }` still
11122                    // gets a full `set_topic` via its CodeRef call.
11123                    self.scope.set_topic_local(item.clone());
11124                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11125                    // Coderef-in-block-position: `map $f, @l` calls `$f($_)`
11126                    // when `$f` is a code reference. Skipped under `--compat`
11127                    // (Perl semantics: re-evaluate expr per iteration as value).
11128                    let val = if !crate::compat_mode() {
11129                        if let Some(sub) = val.as_code_ref() {
11130                            let sub = sub.clone();
11131                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::List, line)?
11132                        } else {
11133                            val
11134                        }
11135                    } else {
11136                        val
11137                    };
11138                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
11139                }
11140                if ctx == WantarrayCtx::List {
11141                    Ok(StrykeValue::array(result))
11142                } else {
11143                    Ok(StrykeValue::integer(result.len() as i64))
11144                }
11145            }
11146            ExprKind::GrepExpr {
11147                block,
11148                list,
11149                keyword,
11150            } => {
11151                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11152                if keyword.is_stream() {
11153                    let out = self.filter_stream_block_output(list_val, block, line)?;
11154                    if ctx == WantarrayCtx::List {
11155                        return Ok(out);
11156                    }
11157                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11158                }
11159                let items = list_val.to_list();
11160                if items.len() == 1 {
11161                    if let Some(p) = items[0].as_pipeline() {
11162                        let sub = self.anon_coderef_from_block(block);
11163                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
11164                        return Ok(StrykeValue::pipeline(Arc::clone(&p)));
11165                    }
11166                }
11167                let mut result = Vec::new();
11168                for item in items {
11169                    self.scope.set_topic(item.clone());
11170                    let val = self.exec_block(block)?;
11171                    // Bare regex in block → match against $_ (Perl: /pat/ in
11172                    // grep is `$_ =~ /pat/`, not a truthy regex object).
11173                    let keep = if let Some(re) = val.as_regex() {
11174                        re.is_match(&item.to_string())
11175                    } else {
11176                        val.is_true()
11177                    };
11178                    if keep {
11179                        result.push(item);
11180                    }
11181                }
11182                if ctx == WantarrayCtx::List {
11183                    Ok(StrykeValue::array(result))
11184                } else {
11185                    Ok(StrykeValue::integer(result.len() as i64))
11186                }
11187            }
11188            ExprKind::GrepExprComma {
11189                expr,
11190                list,
11191                keyword,
11192            } => {
11193                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11194                if keyword.is_stream() {
11195                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
11196                    if ctx == WantarrayCtx::List {
11197                        return Ok(out);
11198                    }
11199                    return Ok(StrykeValue::integer(out.to_list().len() as i64));
11200                }
11201                let items = list_val.to_list();
11202                let mut result = Vec::new();
11203                for item in items {
11204                    // EXPR-form: see comment in MapExprComma above. No block
11205                    // boundary, so no chain shift; `_1` reads the surrounding
11206                    // fn's second arg per iter rather than getting nuked.
11207                    self.scope.set_topic_local(item.clone());
11208                    let val = self.eval_expr(expr)?;
11209                    // Coderef-in-block-position: `grep $f, @l` calls `$f($_)`
11210                    // when `$f` is a code reference, then filters by truthiness
11211                    // of the call result. Skipped under `--compat`.
11212                    let val = if !crate::compat_mode() {
11213                        if let Some(sub) = val.as_code_ref() {
11214                            let sub = sub.clone();
11215                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?
11216                        } else {
11217                            val
11218                        }
11219                    } else {
11220                        val
11221                    };
11222                    let keep = if let Some(re) = val.as_regex() {
11223                        re.is_match(&item.to_string())
11224                    } else {
11225                        val.is_true()
11226                    };
11227                    if keep {
11228                        result.push(item);
11229                    }
11230                }
11231                if ctx == WantarrayCtx::List {
11232                    Ok(StrykeValue::array(result))
11233                } else {
11234                    Ok(StrykeValue::integer(result.len() as i64))
11235                }
11236            }
11237            ExprKind::SortExpr { cmp, list } => {
11238                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11239                let mut items = list_val.to_list();
11240                match cmp {
11241                    Some(SortComparator::Code(code_expr)) => {
11242                        let sub = self.eval_expr(code_expr)?;
11243                        let Some(sub) = sub.as_code_ref() else {
11244                            return Err(StrykeError::runtime(
11245                                "sort: comparator must be a code reference",
11246                                line,
11247                            )
11248                            .into());
11249                        };
11250                        // Save topic chain before sort — set_sort_pair writes to $_
11251                        // which corrupts _< for subsequent pipeline stages.
11252                        let saved_topic = self.scope.save_topic_chain();
11253                        let sub = sub.clone();
11254                        items.sort_by(|a, b| {
11255                            // `set_sort_pair` keeps Perl-style `$a`/`$b` package-
11256                            // global access for `sub cmp { $a <=> $b }`. Passing
11257                            // `(a, b)` as positional args lets stryke lambdas
11258                            // `fn ($a, $b) { $b <=> $a }` receive them via @_.
11259                            self.scope.set_sort_pair(a.clone(), b.clone());
11260                            match self.call_sub(&sub, vec![a.clone(), b.clone()], ctx, line) {
11261                                Ok(v) => {
11262                                    let n = v.to_int();
11263                                    if n < 0 {
11264                                        Ordering::Less
11265                                    } else if n > 0 {
11266                                        Ordering::Greater
11267                                    } else {
11268                                        Ordering::Equal
11269                                    }
11270                                }
11271                                Err(_) => Ordering::Equal,
11272                            }
11273                        });
11274                        self.scope.restore_topic_chain(saved_topic);
11275                    }
11276                    Some(SortComparator::Block(cmp_block)) => {
11277                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
11278                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
11279                        } else {
11280                            // Save topic chain before sort — set_sort_pair writes to $_
11281                            // which corrupts _< for subsequent pipeline stages.
11282                            let saved_topic = self.scope.save_topic_chain();
11283                            let cmp_block = cmp_block.clone();
11284                            items.sort_by(|a, b| {
11285                                self.scope.set_sort_pair(a.clone(), b.clone());
11286                                match self.exec_block(&cmp_block) {
11287                                    Ok(v) => {
11288                                        let n = v.to_int();
11289                                        if n < 0 {
11290                                            Ordering::Less
11291                                        } else if n > 0 {
11292                                            Ordering::Greater
11293                                        } else {
11294                                            Ordering::Equal
11295                                        }
11296                                    }
11297                                    Err(_) => Ordering::Equal,
11298                                }
11299                            });
11300                            self.scope.restore_topic_chain(saved_topic);
11301                        }
11302                    }
11303                    None => {
11304                        items.sort_by_key(|a| a.to_string());
11305                    }
11306                }
11307                Ok(StrykeValue::array(items))
11308            }
11309            ExprKind::Rev(expr) => {
11310                // Eval in scalar context first to preserve set/hash/array ref types
11311                let val = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
11312                if val.is_iterator() {
11313                    return Ok(StrykeValue::iterator(Arc::new(
11314                        crate::value::RevIterator::new(val.into_iterator()),
11315                    )));
11316                }
11317                if let Some(s) = crate::value::set_payload(&val) {
11318                    let mut out = crate::value::PerlSet::new();
11319                    for (k, v) in s.iter().rev() {
11320                        out.insert(k.clone(), v.clone());
11321                    }
11322                    return Ok(StrykeValue::set(Arc::new(out)));
11323                }
11324                if let Some(ar) = val.as_array_ref() {
11325                    let items: Vec<_> = ar.read().iter().rev().cloned().collect();
11326                    return Ok(StrykeValue::array_ref(Arc::new(parking_lot::RwLock::new(
11327                        items,
11328                    ))));
11329                }
11330                if let Some(hr) = val.as_hash_ref() {
11331                    let mut out: indexmap::IndexMap<String, StrykeValue> =
11332                        indexmap::IndexMap::new();
11333                    for (k, v) in hr.read().iter() {
11334                        out.insert(v.to_string(), StrykeValue::string(k.clone()));
11335                    }
11336                    return Ok(StrykeValue::hash_ref(Arc::new(parking_lot::RwLock::new(
11337                        out,
11338                    ))));
11339                }
11340                // Re-eval in list context for bare arrays/hashes
11341                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11342                if let Some(hm) = val.as_hash_map() {
11343                    let mut out: indexmap::IndexMap<String, StrykeValue> =
11344                        indexmap::IndexMap::new();
11345                    for (k, v) in hm.iter() {
11346                        out.insert(v.to_string(), StrykeValue::string(k.clone()));
11347                    }
11348                    return Ok(StrykeValue::hash(out));
11349                }
11350                if val.as_array_vec().is_some() {
11351                    let mut items = val.to_list();
11352                    items.reverse();
11353                    Ok(StrykeValue::array(items))
11354                } else {
11355                    let items = val.to_list();
11356                    if items.len() > 1 {
11357                        let mut items = items;
11358                        items.reverse();
11359                        Ok(StrykeValue::array(items))
11360                    } else {
11361                        let s = val.to_string();
11362                        Ok(StrykeValue::string(s.chars().rev().collect()))
11363                    }
11364                }
11365            }
11366            ExprKind::ReverseExpr(list) => {
11367                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11368                match ctx {
11369                    WantarrayCtx::List => {
11370                        let mut items = val.to_list();
11371                        items.reverse();
11372                        Ok(StrykeValue::array(items))
11373                    }
11374                    _ => {
11375                        let items = val.to_list();
11376                        let s: String = items.iter().map(|v| v.to_string()).collect();
11377                        Ok(StrykeValue::string(s.chars().rev().collect()))
11378                    }
11379                }
11380            }
11381
11382            // ── Parallel operations (rayon-powered) ──
11383            ExprKind::ParLinesExpr {
11384                path,
11385                callback,
11386                progress,
11387            } => self.eval_par_lines_expr(
11388                path.as_ref(),
11389                callback.as_ref(),
11390                progress.as_deref(),
11391                line,
11392            ),
11393            ExprKind::ParWalkExpr {
11394                path,
11395                callback,
11396                progress,
11397            } => {
11398                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
11399            }
11400            ExprKind::PwatchExpr { path, callback } => {
11401                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
11402            }
11403            ExprKind::PMapExpr {
11404                block,
11405                list,
11406                progress,
11407                flat_outputs,
11408                on_cluster,
11409                stream,
11410            } => {
11411                let show_progress = progress
11412                    .as_ref()
11413                    .map(|p| self.eval_expr(p))
11414                    .transpose()?
11415                    .map(|v| v.is_true())
11416                    .unwrap_or(false);
11417                // List context for the operand so `@_` / `@arr` flatten to
11418                // their elements instead of numifying to the array length.
11419                // Scalar context was producing `pmap{_*2}` over `@_` of size
11420                // 13 → one iteration with `_=13` → `[26]` (chunk-size × 2)
11421                // instead of 13 doubled values; same shape inside `~p>` chunk
11422                // workers and at top level.
11423                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11424                if let Some(cluster_e) = on_cluster {
11425                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
11426                    return self.eval_pmap_remote(
11427                        cluster_val,
11428                        list_val,
11429                        show_progress,
11430                        block,
11431                        *flat_outputs,
11432                        line,
11433                    );
11434                }
11435                if *stream {
11436                    let source = crate::map_stream::into_pull_iter(list_val);
11437                    let sub = self.anon_coderef_from_block(block);
11438                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
11439                    return Ok(StrykeValue::iterator(Arc::new(
11440                        crate::map_stream::PMapStreamIterator::new(
11441                            source,
11442                            sub,
11443                            self.subs.clone(),
11444                            capture,
11445                            atomic_arrays,
11446                            atomic_hashes,
11447                            *flat_outputs,
11448                        ),
11449                    )));
11450                }
11451                let items = list_val.to_list();
11452                let block = block.clone();
11453                let subs = self.subs.clone();
11454                let (scope_capture, atomic_arrays, atomic_hashes) =
11455                    self.scope.capture_with_atomics();
11456                let pmap_progress = PmapProgress::new(show_progress, items.len());
11457
11458                if *flat_outputs {
11459                    let mut indexed: Vec<(usize, Vec<StrykeValue>)> = items
11460                        .into_par_iter()
11461                        .enumerate()
11462                        .map(|(i, item)| {
11463                            let mut local_interp = VMHelper::new();
11464                            local_interp.subs = subs.clone();
11465                            local_interp.scope.restore_capture(&scope_capture);
11466                            local_interp
11467                                .scope
11468                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11469                            local_interp.enable_parallel_guard();
11470                            local_interp.scope.set_topic(item);
11471                            let val = match local_interp.exec_block(&block) {
11472                                Ok(val) => val,
11473                                Err(_) => StrykeValue::UNDEF,
11474                            };
11475                            let chunk = val.map_flatten_outputs(true);
11476                            pmap_progress.tick();
11477                            (i, chunk)
11478                        })
11479                        .collect();
11480                    pmap_progress.finish();
11481                    indexed.sort_by_key(|(i, _)| *i);
11482                    let results: Vec<StrykeValue> =
11483                        indexed.into_iter().flat_map(|(_, v)| v).collect();
11484                    Ok(StrykeValue::array(results))
11485                } else {
11486                    let results: Vec<StrykeValue> = items
11487                        .into_par_iter()
11488                        .map(|item| {
11489                            let mut local_interp = VMHelper::new();
11490                            local_interp.subs = subs.clone();
11491                            local_interp.scope.restore_capture(&scope_capture);
11492                            local_interp
11493                                .scope
11494                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11495                            local_interp.enable_parallel_guard();
11496                            local_interp.scope.set_topic(item);
11497                            let val = match local_interp.exec_block(&block) {
11498                                Ok(val) => val,
11499                                Err(_) => StrykeValue::UNDEF,
11500                            };
11501                            pmap_progress.tick();
11502                            val
11503                        })
11504                        .collect();
11505                    pmap_progress.finish();
11506                    Ok(StrykeValue::array(results))
11507                }
11508            }
11509            ExprKind::PMapChunkedExpr {
11510                chunk_size,
11511                block,
11512                list,
11513                progress,
11514            } => {
11515                let show_progress = progress
11516                    .as_ref()
11517                    .map(|p| self.eval_expr(p))
11518                    .transpose()?
11519                    .map(|v| v.is_true())
11520                    .unwrap_or(false);
11521                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
11522                let list_val = self.eval_expr(list)?;
11523                let items = list_val.to_list();
11524                let block = block.clone();
11525                let subs = self.subs.clone();
11526                let (scope_capture, atomic_arrays, atomic_hashes) =
11527                    self.scope.capture_with_atomics();
11528
11529                let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = items
11530                    .chunks(chunk_n)
11531                    .enumerate()
11532                    .map(|(i, c)| (i, c.to_vec()))
11533                    .collect();
11534
11535                let n_chunks = indexed_chunks.len();
11536                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
11537
11538                let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
11539                    .into_par_iter()
11540                    .map(|(chunk_idx, chunk)| {
11541                        let mut local_interp = VMHelper::new();
11542                        local_interp.subs = subs.clone();
11543                        local_interp.scope.restore_capture(&scope_capture);
11544                        local_interp
11545                            .scope
11546                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11547                        local_interp.enable_parallel_guard();
11548                        let mut out = Vec::with_capacity(chunk.len());
11549                        for item in chunk {
11550                            local_interp.scope.set_topic(item);
11551                            match local_interp.exec_block(&block) {
11552                                Ok(val) => out.push(val),
11553                                Err(_) => out.push(StrykeValue::UNDEF),
11554                            }
11555                        }
11556                        pmap_progress.tick();
11557                        (chunk_idx, out)
11558                    })
11559                    .collect();
11560
11561                pmap_progress.finish();
11562                chunk_results.sort_by_key(|(i, _)| *i);
11563                let results: Vec<StrykeValue> =
11564                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
11565                Ok(StrykeValue::array(results))
11566            }
11567            ExprKind::PGrepExpr {
11568                block,
11569                list,
11570                progress,
11571                stream,
11572            } => {
11573                let show_progress = progress
11574                    .as_ref()
11575                    .map(|p| self.eval_expr(p))
11576                    .transpose()?
11577                    .map(|v| v.is_true())
11578                    .unwrap_or(false);
11579                let list_val = self.eval_expr(list)?;
11580                if *stream {
11581                    let source = crate::map_stream::into_pull_iter(list_val);
11582                    let sub = self.anon_coderef_from_block(block);
11583                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
11584                    return Ok(StrykeValue::iterator(Arc::new(
11585                        crate::map_stream::PGrepStreamIterator::new(
11586                            source,
11587                            sub,
11588                            self.subs.clone(),
11589                            capture,
11590                            atomic_arrays,
11591                            atomic_hashes,
11592                        ),
11593                    )));
11594                }
11595                let items = list_val.to_list();
11596                let block = block.clone();
11597                let subs = self.subs.clone();
11598                let (scope_capture, atomic_arrays, atomic_hashes) =
11599                    self.scope.capture_with_atomics();
11600                let pmap_progress = PmapProgress::new(show_progress, items.len());
11601
11602                let results: Vec<StrykeValue> = items
11603                    .into_par_iter()
11604                    .filter_map(|item| {
11605                        let mut local_interp = VMHelper::new();
11606                        local_interp.subs = subs.clone();
11607                        local_interp.scope.restore_capture(&scope_capture);
11608                        local_interp
11609                            .scope
11610                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11611                        local_interp.enable_parallel_guard();
11612                        local_interp.scope.set_topic(item.clone());
11613                        let keep = match local_interp.exec_block(&block) {
11614                            Ok(val) => val.is_true(),
11615                            Err(_) => false,
11616                        };
11617                        pmap_progress.tick();
11618                        if keep {
11619                            Some(item)
11620                        } else {
11621                            None
11622                        }
11623                    })
11624                    .collect();
11625                pmap_progress.finish();
11626                Ok(StrykeValue::array(results))
11627            }
11628            ExprKind::ParExpr { block, list } => {
11629                // Generic parallel-chunk wrapper: split input on a sensible
11630                // boundary (UTF-8 char-aligned for strings, element-aligned
11631                // for arrays), evaluate the block per chunk in parallel
11632                // with `$_` bound to the chunk, then concatenate results.
11633                //
11634                // Chunk count is capped at min(n_threads, 8) because each
11635                // chunk pays a fixed `VMHelper::new()` setup cost (env-var
11636                // parsing, PATH/FPATH split, term-info ioctl, IndexMap
11637                // declarations). On 18-core machines, splitting 18 ways
11638                // makes setup overhead dominate the actual work.
11639                // List context for the operand so `@arr` flattens to its
11640                // elements instead of numifying to the array length —
11641                // matches PMapExpr's eval shape so `par { } @arr` and
11642                // `pmap { } @arr` agree on input semantics.
11643                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11644                let n_threads = rayon::current_num_threads().clamp(1, 8);
11645                let chunks = par_chunk_value(&list_val, n_threads);
11646                if chunks.len() < 2 {
11647                    // Below break-even (small input or unsupported value type):
11648                    // run the block once with the original input as `$_`.
11649                    self.scope.set_topic(list_val);
11650                    let v = self.exec_block(block)?;
11651                    return Ok(v);
11652                }
11653                let block_clone = block.clone();
11654                let subs = self.subs.clone();
11655                let (scope_capture, atomic_arrays, atomic_hashes) =
11656                    self.scope.capture_with_atomics();
11657                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
11658                let err_w = Arc::clone(&first_err);
11659                let per_chunk: Vec<Vec<StrykeValue>> = chunks
11660                    .into_par_iter()
11661                    .map(|chunk| {
11662                        if err_w.lock().is_some() {
11663                            return Vec::new();
11664                        }
11665                        let mut local_interp = VMHelper::new();
11666                        local_interp.subs = subs.clone();
11667                        local_interp.scope.restore_capture(&scope_capture);
11668                        local_interp
11669                            .scope
11670                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11671                        local_interp.enable_parallel_guard();
11672                        local_interp.scope.set_topic(chunk);
11673                        match local_interp.exec_block(&block_clone) {
11674                            Ok(v) => v.map_flatten_outputs(true),
11675                            Err(e) => {
11676                                let mut g = err_w.lock();
11677                                if g.is_none() {
11678                                    *g = Some(format!("par: {:?}", e));
11679                                }
11680                                Vec::new()
11681                            }
11682                        }
11683                    })
11684                    .collect();
11685                if let Some(msg) = first_err.lock().take() {
11686                    return Err(FlowOrError::Error(StrykeError::runtime(msg, line)));
11687                }
11688                let total: usize = per_chunk.iter().map(|v| v.len()).sum();
11689                let mut out = Vec::with_capacity(total);
11690                for v in per_chunk {
11691                    out.extend(v);
11692                }
11693                Ok(StrykeValue::array(out))
11694            }
11695            ExprKind::ParReduceExpr {
11696                extract_block,
11697                reduce_block,
11698                list,
11699            } => {
11700                // Chunk INPUT, run extract per chunk in parallel, then
11701                // reduce pairwise across chunks. With an explicit reduce
11702                // block the user controls merging via `$a` / `$b`. Without
11703                // one, the merger is auto-picked based on the first
11704                // chunk's result type:
11705                //   - hash<num>  → key-wise add (canonical histogram merge)
11706                //   - number     → numeric `+`
11707                //   - array/list → concat
11708                //   - string     → concat
11709                // Use List context so `@a` expands to its elements, not its length.
11710                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11711                let n_threads = rayon::current_num_threads().clamp(1, 8);
11712                let chunks = par_chunk_value(&list_val, n_threads);
11713                if chunks.len() < 2 {
11714                    // Single-chunk fallback: bind the chunk elements to `@_`
11715                    // (not wrapped) so `~p> @a sum` correctly passes all elements
11716                    // to `sum(@_)`. Also set `$_` to the first element for
11717                    // backwards compat with scalar-style blocks.
11718                    let chunk_arr = match list_val.as_array_vec() {
11719                        Some(arr) => arr,
11720                        None => vec![list_val.clone()],
11721                    };
11722                    let first = chunk_arr.first().cloned().unwrap_or(StrykeValue::UNDEF);
11723                    self.scope.declare_array("_", chunk_arr);
11724                    self.scope.set_topic(first);
11725                    return self.exec_block(extract_block);
11726                }
11727                let extract = extract_block.clone();
11728                let subs = self.subs.clone();
11729                let (scope_capture, atomic_arrays, atomic_hashes) =
11730                    self.scope.capture_with_atomics();
11731                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
11732                let err_w = Arc::clone(&first_err);
11733                let per_chunk: Vec<StrykeValue> = chunks
11734                    .into_par_iter()
11735                    .map(|chunk| {
11736                        if err_w.lock().is_some() {
11737                            return StrykeValue::UNDEF;
11738                        }
11739                        let mut local = VMHelper::new();
11740                        local.subs = subs.clone();
11741                        local.scope.restore_capture(&scope_capture);
11742                        local.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
11743                        local.enable_parallel_guard();
11744                        // Bind the chunk elements to `@_` (not wrapped) so
11745                        // `~p> @a sum` correctly passes all elements to `sum(@_)`.
11746                        // Also set `$_` to the first element for backwards compat.
11747                        let chunk_arr = match chunk.as_array_vec() {
11748                            Some(arr) => arr,
11749                            None => vec![chunk.clone()],
11750                        };
11751                        let first = chunk_arr.first().cloned().unwrap_or(StrykeValue::UNDEF);
11752                        local.scope.declare_array("_", chunk_arr);
11753                        local.scope.set_topic(first);
11754                        match local.exec_block(&extract) {
11755                            Ok(v) => v,
11756                            Err(e) => {
11757                                let mut g = err_w.lock();
11758                                if g.is_none() {
11759                                    *g = Some(format!("par_reduce: {:?}", e));
11760                                }
11761                                StrykeValue::UNDEF
11762                            }
11763                        }
11764                    })
11765                    .collect();
11766                if let Some(msg) = first_err.lock().take() {
11767                    return Err(FlowOrError::Error(StrykeError::runtime(msg, line)));
11768                }
11769                if per_chunk.is_empty() {
11770                    return Ok(StrykeValue::UNDEF);
11771                }
11772                // Explicit reducer: pairwise via user block with $a/$b bound.
11773                if let Some(rb) = reduce_block {
11774                    let mut acc = per_chunk[0].clone();
11775                    for v in per_chunk.into_iter().skip(1) {
11776                        self.scope.declare_scalar("a", acc.clone());
11777                        self.scope.declare_scalar("b", v);
11778                        acc = self.exec_block(rb)?;
11779                    }
11780                    return Ok(acc);
11781                }
11782                // Auto-merge.
11783                Ok(par_reduce_auto_merge(per_chunk))
11784            }
11785            ExprKind::DistReduceExpr {
11786                cluster,
11787                extract_block,
11788                list,
11789            } => {
11790                // Distributed counterpart of `~p>` — chunk the source list
11791                // locally, ship each chunk as one JSON work-item via the
11792                // existing `cluster::run_cluster` SSH dispatcher (one slot
11793                // per ssh process, session_init once per slot, JOB frames
11794                // over a shared queue, retry budget per chunk).
11795                //
11796                // Each remote worker receives one chunk (a JSON array) as
11797                // `$_[0]`. We prepend `local @_ = @{$_[0]}` to the block so
11798                // the user-supplied stage chain sees `@_` bound to the
11799                // chunk's elements — identical surface to `~p>`'s extract
11800                // block.
11801                let cluster_pv = self.eval_expr(cluster)?;
11802                let Some(remote_cluster) = cluster_pv.as_remote_cluster() else {
11803                    return Err(StrykeError::runtime(
11804                        "~d>: expected cluster(...) value after `on`",
11805                        line,
11806                    )
11807                    .into());
11808                };
11809                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11810                let items_flat = list_val.to_list();
11811                if items_flat.is_empty() {
11812                    return Ok(StrykeValue::array(vec![]));
11813                }
11814                let (scope_capture, atomic_arrays, atomic_hashes) =
11815                    self.scope.capture_with_atomics();
11816                if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
11817                    return Err(StrykeError::runtime(
11818                        "~d>: mysync/atomic capture is not supported for remote workers",
11819                        line,
11820                    )
11821                    .into());
11822                }
11823                // Chunk size mirrors `~p>`'s heuristic: oversubscribe slots
11824                // by 4× so faster slots can pull more work via the shared
11825                // queue. Floor at 1, ceil at items.len() so we always get
11826                // at least one chunk per slot when items < slots.
11827                let n_slots = remote_cluster.slots.len().max(1);
11828                let target_chunks = (n_slots * 4).min(items_flat.len()).max(1);
11829                let chunk_size = items_flat.len().div_ceil(target_chunks);
11830                let mut chunk_items: Vec<serde_json::Value> = Vec::new();
11831                let mut chunk_jsons_buf: Vec<StrykeValue> = Vec::new();
11832                for item in items_flat.into_iter() {
11833                    chunk_jsons_buf.push(item);
11834                    if chunk_jsons_buf.len() >= chunk_size {
11835                        let drained: Vec<StrykeValue> = std::mem::take(&mut chunk_jsons_buf);
11836                        let as_array = StrykeValue::array(drained);
11837                        chunk_items.push(
11838                            crate::remote_wire::perl_to_json_value(&as_array)
11839                                .map_err(|e| StrykeError::runtime(e, line))?,
11840                        );
11841                    }
11842                }
11843                if !chunk_jsons_buf.is_empty() {
11844                    let as_array = StrykeValue::array(chunk_jsons_buf);
11845                    chunk_items.push(
11846                        crate::remote_wire::perl_to_json_value(&as_array)
11847                            .map_err(|e| StrykeError::runtime(e, line))?,
11848                    );
11849                }
11850                // Drop scope entries that can't be JSON-marshalled (the
11851                // `RemoteCluster` value itself, file handles, code refs,
11852                // etc.). The remote worker doesn't need them — they're
11853                // dispatcher-side context only. Any reference the user's
11854                // stage block legitimately needs goes through this filter
11855                // anyway, and a `RemoteCluster` in remote scope would be
11856                // nonsensical (no controller to dispatch through there).
11857                let cap_json: Vec<(String, serde_json::Value)> = scope_capture
11858                    .iter()
11859                    .filter_map(|(k, v)| {
11860                        crate::remote_wire::perl_to_json_value(v)
11861                            .ok()
11862                            .map(|j| (k.clone(), j))
11863                    })
11864                    .collect();
11865                let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
11866                // Remote agent (`remote_wire::run_job_local`) sets `$_`
11867                // to the chunk value (a flat array) but does NOT populate
11868                // `@_`. Two bridges:
11869                //   1. `@_ = $_;` prologue — make the user's `@_`-threaded
11870                //      stage chain see the chunk's elements as an array
11871                //      (matches `~p>` chunk-block surface).
11872                //   2. Wrap the body in `[ ... ]` so the last expression's
11873                //      list result survives the worker's scalar-context
11874                //      block return + JSON round-trip. Without this,
11875                //      `map { ... } @_` collapses to `scalar(@result) = N`
11876                //      before serialization.
11877                let user_block_src = crate::fmt::format_block(extract_block);
11878                let block_src = format!("@_ = $_;\n[ {user_block_src} ]");
11879                let result_values = crate::cluster::run_cluster(
11880                    &remote_cluster,
11881                    subs_prelude,
11882                    block_src,
11883                    cap_json,
11884                    chunk_items,
11885                )
11886                .map_err(|e| StrykeError::runtime(format!("~d> remote: {e}"), line))?;
11887                // Each chunk's extract returns a value; flat-concat across
11888                // chunks to mirror `~p>`'s auto-merge for list-shaped output.
11889                // Scalar-shaped chunk results stay as one element each.
11890                let mut merged: Vec<StrykeValue> = Vec::new();
11891                for v in result_values {
11892                    if let Some(items) = v.as_array_vec() {
11893                        merged.extend(items);
11894                    } else if let Some(ar) = v.as_array_ref() {
11895                        merged.extend(ar.read().iter().cloned());
11896                    } else {
11897                        merged.push(v);
11898                    }
11899                }
11900                Ok(StrykeValue::array(merged))
11901            }
11902            ExprKind::PForExpr {
11903                block,
11904                list,
11905                progress,
11906            } => {
11907                let show_progress = progress
11908                    .as_ref()
11909                    .map(|p| self.eval_expr(p))
11910                    .transpose()?
11911                    .map(|v| v.is_true())
11912                    .unwrap_or(false);
11913                let list_val = self.eval_expr(list)?;
11914                let items = list_val.to_list();
11915                let block = block.clone();
11916                let subs = self.subs.clone();
11917                let (scope_capture, atomic_arrays, atomic_hashes) =
11918                    self.scope.capture_with_atomics();
11919
11920                let pmap_progress = PmapProgress::new(show_progress, items.len());
11921                let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
11922                items.into_par_iter().for_each(|item| {
11923                    if first_err.lock().is_some() {
11924                        return;
11925                    }
11926                    let mut local_interp = VMHelper::new();
11927                    local_interp.subs = subs.clone();
11928                    local_interp.scope.restore_capture(&scope_capture);
11929                    local_interp
11930                        .scope
11931                        .restore_atomics(&atomic_arrays, &atomic_hashes);
11932                    local_interp.enable_parallel_guard();
11933                    local_interp.scope.set_topic(item);
11934                    match local_interp.exec_block(&block) {
11935                        Ok(_) => {}
11936                        Err(e) => {
11937                            let stryke = match e {
11938                                FlowOrError::Error(stryke) => stryke,
11939                                FlowOrError::Flow(_) => StrykeError::runtime(
11940                                    "return/last/next/redo not supported inside pfor block",
11941                                    line,
11942                                ),
11943                            };
11944                            let mut g = first_err.lock();
11945                            if g.is_none() {
11946                                *g = Some(stryke);
11947                            }
11948                        }
11949                    }
11950                    pmap_progress.tick();
11951                });
11952                pmap_progress.finish();
11953                if let Some(e) = first_err.lock().take() {
11954                    return Err(FlowOrError::Error(e));
11955                }
11956                Ok(StrykeValue::UNDEF)
11957            }
11958            ExprKind::FanExpr {
11959                count,
11960                block,
11961                progress,
11962                capture,
11963            } => {
11964                let show_progress = progress
11965                    .as_ref()
11966                    .map(|p| self.eval_expr(p))
11967                    .transpose()?
11968                    .map(|v| v.is_true())
11969                    .unwrap_or(false);
11970                let n = match count {
11971                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
11972                    None => self.parallel_thread_count(),
11973                };
11974                let block = block.clone();
11975                let subs = self.subs.clone();
11976                let (scope_capture, atomic_arrays, atomic_hashes) =
11977                    self.scope.capture_with_atomics();
11978
11979                let fan_progress = FanProgress::new(show_progress, n);
11980                if *capture {
11981                    if n == 0 {
11982                        return Ok(StrykeValue::array(Vec::new()));
11983                    }
11984                    let pairs: Vec<(usize, ExecResult)> = (0..n)
11985                        .into_par_iter()
11986                        .map(|i| {
11987                            fan_progress.start_worker(i);
11988                            let mut local_interp = VMHelper::new();
11989                            local_interp.subs = subs.clone();
11990                            local_interp.suppress_stdout = show_progress;
11991                            local_interp.scope.restore_capture(&scope_capture);
11992                            local_interp
11993                                .scope
11994                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11995                            local_interp.enable_parallel_guard();
11996                            local_interp.scope.set_topic(StrykeValue::integer(i as i64));
11997                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
11998                            let res = local_interp.exec_block(&block);
11999                            crate::parallel_trace::fan_worker_set_index(None);
12000                            fan_progress.finish_worker(i);
12001                            (i, res)
12002                        })
12003                        .collect();
12004                    fan_progress.finish();
12005                    let mut pairs = pairs;
12006                    pairs.sort_by_key(|(i, _)| *i);
12007                    let mut out = Vec::with_capacity(n);
12008                    for (_, r) in pairs {
12009                        match r {
12010                            Ok(v) => out.push(v),
12011                            Err(e) => return Err(e),
12012                        }
12013                    }
12014                    return Ok(StrykeValue::array(out));
12015                }
12016                let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
12017                (0..n).into_par_iter().for_each(|i| {
12018                    if first_err.lock().is_some() {
12019                        return;
12020                    }
12021                    fan_progress.start_worker(i);
12022                    let mut local_interp = VMHelper::new();
12023                    local_interp.subs = subs.clone();
12024                    local_interp.suppress_stdout = show_progress;
12025                    local_interp.scope.restore_capture(&scope_capture);
12026                    local_interp
12027                        .scope
12028                        .restore_atomics(&atomic_arrays, &atomic_hashes);
12029                    local_interp.enable_parallel_guard();
12030                    local_interp.scope.set_topic(StrykeValue::integer(i as i64));
12031                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
12032                    match local_interp.exec_block(&block) {
12033                        Ok(_) => {}
12034                        Err(e) => {
12035                            let stryke = match e {
12036                                FlowOrError::Error(stryke) => stryke,
12037                                FlowOrError::Flow(_) => StrykeError::runtime(
12038                                    "return/last/next/redo not supported inside fan block",
12039                                    line,
12040                                ),
12041                            };
12042                            let mut g = first_err.lock();
12043                            if g.is_none() {
12044                                *g = Some(stryke);
12045                            }
12046                        }
12047                    }
12048                    crate::parallel_trace::fan_worker_set_index(None);
12049                    fan_progress.finish_worker(i);
12050                });
12051                fan_progress.finish();
12052                if let Some(e) = first_err.lock().take() {
12053                    return Err(FlowOrError::Error(e));
12054                }
12055                Ok(StrykeValue::UNDEF)
12056            }
12057            ExprKind::RetryBlock {
12058                body,
12059                times,
12060                backoff,
12061            } => self.eval_retry_block(body, times, *backoff, line),
12062            ExprKind::RateLimitBlock {
12063                slot,
12064                max,
12065                window,
12066                body,
12067            } => self.eval_rate_limit_block(*slot, max, window, body, line),
12068            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
12069            ExprKind::GenBlock { body } => {
12070                let g = Arc::new(PerlGenerator {
12071                    block: body.clone(),
12072                    pc: Mutex::new(0),
12073                    scope_started: Mutex::new(false),
12074                    exhausted: Mutex::new(false),
12075                });
12076                Ok(StrykeValue::generator(g))
12077            }
12078            ExprKind::Yield(e) => {
12079                if !self.in_generator {
12080                    return Err(StrykeError::runtime("yield outside gen block", line).into());
12081                }
12082                let v = self.eval_expr(e)?;
12083                Err(FlowOrError::Flow(Flow::Yield(v)))
12084            }
12085            ExprKind::AlgebraicMatch { subject, arms } => {
12086                self.eval_algebraic_match(subject, arms, line)
12087            }
12088            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
12089                Ok(self.spawn_async_block(body))
12090            }
12091            ExprKind::Trace { body } => {
12092                crate::parallel_trace::trace_enter();
12093                let out = self.exec_block(body);
12094                crate::parallel_trace::trace_leave();
12095                out
12096            }
12097            ExprKind::Spinner { message, body } => {
12098                use std::io::Write as _;
12099                let msg = self.eval_expr(message)?.to_string();
12100                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
12101                let done2 = done.clone();
12102                let handle = std::thread::spawn(move || {
12103                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
12104                    let mut i = 0;
12105                    let stderr = std::io::stderr();
12106                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
12107                        {
12108                            let stdout = std::io::stdout();
12109                            let _stdout_lock = stdout.lock();
12110                            let mut err = stderr.lock();
12111                            let _ = write!(
12112                                err,
12113                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
12114                                frames[i % frames.len()],
12115                                msg
12116                            );
12117                            let _ = err.flush();
12118                        }
12119                        std::thread::sleep(std::time::Duration::from_millis(80));
12120                        i += 1;
12121                    }
12122                    let mut err = stderr.lock();
12123                    let _ = write!(err, "\r\x1b[2K");
12124                    let _ = err.flush();
12125                });
12126                let result = self.exec_block(body);
12127                done.store(true, std::sync::atomic::Ordering::Relaxed);
12128                let _ = handle.join();
12129                result
12130            }
12131            ExprKind::Timer { body } => {
12132                let start = std::time::Instant::now();
12133                self.exec_block(body)?;
12134                let ms = start.elapsed().as_secs_f64() * 1000.0;
12135                Ok(StrykeValue::float(ms))
12136            }
12137            ExprKind::Bench { body, times } => {
12138                let n = self.eval_expr(times)?.to_int();
12139                if n < 0 {
12140                    return Err(StrykeError::runtime(
12141                        "bench: iteration count must be non-negative",
12142                        line,
12143                    )
12144                    .into());
12145                }
12146                self.run_bench_block(body, n as usize, line)
12147            }
12148            ExprKind::Await(expr) => {
12149                let v = self.eval_expr(expr)?;
12150                if let Some(t) = v.as_async_task() {
12151                    t.await_result().map_err(FlowOrError::from)
12152                } else {
12153                    Ok(v)
12154                }
12155            }
12156            ExprKind::Slurp(e) => {
12157                let path = self.eval_expr(e)?.to_string();
12158                let path = self.resolve_stryke_path_string(&path);
12159                crate::perl_fs::read_file_text_or_glob(&path)
12160                    .map(StrykeValue::string)
12161                    .map_err(|e| {
12162                        FlowOrError::Error(StrykeError::runtime(format!("slurp: {}", e), line))
12163                    })
12164            }
12165            ExprKind::Capture(e) => {
12166                let cmd = self.eval_expr(e)?.to_string();
12167                let output = Command::new("sh")
12168                    .arg("-c")
12169                    .arg(&cmd)
12170                    .output()
12171                    .map_err(|e| {
12172                        FlowOrError::Error(StrykeError::runtime(format!("capture: {}", e), line))
12173                    })?;
12174                self.record_child_exit_status(output.status);
12175                let exitcode = output.status.code().unwrap_or(-1) as i64;
12176                let stdout = decode_utf8_or_latin1(&output.stdout);
12177                let stderr = decode_utf8_or_latin1(&output.stderr);
12178                Ok(StrykeValue::capture(Arc::new(CaptureResult {
12179                    stdout,
12180                    stderr,
12181                    exitcode,
12182                })))
12183            }
12184            ExprKind::Qx(e) => {
12185                let cmd = self.eval_expr(e)?.to_string();
12186                crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)
12187            }
12188            ExprKind::FetchUrl(e) => {
12189                let url = self.eval_expr(e)?.to_string();
12190                ureq::get(&url)
12191                    .call()
12192                    .map_err(|e| {
12193                        FlowOrError::Error(StrykeError::runtime(format!("fetch_url: {}", e), line))
12194                    })
12195                    .and_then(|r| {
12196                        r.into_string().map(StrykeValue::string).map_err(|e| {
12197                            FlowOrError::Error(StrykeError::runtime(
12198                                format!("fetch_url: {}", e),
12199                                line,
12200                            ))
12201                        })
12202                    })
12203            }
12204            ExprKind::Pchannel { capacity } => {
12205                if let Some(c) = capacity {
12206                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
12207                    Ok(crate::pchannel::create_bounded_pair(n))
12208                } else {
12209                    Ok(crate::pchannel::create_pair())
12210                }
12211            }
12212            ExprKind::PSortExpr {
12213                cmp,
12214                list,
12215                progress,
12216            } => {
12217                let show_progress = progress
12218                    .as_ref()
12219                    .map(|p| self.eval_expr(p))
12220                    .transpose()?
12221                    .map(|v| v.is_true())
12222                    .unwrap_or(false);
12223                let list_val = self.eval_expr(list)?;
12224                let mut items = list_val.to_list();
12225                let pmap_progress = PmapProgress::new(show_progress, 2);
12226                pmap_progress.tick();
12227                if let Some(cmp_block) = cmp {
12228                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
12229                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
12230                    } else {
12231                        let cmp_block = cmp_block.clone();
12232                        let subs = self.subs.clone();
12233                        let scope_capture = self.scope.capture();
12234                        items.par_sort_by(|a, b| {
12235                            let mut local_interp = VMHelper::new();
12236                            local_interp.subs = subs.clone();
12237                            local_interp.scope.restore_capture(&scope_capture);
12238                            local_interp.scope.set_sort_pair(a.clone(), b.clone());
12239                            match local_interp.exec_block(&cmp_block) {
12240                                Ok(v) => {
12241                                    let n = v.to_int();
12242                                    if n < 0 {
12243                                        std::cmp::Ordering::Less
12244                                    } else if n > 0 {
12245                                        std::cmp::Ordering::Greater
12246                                    } else {
12247                                        std::cmp::Ordering::Equal
12248                                    }
12249                                }
12250                                Err(_) => std::cmp::Ordering::Equal,
12251                            }
12252                        });
12253                    }
12254                } else {
12255                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
12256                }
12257                pmap_progress.tick();
12258                pmap_progress.finish();
12259                Ok(StrykeValue::array(items))
12260            }
12261
12262            ExprKind::ReduceExpr { block, list } => {
12263                let list_val = self.eval_expr(list)?;
12264                let items = list_val.to_list();
12265                if items.is_empty() {
12266                    return Ok(StrykeValue::UNDEF);
12267                }
12268                if items.len() == 1 {
12269                    return Ok(items.into_iter().next().unwrap());
12270                }
12271                let block = block.clone();
12272                let subs = self.subs.clone();
12273                let scope_capture = self.scope.capture();
12274                let mut acc = items[0].clone();
12275                for b in items.into_iter().skip(1) {
12276                    let mut local_interp = VMHelper::new();
12277                    local_interp.subs = subs.clone();
12278                    local_interp.scope.restore_capture(&scope_capture);
12279                    local_interp.scope.set_sort_pair(acc, b);
12280                    acc = match local_interp.exec_block(&block) {
12281                        Ok(val) => val,
12282                        Err(_) => StrykeValue::UNDEF,
12283                    };
12284                }
12285                Ok(acc)
12286            }
12287
12288            ExprKind::PReduceExpr {
12289                block,
12290                list,
12291                progress,
12292            } => {
12293                let show_progress = progress
12294                    .as_ref()
12295                    .map(|p| self.eval_expr(p))
12296                    .transpose()?
12297                    .map(|v| v.is_true())
12298                    .unwrap_or(false);
12299                let list_val = self.eval_expr(list)?;
12300                let items = list_val.to_list();
12301                if items.is_empty() {
12302                    return Ok(StrykeValue::UNDEF);
12303                }
12304                if items.len() == 1 {
12305                    return Ok(items.into_iter().next().unwrap());
12306                }
12307                let block = block.clone();
12308                let subs = self.subs.clone();
12309                let scope_capture = self.scope.capture();
12310                let pmap_progress = PmapProgress::new(show_progress, items.len());
12311
12312                let result = items
12313                    .into_par_iter()
12314                    .map(|x| {
12315                        pmap_progress.tick();
12316                        x
12317                    })
12318                    .reduce_with(|a, b| {
12319                        let mut local_interp = VMHelper::new();
12320                        local_interp.subs = subs.clone();
12321                        local_interp.scope.restore_capture(&scope_capture);
12322                        local_interp.scope.set_sort_pair(a, b);
12323                        match local_interp.exec_block(&block) {
12324                            Ok(val) => val,
12325                            Err(_) => StrykeValue::UNDEF,
12326                        }
12327                    });
12328                pmap_progress.finish();
12329                Ok(result.unwrap_or(StrykeValue::UNDEF))
12330            }
12331
12332            ExprKind::PReduceInitExpr {
12333                init,
12334                block,
12335                list,
12336                progress,
12337            } => {
12338                let show_progress = progress
12339                    .as_ref()
12340                    .map(|p| self.eval_expr(p))
12341                    .transpose()?
12342                    .map(|v| v.is_true())
12343                    .unwrap_or(false);
12344                let init_val = self.eval_expr(init)?;
12345                let list_val = self.eval_expr(list)?;
12346                let items = list_val.to_list();
12347                if items.is_empty() {
12348                    return Ok(init_val);
12349                }
12350                let block = block.clone();
12351                let subs = self.subs.clone();
12352                let scope_capture = self.scope.capture();
12353                let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
12354                if items.len() == 1 {
12355                    return Ok(fold_preduce_init_step(
12356                        &subs,
12357                        cap,
12358                        &block,
12359                        preduce_init_fold_identity(&init_val),
12360                        items.into_iter().next().unwrap(),
12361                    ));
12362                }
12363                let pmap_progress = PmapProgress::new(show_progress, items.len());
12364                let result = items
12365                    .into_par_iter()
12366                    .fold(
12367                        || preduce_init_fold_identity(&init_val),
12368                        |acc, item| {
12369                            pmap_progress.tick();
12370                            fold_preduce_init_step(&subs, cap, &block, acc, item)
12371                        },
12372                    )
12373                    .reduce(
12374                        || preduce_init_fold_identity(&init_val),
12375                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
12376                    );
12377                pmap_progress.finish();
12378                Ok(result)
12379            }
12380
12381            ExprKind::PMapReduceExpr {
12382                map_block,
12383                reduce_block,
12384                list,
12385                progress,
12386            } => {
12387                let show_progress = progress
12388                    .as_ref()
12389                    .map(|p| self.eval_expr(p))
12390                    .transpose()?
12391                    .map(|v| v.is_true())
12392                    .unwrap_or(false);
12393                let list_val = self.eval_expr(list)?;
12394                let items = list_val.to_list();
12395                if items.is_empty() {
12396                    return Ok(StrykeValue::UNDEF);
12397                }
12398                let map_block = map_block.clone();
12399                let reduce_block = reduce_block.clone();
12400                let subs = self.subs.clone();
12401                let scope_capture = self.scope.capture();
12402                if items.len() == 1 {
12403                    let mut local_interp = VMHelper::new();
12404                    local_interp.subs = subs.clone();
12405                    local_interp.scope.restore_capture(&scope_capture);
12406                    local_interp.scope.set_topic(items[0].clone());
12407                    return match local_interp.exec_block_no_scope(&map_block) {
12408                        Ok(v) => Ok(v),
12409                        Err(_) => Ok(StrykeValue::UNDEF),
12410                    };
12411                }
12412                let pmap_progress = PmapProgress::new(show_progress, items.len());
12413                let result = items
12414                    .into_par_iter()
12415                    .map(|item| {
12416                        let mut local_interp = VMHelper::new();
12417                        local_interp.subs = subs.clone();
12418                        local_interp.scope.restore_capture(&scope_capture);
12419                        local_interp.scope.set_topic(item);
12420                        let val = match local_interp.exec_block_no_scope(&map_block) {
12421                            Ok(val) => val,
12422                            Err(_) => StrykeValue::UNDEF,
12423                        };
12424                        pmap_progress.tick();
12425                        val
12426                    })
12427                    .reduce_with(|a, b| {
12428                        let mut local_interp = VMHelper::new();
12429                        local_interp.subs = subs.clone();
12430                        local_interp.scope.restore_capture(&scope_capture);
12431                        local_interp.scope.set_sort_pair(a, b);
12432                        match local_interp.exec_block_no_scope(&reduce_block) {
12433                            Ok(val) => val,
12434                            Err(_) => StrykeValue::UNDEF,
12435                        }
12436                    });
12437                pmap_progress.finish();
12438                Ok(result.unwrap_or(StrykeValue::UNDEF))
12439            }
12440
12441            ExprKind::PcacheExpr {
12442                block,
12443                list,
12444                progress,
12445            } => {
12446                let show_progress = progress
12447                    .as_ref()
12448                    .map(|p| self.eval_expr(p))
12449                    .transpose()?
12450                    .map(|v| v.is_true())
12451                    .unwrap_or(false);
12452                let list_val = self.eval_expr(list)?;
12453                let items = list_val.to_list();
12454                let block = block.clone();
12455                let subs = self.subs.clone();
12456                let scope_capture = self.scope.capture();
12457                let cache = &*crate::pcache::GLOBAL_PCACHE;
12458                let pmap_progress = PmapProgress::new(show_progress, items.len());
12459                let results: Vec<StrykeValue> = items
12460                    .into_par_iter()
12461                    .map(|item| {
12462                        let k = crate::pcache::cache_key(&item);
12463                        if let Some(v) = cache.get(&k) {
12464                            pmap_progress.tick();
12465                            return v.clone();
12466                        }
12467                        let mut local_interp = VMHelper::new();
12468                        local_interp.subs = subs.clone();
12469                        local_interp.scope.restore_capture(&scope_capture);
12470                        local_interp.scope.set_topic(item.clone());
12471                        let val = match local_interp.exec_block_no_scope(&block) {
12472                            Ok(v) => v,
12473                            Err(_) => StrykeValue::UNDEF,
12474                        };
12475                        cache.insert(k, val.clone());
12476                        pmap_progress.tick();
12477                        val
12478                    })
12479                    .collect();
12480                pmap_progress.finish();
12481                Ok(StrykeValue::array(results))
12482            }
12483
12484            ExprKind::PselectExpr { receivers, timeout } => {
12485                let mut rx_vals = Vec::with_capacity(receivers.len());
12486                for r in receivers {
12487                    rx_vals.push(self.eval_expr(r)?);
12488                }
12489                let dur = if let Some(t) = timeout.as_ref() {
12490                    Some(std::time::Duration::from_secs_f64(
12491                        self.eval_expr(t)?.to_number().max(0.0),
12492                    ))
12493                } else {
12494                    None
12495                };
12496                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
12497                    &rx_vals, dur, line,
12498                )?)
12499            }
12500
12501            // Array ops
12502            ExprKind::Push { array, values } => {
12503                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
12504            }
12505            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
12506            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
12507            ExprKind::Unshift { array, values } => {
12508                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
12509            }
12510            ExprKind::Splice {
12511                array,
12512                offset,
12513                length,
12514                replacement,
12515            } => self.eval_splice_expr(
12516                array.as_ref(),
12517                offset.as_deref(),
12518                length.as_deref(),
12519                replacement.as_slice(),
12520                ctx,
12521                line,
12522            ),
12523            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
12524            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
12525            ExprKind::Keys(expr) => {
12526                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
12527                let keys = Self::keys_from_value(val, line)?;
12528                if ctx == WantarrayCtx::List {
12529                    Ok(keys)
12530                } else {
12531                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
12532                    Ok(StrykeValue::integer(n as i64))
12533                }
12534            }
12535            ExprKind::Values(expr) => {
12536                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
12537                let vals = Self::values_from_value(val, line)?;
12538                if ctx == WantarrayCtx::List {
12539                    Ok(vals)
12540                } else {
12541                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
12542                    Ok(StrykeValue::integer(n as i64))
12543                }
12544            }
12545            ExprKind::Each(_) => {
12546                // Simplified: returns empty list (full iterator state would need more work)
12547                Ok(StrykeValue::array(vec![]))
12548            }
12549
12550            // String ops
12551            ExprKind::Chomp(expr) => {
12552                let val = self.eval_expr(expr)?;
12553                self.chomp_inplace_execute(val, expr)
12554            }
12555            ExprKind::Chop(expr) => {
12556                let val = self.eval_expr(expr)?;
12557                self.chop_inplace_execute(val, expr)
12558            }
12559            ExprKind::Length(expr) => {
12560                let val = self.eval_expr(expr)?;
12561                Ok(if let Some(a) = val.as_array_vec() {
12562                    StrykeValue::integer(a.len() as i64)
12563                } else if let Some(h) = val.as_hash_map() {
12564                    StrykeValue::integer(h.len() as i64)
12565                } else if let Some(b) = val.as_bytes_arc() {
12566                    // Raw byte buffer: always byte count, regardless of utf8 pragma.
12567                    StrykeValue::integer(b.len() as i64)
12568                } else {
12569                    let s = val.to_string();
12570                    let n = if self.utf8_pragma {
12571                        s.chars().count()
12572                    } else {
12573                        s.len()
12574                    };
12575                    StrykeValue::integer(n as i64)
12576                })
12577            }
12578            ExprKind::Substr {
12579                string,
12580                offset,
12581                length,
12582                replacement,
12583            } => self.eval_substr_expr(
12584                string.as_ref(),
12585                offset.as_ref(),
12586                length.as_deref(),
12587                replacement.as_deref(),
12588                line,
12589            ),
12590            ExprKind::Index {
12591                string,
12592                substr,
12593                position,
12594            } => {
12595                let s = self.eval_expr(string)?.to_string();
12596                let sub = self.eval_expr(substr)?.to_string();
12597                let pos = if let Some(p) = position {
12598                    self.eval_expr(p)?.to_int() as usize
12599                } else {
12600                    0
12601                };
12602                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
12603                Ok(StrykeValue::integer(result))
12604            }
12605            ExprKind::Rindex {
12606                string,
12607                substr,
12608                position,
12609            } => {
12610                let s = self.eval_expr(string)?.to_string();
12611                let sub = self.eval_expr(substr)?.to_string();
12612                let end = if let Some(p) = position {
12613                    self.eval_expr(p)?.to_int() as usize + sub.len()
12614                } else {
12615                    s.len()
12616                };
12617                let search = &s[..end.min(s.len())];
12618                let result = search.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
12619                Ok(StrykeValue::integer(result))
12620            }
12621            ExprKind::Sprintf { format, args } => {
12622                let fmt = self.eval_expr(format)?.to_string();
12623                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
12624                // builtins into individual format arguments.
12625                let mut arg_vals = Vec::new();
12626                for a in args {
12627                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
12628                    if let Some(items) = v.as_array_vec() {
12629                        arg_vals.extend(items);
12630                    } else {
12631                        arg_vals.push(v);
12632                    }
12633                }
12634                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
12635                Ok(StrykeValue::string(s))
12636            }
12637            ExprKind::JoinExpr { separator, list } => {
12638                let sep = self.eval_expr(separator)?.to_string();
12639                // Like Perl 5, arguments after the separator are evaluated in list context so
12640                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
12641                // expands `localtime` to nine fields.
12642                let items = if let ExprKind::List(exprs) = &list.kind {
12643                    let saved = self.wantarray_kind;
12644                    self.wantarray_kind = WantarrayCtx::List;
12645                    let mut vals = Vec::new();
12646                    for e in exprs {
12647                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
12648                        if let Some(items) = v.as_array_vec() {
12649                            vals.extend(items);
12650                        } else if v.is_iterator() {
12651                            // `join "", rev chars(...)` etc. — drain the
12652                            // lazy iterator into items so it joins the
12653                            // sequence instead of stringifying as "Iterator".
12654                            vals.extend(v.into_iterator().collect_all());
12655                        } else {
12656                            vals.push(v);
12657                        }
12658                    }
12659                    self.wantarray_kind = saved;
12660                    vals
12661                } else {
12662                    let saved = self.wantarray_kind;
12663                    self.wantarray_kind = WantarrayCtx::List;
12664                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12665                    self.wantarray_kind = saved;
12666                    if let Some(items) = v.as_array_vec() {
12667                        items
12668                    } else if v.is_iterator() {
12669                        // `~> ... rev |> join ""` produces an Iterator from
12670                        // the lazy stages; drain it before joining, otherwise
12671                        // it stringifies as "Iterator".
12672                        v.into_iterator().collect_all()
12673                    } else {
12674                        vec![v]
12675                    }
12676                };
12677                let mut strs = Vec::with_capacity(items.len());
12678                for v in &items {
12679                    strs.push(self.stringify_value(v.clone(), line)?);
12680                }
12681                Ok(StrykeValue::string(strs.join(&sep)))
12682            }
12683            ExprKind::SplitExpr {
12684                pattern,
12685                string,
12686                limit,
12687            } => {
12688                let pat_val = self.eval_expr(pattern)?;
12689                // For a regex value, pull the *source* (not the Display form,
12690                // which wraps an empty regex as `(?:)` and would defeat the
12691                // empty-pattern branch below).  Mirrors the VM's Split path.
12692                let pat = pat_val
12693                    .regex_src_and_flags()
12694                    .map(|(s, _)| s)
12695                    .unwrap_or_else(|| pat_val.to_string());
12696                let s = self.eval_expr(string)?.to_string();
12697                if s.is_empty() {
12698                    return Ok(StrykeValue::array(vec![]));
12699                }
12700                // Perl semantics for the limit field:
12701                //   omitted / 0  → no truncation, *strip* trailing empty fields.
12702                //   > 0          → at most LIMIT fields, keep empties up to limit.
12703                //   < 0          → no truncation, *keep* all empties.
12704                // Stryke previously parsed limit as `usize`, which folded a
12705                // user-supplied -1 into a giant positive number and made the
12706                // strip / keep decision ambiguous. Use `i64` so the sign is
12707                // preserved.
12708                let lim_opt: Option<i64> = limit
12709                    .as_ref()
12710                    .map(|l| self.eval_expr(l).map(|v| v.to_int()))
12711                    .transpose()?;
12712                let re = self.compile_regex(&pat, "", line)?;
12713                let mut parts: Vec<String> = match lim_opt {
12714                    Some(l) if l > 0 => re.splitn_strings(&s, l as usize),
12715                    _ => re.split_strings(&s),
12716                };
12717
12718                // Zero-width patterns (`split //, $s`) are defined by Perl as
12719                // "split between every character" — the regex engine, however,
12720                // also matches the empty string at position 0, producing a
12721                // spurious leading empty field that Perl does not emit. Strip
12722                // it before the trailing-empty rule kicks in.
12723                if pat.is_empty() && parts.first().is_some_and(|p| p.is_empty()) {
12724                    parts.remove(0);
12725                }
12726                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted
12727                // or zero. Positive LIMIT keeps trailing empties; negative
12728                // LIMIT also keeps them.
12729                let strip_trailing = matches!(lim_opt, None | Some(0));
12730                if strip_trailing {
12731                    while parts.last().is_some_and(|p| p.is_empty()) {
12732                        parts.pop();
12733                    }
12734                }
12735
12736                Ok(StrykeValue::array(
12737                    parts.into_iter().map(StrykeValue::string).collect(),
12738                ))
12739            }
12740
12741            // Numeric
12742            ExprKind::Abs(expr) => {
12743                let val = self.eval_expr(expr)?;
12744                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
12745                    return r;
12746                }
12747                Ok(StrykeValue::float(val.to_number().abs()))
12748            }
12749            ExprKind::Int(expr) => {
12750                let val = self.eval_expr(expr)?;
12751                Ok(StrykeValue::integer(val.to_number() as i64))
12752            }
12753            ExprKind::Sqrt(expr) => {
12754                let val = self.eval_expr(expr)?;
12755                Ok(StrykeValue::float(val.to_number().sqrt()))
12756            }
12757            ExprKind::Sin(expr) => {
12758                let val = self.eval_expr(expr)?;
12759                Ok(StrykeValue::float(val.to_number().sin()))
12760            }
12761            ExprKind::Cos(expr) => {
12762                let val = self.eval_expr(expr)?;
12763                Ok(StrykeValue::float(val.to_number().cos()))
12764            }
12765            ExprKind::Atan2 { y, x } => {
12766                let yv = self.eval_expr(y)?.to_number();
12767                let xv = self.eval_expr(x)?.to_number();
12768                Ok(StrykeValue::float(yv.atan2(xv)))
12769            }
12770            ExprKind::Exp(expr) => {
12771                let val = self.eval_expr(expr)?;
12772                Ok(StrykeValue::float(val.to_number().exp()))
12773            }
12774            ExprKind::Log(expr) => {
12775                let val = self.eval_expr(expr)?;
12776                Ok(StrykeValue::float(val.to_number().ln()))
12777            }
12778            ExprKind::Rand(upper) => {
12779                let u = match upper {
12780                    Some(e) => self.eval_expr(e)?.to_number(),
12781                    None => 1.0,
12782                };
12783                Ok(StrykeValue::float(self.perl_rand(u)))
12784            }
12785            ExprKind::Srand(seed) => {
12786                let s = match seed {
12787                    Some(e) => Some(self.eval_expr(e)?.to_number()),
12788                    None => None,
12789                };
12790                Ok(StrykeValue::integer(self.perl_srand(s)))
12791            }
12792            ExprKind::Hex(expr) => {
12793                let val = self.eval_expr(expr)?.to_string();
12794                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
12795                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
12796                Ok(StrykeValue::integer(n))
12797            }
12798            ExprKind::Oct(expr) => {
12799                let val = self.eval_expr(expr)?.to_string();
12800                let s = val.trim();
12801                let n = if s.starts_with("0x") || s.starts_with("0X") {
12802                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
12803                } else if s.starts_with("0b") || s.starts_with("0B") {
12804                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
12805                } else if s.starts_with("0o") || s.starts_with("0O") {
12806                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
12807                } else {
12808                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
12809                };
12810                Ok(StrykeValue::integer(n))
12811            }
12812
12813            // Case
12814            ExprKind::Lc(expr) => Ok(StrykeValue::string(
12815                self.eval_expr(expr)?.to_string().to_lowercase(),
12816            )),
12817            ExprKind::Uc(expr) => Ok(StrykeValue::string(
12818                self.eval_expr(expr)?.to_string().to_uppercase(),
12819            )),
12820            ExprKind::Lcfirst(expr) => {
12821                let s = self.eval_expr(expr)?.to_string();
12822                let mut chars = s.chars();
12823                let result = match chars.next() {
12824                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
12825                    None => String::new(),
12826                };
12827                Ok(StrykeValue::string(result))
12828            }
12829            ExprKind::Ucfirst(expr) => {
12830                let s = self.eval_expr(expr)?.to_string();
12831                let mut chars = s.chars();
12832                let result = match chars.next() {
12833                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
12834                    None => String::new(),
12835                };
12836                Ok(StrykeValue::string(result))
12837            }
12838            ExprKind::Fc(expr) => Ok(StrykeValue::string(default_case_fold_str(
12839                &self.eval_expr(expr)?.to_string(),
12840            ))),
12841            ExprKind::Crypt { plaintext, salt } => {
12842                let p = self.eval_expr(plaintext)?.to_string();
12843                let sl = self.eval_expr(salt)?.to_string();
12844                Ok(StrykeValue::string(perl_crypt(&p, &sl)))
12845            }
12846            ExprKind::Pos(e) => {
12847                let key = match e {
12848                    None => "_".to_string(),
12849                    Some(expr) => match &expr.kind {
12850                        ExprKind::ScalarVar(n) => n.clone(),
12851                        _ => self.eval_expr(expr)?.to_string(),
12852                    },
12853                };
12854                Ok(self
12855                    .regex_pos
12856                    .get(&key)
12857                    .copied()
12858                    .flatten()
12859                    .map(|p| StrykeValue::integer(p as i64))
12860                    .unwrap_or(StrykeValue::UNDEF))
12861            }
12862            ExprKind::Study(expr) => {
12863                let s = self.eval_expr(expr)?.to_string();
12864                Ok(Self::study_return_value(&s))
12865            }
12866
12867            // Type
12868            ExprKind::Defined(expr) => {
12869                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
12870                if let ExprKind::SubroutineRef(name) = &expr.kind {
12871                    let exists = self.resolve_sub_by_name(name).is_some();
12872                    return Ok(StrykeValue::integer(if exists { 1 } else { 0 }));
12873                }
12874                let val = self.eval_expr(expr)?;
12875                Ok(StrykeValue::integer(if val.is_undef() { 0 } else { 1 }))
12876            }
12877            ExprKind::Ref(expr) => {
12878                let val = self.eval_expr(expr)?;
12879                Ok(val.ref_type())
12880            }
12881            ExprKind::ScalarContext(expr) => {
12882                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
12883                Ok(v.scalar_context())
12884            }
12885
12886            // Char
12887            ExprKind::Chr(expr) => {
12888                let n = self.eval_expr(expr)?.to_int() as u32;
12889                Ok(StrykeValue::string(
12890                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
12891                ))
12892            }
12893            ExprKind::Ord(expr) => {
12894                let s = self.eval_expr(expr)?.to_string();
12895                Ok(StrykeValue::integer(
12896                    s.chars().next().map(|c| c as i64).unwrap_or(0),
12897                ))
12898            }
12899
12900            // I/O
12901            ExprKind::OpenMyHandle { .. } => Err(StrykeError::runtime(
12902                "internal: `open my $fh` handle used outside open()",
12903                line,
12904            )
12905            .into()),
12906            ExprKind::Open { handle, mode, file } => {
12907                if let ExprKind::OpenMyHandle { name } = &handle.kind {
12908                    self.scope
12909                        .declare_scalar_frozen(name, StrykeValue::UNDEF, false, None)?;
12910                    self.english_note_lexical_scalar(name);
12911                    let mode_s = self.eval_expr(mode)?.to_string();
12912                    let file_opt = if let Some(f) = file {
12913                        Some(self.eval_expr(f)?.to_string())
12914                    } else {
12915                        None
12916                    };
12917                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
12918                    self.scope.set_scalar(name, ret.clone())?;
12919                    return Ok(ret);
12920                }
12921                let handle_s = self.eval_expr(handle)?.to_string();
12922                let handle_name = self.resolve_io_handle_name(&handle_s);
12923                let mode_s = self.eval_expr(mode)?.to_string();
12924                let file_opt = if let Some(f) = file {
12925                    Some(self.eval_expr(f)?.to_string())
12926                } else {
12927                    None
12928                };
12929                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
12930                    .map_err(Into::into)
12931            }
12932            ExprKind::Close(expr) => {
12933                let s = self.eval_expr(expr)?.to_string();
12934                let name = self.resolve_io_handle_name(&s);
12935                self.close_builtin_execute(name).map_err(Into::into)
12936            }
12937            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
12938                self.readline_builtin_execute_list(handle.as_deref())
12939            } else {
12940                self.readline_builtin_execute(handle.as_deref())
12941            }
12942            .map_err(Into::into),
12943            ExprKind::Eof(expr) => match expr {
12944                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
12945                Some(e) => {
12946                    let name = self.eval_expr(e)?;
12947                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
12948                }
12949            },
12950
12951            ExprKind::Opendir { handle, path } => {
12952                let h = self.eval_expr(handle)?.to_string();
12953                let p = self.eval_expr(path)?.to_string();
12954                Ok(self.opendir_handle(&h, &p))
12955            }
12956            ExprKind::Readdir(e) => {
12957                let h = self.eval_expr(e)?.to_string();
12958                Ok(if ctx == WantarrayCtx::List {
12959                    self.readdir_handle_list(&h)
12960                } else {
12961                    self.readdir_handle(&h)
12962                })
12963            }
12964            ExprKind::Closedir(e) => {
12965                let h = self.eval_expr(e)?.to_string();
12966                Ok(self.closedir_handle(&h))
12967            }
12968            ExprKind::Rewinddir(e) => {
12969                let h = self.eval_expr(e)?.to_string();
12970                Ok(self.rewinddir_handle(&h))
12971            }
12972            ExprKind::Telldir(e) => {
12973                let h = self.eval_expr(e)?.to_string();
12974                Ok(self.telldir_handle(&h))
12975            }
12976            ExprKind::Seekdir { handle, position } => {
12977                let h = self.eval_expr(handle)?.to_string();
12978                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
12979                Ok(self.seekdir_handle(&h, pos))
12980            }
12981
12982            // File tests
12983            ExprKind::FileTest { op, expr } => {
12984                let raw = self.eval_expr(expr)?.to_string();
12985                let path = self.resolve_stryke_path_string(&raw);
12986                // -M, -A, -C return fractional days (float), not boolean
12987                if matches!(op, 'M' | 'A' | 'C') {
12988                    #[cfg(unix)]
12989                    {
12990                        return match crate::perl_fs::filetest_age_days(&path, *op) {
12991                            Some(days) => Ok(StrykeValue::float(days)),
12992                            None => Ok(StrykeValue::UNDEF),
12993                        };
12994                    }
12995                    #[cfg(not(unix))]
12996                    return Ok(StrykeValue::UNDEF);
12997                }
12998                // -s returns file size (or undef on error)
12999                if *op == 's' {
13000                    return match std::fs::metadata(&path) {
13001                        Ok(m) => Ok(StrykeValue::integer(m.len() as i64)),
13002                        Err(_) => Ok(StrykeValue::UNDEF),
13003                    };
13004                }
13005                let result = match op {
13006                    'e' => std::path::Path::new(&path).exists(),
13007                    'f' => std::path::Path::new(&path).is_file(),
13008                    'd' => std::path::Path::new(&path).is_dir(),
13009                    'l' => std::path::Path::new(&path).is_symlink(),
13010                    #[cfg(unix)]
13011                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
13012                    #[cfg(not(unix))]
13013                    'r' => std::fs::metadata(&path).is_ok(),
13014                    #[cfg(unix)]
13015                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
13016                    #[cfg(not(unix))]
13017                    'w' => std::fs::metadata(&path).is_ok(),
13018                    #[cfg(unix)]
13019                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
13020                    #[cfg(not(unix))]
13021                    'x' => false,
13022                    #[cfg(unix)]
13023                    'o' => crate::perl_fs::filetest_owned_effective(&path),
13024                    #[cfg(not(unix))]
13025                    'o' => false,
13026                    #[cfg(unix)]
13027                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
13028                    #[cfg(not(unix))]
13029                    'R' => false,
13030                    #[cfg(unix)]
13031                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
13032                    #[cfg(not(unix))]
13033                    'W' => false,
13034                    #[cfg(unix)]
13035                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
13036                    #[cfg(not(unix))]
13037                    'X' => false,
13038                    #[cfg(unix)]
13039                    'O' => crate::perl_fs::filetest_owned_real(&path),
13040                    #[cfg(not(unix))]
13041                    'O' => false,
13042                    'z' => std::fs::metadata(&path)
13043                        .map(|m| m.len() == 0)
13044                        .unwrap_or(true),
13045                    't' => crate::perl_fs::filetest_is_tty(&path),
13046                    #[cfg(unix)]
13047                    'p' => crate::perl_fs::filetest_is_pipe(&path),
13048                    #[cfg(not(unix))]
13049                    'p' => false,
13050                    #[cfg(unix)]
13051                    'S' => crate::perl_fs::filetest_is_socket(&path),
13052                    #[cfg(not(unix))]
13053                    'S' => false,
13054                    #[cfg(unix)]
13055                    'b' => crate::perl_fs::filetest_is_block_device(&path),
13056                    #[cfg(not(unix))]
13057                    'b' => false,
13058                    #[cfg(unix)]
13059                    'c' => crate::perl_fs::filetest_is_char_device(&path),
13060                    #[cfg(not(unix))]
13061                    'c' => false,
13062                    #[cfg(unix)]
13063                    'u' => crate::perl_fs::filetest_is_setuid(&path),
13064                    #[cfg(not(unix))]
13065                    'u' => false,
13066                    #[cfg(unix)]
13067                    'g' => crate::perl_fs::filetest_is_setgid(&path),
13068                    #[cfg(not(unix))]
13069                    'g' => false,
13070                    #[cfg(unix)]
13071                    'k' => crate::perl_fs::filetest_is_sticky(&path),
13072                    #[cfg(not(unix))]
13073                    'k' => false,
13074                    'T' => crate::perl_fs::filetest_is_text(&path),
13075                    'B' => crate::perl_fs::filetest_is_binary(&path),
13076                    _ => false,
13077                };
13078                Ok(StrykeValue::integer(if result { 1 } else { 0 }))
13079            }
13080
13081            // System
13082            ExprKind::System(args) => {
13083                let mut cmd_args = Vec::new();
13084                for a in args {
13085                    cmd_args.push(self.eval_expr(a)?.to_string());
13086                }
13087                if cmd_args.is_empty() {
13088                    return Ok(StrykeValue::integer(-1));
13089                }
13090                let status = Command::new("sh")
13091                    .arg("-c")
13092                    .arg(cmd_args.join(" "))
13093                    .status();
13094                match status {
13095                    Ok(s) => {
13096                        self.record_child_exit_status(s);
13097                        Ok(StrykeValue::integer(s.code().unwrap_or(-1) as i64))
13098                    }
13099                    Err(e) => {
13100                        self.apply_io_error_to_errno(&e);
13101                        Ok(StrykeValue::integer(-1))
13102                    }
13103                }
13104            }
13105            ExprKind::Exec(args) => {
13106                let mut cmd_args = Vec::new();
13107                for a in args {
13108                    cmd_args.push(self.eval_expr(a)?.to_string());
13109                }
13110                if cmd_args.is_empty() {
13111                    return Ok(StrykeValue::integer(-1));
13112                }
13113                let status = Command::new("sh")
13114                    .arg("-c")
13115                    .arg(cmd_args.join(" "))
13116                    .status();
13117                match status {
13118                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
13119                    Err(e) => {
13120                        self.apply_io_error_to_errno(&e);
13121                        Ok(StrykeValue::integer(-1))
13122                    }
13123                }
13124            }
13125            ExprKind::Eval(expr) => {
13126                self.eval_nesting += 1;
13127                let out = match &expr.kind {
13128                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
13129                        Ok(v) => {
13130                            self.clear_eval_error();
13131                            Ok(v)
13132                        }
13133                        Err(FlowOrError::Error(e)) => {
13134                            self.set_eval_error_from_perl_error(&e);
13135                            Ok(StrykeValue::UNDEF)
13136                        }
13137                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
13138                    },
13139                    _ => {
13140                        let code = self.eval_expr(expr)?.to_string();
13141                        // Parse and execute the string as Perl code
13142                        match crate::parse_and_run_string(&code, self) {
13143                            Ok(v) => {
13144                                self.clear_eval_error();
13145                                Ok(v)
13146                            }
13147                            Err(e) => {
13148                                self.set_eval_error(e.to_string());
13149                                Ok(StrykeValue::UNDEF)
13150                            }
13151                        }
13152                    }
13153                };
13154                self.eval_nesting -= 1;
13155                out
13156            }
13157            ExprKind::Do(expr) => match &expr.kind {
13158                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
13159                _ => {
13160                    let val = self.eval_expr(expr)?;
13161                    let filename = val.to_string();
13162                    match read_file_text_perl_compat(&filename) {
13163                        Ok(code) => {
13164                            let code = crate::data_section::strip_perl_end_marker(&code);
13165                            match crate::parse_and_run_string_in_file(code, self, &filename) {
13166                                Ok(v) => Ok(v),
13167                                Err(e) => {
13168                                    self.set_eval_error(e.to_string());
13169                                    Ok(StrykeValue::UNDEF)
13170                                }
13171                            }
13172                        }
13173                        Err(e) => {
13174                            self.apply_io_error_to_errno(&e);
13175                            Ok(StrykeValue::UNDEF)
13176                        }
13177                    }
13178                }
13179            },
13180            ExprKind::Require(expr) => {
13181                let spec = self.eval_expr(expr)?.to_string();
13182                self.require_execute(&spec, line)
13183                    .map_err(FlowOrError::Error)
13184            }
13185            ExprKind::Exit(code) => {
13186                let c = if let Some(e) = code {
13187                    self.eval_expr(e)?.to_int() as i32
13188                } else {
13189                    0
13190                };
13191                Err(StrykeError::new(ErrorKind::Exit(c), "", line, &self.file).into())
13192            }
13193            ExprKind::Chdir(expr) => {
13194                let path = self.eval_expr(expr)?.to_string();
13195                match std::env::set_current_dir(&path) {
13196                    Ok(_) => {
13197                        if let Ok(c) = std::env::current_dir() {
13198                            self.stryke_pwd = std::fs::canonicalize(&c).unwrap_or(c);
13199                        }
13200                        Ok(StrykeValue::integer(1))
13201                    }
13202                    Err(e) => {
13203                        self.apply_io_error_to_errno(&e);
13204                        Ok(StrykeValue::integer(0))
13205                    }
13206                }
13207            }
13208            ExprKind::Mkdir { path, mode: _ } => {
13209                let raw = self.eval_expr(path)?.to_string();
13210                let p = self.resolve_stryke_path_string(&raw);
13211                match std::fs::create_dir(&p) {
13212                    Ok(_) => Ok(StrykeValue::integer(1)),
13213                    Err(e) => {
13214                        self.apply_io_error_to_errno(&e);
13215                        Ok(StrykeValue::integer(0))
13216                    }
13217                }
13218            }
13219            ExprKind::Unlink(args) => {
13220                let mut count = 0i64;
13221                for a in args {
13222                    let raw = self.eval_expr(a)?.to_string();
13223                    let path = self.resolve_stryke_path_string(&raw);
13224                    if std::fs::remove_file(&path).is_ok() {
13225                        count += 1;
13226                    }
13227                }
13228                Ok(StrykeValue::integer(count))
13229            }
13230            ExprKind::Rename { old, new } => {
13231                let o_raw = self.eval_expr(old)?.to_string();
13232                let n_raw = self.eval_expr(new)?.to_string();
13233                let o = self.resolve_stryke_path_string(&o_raw);
13234                let n = self.resolve_stryke_path_string(&n_raw);
13235                Ok(crate::perl_fs::rename_paths(&o, &n))
13236            }
13237            ExprKind::Chmod(args) => {
13238                let mode = self.eval_expr(&args[0])?.to_int();
13239                let mut paths = Vec::new();
13240                for a in &args[1..] {
13241                    let raw = self.eval_expr(a)?.to_string();
13242                    paths.push(self.resolve_stryke_path_string(&raw));
13243                }
13244                Ok(StrykeValue::integer(crate::perl_fs::chmod_paths(
13245                    &paths, mode,
13246                )))
13247            }
13248            ExprKind::Chown(args) => {
13249                let uid = self.eval_expr(&args[0])?.to_int();
13250                let gid = self.eval_expr(&args[1])?.to_int();
13251                let mut paths = Vec::new();
13252                for a in &args[2..] {
13253                    let raw = self.eval_expr(a)?.to_string();
13254                    paths.push(self.resolve_stryke_path_string(&raw));
13255                }
13256                Ok(StrykeValue::integer(crate::perl_fs::chown_paths(
13257                    &paths, uid, gid,
13258                )))
13259            }
13260            ExprKind::Stat(e) => {
13261                let raw = self.eval_expr(e)?.to_string();
13262                let path = self.resolve_stryke_path_string(&raw);
13263                Ok(crate::perl_fs::stat_path(&path, false))
13264            }
13265            ExprKind::Lstat(e) => {
13266                let raw = self.eval_expr(e)?.to_string();
13267                let path = self.resolve_stryke_path_string(&raw);
13268                Ok(crate::perl_fs::stat_path(&path, true))
13269            }
13270            ExprKind::Link { old, new } => {
13271                let o_raw = self.eval_expr(old)?.to_string();
13272                let n_raw = self.eval_expr(new)?.to_string();
13273                let o = self.resolve_stryke_path_string(&o_raw);
13274                let n = self.resolve_stryke_path_string(&n_raw);
13275                Ok(crate::perl_fs::link_hard(&o, &n))
13276            }
13277            ExprKind::Symlink { old, new } => {
13278                let o = self.eval_expr(old)?.to_string();
13279                let n_raw = self.eval_expr(new)?.to_string();
13280                let n = self.resolve_stryke_path_string(&n_raw);
13281                Ok(crate::perl_fs::link_sym(&o, &n))
13282            }
13283            ExprKind::Readlink(e) => {
13284                let raw = self.eval_expr(e)?.to_string();
13285                let path = self.resolve_stryke_path_string(&raw);
13286                Ok(crate::perl_fs::read_link(&path))
13287            }
13288            ExprKind::Files(args) => {
13289                let dir_raw = if args.is_empty() {
13290                    ".".to_string()
13291                } else {
13292                    self.eval_expr(&args[0])?.to_string()
13293                };
13294                let dir = self.resolve_stryke_path_string(&dir_raw);
13295                Ok(crate::perl_fs::list_files(&dir))
13296            }
13297            ExprKind::Filesf(args) => {
13298                let dir_raw = if args.is_empty() {
13299                    ".".to_string()
13300                } else {
13301                    self.eval_expr(&args[0])?.to_string()
13302                };
13303                let dir = self.resolve_stryke_path_string(&dir_raw);
13304                Ok(crate::perl_fs::list_filesf(&dir))
13305            }
13306            ExprKind::FilesfRecursive(args) => {
13307                let dir_raw = if args.is_empty() {
13308                    ".".to_string()
13309                } else {
13310                    self.eval_expr(&args[0])?.to_string()
13311                };
13312                let dir = self.resolve_stryke_path_string(&dir_raw);
13313                Ok(StrykeValue::iterator(Arc::new(
13314                    crate::value::FsWalkIterator::new(&dir, true),
13315                )))
13316            }
13317            ExprKind::Dirs(args) => {
13318                let dir_raw = if args.is_empty() {
13319                    ".".to_string()
13320                } else {
13321                    self.eval_expr(&args[0])?.to_string()
13322                };
13323                let dir = self.resolve_stryke_path_string(&dir_raw);
13324                Ok(crate::perl_fs::list_dirs(&dir))
13325            }
13326            ExprKind::DirsRecursive(args) => {
13327                let dir_raw = if args.is_empty() {
13328                    ".".to_string()
13329                } else {
13330                    self.eval_expr(&args[0])?.to_string()
13331                };
13332                let dir = self.resolve_stryke_path_string(&dir_raw);
13333                Ok(StrykeValue::iterator(Arc::new(
13334                    crate::value::FsWalkIterator::new(&dir, false),
13335                )))
13336            }
13337            ExprKind::SymLinks(args) => {
13338                let dir_raw = if args.is_empty() {
13339                    ".".to_string()
13340                } else {
13341                    self.eval_expr(&args[0])?.to_string()
13342                };
13343                let dir = self.resolve_stryke_path_string(&dir_raw);
13344                Ok(crate::perl_fs::list_sym_links(&dir))
13345            }
13346            ExprKind::Sockets(args) => {
13347                let dir_raw = if args.is_empty() {
13348                    ".".to_string()
13349                } else {
13350                    self.eval_expr(&args[0])?.to_string()
13351                };
13352                let dir = self.resolve_stryke_path_string(&dir_raw);
13353                Ok(crate::perl_fs::list_sockets(&dir))
13354            }
13355            ExprKind::Pipes(args) => {
13356                let dir_raw = if args.is_empty() {
13357                    ".".to_string()
13358                } else {
13359                    self.eval_expr(&args[0])?.to_string()
13360                };
13361                let dir = self.resolve_stryke_path_string(&dir_raw);
13362                Ok(crate::perl_fs::list_pipes(&dir))
13363            }
13364            ExprKind::BlockDevices(args) => {
13365                let dir_raw = if args.is_empty() {
13366                    ".".to_string()
13367                } else {
13368                    self.eval_expr(&args[0])?.to_string()
13369                };
13370                let dir = self.resolve_stryke_path_string(&dir_raw);
13371                Ok(crate::perl_fs::list_block_devices(&dir))
13372            }
13373            ExprKind::CharDevices(args) => {
13374                let dir_raw = if args.is_empty() {
13375                    ".".to_string()
13376                } else {
13377                    self.eval_expr(&args[0])?.to_string()
13378                };
13379                let dir = self.resolve_stryke_path_string(&dir_raw);
13380                Ok(crate::perl_fs::list_char_devices(&dir))
13381            }
13382            ExprKind::Executables(args) => {
13383                let dir_raw = if args.is_empty() {
13384                    ".".to_string()
13385                } else {
13386                    self.eval_expr(&args[0])?.to_string()
13387                };
13388                let dir = self.resolve_stryke_path_string(&dir_raw);
13389                Ok(crate::perl_fs::list_executables(&dir))
13390            }
13391            ExprKind::Glob(args) => {
13392                // Pass the user's pattern through unchanged: zsh::glob runs from
13393                // OS cwd, which `chdir` keeps in sync with `stryke_pwd`. Resolving
13394                // relative patterns to absolute paths up front would turn
13395                // `glob("**(/)")` results from "sub" into "/abs/.../sub" — breaking
13396                // the documented contract that relative patterns yield relative
13397                // results (pinned in tests/suite/glob_zsh_qualifiers.rs).
13398                let mut pats = Vec::new();
13399                for a in args {
13400                    pats.push(self.eval_expr(a)?.to_string());
13401                }
13402                Ok(crate::perl_fs::glob_patterns(&pats))
13403            }
13404            ExprKind::GlobPar { args, progress } => {
13405                let mut pats = Vec::new();
13406                for a in args {
13407                    pats.push(self.eval_expr(a)?.to_string());
13408                }
13409                let show_progress = progress
13410                    .as_ref()
13411                    .map(|p| self.eval_expr(p))
13412                    .transpose()?
13413                    .map(|v| v.is_true())
13414                    .unwrap_or(false);
13415                if show_progress {
13416                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
13417                } else {
13418                    Ok(crate::perl_fs::glob_par_patterns(&pats))
13419                }
13420            }
13421            ExprKind::ParSed { args, progress } => {
13422                let has_progress = progress.is_some();
13423                let mut vals: Vec<StrykeValue> = Vec::new();
13424                for a in args {
13425                    vals.push(self.eval_expr(a)?);
13426                }
13427                if let Some(p) = progress {
13428                    vals.push(self.eval_expr(p.as_ref())?);
13429                }
13430                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
13431            }
13432            ExprKind::Bless { ref_expr, class } => {
13433                let val = self.eval_expr(ref_expr)?;
13434                let class_name = if let Some(c) = class {
13435                    self.eval_expr(c)?.to_string()
13436                } else {
13437                    self.scope.get_scalar("__PACKAGE__").to_string()
13438                };
13439                Ok(StrykeValue::blessed(Arc::new(
13440                    crate::value::BlessedRef::new_blessed(class_name, val),
13441                )))
13442            }
13443            ExprKind::Caller(_) => {
13444                // Simplified: return package, file, line
13445                Ok(StrykeValue::array(vec![
13446                    StrykeValue::string("main".into()),
13447                    StrykeValue::string(self.file.clone()),
13448                    StrykeValue::integer(line as i64),
13449                ]))
13450            }
13451            ExprKind::Wantarray => Ok(match self.wantarray_kind {
13452                WantarrayCtx::Void => StrykeValue::UNDEF,
13453                WantarrayCtx::Scalar => StrykeValue::integer(0),
13454                WantarrayCtx::List => StrykeValue::integer(1),
13455            }),
13456
13457            ExprKind::List(exprs) => {
13458                // In scalar context, the comma operator evaluates to the last element.
13459                if ctx == WantarrayCtx::Scalar {
13460                    if let Some(last) = exprs.last() {
13461                        // Evaluate earlier expressions for side effects
13462                        for e in &exprs[..exprs.len() - 1] {
13463                            self.eval_expr(e)?;
13464                        }
13465                        return self.eval_expr(last);
13466                    } else {
13467                        return Ok(StrykeValue::UNDEF);
13468                    }
13469                }
13470                let mut vals = Vec::new();
13471                for e in exprs {
13472                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
13473                    if let Some(items) = v.as_array_vec() {
13474                        vals.extend(items);
13475                    } else {
13476                        vals.push(v);
13477                    }
13478                }
13479                if vals.len() == 1 {
13480                    Ok(vals.pop().unwrap())
13481                } else {
13482                    Ok(StrykeValue::array(vals))
13483                }
13484            }
13485
13486            // Postfix modifiers
13487            ExprKind::PostfixIf { expr, condition } => {
13488                if self.eval_postfix_condition(condition)? {
13489                    self.eval_expr(expr)
13490                } else {
13491                    Ok(StrykeValue::UNDEF)
13492                }
13493            }
13494            ExprKind::PostfixUnless { expr, condition } => {
13495                if !self.eval_postfix_condition(condition)? {
13496                    self.eval_expr(expr)
13497                } else {
13498                    Ok(StrykeValue::UNDEF)
13499                }
13500            }
13501            ExprKind::PostfixWhile { expr, condition } => {
13502                // `do { ... } while (COND)` — body runs before the first condition check.
13503                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
13504                let is_do_block = matches!(
13505                    &expr.kind,
13506                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
13507                );
13508                let mut last = StrykeValue::UNDEF;
13509                if is_do_block {
13510                    loop {
13511                        last = self.eval_expr(expr)?;
13512                        if !self.eval_postfix_condition(condition)? {
13513                            break;
13514                        }
13515                    }
13516                } else {
13517                    loop {
13518                        if !self.eval_postfix_condition(condition)? {
13519                            break;
13520                        }
13521                        last = self.eval_expr(expr)?;
13522                    }
13523                }
13524                Ok(last)
13525            }
13526            ExprKind::PostfixUntil { expr, condition } => {
13527                let is_do_block = matches!(
13528                    &expr.kind,
13529                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
13530                );
13531                let mut last = StrykeValue::UNDEF;
13532                if is_do_block {
13533                    loop {
13534                        last = self.eval_expr(expr)?;
13535                        if self.eval_postfix_condition(condition)? {
13536                            break;
13537                        }
13538                    }
13539                } else {
13540                    loop {
13541                        if self.eval_postfix_condition(condition)? {
13542                            break;
13543                        }
13544                        last = self.eval_expr(expr)?;
13545                    }
13546                }
13547                Ok(last)
13548            }
13549            ExprKind::PostfixForeach { expr, list } => {
13550                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
13551                let mut last = StrykeValue::UNDEF;
13552                for item in items {
13553                    self.scope.set_topic(item);
13554                    last = self.eval_expr(expr)?;
13555                }
13556                Ok(last)
13557            }
13558        }
13559    }
13560
13561    // ── Helpers ──
13562
13563    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
13564        match op {
13565            BinOp::Add => Some("+"),
13566            BinOp::Sub => Some("-"),
13567            BinOp::Mul => Some("*"),
13568            BinOp::Div => Some("/"),
13569            BinOp::Mod => Some("%"),
13570            BinOp::Pow => Some("**"),
13571            BinOp::Concat => Some("."),
13572            BinOp::StrEq => Some("eq"),
13573            BinOp::NumEq => Some("=="),
13574            BinOp::StrNe => Some("ne"),
13575            BinOp::NumNe => Some("!="),
13576            BinOp::StrLt => Some("lt"),
13577            BinOp::StrGt => Some("gt"),
13578            BinOp::StrLe => Some("le"),
13579            BinOp::StrGe => Some("ge"),
13580            BinOp::NumLt => Some("<"),
13581            BinOp::NumGt => Some(">"),
13582            BinOp::NumLe => Some("<="),
13583            BinOp::NumGe => Some(">="),
13584            BinOp::Spaceship => Some("<=>"),
13585            BinOp::StrCmp => Some("cmp"),
13586            _ => None,
13587        }
13588    }
13589
13590    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
13591    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
13592        map.get("").or_else(|| map.get("\"\""))
13593    }
13594
13595    /// String context for blessed objects with `overload '""'`.
13596    pub(crate) fn stringify_value(
13597        &mut self,
13598        v: StrykeValue,
13599        line: usize,
13600    ) -> Result<String, FlowOrError> {
13601        if let Some(r) = self.try_overload_stringify(&v, line) {
13602            let pv = r?;
13603            return Ok(pv.to_string());
13604        }
13605        Ok(v.to_string())
13606    }
13607
13608    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
13609    pub(crate) fn perl_sprintf_stringify(
13610        &mut self,
13611        fmt: &str,
13612        args: &[StrykeValue],
13613        line: usize,
13614    ) -> Result<String, FlowOrError> {
13615        // Step 1: build the output and collect any `%n` store-targets.
13616        let (out, pending_n) = {
13617            let mut stringify = |v: &StrykeValue| -> Result<String, FlowOrError> {
13618                self.stringify_value(v.clone(), line)
13619            };
13620            perl_sprintf_format_full(fmt, args, &mut stringify)?
13621        };
13622        // Step 2: apply any `%n` writes through the proper scope path.
13623        for (target, count) in pending_n {
13624            self.assign_scalar_ref_deref(target, StrykeValue::integer(count), line)?;
13625        }
13626        Ok(out)
13627    }
13628
13629    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
13630    pub(crate) fn render_format_template(
13631        &mut self,
13632        tmpl: &crate::format::FormatTemplate,
13633        line: usize,
13634    ) -> Result<String, FlowOrError> {
13635        use crate::format::{FormatRecord, PictureSegment};
13636        let mut buf = String::new();
13637        for rec in &tmpl.records {
13638            match rec {
13639                FormatRecord::Literal(s) => {
13640                    buf.push_str(s);
13641                    buf.push('\n');
13642                }
13643                FormatRecord::Picture { segments, exprs } => {
13644                    let mut vals: Vec<String> = Vec::new();
13645                    for e in exprs {
13646                        let v = self.eval_expr(e)?;
13647                        vals.push(self.stringify_value(v, line)?);
13648                    }
13649                    let mut vi = 0usize;
13650                    let mut line_out = String::new();
13651                    for seg in segments {
13652                        match seg {
13653                            PictureSegment::Literal(t) => line_out.push_str(t),
13654                            PictureSegment::Field {
13655                                width,
13656                                align,
13657                                kind: _,
13658                            } => {
13659                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
13660                                vi += 1;
13661                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
13662                            }
13663                        }
13664                    }
13665                    buf.push_str(line_out.trim_end());
13666                    buf.push('\n');
13667                }
13668            }
13669        }
13670        Ok(buf)
13671    }
13672
13673    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
13674    pub(crate) fn resolve_write_output_handle(
13675        &self,
13676        v: &StrykeValue,
13677        line: usize,
13678    ) -> StrykeResult<String> {
13679        if let Some(n) = v.as_io_handle_name() {
13680            let n = self.resolve_io_handle_name(&n);
13681            if self.is_bound_handle(&n) {
13682                return Ok(n);
13683            }
13684        }
13685        if let Some(s) = v.as_str() {
13686            if self.is_bound_handle(&s) {
13687                return Ok(self.resolve_io_handle_name(&s));
13688            }
13689        }
13690        let s = v.to_string();
13691        if self.is_bound_handle(&s) {
13692            return Ok(self.resolve_io_handle_name(&s));
13693        }
13694        Err(StrykeError::runtime(
13695            format!("write: invalid or unopened filehandle {}", s),
13696            line,
13697        ))
13698    }
13699
13700    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
13701    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
13702    /// that handle like `write FH`.
13703    pub(crate) fn write_format_execute(
13704        &mut self,
13705        args: &[StrykeValue],
13706        line: usize,
13707    ) -> StrykeResult<StrykeValue> {
13708        let handle_name = match args.len() {
13709            0 => self.default_print_handle.clone(),
13710            1 => self.resolve_write_output_handle(&args[0], line)?,
13711            _ => {
13712                return Err(StrykeError::runtime("write: too many arguments", line));
13713            }
13714        };
13715        let pkg = self.current_package();
13716        let mut fmt_name = self.scope.get_scalar("~").to_string();
13717        if fmt_name.is_empty() {
13718            fmt_name = "STDOUT".to_string();
13719        }
13720        let key = format!("{}::{}", pkg, fmt_name);
13721        let tmpl = self
13722            .format_templates
13723            .get(&key)
13724            .map(Arc::clone)
13725            .ok_or_else(|| {
13726                StrykeError::runtime(
13727                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
13728                    line,
13729                )
13730            })?;
13731        let out = self
13732            .render_format_template(&tmpl, line)
13733            .map_err(|e| match e {
13734                FlowOrError::Error(e) => e,
13735                FlowOrError::Flow(_) => {
13736                    StrykeError::runtime("write: unexpected control flow", line)
13737                }
13738            })?;
13739        self.write_formatted_print(handle_name.as_str(), &out, line)?;
13740        Ok(StrykeValue::integer(1))
13741    }
13742
13743    pub(crate) fn try_overload_stringify(
13744        &mut self,
13745        v: &StrykeValue,
13746        line: usize,
13747    ) -> Option<ExecResult> {
13748        // Native class instance: look for method named '""' or 'stringify'
13749        if let Some(c) = v.as_class_inst() {
13750            let method_name = c
13751                .def
13752                .method("stringify")
13753                .or_else(|| c.def.method("\"\""))
13754                .filter(|m| m.body.is_some())?;
13755            let body = method_name.body.clone().unwrap();
13756            let params = method_name.params.clone();
13757            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
13758        }
13759        let br = v.as_blessed_ref()?;
13760        let class = br.class.clone();
13761        let map = self.overload_table.get(&class)?;
13762        let sub_short = Self::overload_stringify_method(map)?;
13763        let fq = format!("{}::{}", class, sub_short);
13764        let sub = self.subs.get(&fq)?.clone();
13765        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
13766    }
13767
13768    /// Map overload operator key to native class method name.
13769    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
13770        match key {
13771            "+" => Some("op_add"),
13772            "-" => Some("op_sub"),
13773            "*" => Some("op_mul"),
13774            "/" => Some("op_div"),
13775            "%" => Some("op_mod"),
13776            "**" => Some("op_pow"),
13777            "." => Some("op_concat"),
13778            "==" => Some("op_eq"),
13779            "!=" => Some("op_ne"),
13780            "<" => Some("op_lt"),
13781            ">" => Some("op_gt"),
13782            "<=" => Some("op_le"),
13783            ">=" => Some("op_ge"),
13784            "<=>" => Some("op_spaceship"),
13785            "eq" => Some("op_str_eq"),
13786            "ne" => Some("op_str_ne"),
13787            "lt" => Some("op_str_lt"),
13788            "gt" => Some("op_str_gt"),
13789            "le" => Some("op_str_le"),
13790            "ge" => Some("op_str_ge"),
13791            "cmp" => Some("op_cmp"),
13792            _ => None,
13793        }
13794    }
13795
13796    pub(crate) fn try_overload_binop(
13797        &mut self,
13798        op: BinOp,
13799        lv: &StrykeValue,
13800        rv: &StrykeValue,
13801        line: usize,
13802    ) -> Option<ExecResult> {
13803        let key = Self::overload_key_for_binop(op)?;
13804        // Native class instance overloading
13805        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
13806            (Some(c.def.clone()), lv.clone(), rv.clone())
13807        } else if let Some(c) = rv.as_class_inst() {
13808            (Some(c.def.clone()), rv.clone(), lv.clone())
13809        } else {
13810            (None, lv.clone(), rv.clone())
13811        };
13812        if let Some(ref def) = ci_def {
13813            if let Some(method_name) = Self::overload_method_name_for_key(key) {
13814                if let Some((m, _)) = self.find_class_method(def, method_name) {
13815                    if let Some(ref body) = m.body {
13816                        let params = m.params.clone();
13817                        return Some(self.call_class_method(
13818                            body,
13819                            &params,
13820                            vec![invocant, other],
13821                            line,
13822                        ));
13823                    }
13824                }
13825            }
13826        }
13827        // Blessed ref overloading (existing path)
13828        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
13829            (br.class.clone(), lv.clone(), rv.clone())
13830        } else if let Some(br) = rv.as_blessed_ref() {
13831            (br.class.clone(), rv.clone(), lv.clone())
13832        } else {
13833            return None;
13834        };
13835        let map = self.overload_table.get(&class)?;
13836        let sub_short = if let Some(s) = map.get(key) {
13837            s.clone()
13838        } else if let Some(nm) = map.get("nomethod") {
13839            let fq = format!("{}::{}", class, nm);
13840            let sub = self.subs.get(&fq)?.clone();
13841            return Some(self.call_sub(
13842                &sub,
13843                vec![invocant, other, StrykeValue::string(key.to_string())],
13844                WantarrayCtx::Scalar,
13845                line,
13846            ));
13847        } else {
13848            return None;
13849        };
13850        let fq = format!("{}::{}", class, sub_short);
13851        let sub = self.subs.get(&fq)?.clone();
13852        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
13853    }
13854
13855    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
13856    pub(crate) fn try_overload_unary_dispatch(
13857        &mut self,
13858        op_key: &str,
13859        val: &StrykeValue,
13860        line: usize,
13861    ) -> Option<ExecResult> {
13862        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
13863        if let Some(c) = val.as_class_inst() {
13864            let method_name = match op_key {
13865                "neg" => "op_neg",
13866                "bool" => "op_bool",
13867                "abs" => "op_abs",
13868                "0+" => "op_numify",
13869                _ => return None,
13870            };
13871            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
13872                if let Some(ref body) = m.body {
13873                    let params = m.params.clone();
13874                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
13875                }
13876            }
13877            return None;
13878        }
13879        // Blessed ref path
13880        let br = val.as_blessed_ref()?;
13881        let class = br.class.clone();
13882        let map = self.overload_table.get(&class)?;
13883        if let Some(s) = map.get(op_key) {
13884            let fq = format!("{}::{}", class, s);
13885            let sub = self.subs.get(&fq)?.clone();
13886            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
13887        }
13888        if let Some(nm) = map.get("nomethod") {
13889            let fq = format!("{}::{}", class, nm);
13890            let sub = self.subs.get(&fq)?.clone();
13891            return Some(self.call_sub(
13892                &sub,
13893                vec![val.clone(), StrykeValue::string(op_key.to_string())],
13894                WantarrayCtx::Scalar,
13895                line,
13896            ));
13897        }
13898        None
13899    }
13900
13901    #[inline]
13902    fn eval_binop(
13903        &mut self,
13904        op: BinOp,
13905        lv: &StrykeValue,
13906        rv: &StrykeValue,
13907        _line: usize,
13908    ) -> ExecResult {
13909        Ok(match op {
13910            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
13911            // Perl `+` is numeric addition only; string concatenation is `.`.
13912            BinOp::Add => {
13913                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13914                    StrykeValue::integer(a.wrapping_add(b))
13915                } else {
13916                    StrykeValue::float(lv.to_number() + rv.to_number())
13917                }
13918            }
13919            BinOp::Sub => {
13920                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13921                    StrykeValue::integer(a.wrapping_sub(b))
13922                } else {
13923                    StrykeValue::float(lv.to_number() - rv.to_number())
13924                }
13925            }
13926            BinOp::Mul => {
13927                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13928                    StrykeValue::integer(a.wrapping_mul(b))
13929                } else {
13930                    StrykeValue::float(lv.to_number() * rv.to_number())
13931                }
13932            }
13933            BinOp::Div => {
13934                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13935                    if b == 0 {
13936                        return Err(StrykeError::division_by_zero(
13937                            "Illegal division by zero",
13938                            _line,
13939                        )
13940                        .into());
13941                    }
13942                    if a % b == 0 {
13943                        StrykeValue::integer(a / b)
13944                    } else {
13945                        StrykeValue::float(a as f64 / b as f64)
13946                    }
13947                } else {
13948                    let d = rv.to_number();
13949                    if d == 0.0 {
13950                        return Err(StrykeError::division_by_zero(
13951                            "Illegal division by zero",
13952                            _line,
13953                        )
13954                        .into());
13955                    }
13956                    StrykeValue::float(lv.to_number() / d)
13957                }
13958            }
13959            BinOp::Mod => {
13960                let d = rv.to_int();
13961                if d == 0 {
13962                    return Err(StrykeError::division_by_zero("Illegal modulus zero", _line).into());
13963                }
13964                StrykeValue::integer(crate::value::perl_mod_i64(lv.to_int(), d))
13965            }
13966            BinOp::Pow => {
13967                // Under `--compat` or `use bigint;`, `compat_pow` promotes
13968                // to `BigInt` on overflow; otherwise it falls back to f64
13969                // (matches Perl's default i64-overflow-to-NV behavior).
13970                if crate::compat_mode() || crate::bigint_pragma() {
13971                    crate::value::compat_pow(lv, rv)
13972                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13973                    let int_pow = (b >= 0)
13974                        .then(|| u32::try_from(b).ok())
13975                        .flatten()
13976                        .and_then(|bu| a.checked_pow(bu))
13977                        .map(StrykeValue::integer);
13978                    int_pow
13979                        .unwrap_or_else(|| StrykeValue::float(lv.to_number().powf(rv.to_number())))
13980                } else {
13981                    StrykeValue::float(lv.to_number().powf(rv.to_number()))
13982                }
13983            }
13984            BinOp::Concat => {
13985                let mut s = String::new();
13986                lv.append_to(&mut s);
13987                rv.append_to(&mut s);
13988                StrykeValue::string(s)
13989            }
13990            BinOp::NumEq => {
13991                // Struct equality: compare all fields
13992                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
13993                    if a.def.name != b.def.name {
13994                        StrykeValue::integer(0)
13995                    } else {
13996                        let av = a.get_values();
13997                        let bv = b.get_values();
13998                        let eq = av.len() == bv.len()
13999                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
14000                        StrykeValue::integer(if eq { 1 } else { 0 })
14001                    }
14002                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14003                    StrykeValue::integer(if a == b { 1 } else { 0 })
14004                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
14005                    // Stryke (non-compat) sugar: `==` falls back to string
14006                    // compare when both operands are non-numeric strings, so
14007                    // `"G" == "G"` is true (Perl's `0 == 0` numeric is also
14008                    // true here, but `"G" == "T"` is false in stryke vs
14009                    // also-true in Perl). See `Op::NumEq` in vm.rs.
14010                    StrykeValue::integer(if lv.to_string() == rv.to_string() {
14011                        1
14012                    } else {
14013                        0
14014                    })
14015                } else {
14016                    StrykeValue::integer(if lv.to_number() == rv.to_number() {
14017                        1
14018                    } else {
14019                        0
14020                    })
14021                }
14022            }
14023            BinOp::NumNe => {
14024                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14025                    StrykeValue::integer(if a != b { 1 } else { 0 })
14026                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
14027                    StrykeValue::integer(if lv.to_string() != rv.to_string() {
14028                        1
14029                    } else {
14030                        0
14031                    })
14032                } else {
14033                    StrykeValue::integer(if lv.to_number() != rv.to_number() {
14034                        1
14035                    } else {
14036                        0
14037                    })
14038                }
14039            }
14040            BinOp::NumLt => {
14041                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14042                    StrykeValue::integer(if a < b { 1 } else { 0 })
14043                } else {
14044                    StrykeValue::integer(if lv.to_number() < rv.to_number() {
14045                        1
14046                    } else {
14047                        0
14048                    })
14049                }
14050            }
14051            BinOp::NumGt => {
14052                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14053                    StrykeValue::integer(if a > b { 1 } else { 0 })
14054                } else {
14055                    StrykeValue::integer(if lv.to_number() > rv.to_number() {
14056                        1
14057                    } else {
14058                        0
14059                    })
14060                }
14061            }
14062            BinOp::NumLe => {
14063                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14064                    StrykeValue::integer(if a <= b { 1 } else { 0 })
14065                } else {
14066                    StrykeValue::integer(if lv.to_number() <= rv.to_number() {
14067                        1
14068                    } else {
14069                        0
14070                    })
14071                }
14072            }
14073            BinOp::NumGe => {
14074                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14075                    StrykeValue::integer(if a >= b { 1 } else { 0 })
14076                } else {
14077                    StrykeValue::integer(if lv.to_number() >= rv.to_number() {
14078                        1
14079                    } else {
14080                        0
14081                    })
14082                }
14083            }
14084            BinOp::Spaceship => {
14085                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
14086                    StrykeValue::integer(if a < b {
14087                        -1
14088                    } else if a > b {
14089                        1
14090                    } else {
14091                        0
14092                    })
14093                } else {
14094                    let a = lv.to_number();
14095                    let b = rv.to_number();
14096                    StrykeValue::integer(if a < b {
14097                        -1
14098                    } else if a > b {
14099                        1
14100                    } else {
14101                        0
14102                    })
14103                }
14104            }
14105            BinOp::StrEq => StrykeValue::integer(if lv.to_string() == rv.to_string() {
14106                1
14107            } else {
14108                0
14109            }),
14110            BinOp::StrNe => StrykeValue::integer(if lv.to_string() != rv.to_string() {
14111                1
14112            } else {
14113                0
14114            }),
14115            BinOp::StrLt => StrykeValue::integer(if lv.to_string() < rv.to_string() {
14116                1
14117            } else {
14118                0
14119            }),
14120            BinOp::StrGt => StrykeValue::integer(if lv.to_string() > rv.to_string() {
14121                1
14122            } else {
14123                0
14124            }),
14125            BinOp::StrLe => StrykeValue::integer(if lv.to_string() <= rv.to_string() {
14126                1
14127            } else {
14128                0
14129            }),
14130            BinOp::StrGe => StrykeValue::integer(if lv.to_string() >= rv.to_string() {
14131                1
14132            } else {
14133                0
14134            }),
14135            BinOp::StrCmp => {
14136                let cmp = lv.to_string().cmp(&rv.to_string());
14137                StrykeValue::integer(match cmp {
14138                    std::cmp::Ordering::Less => -1,
14139                    std::cmp::Ordering::Greater => 1,
14140                    std::cmp::Ordering::Equal => 0,
14141                })
14142            }
14143            BinOp::BitAnd => {
14144                if let Some(s) = crate::value::set_intersection(lv, rv) {
14145                    s
14146                } else {
14147                    StrykeValue::integer(lv.to_int() & rv.to_int())
14148                }
14149            }
14150            BinOp::BitOr => {
14151                if let Some(s) = crate::value::set_union(lv, rv) {
14152                    s
14153                } else {
14154                    StrykeValue::integer(lv.to_int() | rv.to_int())
14155                }
14156            }
14157            BinOp::BitXor => StrykeValue::integer(lv.to_int() ^ rv.to_int()),
14158            BinOp::ShiftLeft => StrykeValue::integer(lv.to_int() << rv.to_int()),
14159            BinOp::ShiftRight => StrykeValue::integer(lv.to_int() >> rv.to_int()),
14160            // These should have been handled by short-circuit above
14161            BinOp::LogAnd
14162            | BinOp::LogOr
14163            | BinOp::DefinedOr
14164            | BinOp::LogAndWord
14165            | BinOp::LogOrWord => unreachable!(),
14166            BinOp::BindMatch | BinOp::BindNotMatch => {
14167                unreachable!("regex bind handled in eval_expr BinOp arm")
14168            }
14169        })
14170    }
14171
14172    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
14173    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
14174    /// length — that was silently wrong vs `perl`.
14175    fn err_modify_symbolic_aggregate_deref_inc_dec(
14176        kind: Sigil,
14177        is_pre: bool,
14178        is_inc: bool,
14179        line: usize,
14180    ) -> FlowOrError {
14181        let agg = match kind {
14182            Sigil::Array => "array",
14183            Sigil::Hash => "hash",
14184            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
14185        };
14186        let op = match (is_pre, is_inc) {
14187            (true, true) => "preincrement (++)",
14188            (true, false) => "predecrement (--)",
14189            (false, true) => "postincrement (++)",
14190            (false, false) => "postdecrement (--)",
14191        };
14192        FlowOrError::Error(StrykeError::runtime(
14193            format!("Can't modify {agg} dereference in {op}"),
14194            line,
14195        ))
14196    }
14197
14198    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
14199    pub(crate) fn symbolic_scalar_ref_postfix(
14200        &mut self,
14201        ref_val: StrykeValue,
14202        decrement: bool,
14203        line: usize,
14204    ) -> Result<StrykeValue, FlowOrError> {
14205        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
14206        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14207        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
14208        Ok(old)
14209    }
14210
14211    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
14212    /// [`Self::assign_value`] and the VM.
14213    pub(crate) fn assign_scalar_ref_deref(
14214        &mut self,
14215        ref_val: StrykeValue,
14216        val: StrykeValue,
14217        line: usize,
14218    ) -> ExecResult {
14219        if let Some(name) = ref_val.as_scalar_binding_name() {
14220            self.set_special_var(&name, &val)
14221                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14222            return Ok(StrykeValue::UNDEF);
14223        }
14224        if let Some(r) = ref_val.as_scalar_ref() {
14225            *r.write() = val;
14226            return Ok(StrykeValue::UNDEF);
14227        }
14228        // Plain primitive scalar value: under no-strict, perl symbolic-derefs
14229        // through the string. With `strict 'refs'`, emit perl's exact diagnostic.
14230        if ref_val.is_integer_like() || ref_val.is_float_like() || ref_val.is_string_like() {
14231            let s = ref_val.to_string();
14232            if self.strict_refs {
14233                return Err(StrykeError::runtime(
14234                    format!(
14235                        "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
14236                        s
14237                    ),
14238                    line,
14239                )
14240                .into());
14241            }
14242            self.set_special_var(&s, &val)
14243                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14244            return Ok(StrykeValue::UNDEF);
14245        }
14246        Err(StrykeError::runtime("Can't assign to non-scalar reference", line).into())
14247    }
14248
14249    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
14250    pub(crate) fn assign_symbolic_array_ref_deref(
14251        &mut self,
14252        ref_val: StrykeValue,
14253        val: StrykeValue,
14254        line: usize,
14255    ) -> ExecResult {
14256        if let Some(a) = ref_val.as_array_ref() {
14257            *a.write() = val.to_list();
14258            return Ok(StrykeValue::UNDEF);
14259        }
14260        if let Some(name) = ref_val.as_array_binding_name() {
14261            self.scope
14262                .set_array(&name, val.to_list())
14263                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14264            return Ok(StrykeValue::UNDEF);
14265        }
14266        if let Some(s) = ref_val.as_str() {
14267            if self.strict_refs {
14268                return Err(StrykeError::runtime(
14269                    format!(
14270                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
14271                        s
14272                    ),
14273                    line,
14274                )
14275                .into());
14276            }
14277            self.scope
14278                .set_array(&s, val.to_list())
14279                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14280            return Ok(StrykeValue::UNDEF);
14281        }
14282        Err(StrykeError::runtime("Can't assign to non-array reference", line).into())
14283    }
14284
14285    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
14286    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
14287    pub(crate) fn assign_symbolic_typeglob_ref_deref(
14288        &mut self,
14289        ref_val: StrykeValue,
14290        val: StrykeValue,
14291        line: usize,
14292    ) -> ExecResult {
14293        let lhs_name = if let Some(s) = ref_val.as_str() {
14294            if self.strict_refs {
14295                return Err(StrykeError::runtime(
14296                    format!(
14297                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
14298                        s
14299                    ),
14300                    line,
14301                )
14302                .into());
14303            }
14304            s.to_string()
14305        } else {
14306            return Err(
14307                StrykeError::runtime("Can't assign to non-glob symbolic reference", line).into(),
14308            );
14309        };
14310        let is_coderef = val.as_code_ref().is_some()
14311            || val
14312                .as_scalar_ref()
14313                .map(|r| r.read().as_code_ref().is_some())
14314                .unwrap_or(false);
14315        if is_coderef {
14316            return self.assign_typeglob_value(&lhs_name, val, line);
14317        }
14318        let rhs_key = val.to_string();
14319        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
14320            .map_err(FlowOrError::Error)?;
14321        Ok(StrykeValue::UNDEF)
14322    }
14323
14324    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
14325    pub(crate) fn assign_symbolic_hash_ref_deref(
14326        &mut self,
14327        ref_val: StrykeValue,
14328        val: StrykeValue,
14329        line: usize,
14330    ) -> ExecResult {
14331        let items = val.to_list();
14332        let mut map = IndexMap::new();
14333        let mut i = 0;
14334        while i + 1 < items.len() {
14335            map.insert(items[i].to_string(), items[i + 1].clone());
14336            i += 2;
14337        }
14338        if let Some(h) = ref_val.as_hash_ref() {
14339            *h.write() = map;
14340            return Ok(StrykeValue::UNDEF);
14341        }
14342        if let Some(name) = ref_val.as_hash_binding_name() {
14343            self.touch_env_hash(&name);
14344            self.scope
14345                .set_hash(&name, map)
14346                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14347            return Ok(StrykeValue::UNDEF);
14348        }
14349        if let Some(s) = ref_val.as_str() {
14350            if self.strict_refs {
14351                return Err(StrykeError::runtime(
14352                    format!(
14353                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
14354                        s
14355                    ),
14356                    line,
14357                )
14358                .into());
14359            }
14360            self.touch_env_hash(&s);
14361            self.scope
14362                .set_hash(&s, map)
14363                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14364            return Ok(StrykeValue::UNDEF);
14365        }
14366        Err(StrykeError::runtime("Can't assign to non-hash reference", line).into())
14367    }
14368
14369    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
14370    pub(crate) fn assign_arrow_hash_deref(
14371        &mut self,
14372        container: StrykeValue,
14373        key: String,
14374        val: StrykeValue,
14375        line: usize,
14376    ) -> ExecResult {
14377        if let Some(b) = container.as_blessed_ref() {
14378            let mut data = b.data.write();
14379            if let Some(r) = data.as_hash_ref() {
14380                r.write().insert(key, val);
14381                return Ok(StrykeValue::UNDEF);
14382            }
14383            if let Some(mut map) = data.as_hash_map() {
14384                map.insert(key, val);
14385                *data = StrykeValue::hash(map);
14386                return Ok(StrykeValue::UNDEF);
14387            }
14388            return Err(
14389                StrykeError::runtime("Can't assign into non-hash blessed ref", line).into(),
14390            );
14391        }
14392        if let Some(r) = container.as_hash_ref() {
14393            r.write().insert(key, val);
14394            return Ok(StrykeValue::UNDEF);
14395        }
14396        if let Some(name) = container.as_hash_binding_name() {
14397            self.touch_env_hash(&name);
14398            self.scope
14399                .set_hash_element(&name, &key, val)
14400                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14401            return Ok(StrykeValue::UNDEF);
14402        }
14403        Err(StrykeError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
14404    }
14405
14406    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
14407    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
14408    pub(crate) fn eval_arrow_array_base(
14409        &mut self,
14410        expr: &Expr,
14411        _line: usize,
14412    ) -> Result<StrykeValue, FlowOrError> {
14413        match &expr.kind {
14414            ExprKind::Deref {
14415                expr: inner,
14416                kind: Sigil::Array | Sigil::Scalar,
14417            } => self.eval_expr(inner),
14418            _ => self.eval_expr(expr),
14419        }
14420    }
14421
14422    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
14423    pub(crate) fn eval_arrow_hash_base(
14424        &mut self,
14425        expr: &Expr,
14426        _line: usize,
14427    ) -> Result<StrykeValue, FlowOrError> {
14428        match &expr.kind {
14429            ExprKind::Deref {
14430                expr: inner,
14431                kind: Sigil::Scalar,
14432            } => self.eval_expr(inner),
14433            _ => self.eval_expr(expr),
14434        }
14435    }
14436
14437    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
14438    pub(crate) fn read_arrow_array_element(
14439        &self,
14440        container: StrykeValue,
14441        idx: i64,
14442        line: usize,
14443    ) -> Result<StrykeValue, FlowOrError> {
14444        if let Some(a) = container.as_array_ref() {
14445            let arr = a.read();
14446            let i = if idx < 0 {
14447                (arr.len() as i64 + idx) as usize
14448            } else {
14449                idx as usize
14450            };
14451            return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
14452        }
14453        if let Some(name) = container.as_array_binding_name() {
14454            return Ok(self.scope.get_array_element(&name, idx));
14455        }
14456        if let Some(arr) = container.as_array_vec() {
14457            let i = if idx < 0 {
14458                (arr.len() as i64 + idx) as usize
14459            } else {
14460                idx as usize
14461            };
14462            return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
14463        }
14464        // Blessed arrayref (e.g. `Pair`) — `pairs` returns blessed `Pair` objects that
14465        // can be indexed via `$_->[0]` / `$_->[1]`.
14466        if let Some(b) = container.as_blessed_ref() {
14467            let inner = b.data.read().clone();
14468            if let Some(a) = inner.as_array_ref() {
14469                let arr = a.read();
14470                let i = if idx < 0 {
14471                    (arr.len() as i64 + idx) as usize
14472                } else {
14473                    idx as usize
14474                };
14475                return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
14476            }
14477        }
14478        Err(StrykeError::runtime("Can't use arrow deref on non-array-ref", line).into())
14479    }
14480
14481    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
14482    pub(crate) fn read_arrow_hash_element(
14483        &mut self,
14484        container: StrykeValue,
14485        key: &str,
14486        line: usize,
14487    ) -> Result<StrykeValue, FlowOrError> {
14488        if let Some(r) = container.as_hash_ref() {
14489            let h = r.read();
14490            return Ok(h.get(key).cloned().unwrap_or(StrykeValue::UNDEF));
14491        }
14492        if let Some(name) = container.as_hash_binding_name() {
14493            self.touch_env_hash(&name);
14494            return Ok(self.scope.get_hash_element(&name, key));
14495        }
14496        if let Some(b) = container.as_blessed_ref() {
14497            let data = b.data.read();
14498            if let Some(v) = data.hash_get(key) {
14499                return Ok(v);
14500            }
14501            if let Some(r) = data.as_hash_ref() {
14502                let h = r.read();
14503                return Ok(h.get(key).cloned().unwrap_or(StrykeValue::UNDEF));
14504            }
14505            return Err(StrykeError::runtime(
14506                "Can't access hash field on non-hash blessed ref",
14507                line,
14508            )
14509            .into());
14510        }
14511        // Struct field access via hash deref syntax: $struct->{field}
14512        if let Some(s) = container.as_struct_inst() {
14513            if let Some(idx) = s.def.field_index(key) {
14514                return Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF));
14515            }
14516            return Err(StrykeError::runtime(
14517                format!("struct {} has no field `{}`", s.def.name, key),
14518                line,
14519            )
14520            .into());
14521        }
14522        // Class instance field access via hash deref: $obj->{field}
14523        if let Some(c) = container.as_class_inst() {
14524            if let Some(idx) = c.def.field_index(key) {
14525                return Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF));
14526            }
14527            return Err(StrykeError::runtime(
14528                format!("class {} has no field `{}`", c.def.name, key),
14529                line,
14530            )
14531            .into());
14532        }
14533        Err(StrykeError::runtime("Can't use arrow deref on non-hash-ref", line).into())
14534    }
14535
14536    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
14537    pub(crate) fn arrow_array_postfix(
14538        &mut self,
14539        container: StrykeValue,
14540        idx: i64,
14541        decrement: bool,
14542        line: usize,
14543    ) -> Result<StrykeValue, FlowOrError> {
14544        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
14545        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14546        self.assign_arrow_array_deref(container, idx, new_val, line)?;
14547        Ok(old)
14548    }
14549
14550    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
14551    pub(crate) fn arrow_hash_postfix(
14552        &mut self,
14553        container: StrykeValue,
14554        key: String,
14555        decrement: bool,
14556        line: usize,
14557    ) -> Result<StrykeValue, FlowOrError> {
14558        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
14559        let new_val = StrykeValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14560        self.assign_arrow_hash_deref(container, key, new_val, line)?;
14561        Ok(old)
14562    }
14563
14564    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` evaluation. If a nullary
14565    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
14566    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
14567    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
14568    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
14569    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
14570    /// subs" in use`).
14571    pub(crate) fn resolve_bareword_rvalue(
14572        &mut self,
14573        name: &str,
14574        want: WantarrayCtx,
14575        line: usize,
14576    ) -> Result<StrykeValue, FlowOrError> {
14577        if name == "__PACKAGE__" {
14578            return Ok(StrykeValue::string(self.current_package()));
14579        }
14580        if let Some(sub) = self.resolve_sub_by_name(name) {
14581            return self.call_sub(&sub, vec![], want, line);
14582        }
14583        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
14584        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
14585            return r.map_err(Into::into);
14586        }
14587        Ok(StrykeValue::string(name.to_string()))
14588    }
14589
14590    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
14591    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
14592    /// compound / inc-dec / assign helpers below.
14593    pub(crate) fn arrow_array_slice_values(
14594        &mut self,
14595        container: StrykeValue,
14596        indices: &[i64],
14597        line: usize,
14598    ) -> Result<StrykeValue, FlowOrError> {
14599        let mut out = Vec::with_capacity(indices.len());
14600        for &idx in indices {
14601            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
14602            out.push(v);
14603        }
14604        Ok(StrykeValue::array(out))
14605    }
14606
14607    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment for
14608    /// multi-index `ArrowDeref { Array, List }`. Shared by the VM
14609    /// [`crate::bytecode::Op::SetArrowArraySlice`].
14610    pub(crate) fn assign_arrow_array_slice(
14611        &mut self,
14612        container: StrykeValue,
14613        indices: Vec<i64>,
14614        val: StrykeValue,
14615        line: usize,
14616    ) -> Result<StrykeValue, FlowOrError> {
14617        if indices.is_empty() {
14618            return Err(StrykeError::runtime("assign to empty array slice", line).into());
14619        }
14620        let vals = val.to_list();
14621        for (i, idx) in indices.iter().enumerate() {
14622            let v = vals.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
14623            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
14624        }
14625        Ok(StrykeValue::UNDEF)
14626    }
14627
14628    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
14629    pub(crate) fn flatten_array_slice_index_specs(
14630        &mut self,
14631        indices: &[Expr],
14632    ) -> Result<Vec<i64>, FlowOrError> {
14633        let mut out = Vec::new();
14634        for idx_expr in indices {
14635            let v = if matches!(
14636                idx_expr.kind,
14637                ExprKind::Range { .. } | ExprKind::SliceRange { .. }
14638            ) {
14639                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
14640            } else {
14641                self.eval_expr(idx_expr)?
14642            };
14643            if let Some(list) = v.as_array_vec() {
14644                for idx in list {
14645                    out.push(idx.to_int());
14646                }
14647            } else {
14648                out.push(v.to_int());
14649            }
14650        }
14651        Ok(out)
14652    }
14653
14654    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
14655    pub(crate) fn assign_named_array_slice(
14656        &mut self,
14657        stash_array_name: &str,
14658        indices: Vec<i64>,
14659        val: StrykeValue,
14660        line: usize,
14661    ) -> Result<StrykeValue, FlowOrError> {
14662        if indices.is_empty() {
14663            return Err(StrykeError::runtime("assign to empty array slice", line).into());
14664        }
14665        let vals = val.to_list();
14666        for (i, idx) in indices.iter().enumerate() {
14667            let v = vals.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
14668            self.scope
14669                .set_array_element(stash_array_name, *idx, v)
14670                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14671        }
14672        Ok(StrykeValue::UNDEF)
14673    }
14674
14675    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
14676    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
14677    pub(crate) fn compound_assign_arrow_array_slice(
14678        &mut self,
14679        container: StrykeValue,
14680        indices: Vec<i64>,
14681        op: BinOp,
14682        rhs: StrykeValue,
14683        line: usize,
14684    ) -> Result<StrykeValue, FlowOrError> {
14685        if indices.is_empty() {
14686            return Err(StrykeError::runtime("assign to empty array slice", line).into());
14687        }
14688        let last_idx = *indices.last().expect("non-empty indices");
14689        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
14690        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
14691        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
14692        Ok(new_val)
14693    }
14694
14695    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
14696    /// pre forms return the new value, post forms return the old **last** element.
14697    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
14698    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
14699    pub(crate) fn arrow_array_slice_inc_dec(
14700        &mut self,
14701        container: StrykeValue,
14702        indices: Vec<i64>,
14703        kind: u8,
14704        line: usize,
14705    ) -> Result<StrykeValue, FlowOrError> {
14706        if indices.is_empty() {
14707            return Err(StrykeError::runtime(
14708                "array slice increment needs at least one index",
14709                line,
14710            )
14711            .into());
14712        }
14713        let last_idx = *indices.last().expect("non-empty indices");
14714        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
14715        let new_val = if kind & 1 == 0 {
14716            StrykeValue::integer(last_old.to_int() + 1)
14717        } else {
14718            StrykeValue::integer(last_old.to_int() - 1)
14719        };
14720        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
14721        Ok(if kind < 2 { new_val } else { last_old })
14722    }
14723
14724    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
14725    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
14726    pub(crate) fn named_array_slice_inc_dec(
14727        &mut self,
14728        stash_array_name: &str,
14729        indices: Vec<i64>,
14730        kind: u8,
14731        line: usize,
14732    ) -> Result<StrykeValue, FlowOrError> {
14733        let last_idx = *indices.last().ok_or_else(|| {
14734            StrykeError::runtime("array slice increment needs at least one index", line)
14735        })?;
14736        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
14737        let new_val = if kind & 1 == 0 {
14738            StrykeValue::integer(last_old.to_int() + 1)
14739        } else {
14740            StrykeValue::integer(last_old.to_int() - 1)
14741        };
14742        self.scope
14743            .set_array_element(stash_array_name, last_idx, new_val.clone())
14744            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14745        Ok(if kind < 2 { new_val } else { last_old })
14746    }
14747
14748    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
14749    pub(crate) fn compound_assign_named_array_slice(
14750        &mut self,
14751        stash_array_name: &str,
14752        indices: Vec<i64>,
14753        op: BinOp,
14754        rhs: StrykeValue,
14755        line: usize,
14756    ) -> Result<StrykeValue, FlowOrError> {
14757        if indices.is_empty() {
14758            return Err(StrykeError::runtime("assign to empty array slice", line).into());
14759        }
14760        let last_idx = *indices.last().expect("non-empty indices");
14761        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
14762        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
14763        self.scope
14764            .set_array_element(stash_array_name, last_idx, new_val.clone())
14765            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14766        Ok(new_val)
14767    }
14768
14769    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
14770    pub(crate) fn assign_arrow_array_deref(
14771        &mut self,
14772        container: StrykeValue,
14773        idx: i64,
14774        val: StrykeValue,
14775        line: usize,
14776    ) -> ExecResult {
14777        if let Some(a) = container.as_array_ref() {
14778            let mut arr = a.write();
14779            let i = if idx < 0 {
14780                (arr.len() as i64 + idx) as usize
14781            } else {
14782                idx as usize
14783            };
14784            if i >= arr.len() {
14785                arr.resize(i + 1, StrykeValue::UNDEF);
14786            }
14787            arr[i] = val;
14788            return Ok(StrykeValue::UNDEF);
14789        }
14790        if let Some(name) = container.as_array_binding_name() {
14791            self.scope
14792                .set_array_element(&name, idx, val)
14793                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14794            return Ok(StrykeValue::UNDEF);
14795        }
14796        Err(StrykeError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
14797    }
14798
14799    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
14800    pub(crate) fn assign_typeglob_value(
14801        &mut self,
14802        name: &str,
14803        val: StrykeValue,
14804        line: usize,
14805    ) -> ExecResult {
14806        let sub = if let Some(c) = val.as_code_ref() {
14807            Some(c)
14808        } else if let Some(r) = val.as_scalar_ref() {
14809            r.read().as_code_ref().map(|c| Arc::clone(&c))
14810        } else {
14811            None
14812        };
14813        if let Some(sub) = sub {
14814            let lhs_sub = self.qualify_typeglob_sub_key(name);
14815            self.subs.insert(lhs_sub, sub);
14816            return Ok(StrykeValue::UNDEF);
14817        }
14818        Err(StrykeError::runtime(
14819            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
14820            line,
14821        )
14822        .into())
14823    }
14824
14825    fn assign_value(&mut self, target: &Expr, val: StrykeValue) -> ExecResult {
14826        match &target.kind {
14827            // `substr($s, $o, $l) = $rhs` — equivalent to the 4-arg form
14828            // `substr($s, $o, $l, $rhs)`. Evaluate the offset/length
14829            // sub-exprs, splice the new substring into the target, then
14830            // recursively call assign_value to write the modified string
14831            // through the original `string` lvalue.
14832            ExprKind::Substr {
14833                string,
14834                offset,
14835                length,
14836                replacement: None,
14837            } => {
14838                let s = self.eval_expr(string)?.to_string();
14839                let off = self.eval_expr(offset)?.to_int();
14840                let start = if off < 0 {
14841                    (s.len() as i64 + off).max(0) as usize
14842                } else {
14843                    (off as usize).min(s.len())
14844                };
14845                let len = if let Some(l) = length {
14846                    let lv = self.eval_expr(l)?.to_int();
14847                    if lv < 0 {
14848                        let remaining = s.len().saturating_sub(start) as i64;
14849                        (remaining + lv).max(0) as usize
14850                    } else {
14851                        lv as usize
14852                    }
14853                } else {
14854                    s.len().saturating_sub(start)
14855                };
14856                let end = start.saturating_add(len).min(s.len());
14857                let mut new_s = String::with_capacity(s.len());
14858                new_s.push_str(&s[..start]);
14859                new_s.push_str(&val.to_string());
14860                new_s.push_str(&s[end..]);
14861                self.assign_value(string, StrykeValue::string(new_s))?;
14862                Ok(StrykeValue::UNDEF)
14863            }
14864            // `(my $copy = $orig) =~ s/.../.../` — at this point the
14865            // MyExpr has already been evaluated as a side-effect of
14866            // `eval_expr(target)` upstream (so `$copy` has been declared
14867            // and initialized). The substitution / transliteration helpers
14868            // call back here to write the *new* string. Bind through the
14869            // declared name without re-running the initializer.
14870            ExprKind::MyExpr { decls, .. } => {
14871                let first = decls.first().ok_or_else(|| {
14872                    FlowOrError::Error(StrykeError::runtime(
14873                        "assign_value: empty MyExpr decl list",
14874                        target.line,
14875                    ))
14876                })?;
14877                match first.sigil {
14878                    Sigil::Scalar => {
14879                        let stor = self.tree_scalar_storage_name(&first.name);
14880                        self.set_special_var(&stor, &val)
14881                            .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
14882                        Ok(StrykeValue::UNDEF)
14883                    }
14884                    Sigil::Array => {
14885                        self.scope.set_array(&first.name, val.to_list())?;
14886                        Ok(StrykeValue::UNDEF)
14887                    }
14888                    Sigil::Hash => {
14889                        let items = val.to_list();
14890                        let mut map = IndexMap::new();
14891                        let mut i = 0;
14892                        while i + 1 < items.len() {
14893                            map.insert(items[i].to_string(), items[i + 1].clone());
14894                            i += 2;
14895                        }
14896                        self.scope.set_hash(&first.name, map)?;
14897                        Ok(StrykeValue::UNDEF)
14898                    }
14899                    Sigil::Typeglob => Ok(StrykeValue::UNDEF),
14900                }
14901            }
14902            ExprKind::ScalarVar(name) => {
14903                let stor = self.tree_scalar_storage_name(name);
14904                if self.scope.is_scalar_frozen(&stor) {
14905                    return Err(FlowOrError::Error(StrykeError::runtime(
14906                        format!("Modification of a frozen value: ${}", name),
14907                        target.line,
14908                    )));
14909                }
14910                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
14911                    let class = obj
14912                        .as_blessed_ref()
14913                        .map(|b| b.class.clone())
14914                        .unwrap_or_default();
14915                    let full = format!("{}::STORE", class);
14916                    if let Some(sub) = self.subs.get(&full).cloned() {
14917                        let arg_vals = vec![obj, val];
14918                        return match self.call_sub(
14919                            &sub,
14920                            arg_vals,
14921                            WantarrayCtx::Scalar,
14922                            target.line,
14923                        ) {
14924                            Ok(_) => Ok(StrykeValue::UNDEF),
14925                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
14926                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
14927                        };
14928                    }
14929                }
14930                self.set_special_var(&stor, &val)
14931                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
14932                Ok(StrykeValue::UNDEF)
14933            }
14934            ExprKind::ArrayVar(name) => {
14935                if self.scope.is_array_frozen(name) {
14936                    return Err(StrykeError::runtime(
14937                        format!("Modification of a frozen value: @{}", name),
14938                        target.line,
14939                    )
14940                    .into());
14941                }
14942                if self.strict_vars
14943                    && !name.contains("::")
14944                    && !self.scope.array_binding_exists(name)
14945                {
14946                    return Err(StrykeError::runtime(
14947                        format!(
14948                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
14949                            name, name
14950                        ),
14951                        target.line,
14952                    )
14953                    .into());
14954                }
14955                self.scope.set_array(name, val.to_list())?;
14956                Ok(StrykeValue::UNDEF)
14957            }
14958            ExprKind::HashVar(name) => {
14959                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
14960                {
14961                    return Err(StrykeError::runtime(
14962                        format!(
14963                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
14964                            name, name
14965                        ),
14966                        target.line,
14967                    )
14968                    .into());
14969                }
14970                let items = val.to_list();
14971                let mut map = IndexMap::new();
14972                let mut i = 0;
14973                while i + 1 < items.len() {
14974                    map.insert(items[i].to_string(), items[i + 1].clone());
14975                    i += 2;
14976                }
14977                self.scope.set_hash(name, map)?;
14978                Ok(StrykeValue::UNDEF)
14979            }
14980            ExprKind::ArrayElement { array, index } => {
14981                if self.strict_vars
14982                    && !array.contains("::")
14983                    && !self.scope.array_binding_exists(array)
14984                {
14985                    return Err(StrykeError::runtime(
14986                        format!(
14987                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
14988                            array, array
14989                        ),
14990                        target.line,
14991                    )
14992                    .into());
14993                }
14994                if self.scope.is_array_frozen(array) {
14995                    return Err(StrykeError::runtime(
14996                        format!("Modification of a frozen value: @{}", array),
14997                        target.line,
14998                    )
14999                    .into());
15000                }
15001                let idx = self.eval_expr(index)?.to_int();
15002                let aname = self.stash_array_name_for_package(array);
15003                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
15004                    let class = obj
15005                        .as_blessed_ref()
15006                        .map(|b| b.class.clone())
15007                        .unwrap_or_default();
15008                    let full = format!("{}::STORE", class);
15009                    if let Some(sub) = self.subs.get(&full).cloned() {
15010                        let arg_vals = vec![obj, StrykeValue::integer(idx), val];
15011                        return match self.call_sub(
15012                            &sub,
15013                            arg_vals,
15014                            WantarrayCtx::Scalar,
15015                            target.line,
15016                        ) {
15017                            Ok(_) => Ok(StrykeValue::UNDEF),
15018                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15019                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15020                        };
15021                    }
15022                }
15023                self.scope.set_array_element(&aname, idx, val)?;
15024                Ok(StrykeValue::UNDEF)
15025            }
15026            ExprKind::ArraySlice { array, indices } => {
15027                if indices.is_empty() {
15028                    return Err(
15029                        StrykeError::runtime("assign to empty array slice", target.line).into(),
15030                    );
15031                }
15032                self.check_strict_array_var(array, target.line)?;
15033                if self.scope.is_array_frozen(array) {
15034                    return Err(StrykeError::runtime(
15035                        format!("Modification of a frozen value: @{}", array),
15036                        target.line,
15037                    )
15038                    .into());
15039                }
15040                let aname = self.stash_array_name_for_package(array);
15041                let flat = self.flatten_array_slice_index_specs(indices)?;
15042                self.assign_named_array_slice(&aname, flat, val, target.line)
15043            }
15044            ExprKind::HashElement { hash, key } => {
15045                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
15046                {
15047                    return Err(StrykeError::runtime(
15048                        format!(
15049                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15050                            hash, hash
15051                        ),
15052                        target.line,
15053                    )
15054                    .into());
15055                }
15056                if self.scope.is_hash_frozen(hash) {
15057                    return Err(StrykeError::runtime(
15058                        format!("Modification of a frozen value: %%{}", hash),
15059                        target.line,
15060                    )
15061                    .into());
15062                }
15063                let k = self.eval_expr(key)?.to_string();
15064                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
15065                    let class = obj
15066                        .as_blessed_ref()
15067                        .map(|b| b.class.clone())
15068                        .unwrap_or_default();
15069                    let full = format!("{}::STORE", class);
15070                    if let Some(sub) = self.subs.get(&full).cloned() {
15071                        let arg_vals = vec![obj, StrykeValue::string(k), val];
15072                        return match self.call_sub(
15073                            &sub,
15074                            arg_vals,
15075                            WantarrayCtx::Scalar,
15076                            target.line,
15077                        ) {
15078                            Ok(_) => Ok(StrykeValue::UNDEF),
15079                            Err(FlowOrError::Flow(_)) => Ok(StrykeValue::UNDEF),
15080                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
15081                        };
15082                    }
15083                }
15084                self.scope.set_hash_element(hash, &k, val)?;
15085                Ok(StrykeValue::UNDEF)
15086            }
15087            ExprKind::HashSlice { hash, keys } => {
15088                if keys.is_empty() {
15089                    return Err(
15090                        StrykeError::runtime("assign to empty hash slice", target.line).into(),
15091                    );
15092                }
15093                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
15094                {
15095                    return Err(StrykeError::runtime(
15096                        format!(
15097                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
15098                            hash, hash
15099                        ),
15100                        target.line,
15101                    )
15102                    .into());
15103                }
15104                if self.scope.is_hash_frozen(hash) {
15105                    return Err(StrykeError::runtime(
15106                        format!("Modification of a frozen value: %%{}", hash),
15107                        target.line,
15108                    )
15109                    .into());
15110                }
15111                let mut key_vals = Vec::with_capacity(keys.len());
15112                for key_expr in keys {
15113                    let v = if matches!(
15114                        key_expr.kind,
15115                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
15116                    ) {
15117                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
15118                    } else {
15119                        self.eval_expr(key_expr)?
15120                    };
15121                    key_vals.push(v);
15122                }
15123                self.assign_named_hash_slice(hash, key_vals, val, target.line)
15124            }
15125            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
15126            ExprKind::TypeglobExpr(e) => {
15127                let name = self.eval_expr(e)?.to_string();
15128                let synthetic = Expr {
15129                    kind: ExprKind::Typeglob(name),
15130                    line: target.line,
15131                };
15132                self.assign_value(&synthetic, val)
15133            }
15134            ExprKind::AnonymousListSlice { source, indices } => {
15135                if let ExprKind::Deref {
15136                    expr: inner,
15137                    kind: Sigil::Array,
15138                } = &source.kind
15139                {
15140                    let container = self.eval_arrow_array_base(inner, target.line)?;
15141                    let vals = val.to_list();
15142                    let n = indices.len().min(vals.len());
15143                    for i in 0..n {
15144                        let idx = self.eval_expr(&indices[i])?.to_int();
15145                        self.assign_arrow_array_deref(
15146                            container.clone(),
15147                            idx,
15148                            vals[i].clone(),
15149                            target.line,
15150                        )?;
15151                    }
15152                    return Ok(StrykeValue::UNDEF);
15153                }
15154                Err(
15155                    StrykeError::runtime("assign to list slice: unsupported base", target.line)
15156                        .into(),
15157                )
15158            }
15159            ExprKind::ArrowDeref {
15160                expr,
15161                index,
15162                kind: DerefKind::Hash,
15163            } => {
15164                let key = self.eval_expr(index)?.to_string();
15165                let container = self.eval_expr(expr)?;
15166                self.assign_arrow_hash_deref(container, key, val, target.line)
15167            }
15168            ExprKind::ArrowDeref {
15169                expr,
15170                index,
15171                kind: DerefKind::Array,
15172            } => {
15173                let container = self.eval_arrow_array_base(expr, target.line)?;
15174                if let ExprKind::List(indices) = &index.kind {
15175                    let vals = val.to_list();
15176                    let n = indices.len().min(vals.len());
15177                    for i in 0..n {
15178                        let idx = self.eval_expr(&indices[i])?.to_int();
15179                        self.assign_arrow_array_deref(
15180                            container.clone(),
15181                            idx,
15182                            vals[i].clone(),
15183                            target.line,
15184                        )?;
15185                    }
15186                    return Ok(StrykeValue::UNDEF);
15187                }
15188                let idx = self.eval_expr(index)?.to_int();
15189                self.assign_arrow_array_deref(container, idx, val, target.line)
15190            }
15191            ExprKind::HashSliceDeref { container, keys } => {
15192                let href = self.eval_expr(container)?;
15193                let mut key_vals = Vec::with_capacity(keys.len());
15194                for key_expr in keys {
15195                    key_vals.push(self.eval_expr(key_expr)?);
15196                }
15197                self.assign_hash_slice_deref(href, key_vals, val, target.line)
15198            }
15199            ExprKind::Deref {
15200                expr,
15201                kind: Sigil::Scalar,
15202            } => {
15203                let ref_val = self.eval_expr(expr)?;
15204                self.assign_scalar_ref_deref(ref_val, val, target.line)
15205            }
15206            ExprKind::Deref {
15207                expr,
15208                kind: Sigil::Array,
15209            } => {
15210                let ref_val = self.eval_expr(expr)?;
15211                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
15212            }
15213            ExprKind::Deref {
15214                expr,
15215                kind: Sigil::Hash,
15216            } => {
15217                let ref_val = self.eval_expr(expr)?;
15218                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
15219            }
15220            ExprKind::Deref {
15221                expr,
15222                kind: Sigil::Typeglob,
15223            } => {
15224                let ref_val = self.eval_expr(expr)?;
15225                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
15226            }
15227            ExprKind::Pos(inner) => {
15228                let key = match inner {
15229                    None => "_".to_string(),
15230                    Some(expr) => match &expr.kind {
15231                        ExprKind::ScalarVar(n) => n.clone(),
15232                        _ => self.eval_expr(expr)?.to_string(),
15233                    },
15234                };
15235                if val.is_undef() {
15236                    self.regex_pos.insert(key, None);
15237                } else {
15238                    let u = val.to_int().max(0) as usize;
15239                    self.regex_pos.insert(key, Some(u));
15240                }
15241                Ok(StrykeValue::UNDEF)
15242            }
15243            // List assignment: `($a, $b, ...) = (val1, val2, ...)`
15244            // RHS is already fully evaluated — distribute elements to targets.
15245            ExprKind::List(targets) => {
15246                let items = val.to_list();
15247                for (i, t) in targets.iter().enumerate() {
15248                    let v = items.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
15249                    self.assign_value(t, v)?;
15250                }
15251                Ok(StrykeValue::UNDEF)
15252            }
15253            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
15254            // write the substitution result back to the assignment target.
15255            ExprKind::Assign { target, .. } => self.assign_value(target, val),
15256            _ => Ok(StrykeValue::UNDEF),
15257        }
15258    }
15259
15260    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
15261    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
15262        (name.starts_with('#') && name.len() > 1)
15263            || name.starts_with('^')
15264            || matches!(
15265                name,
15266                "$$" | "0"
15267                    | "!"
15268                    | "@"
15269                    | "/"
15270                    | "\\"
15271                    | ","
15272                    | "."
15273                    | "]"
15274                    | ";"
15275                    | "ARGV"
15276                    | "^I"
15277                    | "^D"
15278                    | "^P"
15279                    | "^S"
15280                    | "^W"
15281                    | "^O"
15282                    | "^T"
15283                    | "^V"
15284                    | "^E"
15285                    | "^H"
15286                    | "^WARNING_BITS"
15287                    | "^GLOBAL_PHASE"
15288                    | "^MATCH"
15289                    | "^PREMATCH"
15290                    | "^POSTMATCH"
15291                    | "^LAST_SUBMATCH_RESULT"
15292                    | "<"
15293                    | ">"
15294                    | "("
15295                    | ")"
15296                    | "?"
15297                    | "|"
15298                    | "\""
15299                    | "+"
15300                    | "%"
15301                    | "="
15302                    | "-"
15303                    | ":"
15304                    | "*"
15305                    | "INC"
15306            )
15307            || crate::english::is_known_alias(name)
15308    }
15309
15310    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
15311    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
15312    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
15313    /// [`Self::english_no_match_vars`] is set.
15314    #[inline]
15315    /// English alias resolution + `our`/`oursync` package qualification in one call.
15316    /// Returns the storage key the scope expects: `$ARG` → `_`, then `our $x` → `Pkg::x`.
15317    /// Use this for compound ops (`++`, `--`, `+=`, `||=`, etc.) so atomic-RMW lookups
15318    /// hit the package-qualified cell stored by `oursync`.
15319    pub(crate) fn resolved_scalar_storage_name(&self, name: &str) -> String {
15320        self.tree_scalar_storage_name(self.english_scalar_name(name))
15321    }
15322
15323    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
15324        if !self.english_enabled {
15325            return name;
15326        }
15327        if self
15328            .english_lexical_scalars
15329            .iter()
15330            .any(|s| s.contains(name))
15331        {
15332            return name;
15333        }
15334        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
15335            return short;
15336        }
15337        name
15338    }
15339
15340    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
15341    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
15342        // `$#name = N` resizes `@name` (Perl: setting the last index). The
15343        // bare-set path stores under literal `#name` as a separate scalar
15344        // and silently does nothing useful — match the read-side handling
15345        // by routing through `set_special_var`.
15346        (name.starts_with('#') && name.len() > 1)
15347            || name.starts_with('^')
15348            || matches!(
15349                name,
15350                "0" | "/"
15351                    | "\\"
15352                    | ","
15353                    | ";"
15354                    | "\""
15355                    | "%"
15356                    | "="
15357                    | "-"
15358                    | ":"
15359                    | "*"
15360                    | "INC"
15361                    | "^I"
15362                    | "^D"
15363                    | "^P"
15364                    | "^W"
15365                    | "^H"
15366                    | "^WARNING_BITS"
15367                    | "$$"
15368                    | "]"
15369                    | "^S"
15370                    | "ARGV"
15371                    | "|"
15372                    | "+"
15373                    | "?"
15374                    | "!"
15375                    | "@"
15376                    | "."
15377            )
15378            || crate::english::is_known_alias(name)
15379    }
15380
15381    pub(crate) fn get_special_var(&self, name: &str) -> StrykeValue {
15382        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
15383        let name = if !crate::compat_mode() {
15384            match name {
15385                "NR" => ".",
15386                "RS" => "/",
15387                "OFS" => ",",
15388                "ORS" => "\\",
15389                "NF" => {
15390                    let len = self.scope.array_len("F");
15391                    return StrykeValue::integer(len as i64);
15392                }
15393                _ => self.english_scalar_name(name),
15394            }
15395        } else {
15396            self.english_scalar_name(name)
15397        };
15398        match name {
15399            "$$" => StrykeValue::integer(std::process::id() as i64),
15400            "_" => self.scope.get_scalar("_"),
15401            "^MATCH" => StrykeValue::string(self.last_match.clone()),
15402            "^PREMATCH" => StrykeValue::string(self.prematch.clone()),
15403            "^POSTMATCH" => StrykeValue::string(self.postmatch.clone()),
15404            "^LAST_SUBMATCH_RESULT" => StrykeValue::string(self.last_paren_match.clone()),
15405            "0" => StrykeValue::string(self.program_name.clone()),
15406            "!" => StrykeValue::errno_dual(self.errno_code, self.errno.clone()),
15407            "@" => {
15408                if let Some(ref v) = self.eval_error_value {
15409                    v.clone()
15410                } else {
15411                    StrykeValue::errno_dual(self.eval_error_code, self.eval_error.clone())
15412                }
15413            }
15414            "/" => match &self.irs {
15415                Some(s) => StrykeValue::string(s.clone()),
15416                None => StrykeValue::UNDEF,
15417            },
15418            "\\" => StrykeValue::string(self.ors.clone()),
15419            "," => StrykeValue::string(self.ofs.clone()),
15420            "." => {
15421                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
15422                if self.last_readline_handle.is_empty() {
15423                    if self.line_number == 0 {
15424                        StrykeValue::UNDEF
15425                    } else {
15426                        StrykeValue::integer(self.line_number)
15427                    }
15428                } else {
15429                    StrykeValue::integer(
15430                        *self
15431                            .handle_line_numbers
15432                            .get(&self.last_readline_handle)
15433                            .unwrap_or(&0),
15434                    )
15435                }
15436            }
15437            "]" => StrykeValue::float(perl_bracket_version()),
15438            ";" => StrykeValue::string(self.subscript_sep.clone()),
15439            "ARGV" => StrykeValue::string(self.argv_current_file.clone()),
15440            "^I" => StrykeValue::string(self.inplace_edit.clone()),
15441            "^D" => StrykeValue::integer(self.debug_flags),
15442            "^P" => StrykeValue::integer(self.perl_debug_flags),
15443            "^S" => StrykeValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
15444            "^W" => StrykeValue::integer(if self.warnings { 1 } else { 0 }),
15445            "^O" => StrykeValue::string(perl_osname()),
15446            "^T" => StrykeValue::integer(self.script_start_time),
15447            "^V" => StrykeValue::string(perl_version_v_string()),
15448            "^E" => StrykeValue::string(extended_os_error_string()),
15449            "^H" => StrykeValue::integer(self.compile_hints),
15450            "^WARNING_BITS" => StrykeValue::integer(self.warning_bits),
15451            "^GLOBAL_PHASE" => StrykeValue::string(self.global_phase.clone()),
15452            "<" | ">" => StrykeValue::integer(unix_id_for_special(name)),
15453            "(" | ")" => StrykeValue::string(unix_group_list_for_special(name)),
15454            "?" => StrykeValue::integer(self.child_exit_status),
15455            "|" => StrykeValue::integer(if self.output_autoflush { 1 } else { 0 }),
15456            "\"" => StrykeValue::string(self.list_separator.clone()),
15457            "+" => StrykeValue::string(self.last_paren_match.clone()),
15458            "%" => StrykeValue::integer(self.format_page_number),
15459            "=" => StrykeValue::integer(self.format_lines_per_page),
15460            "-" => StrykeValue::integer(self.format_lines_left),
15461            ":" => StrykeValue::string(self.format_line_break_chars.clone()),
15462            "*" => StrykeValue::integer(if self.multiline_match { 1 } else { 0 }),
15463            "^" => StrykeValue::string(self.format_top_name.clone()),
15464            "INC" => StrykeValue::integer(self.inc_hook_index),
15465            "^A" => StrykeValue::string(self.accumulator_format.clone()),
15466            "^C" => StrykeValue::integer(if self.sigint_pending_caret.replace(false) {
15467                1
15468            } else {
15469                0
15470            }),
15471            "^F" => StrykeValue::integer(self.max_system_fd),
15472            "^L" => StrykeValue::string(self.formfeed_string.clone()),
15473            "^M" => StrykeValue::string(self.emergency_memory.clone()),
15474            "^N" => StrykeValue::string(self.last_subpattern_name.clone()),
15475            "^X" => StrykeValue::string(self.executable_path.clone()),
15476            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
15477            "^TAINT" | "^TAINTED" => StrykeValue::integer(0),
15478            "^UNICODE" => StrykeValue::integer(if self.utf8_pragma { 1 } else { 0 }),
15479            "^OPEN" => StrykeValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
15480            "^UTF8LOCALE" => StrykeValue::integer(0),
15481            "^UTF8CACHE" => StrykeValue::integer(-1),
15482            _ if name.starts_with('^') && name.len() > 1 => self
15483                .special_caret_scalars
15484                .get(name)
15485                .cloned()
15486                .unwrap_or(StrykeValue::UNDEF),
15487            _ if name.starts_with('#') && name.len() > 1 => {
15488                let arr = &name[1..];
15489                let aname = self.stash_array_name_for_package(arr);
15490                let len = self.scope.array_len(&aname);
15491                StrykeValue::integer(len as i64 - 1)
15492            }
15493            _ => self.scope.get_scalar(name),
15494        }
15495    }
15496
15497    pub(crate) fn set_special_var(
15498        &mut self,
15499        name: &str,
15500        val: &StrykeValue,
15501    ) -> Result<(), StrykeError> {
15502        let name = self.english_scalar_name(name);
15503        match name {
15504            "!" => {
15505                let code = val.to_int() as i32;
15506                self.errno_code = code;
15507                self.errno = if code == 0 {
15508                    String::new()
15509                } else {
15510                    std::io::Error::from_raw_os_error(code).to_string()
15511                };
15512            }
15513            "@" => {
15514                if let Some((code, msg)) = val.errno_dual_parts() {
15515                    self.eval_error_code = code;
15516                    self.eval_error = msg;
15517                } else {
15518                    self.eval_error = val.to_string();
15519                    let mut code = val.to_int() as i32;
15520                    if code == 0 && !self.eval_error.is_empty() {
15521                        code = 1;
15522                    }
15523                    self.eval_error_code = code;
15524                }
15525            }
15526            "." => {
15527                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
15528                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
15529                let n = val.to_int();
15530                if self.last_readline_handle.is_empty() {
15531                    self.line_number = n;
15532                } else {
15533                    self.handle_line_numbers
15534                        .insert(self.last_readline_handle.clone(), n);
15535                }
15536            }
15537            "0" => self.program_name = val.to_string(),
15538            "/" => {
15539                self.irs = if val.is_undef() {
15540                    None
15541                } else {
15542                    Some(val.to_string())
15543                }
15544            }
15545            "\\" => self.ors = val.to_string(),
15546            "," => self.ofs = val.to_string(),
15547            ";" => self.subscript_sep = val.to_string(),
15548            "\"" => self.list_separator = val.to_string(),
15549            "%" => self.format_page_number = val.to_int(),
15550            "=" => self.format_lines_per_page = val.to_int(),
15551            "-" => self.format_lines_left = val.to_int(),
15552            ":" => self.format_line_break_chars = val.to_string(),
15553            "*" => self.multiline_match = val.to_int() != 0,
15554            "^" => self.format_top_name = val.to_string(),
15555            "INC" => self.inc_hook_index = val.to_int(),
15556            "^A" => self.accumulator_format = val.to_string(),
15557            "^F" => self.max_system_fd = val.to_int(),
15558            "^L" => self.formfeed_string = val.to_string(),
15559            "^M" => self.emergency_memory = val.to_string(),
15560            "^I" => self.inplace_edit = val.to_string(),
15561            "^D" => self.debug_flags = val.to_int(),
15562            "^P" => self.perl_debug_flags = val.to_int(),
15563            "^W" => self.warnings = val.to_int() != 0,
15564            "^H" => self.compile_hints = val.to_int(),
15565            "^WARNING_BITS" => self.warning_bits = val.to_int(),
15566            "|" => {
15567                self.output_autoflush = val.to_int() != 0;
15568                if self.output_autoflush {
15569                    let _ = io::stdout().flush();
15570                }
15571            }
15572            // Read-only or pid-backed
15573            "$$"
15574            | "]"
15575            | "^S"
15576            | "ARGV"
15577            | "?"
15578            | "^O"
15579            | "^T"
15580            | "^V"
15581            | "^E"
15582            | "^GLOBAL_PHASE"
15583            | "^MATCH"
15584            | "^PREMATCH"
15585            | "^POSTMATCH"
15586            | "^LAST_SUBMATCH_RESULT"
15587            | "^C"
15588            | "^N"
15589            | "^X"
15590            | "^TAINT"
15591            | "^TAINTED"
15592            | "^UNICODE"
15593            | "^UTF8LOCALE"
15594            | "^UTF8CACHE"
15595            | "+"
15596            | "<"
15597            | ">"
15598            | "("
15599            | ")" => {}
15600            _ if name.starts_with('^') && name.len() > 1 => {
15601                self.special_caret_scalars
15602                    .insert(name.to_string(), val.clone());
15603            }
15604            _ if name.starts_with('#') && name.len() > 1 => {
15605                // `$#name = N` resizes `@name` to length `N + 1`. Truncates
15606                // when N < current_last_idx, extends with `undef` otherwise.
15607                let arr = &name[1..];
15608                let aname = self.stash_array_name_for_package(arr);
15609                let new_last = val.to_int();
15610                let new_len = if new_last < 0 {
15611                    0
15612                } else {
15613                    (new_last as usize) + 1
15614                };
15615                let mut current = self.scope.get_array(&aname);
15616                current.resize(new_len, StrykeValue::UNDEF);
15617                self.scope.set_array(&aname, current)?;
15618            }
15619            _ => self.scope.set_scalar(name, val.clone())?,
15620        }
15621        Ok(())
15622    }
15623
15624    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
15625        match &expr.kind {
15626            ExprKind::ArrayVar(name) => Ok(name.clone()),
15627            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
15628            _ => Err(StrykeError::runtime("Expected array", expr.line).into()),
15629        }
15630    }
15631
15632    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
15633    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
15634        match &expr.kind {
15635            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
15636            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
15637            _ => expr,
15638        }
15639    }
15640
15641    /// `@$aref` / `@{...}` after optional peeling — for `SpliceExpr` / `pop` operations.
15642    fn try_eval_array_deref_container(
15643        &mut self,
15644        expr: &Expr,
15645    ) -> Result<Option<StrykeValue>, FlowOrError> {
15646        let e = Self::peel_array_builtin_operand(expr);
15647        if let ExprKind::Deref {
15648            expr: inner,
15649            kind: Sigil::Array,
15650        } = &e.kind
15651        {
15652            return Ok(Some(self.eval_or_autoviv_array_ref(inner)?));
15653        }
15654        Ok(None)
15655    }
15656
15657    /// Evaluate `inner` and return an array ref, auto-vivifying when the result is undef
15658    /// and `inner` denotes a writable lvalue (scalar var, hash element, array element).
15659    /// Mirrors Perl 5: `push @{$h{k}}, $x` creates `$h{k}` as an arrayref on demand.
15660    fn eval_or_autoviv_array_ref(&mut self, inner: &Expr) -> Result<StrykeValue, FlowOrError> {
15661        let line = inner.line;
15662        let val = self.eval_expr(inner)?;
15663        if !val.is_undef() {
15664            return Ok(val);
15665        }
15666        let new_ref = StrykeValue::array_ref(Arc::new(RwLock::new(Vec::new())));
15667        match &inner.kind {
15668            ExprKind::ScalarVar(name) => {
15669                self.scope
15670                    .set_scalar(name, new_ref.clone())
15671                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15672                Ok(new_ref)
15673            }
15674            ExprKind::HashElement { hash, key } => {
15675                let k = self.eval_expr(key)?.to_string();
15676                self.scope
15677                    .set_hash_element(hash, &k, new_ref.clone())
15678                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15679                Ok(new_ref)
15680            }
15681            ExprKind::ArrayElement { array, index } => {
15682                let i = self.eval_expr(index)?.to_int();
15683                self.scope
15684                    .set_array_element(array, i, new_ref.clone())
15685                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15686                Ok(new_ref)
15687            }
15688            _ => Ok(val),
15689        }
15690    }
15691
15692    /// Current package (`main` when `__PACKAGE__` is unset or empty).
15693    fn current_package(&self) -> String {
15694        let s = self.scope.get_scalar("__PACKAGE__").to_string();
15695        if s.is_empty() {
15696            "main".to_string()
15697        } else {
15698            s
15699        }
15700    }
15701
15702    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
15703    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
15704    pub(crate) fn package_version_scalar(
15705        &mut self,
15706        package: &str,
15707    ) -> StrykeResult<Option<StrykeValue>> {
15708        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
15709        let _ = self
15710            .scope
15711            .set_scalar("__PACKAGE__", StrykeValue::string(package.to_string()));
15712        let ver = self.get_special_var("VERSION");
15713        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
15714        Ok(if ver.is_undef() { None } else { Some(ver) })
15715    }
15716
15717    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
15718    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<StrykeSub>> {
15719        let root = if start_package.is_empty() {
15720            "main"
15721        } else {
15722            start_package
15723        };
15724        for pkg in self.mro_linearize(root) {
15725            let key = if pkg == "main" {
15726                "AUTOLOAD".to_string()
15727            } else {
15728                format!("{}::AUTOLOAD", pkg)
15729            };
15730            if let Some(s) = self.subs.get(&key) {
15731                return Some(s.clone());
15732            }
15733        }
15734        None
15735    }
15736
15737    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
15738    /// qualified missing sub or method name and invoke the handler (same argument list as the
15739    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
15740    /// the package prefix of the missing name (or current package).
15741    pub(crate) fn try_autoload_call(
15742        &mut self,
15743        missing_name: &str,
15744        args: Vec<StrykeValue>,
15745        line: usize,
15746        want: WantarrayCtx,
15747        method_invocant_class: Option<&str>,
15748    ) -> Option<ExecResult> {
15749        let pkg = self.current_package();
15750        let full = if missing_name.contains("::") {
15751            missing_name.to_string()
15752        } else {
15753            format!("{}::{}", pkg, missing_name)
15754        };
15755        let start_pkg = method_invocant_class.unwrap_or_else(|| {
15756            full.rsplit_once("::")
15757                .map(|(p, _)| p)
15758                .filter(|p| !p.is_empty())
15759                .unwrap_or("main")
15760        });
15761        let sub = self.resolve_autoload_sub(start_pkg)?;
15762        if let Err(e) = self
15763            .scope
15764            .set_scalar("AUTOLOAD", StrykeValue::string(full.clone()))
15765        {
15766            return Some(Err(e.into()));
15767        }
15768        Some(self.call_sub(&sub, args, want, line))
15769    }
15770
15771    pub(crate) fn with_topic_default_args(&self, args: Vec<StrykeValue>) -> Vec<StrykeValue> {
15772        if args.is_empty() {
15773            vec![self.scope.get_scalar("_").clone()]
15774        } else {
15775            args
15776        }
15777    }
15778
15779    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
15780    /// and [`crate::bytecode::Op::IndirectCall`].
15781    pub(crate) fn dispatch_indirect_call(
15782        &mut self,
15783        target: StrykeValue,
15784        arg_vals: Vec<StrykeValue>,
15785        want: WantarrayCtx,
15786        line: usize,
15787    ) -> ExecResult {
15788        if let Some(sub) = target.as_code_ref() {
15789            return self.call_sub(&sub, arg_vals, want, line);
15790        }
15791        if let Some(name) = target.as_str() {
15792            return self.call_named_sub(&name, arg_vals, line, want);
15793        }
15794        Err(StrykeError::runtime("Can't use non-code reference as a subroutine", line).into())
15795    }
15796
15797    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
15798    /// Bare-name dispatch for stryke list builtins (`sum`, `min`, `uniq`, `reduce`, `zip`, …).
15799    /// Resolves short aliases (`uq`, `shuf`, `chk`, `win`, `fst`, `rd`, `med`, `std`, `var`, …)
15800    /// and forwards to [`crate::list_builtins::dispatch_by_name`].
15801    pub(crate) fn call_bare_list_builtin(
15802        &mut self,
15803        name: &str,
15804        args: Vec<StrykeValue>,
15805        line: usize,
15806        want: WantarrayCtx,
15807    ) -> ExecResult {
15808        let canonical = match name {
15809            "distinct" | "uq" => "uniq",
15810            "shuf" => "shuffle",
15811            "chk" => "chunked",
15812            "win" => "windowed",
15813            "zp" => "zip",
15814            "fst" => "first",
15815            "rd" => "reduce",
15816            "med" => "median",
15817            "std" => "stddev",
15818            "var" => "variance",
15819            other => other,
15820        };
15821        // List builtins like `sum`, `min`, `uniq` operate on a list — an empty
15822        // input must aggregate to the identity (0/undef), NOT default to $_.
15823        // `sum(@empty_after_grep)` was returning $_ before this; that produced
15824        // surprising results downstream (e.g. `… |> grep {0} |> sum` = topic).
15825        match crate::list_builtins::dispatch_by_name(self, canonical, &args, want) {
15826            Some(r) => r,
15827            None => Err(StrykeError::runtime(
15828                format!("internal: not a stryke list builtin: {name}"),
15829                line,
15830            )
15831            .into()),
15832        }
15833    }
15834
15835    fn call_named_sub(
15836        &mut self,
15837        name: &str,
15838        args: Vec<StrykeValue>,
15839        line: usize,
15840        want: WantarrayCtx,
15841    ) -> ExecResult {
15842        if let Some(sub) = self.resolve_sub_by_name(name) {
15843            let args = self.with_topic_default_args(args);
15844            // The sub's home package is the qualifier from the resolved registry key.
15845            // `StrykeSub.name` itself may be bare; pass an explicit override so call_sub can
15846            // switch `__PACKAGE__` for cross-package `our`/`oursync` qualification.
15847            let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
15848            return self.call_sub_with_package(&sub, args, want, line, pkg);
15849        }
15850        match name {
15851            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
15852            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
15853            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
15854            | "none" | "notall" | "first" | "fst" | "reduce" | "rd" | "reductions" | "sum"
15855            | "sum0" | "product" | "min" | "max" | "minstr" | "maxstr" | "mean" | "median"
15856            | "med" | "mode" | "stddev" | "std" | "variance" | "var" | "pairs" | "unpairs"
15857            | "pairkeys" | "pairvalues" | "pairgrep" | "pairmap" | "pairfirst" | "blessed"
15858            | "refaddr" | "reftype" | "weaken" | "unweaken" | "isweak" | "set_subname"
15859            | "subname" | "unicode_to_native" => {
15860                self.call_bare_list_builtin(name, args, line, want)
15861            }
15862            "deque" => {
15863                if !args.is_empty() {
15864                    return Err(StrykeError::runtime("deque() takes no arguments", line).into());
15865                }
15866                Ok(StrykeValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
15867            }
15868            "defer__internal" => {
15869                if args.len() != 1 {
15870                    return Err(StrykeError::runtime(
15871                        "defer__internal expects one coderef argument",
15872                        line,
15873                    )
15874                    .into());
15875                }
15876                self.scope.push_defer(args[0].clone());
15877                Ok(StrykeValue::UNDEF)
15878            }
15879            "heap" => {
15880                if args.len() != 1 {
15881                    return Err(
15882                        StrykeError::runtime("heap() expects one comparator sub", line).into(),
15883                    );
15884                }
15885                if let Some(sub) = args[0].as_code_ref() {
15886                    Ok(StrykeValue::heap(Arc::new(Mutex::new(PerlHeap {
15887                        items: Vec::new(),
15888                        cmp: Arc::clone(&sub),
15889                    }))))
15890                } else {
15891                    Err(StrykeError::runtime("heap() requires a code reference", line).into())
15892                }
15893            }
15894            "pipeline" => {
15895                let mut items = Vec::new();
15896                for v in args {
15897                    if let Some(a) = v.as_array_vec() {
15898                        items.extend(a);
15899                    } else {
15900                        items.push(v);
15901                    }
15902                }
15903                Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15904                    source: items,
15905                    ops: Vec::new(),
15906                    has_scalar_terminal: false,
15907                    par_stream: false,
15908                    streaming: false,
15909                    streaming_workers: 0,
15910                    streaming_buffer: 256,
15911                }))))
15912            }
15913            "par_pipeline" => {
15914                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
15915                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
15916                        .map_err(Into::into);
15917                }
15918                Ok(self.builtin_par_pipeline_stream(&args, line)?)
15919            }
15920            "par_pipeline_stream" => {
15921                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
15922                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
15923                        .map_err(Into::into);
15924                }
15925                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
15926            }
15927            "ppool" => {
15928                if args.len() != 1 {
15929                    return Err(StrykeError::runtime(
15930                        "ppool() expects one argument (worker count)",
15931                        line,
15932                    )
15933                    .into());
15934                }
15935                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
15936            }
15937            "barrier" => {
15938                if args.len() != 1 {
15939                    return Err(StrykeError::runtime(
15940                        "barrier() expects one argument (party count)",
15941                        line,
15942                    )
15943                    .into());
15944                }
15945                let n = args[0].to_int().max(1) as usize;
15946                Ok(StrykeValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
15947            }
15948            "cluster" => {
15949                let items = if args.len() == 1 {
15950                    args[0].to_list()
15951                } else {
15952                    args.to_vec()
15953                };
15954                let c = RemoteCluster::from_list_args(&items)
15955                    .map_err(|msg| StrykeError::runtime(msg, line))?;
15956                Ok(StrykeValue::remote_cluster(Arc::new(c)))
15957            }
15958            _ => {
15959                // Late static binding: static::method() resolves to runtime class of $self
15960                if let Some(method_name) = name.strip_prefix("static::") {
15961                    let self_val = self.scope.get_scalar("self");
15962                    if let Some(c) = self_val.as_class_inst() {
15963                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
15964                            if let Some(ref body) = m.body {
15965                                let params = m.params.clone();
15966                                let mut call_args = vec![self_val.clone()];
15967                                call_args.extend(args);
15968                                return match self.call_class_method(body, &params, call_args, line)
15969                                {
15970                                    Ok(v) => Ok(v),
15971                                    Err(FlowOrError::Error(e)) => Err(e.into()),
15972                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15973                                    Err(e) => Err(e),
15974                                };
15975                            }
15976                        }
15977                        return Err(StrykeError::runtime(
15978                            format!(
15979                                "static::{} — method not found on class {}",
15980                                method_name, c.def.name
15981                            ),
15982                            line,
15983                        )
15984                        .into());
15985                    }
15986                    return Err(StrykeError::runtime(
15987                        "static:: can only be used inside a class method",
15988                        line,
15989                    )
15990                    .into());
15991                }
15992                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
15993                if let Some(def) = self.struct_defs.get(name).cloned() {
15994                    return self.struct_construct(&def, args, line);
15995                }
15996                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
15997                if let Some(def) = self.class_defs.get(name).cloned() {
15998                    return self.class_construct(&def, args, line);
15999                }
16000                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
16001                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
16002                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
16003                        return self.enum_construct(&def, variant_name, args, line);
16004                    }
16005                }
16006                // Check for static class method or static field: Math::add(...) / Counter::count()
16007                if let Some((class_name, member_name)) = name.rsplit_once("::") {
16008                    if let Some(def) = self.class_defs.get(class_name).cloned() {
16009                        // Static method
16010                        if let Some(m) = def.method(member_name) {
16011                            if m.is_static {
16012                                if let Some(ref body) = m.body {
16013                                    let params = m.params.clone();
16014                                    return match self.call_static_class_method(
16015                                        body,
16016                                        &params,
16017                                        args.clone(),
16018                                        line,
16019                                    ) {
16020                                        Ok(v) => Ok(v),
16021                                        Err(FlowOrError::Error(e)) => Err(e.into()),
16022                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16023                                        Err(e) => Err(e),
16024                                    };
16025                                }
16026                            }
16027                        }
16028                        // Static field access: getter (0 args) or setter (1 arg)
16029                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
16030                            let key = format!("{}::{}", class_name, member_name);
16031                            match args.len() {
16032                                0 => {
16033                                    let val = self.scope.get_scalar(&key);
16034                                    return Ok(val);
16035                                }
16036                                1 => {
16037                                    let _ = self.scope.set_scalar(&key, args[0].clone());
16038                                    return Ok(args[0].clone());
16039                                }
16040                                _ => {
16041                                    return Err(StrykeError::runtime(
16042                                        format!(
16043                                            "static field `{}::{}` takes 0 or 1 arguments",
16044                                            class_name, member_name
16045                                        ),
16046                                        line,
16047                                    )
16048                                    .into());
16049                                }
16050                            }
16051                        }
16052                    }
16053                }
16054                let args = self.with_topic_default_args(args);
16055                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
16056                    return r;
16057                }
16058                Err(StrykeError::runtime(self.undefined_subroutine_call_message(name), line).into())
16059            }
16060        }
16061    }
16062
16063    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
16064    pub(crate) fn struct_construct(
16065        &mut self,
16066        def: &Arc<StructDef>,
16067        args: Vec<StrykeValue>,
16068        line: usize,
16069    ) -> ExecResult {
16070        // Detect if args are named (key => value pairs) or positional
16071        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
16072        let is_named = args.len() >= 2
16073            && args.len().is_multiple_of(2)
16074            && args.iter().step_by(2).all(|v| {
16075                let s = v.to_string();
16076                def.field_index(&s).is_some()
16077            });
16078
16079        let provided = if is_named {
16080            // Named construction: Point(x => 1, y => 2)
16081            let mut pairs = Vec::new();
16082            let mut i = 0;
16083            while i + 1 < args.len() {
16084                let k = args[i].to_string();
16085                let v = args[i + 1].clone();
16086                pairs.push((k, v));
16087                i += 2;
16088            }
16089            pairs
16090        } else {
16091            // Positional construction: Point(1, 2) fills fields in declaration order
16092            def.fields
16093                .iter()
16094                .zip(args.iter())
16095                .map(|(f, v)| (f.name.clone(), v.clone()))
16096                .collect()
16097        };
16098
16099        // Evaluate default expressions
16100        let mut defaults = Vec::with_capacity(def.fields.len());
16101        for field in &def.fields {
16102            if let Some(ref expr) = field.default {
16103                let val = self.eval_expr(expr)?;
16104                defaults.push(Some(val));
16105            } else {
16106                defaults.push(None);
16107            }
16108        }
16109
16110        Ok(crate::native_data::struct_new_with_defaults(
16111            def, &provided, &defaults, line,
16112        )?)
16113    }
16114
16115    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
16116    pub(crate) fn class_construct(
16117        &mut self,
16118        def: &Arc<ClassDef>,
16119        args: Vec<StrykeValue>,
16120        _line: usize,
16121    ) -> ExecResult {
16122        use crate::value::ClassInstance;
16123
16124        // Prevent instantiation of abstract classes
16125        if def.is_abstract {
16126            return Err(StrykeError::runtime(
16127                format!("cannot instantiate abstract class `{}`", def.name),
16128                _line,
16129            )
16130            .into());
16131        }
16132
16133        // Collect all fields from inheritance chain (parent fields first)
16134        let all_fields = self.collect_class_fields(def);
16135
16136        // Check if args are named
16137        let is_named = args.len() >= 2
16138            && args.len().is_multiple_of(2)
16139            && args.iter().step_by(2).all(|v| {
16140                let s = v.to_string();
16141                all_fields.iter().any(|(name, _, _)| name == &s)
16142            });
16143
16144        let provided: Vec<(String, StrykeValue)> = if is_named {
16145            let mut pairs = Vec::new();
16146            let mut i = 0;
16147            while i + 1 < args.len() {
16148                let k = args[i].to_string();
16149                let v = args[i + 1].clone();
16150                pairs.push((k, v));
16151                i += 2;
16152            }
16153            pairs
16154        } else {
16155            all_fields
16156                .iter()
16157                .zip(args.iter())
16158                .map(|((name, _, _), v)| (name.clone(), v.clone()))
16159                .collect()
16160        };
16161
16162        // Build values array for all fields (inherited + own) with type checking
16163        let mut values = Vec::with_capacity(all_fields.len());
16164        for (name, default, ty) in &all_fields {
16165            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
16166                val.clone()
16167            } else if let Some(ref expr) = default {
16168                self.eval_expr(expr)?
16169            } else {
16170                StrykeValue::UNDEF
16171            };
16172            ty.check_value(&val).map_err(|msg| {
16173                StrykeError::type_error(
16174                    format!("class {} field `{}`: {}", def.name, name, msg),
16175                    _line,
16176                )
16177            })?;
16178            values.push(val);
16179        }
16180
16181        // Compute full ISA chain for type checking
16182        let isa_chain = self.mro_linearize(&def.name);
16183        let instance = StrykeValue::class_inst(Arc::new(ClassInstance::new_with_isa(
16184            Arc::clone(def),
16185            values,
16186            isa_chain,
16187        )));
16188
16189        // Call BUILD hooks: parent BUILD first, then child BUILD
16190        let build_chain = self.collect_build_chain(def);
16191        if !build_chain.is_empty() {
16192            for (body, params) in &build_chain {
16193                let call_args = vec![instance.clone()];
16194                match self.call_class_method(body, params, call_args, _line) {
16195                    Ok(_) => {}
16196                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
16197                    Err(e) => return Err(e),
16198                }
16199            }
16200        }
16201
16202        Ok(instance)
16203    }
16204
16205    /// Collect BUILD methods from parent to child order.
16206    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
16207        let mut chain = Vec::new();
16208        // Parent BUILD first
16209        for parent_name in &def.extends {
16210            if let Some(parent_def) = self.class_defs.get(parent_name) {
16211                chain.extend(self.collect_build_chain(parent_def));
16212            }
16213        }
16214        // Own BUILD
16215        if let Some(m) = def.method("BUILD") {
16216            if let Some(ref body) = m.body {
16217                chain.push((body.clone(), m.params.clone()));
16218            }
16219        }
16220        chain
16221    }
16222
16223    /// Recursively flatten class/struct instances and the hashes/arrays
16224    /// they contain into a plain hashref tree. Atoms (numbers, strings,
16225    /// undef, code refs, regex refs, blessed-non-hash refs, …) round-trip
16226    /// unchanged. Used by `$obj->to_hash_rec` for both class and struct
16227    /// receivers.
16228    pub(crate) fn deep_to_hash_value(&self, v: &StrykeValue) -> StrykeValue {
16229        // Class instance: hashref of fields, recursing into each value.
16230        if let Some(c) = v.as_class_inst() {
16231            let all_fields = self.collect_class_fields_full(&c.def);
16232            let values = c.get_values();
16233            let mut map = IndexMap::new();
16234            for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
16235                if let Some(elem) = values.get(i) {
16236                    map.insert(name.clone(), self.deep_to_hash_value(elem));
16237                }
16238            }
16239            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16240        }
16241        // Struct instance: same shape, declaration order.
16242        if let Some(s) = v.as_struct_inst() {
16243            let values = s.get_values();
16244            let mut map = IndexMap::new();
16245            for (i, field) in s.def.fields.iter().enumerate() {
16246                if let Some(elem) = values.get(i) {
16247                    map.insert(field.name.clone(), self.deep_to_hash_value(elem));
16248                }
16249            }
16250            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16251        }
16252        // Hashref: clone keys, recurse into values.
16253        if let Some(r) = v.as_hash_ref() {
16254            let inner = r.read().clone();
16255            let mut map = IndexMap::new();
16256            for (k, val) in inner.into_iter() {
16257                map.insert(k, self.deep_to_hash_value(&val));
16258            }
16259            return StrykeValue::hash_ref(Arc::new(RwLock::new(map)));
16260        }
16261        // Arrayref: recurse into elements.
16262        if let Some(r) = v.as_array_ref() {
16263            let inner = r.read().clone();
16264            let out: Vec<StrykeValue> = inner.iter().map(|e| self.deep_to_hash_value(e)).collect();
16265            return StrykeValue::array_ref(Arc::new(RwLock::new(out)));
16266        }
16267        // Everything else (scalars, blessed refs, code refs, enums, …)
16268        // round-trips unchanged. Enum instances stringify naturally
16269        // through their existing `Display` so callers see a stable name.
16270        v.clone()
16271    }
16272
16273    /// Collect all fields from a class and its parent hierarchy (parent fields first).
16274    /// Returns (name, default, type, visibility, owning_class_name).
16275    fn collect_class_fields(
16276        &self,
16277        def: &ClassDef,
16278    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
16279        self.collect_class_fields_full(def)
16280            .into_iter()
16281            .map(|(name, default, ty, _, _)| (name, default, ty))
16282            .collect()
16283    }
16284
16285    /// Like collect_class_fields but includes visibility and owning class name.
16286    fn collect_class_fields_full(
16287        &self,
16288        def: &ClassDef,
16289    ) -> Vec<(
16290        String,
16291        Option<Expr>,
16292        crate::ast::PerlTypeName,
16293        crate::ast::Visibility,
16294        String,
16295    )> {
16296        let mut all_fields = Vec::new();
16297
16298        for parent_name in &def.extends {
16299            if let Some(parent_def) = self.class_defs.get(parent_name) {
16300                let parent_fields = self.collect_class_fields_full(parent_def);
16301                all_fields.extend(parent_fields);
16302            }
16303        }
16304
16305        for field in &def.fields {
16306            all_fields.push((
16307                field.name.clone(),
16308                field.default.clone(),
16309                field.ty.clone(),
16310                field.visibility,
16311                def.name.clone(),
16312            ));
16313        }
16314
16315        all_fields
16316    }
16317
16318    /// Collect all method names from class and parents (deduplicates, child overrides parent).
16319    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
16320        // Parent methods first
16321        for parent_name in &def.extends {
16322            if let Some(parent_def) = self.class_defs.get(parent_name) {
16323                self.collect_class_method_names(parent_def, names);
16324            }
16325        }
16326        // Own methods (add if not already present — child overrides parent name)
16327        for m in &def.methods {
16328            if !m.is_static && !names.contains(&m.name) {
16329                names.push(m.name.clone());
16330            }
16331        }
16332    }
16333
16334    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
16335    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
16336        let mut chain = Vec::new();
16337        // Own DESTROY first
16338        if let Some(m) = def.method("DESTROY") {
16339            if let Some(ref body) = m.body {
16340                chain.push((body.clone(), m.params.clone()));
16341            }
16342        }
16343        // Then parent DESTROY
16344        for parent_name in &def.extends {
16345            if let Some(parent_def) = self.class_defs.get(parent_name) {
16346                chain.extend(self.collect_destroy_chain(parent_def));
16347            }
16348        }
16349        chain
16350    }
16351
16352    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
16353    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
16354        if let Some(def) = self.class_defs.get(child) {
16355            for parent in &def.extends {
16356                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
16357                    return true;
16358                }
16359            }
16360        }
16361        false
16362    }
16363
16364    /// Find a method in a class or its parent hierarchy (child methods override parent).
16365    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
16366        // First check the current class
16367        if let Some(m) = def.method(method) {
16368            return Some((m.clone(), def.name.clone()));
16369        }
16370        // Then check parent classes
16371        for parent_name in &def.extends {
16372            if let Some(parent_def) = self.class_defs.get(parent_name) {
16373                if let Some(result) = self.find_class_method(parent_def, method) {
16374                    return Some(result);
16375                }
16376            }
16377        }
16378        None
16379    }
16380
16381    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
16382    pub(crate) fn enum_construct(
16383        &mut self,
16384        def: &Arc<EnumDef>,
16385        variant_name: &str,
16386        args: Vec<StrykeValue>,
16387        line: usize,
16388    ) -> ExecResult {
16389        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
16390            FlowOrError::Error(StrykeError::runtime(
16391                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
16392                line,
16393            ))
16394        })?;
16395        let variant = &def.variants[variant_idx];
16396        let data = if variant.ty.is_some() {
16397            if args.is_empty() {
16398                return Err(StrykeError::runtime(
16399                    format!(
16400                        "enum variant `{}::{}` requires data",
16401                        def.name, variant_name
16402                    ),
16403                    line,
16404                )
16405                .into());
16406            }
16407            if args.len() == 1 {
16408                args.into_iter().next().unwrap()
16409            } else {
16410                StrykeValue::array(args)
16411            }
16412        } else {
16413            if !args.is_empty() {
16414                return Err(StrykeError::runtime(
16415                    format!(
16416                        "enum variant `{}::{}` does not take data",
16417                        def.name, variant_name
16418                    ),
16419                    line,
16420                )
16421                .into());
16422            }
16423            StrykeValue::UNDEF
16424        };
16425        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
16426        Ok(StrykeValue::enum_inst(Arc::new(inst)))
16427    }
16428
16429    /// True if `name` is a registered or standard process-global handle.
16430    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
16431        matches!(name, "STDIN" | "STDOUT" | "STDERR")
16432            || self.input_handles.contains_key(name)
16433            || self.output_handles.contains_key(name)
16434            || self.io_file_slots.contains_key(name)
16435            || self.pipe_children.contains_key(name)
16436    }
16437
16438    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
16439    pub(crate) fn io_handle_method(
16440        &mut self,
16441        name: &str,
16442        method: &str,
16443        args: &[StrykeValue],
16444        line: usize,
16445    ) -> StrykeResult<StrykeValue> {
16446        match method {
16447            "print" => self.io_handle_print(name, args, false, line),
16448            "say" => self.io_handle_print(name, args, true, line),
16449            "printf" => self.io_handle_printf(name, args, line),
16450            "getline" | "readline" => {
16451                if !args.is_empty() {
16452                    return Err(StrykeError::runtime(
16453                        format!("{}: too many arguments", method),
16454                        line,
16455                    ));
16456                }
16457                self.readline_builtin_execute(Some(name))
16458            }
16459            "close" => {
16460                if !args.is_empty() {
16461                    return Err(StrykeError::runtime("close: too many arguments", line));
16462                }
16463                self.close_builtin_execute(name.to_string())
16464            }
16465            "eof" => {
16466                if !args.is_empty() {
16467                    return Err(StrykeError::runtime("eof: too many arguments", line));
16468                }
16469                let at_eof = !self.has_input_handle(name);
16470                Ok(StrykeValue::integer(if at_eof { 1 } else { 0 }))
16471            }
16472            "getc" => {
16473                if !args.is_empty() {
16474                    return Err(StrykeError::runtime("getc: too many arguments", line));
16475                }
16476                match crate::builtins::try_builtin(
16477                    self,
16478                    "getc",
16479                    &[StrykeValue::string(name.to_string())],
16480                    line,
16481                ) {
16482                    Some(r) => r,
16483                    None => Err(StrykeError::runtime("getc: not available", line)),
16484                }
16485            }
16486            "binmode" => match crate::builtins::try_builtin(
16487                self,
16488                "binmode",
16489                &[StrykeValue::string(name.to_string())],
16490                line,
16491            ) {
16492                Some(r) => r,
16493                None => Err(StrykeError::runtime("binmode: not available", line)),
16494            },
16495            "fileno" => match crate::builtins::try_builtin(
16496                self,
16497                "fileno",
16498                &[StrykeValue::string(name.to_string())],
16499                line,
16500            ) {
16501                Some(r) => r,
16502                None => Err(StrykeError::runtime("fileno: not available", line)),
16503            },
16504            "flush" => {
16505                if !args.is_empty() {
16506                    return Err(StrykeError::runtime("flush: too many arguments", line));
16507                }
16508                self.io_handle_flush(name, line)
16509            }
16510            _ => Err(StrykeError::runtime(
16511                format!("Unknown method for filehandle: {}", method),
16512                line,
16513            )),
16514        }
16515    }
16516
16517    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> StrykeResult<StrykeValue> {
16518        match handle_name {
16519            "STDOUT" => {
16520                let _ = IoWrite::flush(&mut io::stdout());
16521            }
16522            "STDERR" => {
16523                let _ = IoWrite::flush(&mut io::stderr());
16524            }
16525            name => {
16526                if let Some(writer) = self.output_handles.get_mut(name) {
16527                    let _ = IoWrite::flush(&mut *writer);
16528                } else {
16529                    return Err(StrykeError::runtime(
16530                        format!("flush on unopened filehandle {}", name),
16531                        line,
16532                    ));
16533                }
16534            }
16535        }
16536        Ok(StrykeValue::integer(1))
16537    }
16538
16539    fn io_handle_print(
16540        &mut self,
16541        handle_name: &str,
16542        args: &[StrykeValue],
16543        newline: bool,
16544        line: usize,
16545    ) -> StrykeResult<StrykeValue> {
16546        if newline && (self.feature_bits & FEAT_SAY) == 0 {
16547            return Err(StrykeError::runtime(
16548                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
16549                line,
16550            ));
16551        }
16552        let mut output = String::new();
16553        if args.is_empty() {
16554            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
16555            output.push_str(&self.scope.get_scalar("_").to_string());
16556        } else {
16557            for (i, val) in args.iter().enumerate() {
16558                if i > 0 && !self.ofs.is_empty() {
16559                    output.push_str(&self.ofs);
16560                }
16561                output.push_str(&val.to_string());
16562            }
16563        }
16564        if newline {
16565            output.push('\n');
16566        }
16567        output.push_str(&self.ors);
16568
16569        self.write_formatted_print(handle_name, &output, line)?;
16570        Ok(StrykeValue::integer(1))
16571    }
16572
16573    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
16574    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
16575    pub(crate) fn write_formatted_print(
16576        &mut self,
16577        handle_name: &str,
16578        output: &str,
16579        line: usize,
16580    ) -> StrykeResult<()> {
16581        match handle_name {
16582            "STDOUT" => {
16583                if !self.suppress_stdout {
16584                    print!("{}", output);
16585                    if self.output_autoflush {
16586                        let _ = io::stdout().flush();
16587                    }
16588                }
16589            }
16590            "STDERR" => {
16591                eprint!("{}", output);
16592                let _ = io::stderr().flush();
16593            }
16594            name => {
16595                if let Some(writer) = self.output_handles.get_mut(name) {
16596                    let _ = writer.write_all(output.as_bytes());
16597                    if self.output_autoflush {
16598                        let _ = writer.flush();
16599                    }
16600                } else {
16601                    return Err(StrykeError::runtime(
16602                        format!("print on unopened filehandle {}", name),
16603                        line,
16604                    ));
16605                }
16606            }
16607        }
16608        Ok(())
16609    }
16610
16611    fn io_handle_printf(
16612        &mut self,
16613        handle_name: &str,
16614        args: &[StrykeValue],
16615        line: usize,
16616    ) -> StrykeResult<StrykeValue> {
16617        let (fmt, rest): (String, &[StrykeValue]) = if args.is_empty() {
16618            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
16619                Ok(s) => s,
16620                Err(FlowOrError::Error(e)) => return Err(e),
16621                Err(FlowOrError::Flow(_)) => {
16622                    return Err(StrykeError::runtime(
16623                        "printf: unexpected control flow in sprintf",
16624                        line,
16625                    ));
16626                }
16627            };
16628            (s, &[])
16629        } else {
16630            (args[0].to_string(), &args[1..])
16631        };
16632        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
16633            Ok(s) => s,
16634            Err(FlowOrError::Error(e)) => return Err(e),
16635            Err(FlowOrError::Flow(_)) => {
16636                return Err(StrykeError::runtime(
16637                    "printf: unexpected control flow in sprintf",
16638                    line,
16639                ));
16640            }
16641        };
16642        match handle_name {
16643            "STDOUT" => {
16644                if !self.suppress_stdout {
16645                    print!("{}", output);
16646                    if self.output_autoflush {
16647                        let _ = IoWrite::flush(&mut io::stdout());
16648                    }
16649                }
16650            }
16651            "STDERR" => {
16652                eprint!("{}", output);
16653                let _ = IoWrite::flush(&mut io::stderr());
16654            }
16655            name => {
16656                if let Some(writer) = self.output_handles.get_mut(name) {
16657                    let _ = writer.write_all(output.as_bytes());
16658                    if self.output_autoflush {
16659                        let _ = writer.flush();
16660                    }
16661                } else {
16662                    return Err(StrykeError::runtime(
16663                        format!("printf on unopened filehandle {}", name),
16664                        line,
16665                    ));
16666                }
16667            }
16668        }
16669        Ok(StrykeValue::integer(1))
16670    }
16671
16672    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
16673    pub(crate) fn try_native_method(
16674        &mut self,
16675        receiver: &StrykeValue,
16676        method: &str,
16677        args: &[StrykeValue],
16678        line: usize,
16679    ) -> Option<StrykeResult<StrykeValue>> {
16680        if let Some(name) = receiver.as_io_handle_name() {
16681            return Some(self.io_handle_method(&name, method, args, line));
16682        }
16683        if let Some(ref s) = receiver.as_str() {
16684            if self.is_bound_handle(s) {
16685                return Some(self.io_handle_method(s, method, args, line));
16686            }
16687        }
16688        if let Some(c) = receiver.as_sqlite_conn() {
16689            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
16690        }
16691        if let Some(s) = receiver.as_struct_inst() {
16692            // Field access: $p->x or $p->x(value)
16693            if let Some(idx) = s.def.field_index(method) {
16694                match args.len() {
16695                    0 => {
16696                        return Some(Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF)));
16697                    }
16698                    1 => {
16699                        let field = &s.def.fields[idx];
16700                        let new_val = args[0].clone();
16701                        if let Err(msg) = field.ty.check_value(&new_val) {
16702                            return Some(Err(StrykeError::type_error(
16703                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
16704                                line,
16705                            )));
16706                        }
16707                        s.set_field(idx, new_val.clone());
16708                        return Some(Ok(new_val));
16709                    }
16710                    _ => {
16711                        return Some(Err(StrykeError::runtime(
16712                            format!(
16713                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
16714                                method,
16715                                args.len()
16716                            ),
16717                            line,
16718                        )));
16719                    }
16720                }
16721            }
16722            // Built-in struct methods
16723            match method {
16724                "with" => {
16725                    // Functional update: $p->with(x => 5) returns new instance with changed field
16726                    let mut new_values = s.get_values();
16727                    let mut i = 0;
16728                    while i + 1 < args.len() {
16729                        let k = args[i].to_string();
16730                        let v = args[i + 1].clone();
16731                        if let Some(idx) = s.def.field_index(&k) {
16732                            let field = &s.def.fields[idx];
16733                            if let Err(msg) = field.ty.check_value(&v) {
16734                                return Some(Err(StrykeError::type_error(
16735                                    format!(
16736                                        "struct {} field `{}`: {}",
16737                                        s.def.name, field.name, msg
16738                                    ),
16739                                    line,
16740                                )));
16741                            }
16742                            new_values[idx] = v;
16743                        } else {
16744                            return Some(Err(StrykeError::runtime(
16745                                format!("struct {}: unknown field `{}`", s.def.name, k),
16746                                line,
16747                            )));
16748                        }
16749                        i += 2;
16750                    }
16751                    return Some(Ok(StrykeValue::struct_inst(Arc::new(
16752                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
16753                    ))));
16754                }
16755                "to_hash" => {
16756                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
16757                    if !args.is_empty() {
16758                        return Some(Err(StrykeError::runtime(
16759                            "struct to_hash takes no arguments",
16760                            line,
16761                        )));
16762                    }
16763                    let mut map = IndexMap::new();
16764                    let values = s.get_values();
16765                    for (i, field) in s.def.fields.iter().enumerate() {
16766                        map.insert(field.name.clone(), values[i].clone());
16767                    }
16768                    return Some(Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map)))));
16769                }
16770                "to_hash_rec" | "to_hash_deep" => {
16771                    // Like to_hash but recurse: nested struct/class/hash/
16772                    // array values become plain hashref/arrayref trees.
16773                    if !args.is_empty() {
16774                        return Some(Err(StrykeError::runtime(
16775                            "struct to_hash_rec takes no arguments",
16776                            line,
16777                        )));
16778                    }
16779                    return Some(Ok(self.deep_to_hash_value(receiver)));
16780                }
16781                "fields" => {
16782                    // Field list: $p->fields returns field names
16783                    if !args.is_empty() {
16784                        return Some(Err(StrykeError::runtime(
16785                            "struct fields takes no arguments",
16786                            line,
16787                        )));
16788                    }
16789                    let names: Vec<StrykeValue> = s
16790                        .def
16791                        .fields
16792                        .iter()
16793                        .map(|f| StrykeValue::string(f.name.clone()))
16794                        .collect();
16795                    return Some(Ok(StrykeValue::array(names)));
16796                }
16797                "clone" => {
16798                    // Clone: $p->clone deep copies
16799                    if !args.is_empty() {
16800                        return Some(Err(StrykeError::runtime(
16801                            "struct clone takes no arguments",
16802                            line,
16803                        )));
16804                    }
16805                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
16806                    return Some(Ok(StrykeValue::struct_inst(Arc::new(
16807                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
16808                    ))));
16809                }
16810                _ => {}
16811            }
16812            // User-defined struct method
16813            if let Some(m) = s.def.method(method) {
16814                let body = m.body.clone();
16815                let params = m.params.clone();
16816                // Build args: $self is the receiver, then the passed args
16817                let mut call_args = vec![receiver.clone()];
16818                call_args.extend(args.iter().cloned());
16819                return Some(
16820                    match self.call_struct_method(&body, &params, call_args, line) {
16821                        Ok(v) => Ok(v),
16822                        Err(FlowOrError::Error(e)) => Err(e),
16823                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16824                        Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
16825                            "unexpected control flow in struct method",
16826                            line,
16827                        )),
16828                    },
16829                );
16830            }
16831            return None;
16832        }
16833        // Class instance method dispatch
16834        if let Some(c) = receiver.as_class_inst() {
16835            // Collect all fields from inheritance chain (with visibility)
16836            let all_fields_full = self.collect_class_fields_full(&c.def);
16837            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
16838                .iter()
16839                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
16840                .collect();
16841
16842            // Field access: $obj->name or $obj->name(value)
16843            if let Some(idx) = all_fields_full
16844                .iter()
16845                .position(|(name, _, _, _, _)| name == method)
16846            {
16847                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
16848
16849                // Enforce field visibility
16850                match vis {
16851                    crate::ast::Visibility::Private => {
16852                        // Only accessible from within the owning class's methods
16853                        let caller_class = self
16854                            .scope
16855                            .get_scalar("self")
16856                            .as_class_inst()
16857                            .map(|ci| ci.def.name.clone());
16858                        if caller_class.as_deref() != Some(owner_class.as_str()) {
16859                            return Some(Err(StrykeError::runtime(
16860                                format!("field `{}` of class {} is private", method, owner_class),
16861                                line,
16862                            )));
16863                        }
16864                    }
16865                    crate::ast::Visibility::Protected => {
16866                        // Accessible from owning class or subclasses
16867                        let caller_class = self
16868                            .scope
16869                            .get_scalar("self")
16870                            .as_class_inst()
16871                            .map(|ci| ci.def.name.clone());
16872                        let allowed = caller_class.as_deref().is_some_and(|caller| {
16873                            caller == owner_class || self.class_inherits_from(caller, owner_class)
16874                        });
16875                        if !allowed {
16876                            return Some(Err(StrykeError::runtime(
16877                                format!("field `{}` of class {} is protected", method, owner_class),
16878                                line,
16879                            )));
16880                        }
16881                    }
16882                    crate::ast::Visibility::Public => {}
16883                }
16884
16885                match args.len() {
16886                    0 => {
16887                        return Some(Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF)));
16888                    }
16889                    1 => {
16890                        let new_val = args[0].clone();
16891                        if let Err(msg) = ty.check_value(&new_val) {
16892                            return Some(Err(StrykeError::type_error(
16893                                format!("class {} field `{}`: {}", c.def.name, method, msg),
16894                                line,
16895                            )));
16896                        }
16897                        c.set_field(idx, new_val.clone());
16898                        return Some(Ok(new_val));
16899                    }
16900                    _ => {
16901                        return Some(Err(StrykeError::runtime(
16902                            format!(
16903                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
16904                                method,
16905                                args.len()
16906                            ),
16907                            line,
16908                        )));
16909                    }
16910                }
16911            }
16912            // Built-in class methods (use all_fields for inheritance)
16913            match method {
16914                "with" => {
16915                    let mut new_values = c.get_values();
16916                    let mut i = 0;
16917                    while i + 1 < args.len() {
16918                        let k = args[i].to_string();
16919                        let v = args[i + 1].clone();
16920                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
16921                            let (_, _, ref ty) = all_fields[idx];
16922                            if let Err(msg) = ty.check_value(&v) {
16923                                return Some(Err(StrykeError::type_error(
16924                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
16925                                    line,
16926                                )));
16927                            }
16928                            new_values[idx] = v;
16929                        } else {
16930                            return Some(Err(StrykeError::runtime(
16931                                format!("class {}: unknown field `{}`", c.def.name, k),
16932                                line,
16933                            )));
16934                        }
16935                        i += 2;
16936                    }
16937                    return Some(Ok(StrykeValue::class_inst(Arc::new(
16938                        crate::value::ClassInstance::new_with_isa(
16939                            Arc::clone(&c.def),
16940                            new_values,
16941                            c.isa_chain.clone(),
16942                        ),
16943                    ))));
16944                }
16945                "to_hash" => {
16946                    if !args.is_empty() {
16947                        return Some(Err(StrykeError::runtime(
16948                            "class to_hash takes no arguments",
16949                            line,
16950                        )));
16951                    }
16952                    let mut map = IndexMap::new();
16953                    let values = c.get_values();
16954                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
16955                        if let Some(v) = values.get(i) {
16956                            map.insert(name.clone(), v.clone());
16957                        }
16958                    }
16959                    return Some(Ok(StrykeValue::hash_ref(Arc::new(RwLock::new(map)))));
16960                }
16961                "to_hash_rec" | "to_hash_deep" => {
16962                    // Recursive flatten: nested class/struct/hash/array
16963                    // values become plain hashref/arrayref trees, so the
16964                    // result is JSON-serializable end-to-end without any
16965                    // surviving ClassInstance/StructInstance leaves.
16966                    if !args.is_empty() {
16967                        return Some(Err(StrykeError::runtime(
16968                            "class to_hash_rec takes no arguments",
16969                            line,
16970                        )));
16971                    }
16972                    return Some(Ok(self.deep_to_hash_value(receiver)));
16973                }
16974                "fields" => {
16975                    if !args.is_empty() {
16976                        return Some(Err(StrykeError::runtime(
16977                            "class fields takes no arguments",
16978                            line,
16979                        )));
16980                    }
16981                    let names: Vec<StrykeValue> = all_fields
16982                        .iter()
16983                        .map(|(name, _, _)| StrykeValue::string(name.clone()))
16984                        .collect();
16985                    return Some(Ok(StrykeValue::array(names)));
16986                }
16987                "clone" => {
16988                    if !args.is_empty() {
16989                        return Some(Err(StrykeError::runtime(
16990                            "class clone takes no arguments",
16991                            line,
16992                        )));
16993                    }
16994                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
16995                    return Some(Ok(StrykeValue::class_inst(Arc::new(
16996                        crate::value::ClassInstance::new_with_isa(
16997                            Arc::clone(&c.def),
16998                            new_values,
16999                            c.isa_chain.clone(),
17000                        ),
17001                    ))));
17002                }
17003                "isa" => {
17004                    if args.len() != 1 {
17005                        return Some(Err(StrykeError::runtime("isa requires one argument", line)));
17006                    }
17007                    let class_name = args[0].to_string();
17008                    let is_a = c.isa(&class_name);
17009                    return Some(Ok(if is_a {
17010                        StrykeValue::integer(1)
17011                    } else {
17012                        StrykeValue::string(String::new())
17013                    }));
17014                }
17015                "does" => {
17016                    if args.len() != 1 {
17017                        return Some(Err(StrykeError::runtime(
17018                            "does requires one argument",
17019                            line,
17020                        )));
17021                    }
17022                    let trait_name = args[0].to_string();
17023                    let implements = c.def.implements.contains(&trait_name);
17024                    return Some(Ok(if implements {
17025                        StrykeValue::integer(1)
17026                    } else {
17027                        StrykeValue::string(String::new())
17028                    }));
17029                }
17030                "methods" => {
17031                    if !args.is_empty() {
17032                        return Some(Err(StrykeError::runtime(
17033                            "methods takes no arguments",
17034                            line,
17035                        )));
17036                    }
17037                    let mut names = Vec::new();
17038                    self.collect_class_method_names(&c.def, &mut names);
17039                    let values: Vec<StrykeValue> =
17040                        names.into_iter().map(StrykeValue::string).collect();
17041                    return Some(Ok(StrykeValue::array(values)));
17042                }
17043                "superclass" => {
17044                    if !args.is_empty() {
17045                        return Some(Err(StrykeError::runtime(
17046                            "superclass takes no arguments",
17047                            line,
17048                        )));
17049                    }
17050                    let parents: Vec<StrykeValue> = c
17051                        .def
17052                        .extends
17053                        .iter()
17054                        .map(|s| StrykeValue::string(s.clone()))
17055                        .collect();
17056                    return Some(Ok(StrykeValue::array(parents)));
17057                }
17058                "destroy" => {
17059                    // Explicit destructor call — runs DESTROY chain child-first
17060                    let destroy_chain = self.collect_destroy_chain(&c.def);
17061                    for (body, params) in &destroy_chain {
17062                        let call_args = vec![receiver.clone()];
17063                        match self.call_class_method(body, params, call_args, line) {
17064                            Ok(_) => {}
17065                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
17066                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
17067                            Err(_) => {}
17068                        }
17069                    }
17070                    return Some(Ok(StrykeValue::UNDEF));
17071                }
17072                _ => {}
17073            }
17074            // User-defined class method (search inheritance chain)
17075            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
17076                // Check visibility
17077                match m.visibility {
17078                    crate::ast::Visibility::Private => {
17079                        let caller_class = self
17080                            .scope
17081                            .get_scalar("self")
17082                            .as_class_inst()
17083                            .map(|ci| ci.def.name.clone());
17084                        if caller_class.as_deref() != Some(owner_class.as_str()) {
17085                            return Some(Err(StrykeError::runtime(
17086                                format!("method `{}` of class {} is private", method, owner_class),
17087                                line,
17088                            )));
17089                        }
17090                    }
17091                    crate::ast::Visibility::Protected => {
17092                        let caller_class = self
17093                            .scope
17094                            .get_scalar("self")
17095                            .as_class_inst()
17096                            .map(|ci| ci.def.name.clone());
17097                        let allowed = caller_class.as_deref().is_some_and(|caller| {
17098                            caller == owner_class.as_str()
17099                                || self.class_inherits_from(caller, owner_class)
17100                        });
17101                        if !allowed {
17102                            return Some(Err(StrykeError::runtime(
17103                                format!(
17104                                    "method `{}` of class {} is protected",
17105                                    method, owner_class
17106                                ),
17107                                line,
17108                            )));
17109                        }
17110                    }
17111                    crate::ast::Visibility::Public => {}
17112                }
17113                if let Some(ref body) = m.body {
17114                    let params = m.params.clone();
17115                    let mut call_args = vec![receiver.clone()];
17116                    call_args.extend(args.iter().cloned());
17117                    return Some(
17118                        match self.call_class_method(body, &params, call_args, line) {
17119                            Ok(v) => Ok(v),
17120                            Err(FlowOrError::Error(e)) => Err(e),
17121                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17122                            Err(FlowOrError::Flow(_)) => Err(StrykeError::runtime(
17123                                "unexpected control flow in class method",
17124                                line,
17125                            )),
17126                        },
17127                    );
17128                }
17129            }
17130            return None;
17131        }
17132        if let Some(d) = receiver.as_dataframe() {
17133            return Some(self.dataframe_method(d, method, args, line));
17134        }
17135        if let Some(s) = crate::value::set_payload(receiver) {
17136            return Some(self.set_method(s, method, args, line));
17137        }
17138        if let Some(d) = receiver.as_deque() {
17139            return Some(self.deque_method(d, method, args, line));
17140        }
17141        if let Some(h) = receiver.as_heap_pq() {
17142            return Some(self.heap_method(h, method, args, line));
17143        }
17144        if let Some(p) = receiver.as_pipeline() {
17145            return Some(self.pipeline_method(p, method, args, line));
17146        }
17147        if let Some(c) = receiver.as_capture() {
17148            return Some(self.capture_method(c, method, args, line));
17149        }
17150        if let Some(p) = receiver.as_ppool() {
17151            return Some(self.ppool_method(p, method, args, line));
17152        }
17153        if let Some(b) = receiver.as_barrier() {
17154            return Some(self.barrier_method(b, method, args, line));
17155        }
17156        if let Some(g) = receiver.as_generator() {
17157            if method == "next" {
17158                if !args.is_empty() {
17159                    return Some(Err(StrykeError::runtime(
17160                        "generator->next takes no arguments",
17161                        line,
17162                    )));
17163                }
17164                return Some(self.generator_next(&g));
17165            }
17166            return None;
17167        }
17168        if let Some(arc) = receiver.as_atomic_arc() {
17169            let inner = arc.lock().clone();
17170            if let Some(d) = inner.as_deque() {
17171                return Some(self.deque_method(d, method, args, line));
17172            }
17173            if let Some(h) = inner.as_heap_pq() {
17174                return Some(self.heap_method(h, method, args, line));
17175            }
17176        }
17177        None
17178    }
17179
17180    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
17181    fn dataframe_method(
17182        &mut self,
17183        d: Arc<Mutex<PerlDataFrame>>,
17184        method: &str,
17185        args: &[StrykeValue],
17186        line: usize,
17187    ) -> StrykeResult<StrykeValue> {
17188        match method {
17189            "nrow" | "nrows" => {
17190                if !args.is_empty() {
17191                    return Err(StrykeError::runtime(
17192                        format!("dataframe {} takes no arguments", method),
17193                        line,
17194                    ));
17195                }
17196                Ok(StrykeValue::integer(d.lock().nrows() as i64))
17197            }
17198            "ncol" | "ncols" => {
17199                if !args.is_empty() {
17200                    return Err(StrykeError::runtime(
17201                        format!("dataframe {} takes no arguments", method),
17202                        line,
17203                    ));
17204                }
17205                Ok(StrykeValue::integer(d.lock().ncols() as i64))
17206            }
17207            "filter" => {
17208                if args.len() != 1 {
17209                    return Err(StrykeError::runtime(
17210                        "dataframe filter expects 1 argument (sub)",
17211                        line,
17212                    ));
17213                }
17214                let Some(sub) = args[0].as_code_ref() else {
17215                    return Err(StrykeError::runtime(
17216                        "dataframe filter expects a code reference",
17217                        line,
17218                    ));
17219                };
17220                let df_guard = d.lock();
17221                let n = df_guard.nrows();
17222                let mut keep = vec![false; n];
17223                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
17224                    let row = df_guard.row_hashref(r);
17225                    self.scope_push_hook();
17226                    self.scope.set_topic(row);
17227                    if let Some(ref env) = sub.closure_env {
17228                        self.scope.restore_capture(env);
17229                    }
17230                    let pass = match self.exec_block_no_scope(&sub.body) {
17231                        Ok(v) => v.is_true(),
17232                        Err(_) => false,
17233                    };
17234                    self.scope_pop_hook();
17235                    *row_keep = pass;
17236                }
17237                let columns = df_guard.columns.clone();
17238                let cols: Vec<Vec<StrykeValue>> = (0..df_guard.ncols())
17239                    .map(|i| {
17240                        let mut out = Vec::new();
17241                        for (r, pass_row) in keep.iter().enumerate().take(n) {
17242                            if *pass_row {
17243                                out.push(df_guard.cols[i][r].clone());
17244                            }
17245                        }
17246                        out
17247                    })
17248                    .collect();
17249                let group_by = df_guard.group_by.clone();
17250                drop(df_guard);
17251                let new_df = PerlDataFrame {
17252                    columns,
17253                    cols,
17254                    group_by,
17255                };
17256                Ok(StrykeValue::dataframe(Arc::new(Mutex::new(new_df))))
17257            }
17258            "group_by" => {
17259                if args.len() != 1 {
17260                    return Err(StrykeError::runtime(
17261                        "dataframe group_by expects 1 column name",
17262                        line,
17263                    ));
17264                }
17265                let key = args[0].to_string();
17266                let inner = d.lock();
17267                if inner.col_index(&key).is_none() {
17268                    return Err(StrykeError::runtime(
17269                        format!("dataframe group_by: unknown column \"{}\"", key),
17270                        line,
17271                    ));
17272                }
17273                let new_df = PerlDataFrame {
17274                    columns: inner.columns.clone(),
17275                    cols: inner.cols.clone(),
17276                    group_by: Some(key),
17277                };
17278                Ok(StrykeValue::dataframe(Arc::new(Mutex::new(new_df))))
17279            }
17280            "sum" => {
17281                if args.len() != 1 {
17282                    return Err(StrykeError::runtime(
17283                        "dataframe sum expects 1 column name",
17284                        line,
17285                    ));
17286                }
17287                let col_name = args[0].to_string();
17288                let inner = d.lock();
17289                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
17290                    StrykeError::runtime(
17291                        format!("dataframe sum: unknown column \"{}\"", col_name),
17292                        line,
17293                    )
17294                })?;
17295                match &inner.group_by {
17296                    Some(gcol) => {
17297                        let gi = inner.col_index(gcol).ok_or_else(|| {
17298                            StrykeError::runtime(
17299                                format!("dataframe sum: unknown group column \"{}\"", gcol),
17300                                line,
17301                            )
17302                        })?;
17303                        let mut acc: IndexMap<String, f64> = IndexMap::new();
17304                        for r in 0..inner.nrows() {
17305                            let k = inner.cols[gi][r].to_string();
17306                            let v = inner.cols[val_idx][r].to_number();
17307                            *acc.entry(k).or_insert(0.0) += v;
17308                        }
17309                        let keys: Vec<String> = acc.keys().cloned().collect();
17310                        let sums: Vec<f64> = acc.values().copied().collect();
17311                        let cols = vec![
17312                            keys.into_iter().map(StrykeValue::string).collect(),
17313                            sums.into_iter().map(StrykeValue::float).collect(),
17314                        ];
17315                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
17316                        let out = PerlDataFrame {
17317                            columns,
17318                            cols,
17319                            group_by: None,
17320                        };
17321                        Ok(StrykeValue::dataframe(Arc::new(Mutex::new(out))))
17322                    }
17323                    None => {
17324                        let total: f64 = (0..inner.nrows())
17325                            .map(|r| inner.cols[val_idx][r].to_number())
17326                            .sum();
17327                        Ok(StrykeValue::float(total))
17328                    }
17329                }
17330            }
17331            _ => Err(StrykeError::runtime(
17332                format!("Unknown method for dataframe: {}", method),
17333                line,
17334            )),
17335        }
17336    }
17337
17338    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
17339    fn set_method(
17340        &self,
17341        s: Arc<crate::value::PerlSet>,
17342        method: &str,
17343        args: &[StrykeValue],
17344        line: usize,
17345    ) -> StrykeResult<StrykeValue> {
17346        match method {
17347            "has" | "contains" | "member" => {
17348                if args.len() != 1 {
17349                    return Err(StrykeError::runtime(
17350                        "set->has expects one argument (element)",
17351                        line,
17352                    ));
17353                }
17354                let k = crate::value::set_member_key(&args[0]);
17355                Ok(StrykeValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
17356            }
17357            "size" | "len" | "count" => {
17358                if !args.is_empty() {
17359                    return Err(StrykeError::runtime("set->size takes no arguments", line));
17360                }
17361                Ok(StrykeValue::integer(s.len() as i64))
17362            }
17363            "values" | "list" | "elements" => {
17364                if !args.is_empty() {
17365                    return Err(StrykeError::runtime("set->values takes no arguments", line));
17366                }
17367                Ok(StrykeValue::array(s.values().cloned().collect()))
17368            }
17369            _ => Err(StrykeError::runtime(
17370                format!("Unknown method for set: {}", method),
17371                line,
17372            )),
17373        }
17374    }
17375
17376    fn deque_method(
17377        &mut self,
17378        d: Arc<Mutex<VecDeque<StrykeValue>>>,
17379        method: &str,
17380        args: &[StrykeValue],
17381        line: usize,
17382    ) -> StrykeResult<StrykeValue> {
17383        match method {
17384            "push_back" => {
17385                if args.len() != 1 {
17386                    return Err(StrykeError::runtime("push_back expects 1 argument", line));
17387                }
17388                d.lock().push_back(args[0].clone());
17389                Ok(StrykeValue::integer(d.lock().len() as i64))
17390            }
17391            "push_front" => {
17392                if args.len() != 1 {
17393                    return Err(StrykeError::runtime("push_front expects 1 argument", line));
17394                }
17395                d.lock().push_front(args[0].clone());
17396                Ok(StrykeValue::integer(d.lock().len() as i64))
17397            }
17398            "pop_back" => Ok(d.lock().pop_back().unwrap_or(StrykeValue::UNDEF)),
17399            "pop_front" => Ok(d.lock().pop_front().unwrap_or(StrykeValue::UNDEF)),
17400            "size" | "len" => Ok(StrykeValue::integer(d.lock().len() as i64)),
17401            _ => Err(StrykeError::runtime(
17402                format!("Unknown method for deque: {}", method),
17403                line,
17404            )),
17405        }
17406    }
17407
17408    fn heap_method(
17409        &mut self,
17410        h: Arc<Mutex<PerlHeap>>,
17411        method: &str,
17412        args: &[StrykeValue],
17413        line: usize,
17414    ) -> StrykeResult<StrykeValue> {
17415        match method {
17416            "push" => {
17417                if args.len() != 1 {
17418                    return Err(StrykeError::runtime("heap push expects 1 argument", line));
17419                }
17420                let mut g = h.lock();
17421                let n = g.items.len();
17422                g.items.push(args[0].clone());
17423                let cmp = g.cmp.clone();
17424                drop(g);
17425                let mut g = h.lock();
17426                self.heap_sift_up(&mut g.items, &cmp, n);
17427                Ok(StrykeValue::integer(g.items.len() as i64))
17428            }
17429            "pop" => {
17430                let mut g = h.lock();
17431                if g.items.is_empty() {
17432                    return Ok(StrykeValue::UNDEF);
17433                }
17434                let cmp = g.cmp.clone();
17435                let n = g.items.len();
17436                g.items.swap(0, n - 1);
17437                let v = g.items.pop().unwrap();
17438                if !g.items.is_empty() {
17439                    self.heap_sift_down(&mut g.items, &cmp, 0);
17440                }
17441                Ok(v)
17442            }
17443            "peek" => Ok(h
17444                .lock()
17445                .items
17446                .first()
17447                .cloned()
17448                .unwrap_or(StrykeValue::UNDEF)),
17449            _ => Err(StrykeError::runtime(
17450                format!("Unknown method for heap: {}", method),
17451                line,
17452            )),
17453        }
17454    }
17455
17456    fn ppool_method(
17457        &mut self,
17458        pool: PerlPpool,
17459        method: &str,
17460        args: &[StrykeValue],
17461        line: usize,
17462    ) -> StrykeResult<StrykeValue> {
17463        match method {
17464            "submit" => pool.submit(self, args, line),
17465            "collect" => {
17466                if !args.is_empty() {
17467                    return Err(StrykeError::runtime("collect() takes no arguments", line));
17468                }
17469                pool.collect(line)
17470            }
17471            _ => Err(StrykeError::runtime(
17472                format!("Unknown method for ppool: {}", method),
17473                line,
17474            )),
17475        }
17476    }
17477
17478    fn barrier_method(
17479        &self,
17480        barrier: PerlBarrier,
17481        method: &str,
17482        args: &[StrykeValue],
17483        line: usize,
17484    ) -> StrykeResult<StrykeValue> {
17485        match method {
17486            "wait" => {
17487                if !args.is_empty() {
17488                    return Err(StrykeError::runtime("wait() takes no arguments", line));
17489                }
17490                let _ = barrier.0.wait();
17491                Ok(StrykeValue::integer(1))
17492            }
17493            _ => Err(StrykeError::runtime(
17494                format!("Unknown method for barrier: {}", method),
17495                line,
17496            )),
17497        }
17498    }
17499
17500    fn capture_method(
17501        &self,
17502        c: Arc<CaptureResult>,
17503        method: &str,
17504        args: &[StrykeValue],
17505        line: usize,
17506    ) -> StrykeResult<StrykeValue> {
17507        if !args.is_empty() {
17508            return Err(StrykeError::runtime(
17509                format!("capture: {} takes no arguments", method),
17510                line,
17511            ));
17512        }
17513        match method {
17514            "stdout" => Ok(StrykeValue::string(c.stdout.clone())),
17515            "stderr" => Ok(StrykeValue::string(c.stderr.clone())),
17516            "exitcode" => Ok(StrykeValue::integer(c.exitcode)),
17517            "failed" => Ok(StrykeValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
17518            _ => Err(StrykeError::runtime(
17519                format!("Unknown method for capture: {}", method),
17520                line,
17521            )),
17522        }
17523    }
17524
17525    pub(crate) fn builtin_par_pipeline_stream(
17526        &mut self,
17527        args: &[StrykeValue],
17528        _line: usize,
17529    ) -> StrykeResult<StrykeValue> {
17530        let mut items = Vec::new();
17531        for v in args {
17532            if let Some(a) = v.as_array_vec() {
17533                items.extend(a);
17534            } else {
17535                items.push(v.clone());
17536            }
17537        }
17538        Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
17539            source: items,
17540            ops: Vec::new(),
17541            has_scalar_terminal: false,
17542            par_stream: true,
17543            streaming: false,
17544            streaming_workers: 0,
17545            streaming_buffer: 256,
17546        }))))
17547    }
17548
17549    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
17550    /// that wires ops through bounded channels on `collect()`.
17551    pub(crate) fn builtin_par_pipeline_stream_new(
17552        &mut self,
17553        args: &[StrykeValue],
17554        _line: usize,
17555    ) -> StrykeResult<StrykeValue> {
17556        let mut items = Vec::new();
17557        let mut workers: usize = 0;
17558        let mut buffer: usize = 256;
17559        // Separate list items from keyword args (workers => N, buffer => N).
17560        let mut i = 0;
17561        while i < args.len() {
17562            let s = args[i].to_string();
17563            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
17564                let val = args[i + 1].to_int().max(1) as usize;
17565                if s == "workers" {
17566                    workers = val;
17567                } else {
17568                    buffer = val;
17569                }
17570                i += 2;
17571            } else if let Some(a) = args[i].as_array_vec() {
17572                items.extend(a);
17573                i += 1;
17574            } else {
17575                items.push(args[i].clone());
17576                i += 1;
17577            }
17578        }
17579        Ok(StrykeValue::pipeline(Arc::new(Mutex::new(PipelineInner {
17580            source: items,
17581            ops: Vec::new(),
17582            has_scalar_terminal: false,
17583            par_stream: false,
17584            streaming: true,
17585            streaming_workers: workers,
17586            streaming_buffer: buffer,
17587        }))))
17588    }
17589
17590    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
17591    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<StrykeSub> {
17592        let line = 1usize;
17593        let body = vec![Statement {
17594            label: None,
17595            kind: StmtKind::Expression(Expr {
17596                kind: ExprKind::BinOp {
17597                    left: Box::new(Expr {
17598                        kind: ExprKind::ScalarVar("_".into()),
17599                        line,
17600                    }),
17601                    op: BinOp::Mul,
17602                    right: Box::new(Expr {
17603                        kind: ExprKind::Integer(k),
17604                        line,
17605                    }),
17606                },
17607                line,
17608            }),
17609            line,
17610        }];
17611        Arc::new(StrykeSub {
17612            name: "__pipeline_int_mul__".into(),
17613            params: vec![],
17614            body,
17615            closure_env: None,
17616            prototype: None,
17617            fib_like: None,
17618        })
17619    }
17620
17621    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<StrykeSub> {
17622        let captured = self.scope.capture();
17623        Arc::new(StrykeSub {
17624            name: "__ANON__".into(),
17625            params: vec![],
17626            body: block.clone(),
17627            closure_env: Some(captured),
17628            prototype: None,
17629            fib_like: None,
17630        })
17631    }
17632
17633    pub(crate) fn builtin_collect_execute(
17634        &mut self,
17635        args: &[StrykeValue],
17636        line: usize,
17637    ) -> StrykeResult<StrykeValue> {
17638        if args.is_empty() {
17639            return Err(StrykeError::runtime(
17640                "collect() expects at least one argument",
17641                line,
17642            ));
17643        }
17644        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
17645        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
17646        if args.len() == 1 {
17647            if let Some(p) = args[0].as_pipeline() {
17648                return self.pipeline_collect(&p, line);
17649            }
17650            return Ok(StrykeValue::array(args[0].to_list()));
17651        }
17652        Ok(StrykeValue::array(args.to_vec()))
17653    }
17654
17655    pub(crate) fn pipeline_push(
17656        &self,
17657        p: &Arc<Mutex<PipelineInner>>,
17658        op: PipelineOp,
17659        line: usize,
17660    ) -> StrykeResult<()> {
17661        let mut g = p.lock();
17662        if g.has_scalar_terminal {
17663            return Err(StrykeError::runtime(
17664                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
17665                line,
17666            ));
17667        }
17668        if matches!(
17669            &op,
17670            PipelineOp::PReduce { .. }
17671                | PipelineOp::PReduceInit { .. }
17672                | PipelineOp::PMapReduce { .. }
17673        ) {
17674            g.has_scalar_terminal = true;
17675        }
17676        g.ops.push(op);
17677        Ok(())
17678    }
17679
17680    fn pipeline_parse_sub_progress(
17681        args: &[StrykeValue],
17682        line: usize,
17683        name: &str,
17684    ) -> StrykeResult<(Arc<StrykeSub>, bool)> {
17685        if args.is_empty() {
17686            return Err(StrykeError::runtime(
17687                format!("pipeline {}: expects at least 1 argument (code ref)", name),
17688                line,
17689            ));
17690        }
17691        let Some(sub) = args[0].as_code_ref() else {
17692            return Err(StrykeError::runtime(
17693                format!("pipeline {}: first argument must be a code reference", name),
17694                line,
17695            ));
17696        };
17697        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
17698        if args.len() > 2 {
17699            return Err(StrykeError::runtime(
17700                format!(
17701                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
17702                    name
17703                ),
17704                line,
17705            ));
17706        }
17707        Ok((sub, progress))
17708    }
17709
17710    pub(crate) fn pipeline_method(
17711        &mut self,
17712        p: Arc<Mutex<PipelineInner>>,
17713        method: &str,
17714        args: &[StrykeValue],
17715        line: usize,
17716    ) -> StrykeResult<StrykeValue> {
17717        match method {
17718            "filter" | "f" | "grep" => {
17719                if args.len() != 1 {
17720                    return Err(StrykeError::runtime(
17721                        "pipeline filter/grep expects 1 argument (sub)",
17722                        line,
17723                    ));
17724                }
17725                let Some(sub) = args[0].as_code_ref() else {
17726                    return Err(StrykeError::runtime(
17727                        "pipeline filter/grep expects a code reference",
17728                        line,
17729                    ));
17730                };
17731                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
17732                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17733            }
17734            "map" => {
17735                if args.len() != 1 {
17736                    return Err(StrykeError::runtime(
17737                        "pipeline map expects 1 argument (sub)",
17738                        line,
17739                    ));
17740                }
17741                let Some(sub) = args[0].as_code_ref() else {
17742                    return Err(StrykeError::runtime(
17743                        "pipeline map expects a code reference",
17744                        line,
17745                    ));
17746                };
17747                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
17748                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17749            }
17750            "tap" | "peek" => {
17751                if args.len() != 1 {
17752                    return Err(StrykeError::runtime(
17753                        "pipeline tap/peek expects 1 argument (sub)",
17754                        line,
17755                    ));
17756                }
17757                let Some(sub) = args[0].as_code_ref() else {
17758                    return Err(StrykeError::runtime(
17759                        "pipeline tap/peek expects a code reference",
17760                        line,
17761                    ));
17762                };
17763                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
17764                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17765            }
17766            "take" => {
17767                if args.len() != 1 {
17768                    return Err(StrykeError::runtime(
17769                        "pipeline take expects 1 argument",
17770                        line,
17771                    ));
17772                }
17773                let n = args[0].to_int();
17774                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
17775                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17776            }
17777            "pmap" => {
17778                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
17779                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
17780                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17781            }
17782            "pgrep" => {
17783                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
17784                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
17785                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17786            }
17787            "pfor" => {
17788                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
17789                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
17790                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17791            }
17792            "pmap_chunked" => {
17793                if args.len() < 2 {
17794                    return Err(StrykeError::runtime(
17795                        "pipeline pmap_chunked expects chunk size and a code reference",
17796                        line,
17797                    ));
17798                }
17799                let chunk = args[0].to_int().max(1);
17800                let Some(sub) = args[1].as_code_ref() else {
17801                    return Err(StrykeError::runtime(
17802                        "pipeline pmap_chunked: second argument must be a code reference",
17803                        line,
17804                    ));
17805                };
17806                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
17807                if args.len() > 3 {
17808                    return Err(StrykeError::runtime(
17809                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
17810                        line,
17811                    ));
17812                }
17813                self.pipeline_push(
17814                    &p,
17815                    PipelineOp::PMapChunked {
17816                        chunk,
17817                        sub,
17818                        progress,
17819                    },
17820                    line,
17821                )?;
17822                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17823            }
17824            "psort" => {
17825                let (cmp, progress) = match args.len() {
17826                    0 => (None, false),
17827                    1 => {
17828                        if let Some(s) = args[0].as_code_ref() {
17829                            (Some(s), false)
17830                        } else {
17831                            (None, args[0].is_true())
17832                        }
17833                    }
17834                    2 => {
17835                        let Some(s) = args[0].as_code_ref() else {
17836                            return Err(StrykeError::runtime(
17837                                "pipeline psort: with two arguments, the first must be a comparator sub",
17838                                line,
17839                            ));
17840                        };
17841                        (Some(s), args[1].is_true())
17842                    }
17843                    _ => {
17844                        return Err(StrykeError::runtime(
17845                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
17846                            line,
17847                        ));
17848                    }
17849                };
17850                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
17851                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17852            }
17853            "pcache" => {
17854                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
17855                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
17856                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17857            }
17858            "preduce" => {
17859                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
17860                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
17861                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17862            }
17863            "preduce_init" => {
17864                if args.len() < 2 {
17865                    return Err(StrykeError::runtime(
17866                        "pipeline preduce_init expects init value and a code reference",
17867                        line,
17868                    ));
17869                }
17870                let init = args[0].clone();
17871                let Some(sub) = args[1].as_code_ref() else {
17872                    return Err(StrykeError::runtime(
17873                        "pipeline preduce_init: second argument must be a code reference",
17874                        line,
17875                    ));
17876                };
17877                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
17878                if args.len() > 3 {
17879                    return Err(StrykeError::runtime(
17880                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
17881                        line,
17882                    ));
17883                }
17884                self.pipeline_push(
17885                    &p,
17886                    PipelineOp::PReduceInit {
17887                        init,
17888                        sub,
17889                        progress,
17890                    },
17891                    line,
17892                )?;
17893                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17894            }
17895            "pmap_reduce" => {
17896                if args.len() < 2 {
17897                    return Err(StrykeError::runtime(
17898                        "pipeline pmap_reduce expects map sub and reduce sub",
17899                        line,
17900                    ));
17901                }
17902                let Some(map) = args[0].as_code_ref() else {
17903                    return Err(StrykeError::runtime(
17904                        "pipeline pmap_reduce: first argument must be a code reference (map)",
17905                        line,
17906                    ));
17907                };
17908                let Some(reduce) = args[1].as_code_ref() else {
17909                    return Err(StrykeError::runtime(
17910                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
17911                        line,
17912                    ));
17913                };
17914                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
17915                if args.len() > 3 {
17916                    return Err(StrykeError::runtime(
17917                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
17918                        line,
17919                    ));
17920                }
17921                self.pipeline_push(
17922                    &p,
17923                    PipelineOp::PMapReduce {
17924                        map,
17925                        reduce,
17926                        progress,
17927                    },
17928                    line,
17929                )?;
17930                Ok(StrykeValue::pipeline(Arc::clone(&p)))
17931            }
17932            "collect" => {
17933                if !args.is_empty() {
17934                    return Err(StrykeError::runtime(
17935                        "pipeline collect takes no arguments",
17936                        line,
17937                    ));
17938                }
17939                self.pipeline_collect(&p, line)
17940            }
17941            _ => {
17942                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
17943                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
17944                if let Some(sub) = self.resolve_sub_by_name(method) {
17945                    if !args.is_empty() {
17946                        return Err(StrykeError::runtime(
17947                            format!(
17948                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
17949                                method
17950                            ),
17951                            line,
17952                        ));
17953                    }
17954                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
17955                    Ok(StrykeValue::pipeline(Arc::clone(&p)))
17956                } else {
17957                    Err(StrykeError::runtime(
17958                        format!("Unknown method for pipeline: {}", method),
17959                        line,
17960                    ))
17961                }
17962            }
17963        }
17964    }
17965
17966    fn pipeline_parallel_map(
17967        &mut self,
17968        items: Vec<StrykeValue>,
17969        sub: &Arc<StrykeSub>,
17970        progress: bool,
17971    ) -> Vec<StrykeValue> {
17972        let subs = self.subs.clone();
17973        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
17974        let pmap_progress = PmapProgress::new(progress, items.len());
17975        let results: Vec<StrykeValue> = items
17976            .into_par_iter()
17977            .map(|item| {
17978                let mut local_interp = VMHelper::new();
17979                local_interp.subs = subs.clone();
17980                local_interp.scope.restore_capture(&scope_capture);
17981                local_interp
17982                    .scope
17983                    .restore_atomics(&atomic_arrays, &atomic_hashes);
17984                local_interp.enable_parallel_guard();
17985                local_interp.scope.set_topic(item);
17986                local_interp.scope_push_hook();
17987                let val = match local_interp.exec_block_no_scope(&sub.body) {
17988                    Ok(val) => val,
17989                    Err(_) => StrykeValue::UNDEF,
17990                };
17991                local_interp.scope_pop_hook();
17992                pmap_progress.tick();
17993                val
17994            })
17995            .collect();
17996        pmap_progress.finish();
17997        results
17998    }
17999
18000    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
18001    fn pipeline_par_stream_filter(
18002        &mut self,
18003        items: Vec<StrykeValue>,
18004        sub: &Arc<StrykeSub>,
18005    ) -> Vec<StrykeValue> {
18006        if items.is_empty() {
18007            return items;
18008        }
18009        let subs = self.subs.clone();
18010        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18011        let indexed: Vec<(usize, StrykeValue)> = items.into_iter().enumerate().collect();
18012        let mut kept: Vec<(usize, StrykeValue)> = indexed
18013            .into_par_iter()
18014            .filter_map(|(i, item)| {
18015                let mut local_interp = VMHelper::new();
18016                local_interp.subs = subs.clone();
18017                local_interp.scope.restore_capture(&scope_capture);
18018                local_interp
18019                    .scope
18020                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18021                local_interp.enable_parallel_guard();
18022                local_interp.scope.set_topic(item.clone());
18023                local_interp.scope_push_hook();
18024                let keep = match local_interp.exec_block_no_scope(&sub.body) {
18025                    Ok(val) => val.is_true(),
18026                    Err(_) => false,
18027                };
18028                local_interp.scope_pop_hook();
18029                if keep {
18030                    Some((i, item))
18031                } else {
18032                    None
18033                }
18034            })
18035            .collect();
18036        kept.sort_by_key(|(i, _)| *i);
18037        kept.into_iter().map(|(_, x)| x).collect()
18038    }
18039
18040    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
18041    fn pipeline_par_stream_map(
18042        &mut self,
18043        items: Vec<StrykeValue>,
18044        sub: &Arc<StrykeSub>,
18045    ) -> Vec<StrykeValue> {
18046        if items.is_empty() {
18047            return items;
18048        }
18049        let subs = self.subs.clone();
18050        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18051        let indexed: Vec<(usize, StrykeValue)> = items.into_iter().enumerate().collect();
18052        let mut mapped: Vec<(usize, StrykeValue)> = indexed
18053            .into_par_iter()
18054            .map(|(i, item)| {
18055                let mut local_interp = VMHelper::new();
18056                local_interp.subs = subs.clone();
18057                local_interp.scope.restore_capture(&scope_capture);
18058                local_interp
18059                    .scope
18060                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18061                local_interp.enable_parallel_guard();
18062                local_interp.scope.set_topic(item);
18063                local_interp.scope_push_hook();
18064                let val = match local_interp.exec_block_no_scope(&sub.body) {
18065                    Ok(val) => val,
18066                    Err(_) => StrykeValue::UNDEF,
18067                };
18068                local_interp.scope_pop_hook();
18069                (i, val)
18070            })
18071            .collect();
18072        mapped.sort_by_key(|(i, _)| *i);
18073        mapped.into_iter().map(|(_, x)| x).collect()
18074    }
18075
18076    fn pipeline_collect(
18077        &mut self,
18078        p: &Arc<Mutex<PipelineInner>>,
18079        line: usize,
18080    ) -> StrykeResult<StrykeValue> {
18081        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
18082            let g = p.lock();
18083            (
18084                g.source.clone(),
18085                g.ops.clone(),
18086                g.par_stream,
18087                g.streaming,
18088                g.streaming_workers,
18089                g.streaming_buffer,
18090            )
18091        };
18092        if streaming {
18093            return self.pipeline_collect_streaming(
18094                v,
18095                &ops,
18096                streaming_workers,
18097                streaming_buffer,
18098                line,
18099            );
18100        }
18101        for op in ops {
18102            match op {
18103                PipelineOp::Filter(sub) => {
18104                    if par_stream {
18105                        v = self.pipeline_par_stream_filter(v, &sub);
18106                    } else {
18107                        let mut out = Vec::new();
18108                        for item in v {
18109                            self.scope_push_hook();
18110                            self.scope.set_topic(item.clone());
18111                            if let Some(ref env) = sub.closure_env {
18112                                self.scope.restore_capture(env);
18113                            }
18114                            let keep = match self.exec_block_no_scope(&sub.body) {
18115                                Ok(val) => val.is_true(),
18116                                Err(_) => false,
18117                            };
18118                            self.scope_pop_hook();
18119                            if keep {
18120                                out.push(item);
18121                            }
18122                        }
18123                        v = out;
18124                    }
18125                }
18126                PipelineOp::Map(sub) => {
18127                    if par_stream {
18128                        v = self.pipeline_par_stream_map(v, &sub);
18129                    } else {
18130                        let mut out = Vec::new();
18131                        for item in v {
18132                            self.scope_push_hook();
18133                            self.scope.set_topic(item);
18134                            if let Some(ref env) = sub.closure_env {
18135                                self.scope.restore_capture(env);
18136                            }
18137                            let mapped = match self.exec_block_no_scope(&sub.body) {
18138                                Ok(val) => val,
18139                                Err(_) => StrykeValue::UNDEF,
18140                            };
18141                            self.scope_pop_hook();
18142                            out.push(mapped);
18143                        }
18144                        v = out;
18145                    }
18146                }
18147                PipelineOp::Tap(sub) => {
18148                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
18149                        Ok(_) => {}
18150                        Err(FlowOrError::Error(e)) => return Err(e),
18151                        Err(FlowOrError::Flow(_)) => {
18152                            return Err(StrykeError::runtime(
18153                                "tap: unsupported control flow in block",
18154                                line,
18155                            ));
18156                        }
18157                    }
18158                }
18159                PipelineOp::Take(n) => {
18160                    let n = n.max(0) as usize;
18161                    if v.len() > n {
18162                        v.truncate(n);
18163                    }
18164                }
18165                PipelineOp::PMap { sub, progress } => {
18166                    v = self.pipeline_parallel_map(v, &sub, progress);
18167                }
18168                PipelineOp::PGrep { sub, progress } => {
18169                    let subs = self.subs.clone();
18170                    let (scope_capture, atomic_arrays, atomic_hashes) =
18171                        self.scope.capture_with_atomics();
18172                    let pmap_progress = PmapProgress::new(progress, v.len());
18173                    v = v
18174                        .into_par_iter()
18175                        .filter_map(|item| {
18176                            let mut local_interp = VMHelper::new();
18177                            local_interp.subs = subs.clone();
18178                            local_interp.scope.restore_capture(&scope_capture);
18179                            local_interp
18180                                .scope
18181                                .restore_atomics(&atomic_arrays, &atomic_hashes);
18182                            local_interp.enable_parallel_guard();
18183                            local_interp.scope.set_topic(item.clone());
18184                            local_interp.scope_push_hook();
18185                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
18186                                Ok(val) => val.is_true(),
18187                                Err(_) => false,
18188                            };
18189                            local_interp.scope_pop_hook();
18190                            pmap_progress.tick();
18191                            if keep {
18192                                Some(item)
18193                            } else {
18194                                None
18195                            }
18196                        })
18197                        .collect();
18198                    pmap_progress.finish();
18199                }
18200                PipelineOp::PFor { sub, progress } => {
18201                    let subs = self.subs.clone();
18202                    let (scope_capture, atomic_arrays, atomic_hashes) =
18203                        self.scope.capture_with_atomics();
18204                    let pmap_progress = PmapProgress::new(progress, v.len());
18205                    let first_err: Arc<Mutex<Option<StrykeError>>> = Arc::new(Mutex::new(None));
18206                    v.clone().into_par_iter().for_each(|item| {
18207                        if first_err.lock().is_some() {
18208                            return;
18209                        }
18210                        let mut local_interp = VMHelper::new();
18211                        local_interp.subs = subs.clone();
18212                        local_interp.scope.restore_capture(&scope_capture);
18213                        local_interp
18214                            .scope
18215                            .restore_atomics(&atomic_arrays, &atomic_hashes);
18216                        local_interp.enable_parallel_guard();
18217                        local_interp.scope.set_topic(item);
18218                        local_interp.scope_push_hook();
18219                        match local_interp.exec_block_no_scope(&sub.body) {
18220                            Ok(_) => {}
18221                            Err(e) => {
18222                                let stryke = match e {
18223                                    FlowOrError::Error(stryke) => stryke,
18224                                    FlowOrError::Flow(_) => StrykeError::runtime(
18225                                        "return/last/next/redo not supported inside pipeline pfor block",
18226                                        line,
18227                                    ),
18228                                };
18229                                let mut g = first_err.lock();
18230                                if g.is_none() {
18231                                    *g = Some(stryke);
18232                                }
18233                            }
18234                        }
18235                        local_interp.scope_pop_hook();
18236                        pmap_progress.tick();
18237                    });
18238                    pmap_progress.finish();
18239                    let pfor_err = first_err.lock().take();
18240                    if let Some(e) = pfor_err {
18241                        return Err(e);
18242                    }
18243                }
18244                PipelineOp::PMapChunked {
18245                    chunk,
18246                    sub,
18247                    progress,
18248                } => {
18249                    let chunk_n = chunk.max(1) as usize;
18250                    let subs = self.subs.clone();
18251                    let (scope_capture, atomic_arrays, atomic_hashes) =
18252                        self.scope.capture_with_atomics();
18253                    let indexed_chunks: Vec<(usize, Vec<StrykeValue>)> = v
18254                        .chunks(chunk_n)
18255                        .enumerate()
18256                        .map(|(i, c)| (i, c.to_vec()))
18257                        .collect();
18258                    let n_chunks = indexed_chunks.len();
18259                    let pmap_progress = PmapProgress::new(progress, n_chunks);
18260                    let mut chunk_results: Vec<(usize, Vec<StrykeValue>)> = indexed_chunks
18261                        .into_par_iter()
18262                        .map(|(chunk_idx, chunk)| {
18263                            let mut local_interp = VMHelper::new();
18264                            local_interp.subs = subs.clone();
18265                            local_interp.scope.restore_capture(&scope_capture);
18266                            local_interp
18267                                .scope
18268                                .restore_atomics(&atomic_arrays, &atomic_hashes);
18269                            local_interp.enable_parallel_guard();
18270                            let mut out = Vec::with_capacity(chunk.len());
18271                            for item in chunk {
18272                                local_interp.scope.set_topic(item);
18273                                local_interp.scope_push_hook();
18274                                match local_interp.exec_block_no_scope(&sub.body) {
18275                                    Ok(val) => {
18276                                        local_interp.scope_pop_hook();
18277                                        out.push(val);
18278                                    }
18279                                    Err(_) => {
18280                                        local_interp.scope_pop_hook();
18281                                        out.push(StrykeValue::UNDEF);
18282                                    }
18283                                }
18284                            }
18285                            pmap_progress.tick();
18286                            (chunk_idx, out)
18287                        })
18288                        .collect();
18289                    pmap_progress.finish();
18290                    chunk_results.sort_by_key(|(i, _)| *i);
18291                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
18292                }
18293                PipelineOp::PSort { cmp, progress } => {
18294                    let pmap_progress = PmapProgress::new(progress, 2);
18295                    pmap_progress.tick();
18296                    match cmp {
18297                        Some(cmp_block) => {
18298                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
18299                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
18300                            } else {
18301                                let subs = self.subs.clone();
18302                                let scope_capture = self.scope.capture();
18303                                v.par_sort_by(|a, b| {
18304                                    let mut local_interp = VMHelper::new();
18305                                    local_interp.subs = subs.clone();
18306                                    local_interp.scope.restore_capture(&scope_capture);
18307                                    local_interp.enable_parallel_guard();
18308                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
18309                                    local_interp.scope_push_hook();
18310                                    let ord =
18311                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
18312                                            Ok(v) => {
18313                                                let n = v.to_int();
18314                                                if n < 0 {
18315                                                    std::cmp::Ordering::Less
18316                                                } else if n > 0 {
18317                                                    std::cmp::Ordering::Greater
18318                                                } else {
18319                                                    std::cmp::Ordering::Equal
18320                                                }
18321                                            }
18322                                            Err(_) => std::cmp::Ordering::Equal,
18323                                        };
18324                                    local_interp.scope_pop_hook();
18325                                    ord
18326                                });
18327                            }
18328                        }
18329                        None => {
18330                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
18331                        }
18332                    }
18333                    pmap_progress.tick();
18334                    pmap_progress.finish();
18335                }
18336                PipelineOp::PCache { sub, progress } => {
18337                    let subs = self.subs.clone();
18338                    let scope_capture = self.scope.capture();
18339                    let cache = &*crate::pcache::GLOBAL_PCACHE;
18340                    let pmap_progress = PmapProgress::new(progress, v.len());
18341                    v = v
18342                        .into_par_iter()
18343                        .map(|item| {
18344                            let k = crate::pcache::cache_key(&item);
18345                            if let Some(cached) = cache.get(&k) {
18346                                pmap_progress.tick();
18347                                return cached.clone();
18348                            }
18349                            let mut local_interp = VMHelper::new();
18350                            local_interp.subs = subs.clone();
18351                            local_interp.scope.restore_capture(&scope_capture);
18352                            local_interp.enable_parallel_guard();
18353                            local_interp.scope.set_topic(item.clone());
18354                            local_interp.scope_push_hook();
18355                            let val = match local_interp.exec_block_no_scope(&sub.body) {
18356                                Ok(v) => v,
18357                                Err(_) => StrykeValue::UNDEF,
18358                            };
18359                            local_interp.scope_pop_hook();
18360                            cache.insert(k, val.clone());
18361                            pmap_progress.tick();
18362                            val
18363                        })
18364                        .collect();
18365                    pmap_progress.finish();
18366                }
18367                PipelineOp::PReduce { sub, progress } => {
18368                    if v.is_empty() {
18369                        return Ok(StrykeValue::UNDEF);
18370                    }
18371                    if v.len() == 1 {
18372                        return Ok(v.into_iter().next().unwrap());
18373                    }
18374                    let block = sub.body.clone();
18375                    let subs = self.subs.clone();
18376                    let scope_capture = self.scope.capture();
18377                    let pmap_progress = PmapProgress::new(progress, v.len());
18378                    let result = v
18379                        .into_par_iter()
18380                        .map(|x| {
18381                            pmap_progress.tick();
18382                            x
18383                        })
18384                        .reduce_with(|a, b| {
18385                            let mut local_interp = VMHelper::new();
18386                            local_interp.subs = subs.clone();
18387                            local_interp.scope.restore_capture(&scope_capture);
18388                            local_interp.enable_parallel_guard();
18389                            local_interp.scope.set_sort_pair(a, b);
18390                            match local_interp.exec_block(&block) {
18391                                Ok(val) => val,
18392                                Err(_) => StrykeValue::UNDEF,
18393                            }
18394                        });
18395                    pmap_progress.finish();
18396                    return Ok(result.unwrap_or(StrykeValue::UNDEF));
18397                }
18398                PipelineOp::PReduceInit {
18399                    init,
18400                    sub,
18401                    progress,
18402                } => {
18403                    if v.is_empty() {
18404                        return Ok(init);
18405                    }
18406                    let block = sub.body.clone();
18407                    let subs = self.subs.clone();
18408                    let scope_capture = self.scope.capture();
18409                    let cap: &[(String, StrykeValue)] = scope_capture.as_slice();
18410                    if v.len() == 1 {
18411                        return Ok(fold_preduce_init_step(
18412                            &subs,
18413                            cap,
18414                            &block,
18415                            preduce_init_fold_identity(&init),
18416                            v.into_iter().next().unwrap(),
18417                        ));
18418                    }
18419                    let pmap_progress = PmapProgress::new(progress, v.len());
18420                    let result = v
18421                        .into_par_iter()
18422                        .fold(
18423                            || preduce_init_fold_identity(&init),
18424                            |acc, item| {
18425                                pmap_progress.tick();
18426                                fold_preduce_init_step(&subs, cap, &block, acc, item)
18427                            },
18428                        )
18429                        .reduce(
18430                            || preduce_init_fold_identity(&init),
18431                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
18432                        );
18433                    pmap_progress.finish();
18434                    return Ok(result);
18435                }
18436                PipelineOp::PMapReduce {
18437                    map,
18438                    reduce,
18439                    progress,
18440                } => {
18441                    if v.is_empty() {
18442                        return Ok(StrykeValue::UNDEF);
18443                    }
18444                    let map_block = map.body.clone();
18445                    let reduce_block = reduce.body.clone();
18446                    let subs = self.subs.clone();
18447                    let scope_capture = self.scope.capture();
18448                    if v.len() == 1 {
18449                        let mut local_interp = VMHelper::new();
18450                        local_interp.subs = subs.clone();
18451                        local_interp.scope.restore_capture(&scope_capture);
18452                        local_interp.scope.set_topic(v[0].clone());
18453                        return match local_interp.exec_block_no_scope(&map_block) {
18454                            Ok(val) => Ok(val),
18455                            Err(_) => Ok(StrykeValue::UNDEF),
18456                        };
18457                    }
18458                    let pmap_progress = PmapProgress::new(progress, v.len());
18459                    let result = v
18460                        .into_par_iter()
18461                        .map(|item| {
18462                            let mut local_interp = VMHelper::new();
18463                            local_interp.subs = subs.clone();
18464                            local_interp.scope.restore_capture(&scope_capture);
18465                            local_interp.scope.set_topic(item);
18466                            let val = match local_interp.exec_block_no_scope(&map_block) {
18467                                Ok(val) => val,
18468                                Err(_) => StrykeValue::UNDEF,
18469                            };
18470                            pmap_progress.tick();
18471                            val
18472                        })
18473                        .reduce_with(|a, b| {
18474                            let mut local_interp = VMHelper::new();
18475                            local_interp.subs = subs.clone();
18476                            local_interp.scope.restore_capture(&scope_capture);
18477                            local_interp.scope.set_sort_pair(a, b);
18478                            match local_interp.exec_block_no_scope(&reduce_block) {
18479                                Ok(val) => val,
18480                                Err(_) => StrykeValue::UNDEF,
18481                            }
18482                        });
18483                    pmap_progress.finish();
18484                    return Ok(result.unwrap_or(StrykeValue::UNDEF));
18485                }
18486            }
18487        }
18488        Ok(StrykeValue::array(v))
18489    }
18490
18491    /// Streaming collect: wire pipeline ops through bounded channels so items flow
18492    /// between stages concurrently.  Order is **not** preserved.
18493    fn pipeline_collect_streaming(
18494        &mut self,
18495        source: Vec<StrykeValue>,
18496        ops: &[PipelineOp],
18497        workers_per_stage: usize,
18498        buffer: usize,
18499        line: usize,
18500    ) -> StrykeResult<StrykeValue> {
18501        use crossbeam::channel::{bounded, Receiver, Sender};
18502
18503        // Validate: reject ops that require all items (can't stream).
18504        for op in ops {
18505            match op {
18506                PipelineOp::PSort { .. }
18507                | PipelineOp::PReduce { .. }
18508                | PipelineOp::PReduceInit { .. }
18509                | PipelineOp::PMapReduce { .. }
18510                | PipelineOp::PMapChunked { .. } => {
18511                    return Err(StrykeError::runtime(
18512                        format!(
18513                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
18514                            std::mem::discriminant(op)
18515                        ),
18516                        line,
18517                    ));
18518                }
18519                _ => {}
18520            }
18521        }
18522
18523        // Filter out non-streamable ops and collect streamable ones.
18524        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
18525        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
18526        if streamable_ops.is_empty() {
18527            return Ok(StrykeValue::array(source));
18528        }
18529
18530        let n_stages = streamable_ops.len();
18531        let wn = if workers_per_stage > 0 {
18532            workers_per_stage
18533        } else {
18534            self.parallel_thread_count()
18535        };
18536        let subs = self.subs.clone();
18537        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18538
18539        // Build channels: one between each pair of stages, plus one for output.
18540        // channel[0]: source → stage 0
18541        // channel[i]: stage i-1 → stage i
18542        // channel[n_stages]: stage n_stages-1 → collector
18543        let mut channels: Vec<(Sender<StrykeValue>, Receiver<StrykeValue>)> =
18544            (0..=n_stages).map(|_| bounded(buffer)).collect();
18545
18546        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
18547        let take_done: Arc<std::sync::atomic::AtomicBool> =
18548            Arc::new(std::sync::atomic::AtomicBool::new(false));
18549
18550        // Collect senders/receivers for each stage.
18551        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
18552        let source_tx = channels[0].0.clone();
18553        let result_rx = channels[n_stages].1.clone();
18554        let results: Arc<Mutex<Vec<StrykeValue>>> = Arc::new(Mutex::new(Vec::new()));
18555
18556        std::thread::scope(|scope| {
18557            // Collector thread: drain results concurrently to avoid deadlock
18558            // when bounded channels fill up.
18559            let result_rx_c = result_rx.clone();
18560            let results_c = Arc::clone(&results);
18561            scope.spawn(move || {
18562                while let Ok(item) = result_rx_c.recv() {
18563                    results_c.lock().push(item);
18564                }
18565            });
18566
18567            // Source feeder thread.
18568            let err_s = Arc::clone(&err);
18569            let take_done_s = Arc::clone(&take_done);
18570            scope.spawn(move || {
18571                for item in source {
18572                    if err_s.lock().is_some()
18573                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
18574                    {
18575                        break;
18576                    }
18577                    if source_tx.send(item).is_err() {
18578                        break;
18579                    }
18580                }
18581            });
18582
18583            // Spawn workers for each stage.
18584            for (stage_idx, op) in streamable_ops.iter().enumerate() {
18585                let rx = channels[stage_idx].1.clone();
18586                let tx = channels[stage_idx + 1].0.clone();
18587
18588                for _ in 0..wn {
18589                    let rx = rx.clone();
18590                    let tx = tx.clone();
18591                    let subs = subs.clone();
18592                    let capture = capture.clone();
18593                    let atomic_arrays = atomic_arrays.clone();
18594                    let atomic_hashes = atomic_hashes.clone();
18595                    let err_w = Arc::clone(&err);
18596                    let take_done_w = Arc::clone(&take_done);
18597
18598                    match *op {
18599                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
18600                            let sub = Arc::clone(sub);
18601                            scope.spawn(move || {
18602                                while let Ok(item) = rx.recv() {
18603                                    if err_w.lock().is_some() {
18604                                        break;
18605                                    }
18606                                    let mut interp = VMHelper::new();
18607                                    interp.subs = subs.clone();
18608                                    interp.scope.restore_capture(&capture);
18609                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
18610                                    interp.enable_parallel_guard();
18611                                    interp.scope.set_topic(item.clone());
18612                                    interp.scope_push_hook();
18613                                    let keep = match interp.exec_block_no_scope(&sub.body) {
18614                                        Ok(val) => val.is_true(),
18615                                        Err(_) => false,
18616                                    };
18617                                    interp.scope_pop_hook();
18618                                    if keep && tx.send(item).is_err() {
18619                                        break;
18620                                    }
18621                                }
18622                            });
18623                        }
18624                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
18625                            let sub = Arc::clone(sub);
18626                            scope.spawn(move || {
18627                                while let Ok(item) = rx.recv() {
18628                                    if err_w.lock().is_some() {
18629                                        break;
18630                                    }
18631                                    let mut interp = VMHelper::new();
18632                                    interp.subs = subs.clone();
18633                                    interp.scope.restore_capture(&capture);
18634                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
18635                                    interp.enable_parallel_guard();
18636                                    interp.scope.set_topic(item);
18637                                    interp.scope_push_hook();
18638                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
18639                                        Ok(val) => val,
18640                                        Err(_) => StrykeValue::UNDEF,
18641                                    };
18642                                    interp.scope_pop_hook();
18643                                    if tx.send(mapped).is_err() {
18644                                        break;
18645                                    }
18646                                }
18647                            });
18648                        }
18649                        PipelineOp::Take(n) => {
18650                            let limit = (*n).max(0) as usize;
18651                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
18652                            let count_w = Arc::clone(&count);
18653                            scope.spawn(move || {
18654                                while let Ok(item) = rx.recv() {
18655                                    let prev =
18656                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
18657                                    if prev >= limit {
18658                                        take_done_w
18659                                            .store(true, std::sync::atomic::Ordering::Relaxed);
18660                                        break;
18661                                    }
18662                                    if tx.send(item).is_err() {
18663                                        break;
18664                                    }
18665                                }
18666                            });
18667                            // Take only needs 1 worker; skip remaining worker spawns.
18668                            break;
18669                        }
18670                        PipelineOp::PFor { ref sub, .. } => {
18671                            let sub = Arc::clone(sub);
18672                            scope.spawn(move || {
18673                                while let Ok(item) = rx.recv() {
18674                                    if err_w.lock().is_some() {
18675                                        break;
18676                                    }
18677                                    let mut interp = VMHelper::new();
18678                                    interp.subs = subs.clone();
18679                                    interp.scope.restore_capture(&capture);
18680                                    interp
18681                                        .scope
18682                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
18683                                    interp.enable_parallel_guard();
18684                                    interp.scope.set_topic(item.clone());
18685                                    interp.scope_push_hook();
18686                                    match interp.exec_block_no_scope(&sub.body) {
18687                                        Ok(_) => {}
18688                                        Err(e) => {
18689                                            let msg = match e {
18690                                                FlowOrError::Error(stryke) => stryke.to_string(),
18691                                                FlowOrError::Flow(_) => {
18692                                                    "unexpected control flow in par_pipeline_stream pfor".into()
18693                                                }
18694                                            };
18695                                            let mut g = err_w.lock();
18696                                            if g.is_none() {
18697                                                *g = Some(msg);
18698                                            }
18699                                            interp.scope_pop_hook();
18700                                            break;
18701                                        }
18702                                    }
18703                                    interp.scope_pop_hook();
18704                                    if tx.send(item).is_err() {
18705                                        break;
18706                                    }
18707                                }
18708                            });
18709                        }
18710                        PipelineOp::Tap(ref sub) => {
18711                            let sub = Arc::clone(sub);
18712                            scope.spawn(move || {
18713                                while let Ok(item) = rx.recv() {
18714                                    if err_w.lock().is_some() {
18715                                        break;
18716                                    }
18717                                    let mut interp = VMHelper::new();
18718                                    interp.subs = subs.clone();
18719                                    interp.scope.restore_capture(&capture);
18720                                    interp
18721                                        .scope
18722                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
18723                                    interp.enable_parallel_guard();
18724                                    match interp.call_sub(
18725                                        &sub,
18726                                        vec![item.clone()],
18727                                        WantarrayCtx::Void,
18728                                        line,
18729                                    )
18730                                    {
18731                                        Ok(_) => {}
18732                                        Err(e) => {
18733                                            let msg = match e {
18734                                                FlowOrError::Error(stryke) => stryke.to_string(),
18735                                                FlowOrError::Flow(_) => {
18736                                                    "unexpected control flow in par_pipeline_stream tap"
18737                                                        .into()
18738                                                }
18739                                            };
18740                                            let mut g = err_w.lock();
18741                                            if g.is_none() {
18742                                                *g = Some(msg);
18743                                            }
18744                                            break;
18745                                        }
18746                                    }
18747                                    if tx.send(item).is_err() {
18748                                        break;
18749                                    }
18750                                }
18751                            });
18752                        }
18753                        PipelineOp::PCache { ref sub, .. } => {
18754                            let sub = Arc::clone(sub);
18755                            scope.spawn(move || {
18756                                while let Ok(item) = rx.recv() {
18757                                    if err_w.lock().is_some() {
18758                                        break;
18759                                    }
18760                                    let k = crate::pcache::cache_key(&item);
18761                                    let val = if let Some(cached) =
18762                                        crate::pcache::GLOBAL_PCACHE.get(&k)
18763                                    {
18764                                        cached.clone()
18765                                    } else {
18766                                        let mut interp = VMHelper::new();
18767                                        interp.subs = subs.clone();
18768                                        interp.scope.restore_capture(&capture);
18769                                        interp
18770                                            .scope
18771                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
18772                                        interp.enable_parallel_guard();
18773                                        interp.scope.set_topic(item);
18774                                        interp.scope_push_hook();
18775                                        let v = match interp.exec_block_no_scope(&sub.body) {
18776                                            Ok(v) => v,
18777                                            Err(_) => StrykeValue::UNDEF,
18778                                        };
18779                                        interp.scope_pop_hook();
18780                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
18781                                        v
18782                                    };
18783                                    if tx.send(val).is_err() {
18784                                        break;
18785                                    }
18786                                }
18787                            });
18788                        }
18789                        // Non-streaming ops already rejected above.
18790                        _ => unreachable!(),
18791                    }
18792                }
18793            }
18794
18795            // Drop our copies of intermediate senders/receivers so channels disconnect
18796            // when workers finish.  Also drop result_rx so the collector thread exits
18797            // once all stage workers are done.
18798            channels.clear();
18799            drop(result_rx);
18800        });
18801
18802        if let Some(msg) = err.lock().take() {
18803            return Err(StrykeError::runtime(msg, line));
18804        }
18805
18806        let results = std::mem::take(&mut *results.lock());
18807        Ok(StrykeValue::array(results))
18808    }
18809
18810    fn heap_compare(&mut self, cmp: &Arc<StrykeSub>, a: &StrykeValue, b: &StrykeValue) -> Ordering {
18811        self.scope_push_hook();
18812        if let Some(ref env) = cmp.closure_env {
18813            self.scope.restore_capture(env);
18814        }
18815        self.scope.set_sort_pair(a.clone(), b.clone());
18816        let ord = match self.exec_block_no_scope(&cmp.body) {
18817            Ok(v) => {
18818                let n = v.to_int();
18819                if n < 0 {
18820                    Ordering::Less
18821                } else if n > 0 {
18822                    Ordering::Greater
18823                } else {
18824                    Ordering::Equal
18825                }
18826            }
18827            Err(_) => Ordering::Equal,
18828        };
18829        self.scope_pop_hook();
18830        ord
18831    }
18832
18833    fn heap_sift_up(&mut self, items: &mut [StrykeValue], cmp: &Arc<StrykeSub>, mut i: usize) {
18834        while i > 0 {
18835            let p = (i - 1) / 2;
18836            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
18837                break;
18838            }
18839            items.swap(i, p);
18840            i = p;
18841        }
18842    }
18843
18844    fn heap_sift_down(&mut self, items: &mut [StrykeValue], cmp: &Arc<StrykeSub>, mut i: usize) {
18845        let n = items.len();
18846        loop {
18847            let mut sm = i;
18848            let l = 2 * i + 1;
18849            let r = 2 * i + 2;
18850            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
18851                sm = l;
18852            }
18853            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
18854                sm = r;
18855            }
18856            if sm == i {
18857                break;
18858            }
18859            items.swap(i, sm);
18860            i = sm;
18861        }
18862    }
18863
18864    fn hash_for_signature_destruct(
18865        &mut self,
18866        v: &StrykeValue,
18867        line: usize,
18868    ) -> StrykeResult<IndexMap<String, StrykeValue>> {
18869        let Some(m) = self.match_subject_as_hash(v) else {
18870            return Err(StrykeError::runtime(
18871                format!(
18872                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
18873                    v.ref_type()
18874                ),
18875                line,
18876            ));
18877        };
18878        Ok(m)
18879    }
18880
18881    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
18882    pub(crate) fn apply_sub_signature(
18883        &mut self,
18884        sub: &StrykeSub,
18885        argv: &[StrykeValue],
18886        line: usize,
18887    ) -> StrykeResult<()> {
18888        if sub.params.is_empty() {
18889            return Ok(());
18890        }
18891        let mut i = 0usize;
18892        for p in &sub.params {
18893            match p {
18894                SubSigParam::Scalar(name, ty, default) => {
18895                    let val = if i < argv.len() {
18896                        argv[i].clone()
18897                    } else if let Some(default_expr) = default {
18898                        match self.eval_expr(default_expr) {
18899                            Ok(v) => v,
18900                            Err(FlowOrError::Error(e)) => return Err(e),
18901                            Err(FlowOrError::Flow(_)) => {
18902                                return Err(StrykeError::runtime(
18903                                    "unexpected control flow in parameter default",
18904                                    line,
18905                                ))
18906                            }
18907                        }
18908                    } else {
18909                        StrykeValue::UNDEF
18910                    };
18911                    i += 1;
18912                    if let Some(t) = ty {
18913                        if let Err(e) = t.check_value(&val) {
18914                            return Err(StrykeError::runtime(
18915                                format!("sub parameter ${}: {}", name, e),
18916                                line,
18917                            ));
18918                        }
18919                    }
18920                    let n = self.english_scalar_name(name);
18921                    self.scope.declare_scalar(n, val);
18922                }
18923                SubSigParam::Array(name, default) => {
18924                    let rest: Vec<StrykeValue> = if i < argv.len() {
18925                        let r = argv[i..].to_vec();
18926                        i = argv.len();
18927                        r
18928                    } else if let Some(default_expr) = default {
18929                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
18930                            Ok(v) => v,
18931                            Err(FlowOrError::Error(e)) => return Err(e),
18932                            Err(FlowOrError::Flow(_)) => {
18933                                return Err(StrykeError::runtime(
18934                                    "unexpected control flow in parameter default",
18935                                    line,
18936                                ))
18937                            }
18938                        };
18939                        val.to_list()
18940                    } else {
18941                        vec![]
18942                    };
18943                    let aname = self.stash_array_name_for_package(name);
18944                    self.scope.declare_array(&aname, rest);
18945                }
18946                SubSigParam::Hash(name, default) => {
18947                    let rest: Vec<StrykeValue> = if i < argv.len() {
18948                        let r = argv[i..].to_vec();
18949                        i = argv.len();
18950                        r
18951                    } else if let Some(default_expr) = default {
18952                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
18953                            Ok(v) => v,
18954                            Err(FlowOrError::Error(e)) => return Err(e),
18955                            Err(FlowOrError::Flow(_)) => {
18956                                return Err(StrykeError::runtime(
18957                                    "unexpected control flow in parameter default",
18958                                    line,
18959                                ))
18960                            }
18961                        };
18962                        val.to_list()
18963                    } else {
18964                        vec![]
18965                    };
18966                    let mut map = IndexMap::new();
18967                    let mut j = 0;
18968                    while j + 1 < rest.len() {
18969                        map.insert(rest[j].to_string(), rest[j + 1].clone());
18970                        j += 2;
18971                    }
18972                    self.scope.declare_hash(name, map);
18973                }
18974                SubSigParam::ArrayDestruct(elems) => {
18975                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
18976                    i += 1;
18977                    let Some(arr) = self.match_subject_as_array(&arg) else {
18978                        return Err(StrykeError::runtime(
18979                            format!(
18980                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
18981                                arg.ref_type()
18982                            ),
18983                            line,
18984                        ));
18985                    };
18986                    let binds = self
18987                        .match_array_pattern_elems(&arr, elems, line)
18988                        .map_err(|e| match e {
18989                            FlowOrError::Error(stryke) => stryke,
18990                            FlowOrError::Flow(_) => StrykeError::runtime(
18991                                "unexpected flow in sub signature array destruct",
18992                                line,
18993                            ),
18994                        })?;
18995                    let Some(binds) = binds else {
18996                        return Err(StrykeError::runtime(
18997                            "sub signature array destruct: length or element mismatch",
18998                            line,
18999                        ));
19000                    };
19001                    for b in binds {
19002                        match b {
19003                            PatternBinding::Scalar(name, v) => {
19004                                let n = self.english_scalar_name(&name);
19005                                self.scope.declare_scalar(n, v);
19006                            }
19007                            PatternBinding::Array(name, elems) => {
19008                                self.scope.declare_array(&name, elems);
19009                            }
19010                        }
19011                    }
19012                }
19013                SubSigParam::HashDestruct(pairs) => {
19014                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19015                    i += 1;
19016                    let map = self.hash_for_signature_destruct(&arg, line)?;
19017                    for (key, varname) in pairs {
19018                        let v = map.get(key).cloned().unwrap_or(StrykeValue::UNDEF);
19019                        let n = self.english_scalar_name(varname);
19020                        self.scope.declare_scalar(n, v);
19021                    }
19022                }
19023            }
19024        }
19025        Ok(())
19026    }
19027
19028    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
19029    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
19030    /// These are `StrykeSub`s with empty bodies and magic keys in `closure_env`.
19031    pub(crate) fn try_hof_dispatch(
19032        &mut self,
19033        sub: &StrykeSub,
19034        args: &[StrykeValue],
19035        want: WantarrayCtx,
19036        line: usize,
19037    ) -> Option<ExecResult> {
19038        let env = sub.closure_env.as_ref()?;
19039        fn env_get<'a>(env: &'a [(String, StrykeValue)], key: &str) -> Option<&'a StrykeValue> {
19040            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
19041        }
19042
19043        match sub.name.as_str() {
19044            // ── compose: right-to-left function application ──
19045            "__comp__" => {
19046                let fns = env_get(env, "__comp_fns__")?.to_list();
19047                let mut val = args.first().cloned().unwrap_or(StrykeValue::UNDEF);
19048                for f in fns.iter().rev() {
19049                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
19050                        Ok(v) => val = v,
19051                        Err(e) => return Some(Err(e)),
19052                    }
19053                }
19054                Some(Ok(val))
19055            }
19056            // ── constantly: always return the captured value ──
19057            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
19058            // ── juxt: call each fn with same args, collect results ──
19059            "__juxt__" => {
19060                let fns = env_get(env, "__juxt_fns__")?.to_list();
19061                let mut results = Vec::with_capacity(fns.len());
19062                for f in &fns {
19063                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
19064                        Ok(v) => results.push(v),
19065                        Err(e) => return Some(Err(e)),
19066                    }
19067                }
19068                Some(Ok(StrykeValue::array(results)))
19069            }
19070            // ── partial: prepend bound args ──
19071            "__partial__" => {
19072                let fn_val = env_get(env, "__partial_fn__")?.clone();
19073                let bound = env_get(env, "__partial_args__")?.to_list();
19074                let mut all_args = bound;
19075                all_args.extend_from_slice(args);
19076                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
19077            }
19078            // ── complement: negate the result ──
19079            "__complement__" => {
19080                let fn_val = env_get(env, "__complement_fn__")?.clone();
19081                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19082                    Ok(v) => Some(Ok(StrykeValue::integer(if v.is_true() { 0 } else { 1 }))),
19083                    Err(e) => Some(Err(e)),
19084                }
19085            }
19086            // ── fnil: replace undef args with defaults ──
19087            "__fnil__" => {
19088                let fn_val = env_get(env, "__fnil_fn__")?.clone();
19089                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
19090                let mut patched = args.to_vec();
19091                for (i, d) in defaults.iter().enumerate() {
19092                    if i < patched.len() {
19093                        if patched[i].is_undef() {
19094                            patched[i] = d.clone();
19095                        }
19096                    } else {
19097                        patched.push(d.clone());
19098                    }
19099                }
19100                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
19101            }
19102            // ── memoize: cache by stringified args ──
19103            "__memoize__" => {
19104                let fn_val = env_get(env, "__memoize_fn__")?.clone();
19105                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
19106                let key = args
19107                    .iter()
19108                    .map(|a| a.to_string())
19109                    .collect::<Vec<_>>()
19110                    .join("\x00");
19111                if let Some(href) = cache_ref.as_hash_ref() {
19112                    if let Some(cached) = href.read().get(&key) {
19113                        return Some(Ok(cached.clone()));
19114                    }
19115                }
19116                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19117                    Ok(v) => {
19118                        if let Some(href) = cache_ref.as_hash_ref() {
19119                            href.write().insert(key, v.clone());
19120                        }
19121                        Some(Ok(v))
19122                    }
19123                    Err(e) => Some(Err(e)),
19124                }
19125            }
19126            // ── curry: accumulate args until arity reached ──
19127            "__curry__" => {
19128                let fn_val = env_get(env, "__curry_fn__")?.clone();
19129                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
19130                let bound = env_get(env, "__curry_bound__")?.to_list();
19131                let mut all = bound;
19132                all.extend_from_slice(args);
19133                if all.len() >= arity {
19134                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
19135                } else {
19136                    let curry_sub = StrykeSub {
19137                        name: "__curry__".to_string(),
19138                        params: vec![],
19139                        body: vec![],
19140                        closure_env: Some(vec![
19141                            ("__curry_fn__".to_string(), fn_val),
19142                            (
19143                                "__curry_arity__".to_string(),
19144                                StrykeValue::integer(arity as i64),
19145                            ),
19146                            ("__curry_bound__".to_string(), StrykeValue::array(all)),
19147                        ]),
19148                        prototype: None,
19149                        fib_like: None,
19150                    };
19151                    Some(Ok(StrykeValue::code_ref(Arc::new(curry_sub))))
19152                }
19153            }
19154            // ── once: call once, cache forever ──
19155            "__once__" => {
19156                let cache_ref = env_get(env, "__once_cache__")?.clone();
19157                if let Some(href) = cache_ref.as_hash_ref() {
19158                    let r = href.read();
19159                    if r.contains_key("done") {
19160                        return Some(Ok(r.get("val").cloned().unwrap_or(StrykeValue::UNDEF)));
19161                    }
19162                }
19163                let fn_val = env_get(env, "__once_fn__")?.clone();
19164                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
19165                    Ok(v) => {
19166                        if let Some(href) = cache_ref.as_hash_ref() {
19167                            let mut w = href.write();
19168                            w.insert("done".to_string(), StrykeValue::integer(1));
19169                            w.insert("val".to_string(), v.clone());
19170                        }
19171                        Some(Ok(v))
19172                    }
19173                    Err(e) => Some(Err(e)),
19174                }
19175            }
19176            _ => None,
19177        }
19178    }
19179
19180    pub(crate) fn call_sub(
19181        &mut self,
19182        sub: &StrykeSub,
19183        args: Vec<StrykeValue>,
19184        want: WantarrayCtx,
19185        line: usize,
19186    ) -> ExecResult {
19187        // Default path: derive the package from `sub.name` if it is qualified. Bare-named
19188        // subs (registered without a `Pkg::` prefix) leave `__PACKAGE__` untouched.
19189        let pkg = sub.name.rsplit_once("::").map(|(p, _)| p.to_string());
19190        self.call_sub_with_package(sub, args, want, line, pkg)
19191    }
19192
19193    /// Internal helper: like [`Self::call_sub`] but takes an explicit home-package override
19194    /// (used by [`Self::call_named_sub`], which knows the qualified registry key even when
19195    /// the cached `StrykeSub.name` is bare).
19196    fn call_sub_with_package(
19197        &mut self,
19198        sub: &StrykeSub,
19199        args: Vec<StrykeValue>,
19200        want: WantarrayCtx,
19201        _line: usize,
19202        home_package: Option<String>,
19203    ) -> ExecResult {
19204        // Push current sub for __SUB__ access
19205        self.current_sub_stack.push(Arc::new(sub.clone()));
19206
19207        // Single frame for both @_ and the block's local variables —
19208        // avoids the double push_frame/pop_frame overhead per call.
19209        self.scope_push_hook();
19210        self.scope.declare_array("_", args.clone());
19211        if let Some(ref env) = sub.closure_env {
19212            self.scope.restore_capture(env);
19213        }
19214        // Switch `__PACKAGE__` to the sub's home package so cross-package `our`/`oursync`
19215        // qualifies correctly inside the body. Bytecode VM rewrites at compile time so it
19216        // never needed this; the tree walker (used by parallel workers) does need it.
19217        // Goes AFTER restore_capture so the closure's captured `__PACKAGE__` doesn't
19218        // overwrite our home-package switch.
19219        if let Some(pkg) = home_package {
19220            self.scope
19221                .declare_scalar("__PACKAGE__", StrykeValue::string(pkg));
19222        }
19223        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
19224        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
19225        // Must be AFTER restore_capture so we don't get shadowed by captured $_
19226        self.scope.set_closure_args(&args);
19227        // Move `@_` out so `fib_like` / hof dispatch take `&[StrykeValue]` without cloning.
19228        let argv = self.scope.take_sub_underscore().unwrap_or_default();
19229        self.apply_sub_signature(sub, &argv, _line)?;
19230        let saved = self.wantarray_kind;
19231        self.wantarray_kind = want;
19232        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
19233            self.wantarray_kind = saved;
19234            self.scope_pop_hook();
19235            self.current_sub_stack.pop();
19236            return match r {
19237                Ok(v) => Ok(v),
19238                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19239                Err(e) => Err(e),
19240            };
19241        }
19242        if let Some(pat) = sub.fib_like.as_ref() {
19243            if argv.len() == 1 {
19244                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
19245                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
19246                    if let Some(p) = &mut self.profiler {
19247                        p.enter_sub(&sub.name);
19248                    }
19249                    self.debugger_enter_sub(&sub.name);
19250                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
19251                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
19252                        p.exit_sub(t0.elapsed());
19253                    }
19254                    self.debugger_leave_sub();
19255                    self.wantarray_kind = saved;
19256                    self.scope_pop_hook();
19257                    self.current_sub_stack.pop();
19258                    return Ok(StrykeValue::integer(n));
19259                }
19260            }
19261        }
19262        self.scope.declare_array("_", argv.clone());
19263        // Note: set_closure_args was already called at line 15077; don't call it again
19264        // as that would incorrectly shift the outer topic stack a second time.
19265        let t0 = self.profiler.is_some().then(std::time::Instant::now);
19266        if let Some(p) = &mut self.profiler {
19267            p.enter_sub(&sub.name);
19268        }
19269        self.debugger_enter_sub(&sub.name);
19270        // Always evaluate the function body's last expression in List context so
19271        // `@array` returns the array contents, not the count. The caller adapts the
19272        // return value to their own wantarray context after receiving it.
19273        let result = self.exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List);
19274        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
19275            p.exit_sub(t0.elapsed());
19276        }
19277        self.debugger_leave_sub();
19278        // For goto &sub, capture @_ before popping the frame
19279        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
19280            Some(self.scope.get_array("_"))
19281        } else {
19282            None
19283        };
19284        self.wantarray_kind = saved;
19285        self.scope_pop_hook();
19286        self.current_sub_stack.pop();
19287        match result {
19288            Ok(v) => Ok(v),
19289            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19290            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
19291                // goto &sub — tail call: look up target and call with same @_
19292                let goto_args = goto_args.unwrap_or_default();
19293                let fqn = if target_name.contains("::") {
19294                    target_name.clone()
19295                } else {
19296                    format!("{}::{}", self.current_package(), target_name)
19297                };
19298                if let Some(target_sub) = self
19299                    .subs
19300                    .get(&fqn)
19301                    .cloned()
19302                    .or_else(|| self.subs.get(&target_name).cloned())
19303                {
19304                    self.call_sub(&target_sub, goto_args, want, _line)
19305                } else {
19306                    Err(StrykeError::runtime(
19307                        format!("Undefined subroutine &{}", target_name),
19308                        _line,
19309                    )
19310                    .into())
19311                }
19312            }
19313            Err(FlowOrError::Flow(Flow::Yield(_))) => {
19314                Err(StrykeError::runtime("yield is only valid inside gen { }", 0).into())
19315            }
19316            Err(e) => Err(e),
19317        }
19318    }
19319
19320    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
19321    fn call_struct_method(
19322        &mut self,
19323        body: &Block,
19324        params: &[SubSigParam],
19325        args: Vec<StrykeValue>,
19326        line: usize,
19327    ) -> ExecResult {
19328        self.scope_push_hook();
19329        self.scope.declare_array("_", args.clone());
19330        // Bind $self to first arg (the receiver)
19331        if let Some(self_val) = args.first() {
19332            self.scope.declare_scalar("self", self_val.clone());
19333        }
19334        // Set $_0, $_1, etc. for the EXPLICIT args (skip $self at args[0]) so
19335        // `fn tom { _ * 2 }; obj->tom(99)` works identically to the standalone
19336        // `fn tom { _ * 2 }; tom(99)` — both treat the first EXPLICIT arg as
19337        // the topic. `$self` stays accessible via the dedicated `$self` binding
19338        // above and via `$_[0]` (full `@_` retained).
19339        if args.len() > 1 {
19340            self.scope.set_closure_args(&args[1..]);
19341        }
19342        // Apply signature if provided - skip the first arg ($self) for user params
19343        let user_args: Vec<StrykeValue> = args.iter().skip(1).cloned().collect();
19344        self.apply_params_to_argv(params, &user_args, line)?;
19345        let result = self.exec_block_no_scope(body);
19346        self.scope_pop_hook();
19347        match result {
19348            Ok(v) => Ok(v),
19349            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19350            Err(e) => Err(e),
19351        }
19352    }
19353
19354    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
19355    pub(crate) fn call_class_method(
19356        &mut self,
19357        body: &Block,
19358        params: &[SubSigParam],
19359        args: Vec<StrykeValue>,
19360        line: usize,
19361    ) -> ExecResult {
19362        self.call_class_method_inner(body, params, args, line, false)
19363    }
19364
19365    /// Call a static class method: `Math::add(...)`.
19366    pub(crate) fn call_static_class_method(
19367        &mut self,
19368        body: &Block,
19369        params: &[SubSigParam],
19370        args: Vec<StrykeValue>,
19371        line: usize,
19372    ) -> ExecResult {
19373        self.call_class_method_inner(body, params, args, line, true)
19374    }
19375
19376    fn call_class_method_inner(
19377        &mut self,
19378        body: &Block,
19379        params: &[SubSigParam],
19380        args: Vec<StrykeValue>,
19381        line: usize,
19382        is_static: bool,
19383    ) -> ExecResult {
19384        self.scope_push_hook();
19385        self.scope.declare_array("_", args.clone());
19386        if !is_static {
19387            // Bind $self to first arg (the receiver) for instance methods
19388            if let Some(self_val) = args.first() {
19389                self.scope.declare_scalar("self", self_val.clone());
19390            }
19391        }
19392        // Set $_0, $_1, etc. for the EXPLICIT args. For instance methods skip
19393        // args[0] (which is $self) so `fn tom { _ * 2 }; obj->tom(99)` behaves
19394        // the same as `fn tom { _ * 2 }; tom(99)` — both treat the first
19395        // EXPLICIT arg as the topic. `$self` is still accessible via the
19396        // dedicated `$self` binding above and via `$_[0]` (full `@_` retained).
19397        // Static methods have no `$self`, so the full args list IS the topic.
19398        if is_static {
19399            self.scope.set_closure_args(&args);
19400        } else if args.len() > 1 {
19401            self.scope.set_closure_args(&args[1..]);
19402        }
19403        // Apply signature: skip first arg ($self) only for instance methods
19404        let user_args: Vec<StrykeValue> = if is_static {
19405            args.clone()
19406        } else {
19407            args.iter().skip(1).cloned().collect()
19408        };
19409        self.apply_params_to_argv(params, &user_args, line)?;
19410        let result = self.exec_block_no_scope(body);
19411        self.scope_pop_hook();
19412        match result {
19413            Ok(v) => Ok(v),
19414            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
19415            Err(e) => Err(e),
19416        }
19417    }
19418
19419    /// Apply SubSigParam bindings without the full StrykeSub machinery.
19420    fn apply_params_to_argv(
19421        &mut self,
19422        params: &[SubSigParam],
19423        argv: &[StrykeValue],
19424        line: usize,
19425    ) -> StrykeResult<()> {
19426        let mut i = 0;
19427        for param in params {
19428            match param {
19429                SubSigParam::Scalar(name, ty_opt, default) => {
19430                    let v = if i < argv.len() {
19431                        argv[i].clone()
19432                    } else if let Some(default_expr) = default {
19433                        match self.eval_expr(default_expr) {
19434                            Ok(v) => v,
19435                            Err(FlowOrError::Error(e)) => return Err(e),
19436                            Err(FlowOrError::Flow(_)) => {
19437                                return Err(StrykeError::runtime(
19438                                    "unexpected control flow in parameter default",
19439                                    line,
19440                                ))
19441                            }
19442                        }
19443                    } else {
19444                        StrykeValue::UNDEF
19445                    };
19446                    i += 1;
19447                    if let Some(ty) = ty_opt {
19448                        ty.check_value(&v).map_err(|msg| {
19449                            StrykeError::type_error(
19450                                format!("method parameter ${}: {}", name, msg),
19451                                line,
19452                            )
19453                        })?;
19454                    }
19455                    let n = self.english_scalar_name(name);
19456                    self.scope.declare_scalar(n, v);
19457                }
19458                SubSigParam::Array(name, default) => {
19459                    let rest: Vec<StrykeValue> = if i < argv.len() {
19460                        let r = argv[i..].to_vec();
19461                        i = argv.len();
19462                        r
19463                    } else if let Some(default_expr) = default {
19464                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19465                            Ok(v) => v,
19466                            Err(FlowOrError::Error(e)) => return Err(e),
19467                            Err(FlowOrError::Flow(_)) => {
19468                                return Err(StrykeError::runtime(
19469                                    "unexpected control flow in parameter default",
19470                                    line,
19471                                ))
19472                            }
19473                        };
19474                        val.to_list()
19475                    } else {
19476                        vec![]
19477                    };
19478                    let aname = self.stash_array_name_for_package(name);
19479                    self.scope.declare_array(&aname, rest);
19480                }
19481                SubSigParam::Hash(name, default) => {
19482                    let rest: Vec<StrykeValue> = if i < argv.len() {
19483                        let r = argv[i..].to_vec();
19484                        i = argv.len();
19485                        r
19486                    } else if let Some(default_expr) = default {
19487                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19488                            Ok(v) => v,
19489                            Err(FlowOrError::Error(e)) => return Err(e),
19490                            Err(FlowOrError::Flow(_)) => {
19491                                return Err(StrykeError::runtime(
19492                                    "unexpected control flow in parameter default",
19493                                    line,
19494                                ))
19495                            }
19496                        };
19497                        val.to_list()
19498                    } else {
19499                        vec![]
19500                    };
19501                    let mut map = IndexMap::new();
19502                    let mut j = 0;
19503                    while j + 1 < rest.len() {
19504                        map.insert(rest[j].to_string(), rest[j + 1].clone());
19505                        j += 2;
19506                    }
19507                    self.scope.declare_hash(name, map);
19508                }
19509                SubSigParam::ArrayDestruct(elems) => {
19510                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19511                    i += 1;
19512                    let Some(arr) = self.match_subject_as_array(&arg) else {
19513                        return Err(StrykeError::runtime(
19514                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
19515                            line,
19516                        ));
19517                    };
19518                    let binds = self
19519                        .match_array_pattern_elems(&arr, elems, line)
19520                        .map_err(|e| match e {
19521                            FlowOrError::Error(stryke) => stryke,
19522                            FlowOrError::Flow(_) => StrykeError::runtime(
19523                                "unexpected flow in method array destruct",
19524                                line,
19525                            ),
19526                        })?;
19527                    let Some(binds) = binds else {
19528                        return Err(StrykeError::runtime(
19529                            format!(
19530                                "method parameter: array destructure failed at position {}",
19531                                i
19532                            ),
19533                            line,
19534                        ));
19535                    };
19536                    for b in binds {
19537                        match b {
19538                            PatternBinding::Scalar(name, v) => {
19539                                let n = self.english_scalar_name(&name);
19540                                self.scope.declare_scalar(n, v);
19541                            }
19542                            PatternBinding::Array(name, elems) => {
19543                                self.scope.declare_array(&name, elems);
19544                            }
19545                        }
19546                    }
19547                }
19548                SubSigParam::HashDestruct(pairs) => {
19549                    let arg = argv.get(i).cloned().unwrap_or(StrykeValue::UNDEF);
19550                    i += 1;
19551                    let map = self.hash_for_signature_destruct(&arg, line)?;
19552                    for (key, varname) in pairs {
19553                        let v = map.get(key).cloned().unwrap_or(StrykeValue::UNDEF);
19554                        let n = self.english_scalar_name(varname);
19555                        self.scope.declare_scalar(n, v);
19556                    }
19557                }
19558            }
19559        }
19560        Ok(())
19561    }
19562
19563    fn builtin_new(&mut self, class: &str, args: Vec<StrykeValue>, line: usize) -> ExecResult {
19564        if class == "Set" {
19565            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
19566        }
19567        if let Some(def) = self.struct_defs.get(class).cloned() {
19568            let mut provided = Vec::new();
19569            let mut i = 1;
19570            while i + 1 < args.len() {
19571                let k = args[i].to_string();
19572                let v = args[i + 1].clone();
19573                provided.push((k, v));
19574                i += 2;
19575            }
19576            let mut defaults = Vec::with_capacity(def.fields.len());
19577            for field in &def.fields {
19578                if let Some(ref expr) = field.default {
19579                    let val = self.eval_expr(expr)?;
19580                    defaults.push(Some(val));
19581                } else {
19582                    defaults.push(None);
19583                }
19584            }
19585            return Ok(crate::native_data::struct_new_with_defaults(
19586                &def, &provided, &defaults, line,
19587            )?);
19588        }
19589        // Stryke `class` declarations route through `class_construct` so the
19590        // result is a real `ClassInstance` (typed-my checks, isa walk, BUILD
19591        // hooks, etc.). Without this, `Class->new` for a registered class
19592        // fell through to the default Perl-style blessed-hashref path,
19593        // breaking `typed my $x : Class = Class->new` even though the
19594        // runtime check for `Struct(name)` was already in place. Skip
19595        // `args[0]` (the class-name receiver) since `class_construct`
19596        // expects user args only.
19597        if let Some(def) = self.class_defs.get(class).cloned() {
19598            let user_args: Vec<StrykeValue> = args.into_iter().skip(1).collect();
19599            return self.class_construct(&def, user_args, line);
19600        }
19601        // Default OO constructor: Class->new(%args) → bless {%args}, class
19602        let mut map = IndexMap::new();
19603        let mut i = 1; // skip $self (first arg is class name)
19604        while i + 1 < args.len() {
19605            let k = args[i].to_string();
19606            let v = args[i + 1].clone();
19607            map.insert(k, v);
19608            i += 2;
19609        }
19610        Ok(StrykeValue::blessed(Arc::new(
19611            crate::value::BlessedRef::new_blessed(class.to_string(), StrykeValue::hash(map)),
19612        )))
19613    }
19614
19615    fn exec_print(
19616        &mut self,
19617        handle: Option<&str>,
19618        args: &[Expr],
19619        newline: bool,
19620        line: usize,
19621    ) -> ExecResult {
19622        if newline && (self.feature_bits & FEAT_SAY) == 0 {
19623            return Err(StrykeError::runtime(
19624                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
19625                line,
19626            )
19627            .into());
19628        }
19629        let mut output = String::new();
19630        if args.is_empty() {
19631            // Perl: print with no LIST prints $_ (same for say).
19632            let topic = self.scope.get_scalar("_").clone();
19633            let s = self.stringify_value(topic, line)?;
19634            output.push_str(&s);
19635        } else {
19636            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
19637            // between those top-level expressions only (not between elements of an expanded `@arr`).
19638            for (i, a) in args.iter().enumerate() {
19639                if i > 0 {
19640                    output.push_str(&self.ofs);
19641                }
19642                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
19643                for item in val.to_list() {
19644                    let s = self.stringify_value(item, line)?;
19645                    output.push_str(&s);
19646                }
19647            }
19648        }
19649        if newline {
19650            output.push('\n');
19651        }
19652        output.push_str(&self.ors);
19653
19654        let handle_name =
19655            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
19656        self.write_formatted_print(handle_name.as_str(), &output, line)?;
19657        Ok(StrykeValue::integer(1))
19658    }
19659
19660    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
19661        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
19662            // Perl: printf with no args uses $_ as the format string.
19663            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
19664            (s, &[])
19665        } else {
19666            (self.eval_expr(&args[0])?.to_string(), &args[1..])
19667        };
19668        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
19669        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
19670        // ranges to flip-flop values, so go through list-context eval and splat.
19671        let mut arg_vals = Vec::new();
19672        for a in rest {
19673            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
19674            if let Some(items) = v.as_array_vec() {
19675                arg_vals.extend(items);
19676            } else {
19677                arg_vals.push(v);
19678            }
19679        }
19680        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
19681        let handle_name =
19682            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
19683        match handle_name.as_str() {
19684            "STDOUT" => {
19685                if !self.suppress_stdout {
19686                    print!("{}", output);
19687                    if self.output_autoflush {
19688                        let _ = io::stdout().flush();
19689                    }
19690                }
19691            }
19692            "STDERR" => {
19693                eprint!("{}", output);
19694                let _ = io::stderr().flush();
19695            }
19696            name => {
19697                if let Some(writer) = self.output_handles.get_mut(name) {
19698                    let _ = writer.write_all(output.as_bytes());
19699                    if self.output_autoflush {
19700                        let _ = writer.flush();
19701                    }
19702                }
19703            }
19704        }
19705        Ok(StrykeValue::integer(1))
19706    }
19707
19708    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
19709    pub(crate) fn eval_substr_expr(
19710        &mut self,
19711        string: &Expr,
19712        offset: &Expr,
19713        length: Option<&Expr>,
19714        replacement: Option<&Expr>,
19715        _line: usize,
19716    ) -> Result<StrykeValue, FlowOrError> {
19717        let s = self.eval_expr(string)?.to_string();
19718        let off = self.eval_expr(offset)?.to_int();
19719        let start = if off < 0 {
19720            (s.len() as i64 + off).max(0) as usize
19721        } else {
19722            off as usize
19723        };
19724        let len = if let Some(l) = length {
19725            let len_val = self.eval_expr(l)?.to_int();
19726            if len_val < 0 {
19727                // Negative length: count from end of string
19728                let remaining = s.len().saturating_sub(start) as i64;
19729                (remaining + len_val).max(0) as usize
19730            } else {
19731                len_val as usize
19732            }
19733        } else {
19734            s.len().saturating_sub(start)
19735        };
19736        let end = start.saturating_add(len).min(s.len());
19737        let result = s.get(start..end).unwrap_or("").to_string();
19738        if let Some(rep) = replacement {
19739            let rep_s = self.eval_expr(rep)?.to_string();
19740            let mut new_s = String::new();
19741            new_s.push_str(&s[..start]);
19742            new_s.push_str(&rep_s);
19743            new_s.push_str(&s[end..]);
19744            self.assign_value(string, StrykeValue::string(new_s))?;
19745        }
19746        Ok(StrykeValue::string(result))
19747    }
19748
19749    pub(crate) fn eval_push_expr(
19750        &mut self,
19751        array: &Expr,
19752        values: &[Expr],
19753        line: usize,
19754    ) -> Result<StrykeValue, FlowOrError> {
19755        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19756            for v in values {
19757                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19758                self.push_array_deref_value(aref.clone(), val, line)?;
19759            }
19760            let len = self.array_deref_len(aref, line)?;
19761            return Ok(StrykeValue::integer(len));
19762        }
19763        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19764        if self.scope.is_array_frozen(&arr_name) {
19765            return Err(StrykeError::runtime(
19766                format!("Modification of a frozen value: @{}", arr_name),
19767                line,
19768            )
19769            .into());
19770        }
19771        for v in values {
19772            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19773            if let Some(items) = val.as_array_vec() {
19774                for item in items {
19775                    self.scope
19776                        .push_to_array(&arr_name, item)
19777                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19778                }
19779            } else {
19780                self.scope
19781                    .push_to_array(&arr_name, val)
19782                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19783            }
19784        }
19785        let len = self.scope.array_len(&arr_name);
19786        Ok(StrykeValue::integer(len as i64))
19787    }
19788
19789    pub(crate) fn eval_pop_expr(
19790        &mut self,
19791        array: &Expr,
19792        line: usize,
19793    ) -> Result<StrykeValue, FlowOrError> {
19794        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19795            return self.pop_array_deref(aref, line);
19796        }
19797        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19798        self.scope
19799            .pop_from_array(&arr_name)
19800            .map_err(|e| FlowOrError::Error(e.at_line(line)))
19801    }
19802
19803    pub(crate) fn eval_shift_expr(
19804        &mut self,
19805        array: &Expr,
19806        line: usize,
19807    ) -> Result<StrykeValue, FlowOrError> {
19808        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19809            return self.shift_array_deref(aref, line);
19810        }
19811        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19812        self.scope
19813            .shift_from_array(&arr_name)
19814            .map_err(|e| FlowOrError::Error(e.at_line(line)))
19815    }
19816
19817    pub(crate) fn eval_unshift_expr(
19818        &mut self,
19819        array: &Expr,
19820        values: &[Expr],
19821        line: usize,
19822    ) -> Result<StrykeValue, FlowOrError> {
19823        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19824            let mut vals = Vec::new();
19825            for v in values {
19826                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19827                if let Some(items) = val.as_array_vec() {
19828                    vals.extend(items);
19829                } else {
19830                    vals.push(val);
19831                }
19832            }
19833            let len = self.unshift_array_deref_multi(aref, vals, line)?;
19834            return Ok(StrykeValue::integer(len));
19835        }
19836        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19837        let mut vals = Vec::new();
19838        for v in values {
19839            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19840            if let Some(items) = val.as_array_vec() {
19841                vals.extend(items);
19842            } else {
19843                vals.push(val);
19844            }
19845        }
19846        let arr = self
19847            .scope
19848            .get_array_mut(&arr_name)
19849            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19850        for (i, v) in vals.into_iter().enumerate() {
19851            arr.insert(i, v);
19852        }
19853        let len = arr.len();
19854        Ok(StrykeValue::integer(len as i64))
19855    }
19856
19857    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
19858    pub(crate) fn push_array_deref_value(
19859        &mut self,
19860        arr_ref: StrykeValue,
19861        val: StrykeValue,
19862        line: usize,
19863    ) -> Result<(), FlowOrError> {
19864        // Resolve binding refs in the value being stored so they snapshot
19865        // the current scope data and survive scope pop.
19866        let val = self.scope.resolve_container_binding_ref(val);
19867        if let Some(r) = arr_ref.as_array_ref() {
19868            let mut w = r.write();
19869            if let Some(items) = val.as_array_vec() {
19870                w.extend(items.iter().cloned());
19871            } else {
19872                w.push(val);
19873            }
19874            return Ok(());
19875        }
19876        if let Some(name) = arr_ref.as_array_binding_name() {
19877            if let Some(items) = val.as_array_vec() {
19878                for item in items {
19879                    self.scope
19880                        .push_to_array(&name, item)
19881                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19882                }
19883            } else {
19884                self.scope
19885                    .push_to_array(&name, val)
19886                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19887            }
19888            return Ok(());
19889        }
19890        if let Some(s) = arr_ref.as_str() {
19891            if self.strict_refs {
19892                return Err(StrykeError::runtime(
19893                    format!(
19894                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19895                        s
19896                    ),
19897                    line,
19898                )
19899                .into());
19900            }
19901            let name = s.to_string();
19902            if let Some(items) = val.as_array_vec() {
19903                for item in items {
19904                    self.scope
19905                        .push_to_array(&name, item)
19906                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19907                }
19908            } else {
19909                self.scope
19910                    .push_to_array(&name, val)
19911                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19912            }
19913            return Ok(());
19914        }
19915        Err(StrykeError::runtime("push argument is not an ARRAY reference", line).into())
19916    }
19917
19918    pub(crate) fn array_deref_len(
19919        &self,
19920        arr_ref: StrykeValue,
19921        line: usize,
19922    ) -> Result<i64, FlowOrError> {
19923        if let Some(r) = arr_ref.as_array_ref() {
19924            return Ok(r.read().len() as i64);
19925        }
19926        if let Some(name) = arr_ref.as_array_binding_name() {
19927            return Ok(self.scope.array_len(&name) as i64);
19928        }
19929        if let Some(s) = arr_ref.as_str() {
19930            if self.strict_refs {
19931                return Err(StrykeError::runtime(
19932                    format!(
19933                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19934                        s
19935                    ),
19936                    line,
19937                )
19938                .into());
19939            }
19940            return Ok(self.scope.array_len(&s) as i64);
19941        }
19942        Err(StrykeError::runtime("argument is not an ARRAY reference", line).into())
19943    }
19944
19945    pub(crate) fn pop_array_deref(
19946        &mut self,
19947        arr_ref: StrykeValue,
19948        line: usize,
19949    ) -> Result<StrykeValue, FlowOrError> {
19950        if let Some(r) = arr_ref.as_array_ref() {
19951            let mut w = r.write();
19952            return Ok(w.pop().unwrap_or(StrykeValue::UNDEF));
19953        }
19954        if let Some(name) = arr_ref.as_array_binding_name() {
19955            return self
19956                .scope
19957                .pop_from_array(&name)
19958                .map_err(|e| FlowOrError::Error(e.at_line(line)));
19959        }
19960        if let Some(s) = arr_ref.as_str() {
19961            if self.strict_refs {
19962                return Err(StrykeError::runtime(
19963                    format!(
19964                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19965                        s
19966                    ),
19967                    line,
19968                )
19969                .into());
19970            }
19971            return self
19972                .scope
19973                .pop_from_array(&s)
19974                .map_err(|e| FlowOrError::Error(e.at_line(line)));
19975        }
19976        Err(StrykeError::runtime("pop argument is not an ARRAY reference", line).into())
19977    }
19978
19979    pub(crate) fn shift_array_deref(
19980        &mut self,
19981        arr_ref: StrykeValue,
19982        line: usize,
19983    ) -> Result<StrykeValue, FlowOrError> {
19984        if let Some(r) = arr_ref.as_array_ref() {
19985            let mut w = r.write();
19986            return Ok(if w.is_empty() {
19987                StrykeValue::UNDEF
19988            } else {
19989                w.remove(0)
19990            });
19991        }
19992        if let Some(name) = arr_ref.as_array_binding_name() {
19993            return self
19994                .scope
19995                .shift_from_array(&name)
19996                .map_err(|e| FlowOrError::Error(e.at_line(line)));
19997        }
19998        if let Some(s) = arr_ref.as_str() {
19999            if self.strict_refs {
20000                return Err(StrykeError::runtime(
20001                    format!(
20002                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20003                        s
20004                    ),
20005                    line,
20006                )
20007                .into());
20008            }
20009            return self
20010                .scope
20011                .shift_from_array(&s)
20012                .map_err(|e| FlowOrError::Error(e.at_line(line)));
20013        }
20014        Err(StrykeError::runtime("shift argument is not an ARRAY reference", line).into())
20015    }
20016
20017    pub(crate) fn unshift_array_deref_multi(
20018        &mut self,
20019        arr_ref: StrykeValue,
20020        vals: Vec<StrykeValue>,
20021        line: usize,
20022    ) -> Result<i64, FlowOrError> {
20023        let mut flat: Vec<StrykeValue> = Vec::new();
20024        for v in vals {
20025            if let Some(items) = v.as_array_vec() {
20026                flat.extend(items);
20027            } else {
20028                flat.push(v);
20029            }
20030        }
20031        if let Some(r) = arr_ref.as_array_ref() {
20032            let mut w = r.write();
20033            for (i, v) in flat.into_iter().enumerate() {
20034                w.insert(i, v);
20035            }
20036            return Ok(w.len() as i64);
20037        }
20038        if let Some(name) = arr_ref.as_array_binding_name() {
20039            let arr = self
20040                .scope
20041                .get_array_mut(&name)
20042                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20043            for (i, v) in flat.into_iter().enumerate() {
20044                arr.insert(i, v);
20045            }
20046            return Ok(arr.len() as i64);
20047        }
20048        if let Some(s) = arr_ref.as_str() {
20049            if self.strict_refs {
20050                return Err(StrykeError::runtime(
20051                    format!(
20052                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20053                        s
20054                    ),
20055                    line,
20056                )
20057                .into());
20058            }
20059            let name = s.to_string();
20060            let arr = self
20061                .scope
20062                .get_array_mut(&name)
20063                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20064            for (i, v) in flat.into_iter().enumerate() {
20065                arr.insert(i, v);
20066            }
20067            return Ok(arr.len() as i64);
20068        }
20069        Err(StrykeError::runtime("unshift argument is not an ARRAY reference", line).into())
20070    }
20071
20072    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
20073    /// / compiler wraps `splice` like other context-sensitive builtins).
20074    pub(crate) fn splice_array_deref(
20075        &mut self,
20076        aref: StrykeValue,
20077        offset_val: StrykeValue,
20078        length_val: StrykeValue,
20079        rep_vals: Vec<StrykeValue>,
20080        line: usize,
20081    ) -> Result<StrykeValue, FlowOrError> {
20082        let ctx = self.wantarray_kind;
20083        if let Some(r) = aref.as_array_ref() {
20084            let arr_len = r.read().len();
20085            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20086            let mut w = r.write();
20087            let removed: Vec<StrykeValue> = w.drain(off..end).collect();
20088            for (i, v) in rep_vals.into_iter().enumerate() {
20089                w.insert(off + i, v);
20090            }
20091            return Ok(match ctx {
20092                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20093                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20094            });
20095        }
20096        if let Some(name) = aref.as_array_binding_name() {
20097            let arr_len = self.scope.array_len(&name);
20098            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20099            let removed = self
20100                .scope
20101                .splice_in_place(&name, off, end, rep_vals)
20102                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20103            return Ok(match ctx {
20104                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20105                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20106            });
20107        }
20108        if let Some(s) = aref.as_str() {
20109            if self.strict_refs {
20110                return Err(StrykeError::runtime(
20111                    format!(
20112                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
20113                        s
20114                    ),
20115                    line,
20116                )
20117                .into());
20118            }
20119            let arr_len = self.scope.array_len(&s);
20120            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20121            let removed = self
20122                .scope
20123                .splice_in_place(&s, off, end, rep_vals)
20124                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20125            return Ok(match ctx {
20126                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20127                WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20128            });
20129        }
20130        Err(StrykeError::runtime("splice argument is not an ARRAY reference", line).into())
20131    }
20132
20133    pub(crate) fn eval_splice_expr(
20134        &mut self,
20135        array: &Expr,
20136        offset: Option<&Expr>,
20137        length: Option<&Expr>,
20138        replacement: &[Expr],
20139        ctx: WantarrayCtx,
20140        line: usize,
20141    ) -> Result<StrykeValue, FlowOrError> {
20142        if let Some(aref) = self.try_eval_array_deref_container(array)? {
20143            let offset_val = if let Some(o) = offset {
20144                self.eval_expr(o)?
20145            } else {
20146                StrykeValue::integer(0)
20147            };
20148            let length_val = if let Some(l) = length {
20149                self.eval_expr(l)?
20150            } else {
20151                StrykeValue::UNDEF
20152            };
20153            let mut rep_vals = Vec::new();
20154            for r in replacement {
20155                rep_vals.push(self.eval_expr(r)?);
20156            }
20157            let saved = self.wantarray_kind;
20158            self.wantarray_kind = ctx;
20159            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
20160            self.wantarray_kind = saved;
20161            return out;
20162        }
20163        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
20164        let arr_len = self.scope.array_len(&arr_name);
20165        let offset_val = if let Some(o) = offset {
20166            self.eval_expr(o)?
20167        } else {
20168            StrykeValue::integer(0)
20169        };
20170        let length_val = if let Some(l) = length {
20171            self.eval_expr(l)?
20172        } else {
20173            StrykeValue::UNDEF
20174        };
20175        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
20176        let mut rep_vals = Vec::new();
20177        for r in replacement {
20178            rep_vals.push(self.eval_expr(r)?);
20179        }
20180        let removed = self
20181            .scope
20182            .splice_in_place(&arr_name, off, end, rep_vals)
20183            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
20184        Ok(match ctx {
20185            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(StrykeValue::UNDEF),
20186            WantarrayCtx::List | WantarrayCtx::Void => StrykeValue::array(removed),
20187        })
20188    }
20189
20190    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
20191    pub(crate) fn keys_from_value(
20192        val: StrykeValue,
20193        line: usize,
20194    ) -> Result<StrykeValue, FlowOrError> {
20195        if let Some(h) = val.as_hash_map() {
20196            Ok(StrykeValue::array(
20197                h.keys().map(|k| StrykeValue::string(k.clone())).collect(),
20198            ))
20199        } else if let Some(r) = val.as_hash_ref() {
20200            Ok(StrykeValue::array(
20201                r.read()
20202                    .keys()
20203                    .map(|k| StrykeValue::string(k.clone()))
20204                    .collect(),
20205            ))
20206        } else {
20207            Err(StrykeError::runtime("keys requires hash", line).into())
20208        }
20209    }
20210
20211    pub(crate) fn eval_keys_expr(
20212        &mut self,
20213        expr: &Expr,
20214        line: usize,
20215    ) -> Result<StrykeValue, FlowOrError> {
20216        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
20217        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
20218        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
20219        Self::keys_from_value(val, line)
20220    }
20221
20222    /// Result of `values EXPR` after `EXPR` has been evaluated.
20223    pub(crate) fn values_from_value(
20224        val: StrykeValue,
20225        line: usize,
20226    ) -> Result<StrykeValue, FlowOrError> {
20227        if let Some(h) = val.as_hash_map() {
20228            Ok(StrykeValue::array(h.values().cloned().collect()))
20229        } else if let Some(r) = val.as_hash_ref() {
20230            Ok(StrykeValue::array(r.read().values().cloned().collect()))
20231        } else {
20232            Err(StrykeError::runtime("values requires hash", line).into())
20233        }
20234    }
20235
20236    pub(crate) fn eval_values_expr(
20237        &mut self,
20238        expr: &Expr,
20239        line: usize,
20240    ) -> Result<StrykeValue, FlowOrError> {
20241        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
20242        Self::values_from_value(val, line)
20243    }
20244
20245    pub(crate) fn eval_delete_operand(
20246        &mut self,
20247        expr: &Expr,
20248        line: usize,
20249    ) -> Result<StrykeValue, FlowOrError> {
20250        match &expr.kind {
20251            ExprKind::HashElement { hash, key } => {
20252                let k = self.eval_expr(key)?.to_string();
20253                self.touch_env_hash(hash);
20254                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
20255                    let class = obj
20256                        .as_blessed_ref()
20257                        .map(|b| b.class.clone())
20258                        .unwrap_or_default();
20259                    let full = format!("{}::DELETE", class);
20260                    if let Some(sub) = self.subs.get(&full).cloned() {
20261                        return self.call_sub(
20262                            &sub,
20263                            vec![obj, StrykeValue::string(k)],
20264                            WantarrayCtx::Scalar,
20265                            line,
20266                        );
20267                    }
20268                }
20269                self.scope
20270                    .delete_hash_element(hash, &k)
20271                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
20272            }
20273            ExprKind::ArrayElement { array, index } => {
20274                self.check_strict_array_var(array, line)?;
20275                let idx = self.eval_expr(index)?.to_int();
20276                let aname = self.stash_array_name_for_package(array);
20277                self.scope
20278                    .delete_array_element(&aname, idx)
20279                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
20280            }
20281            ExprKind::ArrowDeref {
20282                expr: inner,
20283                index,
20284                kind: DerefKind::Hash,
20285            } => {
20286                let k = self.eval_expr(index)?.to_string();
20287                let container = self.eval_expr(inner)?;
20288                self.delete_arrow_hash_element(container, &k, line)
20289                    .map_err(Into::into)
20290            }
20291            ExprKind::ArrowDeref {
20292                expr: inner,
20293                index,
20294                kind: DerefKind::Array,
20295            } => {
20296                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
20297                    return Err(StrykeError::runtime(
20298                        "delete on array element needs scalar subscript",
20299                        line,
20300                    )
20301                    .into());
20302                }
20303                let container = self.eval_expr(inner)?;
20304                let idx = self.eval_expr(index)?.to_int();
20305                self.delete_arrow_array_element(container, idx, line)
20306                    .map_err(Into::into)
20307            }
20308            _ => Err(StrykeError::runtime("delete requires hash or array element", line).into()),
20309        }
20310    }
20311
20312    /// Evaluate a deref-chain in "exists mode" — like [`Self::eval_expr`] but
20313    /// recursively walks `ArrowDeref` chains and turns undef-intermediate
20314    /// derefs into undef (instead of erroring). Used by
20315    /// [`Self::eval_exists_operand`] so `exists $h{x}{y}{z}` returns 0 for
20316    /// any missing level. (BUG-009)
20317    fn eval_expr_exists_mode(&mut self, expr: &Expr) -> Result<StrykeValue, FlowOrError> {
20318        match &expr.kind {
20319            ExprKind::ArrowDeref {
20320                expr: inner,
20321                index,
20322                kind: DerefKind::Hash,
20323            } => {
20324                let inner_val = self.eval_expr_exists_mode(inner)?;
20325                if inner_val.is_undef() {
20326                    return Ok(StrykeValue::UNDEF);
20327                }
20328                if let Some(r) = inner_val.as_hash_ref() {
20329                    let k = self.eval_expr(index)?.to_string();
20330                    return Ok(r.read().get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
20331                }
20332                if let Some(b) = inner_val.as_blessed_ref() {
20333                    let data = b.data.read();
20334                    if let Some(r) = data.as_hash_ref() {
20335                        let k = self.eval_expr(index)?.to_string();
20336                        return Ok(r.read().get(&k).cloned().unwrap_or(StrykeValue::UNDEF));
20337                    }
20338                }
20339                // Struct / class instance — look up the field by name and
20340                // return its value. Without this, `exists $struct->{f}->{k}`
20341                // soft-fails to false even when the field is a real hashref.
20342                if let Some(s) = inner_val.as_struct_inst() {
20343                    let k = self.eval_expr(index)?.to_string();
20344                    if let Some(idx) = s.def.field_index(&k) {
20345                        return Ok(s.get_field(idx).unwrap_or(StrykeValue::UNDEF));
20346                    }
20347                    return Ok(StrykeValue::UNDEF);
20348                }
20349                if let Some(c) = inner_val.as_class_inst() {
20350                    let k = self.eval_expr(index)?.to_string();
20351                    if let Some(idx) = c.def.field_index(&k) {
20352                        return Ok(c.get_field(idx).unwrap_or(StrykeValue::UNDEF));
20353                    }
20354                    return Ok(StrykeValue::UNDEF);
20355                }
20356                Ok(StrykeValue::UNDEF)
20357            }
20358            ExprKind::ArrowDeref {
20359                expr: inner,
20360                index,
20361                kind: DerefKind::Array,
20362            } => {
20363                let inner_val = self.eval_expr_exists_mode(inner)?;
20364                if inner_val.is_undef() {
20365                    return Ok(StrykeValue::UNDEF);
20366                }
20367                if let Some(r) = inner_val.as_array_ref() {
20368                    let idx = self.eval_expr(index)?.to_int();
20369                    let arr = r.read();
20370                    let i = if idx < 0 {
20371                        (arr.len() as i64 + idx).max(0) as usize
20372                    } else {
20373                        idx as usize
20374                    };
20375                    return Ok(arr.get(i).cloned().unwrap_or(StrykeValue::UNDEF));
20376                }
20377                Ok(StrykeValue::UNDEF)
20378            }
20379            _ => self.eval_expr(expr),
20380        }
20381    }
20382
20383    pub(crate) fn eval_exists_operand(
20384        &mut self,
20385        expr: &Expr,
20386        line: usize,
20387    ) -> Result<StrykeValue, FlowOrError> {
20388        match &expr.kind {
20389            ExprKind::HashElement { hash, key } => {
20390                let k = self.eval_expr(key)?.to_string();
20391                self.touch_env_hash(hash);
20392                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
20393                    let class = obj
20394                        .as_blessed_ref()
20395                        .map(|b| b.class.clone())
20396                        .unwrap_or_default();
20397                    let full = format!("{}::EXISTS", class);
20398                    if let Some(sub) = self.subs.get(&full).cloned() {
20399                        return self.call_sub(
20400                            &sub,
20401                            vec![obj, StrykeValue::string(k)],
20402                            WantarrayCtx::Scalar,
20403                            line,
20404                        );
20405                    }
20406                }
20407                Ok(StrykeValue::integer(
20408                    if self.scope.exists_hash_element(hash, &k) {
20409                        1
20410                    } else {
20411                        0
20412                    },
20413                ))
20414            }
20415            ExprKind::ArrayElement { array, index } => {
20416                self.check_strict_array_var(array, line)?;
20417                let idx = self.eval_expr(index)?.to_int();
20418                let aname = self.stash_array_name_for_package(array);
20419                Ok(StrykeValue::integer(
20420                    if self.scope.exists_array_element(&aname, idx) {
20421                        1
20422                    } else {
20423                        0
20424                    },
20425                ))
20426            }
20427            ExprKind::ArrowDeref {
20428                expr: inner,
20429                index,
20430                kind: DerefKind::Hash,
20431            } => {
20432                let k = self.eval_expr(index)?.to_string();
20433                // Evaluate the chain in "exists mode" — undef intermediates
20434                // propagate as undef instead of erroring on missing-key
20435                // deref, matching Perl's `exists $h{x}{y}{z}` returning 0
20436                // for any missing level. (BUG-009)
20437                let container = match self.eval_expr_exists_mode(inner) {
20438                    Ok(v) => v,
20439                    Err(_) => return Ok(StrykeValue::integer(0)),
20440                };
20441                if container.is_undef() {
20442                    return Ok(StrykeValue::integer(0));
20443                }
20444                let yes = self.exists_arrow_hash_element(container, &k, line)?;
20445                Ok(StrykeValue::integer(if yes { 1 } else { 0 }))
20446            }
20447            ExprKind::ArrowDeref {
20448                expr: inner,
20449                index,
20450                kind: DerefKind::Array,
20451            } => {
20452                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
20453                    return Err(StrykeError::runtime(
20454                        "exists on array element needs scalar subscript",
20455                        line,
20456                    )
20457                    .into());
20458                }
20459                let container = match self.eval_expr_exists_mode(inner) {
20460                    Ok(v) => v,
20461                    Err(_) => return Ok(StrykeValue::integer(0)),
20462                };
20463                if container.is_undef() {
20464                    return Ok(StrykeValue::integer(0));
20465                }
20466                let idx = self.eval_expr(index)?.to_int();
20467                let yes = self.exists_arrow_array_element(container, idx, line)?;
20468                Ok(StrykeValue::integer(if yes { 1 } else { 0 }))
20469            }
20470            _ => Err(StrykeError::runtime("exists requires hash or array element", line).into()),
20471        }
20472    }
20473
20474    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
20475    ///
20476    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
20477    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
20478    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
20479    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
20480    /// the new path amortizes the handshake across the whole map.
20481    pub(crate) fn eval_pmap_remote(
20482        &mut self,
20483        cluster_pv: StrykeValue,
20484        list_pv: StrykeValue,
20485        show_progress: bool,
20486        block: &Block,
20487        flat_outputs: bool,
20488        line: usize,
20489    ) -> Result<StrykeValue, FlowOrError> {
20490        let Some(cluster) = cluster_pv.as_remote_cluster() else {
20491            return Err(StrykeError::runtime("pmap_on: expected cluster(...) value", line).into());
20492        };
20493        let items = list_pv.to_list();
20494        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20495        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
20496            return Err(StrykeError::runtime(
20497                "pmap_on: mysync/atomic capture is not supported for remote workers",
20498                line,
20499            )
20500            .into());
20501        }
20502        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
20503            .map_err(|e| StrykeError::runtime(e, line))?;
20504        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
20505        let block_src = crate::fmt::format_block(block);
20506        let item_jsons = crate::cluster::perl_items_to_json(&items)
20507            .map_err(|e| StrykeError::runtime(e, line))?;
20508
20509        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
20510        // synchronous from the caller's POV, so we drive the bar before/after the call.
20511        let pmap_progress = PmapProgress::new(show_progress, items.len());
20512        let result_values =
20513            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
20514                .map_err(|e| StrykeError::runtime(format!("pmap_on remote: {e}"), line))?;
20515        for _ in 0..result_values.len() {
20516            pmap_progress.tick();
20517        }
20518        pmap_progress.finish();
20519
20520        if flat_outputs {
20521            let flattened: Vec<StrykeValue> = result_values
20522                .into_iter()
20523                .flat_map(|v| v.map_flatten_outputs(true))
20524                .collect();
20525            Ok(StrykeValue::array(flattened))
20526        } else {
20527            Ok(StrykeValue::array(result_values))
20528        }
20529    }
20530
20531    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
20532    pub(crate) fn eval_par_lines_expr(
20533        &mut self,
20534        path: &Expr,
20535        callback: &Expr,
20536        progress: Option<&Expr>,
20537        line: usize,
20538    ) -> Result<StrykeValue, FlowOrError> {
20539        let show_progress = progress
20540            .map(|p| self.eval_expr(p))
20541            .transpose()?
20542            .map(|v| v.is_true())
20543            .unwrap_or(false);
20544        let raw = self.eval_expr(path)?.to_string();
20545        let path_s = self.resolve_stryke_path_string(&raw);
20546        let cb_val = self.eval_expr(callback)?;
20547        let sub = if let Some(s) = cb_val.as_code_ref() {
20548            s
20549        } else {
20550            return Err(StrykeError::runtime(
20551                "par_lines: second argument must be a code reference",
20552                line,
20553            )
20554            .into());
20555        };
20556        let subs = self.subs.clone();
20557        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20558        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
20559            FlowOrError::Error(StrykeError::runtime(format!("par_lines: {}", e), line))
20560        })?;
20561        let mmap = unsafe {
20562            memmap2::Mmap::map(&file).map_err(|e| {
20563                FlowOrError::Error(StrykeError::runtime(
20564                    format!("par_lines: mmap: {}", e),
20565                    line,
20566                ))
20567            })?
20568        };
20569        let data: &[u8] = &mmap;
20570        if data.is_empty() {
20571            return Ok(StrykeValue::UNDEF);
20572        }
20573        let line_total = crate::par_lines::line_count_bytes(data);
20574        let pmap_progress = PmapProgress::new(show_progress, line_total);
20575        if self.num_threads == 0 {
20576            self.num_threads = rayon::current_num_threads();
20577        }
20578        let num_chunks = self.num_threads.saturating_mul(8).max(1);
20579        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
20580        chunks.into_par_iter().try_for_each(|(start, end)| {
20581            let slice = &data[start..end];
20582            let mut s = 0usize;
20583            while s < slice.len() {
20584                let e = slice[s..]
20585                    .iter()
20586                    .position(|&b| b == b'\n')
20587                    .map(|p| s + p)
20588                    .unwrap_or(slice.len());
20589                let line_bytes = &slice[s..e];
20590                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
20591                let mut local_interp = VMHelper::new();
20592                local_interp.subs = subs.clone();
20593                local_interp.scope.restore_capture(&scope_capture);
20594                local_interp
20595                    .scope
20596                    .restore_atomics(&atomic_arrays, &atomic_hashes);
20597                local_interp.enable_parallel_guard();
20598                local_interp.scope.set_topic(StrykeValue::string(line_str));
20599                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
20600                    Ok(_) => {}
20601                    Err(e) => return Err(e),
20602                }
20603                pmap_progress.tick();
20604                if e >= slice.len() {
20605                    break;
20606                }
20607                s = e + 1;
20608            }
20609            Ok(())
20610        })?;
20611        pmap_progress.finish();
20612        Ok(StrykeValue::UNDEF)
20613    }
20614
20615    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
20616    pub(crate) fn eval_par_walk_expr(
20617        &mut self,
20618        path: &Expr,
20619        callback: &Expr,
20620        progress: Option<&Expr>,
20621        line: usize,
20622    ) -> Result<StrykeValue, FlowOrError> {
20623        let show_progress = progress
20624            .map(|p| self.eval_expr(p))
20625            .transpose()?
20626            .map(|v| v.is_true())
20627            .unwrap_or(false);
20628        let path_val = self.eval_expr(path)?;
20629        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
20630            arr.into_iter()
20631                .map(|v| PathBuf::from(v.to_string()))
20632                .collect()
20633        } else {
20634            vec![PathBuf::from(path_val.to_string())]
20635        };
20636        let cb_val = self.eval_expr(callback)?;
20637        let sub = if let Some(s) = cb_val.as_code_ref() {
20638            s
20639        } else {
20640            return Err(StrykeError::runtime(
20641                "par_walk: second argument must be a code reference",
20642                line,
20643            )
20644            .into());
20645        };
20646        let subs = self.subs.clone();
20647        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20648
20649        if show_progress {
20650            let paths = crate::par_walk::collect_paths(&roots);
20651            let pmap_progress = PmapProgress::new(true, paths.len());
20652            paths.into_par_iter().try_for_each(|p| {
20653                let s = p.to_string_lossy().into_owned();
20654                let mut local_interp = VMHelper::new();
20655                local_interp.subs = subs.clone();
20656                local_interp.scope.restore_capture(&scope_capture);
20657                local_interp
20658                    .scope
20659                    .restore_atomics(&atomic_arrays, &atomic_hashes);
20660                local_interp.enable_parallel_guard();
20661                local_interp.scope.set_topic(StrykeValue::string(s));
20662                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
20663                    Ok(_) => {}
20664                    Err(e) => return Err(e),
20665                }
20666                pmap_progress.tick();
20667                Ok(())
20668            })?;
20669            pmap_progress.finish();
20670        } else {
20671            for r in &roots {
20672                par_walk_recursive(
20673                    r.as_path(),
20674                    &sub,
20675                    &subs,
20676                    &scope_capture,
20677                    &atomic_arrays,
20678                    &atomic_hashes,
20679                    line,
20680                )?;
20681            }
20682        }
20683        Ok(StrykeValue::UNDEF)
20684    }
20685
20686    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
20687    pub(crate) fn builtin_par_sed(
20688        &mut self,
20689        args: &[StrykeValue],
20690        line: usize,
20691        has_progress: bool,
20692    ) -> StrykeResult<StrykeValue> {
20693        let show_progress = if has_progress {
20694            args.last().map(|v| v.is_true()).unwrap_or(false)
20695        } else {
20696            false
20697        };
20698        let slice = if has_progress {
20699            &args[..args.len().saturating_sub(1)]
20700        } else {
20701            args
20702        };
20703        if slice.len() < 3 {
20704            return Err(StrykeError::runtime(
20705                "par_sed: need pattern, replacement, and at least one file path",
20706                line,
20707            ));
20708        }
20709        let pat_val = &slice[0];
20710        let repl = slice[1].to_string();
20711        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
20712
20713        let re = if let Some(rx) = pat_val.as_regex() {
20714            rx
20715        } else {
20716            let pattern = pat_val.to_string();
20717            match self.compile_regex(&pattern, "g", line) {
20718                Ok(r) => r,
20719                Err(FlowOrError::Error(e)) => return Err(e),
20720                Err(FlowOrError::Flow(f)) => {
20721                    return Err(StrykeError::runtime(format!("par_sed: {:?}", f), line))
20722                }
20723            }
20724        };
20725
20726        let pmap = PmapProgress::new(show_progress, files.len());
20727        let touched = AtomicUsize::new(0);
20728        files.par_iter().try_for_each(|path| {
20729            let content = read_file_text_perl_compat(path)
20730                .map_err(|e| StrykeError::runtime(format!("par_sed {}: {}", path, e), line))?;
20731            let new_s = re.replace_all(&content, &repl);
20732            if new_s != content {
20733                std::fs::write(path, new_s.as_bytes())
20734                    .map_err(|e| StrykeError::runtime(format!("par_sed {}: {}", path, e), line))?;
20735                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
20736            }
20737            pmap.tick();
20738            Ok(())
20739        })?;
20740        pmap.finish();
20741        Ok(StrykeValue::integer(
20742            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
20743        ))
20744    }
20745
20746    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
20747    pub(crate) fn eval_pwatch_expr(
20748        &mut self,
20749        path: &Expr,
20750        callback: &Expr,
20751        line: usize,
20752    ) -> Result<StrykeValue, FlowOrError> {
20753        let pattern_s = self.eval_expr(path)?.to_string();
20754        let cb_val = self.eval_expr(callback)?;
20755        let sub = if let Some(s) = cb_val.as_code_ref() {
20756            s
20757        } else {
20758            return Err(StrykeError::runtime(
20759                "pwatch: second argument must be a code reference",
20760                line,
20761            )
20762            .into());
20763        };
20764        let subs = self.subs.clone();
20765        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20766        crate::pwatch::run_pwatch(
20767            &pattern_s,
20768            sub,
20769            subs,
20770            scope_capture,
20771            atomic_arrays,
20772            atomic_hashes,
20773            line,
20774        )
20775        .map_err(FlowOrError::Error)
20776    }
20777
20778    /// Interpolate `$var` in s/// replacement strings, preserving numeric backrefs ($1, $2, etc.).
20779    fn interpolate_replacement_string(&self, replacement: &str) -> String {
20780        let mut out = String::with_capacity(replacement.len());
20781        let chars: Vec<char> = replacement.chars().collect();
20782        let mut i = 0;
20783        while i < chars.len() {
20784            if chars[i] == '\\' && i + 1 < chars.len() {
20785                out.push(chars[i]);
20786                out.push(chars[i + 1]);
20787                i += 2;
20788                continue;
20789            }
20790            if chars[i] == '$' && i + 1 < chars.len() {
20791                let start = i;
20792                i += 1;
20793                if chars[i].is_ascii_digit() {
20794                    out.push('$');
20795                    while i < chars.len() && chars[i].is_ascii_digit() {
20796                        out.push(chars[i]);
20797                        i += 1;
20798                    }
20799                    continue;
20800                }
20801                if chars[i] == '&' || chars[i] == '`' || chars[i] == '\'' {
20802                    out.push('$');
20803                    out.push(chars[i]);
20804                    i += 1;
20805                    continue;
20806                }
20807                if !chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{' {
20808                    out.push('$');
20809                    continue;
20810                }
20811                let mut name = String::new();
20812                if chars[i] == '{' {
20813                    i += 1;
20814                    while i < chars.len() && chars[i] != '}' {
20815                        name.push(chars[i]);
20816                        i += 1;
20817                    }
20818                    if i < chars.len() {
20819                        i += 1;
20820                    }
20821                } else {
20822                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20823                        name.push(chars[i]);
20824                        i += 1;
20825                    }
20826                }
20827                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
20828                    let val = self.scope.get_scalar(&name);
20829                    out.push_str(&val.to_string());
20830                } else if !name.is_empty() {
20831                    out.push_str(&replacement[start..i]);
20832                } else {
20833                    out.push('$');
20834                }
20835                continue;
20836            }
20837            out.push(chars[i]);
20838            i += 1;
20839        }
20840        out
20841    }
20842
20843    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
20844    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
20845        let mut out = String::with_capacity(pattern.len());
20846        let chars: Vec<char> = pattern.chars().collect();
20847        let mut i = 0;
20848        while i < chars.len() {
20849            if chars[i] == '\\' && i + 1 < chars.len() {
20850                // Preserve escape sequences (including \$ which is literal $)
20851                out.push(chars[i]);
20852                out.push(chars[i + 1]);
20853                i += 2;
20854                continue;
20855            }
20856            if chars[i] == '$' && i + 1 < chars.len() {
20857                i += 1;
20858                // `$` at end of pattern is an anchor, not a variable
20859                if i >= chars.len()
20860                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
20861                {
20862                    out.push('$');
20863                    continue;
20864                }
20865                let mut name = String::new();
20866                if chars[i] == '{' {
20867                    i += 1;
20868                    while i < chars.len() && chars[i] != '}' {
20869                        name.push(chars[i]);
20870                        i += 1;
20871                    }
20872                    if i < chars.len() {
20873                        i += 1;
20874                    } // skip }
20875                } else {
20876                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20877                        name.push(chars[i]);
20878                        i += 1;
20879                    }
20880                }
20881                if !name.is_empty() {
20882                    let val = self.scope.get_scalar(&name);
20883                    out.push_str(&val.to_string());
20884                } else {
20885                    out.push('$');
20886                }
20887                continue;
20888            }
20889            out.push(chars[i]);
20890            i += 1;
20891        }
20892        out
20893    }
20894
20895    pub(crate) fn compile_regex(
20896        &mut self,
20897        pattern: &str,
20898        flags: &str,
20899        line: usize,
20900    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
20901        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
20902        let pattern = if pattern.contains('$') || pattern.contains('@') {
20903            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
20904        } else {
20905            std::borrow::Cow::Borrowed(pattern)
20906        };
20907        let pattern = pattern.as_ref();
20908        // Fast path: same regex as last call (common in loops).
20909        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
20910        let multiline = self.multiline_match;
20911        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
20912            if lp == pattern && lf == flags && *lm == multiline {
20913                return Ok(lr.clone());
20914            }
20915        }
20916        // Slow path: HashMap lookup
20917        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
20918        if let Some(cached) = self.regex_cache.get(&key) {
20919            self.regex_last = Some((
20920                pattern.to_string(),
20921                flags.to_string(),
20922                multiline,
20923                cached.clone(),
20924            ));
20925            return Ok(cached.clone());
20926        }
20927        let expanded = expand_perl_regex_quotemeta(pattern);
20928        let expanded = expand_perl_regex_octal_escapes(&expanded);
20929        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
20930        let mut re_str = String::new();
20931        if flags.contains('i') {
20932            re_str.push_str("(?i)");
20933        }
20934        if flags.contains('s') {
20935            re_str.push_str("(?s)");
20936        }
20937        if flags.contains('m') {
20938            re_str.push_str("(?m)");
20939        }
20940        if flags.contains('x') {
20941            re_str.push_str("(?x)");
20942        }
20943        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
20944        if multiline {
20945            re_str.push_str("(?s)");
20946        }
20947        re_str.push_str(&expanded);
20948        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
20949            FlowOrError::Error(StrykeError::runtime(
20950                format!("Invalid regex /{}/: {}", pattern, e),
20951                line,
20952            ))
20953        })?;
20954        let arc = re;
20955        self.regex_last = Some((
20956            pattern.to_string(),
20957            flags.to_string(),
20958            multiline,
20959            arc.clone(),
20960        ));
20961        self.regex_cache.insert(key, arc.clone());
20962        Ok(arc)
20963    }
20964
20965    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
20966    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
20967        if self.last_readline_handle.is_empty() {
20968            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
20969        }
20970        let n = *self
20971            .handle_line_numbers
20972            .get(&self.last_readline_handle)
20973            .unwrap_or(&0);
20974        if n <= 0 {
20975            return None;
20976        }
20977        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
20978        {
20979            return Some(("<>".to_string(), n));
20980        }
20981        if self.last_readline_handle == "STDIN" {
20982            return Some((self.last_stdin_die_bracket.clone(), n));
20983        }
20984        Some((format!("<{}>", self.last_readline_handle), n))
20985    }
20986
20987    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
20988    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
20989        let mut s = format!(" at {} line {}", self.file, source_line);
20990        if let Some((bracket, n)) = self.die_warn_io_annotation() {
20991            s.push_str(&format!(", {} line {}.", bracket, n));
20992        } else {
20993            s.push('.');
20994        }
20995        s
20996    }
20997
20998    /// Process a line in -n/-p mode via the VM.
20999    ///
21000    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
21001    /// file so `eof` with no arguments matches Perl behavior on that line.
21002    pub fn process_line(
21003        &mut self,
21004        line_str: &str,
21005        _program: &Program,
21006        is_last_input_line: bool,
21007    ) -> StrykeResult<Option<String>> {
21008        let chunk = self
21009            .line_mode_chunk
21010            .as_ref()
21011            .expect("process_line called without compiled chunk — execute() must run first")
21012            .clone();
21013        crate::run_line_body(&chunk, self, line_str, is_last_input_line)
21014    }
21015}
21016
21017/// Mirrors `vm.rs::both_non_numeric_strings`. Used by the tree-walker's
21018/// `==` / `!=` to decide whether to fall back to string compare in
21019/// stryke non-compat mode.
21020fn both_non_numeric_strings_iv(a: &StrykeValue, b: &StrykeValue) -> bool {
21021    if !a.is_string_like() || !b.is_string_like() {
21022        return false;
21023    }
21024    let sa = a.to_string();
21025    let sb = b.to_string();
21026    let looks = |s: &str| {
21027        let t = s.trim();
21028        !t.is_empty() && t.parse::<f64>().is_ok()
21029    };
21030    !looks(&sa) && !looks(&sb)
21031}
21032
21033fn par_walk_invoke_entry(
21034    path: &Path,
21035    sub: &Arc<StrykeSub>,
21036    subs: &HashMap<String, Arc<StrykeSub>>,
21037    scope_capture: &[(String, StrykeValue)],
21038    atomic_arrays: &[(String, crate::scope::AtomicArray)],
21039    atomic_hashes: &[(String, crate::scope::AtomicHash)],
21040    line: usize,
21041) -> Result<(), FlowOrError> {
21042    let s = path.to_string_lossy().into_owned();
21043    let mut local_interp = VMHelper::new();
21044    local_interp.subs = subs.clone();
21045    local_interp.scope.restore_capture(scope_capture);
21046    local_interp
21047        .scope
21048        .restore_atomics(atomic_arrays, atomic_hashes);
21049    local_interp.enable_parallel_guard();
21050    local_interp.scope.set_topic(StrykeValue::string(s));
21051    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
21052    Ok(())
21053}
21054
21055fn par_walk_recursive(
21056    path: &Path,
21057    sub: &Arc<StrykeSub>,
21058    subs: &HashMap<String, Arc<StrykeSub>>,
21059    scope_capture: &[(String, StrykeValue)],
21060    atomic_arrays: &[(String, crate::scope::AtomicArray)],
21061    atomic_hashes: &[(String, crate::scope::AtomicHash)],
21062    line: usize,
21063) -> Result<(), FlowOrError> {
21064    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
21065        return par_walk_invoke_entry(
21066            path,
21067            sub,
21068            subs,
21069            scope_capture,
21070            atomic_arrays,
21071            atomic_hashes,
21072            line,
21073        );
21074    }
21075    if !path.is_dir() {
21076        return Ok(());
21077    }
21078    par_walk_invoke_entry(
21079        path,
21080        sub,
21081        subs,
21082        scope_capture,
21083        atomic_arrays,
21084        atomic_hashes,
21085        line,
21086    )?;
21087    let read = match std::fs::read_dir(path) {
21088        Ok(r) => r,
21089        Err(_) => return Ok(()),
21090    };
21091    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
21092    entries.par_iter().try_for_each(|e| {
21093        par_walk_recursive(
21094            &e.path(),
21095            sub,
21096            subs,
21097            scope_capture,
21098            atomic_arrays,
21099            atomic_hashes,
21100            line,
21101        )
21102    })?;
21103    Ok(())
21104}
21105
21106/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
21107/// Reformat Rust's `{:e}` / `{:E}` exponent style (`1.234568e4`) to the
21108/// Perl/C convention (`1.234568e+04`). Adds a sign character to the
21109/// exponent and zero-pads it to at least two digits.
21110/// Perl-style magical string increment.
21111///
21112/// Returns `Some(new)` when `s` matches `^[A-Za-z]+[0-9]*$` (i.e. some
21113/// letters, optionally followed by digits, ending at the end of string)
21114/// or is the empty string (which becomes `"1"`). Returns `None` for any
21115/// other shape — pure digits, leading whitespace, mixed letters/digits,
21116/// embedded punctuation, etc. — so the caller can fall back to a plain
21117/// numeric increment.
21118///
21119/// Carry rules:
21120/// - In the digit suffix, `9 -> 0` carries left.
21121/// - In the letter prefix, `z -> a` and `Z -> A` carry left.
21122/// - When a carry exits the leftmost letter, a fresh `a` or `A` is
21123///   prepended (case-matched to the first character of the original).
21124///
21125/// Split a `StrykeValue` into approximately `n_threads` chunks for the
21126/// `par { BLOCK }` runtime. Strings are partitioned on UTF-8 char-aligned
21127/// byte boundaries; arrays/lists on element boundaries. Other scalar
21128/// types (int, float, undef, ref) return a single-chunk Vec containing
21129/// the value unchanged — the caller should handle this fallback.
21130///
21131/// Returned chunks are themselves `StrykeValue` so the worker can bind
21132/// each to `$_` and invoke the user's block.
21133fn par_chunk_value(v: &StrykeValue, n_threads: usize) -> Vec<StrykeValue> {
21134    let n = n_threads.max(1);
21135    // String input: split on char boundaries.
21136    if let Some(s) = v.as_str() {
21137        let bytes = s.as_bytes();
21138        if bytes.len() < 16_384 || n < 2 {
21139            return vec![StrykeValue::string(s)];
21140        }
21141        let target = bytes.len().div_ceil(n);
21142        let mut splits = vec![0usize];
21143        let mut cursor = target;
21144        while cursor < bytes.len() {
21145            // Walk forward until we hit a UTF-8 leading byte (`0xxxxxxx` or `11xxxxxx`).
21146            while cursor < bytes.len() && (bytes[cursor] & 0xC0) == 0x80 {
21147                cursor += 1;
21148            }
21149            splits.push(cursor);
21150            cursor += target;
21151        }
21152        splits.push(bytes.len());
21153        return splits
21154            .windows(2)
21155            .map(|w| {
21156                let chunk = std::str::from_utf8(&bytes[w[0]..w[1]]).unwrap_or("");
21157                StrykeValue::string(chunk.to_string())
21158            })
21159            .collect();
21160    }
21161    // Array / list input: split on element boundaries.
21162    if let Some(arr) = v.as_array_vec() {
21163        if arr.len() < 32 || n < 2 {
21164            return vec![StrykeValue::array(arr)];
21165        }
21166        let target = arr.len().div_ceil(n);
21167        let mut chunks = Vec::with_capacity(n);
21168        for slice in arr.chunks(target) {
21169            chunks.push(StrykeValue::array(slice.to_vec()));
21170        }
21171        return chunks;
21172    }
21173    if let Some(arr_ref) = v.as_array_ref() {
21174        let arr = arr_ref.read().clone();
21175        if arr.len() < 32 || n < 2 {
21176            return vec![StrykeValue::array(arr)];
21177        }
21178        let target = arr.len().div_ceil(n);
21179        let mut chunks = Vec::with_capacity(n);
21180        for slice in arr.chunks(target) {
21181            chunks.push(StrykeValue::array(slice.to_vec()));
21182        }
21183        return chunks;
21184    }
21185    // Fallback: single chunk holding the original value.
21186    vec![v.clone()]
21187}
21188
21189/// Auto-merge a list of `par_reduce` per-chunk results when no explicit
21190/// reduce block is supplied. Picks the merger by inspecting the first
21191/// chunk's value type:
21192///
21193/// - **Hash with numeric values** → key-wise add (canonical histogram merge)
21194/// - **Number** → numeric `+`
21195/// - **Array / list** → concat
21196/// - **String** → concat
21197/// - **Anything else** → return chunks as a flat array (caller can post-process)
21198fn par_reduce_auto_merge(chunks: Vec<StrykeValue>) -> StrykeValue {
21199    if chunks.is_empty() {
21200        return StrykeValue::UNDEF;
21201    }
21202    let first = &chunks[0];
21203    // Hash<number> add-merge.
21204    if let Some(_h) = first.as_hash_ref() {
21205        let mut out: indexmap::IndexMap<String, f64> = indexmap::IndexMap::new();
21206        for chunk in &chunks {
21207            if let Some(hr) = chunk.as_hash_ref() {
21208                for (k, v) in hr.read().iter() {
21209                    *out.entry(k.clone()).or_insert(0.0) += v.to_number();
21210                }
21211            }
21212        }
21213        // Round-trip integer values back to integers so `freq`-style
21214        // hashes stay integer-typed downstream.
21215        let mut indexmap_out: indexmap::IndexMap<String, StrykeValue> = indexmap::IndexMap::new();
21216        for (k, v) in out {
21217            let pv = if v == v.trunc() && v.abs() < 1e15 {
21218                StrykeValue::integer(v as i64)
21219            } else {
21220                StrykeValue::float(v)
21221            };
21222            indexmap_out.insert(k, pv);
21223        }
21224        return StrykeValue::hash_ref(Arc::new(parking_lot::RwLock::new(indexmap_out)));
21225    }
21226    // Numeric add-merge (int or float).
21227    if first.is_integer_like() || first.is_float_like() {
21228        let s: f64 = chunks.iter().map(|v| v.to_number()).sum();
21229        if s == s.trunc() && s.abs() < 1e15 {
21230            return StrykeValue::integer(s as i64);
21231        }
21232        return StrykeValue::float(s);
21233    }
21234    // Array concat.
21235    if first.as_array_vec().is_some() || first.as_array_ref().is_some() {
21236        let mut out = Vec::new();
21237        for v in &chunks {
21238            out.extend(v.map_flatten_outputs(true));
21239        }
21240        return StrykeValue::array(out);
21241    }
21242    // String concat.
21243    if first.is_string_like() {
21244        let mut out = String::new();
21245        for v in &chunks {
21246            out.push_str(&v.to_string());
21247        }
21248        return StrykeValue::string(out);
21249    }
21250    // Fallback: flat list of chunk results.
21251    StrykeValue::array(chunks)
21252}
21253
21254/// Decrement has no magic counterpart in Perl 5; this helper is for `++`
21255/// only.
21256fn perl_magic_str_inc(s: &str) -> Option<String> {
21257    if s.is_empty() {
21258        return Some("1".to_string());
21259    }
21260    let bytes = s.as_bytes();
21261    let mut i = 0;
21262    while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
21263        i += 1;
21264    }
21265    let letters_end = i;
21266    while i < bytes.len() && bytes[i].is_ascii_digit() {
21267        i += 1;
21268    }
21269    if i != bytes.len() {
21270        return None;
21271    }
21272    if letters_end == 0 {
21273        // Pure digits: Perl handles these as plain numbers, so defer.
21274        return None;
21275    }
21276
21277    let mut result: Vec<u8> = bytes.to_vec();
21278    let mut carry = true;
21279    let mut idx = result.len();
21280
21281    // Phase 1: digits, right to left.
21282    while carry && idx > letters_end {
21283        idx -= 1;
21284        if result[idx] == b'9' {
21285            result[idx] = b'0';
21286            // carry stays true
21287        } else {
21288            result[idx] += 1;
21289            carry = false;
21290        }
21291    }
21292
21293    // Phase 2: letters, right to left.
21294    while carry && idx > 0 {
21295        idx -= 1;
21296        let c = result[idx];
21297        if c == b'z' {
21298            result[idx] = b'a';
21299        } else if c == b'Z' {
21300            result[idx] = b'A';
21301        } else {
21302            result[idx] += 1;
21303            carry = false;
21304        }
21305    }
21306
21307    // Phase 3: prepend a fresh letter if the carry escaped.
21308    if carry {
21309        let prepend = if bytes[0].is_ascii_uppercase() {
21310            b'A'
21311        } else {
21312            b'a'
21313        };
21314        let mut grown = Vec::with_capacity(result.len() + 1);
21315        grown.push(prepend);
21316        grown.extend_from_slice(&result);
21317        return String::from_utf8(grown).ok();
21318    }
21319
21320    String::from_utf8(result).ok()
21321}
21322
21323/// `++$x` semantics: try magic string increment first when the value is
21324/// already a string; fall back to a numeric +1 for everything else
21325/// (integers, floats, undef, plain numeric strings).
21326pub(crate) fn perl_inc(v: &StrykeValue) -> StrykeValue {
21327    if let Some(s) = v.as_str() {
21328        if let Some(new_s) = perl_magic_str_inc(&s) {
21329            return StrykeValue::string(new_s);
21330        }
21331    }
21332    StrykeValue::integer(v.to_int() + 1)
21333}
21334
21335fn perl_exponent_form(rust_repr: &str, upper: bool) -> String {
21336    let marker = if upper { 'E' } else { 'e' };
21337    if let Some(pos) = rust_repr.find(marker) {
21338        let (mantissa, after) = rust_repr.split_at(pos);
21339        let exp_part = &after[1..]; // skip the 'e' / 'E'
21340        let (sign, digits) = match exp_part.chars().next() {
21341            Some('+') => ("+", &exp_part[1..]),
21342            Some('-') => ("-", &exp_part[1..]),
21343            _ => ("+", exp_part),
21344        };
21345        let padded = if digits.len() < 2 {
21346            format!("0{}", digits)
21347        } else {
21348            digits.to_string()
21349        };
21350        return format!("{}{}{}{}", mantissa, marker, sign, padded);
21351    }
21352    rust_repr.to_string()
21353}
21354
21355/// Hex-float format (`%a` / `%A`). Produces strings like `0x1.8p+0` for
21356/// 1.5 — sign, normalized hex mantissa, then `p[+-]N` decimal exponent of
21357/// the radix-2 form. Matches C99 / POSIX `%a`.
21358fn perl_hex_float(n: f64, upper: bool) -> String {
21359    if n.is_nan() {
21360        return if upper { "NAN" } else { "nan" }.to_string();
21361    }
21362    if n.is_infinite() {
21363        let sign = if n.is_sign_negative() { "-" } else { "" };
21364        let body = if upper { "INF" } else { "inf" };
21365        return format!("{}{}", sign, body);
21366    }
21367    let prefix = if upper { "0X" } else { "0x" };
21368    let p_letter = if upper { 'P' } else { 'p' };
21369    let bits = n.to_bits();
21370    let sign_bit = bits >> 63;
21371    let exp_bits = (bits >> 52) & 0x7FF;
21372    let mant_bits = bits & 0x000F_FFFF_FFFF_FFFF;
21373    let sign_str = if sign_bit == 1 { "-" } else { "" };
21374    if exp_bits == 0 && mant_bits == 0 {
21375        return format!("{}{}{}{}{}", sign_str, prefix, "0", p_letter, "+0");
21376    }
21377    let (lead_digit, exp_unbiased): (u64, i32) = if exp_bits == 0 {
21378        // Subnormal: implicit leading 0, exponent fixed at -1022.
21379        (0, -1022)
21380    } else {
21381        (1, (exp_bits as i32) - 1023)
21382    };
21383    let exp_sign = if exp_unbiased >= 0 { "+" } else { "-" };
21384    let exp_abs = exp_unbiased.unsigned_abs();
21385    if mant_bits == 0 {
21386        return format!(
21387            "{}{}{}{}{}{}",
21388            sign_str, prefix, lead_digit, p_letter, exp_sign, exp_abs
21389        );
21390    }
21391    // 52 mantissa bits = 13 hex digits.
21392    let mant_hex = format!("{:013x}", mant_bits);
21393    let trimmed = mant_hex.trim_end_matches('0');
21394    let mant_str = if upper {
21395        trimmed.to_uppercase()
21396    } else {
21397        trimmed.to_string()
21398    };
21399    format!(
21400        "{}{}{}.{}{}{}{}",
21401        sign_str, prefix, lead_digit, mant_str, p_letter, exp_sign, exp_abs
21402    )
21403}
21404
21405/// Format a value with `%g`-style "shortest of %e or %f, strip trailing
21406/// zeros". Precision is the number of *significant* digits (default 6).
21407fn perl_g_form(n: f64, prec: usize, upper: bool) -> String {
21408    let prec = prec.max(1);
21409    if !n.is_finite() {
21410        return if upper {
21411            format!("{}", n).to_uppercase()
21412        } else {
21413            format!("{}", n)
21414        };
21415    }
21416    // Compute base-10 exponent.
21417    let abs = n.abs();
21418    let x = if abs == 0.0 {
21419        0i32
21420    } else {
21421        abs.log10().floor() as i32
21422    };
21423    // %g rule: use exponential form if x < -4 OR x >= prec.
21424    let use_e = x < -4 || x >= prec as i32;
21425    // Always work in lowercase-`e` form internally so the trim logic has a
21426    // single shape; upcase the marker letter at the end for `%G`.
21427    let formatted = if use_e {
21428        let raw = format!("{:.*e}", prec - 1, n);
21429        perl_exponent_form(&raw, false)
21430    } else {
21431        let f_prec = (prec as i32 - 1 - x).max(0) as usize;
21432        format!("{:.*}", f_prec, n)
21433    };
21434    // Strip trailing zeros from the fractional part (and a trailing '.'),
21435    // but only on the mantissa side — leave the exponent untouched.
21436    let (mant, exp) = if let Some(pos) = formatted.find('e') {
21437        (formatted[..pos].to_string(), formatted[pos..].to_string())
21438    } else {
21439        (formatted.clone(), String::new())
21440    };
21441    let trimmed = if mant.contains('.') {
21442        let t = mant.trim_end_matches('0');
21443        let t = t.trim_end_matches('.');
21444        t.to_string()
21445    } else {
21446        mant
21447    };
21448    let combined = format!("{}{}", trimmed, exp);
21449    if upper {
21450        combined.replace('e', "E")
21451    } else {
21452        combined
21453    }
21454}
21455
21456/// Public sprintf entry point. Returns the formatted string plus the list
21457/// of `%n` store-targets and counts that the caller should apply via
21458/// [`VMHelper::assign_scalar_ref_deref`]. Callers that don't use `%n`
21459/// can ignore the second tuple element.
21460pub(crate) fn perl_sprintf_format_full<F>(
21461    fmt: &str,
21462    args: &[StrykeValue],
21463    string_for_s: &mut F,
21464) -> Result<(String, Vec<(StrykeValue, i64)>), FlowOrError>
21465where
21466    F: FnMut(&StrykeValue) -> Result<String, FlowOrError>,
21467{
21468    let mut pending_n: Vec<(StrykeValue, i64)> = Vec::new();
21469    let mut result = String::new();
21470    let mut arg_idx = 0;
21471    let chars: Vec<char> = fmt.chars().collect();
21472    let mut i = 0;
21473
21474    // Helper to consume the next arg as an i64 (used for `*` width / precision).
21475    let take_arg_int = |args: &[StrykeValue], idx: &mut usize| -> i64 {
21476        let v = args.get(*idx).cloned().unwrap_or(StrykeValue::UNDEF);
21477        *idx += 1;
21478        v.to_int()
21479    };
21480
21481    while i < chars.len() {
21482        if chars[i] == '%' {
21483            i += 1;
21484            if i >= chars.len() {
21485                break;
21486            }
21487            if chars[i] == '%' {
21488                result.push('%');
21489                i += 1;
21490                continue;
21491            }
21492
21493            // Positional `%N$...`: take this conversion's value from args[N-1]
21494            // instead of advancing the sequential cursor. Must be the very
21495            // first thing after `%`. We peek for `digits$` and rewind if the
21496            // `$` isn't there (the digits could just be a width).
21497            let mut positional: Option<usize> = None;
21498            {
21499                let saved = i;
21500                let mut digits = String::new();
21501                let mut j = i;
21502                while j < chars.len() && chars[j].is_ascii_digit() {
21503                    digits.push(chars[j]);
21504                    j += 1;
21505                }
21506                if j < chars.len() && chars[j] == '$' && !digits.is_empty() {
21507                    if let Ok(n) = digits.parse::<usize>() {
21508                        if n >= 1 {
21509                            positional = Some(n - 1);
21510                            i = j + 1; // consume the digits and the '$'
21511                        }
21512                    }
21513                }
21514                if positional.is_none() {
21515                    i = saved;
21516                }
21517            }
21518
21519            // Parse format specifier
21520            let mut flags = String::new();
21521            while i < chars.len() && "-+ #0".contains(chars[i]) {
21522                flags.push(chars[i]);
21523                i += 1;
21524            }
21525            // Vector flag: `v` (separator = ".") or `*v` (separator = next arg).
21526            // When set, the conversion runs once per byte of the value's
21527            // string form, joining results with the separator.
21528            let mut vector_sep: Option<String> = None;
21529            if i < chars.len() && chars[i] == 'v' {
21530                vector_sep = Some(".".to_string());
21531                i += 1;
21532            } else if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == 'v' {
21533                let sep_arg = args.get(arg_idx).cloned().unwrap_or(StrykeValue::UNDEF);
21534                arg_idx += 1;
21535                vector_sep = Some(sep_arg.to_string());
21536                i += 2;
21537            }
21538            // Width: either `*` (consume an arg) or run of digits.
21539            let mut width = String::new();
21540            let mut left_align = flags.contains('-');
21541            if i < chars.len() && chars[i] == '*' {
21542                let n = take_arg_int(args, &mut arg_idx);
21543                if n < 0 {
21544                    // Negative width means left-align with |n|.
21545                    left_align = true;
21546                    width = (-n).to_string();
21547                } else {
21548                    width = n.to_string();
21549                }
21550                i += 1;
21551            } else {
21552                while i < chars.len() && chars[i].is_ascii_digit() {
21553                    width.push(chars[i]);
21554                    i += 1;
21555                }
21556            }
21557            // Precision: `.*` or `.<digits>` (or nothing).
21558            let mut precision = String::new();
21559            if i < chars.len() && chars[i] == '.' {
21560                i += 1;
21561                if i < chars.len() && chars[i] == '*' {
21562                    let n = take_arg_int(args, &mut arg_idx);
21563                    precision = n.max(0).to_string();
21564                    i += 1;
21565                } else {
21566                    while i < chars.len() && chars[i].is_ascii_digit() {
21567                        precision.push(chars[i]);
21568                        i += 1;
21569                    }
21570                    // ".<no digits>" means precision 0 (Perl/C convention).
21571                    if precision.is_empty() {
21572                        precision = "0".to_string();
21573                    }
21574                }
21575            }
21576            if i >= chars.len() {
21577                break;
21578            }
21579            let spec = chars[i];
21580            i += 1;
21581
21582            // For vector conversions the conversion's value-arg is the
21583            // string whose bytes we'll iterate; for non-vector, it's the
21584            // value we format. Either way the index resolution is the
21585            // same: positional or sequential.
21586            let arg = if let Some(idx) = positional {
21587                args.get(idx).cloned().unwrap_or(StrykeValue::UNDEF)
21588            } else {
21589                let v = args.get(arg_idx).cloned().unwrap_or(StrykeValue::UNDEF);
21590                arg_idx += 1;
21591                v
21592            };
21593
21594            let w: usize = width.parse().unwrap_or(0);
21595            let p: usize = precision.parse().unwrap_or(6);
21596
21597            let zero_pad = flags.contains('0') && !left_align;
21598            let plus = flags.contains('+');
21599            let space = flags.contains(' ');
21600            let hash = flags.contains('#');
21601
21602            // Apply width + alignment to a body string. Honors zero-pad for
21603            // numerics (caller passes the raw signed body so we can splice
21604            // zeros after the sign).
21605            let pad_align = |body: &str, width: usize, left: bool, zero: bool| -> String {
21606                if width == 0 || body.len() >= width {
21607                    return body.to_string();
21608                }
21609                if zero && !left {
21610                    if let Some(rest) = body.strip_prefix('-') {
21611                        return format!("-{:0>width$}", rest, width = width - 1);
21612                    }
21613                    if let Some(rest) = body.strip_prefix('+') {
21614                        return format!("+{:0>width$}", rest, width = width - 1);
21615                    }
21616                    return format!("{:0>width$}", body, width = width);
21617                }
21618                if left {
21619                    format!("{:<width$}", body, width = width)
21620                } else {
21621                    format!("{:>width$}", body, width = width)
21622                }
21623            };
21624
21625            // Format a single integer with the inner spec for `%v...`. No
21626            // width/precision is applied here — those are deferred to the
21627            // joined result. Supports the common int-shape conversions.
21628            let format_int_for_vector = |n: i64, spec: char| -> String {
21629                match spec {
21630                    'd' | 'i' => format!("{}", n),
21631                    'u' => format!("{}", n as u64),
21632                    'x' => {
21633                        if hash && n != 0 {
21634                            format!("0x{:x}", n)
21635                        } else {
21636                            format!("{:x}", n)
21637                        }
21638                    }
21639                    'X' => {
21640                        if hash && n != 0 {
21641                            format!("0X{:X}", n)
21642                        } else {
21643                            format!("{:X}", n)
21644                        }
21645                    }
21646                    'o' => {
21647                        if hash && n != 0 {
21648                            format!("0{:o}", n)
21649                        } else {
21650                            format!("{:o}", n)
21651                        }
21652                    }
21653                    'b' => {
21654                        if hash && n != 0 {
21655                            format!("0b{:b}", n)
21656                        } else {
21657                            format!("{:b}", n)
21658                        }
21659                    }
21660                    'c' => char::from_u32(n as u32)
21661                        .map(|c| c.to_string())
21662                        .unwrap_or_default(),
21663                    _ => format!("{}", n),
21664                }
21665            };
21666
21667            // `%v` short-circuit: format each byte of the arg's string form
21668            // with the inner spec, join with `vector_sep`, then pad/align
21669            // the joined string. Skips the regular per-spec match below.
21670            if let Some(ref sep) = vector_sep {
21671                let s = arg.to_string();
21672                let parts: Vec<String> = s
21673                    .bytes()
21674                    .map(|b| format_int_for_vector(b as i64, spec))
21675                    .collect();
21676                let body = parts.join(sep);
21677                let final_body = if width.is_empty() {
21678                    body
21679                } else if left_align {
21680                    format!("{:<width$}", body, width = w)
21681                } else {
21682                    format!("{:>width$}", body, width = w)
21683                };
21684                result.push_str(&final_body);
21685                continue;
21686            }
21687
21688            let formatted = match spec {
21689                'd' | 'i' => {
21690                    let v = arg.to_int();
21691                    let body = if plus && v >= 0 {
21692                        format!("+{}", v)
21693                    } else if space && v >= 0 {
21694                        format!(" {}", v)
21695                    } else {
21696                        format!("{}", v)
21697                    };
21698                    pad_align(&body, w, left_align, zero_pad)
21699                }
21700                'u' => {
21701                    let v = arg.to_int() as u64;
21702                    pad_align(&format!("{}", v), w, left_align, zero_pad)
21703                }
21704                'f' => {
21705                    let n = arg.to_number();
21706                    let body = if plus && n.is_sign_positive() {
21707                        format!("+{:.*}", p, n)
21708                    } else if space && n.is_sign_positive() {
21709                        format!(" {:.*}", p, n)
21710                    } else {
21711                        format!("{:.*}", p, n)
21712                    };
21713                    pad_align(&body, w, left_align, zero_pad)
21714                }
21715                'e' => {
21716                    let n = arg.to_number();
21717                    let raw = format!("{:.*e}", p, n);
21718                    let body0 = perl_exponent_form(&raw, false);
21719                    let body = if plus && n.is_sign_positive() {
21720                        format!("+{}", body0)
21721                    } else if space && n.is_sign_positive() {
21722                        format!(" {}", body0)
21723                    } else {
21724                        body0
21725                    };
21726                    pad_align(&body, w, left_align, zero_pad)
21727                }
21728                'E' => {
21729                    let n = arg.to_number();
21730                    let raw = format!("{:.*E}", p, n);
21731                    let body0 = perl_exponent_form(&raw, true);
21732                    let body = if plus && n.is_sign_positive() {
21733                        format!("+{}", body0)
21734                    } else if space && n.is_sign_positive() {
21735                        format!(" {}", body0)
21736                    } else {
21737                        body0
21738                    };
21739                    pad_align(&body, w, left_align, zero_pad)
21740                }
21741                'g' => {
21742                    let n = arg.to_number();
21743                    // For %g, precision means "significant digits" (default 6).
21744                    let prec_g = if precision.is_empty() { 6 } else { p };
21745                    let body0 = perl_g_form(n, prec_g, false);
21746                    let body = if plus && n.is_sign_positive() {
21747                        format!("+{}", body0)
21748                    } else if space && n.is_sign_positive() {
21749                        format!(" {}", body0)
21750                    } else {
21751                        body0
21752                    };
21753                    pad_align(&body, w, left_align, zero_pad)
21754                }
21755                'G' => {
21756                    let n = arg.to_number();
21757                    let prec_g = if precision.is_empty() { 6 } else { p };
21758                    let body0 = perl_g_form(n, prec_g, true);
21759                    let body = if plus && n.is_sign_positive() {
21760                        format!("+{}", body0)
21761                    } else if space && n.is_sign_positive() {
21762                        format!(" {}", body0)
21763                    } else {
21764                        body0
21765                    };
21766                    pad_align(&body, w, left_align, zero_pad)
21767                }
21768                's' => {
21769                    let s = string_for_s(&arg)?;
21770                    let body = if !precision.is_empty() {
21771                        s.chars().take(p).collect::<String>()
21772                    } else {
21773                        s
21774                    };
21775                    if left_align {
21776                        format!("{:<width$}", body, width = w)
21777                    } else {
21778                        format!("{:>width$}", body, width = w)
21779                    }
21780                }
21781                'x' => {
21782                    let v = arg.to_int();
21783                    let body = if hash && v != 0 {
21784                        format!("0x{:x}", v)
21785                    } else {
21786                        format!("{:x}", v)
21787                    };
21788                    pad_align(&body, w, left_align, zero_pad)
21789                }
21790                'X' => {
21791                    let v = arg.to_int();
21792                    let body = if hash && v != 0 {
21793                        format!("0X{:X}", v)
21794                    } else {
21795                        format!("{:X}", v)
21796                    };
21797                    pad_align(&body, w, left_align, zero_pad)
21798                }
21799                'o' => {
21800                    let v = arg.to_int();
21801                    let body = if hash && v != 0 {
21802                        format!("0{:o}", v)
21803                    } else {
21804                        format!("{:o}", v)
21805                    };
21806                    pad_align(&body, w, left_align, zero_pad)
21807                }
21808                'b' => {
21809                    let v = arg.to_int();
21810                    let body = if hash && v != 0 {
21811                        format!("0b{:b}", v)
21812                    } else {
21813                        format!("{:b}", v)
21814                    };
21815                    pad_align(&body, w, left_align, zero_pad)
21816                }
21817                'c' => char::from_u32(arg.to_int() as u32)
21818                    .map(|c| c.to_string())
21819                    .unwrap_or_default(),
21820                'a' | 'A' => {
21821                    let upper = spec == 'A';
21822                    let body0 = perl_hex_float(arg.to_number(), upper);
21823                    let body = if plus && !body0.starts_with('-') {
21824                        format!("+{}", body0)
21825                    } else if space && !body0.starts_with('-') {
21826                        format!(" {}", body0)
21827                    } else {
21828                        body0
21829                    };
21830                    pad_align(&body, w, left_align, zero_pad)
21831                }
21832                'p' => {
21833                    // Stryke uses placeholder addresses for refs; emit the
21834                    // same `0x...` form here so output stays deterministic
21835                    // and machine-comparable across runs.
21836                    pad_align("0x...", w, left_align, false)
21837                }
21838                'n' => {
21839                    // Write the number of bytes emitted so far into the
21840                    // referent of the arg (must be a scalar ref, e.g.
21841                    // `\$count`). `%n` does NOT consume an output slot, so
21842                    // the formatted body is empty. The store is queued and
21843                    // applied by the caller after formatting finishes —
21844                    // works for both `HeapObject::ScalarRef` and the
21845                    // `ScalarBindingRef` shape that `\$my_var` produces.
21846                    pending_n.push((arg.clone(), result.len() as i64));
21847                    String::new()
21848                }
21849                _ => arg.to_string(),
21850            };
21851
21852            result.push_str(&formatted);
21853        } else {
21854            result.push(chars[i]);
21855            i += 1;
21856        }
21857    }
21858    Ok((result, pending_n))
21859}
21860
21861#[cfg(test)]
21862mod regex_expand_tests {
21863    use super::VMHelper;
21864
21865    #[test]
21866    fn compile_regex_quotemeta_qe_matches_literal() {
21867        let mut i = VMHelper::new();
21868        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
21869        assert!(re.is_match("a.c"));
21870        assert!(!re.is_match("abc"));
21871    }
21872
21873    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
21874    /// stay literal (not rewritten to `(?:\n?\z)`).
21875    #[test]
21876    fn compile_regex_char_class_leading_close_bracket_is_literal() {
21877        let mut i = VMHelper::new();
21878        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
21879        assert!(re.is_match("$"));
21880        assert!(re.is_match("]"));
21881        assert!(!re.is_match("x"));
21882    }
21883}
21884
21885#[cfg(test)]
21886mod special_scalar_name_tests {
21887    use super::VMHelper;
21888
21889    #[test]
21890    fn special_scalar_name_for_get_matches_magic_globals() {
21891        assert!(VMHelper::is_special_scalar_name_for_get("0"));
21892        assert!(VMHelper::is_special_scalar_name_for_get("!"));
21893        assert!(VMHelper::is_special_scalar_name_for_get("^W"));
21894        assert!(VMHelper::is_special_scalar_name_for_get("^O"));
21895        assert!(VMHelper::is_special_scalar_name_for_get("^MATCH"));
21896        assert!(VMHelper::is_special_scalar_name_for_get("<"));
21897        assert!(VMHelper::is_special_scalar_name_for_get("?"));
21898        assert!(VMHelper::is_special_scalar_name_for_get("|"));
21899        assert!(VMHelper::is_special_scalar_name_for_get("^UNICODE"));
21900        assert!(VMHelper::is_special_scalar_name_for_get("\""));
21901        assert!(!VMHelper::is_special_scalar_name_for_get("foo"));
21902        assert!(!VMHelper::is_special_scalar_name_for_get("plainvar"));
21903    }
21904
21905    #[test]
21906    fn special_scalar_name_for_set_matches_set_special_var_arms() {
21907        assert!(VMHelper::is_special_scalar_name_for_set("0"));
21908        assert!(VMHelper::is_special_scalar_name_for_set("^D"));
21909        assert!(VMHelper::is_special_scalar_name_for_set("^H"));
21910        assert!(VMHelper::is_special_scalar_name_for_set("^WARNING_BITS"));
21911        assert!(VMHelper::is_special_scalar_name_for_set("ARGV"));
21912        assert!(VMHelper::is_special_scalar_name_for_set("|"));
21913        assert!(VMHelper::is_special_scalar_name_for_set("?"));
21914        assert!(VMHelper::is_special_scalar_name_for_set("^UNICODE"));
21915        assert!(VMHelper::is_special_scalar_name_for_set("."));
21916        assert!(!VMHelper::is_special_scalar_name_for_set("foo"));
21917        assert!(!VMHelper::is_special_scalar_name_for_set("__PACKAGE__"));
21918    }
21919
21920    #[test]
21921    fn caret_and_id_specials_roundtrip_get() {
21922        let i = VMHelper::new();
21923        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
21924        assert_eq!(
21925            i.get_special_var("^V").to_string(),
21926            format!("v{}", env!("CARGO_PKG_VERSION"))
21927        );
21928        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
21929        assert!(i.get_special_var("^T").to_int() >= 0);
21930        #[cfg(unix)]
21931        {
21932            assert!(i.get_special_var("<").to_int() >= 0);
21933        }
21934    }
21935
21936    #[test]
21937    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
21938        let mut i = VMHelper::new();
21939        i.last_readline_handle.clear();
21940        i.line_number = 3;
21941        i.prepare_flip_flop_vm_slots(1);
21942        assert_eq!(
21943            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
21944            1
21945        );
21946        assert!(i.flip_flop_active[0]);
21947        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
21948        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
21949        assert_eq!(
21950            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
21951            1
21952        );
21953        assert!(i.flip_flop_active[0]);
21954    }
21955
21956    #[test]
21957    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
21958        let mut i = VMHelper::new();
21959        i.last_readline_handle.clear();
21960        i.line_number = 2;
21961        i.prepare_flip_flop_vm_slots(1);
21962        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
21963        assert!(i.flip_flop_active[0]);
21964        i.line_number = 3;
21965        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
21966        assert!(!i.flip_flop_active[0]);
21967        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
21968    }
21969}