Skip to main content

stryke/
interpreter.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::PerlSocket;
25use crate::crypt_util::perl_crypt;
26use crate::error::{ErrorKind, PerlError, PerlResult};
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, PerlAsyncTask, PerlBarrier, PerlDataFrame,
37    PerlGenerator, PerlHeap, PerlPpool, PerlSub, PerlValue, PipelineInner, PipelineOp,
38    RemoteCluster,
39};
40
41/// Merge two counting-hash accumulators (parallel `preduce_init` partials).
42/// Returns a hashref so arrow deref (`$acc->{k}`) stays valid after parallel merge.
43pub(crate) fn preduce_init_merge_maps(
44    mut acc: IndexMap<String, PerlValue>,
45    b: IndexMap<String, PerlValue>,
46) -> PerlValue {
47    for (k, v2) in b {
48        acc.entry(k)
49            .and_modify(|v1| *v1 = PerlValue::float(v1.to_number() + v2.to_number()))
50            .or_insert(v2);
51    }
52    PerlValue::hash_ref(Arc::new(RwLock::new(acc)))
53}
54
55/// `(off, end)` for `splice` / `arr.drain(off..end)` — Perl negative OFFSET/LENGTH; clamps offset to array length.
56#[inline]
57fn splice_compute_range(
58    arr_len: usize,
59    offset_val: &PerlValue,
60    length_val: &PerlValue,
61) -> (usize, usize) {
62    let off_i = offset_val.to_int();
63    let off = if off_i < 0 {
64        arr_len.saturating_sub((-off_i) as usize)
65    } else {
66        (off_i as usize).min(arr_len)
67    };
68    let rest = arr_len.saturating_sub(off);
69    let take = if length_val.is_undef() {
70        rest
71    } else {
72        let l = length_val.to_int();
73        if l < 0 {
74            rest.saturating_sub((-l) as usize)
75        } else {
76            (l as usize).min(rest)
77        }
78    };
79    let end = (off + take).min(arr_len);
80    (off, end)
81}
82
83/// Combine two partial results from `preduce_init`: hash/hashref maps add per-key counts; otherwise
84/// the fold block is invoked with `$a` / `$b` as the two partial accumulators (associative combine).
85pub(crate) fn merge_preduce_init_partials(
86    a: PerlValue,
87    b: PerlValue,
88    block: &Block,
89    subs: &HashMap<String, Arc<PerlSub>>,
90    scope_capture: &[(String, PerlValue)],
91) -> PerlValue {
92    if let (Some(m1), Some(m2)) = (a.as_hash_map(), b.as_hash_map()) {
93        return preduce_init_merge_maps(m1, m2);
94    }
95    if let (Some(r1), Some(r2)) = (a.as_hash_ref(), b.as_hash_ref()) {
96        let m1 = r1.read().clone();
97        let m2 = r2.read().clone();
98        return preduce_init_merge_maps(m1, m2);
99    }
100    if let Some(m1) = a.as_hash_map() {
101        if let Some(r2) = b.as_hash_ref() {
102            let m2 = r2.read().clone();
103            return preduce_init_merge_maps(m1, m2);
104        }
105    }
106    if let Some(r1) = a.as_hash_ref() {
107        if let Some(m2) = b.as_hash_map() {
108            let m1 = r1.read().clone();
109            return preduce_init_merge_maps(m1, m2);
110        }
111    }
112    let mut local_interp = Interpreter::new();
113    local_interp.subs = subs.clone();
114    local_interp.scope.restore_capture(scope_capture);
115    local_interp.enable_parallel_guard();
116    local_interp
117        .scope
118        .declare_array("_", vec![a.clone(), b.clone()]);
119    let _ = local_interp.scope.set_scalar("a", a.clone());
120    let _ = local_interp.scope.set_scalar("b", b.clone());
121    let _ = local_interp.scope.set_scalar("_0", a);
122    let _ = local_interp.scope.set_scalar("_1", b);
123    match local_interp.exec_block(block) {
124        Ok(val) => val,
125        Err(_) => PerlValue::UNDEF,
126    }
127}
128
129/// Seed each parallel chunk from `init` without sharing mutable hashref storage (plain `clone` on
130/// `HashRef` reuses the same `Arc<RwLock<…>>`).
131pub(crate) fn preduce_init_fold_identity(init: &PerlValue) -> PerlValue {
132    if let Some(m) = init.as_hash_map() {
133        return PerlValue::hash(m.clone());
134    }
135    if let Some(r) = init.as_hash_ref() {
136        return PerlValue::hash_ref(Arc::new(RwLock::new(r.read().clone())));
137    }
138    init.clone()
139}
140
141pub(crate) fn fold_preduce_init_step(
142    subs: &HashMap<String, Arc<PerlSub>>,
143    scope_capture: &[(String, PerlValue)],
144    block: &Block,
145    acc: PerlValue,
146    item: PerlValue,
147) -> PerlValue {
148    let mut local_interp = Interpreter::new();
149    local_interp.subs = subs.clone();
150    local_interp.scope.restore_capture(scope_capture);
151    local_interp.enable_parallel_guard();
152    local_interp
153        .scope
154        .declare_array("_", vec![acc.clone(), item.clone()]);
155    let _ = local_interp.scope.set_scalar("a", acc.clone());
156    let _ = local_interp.scope.set_scalar("b", item.clone());
157    let _ = local_interp.scope.set_scalar("_0", acc);
158    let _ = local_interp.scope.set_scalar("_1", item);
159    match local_interp.exec_block(block) {
160        Ok(val) => val,
161        Err(_) => PerlValue::UNDEF,
162    }
163}
164
165/// `use feature 'say'`
166pub const FEAT_SAY: u64 = 1 << 0;
167/// `use feature 'state'`
168pub const FEAT_STATE: u64 = 1 << 1;
169/// `use feature 'switch'` (given/when when fully wired)
170pub const FEAT_SWITCH: u64 = 1 << 2;
171/// `use feature 'unicode_strings'`
172pub const FEAT_UNICODE_STRINGS: u64 = 1 << 3;
173
174/// Flow control signals propagated via Result.
175#[derive(Debug)]
176pub(crate) enum Flow {
177    Return(PerlValue),
178    Last(Option<String>),
179    Next(Option<String>),
180    Redo(Option<String>),
181    Yield(PerlValue),
182    /// `goto &sub` — tail-call: replace current sub with the named one, keeping @_.
183    GotoSub(String),
184}
185
186pub(crate) type ExecResult = Result<PerlValue, FlowOrError>;
187
188#[derive(Debug)]
189pub(crate) enum FlowOrError {
190    Flow(Flow),
191    Error(PerlError),
192}
193
194impl From<PerlError> for FlowOrError {
195    fn from(e: PerlError) -> Self {
196        FlowOrError::Error(e)
197    }
198}
199
200impl From<Flow> for FlowOrError {
201    fn from(f: Flow) -> Self {
202        FlowOrError::Flow(f)
203    }
204}
205
206/// Bindings introduced by a successful algebraic [`MatchPattern`] (scalar vs array).
207enum PatternBinding {
208    Scalar(String, PerlValue),
209    Array(String, Vec<PerlValue>),
210}
211
212/// Perl `$]` — numeric language level (`5 + minor/1000 + patch/1_000_000`).
213/// Emulated Perl 5.x level (not the `stryke` crate semver).
214pub fn perl_bracket_version() -> f64 {
215    const PERL_EMUL_MINOR: u32 = 38;
216    const PERL_EMUL_PATCH: u32 = 0;
217    5.0 + (PERL_EMUL_MINOR as f64) / 1000.0 + (PERL_EMUL_PATCH as f64) / 1_000_000.0
218}
219
220/// Cheap seed for [`StdRng`] at startup (avoids `getentropy` / blocking sources).
221#[inline]
222fn fast_rng_seed() -> u64 {
223    let local: u8 = 0;
224    let addr = &local as *const u8 as u64;
225    (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ addr
226}
227
228/// `$^X` — cache `current_exe()` once per process (tiny win on repeated `Interpreter::new`).
229fn cached_executable_path() -> String {
230    static CACHED: OnceLock<String> = OnceLock::new();
231    CACHED
232        .get_or_init(|| {
233            std::env::current_exe()
234                .map(|p| p.to_string_lossy().into_owned())
235                .unwrap_or_else(|_| "stryke".to_string())
236        })
237        .clone()
238}
239
240fn build_term_hash() -> IndexMap<String, PerlValue> {
241    let mut m = IndexMap::new();
242    m.insert(
243        "TERM".into(),
244        PerlValue::string(std::env::var("TERM").unwrap_or_default()),
245    );
246    m.insert(
247        "COLORTERM".into(),
248        PerlValue::string(std::env::var("COLORTERM").unwrap_or_default()),
249    );
250
251    let (rows, cols) = term_size();
252    m.insert("rows".into(), PerlValue::integer(rows));
253    m.insert("cols".into(), PerlValue::integer(cols));
254
255    #[cfg(unix)]
256    let is_tty = unsafe { libc::isatty(1) != 0 };
257    #[cfg(not(unix))]
258    let is_tty = false;
259    m.insert(
260        "is_tty".into(),
261        PerlValue::integer(if is_tty { 1 } else { 0 }),
262    );
263
264    m
265}
266
267fn term_size() -> (i64, i64) {
268    #[cfg(unix)]
269    {
270        unsafe {
271            let mut ws: libc::winsize = std::mem::zeroed();
272            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 {
273                return (ws.ws_row as i64, ws.ws_col as i64);
274            }
275        }
276    }
277    let rows = std::env::var("LINES")
278        .ok()
279        .and_then(|s| s.parse().ok())
280        .unwrap_or(24);
281    let cols = std::env::var("COLUMNS")
282        .ok()
283        .and_then(|s| s.parse().ok())
284        .unwrap_or(80);
285    (rows, cols)
286}
287
288#[cfg(unix)]
289fn build_uname_hash() -> IndexMap<String, PerlValue> {
290    fn uts_field(slice: &[libc::c_char]) -> String {
291        let n = slice.iter().take_while(|&&c| c != 0).count();
292        let bytes: Vec<u8> = slice[..n].iter().map(|&c| c as u8).collect();
293        String::from_utf8_lossy(&bytes).into_owned()
294    }
295    let mut m = IndexMap::new();
296    let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
297    if unsafe { libc::uname(&mut uts) } == 0 {
298        m.insert(
299            "sysname".into(),
300            PerlValue::string(uts_field(uts.sysname.as_slice())),
301        );
302        m.insert(
303            "nodename".into(),
304            PerlValue::string(uts_field(uts.nodename.as_slice())),
305        );
306        m.insert(
307            "release".into(),
308            PerlValue::string(uts_field(uts.release.as_slice())),
309        );
310        m.insert(
311            "version".into(),
312            PerlValue::string(uts_field(uts.version.as_slice())),
313        );
314        m.insert(
315            "machine".into(),
316            PerlValue::string(uts_field(uts.machine.as_slice())),
317        );
318    }
319    m
320}
321
322#[cfg(unix)]
323fn build_limits_hash() -> IndexMap<String, PerlValue> {
324    use libc::{getrlimit, rlimit, RLIM_INFINITY};
325    #[cfg(target_os = "linux")]
326    type RlimitResource = libc::__rlimit_resource_t;
327    #[cfg(not(target_os = "linux"))]
328    type RlimitResource = libc::c_int;
329    fn get_limit(resource: RlimitResource) -> (i64, i64) {
330        let mut rlim = rlimit {
331            rlim_cur: 0,
332            rlim_max: 0,
333        };
334        if unsafe { getrlimit(resource, &mut rlim) } == 0 {
335            let cur = if rlim.rlim_cur == RLIM_INFINITY {
336                -1
337            } else {
338                rlim.rlim_cur as i64
339            };
340            let max = if rlim.rlim_max == RLIM_INFINITY {
341                -1
342            } else {
343                rlim.rlim_max as i64
344            };
345            (cur, max)
346        } else {
347            (-1, -1)
348        }
349    }
350    let mut m = IndexMap::new();
351    let (cur, max) = get_limit(libc::RLIMIT_NOFILE);
352    m.insert("nofile".into(), PerlValue::integer(cur));
353    m.insert("nofile_max".into(), PerlValue::integer(max));
354    let (cur, max) = get_limit(libc::RLIMIT_STACK);
355    m.insert("stack".into(), PerlValue::integer(cur));
356    m.insert("stack_max".into(), PerlValue::integer(max));
357    let (cur, max) = get_limit(libc::RLIMIT_AS);
358    m.insert("as".into(), PerlValue::integer(cur));
359    m.insert("as_max".into(), PerlValue::integer(max));
360    let (cur, max) = get_limit(libc::RLIMIT_DATA);
361    m.insert("data".into(), PerlValue::integer(cur));
362    m.insert("data_max".into(), PerlValue::integer(max));
363    let (cur, max) = get_limit(libc::RLIMIT_FSIZE);
364    m.insert("fsize".into(), PerlValue::integer(cur));
365    m.insert("fsize_max".into(), PerlValue::integer(max));
366    let (cur, max) = get_limit(libc::RLIMIT_CORE);
367    m.insert("core".into(), PerlValue::integer(cur));
368    m.insert("core_max".into(), PerlValue::integer(max));
369    let (cur, max) = get_limit(libc::RLIMIT_CPU);
370    m.insert("cpu".into(), PerlValue::integer(cur));
371    m.insert("cpu_max".into(), PerlValue::integer(max));
372    let (cur, max) = get_limit(libc::RLIMIT_NPROC);
373    m.insert("nproc".into(), PerlValue::integer(cur));
374    m.insert("nproc_max".into(), PerlValue::integer(max));
375    #[cfg(target_os = "linux")]
376    {
377        let (cur, max) = get_limit(libc::RLIMIT_MEMLOCK);
378        m.insert("memlock".into(), PerlValue::integer(cur));
379        m.insert("memlock_max".into(), PerlValue::integer(max));
380    }
381    m
382}
383
384/// Context of the **current** subroutine call (`wantarray`).
385#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
386pub(crate) enum WantarrayCtx {
387    #[default]
388    Scalar,
389    List,
390    Void,
391}
392
393impl WantarrayCtx {
394    #[inline]
395    pub(crate) fn from_byte(b: u8) -> Self {
396        match b {
397            1 => Self::List,
398            2 => Self::Void,
399            _ => Self::Scalar,
400        }
401    }
402
403    #[inline]
404    pub(crate) fn as_byte(self) -> u8 {
405        match self {
406            Self::Scalar => 0,
407            Self::List => 1,
408            Self::Void => 2,
409        }
410    }
411}
412
413/// Minimum log level filter for `log_*` / `log_json` (trace = most verbose).
414#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
415pub(crate) enum LogLevelFilter {
416    Trace,
417    Debug,
418    Info,
419    Warn,
420    Error,
421}
422
423impl LogLevelFilter {
424    pub(crate) fn parse(s: &str) -> Option<Self> {
425        match s.trim().to_ascii_lowercase().as_str() {
426            "trace" => Some(Self::Trace),
427            "debug" => Some(Self::Debug),
428            "info" => Some(Self::Info),
429            "warn" | "warning" => Some(Self::Warn),
430            "error" => Some(Self::Error),
431            _ => None,
432        }
433    }
434
435    pub(crate) fn as_str(self) -> &'static str {
436        match self {
437            Self::Trace => "trace",
438            Self::Debug => "debug",
439            Self::Info => "info",
440            Self::Warn => "warn",
441            Self::Error => "error",
442        }
443    }
444}
445
446/// True when `@$aref->[IX]` / `IX` needs **list** context on the RHS of `=` (multi-slot slice).
447fn arrow_deref_array_assign_rhs_list_ctx(index: &Expr) -> bool {
448    match &index.kind {
449        ExprKind::Range { .. } | ExprKind::SliceRange { .. } => true,
450        ExprKind::QW(ws) => ws.len() > 1,
451        ExprKind::List(el) => {
452            if el.len() > 1 {
453                true
454            } else if el.len() == 1 {
455                arrow_deref_array_assign_rhs_list_ctx(&el[0])
456            } else {
457                false
458            }
459        }
460        _ => false,
461    }
462}
463
464/// Wantarray for the RHS of a plain `=` assignment — must match [`crate::compiler::Compiler`] lowering
465/// so `<>` / `readline` list-slurp matches Perl for `@a = <>` (not only `my`/`our`/`local` initializers).
466pub(crate) fn assign_rhs_wantarray(target: &Expr) -> WantarrayCtx {
467    match &target.kind {
468        ExprKind::ArrayVar(_) | ExprKind::HashVar(_) => WantarrayCtx::List,
469        ExprKind::ScalarVar(_) | ExprKind::ArrayElement { .. } | ExprKind::HashElement { .. } => {
470            WantarrayCtx::Scalar
471        }
472        ExprKind::Deref { kind, .. } => match kind {
473            Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
474            Sigil::Array | Sigil::Hash => WantarrayCtx::List,
475        },
476        ExprKind::ArrowDeref {
477            index,
478            kind: DerefKind::Array,
479            ..
480        } => {
481            if arrow_deref_array_assign_rhs_list_ctx(index) {
482                WantarrayCtx::List
483            } else {
484                WantarrayCtx::Scalar
485            }
486        }
487        ExprKind::ArrowDeref {
488            kind: DerefKind::Hash,
489            ..
490        }
491        | ExprKind::ArrowDeref {
492            kind: DerefKind::Call,
493            ..
494        } => WantarrayCtx::Scalar,
495        ExprKind::HashSliceDeref { .. } | ExprKind::HashSlice { .. } => WantarrayCtx::List,
496        ExprKind::ArraySlice { indices, .. } => {
497            if indices.len() > 1 {
498                WantarrayCtx::List
499            } else if indices.len() == 1 {
500                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
501                    WantarrayCtx::List
502                } else {
503                    WantarrayCtx::Scalar
504                }
505            } else {
506                WantarrayCtx::Scalar
507            }
508        }
509        ExprKind::AnonymousListSlice { indices, .. } => {
510            if indices.len() > 1 {
511                WantarrayCtx::List
512            } else if indices.len() == 1 {
513                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
514                    WantarrayCtx::List
515                } else {
516                    WantarrayCtx::Scalar
517                }
518            } else {
519                WantarrayCtx::Scalar
520            }
521        }
522        ExprKind::Typeglob(_) | ExprKind::TypeglobExpr(_) => WantarrayCtx::Scalar,
523        ExprKind::List(_) => WantarrayCtx::List,
524        _ => WantarrayCtx::Scalar,
525    }
526}
527
528/// Memoized inputs + result for a non-`g` `regex_match_execute` call. Populated on every
529/// successful match and consulted at the top of the next call; on exact-match (same pattern,
530/// flags, multiline, and haystack content) we skip regex execution + capture-var scope population
531/// entirely, replaying the stored `PerlValue` result. See [`Interpreter::regex_match_memo`].
532#[derive(Clone)]
533pub(crate) struct RegexMatchMemo {
534    pub pattern: String,
535    pub flags: String,
536    pub multiline: bool,
537    pub haystack: String,
538    pub result: PerlValue,
539}
540
541/// State for scalar `..` / `...` (key: `Expr` address).
542#[derive(Clone, Copy, Default)]
543struct FlipFlopTreeState {
544    active: bool,
545    /// Exclusive `...`: `$.` line where the left bound matched — right is only tested when `$.` is
546    /// strictly greater (Perl: do not test the right operand until the next evaluation; for numeric
547    /// `$.` that defers past the left-match line, including multiple evals on that line).
548    exclusive_left_line: Option<i64>,
549}
550
551/// `BufReader` / `print` / `sysread` / `tell` on the same handle share this [`File`] cursor.
552#[derive(Clone)]
553pub(crate) struct IoSharedFile(pub Arc<Mutex<File>>);
554
555impl Read for IoSharedFile {
556    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
557        self.0.lock().read(buf)
558    }
559}
560
561pub(crate) struct IoSharedFileWrite(pub Arc<Mutex<File>>);
562
563impl IoWrite for IoSharedFileWrite {
564    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
565        self.0.lock().write(buf)
566    }
567
568    fn flush(&mut self) -> io::Result<()> {
569        self.0.lock().flush()
570    }
571}
572
573pub struct Interpreter {
574    pub scope: Scope,
575    pub(crate) subs: HashMap<String, Arc<PerlSub>>,
576    pub(crate) file: String,
577    /// File handles: name → writer
578    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
579    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
580    /// Output separator ($,)
581    pub ofs: String,
582    /// Output record separator ($\)
583    pub ors: String,
584    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
585    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
586    pub irs: Option<String>,
587    /// $! — last OS error
588    pub errno: String,
589    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
590    pub errno_code: i32,
591    /// $@ — last eval error (string)
592    pub eval_error: String,
593    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
594    pub eval_error_code: i32,
595    /// When `die` is called with a ref argument, the ref value is preserved here.
596    pub eval_error_value: Option<PerlValue>,
597    /// @ARGV
598    pub argv: Vec<String>,
599    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
600    pub env: IndexMap<String, PerlValue>,
601    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
602    pub env_materialized: bool,
603    /// $0
604    pub program_name: String,
605    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle)
606    pub line_number: i64,
607    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
608    pub last_readline_handle: String,
609    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
610    pub(crate) last_stdin_die_bracket: String,
611    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
612    pub handle_line_numbers: HashMap<String, i64>,
613    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
614    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
615    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
616    pub(crate) flip_flop_active: Vec<bool>,
617    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
618    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
619    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
620    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
621    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
622    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
623    pub(crate) flip_flop_sequence: Vec<i64>,
624    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
625    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
626    /// range on one line return the same sequence number).
627    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
628    /// Scalar `..` / `...` flip-flop state (key: `Expr` address).
629    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
630    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
631    pub sigint_pending_caret: Cell<bool>,
632    /// Auto-split mode (-a)
633    pub auto_split: bool,
634    /// Field separator for -F
635    pub field_separator: Option<String>,
636    /// BEGIN blocks
637    begin_blocks: Vec<Block>,
638    /// `UNITCHECK` blocks (LIFO at run)
639    unit_check_blocks: Vec<Block>,
640    /// `CHECK` blocks (LIFO at run)
641    check_blocks: Vec<Block>,
642    /// `INIT` blocks (FIFO at run)
643    init_blocks: Vec<Block>,
644    /// END blocks
645    end_blocks: Vec<Block>,
646    /// -w warnings / `use warnings` / `$^W`
647    pub warnings: bool,
648    /// Output autoflush (`$|`).
649    pub output_autoflush: bool,
650    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
651    pub default_print_handle: String,
652    /// Suppress stdout output (fan workers with progress bars).
653    pub suppress_stdout: bool,
654    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
655    pub child_exit_status: i64,
656    /// Last successful match (`$&`, `${^MATCH}`).
657    pub last_match: String,
658    /// Before match (`` $` ``, `${^PREMATCH}`).
659    pub prematch: String,
660    /// After match (`$'`, `${^POSTMATCH}`).
661    pub postmatch: String,
662    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
663    pub last_paren_match: String,
664    /// List separator for array stringification in concatenation / interpolation (`$"`).
665    pub list_separator: String,
666    /// Script start time (`$^T`) — seconds since Unix epoch.
667    pub script_start_time: i64,
668    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
669    pub compile_hints: i64,
670    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
671    pub warning_bits: i64,
672    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
673    pub global_phase: String,
674    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
675    pub subscript_sep: String,
676    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
677    /// The `stryke` driver sets this from `-i` / `-i.ext`.
678    pub inplace_edit: String,
679    /// `$^D` — debugging flags (integer; mostly ignored).
680    pub debug_flags: i64,
681    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
682    pub perl_debug_flags: i64,
683    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
684    pub eval_nesting: u32,
685    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
686    pub argv_current_file: String,
687    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
688    pub(crate) diamond_next_idx: usize,
689    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
690    pub(crate) diamond_reader: Option<BufReader<File>>,
691    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
692    pub strict_refs: bool,
693    pub strict_subs: bool,
694    pub strict_vars: bool,
695    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
696    pub utf8_pragma: bool,
697    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
698    pub open_pragma_utf8: bool,
699    /// `use feature` — bit flags (`FEAT_*`).
700    pub feature_bits: u64,
701    /// Number of parallel threads
702    pub num_threads: usize,
703    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
704    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
705    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
706    /// Third flag: `$*` multiline (prepends `(?s)` when true).
707    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
708    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
709    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
710    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
711    /// scope population entirely on cache hit.
712    ///
713    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
714    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
715    /// capture-var side-effect replay is forced on the next hit.
716    regex_match_memo: Option<RegexMatchMemo>,
717    /// False when the user (or some non-regex code path) has written to one of the capture
718    /// variables since the last `apply_regex_captures` call. The memoized match result is still
719    /// valid, but the scope side effects need to be reapplied on the next hit.
720    regex_capture_scope_fresh: bool,
721    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
722    pub(crate) regex_pos: HashMap<String, Option<usize>>,
723    /// Persistent storage for `state` variables, keyed by "line:name".
724    pub(crate) state_vars: HashMap<String, PerlValue>,
725    /// Per-frame tracking of state variable bindings: (var_name, state_key).
726    pub(crate) state_bindings_stack: Vec<Vec<(String, String)>>,
727    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
728    pub(crate) rand_rng: StdRng,
729    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
730    pub(crate) dir_handles: HashMap<String, DirHandleState>,
731    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
732    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
733    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
734    pub(crate) pipe_children: HashMap<String, Child>,
735    /// Sockets from `socket` / `accept` / `connect`.
736    pub(crate) socket_handles: HashMap<String, PerlSocket>,
737    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
738    pub(crate) wantarray_kind: WantarrayCtx,
739    /// `struct Name { ... }` definitions (merged from VM chunks).
740    pub struct_defs: HashMap<String, Arc<StructDef>>,
741    /// `enum Name { ... }` definitions (merged from VM chunks).
742    pub enum_defs: HashMap<String, Arc<EnumDef>>,
743    /// `class Name extends ... impl ... { ... }` definitions.
744    pub class_defs: HashMap<String, Arc<ClassDef>>,
745    /// `trait Name { ... }` definitions.
746    pub trait_defs: HashMap<String, Arc<TraitDef>>,
747    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
748    /// call/return (JIT disabled); per-statement lines and subs.
749    pub profiler: Option<Profiler>,
750    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
751    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
752    /// Virtual modules: path → source (for AOT bundles). Checked before filesystem in `require`.
753    pub(crate) virtual_modules: HashMap<String, String>,
754    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
755    pub(crate) tied_hashes: HashMap<String, PerlValue>,
756    /// `tie $name` — TIESCALAR object for FETCH/STORE.
757    pub(crate) tied_scalars: HashMap<String, PerlValue>,
758    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
759    pub(crate) tied_arrays: HashMap<String, PerlValue>,
760    /// `use overload` — class → Perl overload key → short method name in that package.
761    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
762    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
763    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
764    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
765    pub(crate) special_caret_scalars: HashMap<String, PerlValue>,
766    /// `$%` — format output page number.
767    pub format_page_number: i64,
768    /// `$=` — format lines per page.
769    pub format_lines_per_page: i64,
770    /// `$-` — lines remaining on format page.
771    pub format_lines_left: i64,
772    /// `$:` — characters to break format lines (Perl default `\n`).
773    pub format_line_break_chars: String,
774    /// `$^` — top-of-form format name.
775    pub format_top_name: String,
776    /// `$^A` — format write accumulator.
777    pub accumulator_format: String,
778    /// `$^F` — max system file descriptor (Perl default 2).
779    pub max_system_fd: i64,
780    /// `$^M` — emergency memory buffer (no-op pool in stryke).
781    pub emergency_memory: String,
782    /// `$^N` — last opened named regexp capture name.
783    pub last_subpattern_name: String,
784    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
785    pub inc_hook_index: i64,
786    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
787    pub multiline_match: bool,
788    /// `$^X` — path to this executable (cached).
789    pub executable_path: String,
790    /// `$^L` — formfeed string for formats (Perl default `\f`).
791    pub formfeed_string: String,
792    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
793    pub(crate) glob_handle_alias: HashMap<String, String>,
794    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
795    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
796    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
797    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
798    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
799    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
800    pub(crate) special_var_restore_frames: Vec<Vec<(String, PerlValue)>>,
801    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
802    /// Lazy-init flag: reflection hashes (`%b`, `%stryke::builtins`, etc.)
803    /// are only built on first access to avoid startup cost.
804    pub(crate) reflection_hashes_ready: bool,
805    pub(crate) english_enabled: bool,
806    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
807    pub(crate) english_no_match_vars: bool,
808    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
809    /// available for the rest of the program — Perl exports them into the caller's namespace
810    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
811    pub(crate) english_match_vars_ever_enabled: bool,
812    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
813    english_lexical_scalars: Vec<HashSet<String>>,
814    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
815    our_lexical_scalars: Vec<HashSet<String>>,
816    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
817    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
818    pub vm_jit_enabled: bool,
819    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
820    pub disasm_bytecode: bool,
821    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from SQLite cache hit. When
822    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
823    /// (`.take()`) on first read so re-entry compiles normally.
824    pub cached_chunk: Option<crate::bytecode::Chunk>,
825    /// Sideband: script path for SQLite cache save after compilation (mtime-based).
826    pub sqlite_cache_script_path: Option<std::path::PathBuf>,
827    /// Set while stepping a `gen { }` body (`yield`).
828    pub(crate) in_generator: bool,
829    /// `-n`/`-p` driver: prelude only; body runs per line in [`Self::process_line_vm`].
830    pub line_mode_skip_main: bool,
831    /// Pre-compiled chunk for `-n`/`-p` line mode. Stored after the prelude `execute()` call
832    /// so `process_line_vm` can re-execute the body portion per input line.
833    pub line_mode_chunk: Option<crate::bytecode::Chunk>,
834    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
835    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
836    /// matches Perl (true on the last line of that source).
837    pub(crate) line_mode_eof_pending: bool,
838    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
839    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
840    pub line_mode_stdin_pending: VecDeque<String>,
841    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
842    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
843    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
844    pub(crate) log_level_override: Option<LogLevelFilter>,
845    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
846    /// Pushed on `call_sub` entry, popped on exit.
847    pub(crate) current_sub_stack: Vec<Arc<PerlSub>>,
848    /// Interactive debugger state (`-d` flag).
849    pub debugger: Option<crate::debugger::Debugger>,
850    /// Call stack for debugger: (sub_name, call_line).
851    pub(crate) debug_call_stack: Vec<(String, usize)>,
852}
853
854/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
855#[derive(Debug, Clone, Default)]
856pub struct ReplCompletionSnapshot {
857    pub subs: Vec<String>,
858    pub blessed_scalars: HashMap<String, String>,
859    pub isa_for_class: HashMap<String, Vec<String>>,
860}
861
862impl ReplCompletionSnapshot {
863    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
864    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
865        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
866        let mro = linearize_c3(class, &parents, 0);
867        let mut names = HashSet::new();
868        for pkg in &mro {
869            if pkg == "UNIVERSAL" {
870                continue;
871            }
872            let prefix = format!("{}::", pkg);
873            for k in &self.subs {
874                if k.starts_with(&prefix) {
875                    let rest = &k[prefix.len()..];
876                    if !rest.contains("::") {
877                        names.insert(rest.to_string());
878                    }
879                }
880            }
881        }
882        for k in &self.subs {
883            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
884                if !rest.contains("::") {
885                    names.insert(rest.to_string());
886                }
887            }
888        }
889        let mut v: Vec<String> = names.into_iter().collect();
890        v.sort();
891        v
892    }
893}
894
895fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
896    let left = left.trim_end();
897    if left.is_empty() {
898        return None;
899    }
900    if let Some(i) = left.rfind('$') {
901        let name = left[i + 1..].trim();
902        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
903            return state.blessed_scalars.get(name).cloned();
904        }
905    }
906    let tok = left.split_whitespace().last()?;
907    if tok.contains("::") {
908        return Some(tok.to_string());
909    }
910    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
911        return Some(tok.to_string());
912    }
913    None
914}
915
916/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
917pub fn repl_arrow_method_completions(
918    state: &ReplCompletionSnapshot,
919    line: &str,
920    pos: usize,
921) -> Option<(usize, Vec<String>)> {
922    let pos = pos.min(line.len());
923    let before = &line[..pos];
924    let arrow_idx = before.rfind("->")?;
925    let after_arrow = &before[arrow_idx + 2..];
926    let rest = after_arrow.trim_start();
927    let ws_len = after_arrow.len() - rest.len();
928    let method_start = arrow_idx + 2 + ws_len;
929    let method_prefix = &line[method_start..pos];
930    if !method_prefix
931        .chars()
932        .all(|c| c.is_alphanumeric() || c == '_')
933    {
934        return None;
935    }
936    let left = line[..arrow_idx].trim_end();
937    let class = repl_resolve_class_for_arrow(state, left)?;
938    let mut methods = state.methods_for_class(&class);
939    methods.retain(|m| m.starts_with(method_prefix));
940    Some((method_start, methods))
941}
942
943/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
944#[derive(Debug, Clone, Default)]
945pub(crate) struct ModuleExportLists {
946    /// Default imports for `use Module` with no list.
947    pub export: Vec<String>,
948    /// Extra symbols allowed in `use Module qw(name)`.
949    pub export_ok: Vec<String>,
950}
951
952/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
953fn piped_shell_command(cmd: &str) -> Command {
954    if cfg!(windows) {
955        let mut c = Command::new("cmd");
956        c.arg("/C").arg(cmd);
957        c
958    } else {
959        let mut c = Command::new("sh");
960        c.arg("-c").arg(cmd);
961        c
962    }
963}
964
965/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
966/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
967/// so the Rust `regex` crate can match them.
968/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
969/// so the Rust regex crate can match NUL and other octal-specified bytes.
970/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
971fn expand_perl_regex_octal_escapes(pat: &str) -> String {
972    let mut out = String::with_capacity(pat.len());
973    let mut it = pat.chars().peekable();
974    while let Some(c) = it.next() {
975        if c == '\\' {
976            if let Some(&'0') = it.peek() {
977                // Collect up to 3 octal digits starting with '0'
978                let mut oct = String::new();
979                while oct.len() < 3 {
980                    if let Some(&d) = it.peek() {
981                        if ('0'..='7').contains(&d) {
982                            oct.push(d);
983                            it.next();
984                        } else {
985                            break;
986                        }
987                    } else {
988                        break;
989                    }
990                }
991                if let Ok(val) = u8::from_str_radix(&oct, 8) {
992                    out.push_str(&format!("\\x{:02x}", val));
993                } else {
994                    out.push('\\');
995                    out.push_str(&oct);
996                }
997                continue;
998            }
999        }
1000        out.push(c);
1001    }
1002    out
1003}
1004
1005fn expand_perl_regex_quotemeta(pat: &str) -> String {
1006    let mut out = String::with_capacity(pat.len().saturating_mul(2));
1007    let mut it = pat.chars().peekable();
1008    let mut in_q = false;
1009    while let Some(c) = it.next() {
1010        if in_q {
1011            if c == '\\' && it.peek() == Some(&'E') {
1012                it.next();
1013                in_q = false;
1014                continue;
1015            }
1016            out.push_str(&perl_quotemeta(&c.to_string()));
1017            continue;
1018        }
1019        if c == '\\' && it.peek() == Some(&'Q') {
1020            it.next();
1021            in_q = true;
1022            continue;
1023        }
1024        out.push(c);
1025    }
1026    out
1027}
1028
1029/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
1030///
1031/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
1032/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
1033///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
1034pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
1035    let mut out = String::with_capacity(replacement.len() + 8);
1036    let mut it = replacement.chars().peekable();
1037    while let Some(c) = it.next() {
1038        if c == '\\' {
1039            match it.peek() {
1040                Some(&d) if d.is_ascii_digit() => {
1041                    it.next();
1042                    out.push_str("${");
1043                    out.push(d);
1044                    while let Some(&d2) = it.peek() {
1045                        if !d2.is_ascii_digit() {
1046                            break;
1047                        }
1048                        it.next();
1049                        out.push(d2);
1050                    }
1051                    out.push('}');
1052                }
1053                Some(&'\\') => {
1054                    it.next();
1055                    out.push('\\');
1056                }
1057                _ => out.push('\\'),
1058            }
1059        } else if c == '$' {
1060            match it.peek() {
1061                Some(&d) if d.is_ascii_digit() => {
1062                    it.next();
1063                    out.push_str("${");
1064                    out.push(d);
1065                    while let Some(&d2) = it.peek() {
1066                        if !d2.is_ascii_digit() {
1067                            break;
1068                        }
1069                        it.next();
1070                        out.push(d2);
1071                    }
1072                    out.push('}');
1073                }
1074                Some(&'{') => {
1075                    // already braced — pass through as-is
1076                    out.push('$');
1077                }
1078                _ => out.push('$'),
1079            }
1080        } else {
1081            out.push(c);
1082        }
1083    }
1084    out
1085}
1086
1087/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
1088/// past the closing `]`.
1089fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
1090    debug_assert_eq!(chars.get(i), Some(&'['));
1091    out.push('[');
1092    i += 1;
1093    if i < chars.len() && chars[i] == '^' {
1094        out.push('^');
1095        i += 1;
1096    }
1097    if i >= chars.len() {
1098        return i;
1099    }
1100    // `]` as the first class character is literal iff another unescaped `]` closes the class
1101    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
1102    // this `]`.
1103    if chars[i] == ']' {
1104        if i + 1 < chars.len() && chars[i + 1] == ']' {
1105            // `[]]` / `[^]]`: literal `]` then the closing `]`.
1106            out.push(']');
1107            i += 1;
1108        } else {
1109            let mut scan = i + 1;
1110            let mut found_closing = false;
1111            while scan < chars.len() {
1112                if chars[scan] == '\\' && scan + 1 < chars.len() {
1113                    scan += 2;
1114                    continue;
1115                }
1116                if chars[scan] == ']' {
1117                    found_closing = true;
1118                    break;
1119                }
1120                scan += 1;
1121            }
1122            if found_closing {
1123                out.push(']');
1124                i += 1;
1125            } else {
1126                out.push(']');
1127                return i + 1;
1128            }
1129        }
1130    }
1131    while i < chars.len() && chars[i] != ']' {
1132        if chars[i] == '\\' && i + 1 < chars.len() {
1133            out.push(chars[i]);
1134            out.push(chars[i + 1]);
1135            i += 2;
1136            continue;
1137        }
1138        out.push(chars[i]);
1139        i += 1;
1140    }
1141    if i < chars.len() {
1142        out.push(']');
1143        i += 1;
1144    }
1145    i
1146}
1147
1148/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
1149/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
1150/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1151/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1152fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1153    if multiline_flag {
1154        return pat.to_string();
1155    }
1156    let chars: Vec<char> = pat.chars().collect();
1157    let mut out = String::with_capacity(pat.len().saturating_add(16));
1158    let mut i = 0usize;
1159    while i < chars.len() {
1160        let c = chars[i];
1161        if c == '\\' && i + 1 < chars.len() {
1162            out.push(c);
1163            out.push(chars[i + 1]);
1164            i += 2;
1165            continue;
1166        }
1167        if c == '[' {
1168            i = copy_regex_char_class(&chars, i, &mut out);
1169            continue;
1170        }
1171        if c == '$' {
1172            if let Some(&next) = chars.get(i + 1) {
1173                if next.is_ascii_digit() {
1174                    out.push(c);
1175                    i += 1;
1176                    continue;
1177                }
1178                if next == '{' {
1179                    out.push(c);
1180                    i += 1;
1181                    continue;
1182                }
1183                if next.is_ascii_alphanumeric() || next == '_' {
1184                    out.push(c);
1185                    i += 1;
1186                    continue;
1187                }
1188            }
1189            out.push_str("(?=\\n?\\z)");
1190            i += 1;
1191            continue;
1192        }
1193        out.push(c);
1194        i += 1;
1195    }
1196    out
1197}
1198
1199/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1200#[derive(Debug, Clone)]
1201pub(crate) struct DirHandleState {
1202    pub entries: Vec<String>,
1203    pub pos: usize,
1204}
1205
1206/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1207pub(crate) fn perl_osname() -> String {
1208    match std::env::consts::OS {
1209        "linux" => "linux".to_string(),
1210        "macos" => "darwin".to_string(),
1211        "windows" => "MSWin32".to_string(),
1212        other => other.to_string(),
1213    }
1214}
1215
1216fn perl_version_v_string() -> String {
1217    format!("v{}", env!("CARGO_PKG_VERSION"))
1218}
1219
1220fn extended_os_error_string() -> String {
1221    std::io::Error::last_os_error().to_string()
1222}
1223
1224#[cfg(unix)]
1225fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1226    unsafe {
1227        (
1228            libc::getuid() as i64,
1229            libc::geteuid() as i64,
1230            libc::getgid() as i64,
1231            libc::getegid() as i64,
1232        )
1233    }
1234}
1235
1236#[cfg(not(unix))]
1237fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1238    (0, 0, 0, 0)
1239}
1240
1241fn unix_id_for_special(name: &str) -> i64 {
1242    let (r, e, _, _) = unix_real_effective_ids();
1243    match name {
1244        "<" => r,
1245        ">" => e,
1246        _ => 0,
1247    }
1248}
1249
1250#[cfg(unix)]
1251fn unix_group_list_string(primary: libc::gid_t) -> String {
1252    let mut buf = vec![0 as libc::gid_t; 256];
1253    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1254    if n <= 0 {
1255        return format!("{}", primary);
1256    }
1257    let mut parts = vec![format!("{}", primary)];
1258    for g in buf.iter().take(n as usize) {
1259        parts.push(format!("{}", g));
1260    }
1261    parts.join(" ")
1262}
1263
1264/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1265#[cfg(unix)]
1266fn unix_group_list_for_special(name: &str) -> String {
1267    let (_, _, gid, egid) = unix_real_effective_ids();
1268    match name {
1269        "(" => unix_group_list_string(gid as libc::gid_t),
1270        ")" => unix_group_list_string(egid as libc::gid_t),
1271        _ => String::new(),
1272    }
1273}
1274
1275#[cfg(not(unix))]
1276fn unix_group_list_for_special(_name: &str) -> String {
1277    String::new()
1278}
1279
1280/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1281/// `~/.ssh/config` and keys).
1282#[cfg(unix)]
1283fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1284    use libc::{getpwuid_r, getuid};
1285    use std::ffi::CStr;
1286    use std::os::unix::ffi::OsStringExt;
1287    let uid = unsafe { getuid() };
1288    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1289    let mut result: *mut libc::passwd = std::ptr::null_mut();
1290    let mut buf = vec![0u8; 16_384];
1291    let rc = unsafe {
1292        getpwuid_r(
1293            uid,
1294            &mut pw,
1295            buf.as_mut_ptr().cast::<libc::c_char>(),
1296            buf.len(),
1297            &mut result,
1298        )
1299    };
1300    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1301        return None;
1302    }
1303    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1304    if bytes.is_empty() {
1305        return None;
1306    }
1307    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1308}
1309
1310/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1311#[cfg(unix)]
1312fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1313    use libc::getpwnam_r;
1314    use std::ffi::{CStr, CString};
1315    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1316    let bytes = login.as_bytes();
1317    if bytes.is_empty() || bytes.contains(&0) {
1318        return None;
1319    }
1320    let cname = CString::new(bytes).ok()?;
1321    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1322    let mut result: *mut libc::passwd = std::ptr::null_mut();
1323    let mut buf = vec![0u8; 16_384];
1324    let rc = unsafe {
1325        getpwnam_r(
1326            cname.as_ptr(),
1327            &mut pw,
1328            buf.as_mut_ptr().cast::<libc::c_char>(),
1329            buf.len(),
1330            &mut result,
1331        )
1332    };
1333    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1334        return None;
1335    }
1336    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1337    if dir_bytes.is_empty() {
1338        return None;
1339    }
1340    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1341}
1342
1343impl Default for Interpreter {
1344    fn default() -> Self {
1345        Self::new()
1346    }
1347}
1348
1349/// How [`Interpreter::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1350#[derive(Clone, Copy)]
1351pub(crate) enum CaptureAllMode {
1352    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1353    Empty,
1354    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1355    Append,
1356    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1357    Skip,
1358}
1359
1360impl Interpreter {
1361    pub fn new() -> Self {
1362        let mut scope = Scope::new();
1363        scope.declare_array("INC", vec![PerlValue::string(".".to_string())]);
1364        scope.declare_hash("INC", IndexMap::new());
1365        scope.declare_array("ARGV", vec![]);
1366        scope.declare_array("_", vec![]);
1367
1368        // @path / @p — $PATH split by OS path separator, frozen (immutable)
1369        let path_vec: Vec<PerlValue> = std::env::var("PATH")
1370            .unwrap_or_default()
1371            .split(if cfg!(windows) { ';' } else { ':' })
1372            .filter(|s| !s.is_empty())
1373            .map(|p| PerlValue::string(p.to_string()))
1374            .collect();
1375        scope.declare_array_frozen("path", path_vec.clone(), true);
1376        scope.declare_array_frozen("p", path_vec, true);
1377
1378        // @fpath / @f — $FPATH (zsh function path) split by ':', frozen
1379        let fpath_vec: Vec<PerlValue> = std::env::var("FPATH")
1380            .unwrap_or_default()
1381            .split(':')
1382            .filter(|s| !s.is_empty())
1383            .map(|p| PerlValue::string(p.to_string()))
1384            .collect();
1385        scope.declare_array_frozen("fpath", fpath_vec.clone(), true);
1386        scope.declare_array_frozen("f", fpath_vec, true);
1387        scope.declare_hash("ENV", IndexMap::new());
1388        scope.declare_hash("SIG", IndexMap::new());
1389
1390        // %term — terminal info (frozen)
1391        let term_map = build_term_hash();
1392        scope.declare_hash_global_frozen("term", term_map);
1393
1394        // %uname — system identification (frozen, Unix only)
1395        #[cfg(unix)]
1396        {
1397            let uname_map = build_uname_hash();
1398            scope.declare_hash_global_frozen("uname", uname_map);
1399        }
1400        #[cfg(not(unix))]
1401        {
1402            scope.declare_hash_global_frozen("uname", IndexMap::new());
1403        }
1404
1405        // %limits — resource limits (frozen, Unix only)
1406        #[cfg(unix)]
1407        {
1408            let limits_map = build_limits_hash();
1409            scope.declare_hash_global_frozen("limits", limits_map);
1410        }
1411        #[cfg(not(unix))]
1412        {
1413            scope.declare_hash_global_frozen("limits", IndexMap::new());
1414        }
1415
1416        // Reflection hashes — populated from `build.rs`-generated tables so
1417        // they track the real parser/dispatcher/LSP without hand-maintenance.
1418        // Seven hashes; all lookups are O(1). Forward maps:
1419        //   %b  / %stryke::builtins      — name → category ("parallel", "string", …)
1420        //   %pc / %stryke::perl_compats  — subset: Perl 5 core only
1421        //   %e  / %stryke::extensions    — subset: stryke-only
1422        //   %a  / %stryke::aliases       — alias → primary
1423        //   %d  / %stryke::descriptions  — name → LSP one-liner (sparse)
1424        // Inverted indexes for constant-time reverse queries:
1425        //   %c  / %stryke::categories    — category → arrayref of names
1426        //   %p  / %stryke::primaries     — primary → arrayref of aliases
1427        //
1428        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1429        // together they cover `keys %builtins`. Short aliases use the
1430        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1431        // Reflection hashes are lazily initialized on first access
1432        // (see `ensure_reflection_hashes`). Only declare the version scalar
1433        // eagerly since it's trivial.
1434        scope.declare_scalar(
1435            "stryke::VERSION",
1436            PerlValue::string(env!("CARGO_PKG_VERSION").to_string()),
1437        );
1438        scope.declare_array("-", vec![]);
1439        scope.declare_array("+", vec![]);
1440        scope.declare_array("^CAPTURE", vec![]);
1441        scope.declare_array("^CAPTURE_ALL", vec![]);
1442        scope.declare_hash("^HOOK", IndexMap::new());
1443        scope.declare_scalar("~", PerlValue::string("STDOUT".to_string()));
1444
1445        let script_start_time = std::time::SystemTime::now()
1446            .duration_since(std::time::UNIX_EPOCH)
1447            .map(|d| d.as_secs() as i64)
1448            .unwrap_or(0);
1449
1450        let executable_path = cached_executable_path();
1451
1452        let mut special_caret_scalars: HashMap<String, PerlValue> = HashMap::new();
1453        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1454            special_caret_scalars.insert(format!("^{}", name), PerlValue::UNDEF);
1455        }
1456
1457        let mut s = Self {
1458            scope,
1459            subs: HashMap::new(),
1460            struct_defs: HashMap::new(),
1461            enum_defs: HashMap::new(),
1462            class_defs: HashMap::new(),
1463            trait_defs: HashMap::new(),
1464            file: "-e".to_string(),
1465            output_handles: HashMap::new(),
1466            input_handles: HashMap::new(),
1467            ofs: String::new(),
1468            ors: String::new(),
1469            irs: Some("\n".to_string()),
1470            errno: String::new(),
1471            errno_code: 0,
1472            eval_error: String::new(),
1473            eval_error_code: 0,
1474            eval_error_value: None,
1475            argv: Vec::new(),
1476            env: IndexMap::new(),
1477            env_materialized: false,
1478            program_name: "stryke".to_string(),
1479            line_number: 0,
1480            last_readline_handle: String::new(),
1481            last_stdin_die_bracket: "<STDIN>".to_string(),
1482            handle_line_numbers: HashMap::new(),
1483            flip_flop_active: Vec::new(),
1484            flip_flop_exclusive_left_line: Vec::new(),
1485            flip_flop_sequence: Vec::new(),
1486            flip_flop_last_dot: Vec::new(),
1487            flip_flop_tree: HashMap::new(),
1488            sigint_pending_caret: Cell::new(false),
1489            auto_split: false,
1490            field_separator: None,
1491            begin_blocks: Vec::new(),
1492            unit_check_blocks: Vec::new(),
1493            check_blocks: Vec::new(),
1494            init_blocks: Vec::new(),
1495            end_blocks: Vec::new(),
1496            warnings: false,
1497            output_autoflush: false,
1498            default_print_handle: "STDOUT".to_string(),
1499            suppress_stdout: false,
1500            child_exit_status: 0,
1501            last_match: String::new(),
1502            prematch: String::new(),
1503            postmatch: String::new(),
1504            last_paren_match: String::new(),
1505            list_separator: " ".to_string(),
1506            script_start_time,
1507            compile_hints: 0,
1508            warning_bits: 0,
1509            global_phase: "RUN".to_string(),
1510            subscript_sep: "\x1c".to_string(),
1511            inplace_edit: String::new(),
1512            debug_flags: 0,
1513            perl_debug_flags: 0,
1514            eval_nesting: 0,
1515            argv_current_file: String::new(),
1516            diamond_next_idx: 0,
1517            diamond_reader: None,
1518            strict_refs: false,
1519            strict_subs: false,
1520            strict_vars: false,
1521            utf8_pragma: false,
1522            open_pragma_utf8: false,
1523            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1524            feature_bits: FEAT_SAY,
1525            num_threads: 0, // lazily read from rayon on first parallel op
1526            regex_cache: HashMap::new(),
1527            regex_last: None,
1528            regex_match_memo: None,
1529            regex_capture_scope_fresh: false,
1530            regex_pos: HashMap::new(),
1531            state_vars: HashMap::new(),
1532            state_bindings_stack: Vec::new(),
1533            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1534            dir_handles: HashMap::new(),
1535            io_file_slots: HashMap::new(),
1536            pipe_children: HashMap::new(),
1537            socket_handles: HashMap::new(),
1538            wantarray_kind: WantarrayCtx::Scalar,
1539            profiler: None,
1540            module_export_lists: HashMap::new(),
1541            virtual_modules: HashMap::new(),
1542            tied_hashes: HashMap::new(),
1543            tied_scalars: HashMap::new(),
1544            tied_arrays: HashMap::new(),
1545            overload_table: HashMap::new(),
1546            format_templates: HashMap::new(),
1547            special_caret_scalars,
1548            format_page_number: 0,
1549            format_lines_per_page: 60,
1550            format_lines_left: 0,
1551            format_line_break_chars: "\n".to_string(),
1552            format_top_name: String::new(),
1553            accumulator_format: String::new(),
1554            max_system_fd: 2,
1555            emergency_memory: String::new(),
1556            last_subpattern_name: String::new(),
1557            inc_hook_index: 0,
1558            multiline_match: false,
1559            executable_path,
1560            formfeed_string: "\x0c".to_string(),
1561            glob_handle_alias: HashMap::new(),
1562            glob_restore_frames: vec![Vec::new()],
1563            special_var_restore_frames: vec![Vec::new()],
1564            reflection_hashes_ready: false,
1565            english_enabled: false,
1566            english_no_match_vars: false,
1567            english_match_vars_ever_enabled: false,
1568            english_lexical_scalars: vec![HashSet::new()],
1569            our_lexical_scalars: vec![HashSet::new()],
1570            vm_jit_enabled: !matches!(
1571                std::env::var("STRYKE_NO_JIT"),
1572                Ok(v)
1573                    if v == "1"
1574                        || v.eq_ignore_ascii_case("true")
1575                        || v.eq_ignore_ascii_case("yes")
1576            ),
1577            disasm_bytecode: false,
1578            cached_chunk: None,
1579            sqlite_cache_script_path: None,
1580            in_generator: false,
1581            line_mode_skip_main: false,
1582            line_mode_chunk: None,
1583            line_mode_eof_pending: false,
1584            line_mode_stdin_pending: VecDeque::new(),
1585            rate_limit_slots: Vec::new(),
1586            log_level_override: None,
1587            current_sub_stack: Vec::new(),
1588            debugger: None,
1589            debug_call_stack: Vec::new(),
1590        };
1591        s.install_overload_pragma_stubs();
1592        s
1593    }
1594
1595    /// Lazily populate the reflection hashes (`%b`, `%stryke::builtins`, etc.)
1596    /// on first access. This avoids building ~12k hash entries on startup for
1597    /// one-liners that never touch introspection.
1598    pub(crate) fn ensure_reflection_hashes(&mut self) {
1599        if self.reflection_hashes_ready {
1600            return;
1601        }
1602        self.reflection_hashes_ready = true;
1603        let builtins_map = crate::builtins::builtins_hash_map();
1604        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1605        let extensions_map = crate::builtins::extensions_hash_map();
1606        let aliases_map = crate::builtins::aliases_hash_map();
1607        let descriptions_map = crate::builtins::descriptions_hash_map();
1608        let categories_map = crate::builtins::categories_hash_map();
1609        let primaries_map = crate::builtins::primaries_hash_map();
1610        let all_map = crate::builtins::all_hash_map();
1611        self.scope
1612            .declare_hash_global_frozen("stryke::builtins", builtins_map.clone());
1613        self.scope
1614            .declare_hash_global_frozen("stryke::perl_compats", perl_compats_map.clone());
1615        self.scope
1616            .declare_hash_global_frozen("stryke::extensions", extensions_map.clone());
1617        self.scope
1618            .declare_hash_global_frozen("stryke::aliases", aliases_map.clone());
1619        self.scope
1620            .declare_hash_global_frozen("stryke::descriptions", descriptions_map.clone());
1621        self.scope
1622            .declare_hash_global_frozen("stryke::categories", categories_map.clone());
1623        self.scope
1624            .declare_hash_global_frozen("stryke::primaries", primaries_map.clone());
1625        self.scope
1626            .declare_hash_global_frozen("stryke::all", all_map.clone());
1627        // Short aliases: only declare if no user-declared hash with that name
1628        // exists, to avoid overwriting `my %e` etc.
1629        for (name, val) in [
1630            ("b", builtins_map),
1631            ("pc", perl_compats_map),
1632            ("e", extensions_map),
1633            ("a", aliases_map),
1634            ("d", descriptions_map),
1635            ("c", categories_map),
1636            ("p", primaries_map),
1637            ("all", all_map),
1638        ] {
1639            if !self.scope.any_frame_has_hash(name) {
1640                self.scope.declare_hash_global_frozen(name, val);
1641            }
1642        }
1643    }
1644
1645    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1646    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1647    /// strict subs and to satisfy `use overload ();` call sites.
1648    fn install_overload_pragma_stubs(&mut self) {
1649        let empty: Block = vec![];
1650        for key in ["overload::import", "overload::unimport"] {
1651            let name = key.to_string();
1652            self.subs.insert(
1653                name.clone(),
1654                Arc::new(PerlSub {
1655                    name,
1656                    params: vec![],
1657                    body: empty.clone(),
1658                    prototype: None,
1659                    closure_env: None,
1660                    fib_like: None,
1661                }),
1662            );
1663        }
1664    }
1665
1666    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1667    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1668    pub fn line_mode_worker_clone(&self) -> Interpreter {
1669        Interpreter {
1670            scope: self.scope.clone(),
1671            subs: self.subs.clone(),
1672            struct_defs: self.struct_defs.clone(),
1673            enum_defs: self.enum_defs.clone(),
1674            class_defs: self.class_defs.clone(),
1675            trait_defs: self.trait_defs.clone(),
1676            file: self.file.clone(),
1677            output_handles: HashMap::new(),
1678            input_handles: HashMap::new(),
1679            ofs: self.ofs.clone(),
1680            ors: self.ors.clone(),
1681            irs: self.irs.clone(),
1682            errno: self.errno.clone(),
1683            errno_code: self.errno_code,
1684            eval_error: self.eval_error.clone(),
1685            eval_error_code: self.eval_error_code,
1686            eval_error_value: self.eval_error_value.clone(),
1687            argv: self.argv.clone(),
1688            env: self.env.clone(),
1689            env_materialized: self.env_materialized,
1690            program_name: self.program_name.clone(),
1691            line_number: 0,
1692            last_readline_handle: String::new(),
1693            last_stdin_die_bracket: "<STDIN>".to_string(),
1694            handle_line_numbers: HashMap::new(),
1695            flip_flop_active: Vec::new(),
1696            flip_flop_exclusive_left_line: Vec::new(),
1697            flip_flop_sequence: Vec::new(),
1698            flip_flop_last_dot: Vec::new(),
1699            flip_flop_tree: HashMap::new(),
1700            sigint_pending_caret: Cell::new(false),
1701            auto_split: self.auto_split,
1702            field_separator: self.field_separator.clone(),
1703            begin_blocks: self.begin_blocks.clone(),
1704            unit_check_blocks: self.unit_check_blocks.clone(),
1705            check_blocks: self.check_blocks.clone(),
1706            init_blocks: self.init_blocks.clone(),
1707            end_blocks: self.end_blocks.clone(),
1708            warnings: self.warnings,
1709            output_autoflush: self.output_autoflush,
1710            default_print_handle: self.default_print_handle.clone(),
1711            suppress_stdout: self.suppress_stdout,
1712            child_exit_status: self.child_exit_status,
1713            last_match: self.last_match.clone(),
1714            prematch: self.prematch.clone(),
1715            postmatch: self.postmatch.clone(),
1716            last_paren_match: self.last_paren_match.clone(),
1717            list_separator: self.list_separator.clone(),
1718            script_start_time: self.script_start_time,
1719            compile_hints: self.compile_hints,
1720            warning_bits: self.warning_bits,
1721            global_phase: self.global_phase.clone(),
1722            subscript_sep: self.subscript_sep.clone(),
1723            inplace_edit: self.inplace_edit.clone(),
1724            debug_flags: self.debug_flags,
1725            perl_debug_flags: self.perl_debug_flags,
1726            eval_nesting: self.eval_nesting,
1727            argv_current_file: String::new(),
1728            diamond_next_idx: 0,
1729            diamond_reader: None,
1730            strict_refs: self.strict_refs,
1731            strict_subs: self.strict_subs,
1732            strict_vars: self.strict_vars,
1733            utf8_pragma: self.utf8_pragma,
1734            open_pragma_utf8: self.open_pragma_utf8,
1735            feature_bits: self.feature_bits,
1736            num_threads: 0,
1737            regex_cache: self.regex_cache.clone(),
1738            regex_last: self.regex_last.clone(),
1739            regex_match_memo: self.regex_match_memo.clone(),
1740            regex_capture_scope_fresh: false,
1741            regex_pos: self.regex_pos.clone(),
1742            state_vars: self.state_vars.clone(),
1743            state_bindings_stack: Vec::new(),
1744            rand_rng: self.rand_rng.clone(),
1745            dir_handles: HashMap::new(),
1746            io_file_slots: HashMap::new(),
1747            pipe_children: HashMap::new(),
1748            socket_handles: HashMap::new(),
1749            wantarray_kind: self.wantarray_kind,
1750            profiler: None,
1751            module_export_lists: self.module_export_lists.clone(),
1752            virtual_modules: self.virtual_modules.clone(),
1753            tied_hashes: self.tied_hashes.clone(),
1754            tied_scalars: self.tied_scalars.clone(),
1755            tied_arrays: self.tied_arrays.clone(),
1756            overload_table: self.overload_table.clone(),
1757            format_templates: self.format_templates.clone(),
1758            special_caret_scalars: self.special_caret_scalars.clone(),
1759            format_page_number: self.format_page_number,
1760            format_lines_per_page: self.format_lines_per_page,
1761            format_lines_left: self.format_lines_left,
1762            format_line_break_chars: self.format_line_break_chars.clone(),
1763            format_top_name: self.format_top_name.clone(),
1764            accumulator_format: self.accumulator_format.clone(),
1765            max_system_fd: self.max_system_fd,
1766            emergency_memory: self.emergency_memory.clone(),
1767            last_subpattern_name: self.last_subpattern_name.clone(),
1768            inc_hook_index: self.inc_hook_index,
1769            multiline_match: self.multiline_match,
1770            executable_path: self.executable_path.clone(),
1771            formfeed_string: self.formfeed_string.clone(),
1772            glob_handle_alias: self.glob_handle_alias.clone(),
1773            glob_restore_frames: self.glob_restore_frames.clone(),
1774            special_var_restore_frames: self.special_var_restore_frames.clone(),
1775            reflection_hashes_ready: self.reflection_hashes_ready,
1776            english_enabled: self.english_enabled,
1777            english_no_match_vars: self.english_no_match_vars,
1778            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
1779            english_lexical_scalars: self.english_lexical_scalars.clone(),
1780            our_lexical_scalars: self.our_lexical_scalars.clone(),
1781            vm_jit_enabled: self.vm_jit_enabled,
1782            disasm_bytecode: self.disasm_bytecode,
1783            // Sideband cache fields belong to the top-level driver, not line-mode workers.
1784            cached_chunk: None,
1785            sqlite_cache_script_path: None,
1786            in_generator: false,
1787            line_mode_skip_main: false,
1788            line_mode_chunk: self.line_mode_chunk.clone(),
1789            line_mode_eof_pending: false,
1790            line_mode_stdin_pending: VecDeque::new(),
1791            rate_limit_slots: Vec::new(),
1792            log_level_override: self.log_level_override,
1793            current_sub_stack: Vec::new(),
1794            debugger: None,
1795            debug_call_stack: Vec::new(),
1796        }
1797    }
1798
1799    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
1800    pub(crate) fn parallel_thread_count(&mut self) -> usize {
1801        if self.num_threads == 0 {
1802            self.num_threads = rayon::current_num_threads();
1803        }
1804        self.num_threads
1805    }
1806
1807    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
1808    pub(crate) fn eval_par_list_call(
1809        &mut self,
1810        name: &str,
1811        args: &[PerlValue],
1812        ctx: WantarrayCtx,
1813        line: usize,
1814    ) -> PerlResult<PerlValue> {
1815        match name {
1816            "puniq" => {
1817                let (list_src, show_prog) = match args.len() {
1818                    0 => return Err(PerlError::runtime("puniq: expected LIST", line)),
1819                    1 => (&args[0], false),
1820                    2 => (&args[0], args[1].is_true()),
1821                    _ => {
1822                        return Err(PerlError::runtime(
1823                            "puniq: expected LIST [, progress => EXPR]",
1824                            line,
1825                        ));
1826                    }
1827                };
1828                let list = list_src.to_list();
1829                let n_threads = self.parallel_thread_count();
1830                let pmap_progress = PmapProgress::new(show_prog, list.len());
1831                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
1832                pmap_progress.finish();
1833                if ctx == WantarrayCtx::List {
1834                    Ok(PerlValue::array(out))
1835                } else {
1836                    Ok(PerlValue::integer(out.len() as i64))
1837                }
1838            }
1839            "pfirst" => {
1840                let (code_val, list_src, show_prog) = match args.len() {
1841                    2 => (&args[0], &args[1], false),
1842                    3 => (&args[0], &args[1], args[2].is_true()),
1843                    _ => {
1844                        return Err(PerlError::runtime(
1845                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
1846                            line,
1847                        ));
1848                    }
1849                };
1850                let Some(sub) = code_val.as_code_ref() else {
1851                    return Err(PerlError::runtime(
1852                        "pfirst: first argument must be a code reference",
1853                        line,
1854                    ));
1855                };
1856                let sub = sub.clone();
1857                let list = list_src.to_list();
1858                if list.is_empty() {
1859                    return Ok(PerlValue::UNDEF);
1860                }
1861                let pmap_progress = PmapProgress::new(show_prog, list.len());
1862                let subs = self.subs.clone();
1863                let (scope_capture, atomic_arrays, atomic_hashes) =
1864                    self.scope.capture_with_atomics();
1865                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
1866                    let mut local_interp = Interpreter::new();
1867                    local_interp.subs = subs.clone();
1868                    local_interp.scope.restore_capture(&scope_capture);
1869                    local_interp
1870                        .scope
1871                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1872                    local_interp.enable_parallel_guard();
1873                    local_interp.scope.set_topic(item);
1874                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1875                        Ok(v) => v.is_true(),
1876                        Err(_) => false,
1877                    }
1878                });
1879                pmap_progress.finish();
1880                Ok(out.unwrap_or(PerlValue::UNDEF))
1881            }
1882            "pany" => {
1883                let (code_val, list_src, show_prog) = match args.len() {
1884                    2 => (&args[0], &args[1], false),
1885                    3 => (&args[0], &args[1], args[2].is_true()),
1886                    _ => {
1887                        return Err(PerlError::runtime(
1888                            "pany: expected BLOCK, LIST [, progress => EXPR]",
1889                            line,
1890                        ));
1891                    }
1892                };
1893                let Some(sub) = code_val.as_code_ref() else {
1894                    return Err(PerlError::runtime(
1895                        "pany: first argument must be a code reference",
1896                        line,
1897                    ));
1898                };
1899                let sub = sub.clone();
1900                let list = list_src.to_list();
1901                let pmap_progress = PmapProgress::new(show_prog, list.len());
1902                let subs = self.subs.clone();
1903                let (scope_capture, atomic_arrays, atomic_hashes) =
1904                    self.scope.capture_with_atomics();
1905                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
1906                    let mut local_interp = Interpreter::new();
1907                    local_interp.subs = subs.clone();
1908                    local_interp.scope.restore_capture(&scope_capture);
1909                    local_interp
1910                        .scope
1911                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1912                    local_interp.enable_parallel_guard();
1913                    local_interp.scope.set_topic(item);
1914                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1915                        Ok(v) => v.is_true(),
1916                        Err(_) => false,
1917                    }
1918                });
1919                pmap_progress.finish();
1920                Ok(PerlValue::integer(if b { 1 } else { 0 }))
1921            }
1922            _ => Err(PerlError::runtime(
1923                format!("internal: unknown par_list builtin {name}"),
1924                line,
1925            )),
1926        }
1927    }
1928
1929    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
1930        #[cfg(unix)]
1931        if let Some(sig) = s.signal() {
1932            return sig as i64 & 0x7f;
1933        }
1934        let code = s.code().unwrap_or(0) as i64;
1935        code << 8
1936    }
1937
1938    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
1939        self.child_exit_status = self.encode_exit_status(s);
1940    }
1941
1942    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
1943    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
1944        self.errno = e.to_string();
1945        self.errno_code = e.raw_os_error().unwrap_or(0);
1946    }
1947
1948    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
1949    ///
1950    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
1951    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
1952    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
1953    ///
1954    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
1955    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
1956    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
1957    /// `~/.ssh/config` and keys apply.
1958    pub(crate) fn ssh_builtin_execute(&mut self, args: &[PerlValue]) -> PerlResult<PerlValue> {
1959        use std::process::Command;
1960        let mut cmd = Command::new("ssh");
1961        #[cfg(unix)]
1962        {
1963            use libc::geteuid;
1964            let home_for_ssh = if unsafe { geteuid() } == 0 {
1965                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
1966            } else {
1967                None
1968            };
1969            if let Some(h) = home_for_ssh {
1970                cmd.env("HOME", h);
1971            } else if std::env::var_os("HOME").is_none() {
1972                if let Some(h) = pw_home_dir_for_current_uid() {
1973                    cmd.env("HOME", h);
1974                }
1975            }
1976        }
1977        for a in args {
1978            cmd.arg(a.to_string());
1979        }
1980        match cmd.status() {
1981            Ok(s) => {
1982                self.record_child_exit_status(s);
1983                Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
1984            }
1985            Err(e) => {
1986                self.apply_io_error_to_errno(&e);
1987                Ok(PerlValue::integer(-1))
1988            }
1989        }
1990    }
1991
1992    /// Set `$@` message; numeric side is `0` if empty, else `1`.
1993    pub(crate) fn set_eval_error(&mut self, msg: String) {
1994        self.eval_error = msg;
1995        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
1996        self.eval_error_value = None;
1997    }
1998
1999    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &PerlError) {
2000        self.eval_error = e.to_string();
2001        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2002        self.eval_error_value = e.die_value.clone();
2003    }
2004
2005    pub(crate) fn clear_eval_error(&mut self) {
2006        self.eval_error = String::new();
2007        self.eval_error_code = 0;
2008        self.eval_error_value = None;
2009    }
2010
2011    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
2012    fn bump_line_for_handle(&mut self, handle_key: &str) {
2013        self.last_readline_handle = handle_key.to_string();
2014        *self
2015            .handle_line_numbers
2016            .entry(handle_key.to_string())
2017            .or_insert(0) += 1;
2018    }
2019
2020    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
2021    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
2022        if name.starts_with('^') {
2023            return name.to_string();
2024        }
2025        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
2026            let pkg = self.current_package();
2027            if !pkg.is_empty() && pkg != "main" {
2028                return format!("{}::{}", pkg, name);
2029            }
2030        }
2031        name.to_string()
2032    }
2033
2034    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
2035    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
2036        if name.contains("::") {
2037            return name.to_string();
2038        }
2039        let pkg = self.current_package();
2040        if pkg.is_empty() || pkg == "main" {
2041            format!("main::{}", name)
2042        } else {
2043            format!("{}::{}", pkg, name)
2044        }
2045    }
2046
2047    /// Bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
2048    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
2049        if name.contains("::") {
2050            return name.to_string();
2051        }
2052        for (lex, our) in self
2053            .english_lexical_scalars
2054            .iter()
2055            .zip(self.our_lexical_scalars.iter())
2056            .rev()
2057        {
2058            if lex.contains(name) {
2059                if our.contains(name) {
2060                    return self.stash_scalar_name_for_package(name);
2061                }
2062                return name.to_string();
2063            }
2064        }
2065        name.to_string()
2066    }
2067
2068    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
2069    pub(crate) fn tie_execute(
2070        &mut self,
2071        target_kind: u8,
2072        target_name: &str,
2073        class_and_args: Vec<PerlValue>,
2074        line: usize,
2075    ) -> PerlResult<PerlValue> {
2076        let mut it = class_and_args.into_iter();
2077        let class = it.next().unwrap_or(PerlValue::UNDEF);
2078        let pkg = class.to_string();
2079        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
2080        let tie_ctor = match target_kind {
2081            0 => "TIESCALAR",
2082            1 => "TIEARRAY",
2083            2 => "TIEHASH",
2084            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
2085        };
2086        let tie_fn = format!("{}::{}", pkg, tie_ctor);
2087        let sub = self
2088            .subs
2089            .get(&tie_fn)
2090            .cloned()
2091            .ok_or_else(|| PerlError::runtime(format!("tie: cannot find &{}", tie_fn), line))?;
2092        let mut call_args = vec![PerlValue::string(pkg.clone())];
2093        call_args.extend(it);
2094        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
2095            Ok(v) => v,
2096            Err(FlowOrError::Flow(_)) => PerlValue::UNDEF,
2097            Err(FlowOrError::Error(e)) => return Err(e),
2098        };
2099        match target_kind {
2100            0 => {
2101                self.tied_scalars.insert(target_name.to_string(), obj);
2102            }
2103            1 => {
2104                let key = self.stash_array_name_for_package(target_name);
2105                self.tied_arrays.insert(key, obj);
2106            }
2107            2 => {
2108                self.tied_hashes.insert(target_name.to_string(), obj);
2109            }
2110            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
2111        }
2112        Ok(PerlValue::UNDEF)
2113    }
2114
2115    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
2116    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
2117        let key = format!("{}::ISA", class);
2118        self.scope
2119            .get_array(&key)
2120            .into_iter()
2121            .map(|v| v.to_string())
2122            .collect()
2123    }
2124
2125    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
2126        let p = |c: &str| self.parents_of_class(c);
2127        linearize_c3(class, &p, 0)
2128    }
2129
2130    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
2131    pub(crate) fn resolve_method_full_name(
2132        &self,
2133        invocant_class: &str,
2134        method: &str,
2135        super_mode: bool,
2136    ) -> Option<String> {
2137        let mro = self.mro_linearize(invocant_class);
2138        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
2139        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
2140        // even when running `C::meth`.
2141        let start = if super_mode {
2142            mro.iter()
2143                .position(|p| p == invocant_class)
2144                .map(|i| i + 1)
2145                // If the class string does not appear in MRO (should be rare), skip the first
2146                // entry so we still search parents before giving up.
2147                .unwrap_or(1)
2148        } else {
2149            0
2150        };
2151        for pkg in mro.iter().skip(start) {
2152            if pkg == "UNIVERSAL" {
2153                continue;
2154            }
2155            let fq = format!("{}::{}", pkg, method);
2156            if self.subs.contains_key(&fq) {
2157                return Some(fq);
2158            }
2159        }
2160        mro.iter()
2161            .skip(start)
2162            .find(|p| *p != "UNIVERSAL")
2163            .map(|pkg| format!("{}::{}", pkg, method))
2164    }
2165
2166    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
2167        if let Some(alias) = self.glob_handle_alias.get(name) {
2168            return alias.clone();
2169        }
2170        // `print $fh …` stores the handle as "$varname"; resolve it by
2171        // reading the scalar variable which holds the IO handle name.
2172        if let Some(var_name) = name.strip_prefix('$') {
2173            let val = self.scope.get_scalar(var_name);
2174            let s = val.to_string();
2175            if !s.is_empty() {
2176                return self.resolve_io_handle_name(&s);
2177            }
2178        }
2179        name.to_string()
2180    }
2181
2182    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
2183    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
2184        if name.contains("::") {
2185            name.to_string()
2186        } else {
2187            self.qualify_sub_key(name)
2188        }
2189    }
2190
2191    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
2192    pub(crate) fn copy_typeglob_slots(
2193        &mut self,
2194        lhs: &str,
2195        rhs: &str,
2196        line: usize,
2197    ) -> PerlResult<()> {
2198        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
2199        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
2200        match self.subs.get(&rhs_sub).cloned() {
2201            Some(s) => {
2202                self.subs.insert(lhs_sub, s);
2203            }
2204            None => {
2205                self.subs.remove(&lhs_sub);
2206            }
2207        }
2208        let sv = self.scope.get_scalar(rhs);
2209        self.scope
2210            .set_scalar(lhs, sv.clone())
2211            .map_err(|e| e.at_line(line))?;
2212        let lhs_an = self.stash_array_name_for_package(lhs);
2213        let rhs_an = self.stash_array_name_for_package(rhs);
2214        let av = self.scope.get_array(&rhs_an);
2215        self.scope
2216            .set_array(&lhs_an, av.clone())
2217            .map_err(|e| e.at_line(line))?;
2218        let hv = self.scope.get_hash(rhs);
2219        self.scope
2220            .set_hash(lhs, hv.clone())
2221            .map_err(|e| e.at_line(line))?;
2222        match self.glob_handle_alias.get(rhs).cloned() {
2223            Some(t) => {
2224                self.glob_handle_alias.insert(lhs.to_string(), t);
2225            }
2226            None => {
2227                self.glob_handle_alias.remove(lhs);
2228            }
2229        }
2230        Ok(())
2231    }
2232
2233    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2234    pub(crate) fn install_format_decl(
2235        &mut self,
2236        basename: &str,
2237        lines: &[String],
2238        line: usize,
2239    ) -> PerlResult<()> {
2240        let pkg = self.current_package();
2241        let key = format!("{}::{}", pkg, basename);
2242        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2243        self.format_templates.insert(key, Arc::new(tmpl));
2244        Ok(())
2245    }
2246
2247    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2248    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2249        let pkg = self.current_package();
2250        let ent = self.overload_table.entry(pkg).or_default();
2251        for (k, v) in pairs {
2252            ent.insert(k.clone(), v.clone());
2253        }
2254    }
2255
2256    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2257    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2258    pub(crate) fn local_declare_typeglob(
2259        &mut self,
2260        lhs: &str,
2261        rhs: Option<&str>,
2262        line: usize,
2263    ) -> PerlResult<()> {
2264        let old = self.glob_handle_alias.remove(lhs);
2265        let Some(frame) = self.glob_restore_frames.last_mut() else {
2266            return Err(PerlError::runtime(
2267                "internal: no glob restore frame for local *GLOB",
2268                line,
2269            ));
2270        };
2271        frame.push((lhs.to_string(), old));
2272        if let Some(r) = rhs {
2273            self.glob_handle_alias
2274                .insert(lhs.to_string(), r.to_string());
2275        }
2276        Ok(())
2277    }
2278
2279    pub(crate) fn scope_push_hook(&mut self) {
2280        self.scope.push_frame();
2281        self.glob_restore_frames.push(Vec::new());
2282        self.special_var_restore_frames.push(Vec::new());
2283        self.english_lexical_scalars.push(HashSet::new());
2284        self.our_lexical_scalars.push(HashSet::new());
2285        self.state_bindings_stack.push(Vec::new());
2286    }
2287
2288    #[inline]
2289    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2290        if let Some(s) = self.english_lexical_scalars.last_mut() {
2291            s.insert(name.to_string());
2292        }
2293    }
2294
2295    #[inline]
2296    fn note_our_scalar(&mut self, bare_name: &str) {
2297        if let Some(s) = self.our_lexical_scalars.last_mut() {
2298            s.insert(bare_name.to_string());
2299        }
2300    }
2301
2302    pub(crate) fn scope_pop_hook(&mut self) {
2303        if !self.scope.can_pop_frame() {
2304            return;
2305        }
2306        // Execute deferred blocks in LIFO order before popping the frame.
2307        // Important: defer blocks run in the CURRENT scope (not a new frame),
2308        // so they can modify variables in the enclosing scope.
2309        let defers = self.scope.take_defers();
2310        for coderef in defers {
2311            if let Some(sub) = coderef.as_code_ref() {
2312                // Execute the defer block body directly in the current scope,
2313                // without creating a new frame or restoring closure captures.
2314                // This allows defer { $x = 100 } to modify the outer $x.
2315                let saved_wa = self.wantarray_kind;
2316                self.wantarray_kind = WantarrayCtx::Void;
2317                let _ = self.exec_block_no_scope(&sub.body);
2318                self.wantarray_kind = saved_wa;
2319            }
2320        }
2321        // Save state variable values back before popping the frame
2322        if let Some(bindings) = self.state_bindings_stack.pop() {
2323            for (var_name, state_key) in &bindings {
2324                let val = self.scope.get_scalar(var_name).clone();
2325                self.state_vars.insert(state_key.clone(), val);
2326            }
2327        }
2328        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2329        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2330        if let Some(entries) = self.special_var_restore_frames.pop() {
2331            for (name, old) in entries.into_iter().rev() {
2332                let _ = self.set_special_var(&name, &old);
2333            }
2334        }
2335        if let Some(entries) = self.glob_restore_frames.pop() {
2336            for (name, old) in entries.into_iter().rev() {
2337                match old {
2338                    Some(s) => {
2339                        self.glob_handle_alias.insert(name, s);
2340                    }
2341                    None => {
2342                        self.glob_handle_alias.remove(&name);
2343                    }
2344                }
2345            }
2346        }
2347        self.scope.pop_frame();
2348        let _ = self.english_lexical_scalars.pop();
2349        let _ = self.our_lexical_scalars.pop();
2350    }
2351
2352    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2353    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2354    #[inline]
2355    pub(crate) fn enable_parallel_guard(&mut self) {
2356        self.scope.set_parallel_guard(true);
2357    }
2358
2359    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues after VM compilation.
2360    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2361        self.begin_blocks.clear();
2362        self.unit_check_blocks.clear();
2363        self.check_blocks.clear();
2364        self.init_blocks.clear();
2365        self.end_blocks.clear();
2366    }
2367
2368    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2369    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2370    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2371    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2372    /// there because it only calls [`Scope::pop_frame`].
2373    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2374        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2375            self.scope_pop_hook();
2376        }
2377    }
2378
2379    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2380    ///
2381    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2382    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2383    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2384    /// workers that call `perl_signal::poll`).
2385    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> PerlResult<()> {
2386        self.touch_env_hash("SIG");
2387        let v = self.scope.get_hash_element("SIG", sig);
2388        if v.is_undef() {
2389            return Self::default_sig_action(sig);
2390        }
2391        if let Some(s) = v.as_str() {
2392            if s == "IGNORE" {
2393                return Ok(());
2394            }
2395            if s == "DEFAULT" {
2396                return Self::default_sig_action(sig);
2397            }
2398        }
2399        if let Some(sub) = v.as_code_ref() {
2400            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2401                Ok(_) => Ok(()),
2402                Err(FlowOrError::Flow(_)) => Ok(()),
2403                Err(FlowOrError::Error(e)) => Err(e),
2404            }
2405        } else {
2406            Self::default_sig_action(sig)
2407        }
2408    }
2409
2410    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2411    #[inline]
2412    fn default_sig_action(sig: &str) -> PerlResult<()> {
2413        match sig {
2414            // 128 + signal number (common shell convention)
2415            "INT" => std::process::exit(130),
2416            "TERM" => std::process::exit(143),
2417            "ALRM" => std::process::exit(142),
2418            // Default for SIGCHLD is ignore
2419            "CHLD" => Ok(()),
2420            _ => Ok(()),
2421        }
2422    }
2423
2424    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2425    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2426    pub fn materialize_env_if_needed(&mut self) {
2427        if self.env_materialized {
2428            return;
2429        }
2430        self.env = std::env::vars()
2431            .map(|(k, v)| (k, PerlValue::string(v)))
2432            .collect();
2433        self.scope
2434            .set_hash("ENV", self.env.clone())
2435            .expect("set %ENV");
2436        self.env_materialized = true;
2437    }
2438
2439    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2440    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2441        self.materialize_env_if_needed();
2442        if let Some(x) = self.log_level_override {
2443            return x;
2444        }
2445        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2446        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2447    }
2448
2449    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2450    pub(crate) fn no_color_effective(&mut self) -> bool {
2451        self.materialize_env_if_needed();
2452        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2453        if v.is_undef() {
2454            return false;
2455        }
2456        !v.to_string().is_empty()
2457    }
2458
2459    #[inline]
2460    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2461        if hash_name == "ENV" {
2462            self.materialize_env_if_needed();
2463        } else if !self.reflection_hashes_ready && !self.scope.has_lexical_hash(hash_name) {
2464            match hash_name {
2465                "b"
2466                | "pc"
2467                | "e"
2468                | "a"
2469                | "d"
2470                | "c"
2471                | "p"
2472                | "all"
2473                | "stryke::builtins"
2474                | "stryke::perl_compats"
2475                | "stryke::extensions"
2476                | "stryke::aliases"
2477                | "stryke::descriptions"
2478                | "stryke::categories"
2479                | "stryke::primaries"
2480                | "stryke::all" => {
2481                    self.ensure_reflection_hashes();
2482                }
2483                _ => {}
2484            }
2485        }
2486    }
2487
2488    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2489    pub(crate) fn exists_arrow_hash_element(
2490        &self,
2491        container: PerlValue,
2492        key: &str,
2493        line: usize,
2494    ) -> PerlResult<bool> {
2495        if let Some(r) = container.as_hash_ref() {
2496            return Ok(r.read().contains_key(key));
2497        }
2498        if let Some(b) = container.as_blessed_ref() {
2499            let data = b.data.read();
2500            if let Some(r) = data.as_hash_ref() {
2501                return Ok(r.read().contains_key(key));
2502            }
2503            if let Some(hm) = data.as_hash_map() {
2504                return Ok(hm.contains_key(key));
2505            }
2506            return Err(PerlError::runtime(
2507                "exists argument is not a HASH reference",
2508                line,
2509            ));
2510        }
2511        Err(PerlError::runtime(
2512            "exists argument is not a HASH reference",
2513            line,
2514        ))
2515    }
2516
2517    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
2518    pub(crate) fn delete_arrow_hash_element(
2519        &self,
2520        container: PerlValue,
2521        key: &str,
2522        line: usize,
2523    ) -> PerlResult<PerlValue> {
2524        if let Some(r) = container.as_hash_ref() {
2525            return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2526        }
2527        if let Some(b) = container.as_blessed_ref() {
2528            let mut data = b.data.write();
2529            if let Some(r) = data.as_hash_ref() {
2530                return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2531            }
2532            if let Some(mut map) = data.as_hash_map() {
2533                let v = map.shift_remove(key).unwrap_or(PerlValue::UNDEF);
2534                *data = PerlValue::hash(map);
2535                return Ok(v);
2536            }
2537            return Err(PerlError::runtime(
2538                "delete argument is not a HASH reference",
2539                line,
2540            ));
2541        }
2542        Err(PerlError::runtime(
2543            "delete argument is not a HASH reference",
2544            line,
2545        ))
2546    }
2547
2548    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
2549    pub(crate) fn exists_arrow_array_element(
2550        &self,
2551        container: PerlValue,
2552        idx: i64,
2553        line: usize,
2554    ) -> PerlResult<bool> {
2555        if let Some(a) = container.as_array_ref() {
2556            let arr = a.read();
2557            let i = if idx < 0 {
2558                (arr.len() as i64 + idx) as usize
2559            } else {
2560                idx as usize
2561            };
2562            return Ok(i < arr.len());
2563        }
2564        Err(PerlError::runtime(
2565            "exists argument is not an ARRAY reference",
2566            line,
2567        ))
2568    }
2569
2570    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
2571    pub(crate) fn delete_arrow_array_element(
2572        &self,
2573        container: PerlValue,
2574        idx: i64,
2575        line: usize,
2576    ) -> PerlResult<PerlValue> {
2577        if let Some(a) = container.as_array_ref() {
2578            let mut arr = a.write();
2579            let i = if idx < 0 {
2580                (arr.len() as i64 + idx) as usize
2581            } else {
2582                idx as usize
2583            };
2584            if i >= arr.len() {
2585                return Ok(PerlValue::UNDEF);
2586            }
2587            let old = arr.get(i).cloned().unwrap_or(PerlValue::UNDEF);
2588            arr[i] = PerlValue::UNDEF;
2589            return Ok(old);
2590        }
2591        Err(PerlError::runtime(
2592            "delete argument is not an ARRAY reference",
2593            line,
2594        ))
2595    }
2596
2597    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
2598    pub(crate) fn inc_directories(&self) -> Vec<String> {
2599        let mut v: Vec<String> = self
2600            .scope
2601            .get_array("INC")
2602            .into_iter()
2603            .map(|x| x.to_string())
2604            .filter(|s| !s.is_empty())
2605            .collect();
2606        if v.is_empty() {
2607            v.push(".".to_string());
2608        }
2609        v
2610    }
2611
2612    #[inline]
2613    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
2614        matches!(
2615            name,
2616            "_" | "0"
2617                | "!"
2618                | "@"
2619                | "/"
2620                | "\\"
2621                | ","
2622                | "."
2623                | "__PACKAGE__"
2624                | "$$"
2625                | "|"
2626                | "?"
2627                | "\""
2628                | "&"
2629                | "`"
2630                | "'"
2631                | "+"
2632                | "<"
2633                | ">"
2634                | "("
2635                | ")"
2636                | "]"
2637                | ";"
2638                | "ARGV"
2639                | "%"
2640                | "="
2641                | "-"
2642                | ":"
2643                | "*"
2644                | "INC"
2645        ) || name.chars().all(|c| c.is_ascii_digit())
2646            || name.starts_with('^')
2647            || (name.starts_with('#') && name.len() > 1)
2648    }
2649
2650    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2651        if !self.strict_vars
2652            || Self::strict_scalar_exempt(name)
2653            || name.contains("::")
2654            || self.scope.scalar_binding_exists(name)
2655        {
2656            return Ok(());
2657        }
2658        Err(PerlError::runtime(
2659            format!(
2660                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
2661                name, name
2662            ),
2663            line,
2664        )
2665        .into())
2666    }
2667
2668    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2669        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
2670            return Ok(());
2671        }
2672        Err(PerlError::runtime(
2673            format!(
2674                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
2675                name, name
2676            ),
2677            line,
2678        )
2679        .into())
2680    }
2681
2682    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2683        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
2684        if !self.strict_vars
2685            || name.contains("::")
2686            || self.scope.hash_binding_exists(name)
2687            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
2688        {
2689            return Ok(());
2690        }
2691        Err(PerlError::runtime(
2692            format!(
2693                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
2694                name, name
2695            ),
2696            line,
2697        )
2698        .into())
2699    }
2700
2701    fn looks_like_version_only(spec: &str) -> bool {
2702        let t = spec.trim();
2703        !t.is_empty()
2704            && !t.contains('/')
2705            && !t.contains('\\')
2706            && !t.contains("::")
2707            && t.chars()
2708                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
2709            && t.chars().any(|c| c.is_ascii_digit())
2710    }
2711
2712    fn module_spec_to_relpath(spec: &str) -> String {
2713        let t = spec.trim();
2714        if t.contains("::") {
2715            format!("{}.pm", t.replace("::", "/"))
2716        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
2717            t.replace('\\', "/")
2718        } else {
2719            format!("{}.pm", t)
2720        }
2721    }
2722
2723    /// Lockfile-driven module resolution (RFC §"Module Resolution"). Walks up from
2724    /// `cwd` for `stryke.toml`, then asks [`crate::pkg::commands::resolve_module`]
2725    /// to find the module either in `lib/` or in the lockfile-pinned store. The
2726    /// `relpath` arg is the `@INC`-style path (`Foo/Bar.pm`) used elsewhere in
2727    /// `require`; it is converted to a logical name (`Foo::Bar`) for the resolver.
2728    /// Both `.pm` and `.stk` variants are tried — stryke source uses `.stk`.
2729    fn try_resolve_via_lockfile(relpath: &str) -> Option<std::path::PathBuf> {
2730        let cwd = std::env::current_dir().ok()?;
2731        let project_root = crate::pkg::commands::find_project_root(&cwd)?;
2732
2733        // Convert "Foo/Bar.pm" → "Foo::Bar". Drop the trailing extension so
2734        // `resolve_module` (which appends `.stk`) builds the right path.
2735        let stem = relpath
2736            .strip_suffix(".pm")
2737            .or_else(|| relpath.strip_suffix(".pl"))
2738            .or_else(|| relpath.strip_suffix(".stk"))
2739            .unwrap_or(relpath);
2740        let logical = stem.replace('/', "::");
2741
2742        match crate::pkg::commands::resolve_module(&project_root, &logical) {
2743            Ok(found) => found,
2744            Err(_) => None,
2745        }
2746    }
2747
2748    /// `sub name` in `package P` → stash key `P::name`. `sub Q::name { }` is already fully
2749    /// qualified — do not prepend the current package. Unqualified names in `main` are stored
2750    /// **bare** (`name`), matching the compiler's `Op::Call` interning so the VM's
2751    /// `sub_for_closure_restore` lookup hits in one step.
2752    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
2753        if name.contains("::") {
2754            return name.to_string();
2755        }
2756        let pkg = self.current_package();
2757        if pkg.is_empty() || pkg == "main" {
2758            name.to_string()
2759        } else {
2760            format!("{}::{}", pkg, name)
2761        }
2762    }
2763
2764    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
2765    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
2766        let mut msg = format!("Undefined subroutine &{}", name);
2767        if self.strict_subs {
2768            msg.push_str(
2769                " (strict subs: declare the sub or use a fully qualified name before calling)",
2770            );
2771        }
2772        msg
2773    }
2774
2775    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
2776    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
2777        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
2778        if self.strict_subs {
2779            msg.push_str(
2780                " (strict subs: declare the sub or use a fully qualified name before calling)",
2781            );
2782        }
2783        msg
2784    }
2785
2786    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
2787    fn import_alias_key(&self, short: &str) -> String {
2788        self.qualify_sub_key(short)
2789    }
2790
2791    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
2792    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
2793        if imports.len() == 1 {
2794            match &imports[0].kind {
2795                ExprKind::QW(ws) => return ws.is_empty(),
2796                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
2797                ExprKind::List(xs) => return xs.is_empty(),
2798                _ => {}
2799            }
2800        }
2801        false
2802    }
2803
2804    /// After `require`, copy `Module::export` → caller stash per `use` list.
2805    fn apply_module_import(
2806        &mut self,
2807        module: &str,
2808        imports: &[Expr],
2809        line: usize,
2810    ) -> PerlResult<()> {
2811        if imports.is_empty() {
2812            return self.import_all_from_module(module, line);
2813        }
2814        if Self::is_explicit_empty_import_list(imports) {
2815            return Ok(());
2816        }
2817        let names = Self::pragma_import_strings(imports, line)?;
2818        if names.is_empty() {
2819            return Ok(());
2820        }
2821        for name in names {
2822            self.import_one_symbol(module, &name, line)?;
2823        }
2824        Ok(())
2825    }
2826
2827    fn import_all_from_module(&mut self, module: &str, line: usize) -> PerlResult<()> {
2828        if let Some(lists) = self.module_export_lists.get(module) {
2829            let export: Vec<String> = lists.export.clone();
2830            for short in export {
2831                self.import_named_sub(module, &short, line)?;
2832            }
2833            return Ok(());
2834        }
2835        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
2836        let prefix = format!("{}::", module);
2837        let keys: Vec<String> = self
2838            .subs
2839            .keys()
2840            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
2841            .cloned()
2842            .collect();
2843        for k in keys {
2844            let short = k[prefix.len()..].to_string();
2845            if let Some(sub) = self.subs.get(&k).cloned() {
2846                let alias = self.import_alias_key(&short);
2847                self.subs.insert(alias, sub);
2848            }
2849        }
2850        Ok(())
2851    }
2852
2853    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
2854    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> PerlResult<()> {
2855        let qual = format!("{}::{}", module, short);
2856        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
2857            PerlError::runtime(
2858                format!(
2859                    "`{}` is not defined in module `{}` (expected `{}`)",
2860                    short, module, qual
2861                ),
2862                line,
2863            )
2864        })?;
2865        let alias = self.import_alias_key(short);
2866        self.subs.insert(alias, sub);
2867        Ok(())
2868    }
2869
2870    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> PerlResult<()> {
2871        if let Some(lists) = self.module_export_lists.get(module) {
2872            let allowed: HashSet<&str> = lists
2873                .export
2874                .iter()
2875                .map(|s| s.as_str())
2876                .chain(lists.export_ok.iter().map(|s| s.as_str()))
2877                .collect();
2878            if !allowed.contains(export) {
2879                return Err(PerlError::runtime(
2880                    format!(
2881                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
2882                        export, module
2883                    ),
2884                    line,
2885                ));
2886            }
2887        }
2888        self.import_named_sub(module, export, line)
2889    }
2890
2891    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
2892    fn record_exporter_our_array_name(&mut self, name: &str, items: &[PerlValue]) {
2893        if name != "EXPORT" && name != "EXPORT_OK" {
2894            return;
2895        }
2896        let pkg = self.current_package();
2897        if pkg.is_empty() || pkg == "main" {
2898            return;
2899        }
2900        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
2901        let ent = self.module_export_lists.entry(pkg).or_default();
2902        if name == "EXPORT" {
2903            ent.export = names;
2904        } else {
2905            ent.export_ok = names;
2906        }
2907    }
2908
2909    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
2910    /// Refresh [`PerlSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
2911    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
2912    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
2913        let key = self.qualify_sub_key(name);
2914        let Some(sub) = self.subs.get(&key).cloned() else {
2915            return;
2916        };
2917        let captured = self.scope.capture();
2918        let closure_env = if captured.is_empty() {
2919            None
2920        } else {
2921            Some(captured)
2922        };
2923        let mut new_sub = (*sub).clone();
2924        new_sub.closure_env = closure_env;
2925        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
2926        self.subs.insert(key, Arc::new(new_sub));
2927    }
2928
2929    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<PerlSub>> {
2930        if let Some(s) = self.subs.get(name) {
2931            return Some(s.clone());
2932        }
2933        if !name.contains("::") {
2934            // Non-`main` packages store subs at `Pkg::name`; resolve bare callers there.
2935            let pkg = self.current_package();
2936            if !pkg.is_empty() && pkg != "main" {
2937                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
2938                q.push_str(&pkg);
2939                q.push_str("::");
2940                q.push_str(name);
2941                return self.subs.get(&q).cloned();
2942            }
2943            return None;
2944        }
2945        // `\&main::greet` / `defined &main::greet`: subs in `main` are stored bare so the
2946        // compiler's `Op::Call("greet", ...)` and the runtime stash lookup share a key.
2947        // Strip the `main::` qualifier and try the bare form so explicit qualified callers
2948        // still resolve to the same sub.
2949        if let Some(rest) = name.strip_prefix("main::") {
2950            if !rest.contains("::") {
2951                return self.subs.get(rest).cloned();
2952            }
2953        }
2954        None
2955    }
2956
2957    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
2958    /// before calling `import`).
2959    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
2960        if let Some(first) = imports.first() {
2961            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
2962                return &imports[1..];
2963            }
2964        }
2965        imports
2966    }
2967
2968    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
2969    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> PerlResult<Vec<String>> {
2970        let mut out = Vec::new();
2971        for e in imports {
2972            match &e.kind {
2973                ExprKind::String(s) => out.push(s.clone()),
2974                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
2975                ExprKind::Integer(n) => out.push(n.to_string()),
2976                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
2977                // a single interpolated variable.  Reconstruct the sigil+name form.
2978                ExprKind::InterpolatedString(parts) => {
2979                    let mut s = String::new();
2980                    for p in parts {
2981                        match p {
2982                            StringPart::Literal(l) => s.push_str(l),
2983                            StringPart::ScalarVar(v) => {
2984                                s.push('$');
2985                                s.push_str(v);
2986                            }
2987                            StringPart::ArrayVar(v) => {
2988                                s.push('@');
2989                                s.push_str(v);
2990                            }
2991                            _ => {
2992                                return Err(PerlError::runtime(
2993                                    "pragma import must be a compile-time string, qw(), or integer",
2994                                    e.line.max(default_line),
2995                                ));
2996                            }
2997                        }
2998                    }
2999                    out.push(s);
3000                }
3001                _ => {
3002                    return Err(PerlError::runtime(
3003                        "pragma import must be a compile-time string, qw(), or integer",
3004                        e.line.max(default_line),
3005                    ));
3006                }
3007            }
3008        }
3009        Ok(out)
3010    }
3011
3012    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3013        if imports.is_empty() {
3014            self.strict_refs = true;
3015            self.strict_subs = true;
3016            self.strict_vars = true;
3017            return Ok(());
3018        }
3019        let names = Self::pragma_import_strings(imports, line)?;
3020        for name in names {
3021            match name.as_str() {
3022                "refs" => self.strict_refs = true,
3023                "subs" => self.strict_subs = true,
3024                "vars" => self.strict_vars = true,
3025                _ => {
3026                    return Err(PerlError::runtime(
3027                        format!("Unknown strict mode `{}`", name),
3028                        line,
3029                    ));
3030                }
3031            }
3032        }
3033        Ok(())
3034    }
3035
3036    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3037        if imports.is_empty() {
3038            self.strict_refs = false;
3039            self.strict_subs = false;
3040            self.strict_vars = false;
3041            return Ok(());
3042        }
3043        let names = Self::pragma_import_strings(imports, line)?;
3044        for name in names {
3045            match name.as_str() {
3046                "refs" => self.strict_refs = false,
3047                "subs" => self.strict_subs = false,
3048                "vars" => self.strict_vars = false,
3049                _ => {
3050                    return Err(PerlError::runtime(
3051                        format!("Unknown strict mode `{}`", name),
3052                        line,
3053                    ));
3054                }
3055            }
3056        }
3057        Ok(())
3058    }
3059
3060    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3061        let items = Self::pragma_import_strings(imports, line)?;
3062        if items.is_empty() {
3063            return Err(PerlError::runtime(
3064                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
3065                line,
3066            ));
3067        }
3068        for item in items {
3069            let s = item.trim();
3070            if let Some(rest) = s.strip_prefix(':') {
3071                self.apply_feature_bundle(rest, line)?;
3072            } else {
3073                self.apply_feature_name(s, true, line)?;
3074            }
3075        }
3076        Ok(())
3077    }
3078
3079    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3080        if imports.is_empty() {
3081            self.feature_bits = 0;
3082            return Ok(());
3083        }
3084        let items = Self::pragma_import_strings(imports, line)?;
3085        for item in items {
3086            let s = item.trim();
3087            if let Some(rest) = s.strip_prefix(':') {
3088                self.clear_feature_bundle(rest);
3089            } else {
3090                self.apply_feature_name(s, false, line)?;
3091            }
3092        }
3093        Ok(())
3094    }
3095
3096    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> PerlResult<()> {
3097        let key = v.trim();
3098        match key {
3099            "5.10" | "5.010" | "5.10.0" => {
3100                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3101            }
3102            "5.12" | "5.012" | "5.12.0" => {
3103                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3104            }
3105            _ => {
3106                return Err(PerlError::runtime(
3107                    format!("unsupported feature bundle :{}", key),
3108                    line,
3109                ));
3110            }
3111        }
3112        Ok(())
3113    }
3114
3115    fn clear_feature_bundle(&mut self, v: &str) {
3116        let key = v.trim();
3117        if matches!(
3118            key,
3119            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
3120        ) {
3121            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
3122        }
3123    }
3124
3125    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> PerlResult<()> {
3126        let bit = match name {
3127            "say" => FEAT_SAY,
3128            "state" => FEAT_STATE,
3129            "switch" => FEAT_SWITCH,
3130            "unicode_strings" => FEAT_UNICODE_STRINGS,
3131            // Features that stryke accepts as known but tracks no separate bit for —
3132            // either always-on, always-off, or syntactic sugar already enabled.
3133            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
3134            "postderef"
3135            | "postderef_qq"
3136            | "evalbytes"
3137            | "current_sub"
3138            | "fc"
3139            | "lexical_subs"
3140            | "signatures"
3141            | "refaliasing"
3142            | "bitwise"
3143            | "isa"
3144            | "indirect"
3145            | "multidimensional"
3146            | "bareword_filehandles"
3147            | "try"
3148            | "defer"
3149            | "extra_paired_delimiters"
3150            | "module_true"
3151            | "class"
3152            | "array_base" => return Ok(()),
3153            _ => {
3154                return Err(PerlError::runtime(
3155                    format!("unknown feature `{}`", name),
3156                    line,
3157                ));
3158            }
3159        };
3160        if enable {
3161            self.feature_bits |= bit;
3162        } else {
3163            self.feature_bits &= !bit;
3164        }
3165        Ok(())
3166    }
3167
3168    /// `require EXPR` — load once, record `%INC`, return `1` on success.
3169    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> PerlResult<PerlValue> {
3170        let t = spec.trim();
3171        if t.is_empty() {
3172            return Err(PerlError::runtime("require: empty argument", line));
3173        }
3174        match t {
3175            "strict" => {
3176                self.apply_use_strict(&[], line)?;
3177                return Ok(PerlValue::integer(1));
3178            }
3179            "utf8" => {
3180                self.utf8_pragma = true;
3181                return Ok(PerlValue::integer(1));
3182            }
3183            "feature" | "v5" => {
3184                return Ok(PerlValue::integer(1));
3185            }
3186            "warnings" => {
3187                self.warnings = true;
3188                return Ok(PerlValue::integer(1));
3189            }
3190            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
3191                return Ok(PerlValue::integer(1));
3192            }
3193            _ => {}
3194        }
3195        let p = Path::new(t);
3196        if p.is_absolute() {
3197            return self.require_absolute_path(p, line);
3198        }
3199        if t.starts_with("./") || t.starts_with("../") {
3200            return self.require_relative_path(p, line);
3201        }
3202        if Self::looks_like_version_only(t) {
3203            return Ok(PerlValue::integer(1));
3204        }
3205        let relpath = Self::module_spec_to_relpath(t);
3206        self.require_from_inc(&relpath, line)
3207    }
3208
3209    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
3210    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> PerlResult<()> {
3211        let v = self.scope.get_hash_element("^HOOK", key);
3212        if v.is_undef() {
3213            return Ok(());
3214        }
3215        let Some(sub) = v.as_code_ref() else {
3216            return Ok(());
3217        };
3218        let r = self.call_sub(
3219            sub.as_ref(),
3220            vec![PerlValue::string(path.to_string())],
3221            WantarrayCtx::Scalar,
3222            line,
3223        );
3224        match r {
3225            Ok(_) => Ok(()),
3226            Err(FlowOrError::Error(e)) => Err(e),
3227            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
3228            Err(FlowOrError::Flow(other)) => Err(PerlError::runtime(
3229                format!(
3230                    "require hook {:?} returned unexpected control flow: {:?}",
3231                    key, other
3232                ),
3233                line,
3234            )),
3235        }
3236    }
3237
3238    fn require_absolute_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3239        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
3240        let key = canon.to_string_lossy().into_owned();
3241        if self.scope.exists_hash_element("INC", &key) {
3242            return Ok(PerlValue::integer(1));
3243        }
3244        self.invoke_require_hook("require__before", &key, line)?;
3245        let code = read_file_text_perl_compat(&canon).map_err(|e| {
3246            PerlError::runtime(
3247                format!("Can't open {} for reading: {}", canon.display(), e),
3248                line,
3249            )
3250        })?;
3251        let code = crate::data_section::strip_perl_end_marker(&code);
3252        self.scope
3253            .set_hash_element("INC", &key, PerlValue::string(key.clone()))?;
3254        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3255        let r = crate::parse_and_run_module_in_file(code, self, &key);
3256        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3257        r?;
3258        self.invoke_require_hook("require__after", &key, line)?;
3259        Ok(PerlValue::integer(1))
3260    }
3261
3262    fn require_relative_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3263        if !path.exists() {
3264            return Err(PerlError::runtime(
3265                format!(
3266                    "Can't locate {} (relative path does not exist)",
3267                    path.display()
3268                ),
3269                line,
3270            ));
3271        }
3272        self.require_absolute_path(path, line)
3273    }
3274
3275    fn require_from_inc(&mut self, relpath: &str, line: usize) -> PerlResult<PerlValue> {
3276        if self.scope.exists_hash_element("INC", relpath) {
3277            return Ok(PerlValue::integer(1));
3278        }
3279        self.invoke_require_hook("require__before", relpath, line)?;
3280
3281        // Lockfile-driven module resolution. When the cwd is inside a stryke
3282        // project (`stryke.toml` reachable), `use Foo::Bar` first looks at
3283        // `lib/Foo/Bar.stk` and then at lockfile-pinned store entries before
3284        // falling through to `@INC`. See docs/PACKAGE_REGISTRY.md §"Module
3285        // Resolution".
3286        if let Some(found) = Self::try_resolve_via_lockfile(relpath) {
3287            let code = read_file_text_perl_compat(&found).map_err(|e| {
3288                PerlError::runtime(
3289                    format!("Can't open {} for reading: {}", found.display(), e),
3290                    line,
3291                )
3292            })?;
3293            let code = crate::data_section::strip_perl_end_marker(&code);
3294            let abs = found.canonicalize().unwrap_or(found);
3295            let abs_s = abs.to_string_lossy().into_owned();
3296            self.scope
3297                .set_hash_element("INC", relpath, PerlValue::string(abs_s.clone()))?;
3298            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3299            let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3300            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3301            r?;
3302            self.invoke_require_hook("require__after", relpath, line)?;
3303            return Ok(PerlValue::integer(1));
3304        }
3305
3306        // Check virtual modules first (AOT bundles).
3307        if let Some(code) = self.virtual_modules.get(relpath).cloned() {
3308            let code = crate::data_section::strip_perl_end_marker(&code);
3309            self.scope.set_hash_element(
3310                "INC",
3311                relpath,
3312                PerlValue::string(format!("(virtual)/{}", relpath)),
3313            )?;
3314            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3315            let r = crate::parse_and_run_module_in_file(code, self, relpath);
3316            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3317            r?;
3318            self.invoke_require_hook("require__after", relpath, line)?;
3319            return Ok(PerlValue::integer(1));
3320        }
3321
3322        for dir in self.inc_directories() {
3323            let full = Path::new(&dir).join(relpath);
3324            if full.is_file() {
3325                let code = read_file_text_perl_compat(&full).map_err(|e| {
3326                    PerlError::runtime(
3327                        format!("Can't open {} for reading: {}", full.display(), e),
3328                        line,
3329                    )
3330                })?;
3331                let code = crate::data_section::strip_perl_end_marker(&code);
3332                let abs = full.canonicalize().unwrap_or(full);
3333                let abs_s = abs.to_string_lossy().into_owned();
3334                self.scope
3335                    .set_hash_element("INC", relpath, PerlValue::string(abs_s.clone()))?;
3336                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3337                let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3338                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3339                r?;
3340                self.invoke_require_hook("require__after", relpath, line)?;
3341                return Ok(PerlValue::integer(1));
3342            }
3343        }
3344        Err(PerlError::runtime(
3345            format!(
3346                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
3347                relpath
3348            ),
3349            line,
3350        ))
3351    }
3352
3353    /// Register a virtual module (for AOT bundles). Path should be relative like "lib/foo.stk".
3354    pub fn register_virtual_module(&mut self, path: String, source: String) {
3355        self.virtual_modules.insert(path, source);
3356    }
3357
3358    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
3359    pub(crate) fn exec_use_stmt(
3360        &mut self,
3361        module: &str,
3362        imports: &[Expr],
3363        line: usize,
3364    ) -> PerlResult<()> {
3365        match module {
3366            "strict" => self.apply_use_strict(imports, line),
3367            "utf8" => {
3368                if !imports.is_empty() {
3369                    return Err(PerlError::runtime("use utf8 takes no arguments", line));
3370                }
3371                self.utf8_pragma = true;
3372                Ok(())
3373            }
3374            "feature" => self.apply_use_feature(imports, line),
3375            "v5" => Ok(()),
3376            "warnings" => {
3377                self.warnings = true;
3378                Ok(())
3379            }
3380            "English" => {
3381                self.english_enabled = true;
3382                let args = Self::pragma_import_strings(imports, line)?;
3383                let no_match = args.iter().any(|a| a == "-no_match_vars");
3384                // Once match vars are exported (use English without -no_match_vars),
3385                // they stay available for the rest of the program — Perl exports them
3386                // into the caller's namespace and later pragmas cannot un-export them.
3387                if !no_match {
3388                    self.english_match_vars_ever_enabled = true;
3389                }
3390                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
3391                Ok(())
3392            }
3393            "Env" => self.apply_use_env(imports, line),
3394            "open" => self.apply_use_open(imports, line),
3395            "constant" => self.apply_use_constant(imports, line),
3396            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3397            _ => {
3398                self.require_execute(module, line)?;
3399                let imports = Self::imports_after_leading_use_version(imports);
3400                self.apply_module_import(module, imports, line)?;
3401                Ok(())
3402            }
3403        }
3404    }
3405
3406    /// `no strict 'refs'`, `no warnings`, `no feature`, …
3407    pub(crate) fn exec_no_stmt(
3408        &mut self,
3409        module: &str,
3410        imports: &[Expr],
3411        line: usize,
3412    ) -> PerlResult<()> {
3413        match module {
3414            "strict" => self.apply_no_strict(imports, line),
3415            "utf8" => {
3416                if !imports.is_empty() {
3417                    return Err(PerlError::runtime("no utf8 takes no arguments", line));
3418                }
3419                self.utf8_pragma = false;
3420                Ok(())
3421            }
3422            "feature" => self.apply_no_feature(imports, line),
3423            "v5" => Ok(()),
3424            "warnings" => {
3425                self.warnings = false;
3426                Ok(())
3427            }
3428            "English" => {
3429                self.english_enabled = false;
3430                // Don't reset no_match_vars here — if match vars were ever enabled,
3431                // they persist (Perl's export cannot be un-exported).
3432                if !self.english_match_vars_ever_enabled {
3433                    self.english_no_match_vars = false;
3434                }
3435                Ok(())
3436            }
3437            "open" => {
3438                self.open_pragma_utf8 = false;
3439                Ok(())
3440            }
3441            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3442            _ => Ok(()),
3443        }
3444    }
3445
3446    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
3447    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3448        let names = Self::pragma_import_strings(imports, line)?;
3449        for n in names {
3450            let key = n.trim_start_matches('@');
3451            if key.eq_ignore_ascii_case("PATH") {
3452                let path_env = std::env::var("PATH").unwrap_or_default();
3453                let path_vec: Vec<PerlValue> = std::env::split_paths(&path_env)
3454                    .map(|p| PerlValue::string(p.to_string_lossy().into_owned()))
3455                    .collect();
3456                let aname = self.stash_array_name_for_package("PATH");
3457                self.scope.declare_array(&aname, path_vec);
3458            }
3459        }
3460        Ok(())
3461    }
3462
3463    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
3464    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3465        let items = Self::pragma_import_strings(imports, line)?;
3466        for item in items {
3467            let s = item.trim();
3468            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
3469                self.open_pragma_utf8 = true;
3470                continue;
3471            }
3472            if let Some(rest) = s.strip_prefix(":encoding(") {
3473                if let Some(inner) = rest.strip_suffix(')') {
3474                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
3475                        self.open_pragma_utf8 = true;
3476                    }
3477                }
3478            }
3479        }
3480        Ok(())
3481    }
3482
3483    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
3484    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3485        if imports.is_empty() {
3486            return Ok(());
3487        }
3488        // `use constant 1.03;` — version check only (ignored here).
3489        if imports.len() == 1 {
3490            match &imports[0].kind {
3491                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
3492                _ => {}
3493            }
3494        }
3495        for imp in imports {
3496            match &imp.kind {
3497                ExprKind::List(items) => {
3498                    if items.len() % 2 != 0 {
3499                        return Err(PerlError::runtime(
3500                            format!(
3501                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
3502                                items.len()
3503                            ),
3504                            line,
3505                        ));
3506                    }
3507                    let mut i = 0;
3508                    while i < items.len() {
3509                        let name = match &items[i].kind {
3510                            ExprKind::String(s) => s.clone(),
3511                            _ => {
3512                                return Err(PerlError::runtime(
3513                                    "use constant: constant name must be a string literal",
3514                                    line,
3515                                ));
3516                            }
3517                        };
3518                        let val = match self.eval_expr(&items[i + 1]) {
3519                            Ok(v) => v,
3520                            Err(FlowOrError::Error(e)) => return Err(e),
3521                            Err(FlowOrError::Flow(_)) => {
3522                                return Err(PerlError::runtime(
3523                                    "use constant: unexpected control flow in initializer",
3524                                    line,
3525                                ));
3526                            }
3527                        };
3528                        self.install_constant_sub(&name, &val, line)?;
3529                        i += 2;
3530                    }
3531                }
3532                _ => {
3533                    return Err(PerlError::runtime(
3534                        "use constant: expected list of NAME => VALUE pairs",
3535                        line,
3536                    ));
3537                }
3538            }
3539        }
3540        Ok(())
3541    }
3542
3543    fn install_constant_sub(&mut self, name: &str, val: &PerlValue, line: usize) -> PerlResult<()> {
3544        let key = self.qualify_sub_key(name);
3545        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
3546        let body = vec![Statement {
3547            label: None,
3548            kind: StmtKind::Return(Some(ret_expr)),
3549            line,
3550        }];
3551        self.subs.insert(
3552            key.clone(),
3553            Arc::new(PerlSub {
3554                name: key,
3555                params: vec![],
3556                body,
3557                prototype: None,
3558                closure_env: None,
3559                fib_like: None,
3560            }),
3561        );
3562        Ok(())
3563    }
3564
3565    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
3566    fn perl_value_to_const_literal_expr(&self, v: &PerlValue, line: usize) -> PerlResult<Expr> {
3567        if v.is_undef() {
3568            return Ok(Expr {
3569                kind: ExprKind::Undef,
3570                line,
3571            });
3572        }
3573        if let Some(n) = v.as_integer() {
3574            return Ok(Expr {
3575                kind: ExprKind::Integer(n),
3576                line,
3577            });
3578        }
3579        if let Some(f) = v.as_float() {
3580            return Ok(Expr {
3581                kind: ExprKind::Float(f),
3582                line,
3583            });
3584        }
3585        if let Some(s) = v.as_str() {
3586            return Ok(Expr {
3587                kind: ExprKind::String(s),
3588                line,
3589            });
3590        }
3591        if let Some(arr) = v.as_array_vec() {
3592            let mut elems = Vec::with_capacity(arr.len());
3593            for e in &arr {
3594                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3595            }
3596            return Ok(Expr {
3597                kind: ExprKind::ArrayRef(elems),
3598                line,
3599            });
3600        }
3601        if let Some(h) = v.as_hash_map() {
3602            let mut pairs = Vec::with_capacity(h.len());
3603            for (k, vv) in h.iter() {
3604                pairs.push((
3605                    Expr {
3606                        kind: ExprKind::String(k.clone()),
3607                        line,
3608                    },
3609                    self.perl_value_to_const_literal_expr(vv, line)?,
3610                ));
3611            }
3612            return Ok(Expr {
3613                kind: ExprKind::HashRef(pairs),
3614                line,
3615            });
3616        }
3617        if let Some(aref) = v.as_array_ref() {
3618            let arr = aref.read();
3619            let mut elems = Vec::with_capacity(arr.len());
3620            for e in arr.iter() {
3621                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3622            }
3623            return Ok(Expr {
3624                kind: ExprKind::ArrayRef(elems),
3625                line,
3626            });
3627        }
3628        if let Some(href) = v.as_hash_ref() {
3629            let h = href.read();
3630            let mut pairs = Vec::with_capacity(h.len());
3631            for (k, vv) in h.iter() {
3632                pairs.push((
3633                    Expr {
3634                        kind: ExprKind::String(k.clone()),
3635                        line,
3636                    },
3637                    self.perl_value_to_const_literal_expr(vv, line)?,
3638                ));
3639            }
3640            return Ok(Expr {
3641                kind: ExprKind::HashRef(pairs),
3642                line,
3643            });
3644        }
3645        Err(PerlError::runtime(
3646            format!("use constant: unsupported value type ({v:?})"),
3647            line,
3648        ))
3649    }
3650
3651    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
3652    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> PerlResult<()> {
3653        for stmt in &program.statements {
3654            match &stmt.kind {
3655                StmtKind::Package { name } => {
3656                    let _ = self
3657                        .scope
3658                        .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
3659                }
3660                StmtKind::SubDecl {
3661                    name,
3662                    params,
3663                    body,
3664                    prototype,
3665                } => {
3666                    let key = self.qualify_sub_key(name);
3667                    let mut sub = PerlSub {
3668                        name: name.clone(),
3669                        params: params.clone(),
3670                        body: body.clone(),
3671                        closure_env: None,
3672                        prototype: prototype.clone(),
3673                        fib_like: None,
3674                    };
3675                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
3676                    self.subs.insert(key, Arc::new(sub));
3677                }
3678                StmtKind::UsePerlVersion { .. } => {}
3679                StmtKind::Use { module, imports } => {
3680                    self.exec_use_stmt(module, imports, stmt.line)?;
3681                }
3682                StmtKind::UseOverload { pairs } => {
3683                    self.install_use_overload_pairs(pairs);
3684                }
3685                StmtKind::FormatDecl { name, lines } => {
3686                    self.install_format_decl(name, lines, stmt.line)?;
3687                }
3688                StmtKind::No { module, imports } => {
3689                    self.exec_no_stmt(module, imports, stmt.line)?;
3690                }
3691                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
3692                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
3693                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
3694                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
3695                StmtKind::End(block) => self.end_blocks.push(block.clone()),
3696                _ => {}
3697            }
3698        }
3699        Ok(())
3700    }
3701
3702    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
3703    pub fn install_data_handle(&mut self, data: Vec<u8>) {
3704        self.input_handles.insert(
3705            "DATA".to_string(),
3706            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
3707        );
3708    }
3709
3710    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
3711    ///
3712    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
3713    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
3714    /// [`piped_shell_command`]).
3715    pub(crate) fn open_builtin_execute(
3716        &mut self,
3717        handle_name: String,
3718        mode_s: String,
3719        file_opt: Option<String>,
3720        line: usize,
3721    ) -> PerlResult<PerlValue> {
3722        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
3723        // - leading `|`  → pipe to command (write to child's stdin)
3724        // - trailing `|` → pipe from command (read child's stdout)
3725        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
3726        let (actual_mode, path) = if let Some(f) = file_opt {
3727            (mode_s, f)
3728        } else {
3729            let trimmed = mode_s.trim();
3730            if let Some(rest) = trimmed.strip_prefix('|') {
3731                ("|-".to_string(), rest.trim_start().to_string())
3732            } else if trimmed.ends_with('|') {
3733                let mut cmd = trimmed.to_string();
3734                cmd.pop(); // trailing `|` that selects pipe-from-command
3735                ("-|".to_string(), cmd.trim_end().to_string())
3736            } else if let Some(rest) = trimmed.strip_prefix(">>") {
3737                (">>".to_string(), rest.trim().to_string())
3738            } else if let Some(rest) = trimmed.strip_prefix('>') {
3739                (">".to_string(), rest.trim().to_string())
3740            } else if let Some(rest) = trimmed.strip_prefix('<') {
3741                ("<".to_string(), rest.trim().to_string())
3742            } else {
3743                ("<".to_string(), trimmed.to_string())
3744            }
3745        };
3746        let handle_return = handle_name.clone();
3747        match actual_mode.as_str() {
3748            "-|" => {
3749                let mut cmd = piped_shell_command(&path);
3750                cmd.stdout(Stdio::piped());
3751                let mut child = cmd.spawn().map_err(|e| {
3752                    self.apply_io_error_to_errno(&e);
3753                    PerlError::runtime(format!("Can't open pipe from command: {}", e), line)
3754                })?;
3755                let stdout = child
3756                    .stdout
3757                    .take()
3758                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdout", line))?;
3759                self.input_handles
3760                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
3761                self.pipe_children.insert(handle_name, child);
3762            }
3763            "|-" => {
3764                let mut cmd = piped_shell_command(&path);
3765                cmd.stdin(Stdio::piped());
3766                let mut child = cmd.spawn().map_err(|e| {
3767                    self.apply_io_error_to_errno(&e);
3768                    PerlError::runtime(format!("Can't open pipe to command: {}", e), line)
3769                })?;
3770                let stdin = child
3771                    .stdin
3772                    .take()
3773                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdin", line))?;
3774                self.output_handles
3775                    .insert(handle_name.clone(), Box::new(stdin));
3776                self.pipe_children.insert(handle_name, child);
3777            }
3778            "<" => {
3779                let file = std::fs::File::open(&path).map_err(|e| {
3780                    self.apply_io_error_to_errno(&e);
3781                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3782                })?;
3783                let shared = Arc::new(Mutex::new(file));
3784                self.io_file_slots
3785                    .insert(handle_name.clone(), Arc::clone(&shared));
3786                self.input_handles.insert(
3787                    handle_name.clone(),
3788                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
3789                );
3790            }
3791            ">" => {
3792                let file = std::fs::File::create(&path).map_err(|e| {
3793                    self.apply_io_error_to_errno(&e);
3794                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3795                })?;
3796                let shared = Arc::new(Mutex::new(file));
3797                self.io_file_slots
3798                    .insert(handle_name.clone(), Arc::clone(&shared));
3799                self.output_handles.insert(
3800                    handle_name.clone(),
3801                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3802                );
3803            }
3804            ">>" => {
3805                let file = std::fs::OpenOptions::new()
3806                    .append(true)
3807                    .create(true)
3808                    .open(&path)
3809                    .map_err(|e| {
3810                        self.apply_io_error_to_errno(&e);
3811                        PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3812                    })?;
3813                let shared = Arc::new(Mutex::new(file));
3814                self.io_file_slots
3815                    .insert(handle_name.clone(), Arc::clone(&shared));
3816                self.output_handles.insert(
3817                    handle_name.clone(),
3818                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3819                );
3820            }
3821            _ => {
3822                return Err(PerlError::runtime(
3823                    format!("Unknown open mode '{}'", actual_mode),
3824                    line,
3825                ));
3826            }
3827        }
3828        Ok(PerlValue::io_handle(handle_return))
3829    }
3830
3831    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
3832    /// matches the previous key under [`PerlValue::str_eq`]. Returns a list of arrayrefs
3833    /// (same outer shape as `chunked`).
3834    pub(crate) fn eval_chunk_by_builtin(
3835        &mut self,
3836        key_spec: &Expr,
3837        list_expr: &Expr,
3838        ctx: WantarrayCtx,
3839        line: usize,
3840    ) -> ExecResult {
3841        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
3842        let chunks = match &key_spec.kind {
3843            ExprKind::CodeRef { .. } => {
3844                let cr = self.eval_expr(key_spec)?;
3845                let Some(sub) = cr.as_code_ref() else {
3846                    return Err(PerlError::runtime(
3847                        "group_by/chunk_by: first argument must be { BLOCK }",
3848                        line,
3849                    )
3850                    .into());
3851                };
3852                let sub = sub.clone();
3853                let mut chunks: Vec<PerlValue> = Vec::new();
3854                let mut run: Vec<PerlValue> = Vec::new();
3855                let mut prev_key: Option<PerlValue> = None;
3856                for item in list {
3857                    self.scope.set_topic(item.clone());
3858                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
3859                        Ok(k) => k,
3860                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
3861                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
3862                        Err(_) => PerlValue::UNDEF,
3863                    };
3864                    match &prev_key {
3865                        None => {
3866                            run.push(item);
3867                            prev_key = Some(key);
3868                        }
3869                        Some(pk) => {
3870                            if key.str_eq(pk) {
3871                                run.push(item);
3872                            } else {
3873                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3874                                    std::mem::take(&mut run),
3875                                ))));
3876                                run.push(item);
3877                                prev_key = Some(key);
3878                            }
3879                        }
3880                    }
3881                }
3882                if !run.is_empty() {
3883                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3884                }
3885                chunks
3886            }
3887            _ => {
3888                let mut chunks: Vec<PerlValue> = Vec::new();
3889                let mut run: Vec<PerlValue> = Vec::new();
3890                let mut prev_key: Option<PerlValue> = None;
3891                for item in list {
3892                    self.scope.set_topic(item.clone());
3893                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
3894                    match &prev_key {
3895                        None => {
3896                            run.push(item);
3897                            prev_key = Some(key);
3898                        }
3899                        Some(pk) => {
3900                            if key.str_eq(pk) {
3901                                run.push(item);
3902                            } else {
3903                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3904                                    std::mem::take(&mut run),
3905                                ))));
3906                                run.push(item);
3907                                prev_key = Some(key);
3908                            }
3909                        }
3910                    }
3911                }
3912                if !run.is_empty() {
3913                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3914                }
3915                chunks
3916            }
3917        };
3918        Ok(match ctx {
3919            WantarrayCtx::List => PerlValue::array(chunks),
3920            WantarrayCtx::Scalar => PerlValue::integer(chunks.len() as i64),
3921            WantarrayCtx::Void => PerlValue::UNDEF,
3922        })
3923    }
3924
3925    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
3926    pub(crate) fn list_higher_order_block_builtin(
3927        &mut self,
3928        name: &str,
3929        args: &[PerlValue],
3930        line: usize,
3931    ) -> PerlResult<PerlValue> {
3932        match self.list_higher_order_block_builtin_exec(name, args, line) {
3933            Ok(v) => Ok(v),
3934            Err(FlowOrError::Error(e)) => Err(e),
3935            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
3936            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
3937                format!("{name}: unsupported control flow in block"),
3938                line,
3939            )),
3940        }
3941    }
3942
3943    fn list_higher_order_block_builtin_exec(
3944        &mut self,
3945        name: &str,
3946        args: &[PerlValue],
3947        line: usize,
3948    ) -> ExecResult {
3949        if args.is_empty() {
3950            return Err(
3951                PerlError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
3952            );
3953        }
3954        let Some(sub) = args[0].as_code_ref() else {
3955            return Err(PerlError::runtime(
3956                format!("{name}: first argument must be {{ BLOCK }}"),
3957                line,
3958            )
3959            .into());
3960        };
3961        let sub = sub.clone();
3962        let items: Vec<PerlValue> = args[1..].to_vec();
3963        if matches!(name, "tap" | "peek") && items.len() == 1 {
3964            if let Some(p) = items[0].as_pipeline() {
3965                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
3966                return Ok(PerlValue::pipeline(Arc::clone(&p)));
3967            }
3968            let v = &items[0];
3969            if v.is_iterator() || v.as_array_vec().is_some() {
3970                let source = crate::map_stream::into_pull_iter(v.clone());
3971                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
3972                return Ok(PerlValue::iterator(Arc::new(
3973                    crate::map_stream::TapIterator::new(
3974                        source,
3975                        sub,
3976                        self.subs.clone(),
3977                        capture,
3978                        atomic_arrays,
3979                        atomic_hashes,
3980                    ),
3981                )));
3982            }
3983        }
3984        // Streaming optimization disabled for these functions because the pre-captured
3985        // coderef from args[0] has its closure_env populated at parse time, which causes
3986        // $_ to get stale values on subsequent calls. These functions work correctly in
3987        // the non-streaming eager path below.
3988        let wa = self.wantarray_kind;
3989        match name {
3990            "take_while" => {
3991                let mut out = Vec::new();
3992                for item in items {
3993                    self.scope_push_hook();
3994                    self.scope.set_topic(item.clone());
3995                    let pred = self.exec_block(&sub.body)?;
3996                    self.scope_pop_hook();
3997                    if !pred.is_true() {
3998                        break;
3999                    }
4000                    out.push(item);
4001                }
4002                Ok(match wa {
4003                    WantarrayCtx::List => PerlValue::array(out),
4004                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4005                    WantarrayCtx::Void => PerlValue::UNDEF,
4006                })
4007            }
4008            "drop_while" | "skip_while" => {
4009                let mut i = 0usize;
4010                while i < items.len() {
4011                    self.scope_push_hook();
4012                    self.scope.set_topic(items[i].clone());
4013                    let pred = self.exec_block(&sub.body)?;
4014                    self.scope_pop_hook();
4015                    if !pred.is_true() {
4016                        break;
4017                    }
4018                    i += 1;
4019                }
4020                let rest = items[i..].to_vec();
4021                Ok(match wa {
4022                    WantarrayCtx::List => PerlValue::array(rest),
4023                    WantarrayCtx::Scalar => PerlValue::integer(rest.len() as i64),
4024                    WantarrayCtx::Void => PerlValue::UNDEF,
4025                })
4026            }
4027            "reject" => {
4028                let mut out = Vec::new();
4029                for item in items {
4030                    self.scope_push_hook();
4031                    self.scope.set_topic(item.clone());
4032                    let pred = self.exec_block(&sub.body)?;
4033                    self.scope_pop_hook();
4034                    if !pred.is_true() {
4035                        out.push(item);
4036                    }
4037                }
4038                Ok(match wa {
4039                    WantarrayCtx::List => PerlValue::array(out),
4040                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4041                    WantarrayCtx::Void => PerlValue::UNDEF,
4042                })
4043            }
4044            "tap" | "peek" => {
4045                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
4046                Ok(match wa {
4047                    WantarrayCtx::List => PerlValue::array(items),
4048                    WantarrayCtx::Scalar => PerlValue::integer(items.len() as i64),
4049                    WantarrayCtx::Void => PerlValue::UNDEF,
4050                })
4051            }
4052            "partition" => {
4053                let mut yes = Vec::new();
4054                let mut no = Vec::new();
4055                for item in items {
4056                    self.scope.set_topic(item.clone());
4057                    let pred = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4058                    if pred.is_true() {
4059                        yes.push(item);
4060                    } else {
4061                        no.push(item);
4062                    }
4063                }
4064                let yes_ref = PerlValue::array_ref(Arc::new(RwLock::new(yes)));
4065                let no_ref = PerlValue::array_ref(Arc::new(RwLock::new(no)));
4066                Ok(match wa {
4067                    WantarrayCtx::List => PerlValue::array(vec![yes_ref, no_ref]),
4068                    WantarrayCtx::Scalar => PerlValue::integer(2),
4069                    WantarrayCtx::Void => PerlValue::UNDEF,
4070                })
4071            }
4072            "min_by" => {
4073                let mut best: Option<(PerlValue, PerlValue)> = None;
4074                for item in items {
4075                    self.scope.set_topic(item.clone());
4076                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4077                    best = Some(match best {
4078                        None => (item, key),
4079                        Some((bv, bk)) => {
4080                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
4081                                (item, key)
4082                            } else {
4083                                (bv, bk)
4084                            }
4085                        }
4086                    });
4087                }
4088                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
4089            }
4090            "max_by" => {
4091                let mut best: Option<(PerlValue, PerlValue)> = None;
4092                for item in items {
4093                    self.scope.set_topic(item.clone());
4094                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4095                    best = Some(match best {
4096                        None => (item, key),
4097                        Some((bv, bk)) => {
4098                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
4099                                (item, key)
4100                            } else {
4101                                (bv, bk)
4102                            }
4103                        }
4104                    });
4105                }
4106                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
4107            }
4108            "zip_with" => {
4109                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
4110                // Flatten items, then treat each array ref/binding as a separate list.
4111                let flat: Vec<PerlValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
4112                let refs: Vec<Vec<PerlValue>> = flat
4113                    .iter()
4114                    .map(|el| {
4115                        if let Some(ar) = el.as_array_ref() {
4116                            ar.read().clone()
4117                        } else if let Some(name) = el.as_array_binding_name() {
4118                            self.scope.get_array(&name)
4119                        } else {
4120                            vec![el.clone()]
4121                        }
4122                    })
4123                    .collect();
4124                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
4125                let mut out = Vec::with_capacity(max_len);
4126                for i in 0..max_len {
4127                    let pair: Vec<PerlValue> = refs
4128                        .iter()
4129                        .map(|l| l.get(i).cloned().unwrap_or(PerlValue::UNDEF))
4130                        .collect();
4131                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
4132                    out.push(result);
4133                }
4134                Ok(match wa {
4135                    WantarrayCtx::List => PerlValue::array(out),
4136                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4137                    WantarrayCtx::Void => PerlValue::UNDEF,
4138                })
4139            }
4140            "count_by" => {
4141                let mut counts = indexmap::IndexMap::new();
4142                for item in items {
4143                    self.scope.set_topic(item.clone());
4144                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4145                    let k = key.to_string();
4146                    let entry = counts.entry(k).or_insert(PerlValue::integer(0));
4147                    *entry = PerlValue::integer(entry.to_int() + 1);
4148                }
4149                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(counts))))
4150            }
4151            _ => Err(PerlError::runtime(
4152                format!("internal: unknown list block builtin `{name}`"),
4153                line,
4154            )
4155            .into()),
4156        }
4157    }
4158
4159    /// `rmdir LIST` — remove empty directories; returns count removed.
4160    pub(crate) fn builtin_rmdir_execute(
4161        &mut self,
4162        args: &[PerlValue],
4163        _line: usize,
4164    ) -> PerlResult<PerlValue> {
4165        let mut count = 0i64;
4166        for a in args {
4167            let p = a.to_string();
4168            if p.is_empty() {
4169                continue;
4170            }
4171            if std::fs::remove_dir(&p).is_ok() {
4172                count += 1;
4173            }
4174        }
4175        Ok(PerlValue::integer(count))
4176    }
4177
4178    /// `touch FILE, ...` — create if absent, update timestamps to now.
4179    pub(crate) fn builtin_touch_execute(
4180        &mut self,
4181        args: &[PerlValue],
4182        _line: usize,
4183    ) -> PerlResult<PerlValue> {
4184        let paths: Vec<String> = args.iter().map(|v| v.to_string()).collect();
4185        Ok(PerlValue::integer(crate::perl_fs::touch_paths(&paths)))
4186    }
4187
4188    /// `utime ATIME, MTIME, LIST`
4189    pub(crate) fn builtin_utime_execute(
4190        &mut self,
4191        args: &[PerlValue],
4192        line: usize,
4193    ) -> PerlResult<PerlValue> {
4194        if args.len() < 3 {
4195            return Err(PerlError::runtime(
4196                "utime requires at least three arguments (atime, mtime, files...)",
4197                line,
4198            ));
4199        }
4200        let at = args[0].to_int();
4201        let mt = args[1].to_int();
4202        let paths: Vec<String> = args.iter().skip(2).map(|v| v.to_string()).collect();
4203        let n = crate::perl_fs::utime_paths(at, mt, &paths);
4204        #[cfg(not(unix))]
4205        if !paths.is_empty() && n == 0 {
4206            return Err(PerlError::runtime(
4207                "utime is not supported on this platform",
4208                line,
4209            ));
4210        }
4211        Ok(PerlValue::integer(n))
4212    }
4213
4214    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
4215    pub(crate) fn builtin_umask_execute(
4216        &mut self,
4217        args: &[PerlValue],
4218        line: usize,
4219    ) -> PerlResult<PerlValue> {
4220        #[cfg(unix)]
4221        {
4222            let _ = line;
4223            if args.is_empty() {
4224                let cur = unsafe { libc::umask(0) };
4225                unsafe { libc::umask(cur) };
4226                return Ok(PerlValue::integer(cur as i64));
4227            }
4228            let new_m = args[0].to_int() as libc::mode_t;
4229            let old = unsafe { libc::umask(new_m) };
4230            Ok(PerlValue::integer(old as i64))
4231        }
4232        #[cfg(not(unix))]
4233        {
4234            let _ = args;
4235            Err(PerlError::runtime(
4236                "umask is not supported on this platform",
4237                line,
4238            ))
4239        }
4240    }
4241
4242    /// `getcwd` — current directory or undef on failure.
4243    pub(crate) fn builtin_getcwd_execute(
4244        &mut self,
4245        args: &[PerlValue],
4246        line: usize,
4247    ) -> PerlResult<PerlValue> {
4248        if !args.is_empty() {
4249            return Err(PerlError::runtime("getcwd takes no arguments", line));
4250        }
4251        match std::env::current_dir() {
4252            Ok(p) => Ok(PerlValue::string(p.to_string_lossy().into_owned())),
4253            Err(e) => {
4254                self.apply_io_error_to_errno(&e);
4255                Ok(PerlValue::UNDEF)
4256            }
4257        }
4258    }
4259
4260    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
4261    pub(crate) fn builtin_realpath_execute(
4262        &mut self,
4263        args: &[PerlValue],
4264        line: usize,
4265    ) -> PerlResult<PerlValue> {
4266        let path = args
4267            .first()
4268            .ok_or_else(|| PerlError::runtime("realpath: need path", line))?
4269            .to_string();
4270        if path.is_empty() {
4271            return Err(PerlError::runtime("realpath: need path", line));
4272        }
4273        match crate::perl_fs::realpath_resolved(&path) {
4274            Ok(s) => Ok(PerlValue::string(s)),
4275            Err(e) => {
4276                self.apply_io_error_to_errno(&e);
4277                Ok(PerlValue::UNDEF)
4278            }
4279        }
4280    }
4281
4282    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
4283    pub(crate) fn builtin_pipe_execute(
4284        &mut self,
4285        args: &[PerlValue],
4286        line: usize,
4287    ) -> PerlResult<PerlValue> {
4288        if args.len() != 2 {
4289            return Err(PerlError::runtime(
4290                "pipe requires exactly two arguments",
4291                line,
4292            ));
4293        }
4294        #[cfg(unix)]
4295        {
4296            use std::fs::File;
4297            use std::os::unix::io::FromRawFd;
4298
4299            let read_name = args[0].to_string();
4300            let write_name = args[1].to_string();
4301            if read_name.is_empty() || write_name.is_empty() {
4302                return Err(PerlError::runtime("pipe: invalid handle name", line));
4303            }
4304            let mut fds = [0i32; 2];
4305            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
4306                let e = std::io::Error::last_os_error();
4307                self.apply_io_error_to_errno(&e);
4308                return Ok(PerlValue::integer(0));
4309            }
4310            let read_file = unsafe { File::from_raw_fd(fds[0]) };
4311            let write_file = unsafe { File::from_raw_fd(fds[1]) };
4312
4313            let read_shared = Arc::new(Mutex::new(read_file));
4314            let write_shared = Arc::new(Mutex::new(write_file));
4315
4316            self.close_builtin_execute(read_name.clone()).ok();
4317            self.close_builtin_execute(write_name.clone()).ok();
4318
4319            self.io_file_slots
4320                .insert(read_name.clone(), Arc::clone(&read_shared));
4321            self.input_handles.insert(
4322                read_name,
4323                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
4324            );
4325
4326            self.io_file_slots
4327                .insert(write_name.clone(), Arc::clone(&write_shared));
4328            self.output_handles
4329                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
4330
4331            Ok(PerlValue::integer(1))
4332        }
4333        #[cfg(not(unix))]
4334        {
4335            let _ = args;
4336            Err(PerlError::runtime(
4337                "pipe is not supported on this platform",
4338                line,
4339            ))
4340        }
4341    }
4342
4343    pub(crate) fn close_builtin_execute(&mut self, name: String) -> PerlResult<PerlValue> {
4344        self.output_handles.remove(&name);
4345        self.input_handles.remove(&name);
4346        self.io_file_slots.remove(&name);
4347        if let Some(mut child) = self.pipe_children.remove(&name) {
4348            if let Ok(st) = child.wait() {
4349                self.record_child_exit_status(st);
4350            }
4351        }
4352        Ok(PerlValue::integer(1))
4353    }
4354
4355    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
4356        self.input_handles.contains_key(name)
4357    }
4358
4359    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
4360    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
4361    /// readline-level EOF tracking exists.
4362    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
4363        self.line_mode_eof_pending
4364    }
4365
4366    /// `eof` / `eof()` / `eof FH` — shared by [`crate::vm::VM`] and
4367    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
4368    /// not [`ExprKind::Eof`]).
4369    pub(crate) fn eof_builtin_execute(
4370        &self,
4371        args: &[PerlValue],
4372        line: usize,
4373    ) -> PerlResult<PerlValue> {
4374        match args.len() {
4375            0 => Ok(PerlValue::integer(if self.eof_without_arg_is_true() {
4376                1
4377            } else {
4378                0
4379            })),
4380            1 => {
4381                let name = args[0].to_string();
4382                let at_eof = !self.has_input_handle(&name);
4383                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
4384            }
4385            _ => Err(PerlError::runtime("eof: too many arguments", line)),
4386        }
4387    }
4388
4389    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
4390    /// `0`, stringifies to `""`) for `""`.
4391    pub(crate) fn study_return_value(s: &str) -> PerlValue {
4392        if s.is_empty() {
4393            PerlValue::string(String::new())
4394        } else {
4395            PerlValue::integer(1)
4396        }
4397    }
4398
4399    pub(crate) fn readline_builtin_execute(
4400        &mut self,
4401        handle: Option<&str>,
4402    ) -> PerlResult<PerlValue> {
4403        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
4404        if handle.is_none() {
4405            let argv = self.scope.get_array("ARGV");
4406            if !argv.is_empty() {
4407                loop {
4408                    if self.diamond_reader.is_none() {
4409                        while self.diamond_next_idx < argv.len() {
4410                            let path = argv[self.diamond_next_idx].to_string();
4411                            self.diamond_next_idx += 1;
4412                            match File::open(&path) {
4413                                Ok(f) => {
4414                                    self.argv_current_file = path;
4415                                    self.diamond_reader = Some(BufReader::new(f));
4416                                    break;
4417                                }
4418                                Err(e) => {
4419                                    self.apply_io_error_to_errno(&e);
4420                                }
4421                            }
4422                        }
4423                        if self.diamond_reader.is_none() {
4424                            return Ok(PerlValue::UNDEF);
4425                        }
4426                    }
4427                    let mut line_str = String::new();
4428                    let read_result: Result<usize, io::Error> =
4429                        if let Some(reader) = self.diamond_reader.as_mut() {
4430                            if self.open_pragma_utf8 {
4431                                let mut buf = Vec::new();
4432                                reader.read_until(b'\n', &mut buf).inspect(|n| {
4433                                    if *n > 0 {
4434                                        line_str = String::from_utf8_lossy(&buf).into_owned();
4435                                    }
4436                                })
4437                            } else {
4438                                let mut buf = Vec::new();
4439                                match reader.read_until(b'\n', &mut buf) {
4440                                    Ok(n) => {
4441                                        if n > 0 {
4442                                            line_str =
4443                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
4444                                                &buf,
4445                                            );
4446                                        }
4447                                        Ok(n)
4448                                    }
4449                                    Err(e) => Err(e),
4450                                }
4451                            }
4452                        } else {
4453                            unreachable!()
4454                        };
4455                    match read_result {
4456                        Ok(0) => {
4457                            self.diamond_reader = None;
4458                            continue;
4459                        }
4460                        Ok(_) => {
4461                            self.bump_line_for_handle(&self.argv_current_file.clone());
4462                            return Ok(PerlValue::string(line_str));
4463                        }
4464                        Err(e) => {
4465                            self.apply_io_error_to_errno(&e);
4466                            self.diamond_reader = None;
4467                            continue;
4468                        }
4469                    }
4470                }
4471            } else {
4472                self.argv_current_file.clear();
4473            }
4474        }
4475
4476        let handle_name = handle.unwrap_or("STDIN");
4477        let mut line_str = String::new();
4478        if handle_name == "STDIN" {
4479            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
4480                self.last_stdin_die_bracket = if handle.is_none() {
4481                    "<>".to_string()
4482                } else {
4483                    "<STDIN>".to_string()
4484                };
4485                self.bump_line_for_handle("STDIN");
4486                return Ok(PerlValue::string(queued));
4487            }
4488            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4489                let mut buf = Vec::new();
4490                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
4491                    if *n > 0 {
4492                        line_str = String::from_utf8_lossy(&buf).into_owned();
4493                    }
4494                })
4495            } else {
4496                let mut buf = Vec::new();
4497                let mut lock = io::stdin().lock();
4498                match lock.read_until(b'\n', &mut buf) {
4499                    Ok(n) => {
4500                        if n > 0 {
4501                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4502                        }
4503                        Ok(n)
4504                    }
4505                    Err(e) => Err(e),
4506                }
4507            };
4508            match r {
4509                Ok(0) => Ok(PerlValue::UNDEF),
4510                Ok(_) => {
4511                    self.last_stdin_die_bracket = if handle.is_none() {
4512                        "<>".to_string()
4513                    } else {
4514                        "<STDIN>".to_string()
4515                    };
4516                    self.bump_line_for_handle("STDIN");
4517                    Ok(PerlValue::string(line_str))
4518                }
4519                Err(e) => {
4520                    self.apply_io_error_to_errno(&e);
4521                    Ok(PerlValue::UNDEF)
4522                }
4523            }
4524        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
4525            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4526                let mut buf = Vec::new();
4527                reader.read_until(b'\n', &mut buf).inspect(|n| {
4528                    if *n > 0 {
4529                        line_str = String::from_utf8_lossy(&buf).into_owned();
4530                    }
4531                })
4532            } else {
4533                let mut buf = Vec::new();
4534                match reader.read_until(b'\n', &mut buf) {
4535                    Ok(n) => {
4536                        if n > 0 {
4537                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4538                        }
4539                        Ok(n)
4540                    }
4541                    Err(e) => Err(e),
4542                }
4543            };
4544            match r {
4545                Ok(0) => Ok(PerlValue::UNDEF),
4546                Ok(_) => {
4547                    self.bump_line_for_handle(handle_name);
4548                    Ok(PerlValue::string(line_str))
4549                }
4550                Err(e) => {
4551                    self.apply_io_error_to_errno(&e);
4552                    Ok(PerlValue::UNDEF)
4553                }
4554            }
4555        } else {
4556            Ok(PerlValue::UNDEF)
4557        }
4558    }
4559
4560    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
4561    pub(crate) fn readline_builtin_execute_list(
4562        &mut self,
4563        handle: Option<&str>,
4564    ) -> PerlResult<PerlValue> {
4565        let mut lines = Vec::new();
4566        loop {
4567            let v = self.readline_builtin_execute(handle)?;
4568            if v.is_undef() {
4569                break;
4570            }
4571            lines.push(v);
4572        }
4573        Ok(PerlValue::array(lines))
4574    }
4575
4576    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> PerlValue {
4577        match std::fs::read_dir(path) {
4578            Ok(rd) => {
4579                let entries: Vec<String> = rd
4580                    .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
4581                    .collect();
4582                self.dir_handles
4583                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
4584                PerlValue::integer(1)
4585            }
4586            Err(e) => {
4587                self.apply_io_error_to_errno(&e);
4588                PerlValue::integer(0)
4589            }
4590        }
4591    }
4592
4593    pub(crate) fn readdir_handle(&mut self, handle: &str) -> PerlValue {
4594        if let Some(dh) = self.dir_handles.get_mut(handle) {
4595            if dh.pos < dh.entries.len() {
4596                let s = dh.entries[dh.pos].clone();
4597                dh.pos += 1;
4598                PerlValue::string(s)
4599            } else {
4600                PerlValue::UNDEF
4601            }
4602        } else {
4603            PerlValue::UNDEF
4604        }
4605    }
4606
4607    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
4608    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> PerlValue {
4609        if let Some(dh) = self.dir_handles.get_mut(handle) {
4610            let rest: Vec<PerlValue> = dh.entries[dh.pos..]
4611                .iter()
4612                .cloned()
4613                .map(PerlValue::string)
4614                .collect();
4615            dh.pos = dh.entries.len();
4616            PerlValue::array(rest)
4617        } else {
4618            PerlValue::array(Vec::new())
4619        }
4620    }
4621
4622    pub(crate) fn closedir_handle(&mut self, handle: &str) -> PerlValue {
4623        PerlValue::integer(if self.dir_handles.remove(handle).is_some() {
4624            1
4625        } else {
4626            0
4627        })
4628    }
4629
4630    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> PerlValue {
4631        if let Some(dh) = self.dir_handles.get_mut(handle) {
4632            dh.pos = 0;
4633            PerlValue::integer(1)
4634        } else {
4635            PerlValue::integer(0)
4636        }
4637    }
4638
4639    pub(crate) fn telldir_handle(&mut self, handle: &str) -> PerlValue {
4640        self.dir_handles
4641            .get(handle)
4642            .map(|dh| PerlValue::integer(dh.pos as i64))
4643            .unwrap_or(PerlValue::UNDEF)
4644    }
4645
4646    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> PerlValue {
4647        if let Some(dh) = self.dir_handles.get_mut(handle) {
4648            dh.pos = pos.min(dh.entries.len());
4649            PerlValue::integer(1)
4650        } else {
4651            PerlValue::integer(0)
4652        }
4653    }
4654
4655    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
4656    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
4657    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
4658    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
4659    #[inline]
4660    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
4661        crate::special_vars::is_regex_match_scalar_name(name)
4662    }
4663
4664    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
4665    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
4666    /// `apply_regex_captures` instead of short-circuiting.
4667    #[inline]
4668    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
4669        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
4670            self.regex_capture_scope_fresh = false;
4671        }
4672    }
4673
4674    pub(crate) fn apply_regex_captures(
4675        &mut self,
4676        haystack: &str,
4677        offset: usize,
4678        re: &PerlCompiledRegex,
4679        caps: &PerlCaptures<'_>,
4680        capture_all: CaptureAllMode,
4681    ) -> Result<(), FlowOrError> {
4682        let m0 = caps.get(0).expect("regex capture 0");
4683        let s0 = offset + m0.start;
4684        let e0 = offset + m0.end;
4685        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
4686        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
4687        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
4688        let mut last_paren = String::new();
4689        for i in 1..caps.len() {
4690            if let Some(m) = caps.get(i) {
4691                last_paren = m.text.to_string();
4692            }
4693        }
4694        self.last_paren_match = last_paren;
4695        self.last_subpattern_name = String::new();
4696        for n in re.capture_names().flatten() {
4697            if caps.name(n).is_some() {
4698                self.last_subpattern_name = n.to_string();
4699            }
4700        }
4701        self.scope
4702            .set_scalar("&", PerlValue::string(self.last_match.clone()))?;
4703        self.scope
4704            .set_scalar("`", PerlValue::string(self.prematch.clone()))?;
4705        self.scope
4706            .set_scalar("'", PerlValue::string(self.postmatch.clone()))?;
4707        self.scope
4708            .set_scalar("+", PerlValue::string(self.last_paren_match.clone()))?;
4709        for i in 1..caps.len() {
4710            if let Some(m) = caps.get(i) {
4711                self.scope
4712                    .set_scalar(&i.to_string(), PerlValue::string(m.text.to_string()))?;
4713            }
4714        }
4715        let mut start_arr = vec![PerlValue::integer(s0 as i64)];
4716        let mut end_arr = vec![PerlValue::integer(e0 as i64)];
4717        for i in 1..caps.len() {
4718            if let Some(m) = caps.get(i) {
4719                start_arr.push(PerlValue::integer((offset + m.start) as i64));
4720                end_arr.push(PerlValue::integer((offset + m.end) as i64));
4721            } else {
4722                start_arr.push(PerlValue::integer(-1));
4723                end_arr.push(PerlValue::integer(-1));
4724            }
4725        }
4726        self.scope.set_array("-", start_arr)?;
4727        self.scope.set_array("+", end_arr)?;
4728        let mut named = IndexMap::new();
4729        for name in re.capture_names().flatten() {
4730            if let Some(m) = caps.name(name) {
4731                named.insert(name.to_string(), PerlValue::string(m.text.to_string()));
4732            }
4733        }
4734        self.scope.set_hash("+", named.clone())?;
4735        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
4736        let mut named_minus = IndexMap::new();
4737        for (name, val) in &named {
4738            named_minus.insert(
4739                name.clone(),
4740                PerlValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
4741            );
4742        }
4743        self.scope.set_hash("-", named_minus)?;
4744        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
4745        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
4746        match capture_all {
4747            CaptureAllMode::Empty => {
4748                self.scope.set_array("^CAPTURE_ALL", vec![])?;
4749            }
4750            CaptureAllMode::Append => {
4751                let mut rows = self.scope.get_array("^CAPTURE_ALL");
4752                rows.push(PerlValue::array(cap_flat));
4753                self.scope.set_array("^CAPTURE_ALL", rows)?;
4754            }
4755            CaptureAllMode::Skip => {}
4756        }
4757        Ok(())
4758    }
4759
4760    pub(crate) fn clear_flip_flop_state(&mut self) {
4761        self.flip_flop_active.clear();
4762        self.flip_flop_exclusive_left_line.clear();
4763        self.flip_flop_sequence.clear();
4764        self.flip_flop_last_dot.clear();
4765        self.flip_flop_tree.clear();
4766    }
4767
4768    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
4769        self.flip_flop_active.resize(slots as usize, false);
4770        self.flip_flop_active.fill(false);
4771        self.flip_flop_exclusive_left_line
4772            .resize(slots as usize, None);
4773        self.flip_flop_exclusive_left_line.fill(None);
4774        self.flip_flop_sequence.resize(slots as usize, 0);
4775        self.flip_flop_sequence.fill(0);
4776        self.flip_flop_last_dot.resize(slots as usize, None);
4777        self.flip_flop_last_dot.fill(None);
4778    }
4779
4780    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
4781    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
4782    /// [`Self::handle_line_numbers`]).
4783    #[inline]
4784    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
4785        if self.last_readline_handle.is_empty() {
4786            self.line_number
4787        } else {
4788            *self
4789                .handle_line_numbers
4790                .get(&self.last_readline_handle)
4791                .unwrap_or(&0)
4792        }
4793    }
4794
4795    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
4796    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
4797    ///
4798    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
4799    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
4800    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
4801    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
4802    /// get `0` / `N` from [`PerlValue::to_int`].
4803    pub(crate) fn scalar_flip_flop_eval(
4804        &mut self,
4805        left: i64,
4806        right: i64,
4807        slot: usize,
4808        exclusive: bool,
4809    ) -> PerlResult<PerlValue> {
4810        if self.flip_flop_active.len() <= slot {
4811            self.flip_flop_active.resize(slot + 1, false);
4812        }
4813        if self.flip_flop_exclusive_left_line.len() <= slot {
4814            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4815        }
4816        if self.flip_flop_sequence.len() <= slot {
4817            self.flip_flop_sequence.resize(slot + 1, 0);
4818        }
4819        if self.flip_flop_last_dot.len() <= slot {
4820            self.flip_flop_last_dot.resize(slot + 1, None);
4821        }
4822        let dot = self.scalar_flipflop_dot_line();
4823        let active = &mut self.flip_flop_active[slot];
4824        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4825        let seq = &mut self.flip_flop_sequence[slot];
4826        let last_dot = &mut self.flip_flop_last_dot[slot];
4827        if !*active {
4828            if dot == left {
4829                *active = true;
4830                *seq = 1;
4831                *last_dot = Some(dot);
4832                if exclusive {
4833                    *excl_left = Some(dot);
4834                } else {
4835                    *excl_left = None;
4836                    if dot == right {
4837                        *active = false;
4838                        return Ok(PerlValue::string(format!("{}E0", *seq)));
4839                    }
4840                }
4841                return Ok(PerlValue::string(seq.to_string()));
4842            }
4843            *last_dot = Some(dot);
4844            return Ok(PerlValue::string(String::new()));
4845        }
4846        // Already active: increment the sequence once per new `$.`, so a second evaluation on
4847        // the same line reads the same number (matches Perl `pp_flop`).
4848        if *last_dot != Some(dot) {
4849            *seq += 1;
4850            *last_dot = Some(dot);
4851        }
4852        let cur_seq = *seq;
4853        if let Some(ll) = *excl_left {
4854            if dot == right && dot > ll {
4855                *active = false;
4856                *excl_left = None;
4857                *seq = 0;
4858                return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4859            }
4860        } else if dot == right {
4861            *active = false;
4862            *seq = 0;
4863            return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4864        }
4865        Ok(PerlValue::string(cur_seq.to_string()))
4866    }
4867
4868    fn regex_flip_flop_transition(
4869        active: &mut bool,
4870        excl_left: &mut Option<i64>,
4871        exclusive: bool,
4872        dot: i64,
4873        left_m: bool,
4874        right_m: bool,
4875    ) -> i64 {
4876        if !*active {
4877            if left_m {
4878                *active = true;
4879                if exclusive {
4880                    *excl_left = Some(dot);
4881                } else {
4882                    *excl_left = None;
4883                    if right_m {
4884                        *active = false;
4885                    }
4886                }
4887                return 1;
4888            }
4889            return 0;
4890        }
4891        if let Some(ll) = *excl_left {
4892            if right_m && dot > ll {
4893                *active = false;
4894                *excl_left = None;
4895            }
4896        } else if right_m {
4897            *active = false;
4898        }
4899        1
4900    }
4901
4902    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
4903    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
4904    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
4905    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
4906    pub(crate) fn regex_flip_flop_eval(
4907        &mut self,
4908        left_pat: &str,
4909        left_flags: &str,
4910        right_pat: &str,
4911        right_flags: &str,
4912        slot: usize,
4913        exclusive: bool,
4914        line: usize,
4915    ) -> PerlResult<PerlValue> {
4916        let dot = self.scalar_flipflop_dot_line();
4917        let subject = self.scope.get_scalar("_").to_string();
4918        let left_re = self
4919            .compile_regex(left_pat, left_flags, line)
4920            .map_err(|e| match e {
4921                FlowOrError::Error(err) => err,
4922                FlowOrError::Flow(_) => {
4923                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4924                }
4925            })?;
4926        let right_re = self
4927            .compile_regex(right_pat, right_flags, line)
4928            .map_err(|e| match e {
4929                FlowOrError::Error(err) => err,
4930                FlowOrError::Flow(_) => {
4931                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4932                }
4933            })?;
4934        let left_m = left_re.is_match(&subject);
4935        let right_m = right_re.is_match(&subject);
4936        if self.flip_flop_active.len() <= slot {
4937            self.flip_flop_active.resize(slot + 1, false);
4938        }
4939        if self.flip_flop_exclusive_left_line.len() <= slot {
4940            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4941        }
4942        let active = &mut self.flip_flop_active[slot];
4943        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4944        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4945            active, excl_left, exclusive, dot, left_m, right_m,
4946        )))
4947    }
4948
4949    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
4950    pub(crate) fn regex_flip_flop_eval_dynamic_right(
4951        &mut self,
4952        left_pat: &str,
4953        left_flags: &str,
4954        slot: usize,
4955        exclusive: bool,
4956        line: usize,
4957        right_m: bool,
4958    ) -> PerlResult<PerlValue> {
4959        let dot = self.scalar_flipflop_dot_line();
4960        let subject = self.scope.get_scalar("_").to_string();
4961        let left_re = self
4962            .compile_regex(left_pat, left_flags, line)
4963            .map_err(|e| match e {
4964                FlowOrError::Error(err) => err,
4965                FlowOrError::Flow(_) => {
4966                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4967                }
4968            })?;
4969        let left_m = left_re.is_match(&subject);
4970        if self.flip_flop_active.len() <= slot {
4971            self.flip_flop_active.resize(slot + 1, false);
4972        }
4973        if self.flip_flop_exclusive_left_line.len() <= slot {
4974            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4975        }
4976        let active = &mut self.flip_flop_active[slot];
4977        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4978        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4979            active, excl_left, exclusive, dot, left_m, right_m,
4980        )))
4981    }
4982
4983    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
4984    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
4985        &mut self,
4986        left_pat: &str,
4987        left_flags: &str,
4988        slot: usize,
4989        exclusive: bool,
4990        line: usize,
4991        rhs_line: i64,
4992    ) -> PerlResult<PerlValue> {
4993        let dot = self.scalar_flipflop_dot_line();
4994        let subject = self.scope.get_scalar("_").to_string();
4995        let left_re = self
4996            .compile_regex(left_pat, left_flags, line)
4997            .map_err(|e| match e {
4998                FlowOrError::Error(err) => err,
4999                FlowOrError::Flow(_) => {
5000                    PerlError::runtime("unexpected flow in regex flip-flop", line)
5001                }
5002            })?;
5003        let left_m = left_re.is_match(&subject);
5004        let right_m = dot == rhs_line;
5005        if self.flip_flop_active.len() <= slot {
5006            self.flip_flop_active.resize(slot + 1, false);
5007        }
5008        if self.flip_flop_exclusive_left_line.len() <= slot {
5009            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5010        }
5011        let active = &mut self.flip_flop_active[slot];
5012        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5013        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
5014            active, excl_left, exclusive, dot, left_m, right_m,
5015        )))
5016    }
5017
5018    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
5019    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
5020    /// right test until `$.` is strictly past the line where the left regex matched (same as
5021    /// [`Self::regex_flip_flop_eval`]).
5022    pub(crate) fn regex_eof_flip_flop_eval(
5023        &mut self,
5024        left_pat: &str,
5025        left_flags: &str,
5026        slot: usize,
5027        exclusive: bool,
5028        line: usize,
5029    ) -> PerlResult<PerlValue> {
5030        let dot = self.scalar_flipflop_dot_line();
5031        let subject = self.scope.get_scalar("_").to_string();
5032        let left_re = self
5033            .compile_regex(left_pat, left_flags, line)
5034            .map_err(|e| match e {
5035                FlowOrError::Error(err) => err,
5036                FlowOrError::Flow(_) => {
5037                    PerlError::runtime("unexpected flow in regex/eof flip-flop", line)
5038                }
5039            })?;
5040        let left_m = left_re.is_match(&subject);
5041        let right_m = self.eof_without_arg_is_true();
5042        if self.flip_flop_active.len() <= slot {
5043            self.flip_flop_active.resize(slot + 1, false);
5044        }
5045        if self.flip_flop_exclusive_left_line.len() <= slot {
5046            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5047        }
5048        let active = &mut self.flip_flop_active[slot];
5049        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5050        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
5051            active, excl_left, exclusive, dot, left_m, right_m,
5052        )))
5053    }
5054
5055    /// Shared `chomp` implementation (mutates `target`).
5056    /// `read(FH, $buf, LEN)` — read from filehandle into named variable.
5057    /// Returns bytes read count (or error). Called from VM's ReadIntoVar op.
5058    pub(crate) fn builtin_read_into(
5059        &mut self,
5060        fh_val: PerlValue,
5061        var_name: &str,
5062        length: usize,
5063        line: usize,
5064    ) -> ExecResult {
5065        use std::io::Read;
5066        let fh = fh_val
5067            .as_io_handle_name()
5068            .unwrap_or_else(|| fh_val.to_string());
5069        let mut buf = vec![0u8; length];
5070        let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
5071            slot.lock().read(&mut buf).unwrap_or(0)
5072        } else if fh == "STDIN" {
5073            std::io::stdin().read(&mut buf).unwrap_or(0)
5074        } else {
5075            return Err(PerlError::runtime(format!("read: unopened handle {}", fh), line).into());
5076        };
5077        buf.truncate(n);
5078        let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
5079        let _ = self.scope.set_scalar(var_name, PerlValue::string(read_str));
5080        Ok(PerlValue::integer(n as i64))
5081    }
5082
5083    pub(crate) fn chomp_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
5084        let mut s = val.to_string();
5085        let removed = if s.ends_with('\n') {
5086            s.pop();
5087            1i64
5088        } else {
5089            0i64
5090        };
5091        self.assign_value(target, PerlValue::string(s))?;
5092        Ok(PerlValue::integer(removed))
5093    }
5094
5095    /// Shared `chop` implementation (mutates `target`).
5096    pub(crate) fn chop_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
5097        let mut s = val.to_string();
5098        let chopped = s
5099            .pop()
5100            .map(|c| PerlValue::string(c.to_string()))
5101            .unwrap_or(PerlValue::UNDEF);
5102        self.assign_value(target, PerlValue::string(s))?;
5103        Ok(chopped)
5104    }
5105
5106    /// Shared regex match implementation (`pos` is updated for scalar `/g`).
5107    pub(crate) fn regex_match_execute(
5108        &mut self,
5109        s: String,
5110        pattern: &str,
5111        flags: &str,
5112        scalar_g: bool,
5113        pos_key: &str,
5114        line: usize,
5115    ) -> ExecResult {
5116        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
5117        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
5118        // also keep per-pattern `pos()` state that the memo doesn't track.
5119        //
5120        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
5121        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
5122        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
5123        if !flags.contains('g') && !scalar_g {
5124            let memo_hit = {
5125                if let Some(ref mem) = self.regex_match_memo {
5126                    mem.pattern == pattern
5127                        && mem.flags == flags
5128                        && mem.multiline == self.multiline_match
5129                        && mem.haystack == s
5130                } else {
5131                    false
5132                }
5133            };
5134            if memo_hit {
5135                if self.regex_capture_scope_fresh {
5136                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
5137                }
5138                // Memo hit but scope side effects were invalidated. Re-apply captures
5139                // from the memoized haystack + a fresh compiled regex.
5140                let (memo_s, memo_result) = {
5141                    let mem = self.regex_match_memo.as_ref().expect("memo");
5142                    (mem.haystack.clone(), mem.result.clone())
5143                };
5144                let re = self.compile_regex(pattern, flags, line)?;
5145                if let Some(caps) = re.captures(&memo_s) {
5146                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
5147                }
5148                self.regex_capture_scope_fresh = true;
5149                return Ok(memo_result);
5150            }
5151        }
5152        let re = self.compile_regex(pattern, flags, line)?;
5153        if flags.contains('g') && scalar_g {
5154            let key = pos_key.to_string();
5155            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
5156            if start == 0 {
5157                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5158            }
5159            if start > s.len() {
5160                self.regex_pos.insert(key, None);
5161                return Ok(PerlValue::integer(0));
5162            }
5163            let sub = s.get(start..).unwrap_or("");
5164            if let Some(caps) = re.captures(sub) {
5165                let overall = caps.get(0).expect("capture 0");
5166                let abs_end = start + overall.end;
5167                self.regex_pos.insert(key, Some(abs_end));
5168                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
5169                Ok(PerlValue::integer(1))
5170            } else {
5171                self.regex_pos.insert(key, None);
5172                Ok(PerlValue::integer(0))
5173            }
5174        } else if flags.contains('g') {
5175            let mut rows = Vec::new();
5176            let mut last_caps: Option<PerlCaptures<'_>> = None;
5177            for caps in re.captures_iter(&s) {
5178                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5179                    &caps,
5180                )));
5181                last_caps = Some(caps);
5182            }
5183            self.scope.set_array("^CAPTURE_ALL", rows)?;
5184            let matches: Vec<PerlValue> = match &*re {
5185                PerlCompiledRegex::Rust(r) => r
5186                    .find_iter(&s)
5187                    .map(|m| PerlValue::string(m.as_str().to_string()))
5188                    .collect(),
5189                PerlCompiledRegex::Fancy(r) => r
5190                    .find_iter(&s)
5191                    .filter_map(|m| m.ok())
5192                    .map(|m| PerlValue::string(m.as_str().to_string()))
5193                    .collect(),
5194                PerlCompiledRegex::Pcre2(r) => r
5195                    .find_iter(s.as_bytes())
5196                    .filter_map(|m| m.ok())
5197                    .map(|m| {
5198                        let t = s.get(m.start()..m.end()).unwrap_or("");
5199                        PerlValue::string(t.to_string())
5200                    })
5201                    .collect(),
5202            };
5203            if matches.is_empty() {
5204                Ok(PerlValue::integer(0))
5205            } else {
5206                if let Some(caps) = last_caps {
5207                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
5208                }
5209                Ok(PerlValue::array(matches))
5210            }
5211        } else if let Some(caps) = re.captures(&s) {
5212            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
5213            let result = PerlValue::integer(1);
5214            self.regex_match_memo = Some(RegexMatchMemo {
5215                pattern: pattern.to_string(),
5216                flags: flags.to_string(),
5217                multiline: self.multiline_match,
5218                haystack: s,
5219                result: result.clone(),
5220            });
5221            self.regex_capture_scope_fresh = true;
5222            Ok(result)
5223        } else {
5224            let result = PerlValue::integer(0);
5225            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
5226            self.regex_match_memo = Some(RegexMatchMemo {
5227                pattern: pattern.to_string(),
5228                flags: flags.to_string(),
5229                multiline: self.multiline_match,
5230                haystack: s,
5231                result: result.clone(),
5232            });
5233            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
5234            // the last successful match (if any) set them to. Don't flip the flag.
5235            Ok(result)
5236        }
5237    }
5238
5239    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
5240    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
5241    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
5242    pub(crate) fn expand_env_braces_in_subst(
5243        &mut self,
5244        raw: &str,
5245        line: usize,
5246    ) -> PerlResult<String> {
5247        self.materialize_env_if_needed();
5248        let mut out = String::new();
5249        let mut rest = raw;
5250        while let Some(idx) = rest.find("$ENV{") {
5251            out.push_str(&rest[..idx]);
5252            let after = &rest[idx + 5..];
5253            let end = after
5254                .find('}')
5255                .ok_or_else(|| PerlError::runtime("Unclosed $ENV{...} in s///", line))?;
5256            let key = &after[..end];
5257            let val = self.scope.get_hash_element("ENV", key);
5258            out.push_str(&val.to_string());
5259            rest = &after[end + 1..];
5260        }
5261        out.push_str(rest);
5262        Ok(out)
5263    }
5264
5265    /// Shared `s///` implementation.
5266    ///
5267    /// Perl replacement strings accept both `\1` and `$1` for back-references.
5268    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
5269    /// understand `$N`, so we normalise here.
5270    pub(crate) fn regex_subst_execute(
5271        &mut self,
5272        s: String,
5273        pattern: &str,
5274        replacement: &str,
5275        flags: &str,
5276        target: &Expr,
5277        line: usize,
5278    ) -> ExecResult {
5279        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
5280        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
5281        let re = self.compile_regex(&pattern, &re_flags, line)?;
5282        if flags.contains('e') {
5283            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
5284        }
5285        let replacement = self.expand_env_braces_in_subst(replacement, line)?;
5286        let replacement = self.interpolate_replacement_string(&replacement);
5287        let replacement = normalize_replacement_backrefs(&replacement);
5288        let last_caps = if flags.contains('g') {
5289            let mut rows = Vec::new();
5290            let mut last = None;
5291            for caps in re.captures_iter(&s) {
5292                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5293                    &caps,
5294                )));
5295                last = Some(caps);
5296            }
5297            self.scope.set_array("^CAPTURE_ALL", rows)?;
5298            last
5299        } else {
5300            re.captures(&s)
5301        };
5302        if let Some(caps) = last_caps {
5303            let mode = if flags.contains('g') {
5304                CaptureAllMode::Skip
5305            } else {
5306                CaptureAllMode::Empty
5307            };
5308            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
5309        }
5310        let (new_s, count) = if flags.contains('g') {
5311            let count = re.find_iter_count(&s);
5312            (re.replace_all(&s, replacement.as_str()), count)
5313        } else {
5314            let count = if re.is_match(&s) { 1 } else { 0 };
5315            (re.replace(&s, replacement.as_str()), count)
5316        };
5317        if flags.contains('r') {
5318            // /r — non-destructive: return the modified string, leave target unchanged
5319            Ok(PerlValue::string(new_s))
5320        } else {
5321            self.assign_value(target, PerlValue::string(new_s))?;
5322            Ok(PerlValue::integer(count as i64))
5323        }
5324    }
5325
5326    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
5327    /// and executes the string; the next round uses [`PerlValue::to_string`] of the prior value).
5328    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
5329        let prep_source = |raw: &str| -> String {
5330            let mut code = raw.trim().to_string();
5331            if !code.ends_with(';') {
5332                code.push(';');
5333            }
5334            code
5335        };
5336        let mut cur = prep_source(replacement);
5337        let mut last = PerlValue::UNDEF;
5338        for round in 0..e_count {
5339            last = crate::parse_and_run_string(&cur, self)?;
5340            if round + 1 < e_count {
5341                cur = prep_source(&last.to_string());
5342            }
5343        }
5344        Ok(last)
5345    }
5346
5347    fn regex_subst_execute_eval(
5348        &mut self,
5349        s: String,
5350        re: &PerlCompiledRegex,
5351        replacement: &str,
5352        flags: &str,
5353        target: &Expr,
5354        line: usize,
5355    ) -> ExecResult {
5356        let e_count = flags.chars().filter(|c| *c == 'e').count();
5357        if e_count == 0 {
5358            return Err(PerlError::runtime("s///e: internal error (no e flag)", line).into());
5359        }
5360
5361        if flags.contains('g') {
5362            let mut rows = Vec::new();
5363            let mut out = String::new();
5364            let mut last = 0usize;
5365            let mut count = 0usize;
5366            for caps in re.captures_iter(&s) {
5367                let m0 = caps.get(0).expect("regex capture 0");
5368                out.push_str(&s[last..m0.start]);
5369                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5370                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5371                out.push_str(&repl_val.to_string());
5372                last = m0.end;
5373                count += 1;
5374                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5375                    &caps,
5376                )));
5377            }
5378            self.scope.set_array("^CAPTURE_ALL", rows)?;
5379            out.push_str(&s[last..]);
5380            if flags.contains('r') {
5381                return Ok(PerlValue::string(out));
5382            }
5383            self.assign_value(target, PerlValue::string(out))?;
5384            return Ok(PerlValue::integer(count as i64));
5385        }
5386        if let Some(caps) = re.captures(&s) {
5387            let m0 = caps.get(0).expect("regex capture 0");
5388            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5389            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5390            let mut out = String::new();
5391            out.push_str(&s[..m0.start]);
5392            out.push_str(&repl_val.to_string());
5393            out.push_str(&s[m0.end..]);
5394            if flags.contains('r') {
5395                return Ok(PerlValue::string(out));
5396            }
5397            self.assign_value(target, PerlValue::string(out))?;
5398            return Ok(PerlValue::integer(1));
5399        }
5400        if flags.contains('r') {
5401            return Ok(PerlValue::string(s));
5402        }
5403        self.assign_value(target, PerlValue::string(s))?;
5404        Ok(PerlValue::integer(0))
5405    }
5406
5407    /// Shared `tr///` implementation.
5408    pub(crate) fn regex_transliterate_execute(
5409        &mut self,
5410        s: String,
5411        from: &str,
5412        to: &str,
5413        flags: &str,
5414        target: &Expr,
5415        line: usize,
5416    ) -> ExecResult {
5417        let _ = line;
5418        let from_chars = Self::tr_expand_ranges(from);
5419        let to_chars = Self::tr_expand_ranges(to);
5420        let delete_mode = flags.contains('d');
5421        let mut count = 0i64;
5422        let new_s: String = s
5423            .chars()
5424            .filter_map(|c| {
5425                if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
5426                    count += 1;
5427                    if delete_mode {
5428                        // /d — delete characters that match but have no replacement
5429                        if pos < to_chars.len() {
5430                            Some(to_chars[pos])
5431                        } else {
5432                            None // delete this character
5433                        }
5434                    } else {
5435                        // Normal mode: use last char in to_chars if pos exceeds, or keep original
5436                        Some(to_chars.get(pos).or(to_chars.last()).copied().unwrap_or(c))
5437                    }
5438                } else {
5439                    Some(c)
5440                }
5441            })
5442            .collect();
5443        if flags.contains('r') {
5444            // /r — non-destructive: return the modified string, leave target unchanged
5445            Ok(PerlValue::string(new_s))
5446        } else {
5447            self.assign_value(target, PerlValue::string(new_s))?;
5448            Ok(PerlValue::integer(count))
5449        }
5450    }
5451
5452    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
5453    /// A literal `-` at the start or end of the spec is kept as-is.
5454    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
5455        let raw: Vec<char> = spec.chars().collect();
5456        let mut out = Vec::with_capacity(raw.len());
5457        let mut i = 0;
5458        while i < raw.len() {
5459            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
5460                let start = raw[i] as u32;
5461                let end = raw[i + 2] as u32;
5462                for code in start..=end {
5463                    if let Some(c) = char::from_u32(code) {
5464                        out.push(c);
5465                    }
5466                }
5467                i += 3;
5468            } else {
5469                out.push(raw[i]);
5470                i += 1;
5471            }
5472        }
5473        out
5474    }
5475
5476    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
5477    pub(crate) fn splice_builtin_execute(
5478        &mut self,
5479        args: &[PerlValue],
5480        line: usize,
5481    ) -> PerlResult<PerlValue> {
5482        if args.is_empty() {
5483            return Err(PerlError::runtime("splice: missing array", line));
5484        }
5485        let arr_name = args[0].to_string();
5486        let arr_len = self.scope.array_len(&arr_name);
5487        let offset_val = args
5488            .get(1)
5489            .cloned()
5490            .unwrap_or_else(|| PerlValue::integer(0));
5491        let length_val = match args.get(2) {
5492            None => PerlValue::UNDEF,
5493            Some(v) => v.clone(),
5494        };
5495        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
5496        let rep_vals: Vec<PerlValue> = args.iter().skip(3).cloned().collect();
5497        let arr = self.scope.get_array_mut(&arr_name)?;
5498        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
5499        for (i, v) in rep_vals.into_iter().enumerate() {
5500            arr.insert(off + i, v);
5501        }
5502        Ok(match self.wantarray_kind {
5503            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
5504            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
5505        })
5506    }
5507
5508    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
5509    pub(crate) fn unshift_builtin_execute(
5510        &mut self,
5511        args: &[PerlValue],
5512        line: usize,
5513    ) -> PerlResult<PerlValue> {
5514        if args.is_empty() {
5515            return Err(PerlError::runtime("unshift: missing array", line));
5516        }
5517        let arr_name = args[0].to_string();
5518        let mut flat_vals: Vec<PerlValue> = Vec::new();
5519        for a in args.iter().skip(1) {
5520            if let Some(items) = a.as_array_vec() {
5521                flat_vals.extend(items);
5522            } else {
5523                flat_vals.push(a.clone());
5524            }
5525        }
5526        let arr = self.scope.get_array_mut(&arr_name)?;
5527        for (i, v) in flat_vals.into_iter().enumerate() {
5528            arr.insert(i, v);
5529        }
5530        Ok(PerlValue::integer(arr.len() as i64))
5531    }
5532
5533    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
5534    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
5535    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
5536        if upper == 0.0 {
5537            self.rand_rng.gen_range(0.0..1.0)
5538        } else if upper > 0.0 {
5539            self.rand_rng.gen_range(0.0..upper)
5540        } else {
5541            self.rand_rng.gen_range(upper..0.0)
5542        }
5543    }
5544
5545    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
5546    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
5547        let n = if let Some(s) = seed {
5548            s as i64
5549        } else {
5550            std::time::SystemTime::now()
5551                .duration_since(std::time::UNIX_EPOCH)
5552                .map(|d| d.as_secs() as i64)
5553                .unwrap_or(1)
5554        };
5555        let mag = n.unsigned_abs();
5556        self.rand_rng = StdRng::seed_from_u64(mag);
5557        n.abs()
5558    }
5559
5560    pub fn set_file(&mut self, file: &str) {
5561        self.file = file.to_string();
5562    }
5563
5564    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
5565    pub fn repl_completion_names(&self) -> Vec<String> {
5566        let mut v = self.scope.repl_binding_names();
5567        v.extend(self.subs.keys().cloned());
5568        v.sort();
5569        v.dedup();
5570        v
5571    }
5572
5573    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
5574    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
5575        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
5576        subs.sort();
5577        let mut classes: HashSet<String> = HashSet::new();
5578        for k in &subs {
5579            if let Some((pkg, rest)) = k.split_once("::") {
5580                if !rest.contains("::") {
5581                    classes.insert(pkg.to_string());
5582                }
5583            }
5584        }
5585        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
5586        for bn in self.scope.repl_binding_names() {
5587            if let Some(r) = bn.strip_prefix('$') {
5588                let v = self.scope.get_scalar(r);
5589                if let Some(b) = v.as_blessed_ref() {
5590                    blessed_scalars.insert(r.to_string(), b.class.clone());
5591                    classes.insert(b.class.clone());
5592                }
5593            }
5594        }
5595        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
5596        for c in classes {
5597            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
5598        }
5599        ReplCompletionSnapshot {
5600            subs,
5601            blessed_scalars,
5602            isa_for_class,
5603        }
5604    }
5605
5606    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
5607        if n == 0 {
5608            return Err(FlowOrError::Error(PerlError::runtime(
5609                "bench: iteration count must be positive",
5610                line,
5611            )));
5612        }
5613        let mut samples = Vec::with_capacity(n);
5614        for _ in 0..n {
5615            let start = std::time::Instant::now();
5616            self.exec_block(body)?;
5617            samples.push(start.elapsed().as_secs_f64() * 1000.0);
5618        }
5619        let mut sorted = samples.clone();
5620        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
5621        let min_ms = sorted[0];
5622        let mean = samples.iter().sum::<f64>() / n as f64;
5623        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
5624            .saturating_sub(1)
5625            .min(n - 1);
5626        let p99_ms = sorted[p99_idx];
5627        Ok(PerlValue::string(format!(
5628            "bench: n={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
5629            n, min_ms, mean, p99_ms
5630        )))
5631    }
5632
5633    pub fn execute(&mut self, program: &Program) -> PerlResult<PerlValue> {
5634        // `-n`/`-p`: compile and run only the prelude, store chunk for per-line re-execution.
5635        if self.line_mode_skip_main {
5636            crate::compile_and_run_prelude(program, self)?;
5637            return Ok(PerlValue::UNDEF);
5638        }
5639        crate::try_vm_execute(program, self)
5640            .expect("VM compilation must succeed — all execution is VM-only")
5641    }
5642
5643    /// Run `END` blocks (after `-n`/`-p` line loop when prelude used [`Self::line_mode_skip_main`]).
5644    pub fn run_end_blocks(&mut self) -> PerlResult<()> {
5645        self.global_phase = "END".to_string();
5646        let ends = std::mem::take(&mut self.end_blocks);
5647        for block in &ends {
5648            self.exec_block(block).map_err(|e| match e {
5649                FlowOrError::Error(e) => e,
5650                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in END", 0),
5651            })?;
5652        }
5653        Ok(())
5654    }
5655
5656    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
5657    /// and drain remaining `DESTROY` callbacks.
5658    pub fn run_global_teardown(&mut self) -> PerlResult<()> {
5659        self.global_phase = "DESTRUCT".to_string();
5660        self.drain_pending_destroys(0)
5661    }
5662
5663    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
5664    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> PerlResult<()> {
5665        loop {
5666            let batch = crate::pending_destroy::take_queue();
5667            if batch.is_empty() {
5668                break;
5669            }
5670            for (class, payload) in batch {
5671                let fq = format!("{}::DESTROY", class);
5672                let Some(sub) = self.subs.get(&fq).cloned() else {
5673                    continue;
5674                };
5675                let inv = PerlValue::blessed(Arc::new(
5676                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
5677                ));
5678                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
5679                    Ok(_) => {}
5680                    Err(FlowOrError::Error(e)) => return Err(e),
5681                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
5682                    Err(FlowOrError::Flow(other)) => {
5683                        return Err(PerlError::runtime(
5684                            format!("DESTROY: unexpected control flow ({other:?})"),
5685                            line,
5686                        ));
5687                    }
5688                }
5689            }
5690        }
5691        Ok(())
5692    }
5693
5694    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
5695        self.exec_block_with_tail(block, WantarrayCtx::Void)
5696    }
5697
5698    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
5699    /// Non-final statements stay void context.
5700    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5701        let uses_goto = block
5702            .iter()
5703            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
5704        if uses_goto {
5705            self.scope_push_hook();
5706            let r = self.exec_block_with_goto_tail(block, tail);
5707            self.scope_pop_hook();
5708            r
5709        } else {
5710            self.scope_push_hook();
5711            let result = self.exec_block_no_scope_with_tail(block, tail);
5712            self.scope_pop_hook();
5713            result
5714        }
5715    }
5716
5717    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5718        let mut map: HashMap<String, usize> = HashMap::new();
5719        for (i, s) in block.iter().enumerate() {
5720            if let Some(l) = &s.label {
5721                map.insert(l.clone(), i);
5722            }
5723        }
5724        let mut pc = 0usize;
5725        let mut last = PerlValue::UNDEF;
5726        let last_idx = block.len().saturating_sub(1);
5727        while pc < block.len() {
5728            if let StmtKind::Goto { target } = &block[pc].kind {
5729                let line = block[pc].line;
5730                let name = self.eval_expr(target)?.to_string();
5731                pc = *map.get(&name).ok_or_else(|| {
5732                    FlowOrError::Error(PerlError::runtime(
5733                        format!("goto: unknown label {}", name),
5734                        line,
5735                    ))
5736                })?;
5737                continue;
5738            }
5739            let v = if pc == last_idx {
5740                match &block[pc].kind {
5741                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
5742                    _ => self.exec_statement(&block[pc])?,
5743                }
5744            } else {
5745                self.exec_statement(&block[pc])?
5746            };
5747            last = v;
5748            pc += 1;
5749        }
5750        Ok(last)
5751    }
5752
5753    /// Execute block statements without pushing/popping a scope frame.
5754    /// Used internally by loops and the VM for sub calls.
5755    #[inline]
5756    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
5757        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
5758    }
5759
5760    pub(crate) fn exec_block_no_scope_with_tail(
5761        &mut self,
5762        block: &Block,
5763        tail: WantarrayCtx,
5764    ) -> ExecResult {
5765        if block.is_empty() {
5766            return Ok(PerlValue::UNDEF);
5767        }
5768        let last_i = block.len() - 1;
5769        for (i, stmt) in block.iter().enumerate() {
5770            if i < last_i {
5771                self.exec_statement(stmt)?;
5772            } else {
5773                return match &stmt.kind {
5774                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
5775                    _ => self.exec_statement(stmt),
5776                };
5777            }
5778        }
5779        Ok(PerlValue::UNDEF)
5780    }
5781
5782    /// Spawn `block` on a worker thread; returns an [`PerlValue::AsyncTask`] handle (`async { }` / `spawn { }`).
5783    pub(crate) fn spawn_async_block(&self, block: &Block) -> PerlValue {
5784        use parking_lot::Mutex as ParkMutex;
5785
5786        let block = block.clone();
5787        let subs = self.subs.clone();
5788        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5789        let result = Arc::new(ParkMutex::new(None));
5790        let join = Arc::new(ParkMutex::new(None));
5791        let result2 = result.clone();
5792        let h = std::thread::spawn(move || {
5793            let mut interp = Interpreter::new();
5794            interp.subs = subs;
5795            interp.scope.restore_capture(&scalars);
5796            interp.scope.restore_atomics(&aar, &ahash);
5797            interp.enable_parallel_guard();
5798            let r = match interp.exec_block(&block) {
5799                Ok(v) => Ok(v),
5800                Err(FlowOrError::Error(e)) => Err(e),
5801                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5802                    Err(PerlError::runtime("yield inside async/spawn block", 0))
5803                }
5804                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5805            };
5806            *result2.lock() = Some(r);
5807        });
5808        *join.lock() = Some(h);
5809        PerlValue::async_task(Arc::new(PerlAsyncTask { result, join }))
5810    }
5811
5812    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
5813    pub(crate) fn eval_timeout_block(
5814        &mut self,
5815        body: &Block,
5816        secs: f64,
5817        line: usize,
5818    ) -> ExecResult {
5819        use std::sync::mpsc::channel;
5820        use std::time::Duration;
5821
5822        let block = body.clone();
5823        let subs = self.subs.clone();
5824        let struct_defs = self.struct_defs.clone();
5825        let enum_defs = self.enum_defs.clone();
5826        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5827        self.materialize_env_if_needed();
5828        let env = self.env.clone();
5829        let argv = self.argv.clone();
5830        let inc = self.scope.get_array("INC");
5831        let (tx, rx) = channel::<PerlResult<PerlValue>>();
5832        let _handle = std::thread::spawn(move || {
5833            let mut interp = Interpreter::new();
5834            interp.subs = subs;
5835            interp.struct_defs = struct_defs;
5836            interp.enum_defs = enum_defs;
5837            interp.env = env.clone();
5838            interp.argv = argv.clone();
5839            interp.scope.declare_array(
5840                "ARGV",
5841                argv.iter().map(|s| PerlValue::string(s.clone())).collect(),
5842            );
5843            for (k, v) in env {
5844                interp
5845                    .scope
5846                    .set_hash_element("ENV", &k, v)
5847                    .expect("set ENV in timeout thread");
5848            }
5849            interp.scope.declare_array("INC", inc);
5850            interp.scope.restore_capture(&scalars);
5851            interp.scope.restore_atomics(&aar, &ahash);
5852            interp.enable_parallel_guard();
5853            let out: PerlResult<PerlValue> = match interp.exec_block(&block) {
5854                Ok(v) => Ok(v),
5855                Err(FlowOrError::Error(e)) => Err(e),
5856                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5857                    Err(PerlError::runtime("yield inside eval_timeout block", 0))
5858                }
5859                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5860            };
5861            let _ = tx.send(out);
5862        });
5863        let dur = Duration::from_secs_f64(secs.max(0.0));
5864        match rx.recv_timeout(dur) {
5865            Ok(Ok(v)) => Ok(v),
5866            Ok(Err(e)) => Err(FlowOrError::Error(e)),
5867            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(PerlError::runtime(
5868                format!(
5869                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
5870                    secs
5871                ),
5872                line,
5873            )
5874            .into()),
5875            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(PerlError::runtime(
5876                "eval_timeout: worker thread panicked or disconnected",
5877                line,
5878            )
5879            .into()),
5880        }
5881    }
5882
5883    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
5884        let mut last = PerlValue::UNDEF;
5885        for stmt in body {
5886            match &stmt.kind {
5887                StmtKind::When { cond, body: wb } => {
5888                    if self.when_matches(cond)? {
5889                        return self.exec_block_smart(wb);
5890                    }
5891                }
5892                StmtKind::DefaultCase { body: db } => {
5893                    return self.exec_block_smart(db);
5894                }
5895                _ => {
5896                    last = self.exec_statement(stmt)?;
5897                }
5898            }
5899        }
5900        Ok(last)
5901    }
5902
5903    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
5904    pub(crate) fn exec_given_with_topic_value(
5905        &mut self,
5906        topic: PerlValue,
5907        body: &Block,
5908    ) -> ExecResult {
5909        self.scope_push_hook();
5910        self.scope.declare_scalar("_", topic);
5911        self.english_note_lexical_scalar("_");
5912        let r = self.exec_given_body(body);
5913        self.scope_pop_hook();
5914        r
5915    }
5916
5917    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
5918        let t = self.eval_expr(topic)?;
5919        self.exec_given_with_topic_value(t, body)
5920    }
5921
5922    /// `when (COND)` — topic is `$_` (set by `given`).
5923    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
5924        let topic = self.scope.get_scalar("_");
5925        let line = cond.line;
5926        match &cond.kind {
5927            ExprKind::Regex(pattern, flags) => {
5928                let re = self.compile_regex(pattern, flags, line)?;
5929                let s = topic.to_string();
5930                Ok(re.is_match(&s))
5931            }
5932            ExprKind::String(s) => Ok(topic.to_string() == *s),
5933            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
5934            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
5935            _ => {
5936                let c = self.eval_expr(cond)?;
5937                Ok(self.smartmatch_when(&topic, &c))
5938            }
5939        }
5940    }
5941
5942    fn smartmatch_when(&self, topic: &PerlValue, c: &PerlValue) -> bool {
5943        if let Some(re) = c.as_regex() {
5944            return re.is_match(&topic.to_string());
5945        }
5946        topic.to_string() == c.to_string()
5947    }
5948
5949    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
5950    pub(crate) fn eval_boolean_rvalue_condition(
5951        &mut self,
5952        cond: &Expr,
5953    ) -> Result<bool, FlowOrError> {
5954        match &cond.kind {
5955            ExprKind::Regex(pattern, flags) => {
5956                let topic = self.scope.get_scalar("_");
5957                let line = cond.line;
5958                let s = topic.to_string();
5959                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
5960                Ok(v.is_true())
5961            }
5962            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
5963            ExprKind::ReadLine(_) => {
5964                let v = self.eval_expr(cond)?;
5965                self.scope.set_topic(v.clone());
5966                Ok(!v.is_undef())
5967            }
5968            _ => {
5969                let v = self.eval_expr(cond)?;
5970                Ok(v.is_true())
5971            }
5972        }
5973    }
5974
5975    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
5976    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
5977        self.eval_boolean_rvalue_condition(cond)
5978    }
5979
5980    pub(crate) fn eval_algebraic_match(
5981        &mut self,
5982        subject: &Expr,
5983        arms: &[MatchArm],
5984        line: usize,
5985    ) -> ExecResult {
5986        let val = self.eval_algebraic_match_subject(subject, line)?;
5987        self.eval_algebraic_match_with_subject_value(val, arms, line)
5988    }
5989
5990    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
5991    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
5992        match &subject.kind {
5993            ExprKind::ArrayVar(name) => {
5994                self.check_strict_array_var(name, line)?;
5995                let aname = self.stash_array_name_for_package(name);
5996                Ok(PerlValue::array_binding_ref(aname))
5997            }
5998            ExprKind::HashVar(name) => {
5999                self.check_strict_hash_var(name, line)?;
6000                self.touch_env_hash(name);
6001                Ok(PerlValue::hash_binding_ref(name.clone()))
6002            }
6003            _ => self.eval_expr(subject),
6004        }
6005    }
6006
6007    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
6008    pub(crate) fn eval_algebraic_match_with_subject_value(
6009        &mut self,
6010        val: PerlValue,
6011        arms: &[MatchArm],
6012        line: usize,
6013    ) -> ExecResult {
6014        // Exhaustive enum match: check variant coverage before matching
6015        if let Some(e) = val.as_enum_inst() {
6016            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
6017            if !has_catchall {
6018                let covered: Vec<String> = arms
6019                    .iter()
6020                    .filter_map(|a| {
6021                        if let MatchPattern::Value(expr) = &a.pattern {
6022                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
6023                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
6024                            }
6025                        }
6026                        None
6027                    })
6028                    .collect();
6029                let missing: Vec<&str> = e
6030                    .def
6031                    .variants
6032                    .iter()
6033                    .filter(|v| !covered.contains(&v.name))
6034                    .map(|v| v.name.as_str())
6035                    .collect();
6036                if !missing.is_empty() {
6037                    return Err(PerlError::runtime(
6038                        format!(
6039                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
6040                            e.def.name,
6041                            missing.join(", ")
6042                        ),
6043                        line,
6044                    )
6045                    .into());
6046                }
6047            }
6048        }
6049        for arm in arms {
6050            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
6051                let re = self.compile_regex(pattern, flags, line)?;
6052                let s = val.to_string();
6053                if let Some(caps) = re.captures(&s) {
6054                    self.scope_push_hook();
6055                    self.scope.declare_scalar("_", val.clone());
6056                    self.english_note_lexical_scalar("_");
6057                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
6058                    let guard_ok = if let Some(g) = &arm.guard {
6059                        self.eval_expr(g)?.is_true()
6060                    } else {
6061                        true
6062                    };
6063                    if !guard_ok {
6064                        self.scope_pop_hook();
6065                        continue;
6066                    }
6067                    let out = self.eval_expr(&arm.body);
6068                    self.scope_pop_hook();
6069                    return out;
6070                }
6071                continue;
6072            }
6073            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
6074                self.scope_push_hook();
6075                self.scope.declare_scalar("_", val.clone());
6076                self.english_note_lexical_scalar("_");
6077                for b in bindings {
6078                    match b {
6079                        PatternBinding::Scalar(name, v) => {
6080                            self.scope.declare_scalar(&name, v);
6081                            self.english_note_lexical_scalar(&name);
6082                        }
6083                        PatternBinding::Array(name, elems) => {
6084                            self.scope.declare_array(&name, elems);
6085                        }
6086                    }
6087                }
6088                let guard_ok = if let Some(g) = &arm.guard {
6089                    self.eval_expr(g)?.is_true()
6090                } else {
6091                    true
6092                };
6093                if !guard_ok {
6094                    self.scope_pop_hook();
6095                    continue;
6096                }
6097                let out = self.eval_expr(&arm.body);
6098                self.scope_pop_hook();
6099                return out;
6100            }
6101        }
6102        Err(PerlError::runtime(
6103            "match: no arm matched the value (add a `_` catch-all)",
6104            line,
6105        )
6106        .into())
6107    }
6108
6109    fn parse_duration_seconds(pv: &PerlValue) -> Option<f64> {
6110        let s = pv.to_string();
6111        let s = s.trim();
6112        if let Some(rest) = s.strip_suffix("ms") {
6113            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
6114        }
6115        if let Some(rest) = s.strip_suffix('s') {
6116            return rest.trim().parse::<f64>().ok();
6117        }
6118        if let Some(rest) = s.strip_suffix('m') {
6119            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
6120        }
6121        s.parse::<f64>().ok()
6122    }
6123
6124    fn eval_retry_block(
6125        &mut self,
6126        body: &Block,
6127        times: &Expr,
6128        backoff: RetryBackoff,
6129        _line: usize,
6130    ) -> ExecResult {
6131        let max = self.eval_expr(times)?.to_int().max(1) as usize;
6132        let base_ms: u64 = 10;
6133        let mut attempt = 0usize;
6134        loop {
6135            attempt += 1;
6136            match self.exec_block(body) {
6137                Ok(v) => return Ok(v),
6138                Err(FlowOrError::Error(e)) => {
6139                    if attempt >= max {
6140                        return Err(FlowOrError::Error(e));
6141                    }
6142                    let delay_ms = match backoff {
6143                        RetryBackoff::None => 0,
6144                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
6145                        RetryBackoff::Exponential => {
6146                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
6147                        }
6148                    };
6149                    if delay_ms > 0 {
6150                        std::thread::sleep(Duration::from_millis(delay_ms));
6151                    }
6152                }
6153                Err(e) => return Err(e),
6154            }
6155        }
6156    }
6157
6158    fn eval_rate_limit_block(
6159        &mut self,
6160        slot: u32,
6161        max: &Expr,
6162        window: &Expr,
6163        body: &Block,
6164        _line: usize,
6165    ) -> ExecResult {
6166        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
6167        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
6168            .filter(|s| *s > 0.0)
6169            .unwrap_or(1.0);
6170        let window_d = Duration::from_secs_f64(window_sec);
6171        let slot = slot as usize;
6172        while self.rate_limit_slots.len() <= slot {
6173            self.rate_limit_slots.push(VecDeque::new());
6174        }
6175        {
6176            let dq = &mut self.rate_limit_slots[slot];
6177            loop {
6178                let now = Instant::now();
6179                while let Some(t0) = dq.front().copied() {
6180                    if now.duration_since(t0) >= window_d {
6181                        dq.pop_front();
6182                    } else {
6183                        break;
6184                    }
6185                }
6186                if dq.len() < max_n || max_n == 0 {
6187                    break;
6188                }
6189                let t0 = dq.front().copied().unwrap();
6190                let wait = window_d.saturating_sub(now.duration_since(t0));
6191                if wait.is_zero() {
6192                    dq.pop_front();
6193                    continue;
6194                }
6195                std::thread::sleep(wait);
6196            }
6197            dq.push_back(Instant::now());
6198        }
6199        self.exec_block(body)
6200    }
6201
6202    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
6203        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
6204            .filter(|s| *s > 0.0)
6205            .unwrap_or(1.0);
6206        loop {
6207            match self.exec_block(body) {
6208                Ok(_) => {}
6209                Err(e) => return Err(e),
6210            }
6211            std::thread::sleep(Duration::from_secs_f64(sec));
6212        }
6213    }
6214
6215    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
6216    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> PerlResult<PerlValue> {
6217        let pair = |value: PerlValue, more: i64| {
6218            PerlValue::array_ref(Arc::new(RwLock::new(vec![value, PerlValue::integer(more)])))
6219        };
6220        let mut exhausted = gen.exhausted.lock();
6221        if *exhausted {
6222            return Ok(pair(PerlValue::UNDEF, 0));
6223        }
6224        let mut pc = gen.pc.lock();
6225        let mut scope_started = gen.scope_started.lock();
6226        if *pc >= gen.block.len() {
6227            if *scope_started {
6228                self.scope_pop_hook();
6229                *scope_started = false;
6230            }
6231            *exhausted = true;
6232            return Ok(pair(PerlValue::UNDEF, 0));
6233        }
6234        if !*scope_started {
6235            self.scope_push_hook();
6236            *scope_started = true;
6237        }
6238        self.in_generator = true;
6239        while *pc < gen.block.len() {
6240            let stmt = &gen.block[*pc];
6241            match self.exec_statement(stmt) {
6242                Ok(_) => {
6243                    *pc += 1;
6244                }
6245                Err(FlowOrError::Flow(Flow::Yield(v))) => {
6246                    *pc += 1;
6247                    self.in_generator = false;
6248                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
6249                    // binds in the caller block, not inside a frame left across yield.
6250                    if *scope_started {
6251                        self.scope_pop_hook();
6252                        *scope_started = false;
6253                    }
6254                    return Ok(pair(v, 1));
6255                }
6256                Err(e) => {
6257                    self.in_generator = false;
6258                    if *scope_started {
6259                        self.scope_pop_hook();
6260                        *scope_started = false;
6261                    }
6262                    return Err(match e {
6263                        FlowOrError::Error(ee) => ee,
6264                        FlowOrError::Flow(Flow::Yield(_)) => {
6265                            unreachable!("yield handled above")
6266                        }
6267                        FlowOrError::Flow(flow) => PerlError::runtime(
6268                            format!("unexpected control flow in generator: {:?}", flow),
6269                            0,
6270                        ),
6271                    });
6272                }
6273            }
6274        }
6275        self.in_generator = false;
6276        if *scope_started {
6277            self.scope_pop_hook();
6278            *scope_started = false;
6279        }
6280        *exhausted = true;
6281        Ok(pair(PerlValue::UNDEF, 0))
6282    }
6283
6284    fn match_pattern_try(
6285        &mut self,
6286        subject: &PerlValue,
6287        pattern: &MatchPattern,
6288        line: usize,
6289    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6290        match pattern {
6291            MatchPattern::Any => Ok(Some(vec![])),
6292            MatchPattern::Regex { .. } => {
6293                unreachable!("regex arms are handled in eval_algebraic_match")
6294            }
6295            MatchPattern::Value(expr) => {
6296                if self.match_pattern_value_alternation(subject, expr, line)? {
6297                    Ok(Some(vec![]))
6298                } else {
6299                    Ok(None)
6300                }
6301            }
6302            MatchPattern::Array(elems) => {
6303                let Some(arr) = self.match_subject_as_array(subject) else {
6304                    return Ok(None);
6305                };
6306                self.match_array_pattern_elems(&arr, elems, line)
6307            }
6308            MatchPattern::Hash(pairs) => {
6309                let Some(h) = self.match_subject_as_hash(subject) else {
6310                    return Ok(None);
6311                };
6312                self.match_hash_pattern_pairs(&h, pairs, line)
6313            }
6314            MatchPattern::OptionSome(name) => {
6315                let Some(arr) = self.match_subject_as_array(subject) else {
6316                    return Ok(None);
6317                };
6318                if arr.len() < 2 {
6319                    return Ok(None);
6320                }
6321                if !arr[1].is_true() {
6322                    return Ok(None);
6323                }
6324                Ok(Some(vec![PatternBinding::Scalar(
6325                    name.clone(),
6326                    arr[0].clone(),
6327                )]))
6328            }
6329        }
6330    }
6331
6332    /// Handle pattern alternation (e.g., `"foo" | "bar" | "baz"`) in match patterns.
6333    /// If the expression is a BitOr chain, recursively check if subject matches any alternative.
6334    fn match_pattern_value_alternation(
6335        &mut self,
6336        subject: &PerlValue,
6337        expr: &Expr,
6338        _line: usize,
6339    ) -> Result<bool, FlowOrError> {
6340        if let ExprKind::BinOp {
6341            left,
6342            op: BinOp::BitOr,
6343            right,
6344        } = &expr.kind
6345        {
6346            if self.match_pattern_value_alternation(subject, left, _line)? {
6347                return Ok(true);
6348            }
6349            return self.match_pattern_value_alternation(subject, right, _line);
6350        }
6351        let pv = self.eval_expr(expr)?;
6352        Ok(self.smartmatch_when(subject, &pv))
6353    }
6354
6355    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
6356    fn match_subject_as_array(&self, v: &PerlValue) -> Option<Vec<PerlValue>> {
6357        if let Some(a) = v.as_array_vec() {
6358            return Some(a);
6359        }
6360        if let Some(r) = v.as_array_ref() {
6361            return Some(r.read().clone());
6362        }
6363        if let Some(name) = v.as_array_binding_name() {
6364            return Some(self.scope.get_array(&name));
6365        }
6366        None
6367    }
6368
6369    fn match_subject_as_hash(&mut self, v: &PerlValue) -> Option<IndexMap<String, PerlValue>> {
6370        if let Some(h) = v.as_hash_map() {
6371            return Some(h);
6372        }
6373        if let Some(r) = v.as_hash_ref() {
6374            return Some(r.read().clone());
6375        }
6376        if let Some(name) = v.as_hash_binding_name() {
6377            self.touch_env_hash(&name);
6378            return Some(self.scope.get_hash(&name));
6379        }
6380        None
6381    }
6382
6383    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
6384    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
6385    pub(crate) fn hash_slice_deref_values(
6386        &mut self,
6387        container: &PerlValue,
6388        key_values: &[PerlValue],
6389        line: usize,
6390    ) -> Result<PerlValue, FlowOrError> {
6391        let h = if let Some(m) = self.match_subject_as_hash(container) {
6392            m
6393        } else {
6394            return Err(PerlError::runtime(
6395                "Hash slice dereference needs a hash or hash reference value",
6396                line,
6397            )
6398            .into());
6399        };
6400        let mut result = Vec::new();
6401        for kv in key_values {
6402            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6403                vv.iter().map(|x| x.to_string()).collect()
6404            } else {
6405                vec![kv.to_string()]
6406            };
6407            for k in key_strings {
6408                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6409            }
6410        }
6411        Ok(PerlValue::array(result))
6412    }
6413
6414    /// Single-key write for a hash slice container (hash ref or package hash name).
6415    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
6416    pub(crate) fn assign_hash_slice_one_key(
6417        &mut self,
6418        container: PerlValue,
6419        key: &str,
6420        val: PerlValue,
6421        line: usize,
6422    ) -> Result<PerlValue, FlowOrError> {
6423        if let Some(r) = container.as_hash_ref() {
6424            r.write().insert(key.to_string(), val);
6425            return Ok(PerlValue::UNDEF);
6426        }
6427        if let Some(name) = container.as_hash_binding_name() {
6428            self.touch_env_hash(&name);
6429            self.scope
6430                .set_hash_element(&name, key, val)
6431                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6432            return Ok(PerlValue::UNDEF);
6433        }
6434        if let Some(s) = container.as_str() {
6435            self.touch_env_hash(&s);
6436            if self.strict_refs {
6437                return Err(PerlError::runtime(
6438                    format!(
6439                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6440                        s
6441                    ),
6442                    line,
6443                )
6444                .into());
6445            }
6446            self.scope
6447                .set_hash_element(&s, key, val)
6448                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6449            return Ok(PerlValue::UNDEF);
6450        }
6451        Err(PerlError::runtime(
6452            "Hash slice assignment needs a hash or hash reference value",
6453            line,
6454        )
6455        .into())
6456    }
6457
6458    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
6459    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
6460    pub(crate) fn assign_named_hash_slice(
6461        &mut self,
6462        hash: &str,
6463        key_values: Vec<PerlValue>,
6464        val: PerlValue,
6465        line: usize,
6466    ) -> Result<PerlValue, FlowOrError> {
6467        self.touch_env_hash(hash);
6468        let mut ks: Vec<String> = Vec::new();
6469        for kv in key_values {
6470            if let Some(vv) = kv.as_array_vec() {
6471                ks.extend(vv.iter().map(|x| x.to_string()));
6472            } else {
6473                ks.push(kv.to_string());
6474            }
6475        }
6476        if ks.is_empty() {
6477            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6478        }
6479        let items = val.to_list();
6480        for (i, k) in ks.iter().enumerate() {
6481            let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6482            self.scope
6483                .set_hash_element(hash, k, v)
6484                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6485        }
6486        Ok(PerlValue::UNDEF)
6487    }
6488
6489    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
6490    pub(crate) fn assign_hash_slice_deref(
6491        &mut self,
6492        container: PerlValue,
6493        key_values: Vec<PerlValue>,
6494        val: PerlValue,
6495        line: usize,
6496    ) -> Result<PerlValue, FlowOrError> {
6497        let mut ks: Vec<String> = Vec::new();
6498        for kv in key_values {
6499            if let Some(vv) = kv.as_array_vec() {
6500                ks.extend(vv.iter().map(|x| x.to_string()));
6501            } else {
6502                ks.push(kv.to_string());
6503            }
6504        }
6505        if ks.is_empty() {
6506            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6507        }
6508        let items = val.to_list();
6509        if let Some(r) = container.as_hash_ref() {
6510            let mut h = r.write();
6511            for (i, k) in ks.iter().enumerate() {
6512                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6513                h.insert(k.clone(), v);
6514            }
6515            return Ok(PerlValue::UNDEF);
6516        }
6517        if let Some(name) = container.as_hash_binding_name() {
6518            self.touch_env_hash(&name);
6519            for (i, k) in ks.iter().enumerate() {
6520                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6521                self.scope
6522                    .set_hash_element(&name, k, v)
6523                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6524            }
6525            return Ok(PerlValue::UNDEF);
6526        }
6527        if let Some(s) = container.as_str() {
6528            if self.strict_refs {
6529                return Err(PerlError::runtime(
6530                    format!(
6531                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6532                        s
6533                    ),
6534                    line,
6535                )
6536                .into());
6537            }
6538            self.touch_env_hash(&s);
6539            for (i, k) in ks.iter().enumerate() {
6540                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6541                self.scope
6542                    .set_hash_element(&s, k, v)
6543                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6544            }
6545            return Ok(PerlValue::UNDEF);
6546        }
6547        Err(PerlError::runtime(
6548            "Hash slice assignment needs a hash or hash reference value",
6549            line,
6550        )
6551        .into())
6552    }
6553
6554    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
6555    /// Perl 5 applies the compound op only to the **last** slice element.
6556    pub(crate) fn compound_assign_hash_slice_deref(
6557        &mut self,
6558        container: PerlValue,
6559        key_values: Vec<PerlValue>,
6560        op: BinOp,
6561        rhs: PerlValue,
6562        line: usize,
6563    ) -> Result<PerlValue, FlowOrError> {
6564        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6565        let last_old = old_list
6566            .to_list()
6567            .last()
6568            .cloned()
6569            .unwrap_or(PerlValue::UNDEF);
6570        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6571        let mut ks: Vec<String> = Vec::new();
6572        for kv in &key_values {
6573            if let Some(vv) = kv.as_array_vec() {
6574                ks.extend(vv.iter().map(|x| x.to_string()));
6575            } else {
6576                ks.push(kv.to_string());
6577            }
6578        }
6579        if ks.is_empty() {
6580            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6581        }
6582        let last_key = ks.last().expect("non-empty ks");
6583        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6584        Ok(new_val)
6585    }
6586
6587    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
6588    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
6589    /// the **old** value of that last element.
6590    ///
6591    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
6592    pub(crate) fn hash_slice_deref_inc_dec(
6593        &mut self,
6594        container: PerlValue,
6595        key_values: Vec<PerlValue>,
6596        kind: u8,
6597        line: usize,
6598    ) -> Result<PerlValue, FlowOrError> {
6599        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6600        let last_old = old_list
6601            .to_list()
6602            .last()
6603            .cloned()
6604            .unwrap_or(PerlValue::UNDEF);
6605        let new_val = if kind & 1 == 0 {
6606            PerlValue::integer(last_old.to_int() + 1)
6607        } else {
6608            PerlValue::integer(last_old.to_int() - 1)
6609        };
6610        let mut ks: Vec<String> = Vec::new();
6611        for kv in &key_values {
6612            if let Some(vv) = kv.as_array_vec() {
6613                ks.extend(vv.iter().map(|x| x.to_string()));
6614            } else {
6615                ks.push(kv.to_string());
6616            }
6617        }
6618        let last_key = ks.last().ok_or_else(|| {
6619            PerlError::runtime("Hash slice increment needs at least one key", line)
6620        })?;
6621        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6622        Ok(if kind < 2 { new_val } else { last_old })
6623    }
6624
6625    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[PerlValue]) -> PerlValue {
6626        self.touch_env_hash(hash);
6627        let h = self.scope.get_hash(hash);
6628        let mut result = Vec::new();
6629        for kv in key_values {
6630            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6631                vv.iter().map(|x| x.to_string()).collect()
6632            } else {
6633                vec![kv.to_string()]
6634            };
6635            for k in key_strings {
6636                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6637            }
6638        }
6639        PerlValue::array(result)
6640    }
6641
6642    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
6643    pub(crate) fn compound_assign_named_hash_slice(
6644        &mut self,
6645        hash: &str,
6646        key_values: Vec<PerlValue>,
6647        op: BinOp,
6648        rhs: PerlValue,
6649        line: usize,
6650    ) -> Result<PerlValue, FlowOrError> {
6651        let old_list = self.hash_slice_named_values(hash, &key_values);
6652        let last_old = old_list
6653            .to_list()
6654            .last()
6655            .cloned()
6656            .unwrap_or(PerlValue::UNDEF);
6657        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6658        let mut ks: Vec<String> = Vec::new();
6659        for kv in &key_values {
6660            if let Some(vv) = kv.as_array_vec() {
6661                ks.extend(vv.iter().map(|x| x.to_string()));
6662            } else {
6663                ks.push(kv.to_string());
6664            }
6665        }
6666        if ks.is_empty() {
6667            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6668        }
6669        let last_key = ks.last().expect("non-empty ks");
6670        let container = PerlValue::string(hash.to_string());
6671        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6672        Ok(new_val)
6673    }
6674
6675    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
6676    pub(crate) fn named_hash_slice_inc_dec(
6677        &mut self,
6678        hash: &str,
6679        key_values: Vec<PerlValue>,
6680        kind: u8,
6681        line: usize,
6682    ) -> Result<PerlValue, FlowOrError> {
6683        let old_list = self.hash_slice_named_values(hash, &key_values);
6684        let last_old = old_list
6685            .to_list()
6686            .last()
6687            .cloned()
6688            .unwrap_or(PerlValue::UNDEF);
6689        let new_val = if kind & 1 == 0 {
6690            PerlValue::integer(last_old.to_int() + 1)
6691        } else {
6692            PerlValue::integer(last_old.to_int() - 1)
6693        };
6694        let mut ks: Vec<String> = Vec::new();
6695        for kv in &key_values {
6696            if let Some(vv) = kv.as_array_vec() {
6697                ks.extend(vv.iter().map(|x| x.to_string()));
6698            } else {
6699                ks.push(kv.to_string());
6700            }
6701        }
6702        let last_key = ks.last().ok_or_else(|| {
6703            PerlError::runtime("Hash slice increment needs at least one key", line)
6704        })?;
6705        let container = PerlValue::string(hash.to_string());
6706        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6707        Ok(if kind < 2 { new_val } else { last_old })
6708    }
6709
6710    fn match_array_pattern_elems(
6711        &mut self,
6712        arr: &[PerlValue],
6713        elems: &[MatchArrayElem],
6714        line: usize,
6715    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6716        let has_rest = elems
6717            .iter()
6718            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
6719        let mut binds: Vec<PatternBinding> = Vec::new();
6720        let mut idx = 0usize;
6721        for (i, elem) in elems.iter().enumerate() {
6722            match elem {
6723                MatchArrayElem::Rest => {
6724                    if i != elems.len() - 1 {
6725                        return Err(PerlError::runtime(
6726                            "internal: `*` must be last in array match pattern",
6727                            line,
6728                        )
6729                        .into());
6730                    }
6731                    return Ok(Some(binds));
6732                }
6733                MatchArrayElem::RestBind(name) => {
6734                    if i != elems.len() - 1 {
6735                        return Err(PerlError::runtime(
6736                            "internal: `@name` rest bind must be last in array match pattern",
6737                            line,
6738                        )
6739                        .into());
6740                    }
6741                    let tail = arr[idx..].to_vec();
6742                    binds.push(PatternBinding::Array(name.clone(), tail));
6743                    return Ok(Some(binds));
6744                }
6745                MatchArrayElem::CaptureScalar(name) => {
6746                    if idx >= arr.len() {
6747                        return Ok(None);
6748                    }
6749                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
6750                    idx += 1;
6751                }
6752                MatchArrayElem::Expr(e) => {
6753                    if idx >= arr.len() {
6754                        return Ok(None);
6755                    }
6756                    let expected = self.eval_expr(e)?;
6757                    if !self.smartmatch_when(&arr[idx], &expected) {
6758                        return Ok(None);
6759                    }
6760                    idx += 1;
6761                }
6762            }
6763        }
6764        if !has_rest && idx != arr.len() {
6765            return Ok(None);
6766        }
6767        Ok(Some(binds))
6768    }
6769
6770    fn match_hash_pattern_pairs(
6771        &mut self,
6772        h: &IndexMap<String, PerlValue>,
6773        pairs: &[MatchHashPair],
6774        _line: usize,
6775    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6776        let mut binds = Vec::new();
6777        for pair in pairs {
6778            match pair {
6779                MatchHashPair::KeyOnly { key } => {
6780                    let ks = self.eval_expr(key)?.to_string();
6781                    if !h.contains_key(&ks) {
6782                        return Ok(None);
6783                    }
6784                }
6785                MatchHashPair::Capture { key, name } => {
6786                    let ks = self.eval_expr(key)?.to_string();
6787                    let Some(v) = h.get(&ks) else {
6788                        return Ok(None);
6789                    };
6790                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
6791                }
6792            }
6793        }
6794        Ok(Some(binds))
6795    }
6796
6797    /// Check if a block declares variables (needs its own scope frame).
6798    #[inline]
6799    fn block_needs_scope(block: &Block) -> bool {
6800        block.iter().any(|s| match &s.kind {
6801            StmtKind::My(_)
6802            | StmtKind::Our(_)
6803            | StmtKind::Local(_)
6804            | StmtKind::State(_)
6805            | StmtKind::LocalExpr { .. } => true,
6806            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
6807            _ => false,
6808        })
6809    }
6810
6811    /// Execute block, only pushing a scope frame if needed.
6812    #[inline]
6813    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
6814        if Self::block_needs_scope(block) {
6815            self.exec_block(block)
6816        } else {
6817            self.exec_block_no_scope(block)
6818        }
6819    }
6820
6821    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
6822        let t0 = self.profiler.is_some().then(std::time::Instant::now);
6823        let r = self.exec_statement_inner(stmt);
6824        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
6825            prof.on_line(&self.file, stmt.line, t0.elapsed());
6826        }
6827        r
6828    }
6829
6830    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
6831        if let Err(e) = crate::perl_signal::poll(self) {
6832            return Err(FlowOrError::Error(e));
6833        }
6834        if let Err(e) = self.drain_pending_destroys(stmt.line) {
6835            return Err(FlowOrError::Error(e));
6836        }
6837        match &stmt.kind {
6838            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
6839            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
6840            StmtKind::If {
6841                condition,
6842                body,
6843                elsifs,
6844                else_block,
6845            } => {
6846                if self.eval_boolean_rvalue_condition(condition)? {
6847                    return self.exec_block(body);
6848                }
6849                for (c, b) in elsifs {
6850                    if self.eval_boolean_rvalue_condition(c)? {
6851                        return self.exec_block(b);
6852                    }
6853                }
6854                if let Some(eb) = else_block {
6855                    return self.exec_block(eb);
6856                }
6857                Ok(PerlValue::UNDEF)
6858            }
6859            StmtKind::Unless {
6860                condition,
6861                body,
6862                else_block,
6863            } => {
6864                if !self.eval_boolean_rvalue_condition(condition)? {
6865                    return self.exec_block(body);
6866                }
6867                if let Some(eb) = else_block {
6868                    return self.exec_block(eb);
6869                }
6870                Ok(PerlValue::UNDEF)
6871            }
6872            StmtKind::While {
6873                condition,
6874                body,
6875                label,
6876                continue_block,
6877            } => {
6878                'outer: loop {
6879                    if !self.eval_boolean_rvalue_condition(condition)? {
6880                        break;
6881                    }
6882                    'inner: loop {
6883                        match self.exec_block_smart(body) {
6884                            Ok(_) => break 'inner,
6885                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6886                                if l == label || l.is_none() =>
6887                            {
6888                                break 'outer;
6889                            }
6890                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6891                                if l == label || l.is_none() =>
6892                            {
6893                                if let Some(cb) = continue_block {
6894                                    let _ = self.exec_block_smart(cb);
6895                                }
6896                                continue 'outer;
6897                            }
6898                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6899                                if l == label || l.is_none() =>
6900                            {
6901                                continue 'inner;
6902                            }
6903                            Err(e) => return Err(e),
6904                        }
6905                    }
6906                    if let Some(cb) = continue_block {
6907                        let _ = self.exec_block_smart(cb);
6908                    }
6909                }
6910                Ok(PerlValue::UNDEF)
6911            }
6912            StmtKind::Until {
6913                condition,
6914                body,
6915                label,
6916                continue_block,
6917            } => {
6918                'outer: loop {
6919                    if self.eval_boolean_rvalue_condition(condition)? {
6920                        break;
6921                    }
6922                    'inner: loop {
6923                        match self.exec_block(body) {
6924                            Ok(_) => break 'inner,
6925                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6926                                if l == label || l.is_none() =>
6927                            {
6928                                break 'outer;
6929                            }
6930                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6931                                if l == label || l.is_none() =>
6932                            {
6933                                if let Some(cb) = continue_block {
6934                                    let _ = self.exec_block_smart(cb);
6935                                }
6936                                continue 'outer;
6937                            }
6938                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6939                                if l == label || l.is_none() =>
6940                            {
6941                                continue 'inner;
6942                            }
6943                            Err(e) => return Err(e),
6944                        }
6945                    }
6946                    if let Some(cb) = continue_block {
6947                        let _ = self.exec_block_smart(cb);
6948                    }
6949                }
6950                Ok(PerlValue::UNDEF)
6951            }
6952            StmtKind::DoWhile { body, condition } => {
6953                loop {
6954                    self.exec_block(body)?;
6955                    if !self.eval_boolean_rvalue_condition(condition)? {
6956                        break;
6957                    }
6958                }
6959                Ok(PerlValue::UNDEF)
6960            }
6961            StmtKind::For {
6962                init,
6963                condition,
6964                step,
6965                body,
6966                label,
6967                continue_block,
6968            } => {
6969                self.scope_push_hook();
6970                if let Some(init) = init {
6971                    self.exec_statement(init)?;
6972                }
6973                'outer: loop {
6974                    if let Some(cond) = condition {
6975                        if !self.eval_boolean_rvalue_condition(cond)? {
6976                            break;
6977                        }
6978                    }
6979                    'inner: loop {
6980                        match self.exec_block_smart(body) {
6981                            Ok(_) => break 'inner,
6982                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6983                                if l == label || l.is_none() =>
6984                            {
6985                                break 'outer;
6986                            }
6987                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6988                                if l == label || l.is_none() =>
6989                            {
6990                                if let Some(cb) = continue_block {
6991                                    let _ = self.exec_block_smart(cb);
6992                                }
6993                                if let Some(step) = step {
6994                                    self.eval_expr(step)?;
6995                                }
6996                                continue 'outer;
6997                            }
6998                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6999                                if l == label || l.is_none() =>
7000                            {
7001                                continue 'inner;
7002                            }
7003                            Err(e) => {
7004                                self.scope_pop_hook();
7005                                return Err(e);
7006                            }
7007                        }
7008                    }
7009                    if let Some(cb) = continue_block {
7010                        let _ = self.exec_block_smart(cb);
7011                    }
7012                    if let Some(step) = step {
7013                        self.eval_expr(step)?;
7014                    }
7015                }
7016                self.scope_pop_hook();
7017                Ok(PerlValue::UNDEF)
7018            }
7019            StmtKind::Foreach {
7020                var,
7021                list,
7022                body,
7023                label,
7024                continue_block,
7025            } => {
7026                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
7027                let items = list_val.to_list();
7028                self.scope_push_hook();
7029                self.scope.declare_scalar(var, PerlValue::UNDEF);
7030                self.english_note_lexical_scalar(var);
7031                let mut i = 0usize;
7032                'outer: while i < items.len() {
7033                    self.scope
7034                        .set_scalar(var, items[i].clone())
7035                        .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
7036                    'inner: loop {
7037                        match self.exec_block_smart(body) {
7038                            Ok(_) => break 'inner,
7039                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7040                                if l == label || l.is_none() =>
7041                            {
7042                                break 'outer;
7043                            }
7044                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7045                                if l == label || l.is_none() =>
7046                            {
7047                                if let Some(cb) = continue_block {
7048                                    let _ = self.exec_block_smart(cb);
7049                                }
7050                                i += 1;
7051                                continue 'outer;
7052                            }
7053                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7054                                if l == label || l.is_none() =>
7055                            {
7056                                continue 'inner;
7057                            }
7058                            Err(e) => {
7059                                self.scope_pop_hook();
7060                                return Err(e);
7061                            }
7062                        }
7063                    }
7064                    if let Some(cb) = continue_block {
7065                        let _ = self.exec_block_smart(cb);
7066                    }
7067                    i += 1;
7068                }
7069                self.scope_pop_hook();
7070                Ok(PerlValue::UNDEF)
7071            }
7072            StmtKind::SubDecl {
7073                name,
7074                params,
7075                body,
7076                prototype,
7077            } => {
7078                let key = self.qualify_sub_key(name);
7079                let captured = self.scope.capture();
7080                let closure_env = if captured.is_empty() {
7081                    None
7082                } else {
7083                    Some(captured)
7084                };
7085                let mut sub = PerlSub {
7086                    name: name.clone(),
7087                    params: params.clone(),
7088                    body: body.clone(),
7089                    closure_env,
7090                    prototype: prototype.clone(),
7091                    fib_like: None,
7092                };
7093                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
7094                self.subs.insert(key, Arc::new(sub));
7095                Ok(PerlValue::UNDEF)
7096            }
7097            StmtKind::StructDecl { def } => {
7098                if self.struct_defs.contains_key(&def.name) {
7099                    return Err(PerlError::runtime(
7100                        format!("duplicate struct `{}`", def.name),
7101                        stmt.line,
7102                    )
7103                    .into());
7104                }
7105                self.struct_defs
7106                    .insert(def.name.clone(), Arc::new(def.clone()));
7107                Ok(PerlValue::UNDEF)
7108            }
7109            StmtKind::EnumDecl { def } => {
7110                if self.enum_defs.contains_key(&def.name) {
7111                    return Err(PerlError::runtime(
7112                        format!("duplicate enum `{}`", def.name),
7113                        stmt.line,
7114                    )
7115                    .into());
7116                }
7117                self.enum_defs
7118                    .insert(def.name.clone(), Arc::new(def.clone()));
7119                Ok(PerlValue::UNDEF)
7120            }
7121            StmtKind::ClassDecl { def } => {
7122                if self.class_defs.contains_key(&def.name) {
7123                    return Err(PerlError::runtime(
7124                        format!("duplicate class `{}`", def.name),
7125                        stmt.line,
7126                    )
7127                    .into());
7128                }
7129                // Final class enforcement: prevent subclassing
7130                for parent_name in &def.extends {
7131                    if let Some(parent_def) = self.class_defs.get(parent_name) {
7132                        if parent_def.is_final {
7133                            return Err(PerlError::runtime(
7134                                format!("cannot extend final class `{}`", parent_name),
7135                                stmt.line,
7136                            )
7137                            .into());
7138                        }
7139                        // Final method enforcement: prevent overriding
7140                        for m in &def.methods {
7141                            if let Some(parent_method) = parent_def.method(&m.name) {
7142                                if parent_method.is_final {
7143                                    return Err(PerlError::runtime(
7144                                        format!(
7145                                            "cannot override final method `{}` from class `{}`",
7146                                            m.name, parent_name
7147                                        ),
7148                                        stmt.line,
7149                                    )
7150                                    .into());
7151                                }
7152                            }
7153                        }
7154                    }
7155                }
7156                // Trait contract enforcement + default method inheritance
7157                let mut def = def.clone();
7158                for trait_name in &def.implements.clone() {
7159                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
7160                        for required in trait_def.required_methods() {
7161                            let has_method = def.methods.iter().any(|m| m.name == required.name);
7162                            if !has_method {
7163                                return Err(PerlError::runtime(
7164                                    format!(
7165                                        "class `{}` implements trait `{}` but does not define required method `{}`",
7166                                        def.name, trait_name, required.name
7167                                    ),
7168                                    stmt.line,
7169                                )
7170                                .into());
7171                            }
7172                        }
7173                        // Inherit default methods from trait (methods with bodies)
7174                        for tm in &trait_def.methods {
7175                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
7176                                def.methods.push(tm.clone());
7177                            }
7178                        }
7179                    }
7180                }
7181                // Abstract method enforcement: concrete subclasses must implement
7182                // all abstract methods (body-less methods) from abstract parents
7183                if !def.is_abstract {
7184                    for parent_name in &def.extends.clone() {
7185                        if let Some(parent_def) = self.class_defs.get(parent_name) {
7186                            if parent_def.is_abstract {
7187                                for m in &parent_def.methods {
7188                                    if m.body.is_none()
7189                                        && !def.methods.iter().any(|dm| dm.name == m.name)
7190                                    {
7191                                        return Err(PerlError::runtime(
7192                                            format!(
7193                                                "class `{}` must implement abstract method `{}` from `{}`",
7194                                                def.name, m.name, parent_name
7195                                            ),
7196                                            stmt.line,
7197                                        )
7198                                        .into());
7199                                    }
7200                                }
7201                            }
7202                        }
7203                    }
7204                }
7205                // Initialize static fields
7206                for sf in &def.static_fields {
7207                    let val = if let Some(ref expr) = sf.default {
7208                        self.eval_expr(expr)?
7209                    } else {
7210                        PerlValue::UNDEF
7211                    };
7212                    let key = format!("{}::{}", def.name, sf.name);
7213                    self.scope.declare_scalar(&key, val);
7214                }
7215                // Register class methods into self.subs so method dispatch finds them.
7216                for m in &def.methods {
7217                    if let Some(ref body) = m.body {
7218                        let fq = format!("{}::{}", def.name, m.name);
7219                        let sub = Arc::new(PerlSub {
7220                            name: fq.clone(),
7221                            params: m.params.clone(),
7222                            body: body.clone(),
7223                            closure_env: None,
7224                            prototype: None,
7225                            fib_like: None,
7226                        });
7227                        self.subs.insert(fq, sub);
7228                    }
7229                }
7230                // Set @ClassName::ISA so MRO/isa resolution works.
7231                if !def.extends.is_empty() {
7232                    let isa_key = format!("{}::ISA", def.name);
7233                    let parents: Vec<PerlValue> = def
7234                        .extends
7235                        .iter()
7236                        .map(|p| PerlValue::string(p.clone()))
7237                        .collect();
7238                    self.scope.declare_array(&isa_key, parents);
7239                }
7240                self.class_defs.insert(def.name.clone(), Arc::new(def));
7241                Ok(PerlValue::UNDEF)
7242            }
7243            StmtKind::TraitDecl { def } => {
7244                if self.trait_defs.contains_key(&def.name) {
7245                    return Err(PerlError::runtime(
7246                        format!("duplicate trait `{}`", def.name),
7247                        stmt.line,
7248                    )
7249                    .into());
7250                }
7251                self.trait_defs
7252                    .insert(def.name.clone(), Arc::new(def.clone()));
7253                Ok(PerlValue::UNDEF)
7254            }
7255            StmtKind::My(decls) | StmtKind::Our(decls) => {
7256                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
7257                // For list assignment my ($a, $b) = (10, 20), distribute elements.
7258                // All decls share the same initializer in the AST (parser clones it).
7259                if decls.len() > 1 && decls[0].initializer.is_some() {
7260                    let val = self.eval_expr_ctx(
7261                        decls[0].initializer.as_ref().unwrap(),
7262                        WantarrayCtx::List,
7263                    )?;
7264                    let items = val.to_list();
7265                    let mut idx = 0;
7266                    for decl in decls {
7267                        match decl.sigil {
7268                            Sigil::Scalar => {
7269                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7270                                let skey = if is_our {
7271                                    self.stash_scalar_name_for_package(&decl.name)
7272                                } else {
7273                                    decl.name.clone()
7274                                };
7275                                self.scope.declare_scalar_frozen(
7276                                    &skey,
7277                                    v,
7278                                    decl.frozen,
7279                                    decl.type_annotation.clone(),
7280                                )?;
7281                                self.english_note_lexical_scalar(&decl.name);
7282                                if is_our {
7283                                    self.note_our_scalar(&decl.name);
7284                                }
7285                                idx += 1;
7286                            }
7287                            Sigil::Array => {
7288                                // Array slurps remaining elements
7289                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7290                                idx = items.len();
7291                                if is_our {
7292                                    self.record_exporter_our_array_name(&decl.name, &rest);
7293                                }
7294                                let aname = self.stash_array_name_for_package(&decl.name);
7295                                self.scope.declare_array(&aname, rest);
7296                            }
7297                            Sigil::Hash => {
7298                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7299                                idx = items.len();
7300                                let mut map = IndexMap::new();
7301                                let mut i = 0;
7302                                while i + 1 < rest.len() {
7303                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7304                                    i += 2;
7305                                }
7306                                self.scope.declare_hash(&decl.name, map);
7307                            }
7308                            Sigil::Typeglob => {
7309                                return Err(PerlError::runtime(
7310                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
7311                                    stmt.line,
7312                                )
7313                                .into());
7314                            }
7315                        }
7316                    }
7317                } else {
7318                    // Single decl or no initializer
7319                    for decl in decls {
7320                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
7321                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
7322                        // compound op reads the lhs (see system Exporter.pm).
7323                        let compound_init = decl
7324                            .initializer
7325                            .as_ref()
7326                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
7327
7328                        if compound_init {
7329                            match decl.sigil {
7330                                Sigil::Typeglob => {
7331                                    return Err(PerlError::runtime(
7332                                        "compound assignment on typeglob declaration is not supported",
7333                                        stmt.line,
7334                                    )
7335                                    .into());
7336                                }
7337                                Sigil::Scalar => {
7338                                    let skey = if is_our {
7339                                        self.stash_scalar_name_for_package(&decl.name)
7340                                    } else {
7341                                        decl.name.clone()
7342                                    };
7343                                    self.scope.declare_scalar_frozen(
7344                                        &skey,
7345                                        PerlValue::UNDEF,
7346                                        decl.frozen,
7347                                        decl.type_annotation.clone(),
7348                                    )?;
7349                                    self.english_note_lexical_scalar(&decl.name);
7350                                    if is_our {
7351                                        self.note_our_scalar(&decl.name);
7352                                    }
7353                                    let init = decl.initializer.as_ref().unwrap();
7354                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7355                                }
7356                                Sigil::Array => {
7357                                    let aname = self.stash_array_name_for_package(&decl.name);
7358                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
7359                                    let init = decl.initializer.as_ref().unwrap();
7360                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7361                                    if is_our {
7362                                        let items = self.scope.get_array(&aname);
7363                                        self.record_exporter_our_array_name(&decl.name, &items);
7364                                    }
7365                                }
7366                                Sigil::Hash => {
7367                                    self.scope.declare_hash_frozen(
7368                                        &decl.name,
7369                                        IndexMap::new(),
7370                                        decl.frozen,
7371                                    );
7372                                    let init = decl.initializer.as_ref().unwrap();
7373                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7374                                }
7375                            }
7376                            continue;
7377                        }
7378
7379                        let val = if let Some(init) = &decl.initializer {
7380                            let ctx = match decl.sigil {
7381                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7382                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7383                            };
7384                            self.eval_expr_ctx(init, ctx)?
7385                        } else {
7386                            PerlValue::UNDEF
7387                        };
7388                        match decl.sigil {
7389                            Sigil::Typeglob => {
7390                                return Err(PerlError::runtime(
7391                                    "`my *FH` / typeglob declaration is not supported",
7392                                    stmt.line,
7393                                )
7394                                .into());
7395                            }
7396                            Sigil::Scalar => {
7397                                let skey = if is_our {
7398                                    self.stash_scalar_name_for_package(&decl.name)
7399                                } else {
7400                                    decl.name.clone()
7401                                };
7402                                self.scope.declare_scalar_frozen(
7403                                    &skey,
7404                                    val,
7405                                    decl.frozen,
7406                                    decl.type_annotation.clone(),
7407                                )?;
7408                                self.english_note_lexical_scalar(&decl.name);
7409                                if is_our {
7410                                    self.note_our_scalar(&decl.name);
7411                                }
7412                            }
7413                            Sigil::Array => {
7414                                let items = val.to_list();
7415                                if is_our {
7416                                    self.record_exporter_our_array_name(&decl.name, &items);
7417                                }
7418                                let aname = self.stash_array_name_for_package(&decl.name);
7419                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
7420                            }
7421                            Sigil::Hash => {
7422                                let items = val.to_list();
7423                                let mut map = IndexMap::new();
7424                                let mut i = 0;
7425                                while i + 1 < items.len() {
7426                                    let k = items[i].to_string();
7427                                    let v = items[i + 1].clone();
7428                                    map.insert(k, v);
7429                                    i += 2;
7430                                }
7431                                self.scope.declare_hash_frozen(&decl.name, map, decl.frozen);
7432                            }
7433                        }
7434                    }
7435                }
7436                Ok(PerlValue::UNDEF)
7437            }
7438            StmtKind::State(decls) => {
7439                // `state` variables persist across subroutine calls.
7440                // Key by source line + name for uniqueness.
7441                for decl in decls {
7442                    let state_key = format!("{}:{}", stmt.line, decl.name);
7443                    match decl.sigil {
7444                        Sigil::Scalar => {
7445                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
7446                                // Already initialized — declare with persisted value
7447                                self.scope.declare_scalar(&decl.name, prev);
7448                            } else {
7449                                // First encounter — evaluate initializer
7450                                let val = if let Some(init) = &decl.initializer {
7451                                    self.eval_expr(init)?
7452                                } else {
7453                                    PerlValue::UNDEF
7454                                };
7455                                self.state_vars.insert(state_key.clone(), val.clone());
7456                                self.scope.declare_scalar(&decl.name, val);
7457                            }
7458                            // Register for save-back when scope pops
7459                            if let Some(frame) = self.state_bindings_stack.last_mut() {
7460                                frame.push((decl.name.clone(), state_key));
7461                            }
7462                        }
7463                        _ => {
7464                            // For arrays/hashes, fall back to simple my-like behavior
7465                            let val = if let Some(init) = &decl.initializer {
7466                                self.eval_expr(init)?
7467                            } else {
7468                                PerlValue::UNDEF
7469                            };
7470                            match decl.sigil {
7471                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
7472                                Sigil::Hash => {
7473                                    let items = val.to_list();
7474                                    let mut map = IndexMap::new();
7475                                    let mut i = 0;
7476                                    while i + 1 < items.len() {
7477                                        map.insert(items[i].to_string(), items[i + 1].clone());
7478                                        i += 2;
7479                                    }
7480                                    self.scope.declare_hash(&decl.name, map);
7481                                }
7482                                _ => {}
7483                            }
7484                        }
7485                    }
7486                }
7487                Ok(PerlValue::UNDEF)
7488            }
7489            StmtKind::Local(decls) => {
7490                if decls.len() > 1 && decls[0].initializer.is_some() {
7491                    let val = self.eval_expr_ctx(
7492                        decls[0].initializer.as_ref().unwrap(),
7493                        WantarrayCtx::List,
7494                    )?;
7495                    let items = val.to_list();
7496                    let mut idx = 0;
7497                    for decl in decls {
7498                        match decl.sigil {
7499                            Sigil::Scalar => {
7500                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7501                                idx += 1;
7502                                self.scope.local_set_scalar(&decl.name, v)?;
7503                            }
7504                            Sigil::Array => {
7505                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7506                                idx = items.len();
7507                                self.scope.local_set_array(&decl.name, rest)?;
7508                            }
7509                            Sigil::Hash => {
7510                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7511                                idx = items.len();
7512                                if decl.name == "ENV" {
7513                                    self.materialize_env_if_needed();
7514                                }
7515                                let mut map = IndexMap::new();
7516                                let mut i = 0;
7517                                while i + 1 < rest.len() {
7518                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7519                                    i += 2;
7520                                }
7521                                self.scope.local_set_hash(&decl.name, map)?;
7522                            }
7523                            Sigil::Typeglob => {
7524                                return Err(PerlError::runtime(
7525                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
7526                                    stmt.line,
7527                                )
7528                                .into());
7529                            }
7530                        }
7531                    }
7532                    Ok(val)
7533                } else {
7534                    let mut last_val = PerlValue::UNDEF;
7535                    for decl in decls {
7536                        let val = if let Some(init) = &decl.initializer {
7537                            let ctx = match decl.sigil {
7538                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7539                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7540                            };
7541                            self.eval_expr_ctx(init, ctx)?
7542                        } else {
7543                            PerlValue::UNDEF
7544                        };
7545                        last_val = val.clone();
7546                        match decl.sigil {
7547                            Sigil::Typeglob => {
7548                                let old = self.glob_handle_alias.remove(&decl.name);
7549                                if let Some(frame) = self.glob_restore_frames.last_mut() {
7550                                    frame.push((decl.name.clone(), old));
7551                                }
7552                                if let Some(init) = &decl.initializer {
7553                                    if let ExprKind::Typeglob(rhs) = &init.kind {
7554                                        self.glob_handle_alias
7555                                            .insert(decl.name.clone(), rhs.clone());
7556                                    } else {
7557                                        return Err(PerlError::runtime(
7558                                            "local *GLOB = *OTHER — right side must be a typeglob",
7559                                            stmt.line,
7560                                        )
7561                                        .into());
7562                                    }
7563                                }
7564                            }
7565                            Sigil::Scalar => {
7566                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
7567                                // must update the interpreter's backing field too — these are
7568                                // not stored in `Scope`. Save the prior value for restoration
7569                                // on `scope_pop_hook` so the block-exit restore is visible to
7570                                // print/I/O code.
7571                                if Self::is_special_scalar_name_for_set(&decl.name) {
7572                                    let old = self.get_special_var(&decl.name);
7573                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
7574                                    {
7575                                        frame.push((decl.name.clone(), old));
7576                                    }
7577                                    self.set_special_var(&decl.name, &val)
7578                                        .map_err(|e| e.at_line(stmt.line))?;
7579                                }
7580                                self.scope.local_set_scalar(&decl.name, val)?;
7581                            }
7582                            Sigil::Array => {
7583                                self.scope.local_set_array(&decl.name, val.to_list())?;
7584                            }
7585                            Sigil::Hash => {
7586                                if decl.name == "ENV" {
7587                                    self.materialize_env_if_needed();
7588                                }
7589                                let items = val.to_list();
7590                                let mut map = IndexMap::new();
7591                                let mut i = 0;
7592                                while i + 1 < items.len() {
7593                                    let k = items[i].to_string();
7594                                    let v = items[i + 1].clone();
7595                                    map.insert(k, v);
7596                                    i += 2;
7597                                }
7598                                self.scope.local_set_hash(&decl.name, map)?;
7599                            }
7600                        }
7601                    }
7602                    Ok(last_val)
7603                }
7604            }
7605            StmtKind::LocalExpr {
7606                target,
7607                initializer,
7608            } => {
7609                let rhs_name = |init: &Expr| -> PerlResult<Option<String>> {
7610                    match &init.kind {
7611                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
7612                        _ => Err(PerlError::runtime(
7613                            "local *GLOB = *OTHER — right side must be a typeglob",
7614                            stmt.line,
7615                        )),
7616                    }
7617                };
7618                match &target.kind {
7619                    ExprKind::Typeglob(name) => {
7620                        let rhs = if let Some(init) = initializer {
7621                            rhs_name(init)?
7622                        } else {
7623                            None
7624                        };
7625                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
7626                        return Ok(PerlValue::UNDEF);
7627                    }
7628                    ExprKind::Deref {
7629                        expr,
7630                        kind: Sigil::Typeglob,
7631                    } => {
7632                        let lhs = self.eval_expr(expr)?.to_string();
7633                        let rhs = if let Some(init) = initializer {
7634                            rhs_name(init)?
7635                        } else {
7636                            None
7637                        };
7638                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7639                        return Ok(PerlValue::UNDEF);
7640                    }
7641                    ExprKind::TypeglobExpr(e) => {
7642                        let lhs = self.eval_expr(e)?.to_string();
7643                        let rhs = if let Some(init) = initializer {
7644                            rhs_name(init)?
7645                        } else {
7646                            None
7647                        };
7648                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7649                        return Ok(PerlValue::UNDEF);
7650                    }
7651                    _ => {}
7652                }
7653                let val = if let Some(init) = initializer {
7654                    let ctx = match &target.kind {
7655                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
7656                        _ => WantarrayCtx::Scalar,
7657                    };
7658                    self.eval_expr_ctx(init, ctx)?
7659                } else {
7660                    PerlValue::UNDEF
7661                };
7662                match &target.kind {
7663                    ExprKind::ScalarVar(name) => {
7664                        // `local $X = …` on a special var — see twin block in
7665                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
7666                        if Self::is_special_scalar_name_for_set(name) {
7667                            let old = self.get_special_var(name);
7668                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
7669                                frame.push((name.clone(), old));
7670                            }
7671                            self.set_special_var(name, &val)
7672                                .map_err(|e| e.at_line(stmt.line))?;
7673                        }
7674                        self.scope.local_set_scalar(name, val.clone())?;
7675                    }
7676                    ExprKind::ArrayVar(name) => {
7677                        self.scope.local_set_array(name, val.to_list())?;
7678                    }
7679                    ExprKind::HashVar(name) => {
7680                        if name == "ENV" {
7681                            self.materialize_env_if_needed();
7682                        }
7683                        let items = val.to_list();
7684                        let mut map = IndexMap::new();
7685                        let mut i = 0;
7686                        while i + 1 < items.len() {
7687                            map.insert(items[i].to_string(), items[i + 1].clone());
7688                            i += 2;
7689                        }
7690                        self.scope.local_set_hash(name, map)?;
7691                    }
7692                    ExprKind::HashElement { hash, key } => {
7693                        let ks = self.eval_expr(key)?.to_string();
7694                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
7695                    }
7696                    ExprKind::ArrayElement { array, index } => {
7697                        self.check_strict_array_var(array, stmt.line)?;
7698                        let aname = self.stash_array_name_for_package(array);
7699                        let idx = self.eval_expr(index)?.to_int();
7700                        self.scope
7701                            .local_set_array_element(&aname, idx, val.clone())?;
7702                    }
7703                    _ => {
7704                        return Err(PerlError::runtime(
7705                            format!(
7706                                "local on this lvalue is not supported yet ({:?})",
7707                                target.kind
7708                            ),
7709                            stmt.line,
7710                        )
7711                        .into());
7712                    }
7713                }
7714                Ok(val)
7715            }
7716            StmtKind::MySync(decls) => {
7717                for decl in decls {
7718                    let val = if let Some(init) = &decl.initializer {
7719                        self.eval_expr(init)?
7720                    } else {
7721                        PerlValue::UNDEF
7722                    };
7723                    match decl.sigil {
7724                        Sigil::Typeglob => {
7725                            return Err(PerlError::runtime(
7726                                "`mysync` does not support typeglob variables",
7727                                stmt.line,
7728                            )
7729                            .into());
7730                        }
7731                        Sigil::Scalar => {
7732                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
7733                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
7734                            let stored = if val.is_mysync_deque_or_heap() {
7735                                val
7736                            } else {
7737                                PerlValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(val)))
7738                            };
7739                            self.scope.declare_scalar(&decl.name, stored);
7740                        }
7741                        Sigil::Array => {
7742                            self.scope.declare_atomic_array(&decl.name, val.to_list());
7743                        }
7744                        Sigil::Hash => {
7745                            let items = val.to_list();
7746                            let mut map = IndexMap::new();
7747                            let mut i = 0;
7748                            while i + 1 < items.len() {
7749                                map.insert(items[i].to_string(), items[i + 1].clone());
7750                                i += 2;
7751                            }
7752                            self.scope.declare_atomic_hash(&decl.name, map);
7753                        }
7754                    }
7755                }
7756                Ok(PerlValue::UNDEF)
7757            }
7758            StmtKind::Package { name } => {
7759                // Minimal package support — just set a variable
7760                let _ = self
7761                    .scope
7762                    .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
7763                Ok(PerlValue::UNDEF)
7764            }
7765            StmtKind::UsePerlVersion { .. } => Ok(PerlValue::UNDEF),
7766            StmtKind::Use { .. } => {
7767                // Handled in `prepare_program_top_level` before BEGIN / main.
7768                Ok(PerlValue::UNDEF)
7769            }
7770            StmtKind::UseOverload { pairs } => {
7771                self.install_use_overload_pairs(pairs);
7772                Ok(PerlValue::UNDEF)
7773            }
7774            StmtKind::No { .. } => {
7775                // Handled in `prepare_program_top_level` (same phase as `use`).
7776                Ok(PerlValue::UNDEF)
7777            }
7778            StmtKind::Return(val) => {
7779                let v = if let Some(e) = val {
7780                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
7781                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
7782                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
7783                    self.eval_expr_ctx(e, self.wantarray_kind)?
7784                } else {
7785                    PerlValue::UNDEF
7786                };
7787                Err(Flow::Return(v).into())
7788            }
7789            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
7790            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
7791            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
7792            StmtKind::Block(block) => self.exec_block(block),
7793            StmtKind::Begin(_)
7794            | StmtKind::UnitCheck(_)
7795            | StmtKind::Check(_)
7796            | StmtKind::Init(_)
7797            | StmtKind::End(_) => Ok(PerlValue::UNDEF),
7798            StmtKind::Empty => Ok(PerlValue::UNDEF),
7799            StmtKind::Goto { target } => {
7800                // goto &sub — tail call
7801                if let ExprKind::SubroutineRef(name) = &target.kind {
7802                    return Err(Flow::GotoSub(name.clone()).into());
7803                }
7804                Err(PerlError::runtime("goto reached outside goto-aware block", stmt.line).into())
7805            }
7806            StmtKind::EvalTimeout { timeout, body } => {
7807                let secs = self.eval_expr(timeout)?.to_number();
7808                self.eval_timeout_block(body, secs, stmt.line)
7809            }
7810            StmtKind::Tie {
7811                target,
7812                class,
7813                args,
7814            } => {
7815                let kind = match &target {
7816                    TieTarget::Scalar(_) => 0u8,
7817                    TieTarget::Array(_) => 1u8,
7818                    TieTarget::Hash(_) => 2u8,
7819                };
7820                let name = match &target {
7821                    TieTarget::Scalar(s) => s.as_str(),
7822                    TieTarget::Array(a) => a.as_str(),
7823                    TieTarget::Hash(h) => h.as_str(),
7824                };
7825                let mut vals = vec![self.eval_expr(class)?];
7826                for a in args {
7827                    vals.push(self.eval_expr(a)?);
7828                }
7829                self.tie_execute(kind, name, vals, stmt.line)
7830                    .map_err(Into::into)
7831            }
7832            StmtKind::TryCatch {
7833                try_block,
7834                catch_var,
7835                catch_block,
7836                finally_block,
7837            } => match self.exec_block(try_block) {
7838                Ok(v) => {
7839                    if let Some(fb) = finally_block {
7840                        self.exec_block(fb)?;
7841                    }
7842                    Ok(v)
7843                }
7844                Err(FlowOrError::Error(e)) => {
7845                    if matches!(e.kind, ErrorKind::Exit(_)) {
7846                        return Err(FlowOrError::Error(e));
7847                    }
7848                    self.scope_push_hook();
7849                    self.scope
7850                        .declare_scalar(catch_var, PerlValue::string(e.to_string()));
7851                    self.english_note_lexical_scalar(catch_var);
7852                    let r = self.exec_block(catch_block);
7853                    self.scope_pop_hook();
7854                    if let Some(fb) = finally_block {
7855                        self.exec_block(fb)?;
7856                    }
7857                    r
7858                }
7859                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
7860            },
7861            StmtKind::Given { topic, body } => self.exec_given(topic, body),
7862            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(PerlError::runtime(
7863                "when/default may only appear inside a given block",
7864                stmt.line,
7865            )
7866            .into()),
7867            StmtKind::FormatDecl { .. } => {
7868                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
7869                Ok(PerlValue::UNDEF)
7870            }
7871            StmtKind::Continue(block) => self.exec_block_smart(block),
7872        }
7873    }
7874
7875    #[inline]
7876    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
7877        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
7878    }
7879
7880    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
7881    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
7882    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
7883    pub(crate) fn scalar_compound_assign_scalar_target(
7884        &mut self,
7885        name: &str,
7886        op: BinOp,
7887        rhs: PerlValue,
7888    ) -> Result<PerlValue, PerlError> {
7889        if op == BinOp::Concat {
7890            return self.scope.scalar_concat_inplace(name, &rhs);
7891        }
7892        Ok(self
7893            .scope
7894            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs)))
7895    }
7896
7897    fn compound_scalar_binop(old: &PerlValue, op: BinOp, rhs: &PerlValue) -> PerlValue {
7898        match op {
7899            BinOp::Add => {
7900                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7901                    PerlValue::integer(a.wrapping_add(b))
7902                } else {
7903                    PerlValue::float(old.to_number() + rhs.to_number())
7904                }
7905            }
7906            BinOp::Sub => {
7907                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7908                    PerlValue::integer(a.wrapping_sub(b))
7909                } else {
7910                    PerlValue::float(old.to_number() - rhs.to_number())
7911                }
7912            }
7913            BinOp::Mul => {
7914                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7915                    PerlValue::integer(a.wrapping_mul(b))
7916                } else {
7917                    PerlValue::float(old.to_number() * rhs.to_number())
7918                }
7919            }
7920            BinOp::BitAnd => {
7921                if let Some(s) = crate::value::set_intersection(old, rhs) {
7922                    s
7923                } else {
7924                    PerlValue::integer(old.to_int() & rhs.to_int())
7925                }
7926            }
7927            BinOp::BitOr => {
7928                if let Some(s) = crate::value::set_union(old, rhs) {
7929                    s
7930                } else {
7931                    PerlValue::integer(old.to_int() | rhs.to_int())
7932                }
7933            }
7934            BinOp::BitXor => PerlValue::integer(old.to_int() ^ rhs.to_int()),
7935            BinOp::ShiftLeft => PerlValue::integer(old.to_int() << rhs.to_int()),
7936            BinOp::ShiftRight => PerlValue::integer(old.to_int() >> rhs.to_int()),
7937            BinOp::Div => PerlValue::float(old.to_number() / rhs.to_number()),
7938            BinOp::Mod => PerlValue::float(old.to_number() % rhs.to_number()),
7939            BinOp::Pow => PerlValue::float(old.to_number().powf(rhs.to_number())),
7940            BinOp::LogOr => {
7941                if old.is_true() {
7942                    old.clone()
7943                } else {
7944                    rhs.clone()
7945                }
7946            }
7947            BinOp::DefinedOr => {
7948                if !old.is_undef() {
7949                    old.clone()
7950                } else {
7951                    rhs.clone()
7952                }
7953            }
7954            BinOp::LogAnd => {
7955                if old.is_true() {
7956                    rhs.clone()
7957                } else {
7958                    old.clone()
7959                }
7960            }
7961            _ => PerlValue::float(old.to_number() + rhs.to_number()),
7962        }
7963    }
7964
7965    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
7966    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
7967    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
7968    fn eval_hash_slice_key_components(
7969        &mut self,
7970        key_expr: &Expr,
7971    ) -> Result<Vec<String>, FlowOrError> {
7972        let v = if matches!(
7973            key_expr.kind,
7974            ExprKind::Range { .. } | ExprKind::SliceRange { .. }
7975        ) {
7976            self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
7977        } else {
7978            self.eval_expr(key_expr)?
7979        };
7980        if let Some(vv) = v.as_array_vec() {
7981            Ok(vv.iter().map(|x| x.to_string()).collect())
7982        } else {
7983            Ok(vec![v.to_string()])
7984        }
7985    }
7986
7987    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
7988    pub(crate) fn symbolic_deref(
7989        &mut self,
7990        val: PerlValue,
7991        kind: Sigil,
7992        line: usize,
7993    ) -> ExecResult {
7994        match kind {
7995            Sigil::Scalar => {
7996                if let Some(name) = val.as_scalar_binding_name() {
7997                    return Ok(self.get_special_var(&name));
7998                }
7999                if let Some(r) = val.as_scalar_ref() {
8000                    return Ok(r.read().clone());
8001                }
8002                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
8003                if let Some(r) = val.as_array_ref() {
8004                    return Ok(PerlValue::array(r.read().clone()));
8005                }
8006                if let Some(name) = val.as_array_binding_name() {
8007                    return Ok(PerlValue::array(self.scope.get_array(&name)));
8008                }
8009                if let Some(r) = val.as_hash_ref() {
8010                    return Ok(PerlValue::hash(r.read().clone()));
8011                }
8012                if let Some(name) = val.as_hash_binding_name() {
8013                    self.touch_env_hash(&name);
8014                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
8015                }
8016                if let Some(s) = val.as_str() {
8017                    if self.strict_refs {
8018                        return Err(PerlError::runtime(
8019                            format!(
8020                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
8021                                s
8022                            ),
8023                            line,
8024                        )
8025                        .into());
8026                    }
8027                    return Ok(self.get_special_var(&s));
8028                }
8029                Err(PerlError::runtime("Can't dereference non-reference as scalar", line).into())
8030            }
8031            Sigil::Array => {
8032                if let Some(r) = val.as_array_ref() {
8033                    return Ok(PerlValue::array(r.read().clone()));
8034                }
8035                if let Some(name) = val.as_array_binding_name() {
8036                    return Ok(PerlValue::array(self.scope.get_array(&name)));
8037                }
8038                if let Some(s) = val.as_str() {
8039                    if self.strict_refs {
8040                        return Err(PerlError::runtime(
8041                            format!(
8042                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8043                                s
8044                            ),
8045                            line,
8046                        )
8047                        .into());
8048                    }
8049                    return Ok(PerlValue::array(self.scope.get_array(&s)));
8050                }
8051                Err(PerlError::runtime("Can't dereference non-reference as array", line).into())
8052            }
8053            Sigil::Hash => {
8054                if let Some(r) = val.as_hash_ref() {
8055                    return Ok(PerlValue::hash(r.read().clone()));
8056                }
8057                if let Some(name) = val.as_hash_binding_name() {
8058                    self.touch_env_hash(&name);
8059                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
8060                }
8061                if let Some(s) = val.as_str() {
8062                    if self.strict_refs {
8063                        return Err(PerlError::runtime(
8064                            format!(
8065                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8066                                s
8067                            ),
8068                            line,
8069                        )
8070                        .into());
8071                    }
8072                    self.touch_env_hash(&s);
8073                    return Ok(PerlValue::hash(self.scope.get_hash(&s)));
8074                }
8075                Err(PerlError::runtime("Can't dereference non-reference as hash", line).into())
8076            }
8077            Sigil::Typeglob => {
8078                if let Some(s) = val.as_str() {
8079                    return Ok(PerlValue::string(self.resolve_io_handle_name(&s)));
8080                }
8081                Err(PerlError::runtime("Can't dereference non-reference as typeglob", line).into())
8082            }
8083        }
8084    }
8085
8086    /// `qq` list join expects a plain array; if a bare [`PerlValue::array_ref`] reaches join, peel
8087    /// one level so elements stringify like Perl (`"@$r"`).
8088    #[inline]
8089    pub(crate) fn peel_array_ref_for_list_join(&self, v: PerlValue) -> PerlValue {
8090        if let Some(r) = v.as_array_ref() {
8091            return PerlValue::array(r.read().clone());
8092        }
8093        v
8094    }
8095
8096    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
8097    pub(crate) fn make_array_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
8098        if let Some(a) = val.as_array_ref() {
8099            return Ok(PerlValue::array_ref(Arc::clone(&a)));
8100        }
8101        if let Some(name) = val.as_array_binding_name() {
8102            return Ok(PerlValue::array_binding_ref(name));
8103        }
8104        if let Some(s) = val.as_str() {
8105            if self.strict_refs {
8106                return Err(PerlError::runtime(
8107                    format!(
8108                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8109                        s
8110                    ),
8111                    line,
8112                )
8113                .into());
8114            }
8115            return Ok(PerlValue::array_binding_ref(s.to_string()));
8116        }
8117        if let Some(r) = val.as_scalar_ref() {
8118            let inner = r.read().clone();
8119            return self.make_array_ref_alias(inner, line);
8120        }
8121        Err(PerlError::runtime("Can't make array reference from value", line).into())
8122    }
8123
8124    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
8125    pub(crate) fn make_hash_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
8126        if let Some(h) = val.as_hash_ref() {
8127            return Ok(PerlValue::hash_ref(Arc::clone(&h)));
8128        }
8129        if let Some(name) = val.as_hash_binding_name() {
8130            return Ok(PerlValue::hash_binding_ref(name));
8131        }
8132        if let Some(s) = val.as_str() {
8133            if self.strict_refs {
8134                return Err(PerlError::runtime(
8135                    format!(
8136                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8137                        s
8138                    ),
8139                    line,
8140                )
8141                .into());
8142            }
8143            return Ok(PerlValue::hash_binding_ref(s.to_string()));
8144        }
8145        if let Some(r) = val.as_scalar_ref() {
8146            let inner = r.read().clone();
8147            return self.make_hash_ref_alias(inner, line);
8148        }
8149        Err(PerlError::runtime("Can't make hash reference from value", line).into())
8150    }
8151
8152    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
8153    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
8154    pub(crate) fn process_case_escapes(s: &str) -> String {
8155        // Quick check: if no backslash, nothing to do
8156        if !s.contains('\\') {
8157            return s.to_string();
8158        }
8159        let mut result = String::with_capacity(s.len());
8160        let mut chars = s.chars().peekable();
8161        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
8162        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
8163
8164        while let Some(c) = chars.next() {
8165            if c == '\\' {
8166                match chars.peek() {
8167                    Some(&'U') => {
8168                        chars.next();
8169                        mode = Some('U');
8170                        continue;
8171                    }
8172                    Some(&'L') => {
8173                        chars.next();
8174                        mode = Some('L');
8175                        continue;
8176                    }
8177                    Some(&'Q') => {
8178                        chars.next();
8179                        mode = Some('Q');
8180                        continue;
8181                    }
8182                    Some(&'E') => {
8183                        chars.next();
8184                        mode = None;
8185                        next_char_mod = None;
8186                        continue;
8187                    }
8188                    Some(&'u') => {
8189                        chars.next();
8190                        next_char_mod = Some('u');
8191                        continue;
8192                    }
8193                    Some(&'l') => {
8194                        chars.next();
8195                        next_char_mod = Some('l');
8196                        continue;
8197                    }
8198                    _ => {}
8199                }
8200            }
8201
8202            let ch = c;
8203
8204            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
8205            if let Some(m) = next_char_mod.take() {
8206                let transformed = match m {
8207                    'u' => ch.to_uppercase().next().unwrap_or(ch),
8208                    'l' => ch.to_lowercase().next().unwrap_or(ch),
8209                    _ => ch,
8210                };
8211                result.push(transformed);
8212            } else {
8213                // Apply ongoing mode
8214                match mode {
8215                    Some('U') => {
8216                        for uc in ch.to_uppercase() {
8217                            result.push(uc);
8218                        }
8219                    }
8220                    Some('L') => {
8221                        for lc in ch.to_lowercase() {
8222                            result.push(lc);
8223                        }
8224                    }
8225                    Some('Q') => {
8226                        if !ch.is_ascii_alphanumeric() && ch != '_' {
8227                            result.push('\\');
8228                        }
8229                        result.push(ch);
8230                    }
8231                    None | Some(_) => {
8232                        result.push(ch);
8233                    }
8234                }
8235            }
8236        }
8237        result
8238    }
8239
8240    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
8241        let line = expr.line;
8242        match &expr.kind {
8243            ExprKind::Integer(n) => Ok(PerlValue::integer(*n)),
8244            ExprKind::Float(f) => Ok(PerlValue::float(*f)),
8245            ExprKind::String(s) => {
8246                let processed = Self::process_case_escapes(s);
8247                Ok(PerlValue::string(processed))
8248            }
8249            ExprKind::Bareword(s) => {
8250                if s == "__PACKAGE__" {
8251                    return Ok(PerlValue::string(self.current_package()));
8252                }
8253                if let Some(sub) = self.resolve_sub_by_name(s) {
8254                    return self.call_sub(&sub, vec![], ctx, line);
8255                }
8256                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
8257                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
8258                    return r.map_err(Into::into);
8259                }
8260                Ok(PerlValue::string(s.clone()))
8261            }
8262            ExprKind::Undef => Ok(PerlValue::UNDEF),
8263            ExprKind::MagicConst(MagicConstKind::File) => Ok(PerlValue::string(self.file.clone())),
8264            ExprKind::MagicConst(MagicConstKind::Line) => Ok(PerlValue::integer(expr.line as i64)),
8265            ExprKind::MagicConst(MagicConstKind::Sub) => {
8266                if let Some(sub) = self.current_sub_stack.last().cloned() {
8267                    Ok(PerlValue::code_ref(sub))
8268                } else {
8269                    Ok(PerlValue::UNDEF)
8270                }
8271            }
8272            ExprKind::Regex(pattern, flags) => {
8273                if ctx == WantarrayCtx::Void {
8274                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
8275                    let topic = self.scope.get_scalar("_");
8276                    let s = topic.to_string();
8277                    self.regex_match_execute(s, pattern, flags, false, "_", line)
8278                } else {
8279                    let re = self.compile_regex(pattern, flags, line)?;
8280                    Ok(PerlValue::regex(re, pattern.clone(), flags.clone()))
8281                }
8282            }
8283            ExprKind::QW(words) => Ok(PerlValue::array(
8284                words.iter().map(|w| PerlValue::string(w.clone())).collect(),
8285            )),
8286
8287            // Interpolated strings
8288            ExprKind::InterpolatedString(parts) => {
8289                let mut raw_result = String::new();
8290                for part in parts {
8291                    match part {
8292                        StringPart::Literal(s) => raw_result.push_str(s),
8293                        StringPart::ScalarVar(name) => {
8294                            self.check_strict_scalar_var(name, line)?;
8295                            let val = self.get_special_var(name);
8296                            let s = self.stringify_value(val, line)?;
8297                            raw_result.push_str(&s);
8298                        }
8299                        StringPart::ArrayVar(name) => {
8300                            self.check_strict_array_var(name, line)?;
8301                            let aname = self.stash_array_name_for_package(name);
8302                            let arr = self.scope.get_array(&aname);
8303                            let mut parts = Vec::with_capacity(arr.len());
8304                            for v in &arr {
8305                                parts.push(self.stringify_value(v.clone(), line)?);
8306                            }
8307                            let sep = self.list_separator.clone();
8308                            raw_result.push_str(&parts.join(&sep));
8309                        }
8310                        StringPart::Expr(e) => {
8311                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
8312                                self.check_strict_array_var(array, line)?;
8313                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8314                                let val = self.peel_array_ref_for_list_join(val);
8315                                let list = val.to_list();
8316                                let sep = self.list_separator.clone();
8317                                let mut parts = Vec::with_capacity(list.len());
8318                                for v in list {
8319                                    parts.push(self.stringify_value(v, line)?);
8320                                }
8321                                raw_result.push_str(&parts.join(&sep));
8322                            } else if let ExprKind::Deref {
8323                                kind: Sigil::Array, ..
8324                            } = &e.kind
8325                            {
8326                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8327                                let val = self.peel_array_ref_for_list_join(val);
8328                                let list = val.to_list();
8329                                let sep = self.list_separator.clone();
8330                                let mut parts = Vec::with_capacity(list.len());
8331                                for v in list {
8332                                    parts.push(self.stringify_value(v, line)?);
8333                                }
8334                                raw_result.push_str(&parts.join(&sep));
8335                            } else {
8336                                let val = self.eval_expr(e)?;
8337                                let s = self.stringify_value(val, line)?;
8338                                raw_result.push_str(&s);
8339                            }
8340                        }
8341                    }
8342                }
8343                let result = Self::process_case_escapes(&raw_result);
8344                Ok(PerlValue::string(result))
8345            }
8346
8347            // Variables
8348            ExprKind::ScalarVar(name) => {
8349                self.check_strict_scalar_var(name, line)?;
8350                let stor = self.tree_scalar_storage_name(name);
8351                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
8352                    let class = obj
8353                        .as_blessed_ref()
8354                        .map(|b| b.class.clone())
8355                        .unwrap_or_default();
8356                    let full = format!("{}::FETCH", class);
8357                    if let Some(sub) = self.subs.get(&full).cloned() {
8358                        return self.call_sub(&sub, vec![obj], ctx, line);
8359                    }
8360                }
8361                Ok(self.get_special_var(&stor))
8362            }
8363            ExprKind::ArrayVar(name) => {
8364                self.check_strict_array_var(name, line)?;
8365                let aname = self.stash_array_name_for_package(name);
8366                let arr = self.scope.get_array(&aname);
8367                if ctx == WantarrayCtx::List {
8368                    Ok(PerlValue::array(arr))
8369                } else {
8370                    Ok(PerlValue::integer(arr.len() as i64))
8371                }
8372            }
8373            ExprKind::HashVar(name) => {
8374                self.check_strict_hash_var(name, line)?;
8375                self.touch_env_hash(name);
8376                let h = self.scope.get_hash(name);
8377                let pv = PerlValue::hash(h);
8378                if ctx == WantarrayCtx::List {
8379                    Ok(pv)
8380                } else {
8381                    Ok(pv.scalar_context())
8382                }
8383            }
8384            ExprKind::Typeglob(name) => {
8385                let n = self.resolve_io_handle_name(name);
8386                Ok(PerlValue::string(n))
8387            }
8388            ExprKind::TypeglobExpr(e) => {
8389                let name = self.eval_expr(e)?.to_string();
8390                let n = self.resolve_io_handle_name(&name);
8391                Ok(PerlValue::string(n))
8392            }
8393            ExprKind::ArrayElement { array, index } => {
8394                self.check_strict_array_var(array, line)?;
8395                let idx = self.eval_expr(index)?.to_int();
8396                let aname = self.stash_array_name_for_package(array);
8397                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
8398                    let class = obj
8399                        .as_blessed_ref()
8400                        .map(|b| b.class.clone())
8401                        .unwrap_or_default();
8402                    let full = format!("{}::FETCH", class);
8403                    if let Some(sub) = self.subs.get(&full).cloned() {
8404                        let arg_vals = vec![obj, PerlValue::integer(idx)];
8405                        return self.call_sub(&sub, arg_vals, ctx, line);
8406                    }
8407                }
8408                Ok(self.scope.get_array_element(&aname, idx))
8409            }
8410            ExprKind::HashElement { hash, key } => {
8411                self.check_strict_hash_var(hash, line)?;
8412                let k = self.eval_expr(key)?.to_string();
8413                self.touch_env_hash(hash);
8414                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
8415                    let class = obj
8416                        .as_blessed_ref()
8417                        .map(|b| b.class.clone())
8418                        .unwrap_or_default();
8419                    let full = format!("{}::FETCH", class);
8420                    if let Some(sub) = self.subs.get(&full).cloned() {
8421                        let arg_vals = vec![obj, PerlValue::string(k)];
8422                        return self.call_sub(&sub, arg_vals, ctx, line);
8423                    }
8424                }
8425                Ok(self.scope.get_hash_element(hash, &k))
8426            }
8427            ExprKind::ArraySlice { array, indices } => {
8428                self.check_strict_array_var(array, line)?;
8429                let aname = self.stash_array_name_for_package(array);
8430                let flat = self.flatten_array_slice_index_specs(indices)?;
8431                let mut result = Vec::with_capacity(flat.len());
8432                for idx in flat {
8433                    result.push(self.scope.get_array_element(&aname, idx));
8434                }
8435                Ok(PerlValue::array(result))
8436            }
8437            ExprKind::HashSlice { hash, keys } => {
8438                self.check_strict_hash_var(hash, line)?;
8439                self.touch_env_hash(hash);
8440                let mut result = Vec::new();
8441                for key_expr in keys {
8442                    for k in self.eval_hash_slice_key_components(key_expr)? {
8443                        result.push(self.scope.get_hash_element(hash, &k));
8444                    }
8445                }
8446                Ok(PerlValue::array(result))
8447            }
8448            ExprKind::HashSliceDeref { container, keys } => {
8449                let hv = self.eval_expr(container)?;
8450                let mut key_vals = Vec::with_capacity(keys.len());
8451                for key_expr in keys {
8452                    let v = if matches!(
8453                        key_expr.kind,
8454                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
8455                    ) {
8456                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
8457                    } else {
8458                        self.eval_expr(key_expr)?
8459                    };
8460                    key_vals.push(v);
8461                }
8462                self.hash_slice_deref_values(&hv, &key_vals, line)
8463            }
8464            ExprKind::AnonymousListSlice { source, indices } => {
8465                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
8466                let items = list_val.to_list();
8467                let flat = self.flatten_array_slice_index_specs(indices)?;
8468                let mut out = Vec::with_capacity(flat.len());
8469                for idx in flat {
8470                    let i = if idx < 0 {
8471                        (items.len() as i64 + idx) as usize
8472                    } else {
8473                        idx as usize
8474                    };
8475                    out.push(items.get(i).cloned().unwrap_or(PerlValue::UNDEF));
8476                }
8477                let arr = PerlValue::array(out);
8478                if ctx != WantarrayCtx::List {
8479                    let v = arr.to_list();
8480                    Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF))
8481                } else {
8482                    Ok(arr)
8483                }
8484            }
8485
8486            // References
8487            ExprKind::ScalarRef(inner) => match &inner.kind {
8488                ExprKind::ScalarVar(name) => Ok(PerlValue::scalar_binding_ref(name.clone())),
8489                ExprKind::ArrayVar(name) => {
8490                    self.check_strict_array_var(name, line)?;
8491                    let aname = self.stash_array_name_for_package(name);
8492                    // Promote the scope's array to shared Arc-backed storage.
8493                    // Both the scope and the returned ref share the same Arc.
8494                    let arc = self.scope.promote_array_to_shared(&aname);
8495                    Ok(PerlValue::array_ref(arc))
8496                }
8497                ExprKind::HashVar(name) => {
8498                    self.check_strict_hash_var(name, line)?;
8499                    let arc = self.scope.promote_hash_to_shared(name);
8500                    Ok(PerlValue::hash_ref(arc))
8501                }
8502                ExprKind::Deref {
8503                    expr: e,
8504                    kind: Sigil::Array,
8505                } => {
8506                    let v = self.eval_expr(e)?;
8507                    self.make_array_ref_alias(v, line)
8508                }
8509                ExprKind::Deref {
8510                    expr: e,
8511                    kind: Sigil::Hash,
8512                } => {
8513                    let v = self.eval_expr(e)?;
8514                    self.make_hash_ref_alias(v, line)
8515                }
8516                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
8517                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8518                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8519                }
8520                ExprKind::HashSliceDeref { .. } => {
8521                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8522                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8523                }
8524                _ => {
8525                    let val = self.eval_expr(inner)?;
8526                    Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8527                }
8528            },
8529            ExprKind::ArrayRef(elems) => {
8530                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
8531                // variables flatten into the ref rather than collapsing to a scalar count /
8532                // flip-flop value.
8533                let mut arr = Vec::with_capacity(elems.len());
8534                for e in elems {
8535                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8536                    let v = self.scope.resolve_container_binding_ref(v);
8537                    if let Some(vec) = v.as_array_vec() {
8538                        arr.extend(vec);
8539                    } else {
8540                        arr.push(v);
8541                    }
8542                }
8543                Ok(PerlValue::array_ref(Arc::new(RwLock::new(arr))))
8544            }
8545            ExprKind::HashRef(pairs) => {
8546                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
8547                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
8548                let mut map = IndexMap::new();
8549                for (k, v) in pairs {
8550                    let key_str = self.eval_expr(k)?.to_string();
8551                    if key_str == "__HASH_SPREAD__" {
8552                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
8553                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8554                        let items = spread.to_list();
8555                        let mut i = 0;
8556                        while i + 1 < items.len() {
8557                            map.insert(items[i].to_string(), items[i + 1].clone());
8558                            i += 2;
8559                        }
8560                    } else {
8561                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8562                        map.insert(key_str, val);
8563                    }
8564                }
8565                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map))))
8566            }
8567            ExprKind::CodeRef { params, body } => {
8568                let captured = self.scope.capture();
8569                Ok(PerlValue::code_ref(Arc::new(PerlSub {
8570                    name: "__ANON__".to_string(),
8571                    params: params.clone(),
8572                    body: body.clone(),
8573                    closure_env: Some(captured),
8574                    prototype: None,
8575                    fib_like: None,
8576                })))
8577            }
8578            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
8579            ExprKind::SubroutineCodeRef(name) => {
8580                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
8581                    PerlError::runtime(self.undefined_subroutine_resolve_message(name), line)
8582                })?;
8583                Ok(PerlValue::code_ref(sub))
8584            }
8585            ExprKind::DynamicSubCodeRef(expr) => {
8586                let name = self.eval_expr(expr)?.to_string();
8587                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
8588                    PerlError::runtime(self.undefined_subroutine_resolve_message(&name), line)
8589                })?;
8590                Ok(PerlValue::code_ref(sub))
8591            }
8592            ExprKind::Deref { expr, kind } => {
8593                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
8594                    let val = self.eval_expr(expr)?;
8595                    let n = self.array_deref_len(val, line)?;
8596                    return Ok(PerlValue::integer(n));
8597                }
8598                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
8599                    let val = self.eval_expr(expr)?;
8600                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
8601                    return Ok(h.scalar_context());
8602                }
8603                let val = self.eval_expr(expr)?;
8604                self.symbolic_deref(val, *kind, line)
8605            }
8606            ExprKind::ArrowDeref { expr, index, kind } => {
8607                match kind {
8608                    DerefKind::Array => {
8609                        let container = self.eval_arrow_array_base(expr, line)?;
8610                        if let ExprKind::List(indices) = &index.kind {
8611                            let mut out = Vec::with_capacity(indices.len());
8612                            for ix in indices {
8613                                let idx = self.eval_expr(ix)?.to_int();
8614                                out.push(self.read_arrow_array_element(
8615                                    container.clone(),
8616                                    idx,
8617                                    line,
8618                                )?);
8619                            }
8620                            let arr = PerlValue::array(out);
8621                            if ctx != WantarrayCtx::List {
8622                                let v = arr.to_list();
8623                                return Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF));
8624                            }
8625                            return Ok(arr);
8626                        }
8627                        let idx = self.eval_expr(index)?.to_int();
8628                        self.read_arrow_array_element(container, idx, line)
8629                    }
8630                    DerefKind::Hash => {
8631                        let val = self.eval_arrow_hash_base(expr, line)?;
8632                        let key = self.eval_expr(index)?.to_string();
8633                        self.read_arrow_hash_element(val, key.as_str(), line)
8634                    }
8635                    DerefKind::Call => {
8636                        // $coderef->(args)
8637                        let val = self.eval_expr(expr)?;
8638                        if let ExprKind::List(ref arg_exprs) = index.kind {
8639                            let mut args = Vec::new();
8640                            for a in arg_exprs {
8641                                args.push(self.eval_expr(a)?);
8642                            }
8643                            // Auto-deref ScalarRef for closure self-reference: $f->()
8644                            let callable = if let Some(inner) = val.as_scalar_ref() {
8645                                inner.read().clone()
8646                            } else {
8647                                val
8648                            };
8649                            if let Some(sub) = callable.as_code_ref() {
8650                                return self.call_sub(&sub, args, ctx, line);
8651                            }
8652                            Err(PerlError::runtime("Not a code reference", line).into())
8653                        } else {
8654                            Err(PerlError::runtime("Invalid call deref", line).into())
8655                        }
8656                    }
8657                }
8658            }
8659
8660            // Binary operators
8661            ExprKind::BinOp { left, op, right } => {
8662                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
8663                match op {
8664                    BinOp::BindMatch => {
8665                        let lv = self.eval_expr(left)?;
8666                        let rv = self.eval_expr(right)?;
8667                        let s = lv.to_string();
8668                        let pat = rv.to_string();
8669                        return self.regex_match_execute(s, &pat, "", false, "_", line);
8670                    }
8671                    BinOp::BindNotMatch => {
8672                        let lv = self.eval_expr(left)?;
8673                        let rv = self.eval_expr(right)?;
8674                        let s = lv.to_string();
8675                        let pat = rv.to_string();
8676                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
8677                        return Ok(PerlValue::integer(if m.is_true() { 0 } else { 1 }));
8678                    }
8679                    BinOp::LogAnd | BinOp::LogAndWord => {
8680                        match &left.kind {
8681                            ExprKind::Regex(_, _) => {
8682                                if !self.eval_boolean_rvalue_condition(left)? {
8683                                    return Ok(PerlValue::string(String::new()));
8684                                }
8685                            }
8686                            _ => {
8687                                let lv = self.eval_expr(left)?;
8688                                if !lv.is_true() {
8689                                    return Ok(lv);
8690                                }
8691                            }
8692                        }
8693                        return match &right.kind {
8694                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8695                                if self.eval_boolean_rvalue_condition(right)? {
8696                                    1
8697                                } else {
8698                                    0
8699                                },
8700                            )),
8701                            _ => self.eval_expr(right),
8702                        };
8703                    }
8704                    BinOp::LogOr | BinOp::LogOrWord => {
8705                        match &left.kind {
8706                            ExprKind::Regex(_, _) => {
8707                                if self.eval_boolean_rvalue_condition(left)? {
8708                                    return Ok(PerlValue::integer(1));
8709                                }
8710                            }
8711                            _ => {
8712                                let lv = self.eval_expr(left)?;
8713                                if lv.is_true() {
8714                                    return Ok(lv);
8715                                }
8716                            }
8717                        }
8718                        return match &right.kind {
8719                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8720                                if self.eval_boolean_rvalue_condition(right)? {
8721                                    1
8722                                } else {
8723                                    0
8724                                },
8725                            )),
8726                            _ => self.eval_expr(right),
8727                        };
8728                    }
8729                    BinOp::DefinedOr => {
8730                        let lv = self.eval_expr(left)?;
8731                        if !lv.is_undef() {
8732                            return Ok(lv);
8733                        }
8734                        return self.eval_expr(right);
8735                    }
8736                    _ => {}
8737                }
8738                let lv = self.eval_expr(left)?;
8739                let rv = self.eval_expr(right)?;
8740                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
8741                    return r;
8742                }
8743                self.eval_binop(*op, &lv, &rv, line)
8744            }
8745
8746            // Unary
8747            ExprKind::UnaryOp { op, expr } => match op {
8748                UnaryOp::PreIncrement => {
8749                    if let ExprKind::ScalarVar(name) = &expr.kind {
8750                        self.check_strict_scalar_var(name, line)?;
8751                        let n = self.english_scalar_name(name);
8752                        return Ok(self
8753                            .scope
8754                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() + 1)));
8755                    }
8756                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8757                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8758                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8759                                *kind, true, true, line,
8760                            ));
8761                        }
8762                    }
8763                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8764                        let href = self.eval_expr(container)?;
8765                        let mut key_vals = Vec::with_capacity(keys.len());
8766                        for key_expr in keys {
8767                            key_vals.push(self.eval_expr(key_expr)?);
8768                        }
8769                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
8770                    }
8771                    if let ExprKind::ArrowDeref {
8772                        expr: arr_expr,
8773                        index,
8774                        kind: DerefKind::Array,
8775                    } = &expr.kind
8776                    {
8777                        if let ExprKind::List(indices) = &index.kind {
8778                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8779                            let mut idxs = Vec::with_capacity(indices.len());
8780                            for ix in indices {
8781                                idxs.push(self.eval_expr(ix)?.to_int());
8782                            }
8783                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
8784                        }
8785                    }
8786                    let val = self.eval_expr(expr)?;
8787                    let new_val = PerlValue::integer(val.to_int() + 1);
8788                    self.assign_value(expr, new_val.clone())?;
8789                    Ok(new_val)
8790                }
8791                UnaryOp::PreDecrement => {
8792                    if let ExprKind::ScalarVar(name) = &expr.kind {
8793                        self.check_strict_scalar_var(name, line)?;
8794                        let n = self.english_scalar_name(name);
8795                        return Ok(self
8796                            .scope
8797                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() - 1)));
8798                    }
8799                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8800                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8801                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8802                                *kind, true, false, line,
8803                            ));
8804                        }
8805                    }
8806                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8807                        let href = self.eval_expr(container)?;
8808                        let mut key_vals = Vec::with_capacity(keys.len());
8809                        for key_expr in keys {
8810                            key_vals.push(self.eval_expr(key_expr)?);
8811                        }
8812                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
8813                    }
8814                    if let ExprKind::ArrowDeref {
8815                        expr: arr_expr,
8816                        index,
8817                        kind: DerefKind::Array,
8818                    } = &expr.kind
8819                    {
8820                        if let ExprKind::List(indices) = &index.kind {
8821                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8822                            let mut idxs = Vec::with_capacity(indices.len());
8823                            for ix in indices {
8824                                idxs.push(self.eval_expr(ix)?.to_int());
8825                            }
8826                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
8827                        }
8828                    }
8829                    let val = self.eval_expr(expr)?;
8830                    let new_val = PerlValue::integer(val.to_int() - 1);
8831                    self.assign_value(expr, new_val.clone())?;
8832                    Ok(new_val)
8833                }
8834                _ => {
8835                    match op {
8836                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
8837                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
8838                                let topic = self.scope.get_scalar("_");
8839                                let rl = expr.line;
8840                                let s = topic.to_string();
8841                                let v =
8842                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
8843                                return Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }));
8844                            }
8845                        }
8846                        _ => {}
8847                    }
8848                    let val = self.eval_expr(expr)?;
8849                    match op {
8850                        UnaryOp::Negate => {
8851                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
8852                                return r;
8853                            }
8854                            if let Some(n) = val.as_integer() {
8855                                Ok(PerlValue::integer(-n))
8856                            } else {
8857                                Ok(PerlValue::float(-val.to_number()))
8858                            }
8859                        }
8860                        UnaryOp::LogNot => {
8861                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8862                                let pv = r?;
8863                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8864                            }
8865                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8866                        }
8867                        UnaryOp::BitNot => Ok(PerlValue::integer(!val.to_int())),
8868                        UnaryOp::LogNotWord => {
8869                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8870                                let pv = r?;
8871                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8872                            }
8873                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8874                        }
8875                        UnaryOp::Ref => {
8876                            if let ExprKind::ScalarVar(name) = &expr.kind {
8877                                return Ok(PerlValue::scalar_binding_ref(name.clone()));
8878                            }
8879                            Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8880                        }
8881                        _ => unreachable!(),
8882                    }
8883                }
8884            },
8885
8886            ExprKind::PostfixOp { expr, op } => {
8887                // For scalar variables, use atomic_mutate_post to hold the lock
8888                // for the entire read-modify-write (critical for mysync).
8889                if let ExprKind::ScalarVar(name) = &expr.kind {
8890                    self.check_strict_scalar_var(name, line)?;
8891                    let n = self.english_scalar_name(name);
8892                    let f: fn(&PerlValue) -> PerlValue = match op {
8893                        PostfixOp::Increment => |v| PerlValue::integer(v.to_int() + 1),
8894                        PostfixOp::Decrement => |v| PerlValue::integer(v.to_int() - 1),
8895                    };
8896                    return Ok(self.scope.atomic_mutate_post(n, f));
8897                }
8898                if let ExprKind::Deref { kind, .. } = &expr.kind {
8899                    if matches!(kind, Sigil::Array | Sigil::Hash) {
8900                        let is_inc = matches!(op, PostfixOp::Increment);
8901                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8902                            *kind, false, is_inc, line,
8903                        ));
8904                    }
8905                }
8906                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8907                    let href = self.eval_expr(container)?;
8908                    let mut key_vals = Vec::with_capacity(keys.len());
8909                    for key_expr in keys {
8910                        key_vals.push(self.eval_expr(key_expr)?);
8911                    }
8912                    let kind_byte = match op {
8913                        PostfixOp::Increment => 2u8,
8914                        PostfixOp::Decrement => 3u8,
8915                    };
8916                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
8917                }
8918                if let ExprKind::ArrowDeref {
8919                    expr: arr_expr,
8920                    index,
8921                    kind: DerefKind::Array,
8922                } = &expr.kind
8923                {
8924                    if let ExprKind::List(indices) = &index.kind {
8925                        let container = self.eval_arrow_array_base(arr_expr, line)?;
8926                        let mut idxs = Vec::with_capacity(indices.len());
8927                        for ix in indices {
8928                            idxs.push(self.eval_expr(ix)?.to_int());
8929                        }
8930                        let kind_byte = match op {
8931                            PostfixOp::Increment => 2u8,
8932                            PostfixOp::Decrement => 3u8,
8933                        };
8934                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
8935                    }
8936                }
8937                let val = self.eval_expr(expr)?;
8938                let old = val.clone();
8939                let new_val = match op {
8940                    PostfixOp::Increment => PerlValue::integer(val.to_int() + 1),
8941                    PostfixOp::Decrement => PerlValue::integer(val.to_int() - 1),
8942                };
8943                self.assign_value(expr, new_val)?;
8944                Ok(old)
8945            }
8946
8947            // Assignment
8948            ExprKind::Assign { target, value } => {
8949                if let ExprKind::Typeglob(lhs) = &target.kind {
8950                    if let ExprKind::Typeglob(rhs) = &value.kind {
8951                        self.copy_typeglob_slots(lhs, rhs, line)?;
8952                        return self.eval_expr(value);
8953                    }
8954                }
8955                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
8956                self.assign_value(target, val.clone())?;
8957                Ok(val)
8958            }
8959            ExprKind::CompoundAssign { target, op, value } => {
8960                // For scalar targets, use atomic_mutate to hold the lock.
8961                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
8962                if let ExprKind::ScalarVar(name) = &target.kind {
8963                    self.check_strict_scalar_var(name, line)?;
8964                    let n = self.english_scalar_name(name);
8965                    let op = *op;
8966                    let rhs = match op {
8967                        BinOp::LogOr => {
8968                            let old = self.scope.get_scalar(n);
8969                            if old.is_true() {
8970                                return Ok(old);
8971                            }
8972                            self.eval_expr(value)?
8973                        }
8974                        BinOp::DefinedOr => {
8975                            let old = self.scope.get_scalar(n);
8976                            if !old.is_undef() {
8977                                return Ok(old);
8978                            }
8979                            self.eval_expr(value)?
8980                        }
8981                        BinOp::LogAnd => {
8982                            let old = self.scope.get_scalar(n);
8983                            if !old.is_true() {
8984                                return Ok(old);
8985                            }
8986                            self.eval_expr(value)?
8987                        }
8988                        _ => self.eval_expr(value)?,
8989                    };
8990                    return Ok(self.scalar_compound_assign_scalar_target(n, op, rhs)?);
8991                }
8992                let rhs = self.eval_expr(value)?;
8993                // For hash element targets: $h{key} += 1
8994                if let ExprKind::HashElement { hash, key } = &target.kind {
8995                    self.check_strict_hash_var(hash, line)?;
8996                    let k = self.eval_expr(key)?.to_string();
8997                    let op = *op;
8998                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
8999                        BinOp::Add => {
9000                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9001                                PerlValue::integer(a.wrapping_add(b))
9002                            } else {
9003                                PerlValue::float(old.to_number() + rhs.to_number())
9004                            }
9005                        }
9006                        BinOp::Sub => {
9007                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9008                                PerlValue::integer(a.wrapping_sub(b))
9009                            } else {
9010                                PerlValue::float(old.to_number() - rhs.to_number())
9011                            }
9012                        }
9013                        BinOp::Concat => {
9014                            let mut s = old.to_string();
9015                            rhs.append_to(&mut s);
9016                            PerlValue::string(s)
9017                        }
9018                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
9019                    })?);
9020                }
9021                // For array element targets: $a[i] += 1
9022                if let ExprKind::ArrayElement { array, index } = &target.kind {
9023                    self.check_strict_array_var(array, line)?;
9024                    let idx = self.eval_expr(index)?.to_int();
9025                    let op = *op;
9026                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
9027                        BinOp::Add => {
9028                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9029                                PerlValue::integer(a.wrapping_add(b))
9030                            } else {
9031                                PerlValue::float(old.to_number() + rhs.to_number())
9032                            }
9033                        }
9034                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
9035                    })?);
9036                }
9037                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
9038                    let href = self.eval_expr(container)?;
9039                    let mut key_vals = Vec::with_capacity(keys.len());
9040                    for key_expr in keys {
9041                        key_vals.push(self.eval_expr(key_expr)?);
9042                    }
9043                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
9044                }
9045                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
9046                    if let ExprKind::Deref {
9047                        expr: inner,
9048                        kind: Sigil::Array,
9049                    } = &source.kind
9050                    {
9051                        let container = self.eval_arrow_array_base(inner, line)?;
9052                        let idxs = self.flatten_array_slice_index_specs(indices)?;
9053                        return self
9054                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
9055                    }
9056                }
9057                if let ExprKind::ArrowDeref {
9058                    expr: arr_expr,
9059                    index,
9060                    kind: DerefKind::Array,
9061                } = &target.kind
9062                {
9063                    if let ExprKind::List(indices) = &index.kind {
9064                        let container = self.eval_arrow_array_base(arr_expr, line)?;
9065                        let mut idxs = Vec::with_capacity(indices.len());
9066                        for ix in indices {
9067                            idxs.push(self.eval_expr(ix)?.to_int());
9068                        }
9069                        return self
9070                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
9071                    }
9072                }
9073                let old = self.eval_expr(target)?;
9074                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
9075                self.assign_value(target, new_val.clone())?;
9076                Ok(new_val)
9077            }
9078
9079            // Ternary
9080            ExprKind::Ternary {
9081                condition,
9082                then_expr,
9083                else_expr,
9084            } => {
9085                if self.eval_boolean_rvalue_condition(condition)? {
9086                    self.eval_expr(then_expr)
9087                } else {
9088                    self.eval_expr(else_expr)
9089                }
9090            }
9091
9092            // Range
9093            ExprKind::Range {
9094                from,
9095                to,
9096                exclusive,
9097                step,
9098            } => {
9099                if ctx == WantarrayCtx::List {
9100                    let f = self.eval_expr(from)?;
9101                    let t = self.eval_expr(to)?;
9102                    if let Some(s) = step {
9103                        let step_val = self.eval_expr(s)?.to_int();
9104                        let from_i = f.to_int();
9105                        let to_i = t.to_int();
9106                        let list = if step_val == 0 {
9107                            vec![]
9108                        } else if step_val > 0 {
9109                            (from_i..=to_i)
9110                                .step_by(step_val as usize)
9111                                .map(PerlValue::integer)
9112                                .collect()
9113                        } else {
9114                            std::iter::successors(Some(from_i), |&x| {
9115                                let next = x - step_val.abs();
9116                                if next >= to_i {
9117                                    Some(next)
9118                                } else {
9119                                    None
9120                                }
9121                            })
9122                            .map(PerlValue::integer)
9123                            .collect()
9124                        };
9125                        Ok(PerlValue::array(list))
9126                    } else {
9127                        let list = perl_list_range_expand(f, t);
9128                        Ok(PerlValue::array(list))
9129                    }
9130                } else {
9131                    let key = std::ptr::from_ref(expr) as usize;
9132                    match (&from.kind, &to.kind) {
9133                        (
9134                            ExprKind::Regex(left_pat, left_flags),
9135                            ExprKind::Regex(right_pat, right_flags),
9136                        ) => {
9137                            let dot = self.scalar_flipflop_dot_line();
9138                            let subject = self.scope.get_scalar("_").to_string();
9139                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9140                                |e| match e {
9141                                    FlowOrError::Error(err) => err,
9142                                    FlowOrError::Flow(_) => PerlError::runtime(
9143                                        "unexpected flow in regex flip-flop",
9144                                        line,
9145                                    ),
9146                                },
9147                            )?;
9148                            let right_re = self
9149                                .compile_regex(right_pat, right_flags, line)
9150                                .map_err(|e| match e {
9151                                    FlowOrError::Error(err) => err,
9152                                    FlowOrError::Flow(_) => PerlError::runtime(
9153                                        "unexpected flow in regex flip-flop",
9154                                        line,
9155                                    ),
9156                                })?;
9157                            let left_m = left_re.is_match(&subject);
9158                            let right_m = right_re.is_match(&subject);
9159                            let st = self.flip_flop_tree.entry(key).or_default();
9160                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9161                                &mut st.active,
9162                                &mut st.exclusive_left_line,
9163                                *exclusive,
9164                                dot,
9165                                left_m,
9166                                right_m,
9167                            )))
9168                        }
9169                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
9170                            let dot = self.scalar_flipflop_dot_line();
9171                            let subject = self.scope.get_scalar("_").to_string();
9172                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9173                                |e| match e {
9174                                    FlowOrError::Error(err) => err,
9175                                    FlowOrError::Flow(_) => PerlError::runtime(
9176                                        "unexpected flow in regex/eof flip-flop",
9177                                        line,
9178                                    ),
9179                                },
9180                            )?;
9181                            let left_m = left_re.is_match(&subject);
9182                            let right_m = self.eof_without_arg_is_true();
9183                            let st = self.flip_flop_tree.entry(key).or_default();
9184                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9185                                &mut st.active,
9186                                &mut st.exclusive_left_line,
9187                                *exclusive,
9188                                dot,
9189                                left_m,
9190                                right_m,
9191                            )))
9192                        }
9193                        (
9194                            ExprKind::Regex(left_pat, left_flags),
9195                            ExprKind::Integer(_) | ExprKind::Float(_),
9196                        ) => {
9197                            let dot = self.scalar_flipflop_dot_line();
9198                            let right = self.eval_expr(to)?.to_int();
9199                            let subject = self.scope.get_scalar("_").to_string();
9200                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9201                                |e| match e {
9202                                    FlowOrError::Error(err) => err,
9203                                    FlowOrError::Flow(_) => PerlError::runtime(
9204                                        "unexpected flow in regex flip-flop",
9205                                        line,
9206                                    ),
9207                                },
9208                            )?;
9209                            let left_m = left_re.is_match(&subject);
9210                            let right_m = dot == right;
9211                            let st = self.flip_flop_tree.entry(key).or_default();
9212                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9213                                &mut st.active,
9214                                &mut st.exclusive_left_line,
9215                                *exclusive,
9216                                dot,
9217                                left_m,
9218                                right_m,
9219                            )))
9220                        }
9221                        (ExprKind::Regex(left_pat, left_flags), _) => {
9222                            if let ExprKind::Eof(Some(_)) = &to.kind {
9223                                return Err(FlowOrError::Error(PerlError::runtime(
9224                                    "regex flip-flop with eof(HANDLE) is not supported",
9225                                    line,
9226                                )));
9227                            }
9228                            let dot = self.scalar_flipflop_dot_line();
9229                            let subject = self.scope.get_scalar("_").to_string();
9230                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9231                                |e| match e {
9232                                    FlowOrError::Error(err) => err,
9233                                    FlowOrError::Flow(_) => PerlError::runtime(
9234                                        "unexpected flow in regex flip-flop",
9235                                        line,
9236                                    ),
9237                                },
9238                            )?;
9239                            let left_m = left_re.is_match(&subject);
9240                            let right_m = self.eval_boolean_rvalue_condition(to)?;
9241                            let st = self.flip_flop_tree.entry(key).or_default();
9242                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9243                                &mut st.active,
9244                                &mut st.exclusive_left_line,
9245                                *exclusive,
9246                                dot,
9247                                left_m,
9248                                right_m,
9249                            )))
9250                        }
9251                        _ => {
9252                            let left = self.eval_expr(from)?.to_int();
9253                            let right = self.eval_expr(to)?.to_int();
9254                            let dot = self.scalar_flipflop_dot_line();
9255                            let st = self.flip_flop_tree.entry(key).or_default();
9256                            if !st.active {
9257                                if dot == left {
9258                                    st.active = true;
9259                                    if *exclusive {
9260                                        st.exclusive_left_line = Some(dot);
9261                                    } else {
9262                                        st.exclusive_left_line = None;
9263                                        if dot == right {
9264                                            st.active = false;
9265                                        }
9266                                    }
9267                                    return Ok(PerlValue::integer(1));
9268                                }
9269                                return Ok(PerlValue::integer(0));
9270                            }
9271                            if let Some(ll) = st.exclusive_left_line {
9272                                if dot == right && dot > ll {
9273                                    st.active = false;
9274                                    st.exclusive_left_line = None;
9275                                }
9276                            } else if dot == right {
9277                                st.active = false;
9278                            }
9279                            Ok(PerlValue::integer(1))
9280                        }
9281                    }
9282                }
9283            }
9284
9285            // SliceRange — open-ended Python-style slice expansion. Reachable from the
9286            // tree-walker when slice subscripts are evaluated outside the VM (rare; VM is
9287            // the primary execution engine). Only closed forms (`from:to[:step]`) can be
9288            // expanded here without container length context; open ends require a slice
9289            // op (`Op::ArraySliceRange` / `Op::HashSliceRange`) which knows the container.
9290            ExprKind::SliceRange { from, to, step } => {
9291                let f = match from {
9292                    Some(e) => self.eval_expr(e)?,
9293                    None => {
9294                        return Err(PerlError::runtime(
9295                            "open-ended slice range cannot be evaluated outside slice subscript",
9296                            line,
9297                        )
9298                        .into());
9299                    }
9300                };
9301                let t = match to {
9302                    Some(e) => self.eval_expr(e)?,
9303                    None => {
9304                        return Err(PerlError::runtime(
9305                            "open-ended slice range cannot be evaluated outside slice subscript",
9306                            line,
9307                        )
9308                        .into());
9309                    }
9310                };
9311                let list = if let Some(s) = step {
9312                    let sv = self.eval_expr(s)?;
9313                    crate::value::perl_list_range_expand_stepped(f, t, sv)
9314                } else {
9315                    perl_list_range_expand(f, t)
9316                };
9317                Ok(PerlValue::array(list))
9318            }
9319
9320            // Repeat
9321            ExprKind::Repeat { expr, count } => {
9322                let val = self.eval_expr(expr)?;
9323                let n = self.eval_expr(count)?.to_int().max(0) as usize;
9324                if let Some(s) = val.as_str() {
9325                    Ok(PerlValue::string(s.repeat(n)))
9326                } else if let Some(a) = val.as_array_vec() {
9327                    let mut result = Vec::with_capacity(a.len() * n);
9328                    for _ in 0..n {
9329                        result.extend(a.iter().cloned());
9330                    }
9331                    Ok(PerlValue::array(result))
9332                } else {
9333                    Ok(PerlValue::string(val.to_string().repeat(n)))
9334                }
9335            }
9336
9337            // `my $x = …` / `our` / `state` / `local` used as an expression
9338            // (e.g. `if (my $line = readline)`).  Declare each variable in the
9339            // current scope, evaluate the initializer (if any), and return the
9340            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
9341            ExprKind::MyExpr { keyword, decls } => {
9342                // Build a temporary statement and dispatch to the canonical
9343                // statement handler so behavior matches `my $x = …;` exactly.
9344                let stmt_kind = match keyword.as_str() {
9345                    "my" => StmtKind::My(decls.clone()),
9346                    "our" => StmtKind::Our(decls.clone()),
9347                    "state" => StmtKind::State(decls.clone()),
9348                    "local" => StmtKind::Local(decls.clone()),
9349                    _ => StmtKind::My(decls.clone()),
9350                };
9351                let stmt = Statement {
9352                    label: None,
9353                    kind: stmt_kind,
9354                    line,
9355                };
9356                self.exec_statement(&stmt)?;
9357                // Return the value of the (first) declared variable so the
9358                // surrounding expression sees the assigned value, matching
9359                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
9360                let first = decls.first().ok_or_else(|| {
9361                    FlowOrError::Error(PerlError::runtime("MyExpr: empty decl list", line))
9362                })?;
9363                Ok(match first.sigil {
9364                    Sigil::Scalar => self.scope.get_scalar(&first.name),
9365                    Sigil::Array => PerlValue::array(self.scope.get_array(&first.name)),
9366                    Sigil::Hash => {
9367                        let h = self.scope.get_hash(&first.name);
9368                        let mut flat: Vec<PerlValue> = Vec::with_capacity(h.len() * 2);
9369                        for (k, v) in h {
9370                            flat.push(PerlValue::string(k));
9371                            flat.push(v);
9372                        }
9373                        PerlValue::array(flat)
9374                    }
9375                    Sigil::Typeglob => PerlValue::UNDEF,
9376                })
9377            }
9378
9379            // Function calls
9380            ExprKind::FuncCall { name, args } => {
9381                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
9382                // bare-name dispatch so the matches below stay flat.
9383                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
9384                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
9385                if matches!(dispatch_name, "read") && args.len() >= 3 {
9386                    let fh_val = self.eval_expr(&args[0])?;
9387                    let fh = fh_val
9388                        .as_io_handle_name()
9389                        .unwrap_or_else(|| fh_val.to_string());
9390                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
9391                    let offset = if args.len() > 3 {
9392                        self.eval_expr(&args[3])?.to_int().max(0) as usize
9393                    } else {
9394                        0
9395                    };
9396                    // Extract the variable name from the AST
9397                    let var_name = match &args[1].kind {
9398                        ExprKind::ScalarVar(n) => n.clone(),
9399                        _ => self.eval_expr(&args[1])?.to_string(),
9400                    };
9401                    let mut buf = vec![0u8; len];
9402                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
9403                        slot.lock().read(&mut buf).unwrap_or(0)
9404                    } else if fh == "STDIN" {
9405                        std::io::stdin().read(&mut buf).unwrap_or(0)
9406                    } else {
9407                        return Err(PerlError::runtime(
9408                            format!("read: unopened handle {}", fh),
9409                            line,
9410                        )
9411                        .into());
9412                    };
9413                    buf.truncate(n);
9414                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
9415                    if offset > 0 {
9416                        let mut existing = self.scope.get_scalar(&var_name).to_string();
9417                        while existing.len() < offset {
9418                            existing.push('\0');
9419                        }
9420                        existing.push_str(&read_str);
9421                        let _ = self
9422                            .scope
9423                            .set_scalar(&var_name, PerlValue::string(existing));
9424                    } else {
9425                        let _ = self
9426                            .scope
9427                            .set_scalar(&var_name, PerlValue::string(read_str));
9428                    }
9429                    return Ok(PerlValue::integer(n as i64));
9430                }
9431                if matches!(dispatch_name, "group_by" | "chunk_by") {
9432                    if args.len() != 2 {
9433                        return Err(PerlError::runtime(
9434                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
9435                            line,
9436                        )
9437                        .into());
9438                    }
9439                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
9440                }
9441                if matches!(dispatch_name, "puniq" | "pfirst" | "pany") {
9442                    let mut arg_vals = Vec::with_capacity(args.len());
9443                    for a in args {
9444                        arg_vals.push(self.eval_expr(a)?);
9445                    }
9446                    let saved_wa = self.wantarray_kind;
9447                    self.wantarray_kind = ctx;
9448                    let r = self.eval_par_list_call(dispatch_name, &arg_vals, ctx, line);
9449                    self.wantarray_kind = saved_wa;
9450                    return r.map_err(Into::into);
9451                }
9452                let arg_vals = if matches!(dispatch_name, "any" | "all" | "none" | "first")
9453                    || matches!(
9454                        dispatch_name,
9455                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9456                    )
9457                    || matches!(
9458                        dispatch_name,
9459                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
9460                    ) {
9461                    if args.len() != 2 {
9462                        return Err(PerlError::runtime(
9463                            format!("{}: expected BLOCK, LIST", name),
9464                            line,
9465                        )
9466                        .into());
9467                    }
9468                    let cr = self.eval_expr(&args[0])?;
9469                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
9470                    let mut v = vec![cr];
9471                    v.extend(list_src.to_list());
9472                    v
9473                } else if matches!(
9474                    dispatch_name,
9475                    "zip"
9476                        | "zip_longest"
9477                        | "zip_shortest"
9478                        | "mesh"
9479                        | "mesh_longest"
9480                        | "mesh_shortest"
9481                ) {
9482                    let mut v = Vec::with_capacity(args.len());
9483                    for a in args {
9484                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9485                    }
9486                    v
9487                } else if matches!(
9488                    dispatch_name,
9489                    "uniq"
9490                        | "distinct"
9491                        | "uniqstr"
9492                        | "uniqint"
9493                        | "uniqnum"
9494                        | "flatten"
9495                        | "set"
9496                        | "list_count"
9497                        | "list_size"
9498                        | "count"
9499                        | "size"
9500                        | "cnt"
9501                        | "with_index"
9502                        | "shuffle"
9503                        | "sum"
9504                        | "sum0"
9505                        | "product"
9506                        | "min"
9507                        | "max"
9508                        | "minstr"
9509                        | "maxstr"
9510                        | "mean"
9511                        | "median"
9512                        | "mode"
9513                        | "stddev"
9514                        | "variance"
9515                        | "pairs"
9516                        | "unpairs"
9517                        | "pairkeys"
9518                        | "pairvalues"
9519                ) {
9520                    // Slurpy list `(@)`: one list expr (`uniq @x`) or multiple actuals
9521                    // (`uniq(1, 1, 2)`). Each actual is evaluated in list context so
9522                    // `@a, @b` flattens.
9523                    let mut list_out = Vec::new();
9524                    if args.len() == 1 {
9525                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
9526                    } else {
9527                        for a in args {
9528                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
9529                        }
9530                    }
9531                    list_out
9532                } else if matches!(dispatch_name, "take" | "head" | "tail" | "drop") {
9533                    if args.is_empty() {
9534                        return Err(PerlError::runtime(
9535                            "take/head/tail/drop: need LIST..., N or unary N",
9536                            line,
9537                        )
9538                        .into());
9539                    }
9540                    let mut arg_vals = Vec::with_capacity(args.len());
9541                    if args.len() == 1 {
9542                        // head @l == head @l, 1 — evaluate in list context
9543                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9544                    } else {
9545                        for a in &args[..args.len() - 1] {
9546                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9547                        }
9548                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
9549                    }
9550                    arg_vals
9551                } else if matches!(dispatch_name, "chunked" | "windowed") {
9552                    let mut list_out = Vec::new();
9553                    match args.len() {
9554                        0 => {
9555                            return Err(PerlError::runtime(
9556                                format!("{name}: expected (LIST, N) or unary N after |>"),
9557                                line,
9558                            )
9559                            .into());
9560                        }
9561                        1 => {
9562                            // chunked @l / windowed @l — evaluate in list context, default size
9563                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9564                        }
9565                        2 => {
9566                            list_out.extend(
9567                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
9568                            );
9569                            list_out.push(self.eval_expr(&args[1])?);
9570                        }
9571                        _ => {
9572                            return Err(PerlError::runtime(
9573                                format!(
9574                                    "{name}: expected exactly (LIST, N); use one list expression then size"
9575                                ),
9576                                line,
9577                            )
9578                            .into());
9579                        }
9580                    }
9581                    list_out
9582                } else {
9583                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
9584                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
9585                    let mut arg_vals = Vec::with_capacity(args.len());
9586                    for a in args {
9587                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
9588                        if let Some(items) = v.as_array_vec() {
9589                            arg_vals.extend(items);
9590                        } else {
9591                            arg_vals.push(v);
9592                        }
9593                    }
9594                    arg_vals
9595                };
9596                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
9597                let saved_wa = self.wantarray_kind;
9598                self.wantarray_kind = ctx;
9599                // Builtins first — immune to monkey-patching (matches VM dispatch order).
9600                // In compat mode, user subs shadow builtins (Perl 5 semantics).
9601                if !crate::compat_mode() {
9602                    if matches!(
9603                        dispatch_name,
9604                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9605                    ) {
9606                        let r =
9607                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
9608                        self.wantarray_kind = saved_wa;
9609                        return r.map_err(Into::into);
9610                    }
9611                    if let Some(r) =
9612                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
9613                    {
9614                        self.wantarray_kind = saved_wa;
9615                        return r.map_err(Into::into);
9616                    }
9617                }
9618                if let Some(sub) = self.resolve_sub_by_name(name) {
9619                    self.wantarray_kind = saved_wa;
9620                    let args = self.with_topic_default_args(arg_vals);
9621                    return self.call_sub(&sub, args, ctx, line);
9622                }
9623                // Compat mode: check builtins after user subs (Perl 5 semantics).
9624                if crate::compat_mode() {
9625                    if matches!(
9626                        dispatch_name,
9627                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9628                    ) {
9629                        let r =
9630                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
9631                        self.wantarray_kind = saved_wa;
9632                        return r.map_err(Into::into);
9633                    }
9634                    if let Some(r) =
9635                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
9636                    {
9637                        self.wantarray_kind = saved_wa;
9638                        return r.map_err(Into::into);
9639                    }
9640                }
9641                self.wantarray_kind = saved_wa;
9642                self.call_named_sub(name, arg_vals, line, ctx)
9643            }
9644            ExprKind::IndirectCall {
9645                target,
9646                args,
9647                ampersand: _,
9648                pass_caller_arglist,
9649            } => {
9650                let tval = self.eval_expr(target)?;
9651                let arg_vals = if *pass_caller_arglist {
9652                    self.scope.get_array("_")
9653                } else {
9654                    let mut v = Vec::with_capacity(args.len());
9655                    for a in args {
9656                        v.push(self.eval_expr(a)?);
9657                    }
9658                    v
9659                };
9660                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
9661            }
9662            ExprKind::MethodCall {
9663                object,
9664                method,
9665                args,
9666                super_call,
9667            } => {
9668                let obj = self.eval_expr(object)?;
9669                let mut arg_vals = vec![obj.clone()];
9670                for a in args {
9671                    arg_vals.push(self.eval_expr(a)?);
9672                }
9673                if let Some(r) =
9674                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
9675                {
9676                    return r.map_err(Into::into);
9677                }
9678                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
9679                    return r.map_err(Into::into);
9680                }
9681                // Get class name
9682                let class = if let Some(b) = obj.as_blessed_ref() {
9683                    b.class.clone()
9684                } else if let Some(s) = obj.as_str() {
9685                    s // Class->method()
9686                } else {
9687                    return Err(PerlError::runtime("Can't call method on non-object", line).into());
9688                };
9689                if method == "VERSION" && !*super_call {
9690                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
9691                        return Ok(ver);
9692                    }
9693                }
9694                // UNIVERSAL methods: isa, can, DOES
9695                if !*super_call {
9696                    match method.as_str() {
9697                        "isa" => {
9698                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9699                            let mro = self.mro_linearize(&class);
9700                            let result = mro.iter().any(|c| c == &target);
9701                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9702                        }
9703                        "can" => {
9704                            let target_method =
9705                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9706                            let found = self
9707                                .resolve_method_full_name(&class, &target_method, false)
9708                                .and_then(|fq| self.subs.get(&fq))
9709                                .is_some();
9710                            if found {
9711                                return Ok(PerlValue::code_ref(Arc::new(PerlSub {
9712                                    name: target_method,
9713                                    params: vec![],
9714                                    body: vec![],
9715                                    closure_env: None,
9716                                    prototype: None,
9717                                    fib_like: None,
9718                                })));
9719                            } else {
9720                                return Ok(PerlValue::UNDEF);
9721                            }
9722                        }
9723                        "DOES" => {
9724                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9725                            let mro = self.mro_linearize(&class);
9726                            let result = mro.iter().any(|c| c == &target);
9727                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9728                        }
9729                        _ => {}
9730                    }
9731                }
9732                let full_name = self
9733                    .resolve_method_full_name(&class, method, *super_call)
9734                    .ok_or_else(|| {
9735                        PerlError::runtime(
9736                            format!(
9737                                "Can't locate method \"{}\" for invocant \"{}\"",
9738                                method, class
9739                            ),
9740                            line,
9741                        )
9742                    })?;
9743                if let Some(sub) = self.subs.get(&full_name).cloned() {
9744                    self.call_sub(&sub, arg_vals, ctx, line)
9745                } else if method == "new" && !*super_call {
9746                    // Default constructor
9747                    self.builtin_new(&class, arg_vals, line)
9748                } else if let Some(r) =
9749                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
9750                {
9751                    r
9752                } else {
9753                    Err(PerlError::runtime(
9754                        format!(
9755                            "Can't locate method \"{}\" in package \"{}\"",
9756                            method, class
9757                        ),
9758                        line,
9759                    )
9760                    .into())
9761                }
9762            }
9763
9764            // Print/Say/Printf
9765            ExprKind::Print { handle, args } => {
9766                self.exec_print(handle.as_deref(), args, false, line)
9767            }
9768            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
9769            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
9770            ExprKind::Die(args) => {
9771                if args.is_empty() {
9772                    // `die` with no args: re-die with current $@ or "Died"
9773                    let current = self.scope.get_scalar("@");
9774                    let msg = if current.is_undef() || current.to_string().is_empty() {
9775                        let mut m = "Died".to_string();
9776                        m.push_str(&self.die_warn_at_suffix(line));
9777                        m.push('\n');
9778                        m
9779                    } else {
9780                        current.to_string()
9781                    };
9782                    return Err(PerlError::die(msg, line).into());
9783                }
9784                // Single ref argument: store the ref value in $@
9785                if args.len() == 1 {
9786                    let v = self.eval_expr(&args[0])?;
9787                    if v.as_hash_ref().is_some()
9788                        || v.as_blessed_ref().is_some()
9789                        || v.as_array_ref().is_some()
9790                        || v.as_code_ref().is_some()
9791                    {
9792                        let msg = v.to_string();
9793                        return Err(PerlError::die_with_value(v, msg, line).into());
9794                    }
9795                }
9796                let mut msg = String::new();
9797                for a in args {
9798                    let v = self.eval_expr(a)?;
9799                    msg.push_str(&v.to_string());
9800                }
9801                if msg.is_empty() {
9802                    msg = "Died".to_string();
9803                }
9804                if !msg.ends_with('\n') {
9805                    msg.push_str(&self.die_warn_at_suffix(line));
9806                    msg.push('\n');
9807                }
9808                Err(PerlError::die(msg, line).into())
9809            }
9810            ExprKind::Warn(args) => {
9811                let mut msg = String::new();
9812                for a in args {
9813                    let v = self.eval_expr(a)?;
9814                    msg.push_str(&v.to_string());
9815                }
9816                if msg.is_empty() {
9817                    msg = "Warning: something's wrong".to_string();
9818                }
9819                if !msg.ends_with('\n') {
9820                    msg.push_str(&self.die_warn_at_suffix(line));
9821                    msg.push('\n');
9822                }
9823                eprint!("{}", msg);
9824                Ok(PerlValue::integer(1))
9825            }
9826
9827            // Regex
9828            ExprKind::Match {
9829                expr,
9830                pattern,
9831                flags,
9832                scalar_g,
9833                delim: _,
9834            } => {
9835                let val = self.eval_expr(expr)?;
9836                if val.is_iterator() {
9837                    let source = crate::map_stream::into_pull_iter(val);
9838                    let re = self.compile_regex(pattern, flags, line)?;
9839                    let global = flags.contains('g');
9840                    if global {
9841                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9842                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
9843                        )));
9844                    } else {
9845                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9846                            crate::map_stream::MatchStreamIterator::new(source, re),
9847                        )));
9848                    }
9849                }
9850                let s = val.to_string();
9851                let pos_key = match &expr.kind {
9852                    ExprKind::ScalarVar(n) => n.as_str(),
9853                    _ => "_",
9854                };
9855                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
9856            }
9857            ExprKind::Substitution {
9858                expr,
9859                pattern,
9860                replacement,
9861                flags,
9862                delim: _,
9863            } => {
9864                let val = self.eval_expr(expr)?;
9865                if val.is_iterator() {
9866                    let source = crate::map_stream::into_pull_iter(val);
9867                    let re = self.compile_regex(pattern, flags, line)?;
9868                    let global = flags.contains('g');
9869                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9870                        crate::map_stream::SubstStreamIterator::new(
9871                            source,
9872                            re,
9873                            normalize_replacement_backrefs(replacement),
9874                            global,
9875                        ),
9876                    )));
9877                }
9878                let s = val.to_string();
9879                self.regex_subst_execute(
9880                    s,
9881                    pattern,
9882                    replacement.as_str(),
9883                    flags.as_str(),
9884                    expr,
9885                    line,
9886                )
9887            }
9888            ExprKind::Transliterate {
9889                expr,
9890                from,
9891                to,
9892                flags,
9893                delim: _,
9894            } => {
9895                let val = self.eval_expr(expr)?;
9896                if val.is_iterator() {
9897                    let source = crate::map_stream::into_pull_iter(val);
9898                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9899                        crate::map_stream::TransliterateStreamIterator::new(
9900                            source, from, to, flags,
9901                        ),
9902                    )));
9903                }
9904                let s = val.to_string();
9905                self.regex_transliterate_execute(
9906                    s,
9907                    from.as_str(),
9908                    to.as_str(),
9909                    flags.as_str(),
9910                    expr,
9911                    line,
9912                )
9913            }
9914
9915            // List operations
9916            ExprKind::MapExpr {
9917                block,
9918                list,
9919                flatten_array_refs,
9920                stream,
9921            } => {
9922                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9923                if *stream {
9924                    let out =
9925                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
9926                    if ctx == WantarrayCtx::List {
9927                        return Ok(out);
9928                    }
9929                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9930                }
9931                let items = list_val.to_list();
9932                if items.len() == 1 {
9933                    if let Some(p) = items[0].as_pipeline() {
9934                        if *flatten_array_refs {
9935                            return Err(PerlError::runtime(
9936                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
9937                                line,
9938                            )
9939                            .into());
9940                        }
9941                        let sub = self.anon_coderef_from_block(block);
9942                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
9943                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
9944                    }
9945                }
9946                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
9947                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
9948                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
9949                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
9950                let mut result = Vec::new();
9951                for item in items {
9952                    self.scope.set_topic(item);
9953                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
9954                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
9955                }
9956                if ctx == WantarrayCtx::List {
9957                    Ok(PerlValue::array(result))
9958                } else {
9959                    Ok(PerlValue::integer(result.len() as i64))
9960                }
9961            }
9962            ExprKind::ForEachExpr { block, list } => {
9963                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9964                // Lazy: consume iterator one-at-a-time without materializing.
9965                if list_val.is_iterator() {
9966                    let iter = list_val.into_iterator();
9967                    let mut count = 0i64;
9968                    while let Some(item) = iter.next_item() {
9969                        count += 1;
9970                        self.scope.set_topic(item);
9971                        self.exec_block(block)?;
9972                    }
9973                    return Ok(PerlValue::integer(count));
9974                }
9975                let items = list_val.to_list();
9976                let count = items.len();
9977                for item in items {
9978                    self.scope.set_topic(item);
9979                    self.exec_block(block)?;
9980                }
9981                Ok(PerlValue::integer(count as i64))
9982            }
9983            ExprKind::MapExprComma {
9984                expr,
9985                list,
9986                flatten_array_refs,
9987                stream,
9988            } => {
9989                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9990                if *stream {
9991                    let out =
9992                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
9993                    if ctx == WantarrayCtx::List {
9994                        return Ok(out);
9995                    }
9996                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9997                }
9998                let items = list_val.to_list();
9999                let mut result = Vec::new();
10000                for item in items {
10001                    self.scope.set_topic(item.clone());
10002                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10003                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
10004                }
10005                if ctx == WantarrayCtx::List {
10006                    Ok(PerlValue::array(result))
10007                } else {
10008                    Ok(PerlValue::integer(result.len() as i64))
10009                }
10010            }
10011            ExprKind::GrepExpr {
10012                block,
10013                list,
10014                keyword,
10015            } => {
10016                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10017                if keyword.is_stream() {
10018                    let out = self.filter_stream_block_output(list_val, block, line)?;
10019                    if ctx == WantarrayCtx::List {
10020                        return Ok(out);
10021                    }
10022                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10023                }
10024                let items = list_val.to_list();
10025                if items.len() == 1 {
10026                    if let Some(p) = items[0].as_pipeline() {
10027                        let sub = self.anon_coderef_from_block(block);
10028                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
10029                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
10030                    }
10031                }
10032                let mut result = Vec::new();
10033                for item in items {
10034                    self.scope.set_topic(item.clone());
10035                    let val = self.exec_block(block)?;
10036                    // Bare regex in block → match against $_ (Perl: /pat/ in
10037                    // grep is `$_ =~ /pat/`, not a truthy regex object).
10038                    let keep = if let Some(re) = val.as_regex() {
10039                        re.is_match(&item.to_string())
10040                    } else {
10041                        val.is_true()
10042                    };
10043                    if keep {
10044                        result.push(item);
10045                    }
10046                }
10047                if ctx == WantarrayCtx::List {
10048                    Ok(PerlValue::array(result))
10049                } else {
10050                    Ok(PerlValue::integer(result.len() as i64))
10051                }
10052            }
10053            ExprKind::GrepExprComma {
10054                expr,
10055                list,
10056                keyword,
10057            } => {
10058                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10059                if keyword.is_stream() {
10060                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
10061                    if ctx == WantarrayCtx::List {
10062                        return Ok(out);
10063                    }
10064                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10065                }
10066                let items = list_val.to_list();
10067                let mut result = Vec::new();
10068                for item in items {
10069                    self.scope.set_topic(item.clone());
10070                    let val = self.eval_expr(expr)?;
10071                    let keep = if let Some(re) = val.as_regex() {
10072                        re.is_match(&item.to_string())
10073                    } else {
10074                        val.is_true()
10075                    };
10076                    if keep {
10077                        result.push(item);
10078                    }
10079                }
10080                if ctx == WantarrayCtx::List {
10081                    Ok(PerlValue::array(result))
10082                } else {
10083                    Ok(PerlValue::integer(result.len() as i64))
10084                }
10085            }
10086            ExprKind::SortExpr { cmp, list } => {
10087                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10088                let mut items = list_val.to_list();
10089                match cmp {
10090                    Some(SortComparator::Code(code_expr)) => {
10091                        let sub = self.eval_expr(code_expr)?;
10092                        let Some(sub) = sub.as_code_ref() else {
10093                            return Err(PerlError::runtime(
10094                                "sort: comparator must be a code reference",
10095                                line,
10096                            )
10097                            .into());
10098                        };
10099                        let sub = sub.clone();
10100                        items.sort_by(|a, b| {
10101                            let _ = self.scope.set_scalar("a", a.clone());
10102                            let _ = self.scope.set_scalar("b", b.clone());
10103                            let _ = self.scope.set_scalar("_0", a.clone());
10104                            let _ = self.scope.set_scalar("_1", b.clone());
10105                            match self.call_sub(&sub, vec![], ctx, line) {
10106                                Ok(v) => {
10107                                    let n = v.to_int();
10108                                    if n < 0 {
10109                                        Ordering::Less
10110                                    } else if n > 0 {
10111                                        Ordering::Greater
10112                                    } else {
10113                                        Ordering::Equal
10114                                    }
10115                                }
10116                                Err(_) => Ordering::Equal,
10117                            }
10118                        });
10119                    }
10120                    Some(SortComparator::Block(cmp_block)) => {
10121                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
10122                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
10123                        } else {
10124                            let cmp_block = cmp_block.clone();
10125                            items.sort_by(|a, b| {
10126                                let _ = self.scope.set_scalar("a", a.clone());
10127                                let _ = self.scope.set_scalar("b", b.clone());
10128                                let _ = self.scope.set_scalar("_0", a.clone());
10129                                let _ = self.scope.set_scalar("_1", b.clone());
10130                                match self.exec_block(&cmp_block) {
10131                                    Ok(v) => {
10132                                        let n = v.to_int();
10133                                        if n < 0 {
10134                                            Ordering::Less
10135                                        } else if n > 0 {
10136                                            Ordering::Greater
10137                                        } else {
10138                                            Ordering::Equal
10139                                        }
10140                                    }
10141                                    Err(_) => Ordering::Equal,
10142                                }
10143                            });
10144                        }
10145                    }
10146                    None => {
10147                        items.sort_by_key(|a| a.to_string());
10148                    }
10149                }
10150                Ok(PerlValue::array(items))
10151            }
10152            ExprKind::Rev(expr) => {
10153                // Eval in scalar context first to preserve set/hash/array ref types
10154                let val = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
10155                if val.is_iterator() {
10156                    return Ok(PerlValue::iterator(Arc::new(
10157                        crate::value::RevIterator::new(val.into_iterator()),
10158                    )));
10159                }
10160                if let Some(s) = crate::value::set_payload(&val) {
10161                    let mut out = crate::value::PerlSet::new();
10162                    for (k, v) in s.iter().rev() {
10163                        out.insert(k.clone(), v.clone());
10164                    }
10165                    return Ok(PerlValue::set(Arc::new(out)));
10166                }
10167                if let Some(ar) = val.as_array_ref() {
10168                    let items: Vec<_> = ar.read().iter().rev().cloned().collect();
10169                    return Ok(PerlValue::array_ref(Arc::new(parking_lot::RwLock::new(
10170                        items,
10171                    ))));
10172                }
10173                if let Some(hr) = val.as_hash_ref() {
10174                    let mut out: indexmap::IndexMap<String, PerlValue> = indexmap::IndexMap::new();
10175                    for (k, v) in hr.read().iter() {
10176                        out.insert(v.to_string(), PerlValue::string(k.clone()));
10177                    }
10178                    return Ok(PerlValue::hash_ref(Arc::new(parking_lot::RwLock::new(out))));
10179                }
10180                // Re-eval in list context for bare arrays/hashes
10181                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10182                if let Some(hm) = val.as_hash_map() {
10183                    let mut out: indexmap::IndexMap<String, PerlValue> = indexmap::IndexMap::new();
10184                    for (k, v) in hm.iter() {
10185                        out.insert(v.to_string(), PerlValue::string(k.clone()));
10186                    }
10187                    return Ok(PerlValue::hash(out));
10188                }
10189                if val.as_array_vec().is_some() {
10190                    let mut items = val.to_list();
10191                    items.reverse();
10192                    Ok(PerlValue::array(items))
10193                } else {
10194                    let items = val.to_list();
10195                    if items.len() > 1 {
10196                        let mut items = items;
10197                        items.reverse();
10198                        Ok(PerlValue::array(items))
10199                    } else {
10200                        let s = val.to_string();
10201                        Ok(PerlValue::string(s.chars().rev().collect()))
10202                    }
10203                }
10204            }
10205            ExprKind::ReverseExpr(list) => {
10206                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10207                match ctx {
10208                    WantarrayCtx::List => {
10209                        let mut items = val.to_list();
10210                        items.reverse();
10211                        Ok(PerlValue::array(items))
10212                    }
10213                    _ => {
10214                        let items = val.to_list();
10215                        let s: String = items.iter().map(|v| v.to_string()).collect();
10216                        Ok(PerlValue::string(s.chars().rev().collect()))
10217                    }
10218                }
10219            }
10220
10221            // ── Parallel operations (rayon-powered) ──
10222            ExprKind::ParLinesExpr {
10223                path,
10224                callback,
10225                progress,
10226            } => self.eval_par_lines_expr(
10227                path.as_ref(),
10228                callback.as_ref(),
10229                progress.as_deref(),
10230                line,
10231            ),
10232            ExprKind::ParWalkExpr {
10233                path,
10234                callback,
10235                progress,
10236            } => {
10237                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
10238            }
10239            ExprKind::PwatchExpr { path, callback } => {
10240                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
10241            }
10242            ExprKind::PMapExpr {
10243                block,
10244                list,
10245                progress,
10246                flat_outputs,
10247                on_cluster,
10248                stream,
10249            } => {
10250                let show_progress = progress
10251                    .as_ref()
10252                    .map(|p| self.eval_expr(p))
10253                    .transpose()?
10254                    .map(|v| v.is_true())
10255                    .unwrap_or(false);
10256                let list_val = self.eval_expr(list)?;
10257                if let Some(cluster_e) = on_cluster {
10258                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
10259                    return self.eval_pmap_remote(
10260                        cluster_val,
10261                        list_val,
10262                        show_progress,
10263                        block,
10264                        *flat_outputs,
10265                        line,
10266                    );
10267                }
10268                if *stream {
10269                    let source = crate::map_stream::into_pull_iter(list_val);
10270                    let sub = self.anon_coderef_from_block(block);
10271                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
10272                    return Ok(PerlValue::iterator(Arc::new(
10273                        crate::map_stream::PMapStreamIterator::new(
10274                            source,
10275                            sub,
10276                            self.subs.clone(),
10277                            capture,
10278                            atomic_arrays,
10279                            atomic_hashes,
10280                            *flat_outputs,
10281                        ),
10282                    )));
10283                }
10284                let items = list_val.to_list();
10285                let block = block.clone();
10286                let subs = self.subs.clone();
10287                let (scope_capture, atomic_arrays, atomic_hashes) =
10288                    self.scope.capture_with_atomics();
10289                let pmap_progress = PmapProgress::new(show_progress, items.len());
10290
10291                if *flat_outputs {
10292                    let mut indexed: Vec<(usize, Vec<PerlValue>)> = items
10293                        .into_par_iter()
10294                        .enumerate()
10295                        .map(|(i, item)| {
10296                            let mut local_interp = Interpreter::new();
10297                            local_interp.subs = subs.clone();
10298                            local_interp.scope.restore_capture(&scope_capture);
10299                            local_interp
10300                                .scope
10301                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10302                            local_interp.enable_parallel_guard();
10303                            local_interp.scope.set_topic(item);
10304                            let val = match local_interp.exec_block(&block) {
10305                                Ok(val) => val,
10306                                Err(_) => PerlValue::UNDEF,
10307                            };
10308                            let chunk = val.map_flatten_outputs(true);
10309                            pmap_progress.tick();
10310                            (i, chunk)
10311                        })
10312                        .collect();
10313                    pmap_progress.finish();
10314                    indexed.sort_by_key(|(i, _)| *i);
10315                    let results: Vec<PerlValue> =
10316                        indexed.into_iter().flat_map(|(_, v)| v).collect();
10317                    Ok(PerlValue::array(results))
10318                } else {
10319                    let results: Vec<PerlValue> = items
10320                        .into_par_iter()
10321                        .map(|item| {
10322                            let mut local_interp = Interpreter::new();
10323                            local_interp.subs = subs.clone();
10324                            local_interp.scope.restore_capture(&scope_capture);
10325                            local_interp
10326                                .scope
10327                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10328                            local_interp.enable_parallel_guard();
10329                            local_interp.scope.set_topic(item);
10330                            let val = match local_interp.exec_block(&block) {
10331                                Ok(val) => val,
10332                                Err(_) => PerlValue::UNDEF,
10333                            };
10334                            pmap_progress.tick();
10335                            val
10336                        })
10337                        .collect();
10338                    pmap_progress.finish();
10339                    Ok(PerlValue::array(results))
10340                }
10341            }
10342            ExprKind::PMapChunkedExpr {
10343                chunk_size,
10344                block,
10345                list,
10346                progress,
10347            } => {
10348                let show_progress = progress
10349                    .as_ref()
10350                    .map(|p| self.eval_expr(p))
10351                    .transpose()?
10352                    .map(|v| v.is_true())
10353                    .unwrap_or(false);
10354                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
10355                let list_val = self.eval_expr(list)?;
10356                let items = list_val.to_list();
10357                let block = block.clone();
10358                let subs = self.subs.clone();
10359                let (scope_capture, atomic_arrays, atomic_hashes) =
10360                    self.scope.capture_with_atomics();
10361
10362                let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = items
10363                    .chunks(chunk_n)
10364                    .enumerate()
10365                    .map(|(i, c)| (i, c.to_vec()))
10366                    .collect();
10367
10368                let n_chunks = indexed_chunks.len();
10369                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
10370
10371                let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
10372                    .into_par_iter()
10373                    .map(|(chunk_idx, chunk)| {
10374                        let mut local_interp = Interpreter::new();
10375                        local_interp.subs = subs.clone();
10376                        local_interp.scope.restore_capture(&scope_capture);
10377                        local_interp
10378                            .scope
10379                            .restore_atomics(&atomic_arrays, &atomic_hashes);
10380                        local_interp.enable_parallel_guard();
10381                        let mut out = Vec::with_capacity(chunk.len());
10382                        for item in chunk {
10383                            local_interp.scope.set_topic(item);
10384                            match local_interp.exec_block(&block) {
10385                                Ok(val) => out.push(val),
10386                                Err(_) => out.push(PerlValue::UNDEF),
10387                            }
10388                        }
10389                        pmap_progress.tick();
10390                        (chunk_idx, out)
10391                    })
10392                    .collect();
10393
10394                pmap_progress.finish();
10395                chunk_results.sort_by_key(|(i, _)| *i);
10396                let results: Vec<PerlValue> =
10397                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
10398                Ok(PerlValue::array(results))
10399            }
10400            ExprKind::PGrepExpr {
10401                block,
10402                list,
10403                progress,
10404                stream,
10405            } => {
10406                let show_progress = progress
10407                    .as_ref()
10408                    .map(|p| self.eval_expr(p))
10409                    .transpose()?
10410                    .map(|v| v.is_true())
10411                    .unwrap_or(false);
10412                let list_val = self.eval_expr(list)?;
10413                if *stream {
10414                    let source = crate::map_stream::into_pull_iter(list_val);
10415                    let sub = self.anon_coderef_from_block(block);
10416                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
10417                    return Ok(PerlValue::iterator(Arc::new(
10418                        crate::map_stream::PGrepStreamIterator::new(
10419                            source,
10420                            sub,
10421                            self.subs.clone(),
10422                            capture,
10423                            atomic_arrays,
10424                            atomic_hashes,
10425                        ),
10426                    )));
10427                }
10428                let items = list_val.to_list();
10429                let block = block.clone();
10430                let subs = self.subs.clone();
10431                let (scope_capture, atomic_arrays, atomic_hashes) =
10432                    self.scope.capture_with_atomics();
10433                let pmap_progress = PmapProgress::new(show_progress, items.len());
10434
10435                let results: Vec<PerlValue> = items
10436                    .into_par_iter()
10437                    .filter_map(|item| {
10438                        let mut local_interp = Interpreter::new();
10439                        local_interp.subs = subs.clone();
10440                        local_interp.scope.restore_capture(&scope_capture);
10441                        local_interp
10442                            .scope
10443                            .restore_atomics(&atomic_arrays, &atomic_hashes);
10444                        local_interp.enable_parallel_guard();
10445                        local_interp.scope.set_topic(item.clone());
10446                        let keep = match local_interp.exec_block(&block) {
10447                            Ok(val) => val.is_true(),
10448                            Err(_) => false,
10449                        };
10450                        pmap_progress.tick();
10451                        if keep {
10452                            Some(item)
10453                        } else {
10454                            None
10455                        }
10456                    })
10457                    .collect();
10458                pmap_progress.finish();
10459                Ok(PerlValue::array(results))
10460            }
10461            ExprKind::PForExpr {
10462                block,
10463                list,
10464                progress,
10465            } => {
10466                let show_progress = progress
10467                    .as_ref()
10468                    .map(|p| self.eval_expr(p))
10469                    .transpose()?
10470                    .map(|v| v.is_true())
10471                    .unwrap_or(false);
10472                let list_val = self.eval_expr(list)?;
10473                let items = list_val.to_list();
10474                let block = block.clone();
10475                let subs = self.subs.clone();
10476                let (scope_capture, atomic_arrays, atomic_hashes) =
10477                    self.scope.capture_with_atomics();
10478
10479                let pmap_progress = PmapProgress::new(show_progress, items.len());
10480                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10481                items.into_par_iter().for_each(|item| {
10482                    if first_err.lock().is_some() {
10483                        return;
10484                    }
10485                    let mut local_interp = Interpreter::new();
10486                    local_interp.subs = subs.clone();
10487                    local_interp.scope.restore_capture(&scope_capture);
10488                    local_interp
10489                        .scope
10490                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10491                    local_interp.enable_parallel_guard();
10492                    local_interp.scope.set_topic(item);
10493                    match local_interp.exec_block(&block) {
10494                        Ok(_) => {}
10495                        Err(e) => {
10496                            let stryke = match e {
10497                                FlowOrError::Error(stryke) => stryke,
10498                                FlowOrError::Flow(_) => PerlError::runtime(
10499                                    "return/last/next/redo not supported inside pfor block",
10500                                    line,
10501                                ),
10502                            };
10503                            let mut g = first_err.lock();
10504                            if g.is_none() {
10505                                *g = Some(stryke);
10506                            }
10507                        }
10508                    }
10509                    pmap_progress.tick();
10510                });
10511                pmap_progress.finish();
10512                if let Some(e) = first_err.lock().take() {
10513                    return Err(FlowOrError::Error(e));
10514                }
10515                Ok(PerlValue::UNDEF)
10516            }
10517            ExprKind::FanExpr {
10518                count,
10519                block,
10520                progress,
10521                capture,
10522            } => {
10523                let show_progress = progress
10524                    .as_ref()
10525                    .map(|p| self.eval_expr(p))
10526                    .transpose()?
10527                    .map(|v| v.is_true())
10528                    .unwrap_or(false);
10529                let n = match count {
10530                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
10531                    None => self.parallel_thread_count(),
10532                };
10533                let block = block.clone();
10534                let subs = self.subs.clone();
10535                let (scope_capture, atomic_arrays, atomic_hashes) =
10536                    self.scope.capture_with_atomics();
10537
10538                let fan_progress = FanProgress::new(show_progress, n);
10539                if *capture {
10540                    if n == 0 {
10541                        return Ok(PerlValue::array(Vec::new()));
10542                    }
10543                    let pairs: Vec<(usize, ExecResult)> = (0..n)
10544                        .into_par_iter()
10545                        .map(|i| {
10546                            fan_progress.start_worker(i);
10547                            let mut local_interp = Interpreter::new();
10548                            local_interp.subs = subs.clone();
10549                            local_interp.suppress_stdout = show_progress;
10550                            local_interp.scope.restore_capture(&scope_capture);
10551                            local_interp
10552                                .scope
10553                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10554                            local_interp.enable_parallel_guard();
10555                            local_interp.scope.set_topic(PerlValue::integer(i as i64));
10556                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10557                            let res = local_interp.exec_block(&block);
10558                            crate::parallel_trace::fan_worker_set_index(None);
10559                            fan_progress.finish_worker(i);
10560                            (i, res)
10561                        })
10562                        .collect();
10563                    fan_progress.finish();
10564                    let mut pairs = pairs;
10565                    pairs.sort_by_key(|(i, _)| *i);
10566                    let mut out = Vec::with_capacity(n);
10567                    for (_, r) in pairs {
10568                        match r {
10569                            Ok(v) => out.push(v),
10570                            Err(e) => return Err(e),
10571                        }
10572                    }
10573                    return Ok(PerlValue::array(out));
10574                }
10575                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10576                (0..n).into_par_iter().for_each(|i| {
10577                    if first_err.lock().is_some() {
10578                        return;
10579                    }
10580                    fan_progress.start_worker(i);
10581                    let mut local_interp = Interpreter::new();
10582                    local_interp.subs = subs.clone();
10583                    local_interp.suppress_stdout = show_progress;
10584                    local_interp.scope.restore_capture(&scope_capture);
10585                    local_interp
10586                        .scope
10587                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10588                    local_interp.enable_parallel_guard();
10589                    local_interp.scope.set_topic(PerlValue::integer(i as i64));
10590                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10591                    match local_interp.exec_block(&block) {
10592                        Ok(_) => {}
10593                        Err(e) => {
10594                            let stryke = match e {
10595                                FlowOrError::Error(stryke) => stryke,
10596                                FlowOrError::Flow(_) => PerlError::runtime(
10597                                    "return/last/next/redo not supported inside fan block",
10598                                    line,
10599                                ),
10600                            };
10601                            let mut g = first_err.lock();
10602                            if g.is_none() {
10603                                *g = Some(stryke);
10604                            }
10605                        }
10606                    }
10607                    crate::parallel_trace::fan_worker_set_index(None);
10608                    fan_progress.finish_worker(i);
10609                });
10610                fan_progress.finish();
10611                if let Some(e) = first_err.lock().take() {
10612                    return Err(FlowOrError::Error(e));
10613                }
10614                Ok(PerlValue::UNDEF)
10615            }
10616            ExprKind::RetryBlock {
10617                body,
10618                times,
10619                backoff,
10620            } => self.eval_retry_block(body, times, *backoff, line),
10621            ExprKind::RateLimitBlock {
10622                slot,
10623                max,
10624                window,
10625                body,
10626            } => self.eval_rate_limit_block(*slot, max, window, body, line),
10627            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
10628            ExprKind::GenBlock { body } => {
10629                let g = Arc::new(PerlGenerator {
10630                    block: body.clone(),
10631                    pc: Mutex::new(0),
10632                    scope_started: Mutex::new(false),
10633                    exhausted: Mutex::new(false),
10634                });
10635                Ok(PerlValue::generator(g))
10636            }
10637            ExprKind::Yield(e) => {
10638                if !self.in_generator {
10639                    return Err(PerlError::runtime("yield outside gen block", line).into());
10640                }
10641                let v = self.eval_expr(e)?;
10642                Err(FlowOrError::Flow(Flow::Yield(v)))
10643            }
10644            ExprKind::AlgebraicMatch { subject, arms } => {
10645                self.eval_algebraic_match(subject, arms, line)
10646            }
10647            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
10648                Ok(self.spawn_async_block(body))
10649            }
10650            ExprKind::Trace { body } => {
10651                crate::parallel_trace::trace_enter();
10652                let out = self.exec_block(body);
10653                crate::parallel_trace::trace_leave();
10654                out
10655            }
10656            ExprKind::Spinner { message, body } => {
10657                use std::io::Write as _;
10658                let msg = self.eval_expr(message)?.to_string();
10659                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
10660                let done2 = done.clone();
10661                let handle = std::thread::spawn(move || {
10662                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
10663                    let mut i = 0;
10664                    let stderr = std::io::stderr();
10665                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
10666                        {
10667                            let stdout = std::io::stdout();
10668                            let _stdout_lock = stdout.lock();
10669                            let mut err = stderr.lock();
10670                            let _ = write!(
10671                                err,
10672                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
10673                                frames[i % frames.len()],
10674                                msg
10675                            );
10676                            let _ = err.flush();
10677                        }
10678                        std::thread::sleep(std::time::Duration::from_millis(80));
10679                        i += 1;
10680                    }
10681                    let mut err = stderr.lock();
10682                    let _ = write!(err, "\r\x1b[2K");
10683                    let _ = err.flush();
10684                });
10685                let result = self.exec_block(body);
10686                done.store(true, std::sync::atomic::Ordering::Relaxed);
10687                let _ = handle.join();
10688                result
10689            }
10690            ExprKind::Timer { body } => {
10691                let start = std::time::Instant::now();
10692                self.exec_block(body)?;
10693                let ms = start.elapsed().as_secs_f64() * 1000.0;
10694                Ok(PerlValue::float(ms))
10695            }
10696            ExprKind::Bench { body, times } => {
10697                let n = self.eval_expr(times)?.to_int();
10698                if n < 0 {
10699                    return Err(PerlError::runtime(
10700                        "bench: iteration count must be non-negative",
10701                        line,
10702                    )
10703                    .into());
10704                }
10705                self.run_bench_block(body, n as usize, line)
10706            }
10707            ExprKind::Await(expr) => {
10708                let v = self.eval_expr(expr)?;
10709                if let Some(t) = v.as_async_task() {
10710                    t.await_result().map_err(FlowOrError::from)
10711                } else {
10712                    Ok(v)
10713                }
10714            }
10715            ExprKind::Slurp(e) => {
10716                let path = self.eval_expr(e)?.to_string();
10717                read_file_text_perl_compat(&path)
10718                    .map(PerlValue::string)
10719                    .map_err(|e| {
10720                        FlowOrError::Error(PerlError::runtime(format!("slurp: {}", e), line))
10721                    })
10722            }
10723            ExprKind::Capture(e) => {
10724                let cmd = self.eval_expr(e)?.to_string();
10725                let output = Command::new("sh")
10726                    .arg("-c")
10727                    .arg(&cmd)
10728                    .output()
10729                    .map_err(|e| {
10730                        FlowOrError::Error(PerlError::runtime(format!("capture: {}", e), line))
10731                    })?;
10732                self.record_child_exit_status(output.status);
10733                let exitcode = output.status.code().unwrap_or(-1) as i64;
10734                let stdout = decode_utf8_or_latin1(&output.stdout);
10735                let stderr = decode_utf8_or_latin1(&output.stderr);
10736                Ok(PerlValue::capture(Arc::new(CaptureResult {
10737                    stdout,
10738                    stderr,
10739                    exitcode,
10740                })))
10741            }
10742            ExprKind::Qx(e) => {
10743                let cmd = self.eval_expr(e)?.to_string();
10744                crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)
10745            }
10746            ExprKind::FetchUrl(e) => {
10747                let url = self.eval_expr(e)?.to_string();
10748                ureq::get(&url)
10749                    .call()
10750                    .map_err(|e| {
10751                        FlowOrError::Error(PerlError::runtime(format!("fetch_url: {}", e), line))
10752                    })
10753                    .and_then(|r| {
10754                        r.into_string().map(PerlValue::string).map_err(|e| {
10755                            FlowOrError::Error(PerlError::runtime(
10756                                format!("fetch_url: {}", e),
10757                                line,
10758                            ))
10759                        })
10760                    })
10761            }
10762            ExprKind::Pchannel { capacity } => {
10763                if let Some(c) = capacity {
10764                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
10765                    Ok(crate::pchannel::create_bounded_pair(n))
10766                } else {
10767                    Ok(crate::pchannel::create_pair())
10768                }
10769            }
10770            ExprKind::PSortExpr {
10771                cmp,
10772                list,
10773                progress,
10774            } => {
10775                let show_progress = progress
10776                    .as_ref()
10777                    .map(|p| self.eval_expr(p))
10778                    .transpose()?
10779                    .map(|v| v.is_true())
10780                    .unwrap_or(false);
10781                let list_val = self.eval_expr(list)?;
10782                let mut items = list_val.to_list();
10783                let pmap_progress = PmapProgress::new(show_progress, 2);
10784                pmap_progress.tick();
10785                if let Some(cmp_block) = cmp {
10786                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
10787                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
10788                    } else {
10789                        let cmp_block = cmp_block.clone();
10790                        let subs = self.subs.clone();
10791                        let scope_capture = self.scope.capture();
10792                        items.par_sort_by(|a, b| {
10793                            let mut local_interp = Interpreter::new();
10794                            local_interp.subs = subs.clone();
10795                            local_interp.scope.restore_capture(&scope_capture);
10796                            let _ = local_interp.scope.set_scalar("a", a.clone());
10797                            let _ = local_interp.scope.set_scalar("b", b.clone());
10798                            let _ = local_interp.scope.set_scalar("_0", a.clone());
10799                            let _ = local_interp.scope.set_scalar("_1", b.clone());
10800                            match local_interp.exec_block(&cmp_block) {
10801                                Ok(v) => {
10802                                    let n = v.to_int();
10803                                    if n < 0 {
10804                                        std::cmp::Ordering::Less
10805                                    } else if n > 0 {
10806                                        std::cmp::Ordering::Greater
10807                                    } else {
10808                                        std::cmp::Ordering::Equal
10809                                    }
10810                                }
10811                                Err(_) => std::cmp::Ordering::Equal,
10812                            }
10813                        });
10814                    }
10815                } else {
10816                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
10817                }
10818                pmap_progress.tick();
10819                pmap_progress.finish();
10820                Ok(PerlValue::array(items))
10821            }
10822
10823            ExprKind::ReduceExpr { block, list } => {
10824                let list_val = self.eval_expr(list)?;
10825                let items = list_val.to_list();
10826                if items.is_empty() {
10827                    return Ok(PerlValue::UNDEF);
10828                }
10829                if items.len() == 1 {
10830                    return Ok(items.into_iter().next().unwrap());
10831                }
10832                let block = block.clone();
10833                let subs = self.subs.clone();
10834                let scope_capture = self.scope.capture();
10835                let mut acc = items[0].clone();
10836                for b in items.into_iter().skip(1) {
10837                    let mut local_interp = Interpreter::new();
10838                    local_interp.subs = subs.clone();
10839                    local_interp.scope.restore_capture(&scope_capture);
10840                    let _ = local_interp.scope.set_scalar("a", acc.clone());
10841                    let _ = local_interp.scope.set_scalar("b", b.clone());
10842                    let _ = local_interp.scope.set_scalar("_0", acc);
10843                    let _ = local_interp.scope.set_scalar("_1", b);
10844                    acc = match local_interp.exec_block(&block) {
10845                        Ok(val) => val,
10846                        Err(_) => PerlValue::UNDEF,
10847                    };
10848                }
10849                Ok(acc)
10850            }
10851
10852            ExprKind::PReduceExpr {
10853                block,
10854                list,
10855                progress,
10856            } => {
10857                let show_progress = progress
10858                    .as_ref()
10859                    .map(|p| self.eval_expr(p))
10860                    .transpose()?
10861                    .map(|v| v.is_true())
10862                    .unwrap_or(false);
10863                let list_val = self.eval_expr(list)?;
10864                let items = list_val.to_list();
10865                if items.is_empty() {
10866                    return Ok(PerlValue::UNDEF);
10867                }
10868                if items.len() == 1 {
10869                    return Ok(items.into_iter().next().unwrap());
10870                }
10871                let block = block.clone();
10872                let subs = self.subs.clone();
10873                let scope_capture = self.scope.capture();
10874                let pmap_progress = PmapProgress::new(show_progress, items.len());
10875
10876                let result = items
10877                    .into_par_iter()
10878                    .map(|x| {
10879                        pmap_progress.tick();
10880                        x
10881                    })
10882                    .reduce_with(|a, b| {
10883                        let mut local_interp = Interpreter::new();
10884                        local_interp.subs = subs.clone();
10885                        local_interp.scope.restore_capture(&scope_capture);
10886                        let _ = local_interp.scope.set_scalar("a", a.clone());
10887                        let _ = local_interp.scope.set_scalar("b", b.clone());
10888                        let _ = local_interp.scope.set_scalar("_0", a);
10889                        let _ = local_interp.scope.set_scalar("_1", b);
10890                        match local_interp.exec_block(&block) {
10891                            Ok(val) => val,
10892                            Err(_) => PerlValue::UNDEF,
10893                        }
10894                    });
10895                pmap_progress.finish();
10896                Ok(result.unwrap_or(PerlValue::UNDEF))
10897            }
10898
10899            ExprKind::PReduceInitExpr {
10900                init,
10901                block,
10902                list,
10903                progress,
10904            } => {
10905                let show_progress = progress
10906                    .as_ref()
10907                    .map(|p| self.eval_expr(p))
10908                    .transpose()?
10909                    .map(|v| v.is_true())
10910                    .unwrap_or(false);
10911                let init_val = self.eval_expr(init)?;
10912                let list_val = self.eval_expr(list)?;
10913                let items = list_val.to_list();
10914                if items.is_empty() {
10915                    return Ok(init_val);
10916                }
10917                let block = block.clone();
10918                let subs = self.subs.clone();
10919                let scope_capture = self.scope.capture();
10920                let cap: &[(String, PerlValue)] = scope_capture.as_slice();
10921                if items.len() == 1 {
10922                    return Ok(fold_preduce_init_step(
10923                        &subs,
10924                        cap,
10925                        &block,
10926                        preduce_init_fold_identity(&init_val),
10927                        items.into_iter().next().unwrap(),
10928                    ));
10929                }
10930                let pmap_progress = PmapProgress::new(show_progress, items.len());
10931                let result = items
10932                    .into_par_iter()
10933                    .fold(
10934                        || preduce_init_fold_identity(&init_val),
10935                        |acc, item| {
10936                            pmap_progress.tick();
10937                            fold_preduce_init_step(&subs, cap, &block, acc, item)
10938                        },
10939                    )
10940                    .reduce(
10941                        || preduce_init_fold_identity(&init_val),
10942                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
10943                    );
10944                pmap_progress.finish();
10945                Ok(result)
10946            }
10947
10948            ExprKind::PMapReduceExpr {
10949                map_block,
10950                reduce_block,
10951                list,
10952                progress,
10953            } => {
10954                let show_progress = progress
10955                    .as_ref()
10956                    .map(|p| self.eval_expr(p))
10957                    .transpose()?
10958                    .map(|v| v.is_true())
10959                    .unwrap_or(false);
10960                let list_val = self.eval_expr(list)?;
10961                let items = list_val.to_list();
10962                if items.is_empty() {
10963                    return Ok(PerlValue::UNDEF);
10964                }
10965                let map_block = map_block.clone();
10966                let reduce_block = reduce_block.clone();
10967                let subs = self.subs.clone();
10968                let scope_capture = self.scope.capture();
10969                if items.len() == 1 {
10970                    let mut local_interp = Interpreter::new();
10971                    local_interp.subs = subs.clone();
10972                    local_interp.scope.restore_capture(&scope_capture);
10973                    local_interp.scope.set_topic(items[0].clone());
10974                    return match local_interp.exec_block_no_scope(&map_block) {
10975                        Ok(v) => Ok(v),
10976                        Err(_) => Ok(PerlValue::UNDEF),
10977                    };
10978                }
10979                let pmap_progress = PmapProgress::new(show_progress, items.len());
10980                let result = items
10981                    .into_par_iter()
10982                    .map(|item| {
10983                        let mut local_interp = Interpreter::new();
10984                        local_interp.subs = subs.clone();
10985                        local_interp.scope.restore_capture(&scope_capture);
10986                        local_interp.scope.set_topic(item);
10987                        let val = match local_interp.exec_block_no_scope(&map_block) {
10988                            Ok(val) => val,
10989                            Err(_) => PerlValue::UNDEF,
10990                        };
10991                        pmap_progress.tick();
10992                        val
10993                    })
10994                    .reduce_with(|a, b| {
10995                        let mut local_interp = Interpreter::new();
10996                        local_interp.subs = subs.clone();
10997                        local_interp.scope.restore_capture(&scope_capture);
10998                        let _ = local_interp.scope.set_scalar("a", a.clone());
10999                        let _ = local_interp.scope.set_scalar("b", b.clone());
11000                        let _ = local_interp.scope.set_scalar("_0", a);
11001                        let _ = local_interp.scope.set_scalar("_1", b);
11002                        match local_interp.exec_block_no_scope(&reduce_block) {
11003                            Ok(val) => val,
11004                            Err(_) => PerlValue::UNDEF,
11005                        }
11006                    });
11007                pmap_progress.finish();
11008                Ok(result.unwrap_or(PerlValue::UNDEF))
11009            }
11010
11011            ExprKind::PcacheExpr {
11012                block,
11013                list,
11014                progress,
11015            } => {
11016                let show_progress = progress
11017                    .as_ref()
11018                    .map(|p| self.eval_expr(p))
11019                    .transpose()?
11020                    .map(|v| v.is_true())
11021                    .unwrap_or(false);
11022                let list_val = self.eval_expr(list)?;
11023                let items = list_val.to_list();
11024                let block = block.clone();
11025                let subs = self.subs.clone();
11026                let scope_capture = self.scope.capture();
11027                let cache = &*crate::pcache::GLOBAL_PCACHE;
11028                let pmap_progress = PmapProgress::new(show_progress, items.len());
11029                let results: Vec<PerlValue> = items
11030                    .into_par_iter()
11031                    .map(|item| {
11032                        let k = crate::pcache::cache_key(&item);
11033                        if let Some(v) = cache.get(&k) {
11034                            pmap_progress.tick();
11035                            return v.clone();
11036                        }
11037                        let mut local_interp = Interpreter::new();
11038                        local_interp.subs = subs.clone();
11039                        local_interp.scope.restore_capture(&scope_capture);
11040                        local_interp.scope.set_topic(item.clone());
11041                        let val = match local_interp.exec_block_no_scope(&block) {
11042                            Ok(v) => v,
11043                            Err(_) => PerlValue::UNDEF,
11044                        };
11045                        cache.insert(k, val.clone());
11046                        pmap_progress.tick();
11047                        val
11048                    })
11049                    .collect();
11050                pmap_progress.finish();
11051                Ok(PerlValue::array(results))
11052            }
11053
11054            ExprKind::PselectExpr { receivers, timeout } => {
11055                let mut rx_vals = Vec::with_capacity(receivers.len());
11056                for r in receivers {
11057                    rx_vals.push(self.eval_expr(r)?);
11058                }
11059                let dur = if let Some(t) = timeout.as_ref() {
11060                    Some(std::time::Duration::from_secs_f64(
11061                        self.eval_expr(t)?.to_number().max(0.0),
11062                    ))
11063                } else {
11064                    None
11065                };
11066                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
11067                    &rx_vals, dur, line,
11068                )?)
11069            }
11070
11071            // Array ops
11072            ExprKind::Push { array, values } => {
11073                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
11074            }
11075            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
11076            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
11077            ExprKind::Unshift { array, values } => {
11078                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
11079            }
11080            ExprKind::Splice {
11081                array,
11082                offset,
11083                length,
11084                replacement,
11085            } => self.eval_splice_expr(
11086                array.as_ref(),
11087                offset.as_deref(),
11088                length.as_deref(),
11089                replacement.as_slice(),
11090                ctx,
11091                line,
11092            ),
11093            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
11094            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
11095            ExprKind::Keys(expr) => {
11096                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11097                let keys = Self::keys_from_value(val, line)?;
11098                if ctx == WantarrayCtx::List {
11099                    Ok(keys)
11100                } else {
11101                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
11102                    Ok(PerlValue::integer(n as i64))
11103                }
11104            }
11105            ExprKind::Values(expr) => {
11106                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11107                let vals = Self::values_from_value(val, line)?;
11108                if ctx == WantarrayCtx::List {
11109                    Ok(vals)
11110                } else {
11111                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
11112                    Ok(PerlValue::integer(n as i64))
11113                }
11114            }
11115            ExprKind::Each(_) => {
11116                // Simplified: returns empty list (full iterator state would need more work)
11117                Ok(PerlValue::array(vec![]))
11118            }
11119
11120            // String ops
11121            ExprKind::Chomp(expr) => {
11122                let val = self.eval_expr(expr)?;
11123                self.chomp_inplace_execute(val, expr)
11124            }
11125            ExprKind::Chop(expr) => {
11126                let val = self.eval_expr(expr)?;
11127                self.chop_inplace_execute(val, expr)
11128            }
11129            ExprKind::Length(expr) => {
11130                let val = self.eval_expr(expr)?;
11131                Ok(if let Some(a) = val.as_array_vec() {
11132                    PerlValue::integer(a.len() as i64)
11133                } else if let Some(h) = val.as_hash_map() {
11134                    PerlValue::integer(h.len() as i64)
11135                } else if let Some(b) = val.as_bytes_arc() {
11136                    PerlValue::integer(b.len() as i64)
11137                } else {
11138                    PerlValue::integer(val.to_string().len() as i64)
11139                })
11140            }
11141            ExprKind::Substr {
11142                string,
11143                offset,
11144                length,
11145                replacement,
11146            } => self.eval_substr_expr(
11147                string.as_ref(),
11148                offset.as_ref(),
11149                length.as_deref(),
11150                replacement.as_deref(),
11151                line,
11152            ),
11153            ExprKind::Index {
11154                string,
11155                substr,
11156                position,
11157            } => {
11158                let s = self.eval_expr(string)?.to_string();
11159                let sub = self.eval_expr(substr)?.to_string();
11160                let pos = if let Some(p) = position {
11161                    self.eval_expr(p)?.to_int() as usize
11162                } else {
11163                    0
11164                };
11165                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
11166                Ok(PerlValue::integer(result))
11167            }
11168            ExprKind::Rindex {
11169                string,
11170                substr,
11171                position,
11172            } => {
11173                let s = self.eval_expr(string)?.to_string();
11174                let sub = self.eval_expr(substr)?.to_string();
11175                let end = if let Some(p) = position {
11176                    self.eval_expr(p)?.to_int() as usize + sub.len()
11177                } else {
11178                    s.len()
11179                };
11180                let search = &s[..end.min(s.len())];
11181                let result = search.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
11182                Ok(PerlValue::integer(result))
11183            }
11184            ExprKind::Sprintf { format, args } => {
11185                let fmt = self.eval_expr(format)?.to_string();
11186                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
11187                // builtins into individual format arguments.
11188                let mut arg_vals = Vec::new();
11189                for a in args {
11190                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
11191                    if let Some(items) = v.as_array_vec() {
11192                        arg_vals.extend(items);
11193                    } else {
11194                        arg_vals.push(v);
11195                    }
11196                }
11197                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
11198                Ok(PerlValue::string(s))
11199            }
11200            ExprKind::JoinExpr { separator, list } => {
11201                let sep = self.eval_expr(separator)?.to_string();
11202                // Like Perl 5, arguments after the separator are evaluated in list context so
11203                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
11204                // expands `localtime` to nine fields.
11205                let items = if let ExprKind::List(exprs) = &list.kind {
11206                    let saved = self.wantarray_kind;
11207                    self.wantarray_kind = WantarrayCtx::List;
11208                    let mut vals = Vec::new();
11209                    for e in exprs {
11210                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
11211                        if let Some(items) = v.as_array_vec() {
11212                            vals.extend(items);
11213                        } else {
11214                            vals.push(v);
11215                        }
11216                    }
11217                    self.wantarray_kind = saved;
11218                    vals
11219                } else {
11220                    let saved = self.wantarray_kind;
11221                    self.wantarray_kind = WantarrayCtx::List;
11222                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11223                    self.wantarray_kind = saved;
11224                    if let Some(items) = v.as_array_vec() {
11225                        items
11226                    } else {
11227                        vec![v]
11228                    }
11229                };
11230                let mut strs = Vec::with_capacity(items.len());
11231                for v in &items {
11232                    strs.push(self.stringify_value(v.clone(), line)?);
11233                }
11234                Ok(PerlValue::string(strs.join(&sep)))
11235            }
11236            ExprKind::SplitExpr {
11237                pattern,
11238                string,
11239                limit,
11240            } => {
11241                let pat = self.eval_expr(pattern)?.to_string();
11242                let s = self.eval_expr(string)?.to_string();
11243                let lim = if let Some(l) = limit {
11244                    self.eval_expr(l)?.to_int() as usize
11245                } else {
11246                    0
11247                };
11248                let re = self.compile_regex(&pat, "", line)?;
11249                let parts: Vec<PerlValue> = if lim > 0 {
11250                    re.splitn_strings(&s, lim)
11251                        .into_iter()
11252                        .map(PerlValue::string)
11253                        .collect()
11254                } else {
11255                    re.split_strings(&s)
11256                        .into_iter()
11257                        .map(PerlValue::string)
11258                        .collect()
11259                };
11260                Ok(PerlValue::array(parts))
11261            }
11262
11263            // Numeric
11264            ExprKind::Abs(expr) => {
11265                let val = self.eval_expr(expr)?;
11266                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
11267                    return r;
11268                }
11269                Ok(PerlValue::float(val.to_number().abs()))
11270            }
11271            ExprKind::Int(expr) => {
11272                let val = self.eval_expr(expr)?;
11273                Ok(PerlValue::integer(val.to_number() as i64))
11274            }
11275            ExprKind::Sqrt(expr) => {
11276                let val = self.eval_expr(expr)?;
11277                Ok(PerlValue::float(val.to_number().sqrt()))
11278            }
11279            ExprKind::Sin(expr) => {
11280                let val = self.eval_expr(expr)?;
11281                Ok(PerlValue::float(val.to_number().sin()))
11282            }
11283            ExprKind::Cos(expr) => {
11284                let val = self.eval_expr(expr)?;
11285                Ok(PerlValue::float(val.to_number().cos()))
11286            }
11287            ExprKind::Atan2 { y, x } => {
11288                let yv = self.eval_expr(y)?.to_number();
11289                let xv = self.eval_expr(x)?.to_number();
11290                Ok(PerlValue::float(yv.atan2(xv)))
11291            }
11292            ExprKind::Exp(expr) => {
11293                let val = self.eval_expr(expr)?;
11294                Ok(PerlValue::float(val.to_number().exp()))
11295            }
11296            ExprKind::Log(expr) => {
11297                let val = self.eval_expr(expr)?;
11298                Ok(PerlValue::float(val.to_number().ln()))
11299            }
11300            ExprKind::Rand(upper) => {
11301                let u = match upper {
11302                    Some(e) => self.eval_expr(e)?.to_number(),
11303                    None => 1.0,
11304                };
11305                Ok(PerlValue::float(self.perl_rand(u)))
11306            }
11307            ExprKind::Srand(seed) => {
11308                let s = match seed {
11309                    Some(e) => Some(self.eval_expr(e)?.to_number()),
11310                    None => None,
11311                };
11312                Ok(PerlValue::integer(self.perl_srand(s)))
11313            }
11314            ExprKind::Hex(expr) => {
11315                let val = self.eval_expr(expr)?.to_string();
11316                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
11317                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
11318                Ok(PerlValue::integer(n))
11319            }
11320            ExprKind::Oct(expr) => {
11321                let val = self.eval_expr(expr)?.to_string();
11322                let s = val.trim();
11323                let n = if s.starts_with("0x") || s.starts_with("0X") {
11324                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
11325                } else if s.starts_with("0b") || s.starts_with("0B") {
11326                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
11327                } else if s.starts_with("0o") || s.starts_with("0O") {
11328                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
11329                } else {
11330                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
11331                };
11332                Ok(PerlValue::integer(n))
11333            }
11334
11335            // Case
11336            ExprKind::Lc(expr) => Ok(PerlValue::string(
11337                self.eval_expr(expr)?.to_string().to_lowercase(),
11338            )),
11339            ExprKind::Uc(expr) => Ok(PerlValue::string(
11340                self.eval_expr(expr)?.to_string().to_uppercase(),
11341            )),
11342            ExprKind::Lcfirst(expr) => {
11343                let s = self.eval_expr(expr)?.to_string();
11344                let mut chars = s.chars();
11345                let result = match chars.next() {
11346                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
11347                    None => String::new(),
11348                };
11349                Ok(PerlValue::string(result))
11350            }
11351            ExprKind::Ucfirst(expr) => {
11352                let s = self.eval_expr(expr)?.to_string();
11353                let mut chars = s.chars();
11354                let result = match chars.next() {
11355                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
11356                    None => String::new(),
11357                };
11358                Ok(PerlValue::string(result))
11359            }
11360            ExprKind::Fc(expr) => Ok(PerlValue::string(default_case_fold_str(
11361                &self.eval_expr(expr)?.to_string(),
11362            ))),
11363            ExprKind::Crypt { plaintext, salt } => {
11364                let p = self.eval_expr(plaintext)?.to_string();
11365                let sl = self.eval_expr(salt)?.to_string();
11366                Ok(PerlValue::string(perl_crypt(&p, &sl)))
11367            }
11368            ExprKind::Pos(e) => {
11369                let key = match e {
11370                    None => "_".to_string(),
11371                    Some(expr) => match &expr.kind {
11372                        ExprKind::ScalarVar(n) => n.clone(),
11373                        _ => self.eval_expr(expr)?.to_string(),
11374                    },
11375                };
11376                Ok(self
11377                    .regex_pos
11378                    .get(&key)
11379                    .copied()
11380                    .flatten()
11381                    .map(|p| PerlValue::integer(p as i64))
11382                    .unwrap_or(PerlValue::UNDEF))
11383            }
11384            ExprKind::Study(expr) => {
11385                let s = self.eval_expr(expr)?.to_string();
11386                Ok(Self::study_return_value(&s))
11387            }
11388
11389            // Type
11390            ExprKind::Defined(expr) => {
11391                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
11392                if let ExprKind::SubroutineRef(name) = &expr.kind {
11393                    let exists = self.resolve_sub_by_name(name).is_some();
11394                    return Ok(PerlValue::integer(if exists { 1 } else { 0 }));
11395                }
11396                let val = self.eval_expr(expr)?;
11397                Ok(PerlValue::integer(if val.is_undef() { 0 } else { 1 }))
11398            }
11399            ExprKind::Ref(expr) => {
11400                let val = self.eval_expr(expr)?;
11401                Ok(val.ref_type())
11402            }
11403            ExprKind::ScalarContext(expr) => {
11404                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
11405                Ok(v.scalar_context())
11406            }
11407
11408            // Char
11409            ExprKind::Chr(expr) => {
11410                let n = self.eval_expr(expr)?.to_int() as u32;
11411                Ok(PerlValue::string(
11412                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
11413                ))
11414            }
11415            ExprKind::Ord(expr) => {
11416                let s = self.eval_expr(expr)?.to_string();
11417                Ok(PerlValue::integer(
11418                    s.chars().next().map(|c| c as i64).unwrap_or(0),
11419                ))
11420            }
11421
11422            // I/O
11423            ExprKind::OpenMyHandle { .. } => Err(PerlError::runtime(
11424                "internal: `open my $fh` handle used outside open()",
11425                line,
11426            )
11427            .into()),
11428            ExprKind::Open { handle, mode, file } => {
11429                if let ExprKind::OpenMyHandle { name } = &handle.kind {
11430                    self.scope
11431                        .declare_scalar_frozen(name, PerlValue::UNDEF, false, None)?;
11432                    self.english_note_lexical_scalar(name);
11433                    let mode_s = self.eval_expr(mode)?.to_string();
11434                    let file_opt = if let Some(f) = file {
11435                        Some(self.eval_expr(f)?.to_string())
11436                    } else {
11437                        None
11438                    };
11439                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
11440                    self.scope.set_scalar(name, ret.clone())?;
11441                    return Ok(ret);
11442                }
11443                let handle_s = self.eval_expr(handle)?.to_string();
11444                let handle_name = self.resolve_io_handle_name(&handle_s);
11445                let mode_s = self.eval_expr(mode)?.to_string();
11446                let file_opt = if let Some(f) = file {
11447                    Some(self.eval_expr(f)?.to_string())
11448                } else {
11449                    None
11450                };
11451                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
11452                    .map_err(Into::into)
11453            }
11454            ExprKind::Close(expr) => {
11455                let s = self.eval_expr(expr)?.to_string();
11456                let name = self.resolve_io_handle_name(&s);
11457                self.close_builtin_execute(name).map_err(Into::into)
11458            }
11459            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
11460                self.readline_builtin_execute_list(handle.as_deref())
11461            } else {
11462                self.readline_builtin_execute(handle.as_deref())
11463            }
11464            .map_err(Into::into),
11465            ExprKind::Eof(expr) => match expr {
11466                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
11467                Some(e) => {
11468                    let name = self.eval_expr(e)?;
11469                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
11470                }
11471            },
11472
11473            ExprKind::Opendir { handle, path } => {
11474                let h = self.eval_expr(handle)?.to_string();
11475                let p = self.eval_expr(path)?.to_string();
11476                Ok(self.opendir_handle(&h, &p))
11477            }
11478            ExprKind::Readdir(e) => {
11479                let h = self.eval_expr(e)?.to_string();
11480                Ok(if ctx == WantarrayCtx::List {
11481                    self.readdir_handle_list(&h)
11482                } else {
11483                    self.readdir_handle(&h)
11484                })
11485            }
11486            ExprKind::Closedir(e) => {
11487                let h = self.eval_expr(e)?.to_string();
11488                Ok(self.closedir_handle(&h))
11489            }
11490            ExprKind::Rewinddir(e) => {
11491                let h = self.eval_expr(e)?.to_string();
11492                Ok(self.rewinddir_handle(&h))
11493            }
11494            ExprKind::Telldir(e) => {
11495                let h = self.eval_expr(e)?.to_string();
11496                Ok(self.telldir_handle(&h))
11497            }
11498            ExprKind::Seekdir { handle, position } => {
11499                let h = self.eval_expr(handle)?.to_string();
11500                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
11501                Ok(self.seekdir_handle(&h, pos))
11502            }
11503
11504            // File tests
11505            ExprKind::FileTest { op, expr } => {
11506                let path = self.eval_expr(expr)?.to_string();
11507                // -M, -A, -C return fractional days (float), not boolean
11508                if matches!(op, 'M' | 'A' | 'C') {
11509                    #[cfg(unix)]
11510                    {
11511                        return match crate::perl_fs::filetest_age_days(&path, *op) {
11512                            Some(days) => Ok(PerlValue::float(days)),
11513                            None => Ok(PerlValue::UNDEF),
11514                        };
11515                    }
11516                    #[cfg(not(unix))]
11517                    return Ok(PerlValue::UNDEF);
11518                }
11519                // -s returns file size (or undef on error)
11520                if *op == 's' {
11521                    return match std::fs::metadata(&path) {
11522                        Ok(m) => Ok(PerlValue::integer(m.len() as i64)),
11523                        Err(_) => Ok(PerlValue::UNDEF),
11524                    };
11525                }
11526                let result = match op {
11527                    'e' => std::path::Path::new(&path).exists(),
11528                    'f' => std::path::Path::new(&path).is_file(),
11529                    'd' => std::path::Path::new(&path).is_dir(),
11530                    'l' => std::path::Path::new(&path).is_symlink(),
11531                    #[cfg(unix)]
11532                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
11533                    #[cfg(not(unix))]
11534                    'r' => std::fs::metadata(&path).is_ok(),
11535                    #[cfg(unix)]
11536                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
11537                    #[cfg(not(unix))]
11538                    'w' => std::fs::metadata(&path).is_ok(),
11539                    #[cfg(unix)]
11540                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
11541                    #[cfg(not(unix))]
11542                    'x' => false,
11543                    #[cfg(unix)]
11544                    'o' => crate::perl_fs::filetest_owned_effective(&path),
11545                    #[cfg(not(unix))]
11546                    'o' => false,
11547                    #[cfg(unix)]
11548                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
11549                    #[cfg(not(unix))]
11550                    'R' => false,
11551                    #[cfg(unix)]
11552                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
11553                    #[cfg(not(unix))]
11554                    'W' => false,
11555                    #[cfg(unix)]
11556                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
11557                    #[cfg(not(unix))]
11558                    'X' => false,
11559                    #[cfg(unix)]
11560                    'O' => crate::perl_fs::filetest_owned_real(&path),
11561                    #[cfg(not(unix))]
11562                    'O' => false,
11563                    'z' => std::fs::metadata(&path)
11564                        .map(|m| m.len() == 0)
11565                        .unwrap_or(true),
11566                    't' => crate::perl_fs::filetest_is_tty(&path),
11567                    #[cfg(unix)]
11568                    'p' => crate::perl_fs::filetest_is_pipe(&path),
11569                    #[cfg(not(unix))]
11570                    'p' => false,
11571                    #[cfg(unix)]
11572                    'S' => crate::perl_fs::filetest_is_socket(&path),
11573                    #[cfg(not(unix))]
11574                    'S' => false,
11575                    #[cfg(unix)]
11576                    'b' => crate::perl_fs::filetest_is_block_device(&path),
11577                    #[cfg(not(unix))]
11578                    'b' => false,
11579                    #[cfg(unix)]
11580                    'c' => crate::perl_fs::filetest_is_char_device(&path),
11581                    #[cfg(not(unix))]
11582                    'c' => false,
11583                    #[cfg(unix)]
11584                    'u' => crate::perl_fs::filetest_is_setuid(&path),
11585                    #[cfg(not(unix))]
11586                    'u' => false,
11587                    #[cfg(unix)]
11588                    'g' => crate::perl_fs::filetest_is_setgid(&path),
11589                    #[cfg(not(unix))]
11590                    'g' => false,
11591                    #[cfg(unix)]
11592                    'k' => crate::perl_fs::filetest_is_sticky(&path),
11593                    #[cfg(not(unix))]
11594                    'k' => false,
11595                    'T' => crate::perl_fs::filetest_is_text(&path),
11596                    'B' => crate::perl_fs::filetest_is_binary(&path),
11597                    _ => false,
11598                };
11599                Ok(PerlValue::integer(if result { 1 } else { 0 }))
11600            }
11601
11602            // System
11603            ExprKind::System(args) => {
11604                let mut cmd_args = Vec::new();
11605                for a in args {
11606                    cmd_args.push(self.eval_expr(a)?.to_string());
11607                }
11608                if cmd_args.is_empty() {
11609                    return Ok(PerlValue::integer(-1));
11610                }
11611                let status = Command::new("sh")
11612                    .arg("-c")
11613                    .arg(cmd_args.join(" "))
11614                    .status();
11615                match status {
11616                    Ok(s) => {
11617                        self.record_child_exit_status(s);
11618                        Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
11619                    }
11620                    Err(e) => {
11621                        self.apply_io_error_to_errno(&e);
11622                        Ok(PerlValue::integer(-1))
11623                    }
11624                }
11625            }
11626            ExprKind::Exec(args) => {
11627                let mut cmd_args = Vec::new();
11628                for a in args {
11629                    cmd_args.push(self.eval_expr(a)?.to_string());
11630                }
11631                if cmd_args.is_empty() {
11632                    return Ok(PerlValue::integer(-1));
11633                }
11634                let status = Command::new("sh")
11635                    .arg("-c")
11636                    .arg(cmd_args.join(" "))
11637                    .status();
11638                match status {
11639                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
11640                    Err(e) => {
11641                        self.apply_io_error_to_errno(&e);
11642                        Ok(PerlValue::integer(-1))
11643                    }
11644                }
11645            }
11646            ExprKind::Eval(expr) => {
11647                self.eval_nesting += 1;
11648                let out = match &expr.kind {
11649                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
11650                        Ok(v) => {
11651                            self.clear_eval_error();
11652                            Ok(v)
11653                        }
11654                        Err(FlowOrError::Error(e)) => {
11655                            self.set_eval_error_from_perl_error(&e);
11656                            Ok(PerlValue::UNDEF)
11657                        }
11658                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
11659                    },
11660                    _ => {
11661                        let code = self.eval_expr(expr)?.to_string();
11662                        // Parse and execute the string as Perl code
11663                        match crate::parse_and_run_string(&code, self) {
11664                            Ok(v) => {
11665                                self.clear_eval_error();
11666                                Ok(v)
11667                            }
11668                            Err(e) => {
11669                                self.set_eval_error(e.to_string());
11670                                Ok(PerlValue::UNDEF)
11671                            }
11672                        }
11673                    }
11674                };
11675                self.eval_nesting -= 1;
11676                out
11677            }
11678            ExprKind::Do(expr) => match &expr.kind {
11679                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
11680                _ => {
11681                    let val = self.eval_expr(expr)?;
11682                    let filename = val.to_string();
11683                    match read_file_text_perl_compat(&filename) {
11684                        Ok(code) => {
11685                            let code = crate::data_section::strip_perl_end_marker(&code);
11686                            match crate::parse_and_run_string_in_file(code, self, &filename) {
11687                                Ok(v) => Ok(v),
11688                                Err(e) => {
11689                                    self.set_eval_error(e.to_string());
11690                                    Ok(PerlValue::UNDEF)
11691                                }
11692                            }
11693                        }
11694                        Err(e) => {
11695                            self.apply_io_error_to_errno(&e);
11696                            Ok(PerlValue::UNDEF)
11697                        }
11698                    }
11699                }
11700            },
11701            ExprKind::Require(expr) => {
11702                let spec = self.eval_expr(expr)?.to_string();
11703                self.require_execute(&spec, line)
11704                    .map_err(FlowOrError::Error)
11705            }
11706            ExprKind::Exit(code) => {
11707                let c = if let Some(e) = code {
11708                    self.eval_expr(e)?.to_int() as i32
11709                } else {
11710                    0
11711                };
11712                Err(PerlError::new(ErrorKind::Exit(c), "", line, &self.file).into())
11713            }
11714            ExprKind::Chdir(expr) => {
11715                let path = self.eval_expr(expr)?.to_string();
11716                match std::env::set_current_dir(&path) {
11717                    Ok(_) => Ok(PerlValue::integer(1)),
11718                    Err(e) => {
11719                        self.apply_io_error_to_errno(&e);
11720                        Ok(PerlValue::integer(0))
11721                    }
11722                }
11723            }
11724            ExprKind::Mkdir { path, mode: _ } => {
11725                let p = self.eval_expr(path)?.to_string();
11726                match std::fs::create_dir(&p) {
11727                    Ok(_) => Ok(PerlValue::integer(1)),
11728                    Err(e) => {
11729                        self.apply_io_error_to_errno(&e);
11730                        Ok(PerlValue::integer(0))
11731                    }
11732                }
11733            }
11734            ExprKind::Unlink(args) => {
11735                let mut count = 0i64;
11736                for a in args {
11737                    let path = self.eval_expr(a)?.to_string();
11738                    if std::fs::remove_file(&path).is_ok() {
11739                        count += 1;
11740                    }
11741                }
11742                Ok(PerlValue::integer(count))
11743            }
11744            ExprKind::Rename { old, new } => {
11745                let o = self.eval_expr(old)?.to_string();
11746                let n = self.eval_expr(new)?.to_string();
11747                Ok(crate::perl_fs::rename_paths(&o, &n))
11748            }
11749            ExprKind::Chmod(args) => {
11750                let mode = self.eval_expr(&args[0])?.to_int();
11751                let mut paths = Vec::new();
11752                for a in &args[1..] {
11753                    paths.push(self.eval_expr(a)?.to_string());
11754                }
11755                Ok(PerlValue::integer(crate::perl_fs::chmod_paths(
11756                    &paths, mode,
11757                )))
11758            }
11759            ExprKind::Chown(args) => {
11760                let uid = self.eval_expr(&args[0])?.to_int();
11761                let gid = self.eval_expr(&args[1])?.to_int();
11762                let mut paths = Vec::new();
11763                for a in &args[2..] {
11764                    paths.push(self.eval_expr(a)?.to_string());
11765                }
11766                Ok(PerlValue::integer(crate::perl_fs::chown_paths(
11767                    &paths, uid, gid,
11768                )))
11769            }
11770            ExprKind::Stat(e) => {
11771                let path = self.eval_expr(e)?.to_string();
11772                Ok(crate::perl_fs::stat_path(&path, false))
11773            }
11774            ExprKind::Lstat(e) => {
11775                let path = self.eval_expr(e)?.to_string();
11776                Ok(crate::perl_fs::stat_path(&path, true))
11777            }
11778            ExprKind::Link { old, new } => {
11779                let o = self.eval_expr(old)?.to_string();
11780                let n = self.eval_expr(new)?.to_string();
11781                Ok(crate::perl_fs::link_hard(&o, &n))
11782            }
11783            ExprKind::Symlink { old, new } => {
11784                let o = self.eval_expr(old)?.to_string();
11785                let n = self.eval_expr(new)?.to_string();
11786                Ok(crate::perl_fs::link_sym(&o, &n))
11787            }
11788            ExprKind::Readlink(e) => {
11789                let path = self.eval_expr(e)?.to_string();
11790                Ok(crate::perl_fs::read_link(&path))
11791            }
11792            ExprKind::Files(args) => {
11793                let dir = if args.is_empty() {
11794                    ".".to_string()
11795                } else {
11796                    self.eval_expr(&args[0])?.to_string()
11797                };
11798                Ok(crate::perl_fs::list_files(&dir))
11799            }
11800            ExprKind::Filesf(args) => {
11801                let dir = if args.is_empty() {
11802                    ".".to_string()
11803                } else {
11804                    self.eval_expr(&args[0])?.to_string()
11805                };
11806                Ok(crate::perl_fs::list_filesf(&dir))
11807            }
11808            ExprKind::FilesfRecursive(args) => {
11809                let dir = if args.is_empty() {
11810                    ".".to_string()
11811                } else {
11812                    self.eval_expr(&args[0])?.to_string()
11813                };
11814                Ok(PerlValue::iterator(Arc::new(
11815                    crate::value::FsWalkIterator::new(&dir, true),
11816                )))
11817            }
11818            ExprKind::Dirs(args) => {
11819                let dir = if args.is_empty() {
11820                    ".".to_string()
11821                } else {
11822                    self.eval_expr(&args[0])?.to_string()
11823                };
11824                Ok(crate::perl_fs::list_dirs(&dir))
11825            }
11826            ExprKind::DirsRecursive(args) => {
11827                let dir = if args.is_empty() {
11828                    ".".to_string()
11829                } else {
11830                    self.eval_expr(&args[0])?.to_string()
11831                };
11832                Ok(PerlValue::iterator(Arc::new(
11833                    crate::value::FsWalkIterator::new(&dir, false),
11834                )))
11835            }
11836            ExprKind::SymLinks(args) => {
11837                let dir = if args.is_empty() {
11838                    ".".to_string()
11839                } else {
11840                    self.eval_expr(&args[0])?.to_string()
11841                };
11842                Ok(crate::perl_fs::list_sym_links(&dir))
11843            }
11844            ExprKind::Sockets(args) => {
11845                let dir = if args.is_empty() {
11846                    ".".to_string()
11847                } else {
11848                    self.eval_expr(&args[0])?.to_string()
11849                };
11850                Ok(crate::perl_fs::list_sockets(&dir))
11851            }
11852            ExprKind::Pipes(args) => {
11853                let dir = if args.is_empty() {
11854                    ".".to_string()
11855                } else {
11856                    self.eval_expr(&args[0])?.to_string()
11857                };
11858                Ok(crate::perl_fs::list_pipes(&dir))
11859            }
11860            ExprKind::BlockDevices(args) => {
11861                let dir = if args.is_empty() {
11862                    ".".to_string()
11863                } else {
11864                    self.eval_expr(&args[0])?.to_string()
11865                };
11866                Ok(crate::perl_fs::list_block_devices(&dir))
11867            }
11868            ExprKind::CharDevices(args) => {
11869                let dir = if args.is_empty() {
11870                    ".".to_string()
11871                } else {
11872                    self.eval_expr(&args[0])?.to_string()
11873                };
11874                Ok(crate::perl_fs::list_char_devices(&dir))
11875            }
11876            ExprKind::Executables(args) => {
11877                let dir = if args.is_empty() {
11878                    ".".to_string()
11879                } else {
11880                    self.eval_expr(&args[0])?.to_string()
11881                };
11882                Ok(crate::perl_fs::list_executables(&dir))
11883            }
11884            ExprKind::Glob(args) => {
11885                let mut pats = Vec::new();
11886                for a in args {
11887                    pats.push(self.eval_expr(a)?.to_string());
11888                }
11889                Ok(crate::perl_fs::glob_patterns(&pats))
11890            }
11891            ExprKind::GlobPar { args, progress } => {
11892                let mut pats = Vec::new();
11893                for a in args {
11894                    pats.push(self.eval_expr(a)?.to_string());
11895                }
11896                let show_progress = progress
11897                    .as_ref()
11898                    .map(|p| self.eval_expr(p))
11899                    .transpose()?
11900                    .map(|v| v.is_true())
11901                    .unwrap_or(false);
11902                if show_progress {
11903                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
11904                } else {
11905                    Ok(crate::perl_fs::glob_par_patterns(&pats))
11906                }
11907            }
11908            ExprKind::ParSed { args, progress } => {
11909                let has_progress = progress.is_some();
11910                let mut vals: Vec<PerlValue> = Vec::new();
11911                for a in args {
11912                    vals.push(self.eval_expr(a)?);
11913                }
11914                if let Some(p) = progress {
11915                    vals.push(self.eval_expr(p.as_ref())?);
11916                }
11917                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
11918            }
11919            ExprKind::Bless { ref_expr, class } => {
11920                let val = self.eval_expr(ref_expr)?;
11921                let class_name = if let Some(c) = class {
11922                    self.eval_expr(c)?.to_string()
11923                } else {
11924                    self.scope.get_scalar("__PACKAGE__").to_string()
11925                };
11926                Ok(PerlValue::blessed(Arc::new(
11927                    crate::value::BlessedRef::new_blessed(class_name, val),
11928                )))
11929            }
11930            ExprKind::Caller(_) => {
11931                // Simplified: return package, file, line
11932                Ok(PerlValue::array(vec![
11933                    PerlValue::string("main".into()),
11934                    PerlValue::string(self.file.clone()),
11935                    PerlValue::integer(line as i64),
11936                ]))
11937            }
11938            ExprKind::Wantarray => Ok(match self.wantarray_kind {
11939                WantarrayCtx::Void => PerlValue::UNDEF,
11940                WantarrayCtx::Scalar => PerlValue::integer(0),
11941                WantarrayCtx::List => PerlValue::integer(1),
11942            }),
11943
11944            ExprKind::List(exprs) => {
11945                // In scalar context, the comma operator evaluates to the last element.
11946                if ctx == WantarrayCtx::Scalar {
11947                    if let Some(last) = exprs.last() {
11948                        // Evaluate earlier expressions for side effects
11949                        for e in &exprs[..exprs.len() - 1] {
11950                            self.eval_expr(e)?;
11951                        }
11952                        return self.eval_expr(last);
11953                    } else {
11954                        return Ok(PerlValue::UNDEF);
11955                    }
11956                }
11957                let mut vals = Vec::new();
11958                for e in exprs {
11959                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
11960                    if let Some(items) = v.as_array_vec() {
11961                        vals.extend(items);
11962                    } else {
11963                        vals.push(v);
11964                    }
11965                }
11966                if vals.len() == 1 {
11967                    Ok(vals.pop().unwrap())
11968                } else {
11969                    Ok(PerlValue::array(vals))
11970                }
11971            }
11972
11973            // Postfix modifiers
11974            ExprKind::PostfixIf { expr, condition } => {
11975                if self.eval_postfix_condition(condition)? {
11976                    self.eval_expr(expr)
11977                } else {
11978                    Ok(PerlValue::UNDEF)
11979                }
11980            }
11981            ExprKind::PostfixUnless { expr, condition } => {
11982                if !self.eval_postfix_condition(condition)? {
11983                    self.eval_expr(expr)
11984                } else {
11985                    Ok(PerlValue::UNDEF)
11986                }
11987            }
11988            ExprKind::PostfixWhile { expr, condition } => {
11989                // `do { ... } while (COND)` — body runs before the first condition check.
11990                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
11991                let is_do_block = matches!(
11992                    &expr.kind,
11993                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
11994                );
11995                let mut last = PerlValue::UNDEF;
11996                if is_do_block {
11997                    loop {
11998                        last = self.eval_expr(expr)?;
11999                        if !self.eval_postfix_condition(condition)? {
12000                            break;
12001                        }
12002                    }
12003                } else {
12004                    loop {
12005                        if !self.eval_postfix_condition(condition)? {
12006                            break;
12007                        }
12008                        last = self.eval_expr(expr)?;
12009                    }
12010                }
12011                Ok(last)
12012            }
12013            ExprKind::PostfixUntil { expr, condition } => {
12014                let is_do_block = matches!(
12015                    &expr.kind,
12016                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
12017                );
12018                let mut last = PerlValue::UNDEF;
12019                if is_do_block {
12020                    loop {
12021                        last = self.eval_expr(expr)?;
12022                        if self.eval_postfix_condition(condition)? {
12023                            break;
12024                        }
12025                    }
12026                } else {
12027                    loop {
12028                        if self.eval_postfix_condition(condition)? {
12029                            break;
12030                        }
12031                        last = self.eval_expr(expr)?;
12032                    }
12033                }
12034                Ok(last)
12035            }
12036            ExprKind::PostfixForeach { expr, list } => {
12037                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
12038                let mut last = PerlValue::UNDEF;
12039                for item in items {
12040                    self.scope.set_topic(item);
12041                    last = self.eval_expr(expr)?;
12042                }
12043                Ok(last)
12044            }
12045        }
12046    }
12047
12048    // ── Helpers ──
12049
12050    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
12051        match op {
12052            BinOp::Add => Some("+"),
12053            BinOp::Sub => Some("-"),
12054            BinOp::Mul => Some("*"),
12055            BinOp::Div => Some("/"),
12056            BinOp::Mod => Some("%"),
12057            BinOp::Pow => Some("**"),
12058            BinOp::Concat => Some("."),
12059            BinOp::StrEq => Some("eq"),
12060            BinOp::NumEq => Some("=="),
12061            BinOp::StrNe => Some("ne"),
12062            BinOp::NumNe => Some("!="),
12063            BinOp::StrLt => Some("lt"),
12064            BinOp::StrGt => Some("gt"),
12065            BinOp::StrLe => Some("le"),
12066            BinOp::StrGe => Some("ge"),
12067            BinOp::NumLt => Some("<"),
12068            BinOp::NumGt => Some(">"),
12069            BinOp::NumLe => Some("<="),
12070            BinOp::NumGe => Some(">="),
12071            BinOp::Spaceship => Some("<=>"),
12072            BinOp::StrCmp => Some("cmp"),
12073            _ => None,
12074        }
12075    }
12076
12077    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
12078    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
12079        map.get("").or_else(|| map.get("\"\""))
12080    }
12081
12082    /// String context for blessed objects with `overload '""'`.
12083    pub(crate) fn stringify_value(
12084        &mut self,
12085        v: PerlValue,
12086        line: usize,
12087    ) -> Result<String, FlowOrError> {
12088        if let Some(r) = self.try_overload_stringify(&v, line) {
12089            let pv = r?;
12090            return Ok(pv.to_string());
12091        }
12092        Ok(v.to_string())
12093    }
12094
12095    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
12096    pub(crate) fn perl_sprintf_stringify(
12097        &mut self,
12098        fmt: &str,
12099        args: &[PerlValue],
12100        line: usize,
12101    ) -> Result<String, FlowOrError> {
12102        perl_sprintf_format_with(fmt, args, |v| self.stringify_value(v.clone(), line))
12103    }
12104
12105    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
12106    pub(crate) fn render_format_template(
12107        &mut self,
12108        tmpl: &crate::format::FormatTemplate,
12109        line: usize,
12110    ) -> Result<String, FlowOrError> {
12111        use crate::format::{FormatRecord, PictureSegment};
12112        let mut buf = String::new();
12113        for rec in &tmpl.records {
12114            match rec {
12115                FormatRecord::Literal(s) => {
12116                    buf.push_str(s);
12117                    buf.push('\n');
12118                }
12119                FormatRecord::Picture { segments, exprs } => {
12120                    let mut vals: Vec<String> = Vec::new();
12121                    for e in exprs {
12122                        let v = self.eval_expr(e)?;
12123                        vals.push(self.stringify_value(v, line)?);
12124                    }
12125                    let mut vi = 0usize;
12126                    let mut line_out = String::new();
12127                    for seg in segments {
12128                        match seg {
12129                            PictureSegment::Literal(t) => line_out.push_str(t),
12130                            PictureSegment::Field {
12131                                width,
12132                                align,
12133                                kind: _,
12134                            } => {
12135                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
12136                                vi += 1;
12137                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
12138                            }
12139                        }
12140                    }
12141                    buf.push_str(line_out.trim_end());
12142                    buf.push('\n');
12143                }
12144            }
12145        }
12146        Ok(buf)
12147    }
12148
12149    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
12150    pub(crate) fn resolve_write_output_handle(
12151        &self,
12152        v: &PerlValue,
12153        line: usize,
12154    ) -> PerlResult<String> {
12155        if let Some(n) = v.as_io_handle_name() {
12156            let n = self.resolve_io_handle_name(&n);
12157            if self.is_bound_handle(&n) {
12158                return Ok(n);
12159            }
12160        }
12161        if let Some(s) = v.as_str() {
12162            if self.is_bound_handle(&s) {
12163                return Ok(self.resolve_io_handle_name(&s));
12164            }
12165        }
12166        let s = v.to_string();
12167        if self.is_bound_handle(&s) {
12168            return Ok(self.resolve_io_handle_name(&s));
12169        }
12170        Err(PerlError::runtime(
12171            format!("write: invalid or unopened filehandle {}", s),
12172            line,
12173        ))
12174    }
12175
12176    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
12177    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
12178    /// that handle like `write FH`.
12179    pub(crate) fn write_format_execute(
12180        &mut self,
12181        args: &[PerlValue],
12182        line: usize,
12183    ) -> PerlResult<PerlValue> {
12184        let handle_name = match args.len() {
12185            0 => self.default_print_handle.clone(),
12186            1 => self.resolve_write_output_handle(&args[0], line)?,
12187            _ => {
12188                return Err(PerlError::runtime("write: too many arguments", line));
12189            }
12190        };
12191        let pkg = self.current_package();
12192        let mut fmt_name = self.scope.get_scalar("~").to_string();
12193        if fmt_name.is_empty() {
12194            fmt_name = "STDOUT".to_string();
12195        }
12196        let key = format!("{}::{}", pkg, fmt_name);
12197        let tmpl = self
12198            .format_templates
12199            .get(&key)
12200            .map(Arc::clone)
12201            .ok_or_else(|| {
12202                PerlError::runtime(
12203                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
12204                    line,
12205                )
12206            })?;
12207        let out = self
12208            .render_format_template(&tmpl, line)
12209            .map_err(|e| match e {
12210                FlowOrError::Error(e) => e,
12211                FlowOrError::Flow(_) => PerlError::runtime("write: unexpected control flow", line),
12212            })?;
12213        self.write_formatted_print(handle_name.as_str(), &out, line)?;
12214        Ok(PerlValue::integer(1))
12215    }
12216
12217    pub(crate) fn try_overload_stringify(
12218        &mut self,
12219        v: &PerlValue,
12220        line: usize,
12221    ) -> Option<ExecResult> {
12222        // Native class instance: look for method named '""' or 'stringify'
12223        if let Some(c) = v.as_class_inst() {
12224            let method_name = c
12225                .def
12226                .method("stringify")
12227                .or_else(|| c.def.method("\"\""))
12228                .filter(|m| m.body.is_some())?;
12229            let body = method_name.body.clone().unwrap();
12230            let params = method_name.params.clone();
12231            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
12232        }
12233        let br = v.as_blessed_ref()?;
12234        let class = br.class.clone();
12235        let map = self.overload_table.get(&class)?;
12236        let sub_short = Self::overload_stringify_method(map)?;
12237        let fq = format!("{}::{}", class, sub_short);
12238        let sub = self.subs.get(&fq)?.clone();
12239        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
12240    }
12241
12242    /// Map overload operator key to native class method name.
12243    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
12244        match key {
12245            "+" => Some("op_add"),
12246            "-" => Some("op_sub"),
12247            "*" => Some("op_mul"),
12248            "/" => Some("op_div"),
12249            "%" => Some("op_mod"),
12250            "**" => Some("op_pow"),
12251            "." => Some("op_concat"),
12252            "==" => Some("op_eq"),
12253            "!=" => Some("op_ne"),
12254            "<" => Some("op_lt"),
12255            ">" => Some("op_gt"),
12256            "<=" => Some("op_le"),
12257            ">=" => Some("op_ge"),
12258            "<=>" => Some("op_spaceship"),
12259            "eq" => Some("op_str_eq"),
12260            "ne" => Some("op_str_ne"),
12261            "lt" => Some("op_str_lt"),
12262            "gt" => Some("op_str_gt"),
12263            "le" => Some("op_str_le"),
12264            "ge" => Some("op_str_ge"),
12265            "cmp" => Some("op_cmp"),
12266            _ => None,
12267        }
12268    }
12269
12270    pub(crate) fn try_overload_binop(
12271        &mut self,
12272        op: BinOp,
12273        lv: &PerlValue,
12274        rv: &PerlValue,
12275        line: usize,
12276    ) -> Option<ExecResult> {
12277        let key = Self::overload_key_for_binop(op)?;
12278        // Native class instance overloading
12279        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
12280            (Some(c.def.clone()), lv.clone(), rv.clone())
12281        } else if let Some(c) = rv.as_class_inst() {
12282            (Some(c.def.clone()), rv.clone(), lv.clone())
12283        } else {
12284            (None, lv.clone(), rv.clone())
12285        };
12286        if let Some(ref def) = ci_def {
12287            if let Some(method_name) = Self::overload_method_name_for_key(key) {
12288                if let Some((m, _)) = self.find_class_method(def, method_name) {
12289                    if let Some(ref body) = m.body {
12290                        let params = m.params.clone();
12291                        return Some(self.call_class_method(
12292                            body,
12293                            &params,
12294                            vec![invocant, other],
12295                            line,
12296                        ));
12297                    }
12298                }
12299            }
12300        }
12301        // Blessed ref overloading (existing path)
12302        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
12303            (br.class.clone(), lv.clone(), rv.clone())
12304        } else if let Some(br) = rv.as_blessed_ref() {
12305            (br.class.clone(), rv.clone(), lv.clone())
12306        } else {
12307            return None;
12308        };
12309        let map = self.overload_table.get(&class)?;
12310        let sub_short = if let Some(s) = map.get(key) {
12311            s.clone()
12312        } else if let Some(nm) = map.get("nomethod") {
12313            let fq = format!("{}::{}", class, nm);
12314            let sub = self.subs.get(&fq)?.clone();
12315            return Some(self.call_sub(
12316                &sub,
12317                vec![invocant, other, PerlValue::string(key.to_string())],
12318                WantarrayCtx::Scalar,
12319                line,
12320            ));
12321        } else {
12322            return None;
12323        };
12324        let fq = format!("{}::{}", class, sub_short);
12325        let sub = self.subs.get(&fq)?.clone();
12326        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
12327    }
12328
12329    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
12330    pub(crate) fn try_overload_unary_dispatch(
12331        &mut self,
12332        op_key: &str,
12333        val: &PerlValue,
12334        line: usize,
12335    ) -> Option<ExecResult> {
12336        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
12337        if let Some(c) = val.as_class_inst() {
12338            let method_name = match op_key {
12339                "neg" => "op_neg",
12340                "bool" => "op_bool",
12341                "abs" => "op_abs",
12342                "0+" => "op_numify",
12343                _ => return None,
12344            };
12345            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
12346                if let Some(ref body) = m.body {
12347                    let params = m.params.clone();
12348                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
12349                }
12350            }
12351            return None;
12352        }
12353        // Blessed ref path
12354        let br = val.as_blessed_ref()?;
12355        let class = br.class.clone();
12356        let map = self.overload_table.get(&class)?;
12357        if let Some(s) = map.get(op_key) {
12358            let fq = format!("{}::{}", class, s);
12359            let sub = self.subs.get(&fq)?.clone();
12360            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
12361        }
12362        if let Some(nm) = map.get("nomethod") {
12363            let fq = format!("{}::{}", class, nm);
12364            let sub = self.subs.get(&fq)?.clone();
12365            return Some(self.call_sub(
12366                &sub,
12367                vec![val.clone(), PerlValue::string(op_key.to_string())],
12368                WantarrayCtx::Scalar,
12369                line,
12370            ));
12371        }
12372        None
12373    }
12374
12375    #[inline]
12376    fn eval_binop(
12377        &mut self,
12378        op: BinOp,
12379        lv: &PerlValue,
12380        rv: &PerlValue,
12381        _line: usize,
12382    ) -> ExecResult {
12383        Ok(match op {
12384            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
12385            // Perl `+` is numeric addition only; string concatenation is `.`.
12386            BinOp::Add => {
12387                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12388                    PerlValue::integer(a.wrapping_add(b))
12389                } else {
12390                    PerlValue::float(lv.to_number() + rv.to_number())
12391                }
12392            }
12393            BinOp::Sub => {
12394                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12395                    PerlValue::integer(a.wrapping_sub(b))
12396                } else {
12397                    PerlValue::float(lv.to_number() - rv.to_number())
12398                }
12399            }
12400            BinOp::Mul => {
12401                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12402                    PerlValue::integer(a.wrapping_mul(b))
12403                } else {
12404                    PerlValue::float(lv.to_number() * rv.to_number())
12405                }
12406            }
12407            BinOp::Div => {
12408                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12409                    if b == 0 {
12410                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
12411                    }
12412                    if a % b == 0 {
12413                        PerlValue::integer(a / b)
12414                    } else {
12415                        PerlValue::float(a as f64 / b as f64)
12416                    }
12417                } else {
12418                    let d = rv.to_number();
12419                    if d == 0.0 {
12420                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
12421                    }
12422                    PerlValue::float(lv.to_number() / d)
12423                }
12424            }
12425            BinOp::Mod => {
12426                let d = rv.to_int();
12427                if d == 0 {
12428                    return Err(PerlError::runtime("Illegal modulus zero", _line).into());
12429                }
12430                PerlValue::integer(lv.to_int() % d)
12431            }
12432            BinOp::Pow => {
12433                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12434                    let int_pow = (b >= 0)
12435                        .then(|| u32::try_from(b).ok())
12436                        .flatten()
12437                        .and_then(|bu| a.checked_pow(bu))
12438                        .map(PerlValue::integer);
12439                    int_pow.unwrap_or_else(|| PerlValue::float(lv.to_number().powf(rv.to_number())))
12440                } else {
12441                    PerlValue::float(lv.to_number().powf(rv.to_number()))
12442                }
12443            }
12444            BinOp::Concat => {
12445                let mut s = String::new();
12446                lv.append_to(&mut s);
12447                rv.append_to(&mut s);
12448                PerlValue::string(s)
12449            }
12450            BinOp::NumEq => {
12451                // Struct equality: compare all fields
12452                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
12453                    if a.def.name != b.def.name {
12454                        PerlValue::integer(0)
12455                    } else {
12456                        let av = a.get_values();
12457                        let bv = b.get_values();
12458                        let eq = av.len() == bv.len()
12459                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
12460                        PerlValue::integer(if eq { 1 } else { 0 })
12461                    }
12462                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12463                    PerlValue::integer(if a == b { 1 } else { 0 })
12464                } else {
12465                    PerlValue::integer(if lv.to_number() == rv.to_number() {
12466                        1
12467                    } else {
12468                        0
12469                    })
12470                }
12471            }
12472            BinOp::NumNe => {
12473                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12474                    PerlValue::integer(if a != b { 1 } else { 0 })
12475                } else {
12476                    PerlValue::integer(if lv.to_number() != rv.to_number() {
12477                        1
12478                    } else {
12479                        0
12480                    })
12481                }
12482            }
12483            BinOp::NumLt => {
12484                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12485                    PerlValue::integer(if a < b { 1 } else { 0 })
12486                } else {
12487                    PerlValue::integer(if lv.to_number() < rv.to_number() {
12488                        1
12489                    } else {
12490                        0
12491                    })
12492                }
12493            }
12494            BinOp::NumGt => {
12495                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12496                    PerlValue::integer(if a > b { 1 } else { 0 })
12497                } else {
12498                    PerlValue::integer(if lv.to_number() > rv.to_number() {
12499                        1
12500                    } else {
12501                        0
12502                    })
12503                }
12504            }
12505            BinOp::NumLe => {
12506                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12507                    PerlValue::integer(if a <= b { 1 } else { 0 })
12508                } else {
12509                    PerlValue::integer(if lv.to_number() <= rv.to_number() {
12510                        1
12511                    } else {
12512                        0
12513                    })
12514                }
12515            }
12516            BinOp::NumGe => {
12517                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12518                    PerlValue::integer(if a >= b { 1 } else { 0 })
12519                } else {
12520                    PerlValue::integer(if lv.to_number() >= rv.to_number() {
12521                        1
12522                    } else {
12523                        0
12524                    })
12525                }
12526            }
12527            BinOp::Spaceship => {
12528                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12529                    PerlValue::integer(if a < b {
12530                        -1
12531                    } else if a > b {
12532                        1
12533                    } else {
12534                        0
12535                    })
12536                } else {
12537                    let a = lv.to_number();
12538                    let b = rv.to_number();
12539                    PerlValue::integer(if a < b {
12540                        -1
12541                    } else if a > b {
12542                        1
12543                    } else {
12544                        0
12545                    })
12546                }
12547            }
12548            BinOp::StrEq => PerlValue::integer(if lv.to_string() == rv.to_string() {
12549                1
12550            } else {
12551                0
12552            }),
12553            BinOp::StrNe => PerlValue::integer(if lv.to_string() != rv.to_string() {
12554                1
12555            } else {
12556                0
12557            }),
12558            BinOp::StrLt => PerlValue::integer(if lv.to_string() < rv.to_string() {
12559                1
12560            } else {
12561                0
12562            }),
12563            BinOp::StrGt => PerlValue::integer(if lv.to_string() > rv.to_string() {
12564                1
12565            } else {
12566                0
12567            }),
12568            BinOp::StrLe => PerlValue::integer(if lv.to_string() <= rv.to_string() {
12569                1
12570            } else {
12571                0
12572            }),
12573            BinOp::StrGe => PerlValue::integer(if lv.to_string() >= rv.to_string() {
12574                1
12575            } else {
12576                0
12577            }),
12578            BinOp::StrCmp => {
12579                let cmp = lv.to_string().cmp(&rv.to_string());
12580                PerlValue::integer(match cmp {
12581                    std::cmp::Ordering::Less => -1,
12582                    std::cmp::Ordering::Greater => 1,
12583                    std::cmp::Ordering::Equal => 0,
12584                })
12585            }
12586            BinOp::BitAnd => {
12587                if let Some(s) = crate::value::set_intersection(lv, rv) {
12588                    s
12589                } else {
12590                    PerlValue::integer(lv.to_int() & rv.to_int())
12591                }
12592            }
12593            BinOp::BitOr => {
12594                if let Some(s) = crate::value::set_union(lv, rv) {
12595                    s
12596                } else {
12597                    PerlValue::integer(lv.to_int() | rv.to_int())
12598                }
12599            }
12600            BinOp::BitXor => PerlValue::integer(lv.to_int() ^ rv.to_int()),
12601            BinOp::ShiftLeft => PerlValue::integer(lv.to_int() << rv.to_int()),
12602            BinOp::ShiftRight => PerlValue::integer(lv.to_int() >> rv.to_int()),
12603            // These should have been handled by short-circuit above
12604            BinOp::LogAnd
12605            | BinOp::LogOr
12606            | BinOp::DefinedOr
12607            | BinOp::LogAndWord
12608            | BinOp::LogOrWord => unreachable!(),
12609            BinOp::BindMatch | BinOp::BindNotMatch => {
12610                unreachable!("regex bind handled in eval_expr BinOp arm")
12611            }
12612        })
12613    }
12614
12615    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
12616    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
12617    /// length — that was silently wrong vs `perl`.
12618    fn err_modify_symbolic_aggregate_deref_inc_dec(
12619        kind: Sigil,
12620        is_pre: bool,
12621        is_inc: bool,
12622        line: usize,
12623    ) -> FlowOrError {
12624        let agg = match kind {
12625            Sigil::Array => "array",
12626            Sigil::Hash => "hash",
12627            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
12628        };
12629        let op = match (is_pre, is_inc) {
12630            (true, true) => "preincrement (++)",
12631            (true, false) => "predecrement (--)",
12632            (false, true) => "postincrement (++)",
12633            (false, false) => "postdecrement (--)",
12634        };
12635        FlowOrError::Error(PerlError::runtime(
12636            format!("Can't modify {agg} dereference in {op}"),
12637            line,
12638        ))
12639    }
12640
12641    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
12642    pub(crate) fn symbolic_scalar_ref_postfix(
12643        &mut self,
12644        ref_val: PerlValue,
12645        decrement: bool,
12646        line: usize,
12647    ) -> Result<PerlValue, FlowOrError> {
12648        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
12649        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12650        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
12651        Ok(old)
12652    }
12653
12654    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
12655    /// [`Self::assign_value`] and the VM.
12656    pub(crate) fn assign_scalar_ref_deref(
12657        &mut self,
12658        ref_val: PerlValue,
12659        val: PerlValue,
12660        line: usize,
12661    ) -> ExecResult {
12662        if let Some(name) = ref_val.as_scalar_binding_name() {
12663            self.set_special_var(&name, &val)
12664                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12665            return Ok(PerlValue::UNDEF);
12666        }
12667        if let Some(r) = ref_val.as_scalar_ref() {
12668            *r.write() = val;
12669            return Ok(PerlValue::UNDEF);
12670        }
12671        Err(PerlError::runtime("Can't assign to non-scalar reference", line).into())
12672    }
12673
12674    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
12675    pub(crate) fn assign_symbolic_array_ref_deref(
12676        &mut self,
12677        ref_val: PerlValue,
12678        val: PerlValue,
12679        line: usize,
12680    ) -> ExecResult {
12681        if let Some(a) = ref_val.as_array_ref() {
12682            *a.write() = val.to_list();
12683            return Ok(PerlValue::UNDEF);
12684        }
12685        if let Some(name) = ref_val.as_array_binding_name() {
12686            self.scope
12687                .set_array(&name, val.to_list())
12688                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12689            return Ok(PerlValue::UNDEF);
12690        }
12691        if let Some(s) = ref_val.as_str() {
12692            if self.strict_refs {
12693                return Err(PerlError::runtime(
12694                    format!(
12695                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
12696                        s
12697                    ),
12698                    line,
12699                )
12700                .into());
12701            }
12702            self.scope
12703                .set_array(&s, val.to_list())
12704                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12705            return Ok(PerlValue::UNDEF);
12706        }
12707        Err(PerlError::runtime("Can't assign to non-array reference", line).into())
12708    }
12709
12710    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
12711    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
12712    pub(crate) fn assign_symbolic_typeglob_ref_deref(
12713        &mut self,
12714        ref_val: PerlValue,
12715        val: PerlValue,
12716        line: usize,
12717    ) -> ExecResult {
12718        let lhs_name = if let Some(s) = ref_val.as_str() {
12719            if self.strict_refs {
12720                return Err(PerlError::runtime(
12721                    format!(
12722                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
12723                        s
12724                    ),
12725                    line,
12726                )
12727                .into());
12728            }
12729            s.to_string()
12730        } else {
12731            return Err(
12732                PerlError::runtime("Can't assign to non-glob symbolic reference", line).into(),
12733            );
12734        };
12735        let is_coderef = val.as_code_ref().is_some()
12736            || val
12737                .as_scalar_ref()
12738                .map(|r| r.read().as_code_ref().is_some())
12739                .unwrap_or(false);
12740        if is_coderef {
12741            return self.assign_typeglob_value(&lhs_name, val, line);
12742        }
12743        let rhs_key = val.to_string();
12744        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
12745            .map_err(FlowOrError::Error)?;
12746        Ok(PerlValue::UNDEF)
12747    }
12748
12749    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
12750    pub(crate) fn assign_symbolic_hash_ref_deref(
12751        &mut self,
12752        ref_val: PerlValue,
12753        val: PerlValue,
12754        line: usize,
12755    ) -> ExecResult {
12756        let items = val.to_list();
12757        let mut map = IndexMap::new();
12758        let mut i = 0;
12759        while i + 1 < items.len() {
12760            map.insert(items[i].to_string(), items[i + 1].clone());
12761            i += 2;
12762        }
12763        if let Some(h) = ref_val.as_hash_ref() {
12764            *h.write() = map;
12765            return Ok(PerlValue::UNDEF);
12766        }
12767        if let Some(name) = ref_val.as_hash_binding_name() {
12768            self.touch_env_hash(&name);
12769            self.scope
12770                .set_hash(&name, map)
12771                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12772            return Ok(PerlValue::UNDEF);
12773        }
12774        if let Some(s) = ref_val.as_str() {
12775            if self.strict_refs {
12776                return Err(PerlError::runtime(
12777                    format!(
12778                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
12779                        s
12780                    ),
12781                    line,
12782                )
12783                .into());
12784            }
12785            self.touch_env_hash(&s);
12786            self.scope
12787                .set_hash(&s, map)
12788                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12789            return Ok(PerlValue::UNDEF);
12790        }
12791        Err(PerlError::runtime("Can't assign to non-hash reference", line).into())
12792    }
12793
12794    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
12795    pub(crate) fn assign_arrow_hash_deref(
12796        &mut self,
12797        container: PerlValue,
12798        key: String,
12799        val: PerlValue,
12800        line: usize,
12801    ) -> ExecResult {
12802        if let Some(b) = container.as_blessed_ref() {
12803            let mut data = b.data.write();
12804            if let Some(r) = data.as_hash_ref() {
12805                r.write().insert(key, val);
12806                return Ok(PerlValue::UNDEF);
12807            }
12808            if let Some(mut map) = data.as_hash_map() {
12809                map.insert(key, val);
12810                *data = PerlValue::hash(map);
12811                return Ok(PerlValue::UNDEF);
12812            }
12813            return Err(PerlError::runtime("Can't assign into non-hash blessed ref", line).into());
12814        }
12815        if let Some(r) = container.as_hash_ref() {
12816            r.write().insert(key, val);
12817            return Ok(PerlValue::UNDEF);
12818        }
12819        if let Some(name) = container.as_hash_binding_name() {
12820            self.touch_env_hash(&name);
12821            self.scope
12822                .set_hash_element(&name, &key, val)
12823                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12824            return Ok(PerlValue::UNDEF);
12825        }
12826        Err(PerlError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
12827    }
12828
12829    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
12830    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
12831    pub(crate) fn eval_arrow_array_base(
12832        &mut self,
12833        expr: &Expr,
12834        _line: usize,
12835    ) -> Result<PerlValue, FlowOrError> {
12836        match &expr.kind {
12837            ExprKind::Deref {
12838                expr: inner,
12839                kind: Sigil::Array | Sigil::Scalar,
12840            } => self.eval_expr(inner),
12841            _ => self.eval_expr(expr),
12842        }
12843    }
12844
12845    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
12846    pub(crate) fn eval_arrow_hash_base(
12847        &mut self,
12848        expr: &Expr,
12849        _line: usize,
12850    ) -> Result<PerlValue, FlowOrError> {
12851        match &expr.kind {
12852            ExprKind::Deref {
12853                expr: inner,
12854                kind: Sigil::Scalar,
12855            } => self.eval_expr(inner),
12856            _ => self.eval_expr(expr),
12857        }
12858    }
12859
12860    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
12861    pub(crate) fn read_arrow_array_element(
12862        &self,
12863        container: PerlValue,
12864        idx: i64,
12865        line: usize,
12866    ) -> Result<PerlValue, FlowOrError> {
12867        if let Some(a) = container.as_array_ref() {
12868            let arr = a.read();
12869            let i = if idx < 0 {
12870                (arr.len() as i64 + idx) as usize
12871            } else {
12872                idx as usize
12873            };
12874            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12875        }
12876        if let Some(name) = container.as_array_binding_name() {
12877            return Ok(self.scope.get_array_element(&name, idx));
12878        }
12879        if let Some(arr) = container.as_array_vec() {
12880            let i = if idx < 0 {
12881                (arr.len() as i64 + idx) as usize
12882            } else {
12883                idx as usize
12884            };
12885            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12886        }
12887        // Blessed arrayref (e.g. `Pair`) — `pairs` returns blessed `Pair` objects that
12888        // can be indexed via `$_->[0]` / `$_->[1]`.
12889        if let Some(b) = container.as_blessed_ref() {
12890            let inner = b.data.read().clone();
12891            if let Some(a) = inner.as_array_ref() {
12892                let arr = a.read();
12893                let i = if idx < 0 {
12894                    (arr.len() as i64 + idx) as usize
12895                } else {
12896                    idx as usize
12897                };
12898                return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12899            }
12900        }
12901        Err(PerlError::runtime("Can't use arrow deref on non-array-ref", line).into())
12902    }
12903
12904    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
12905    pub(crate) fn read_arrow_hash_element(
12906        &mut self,
12907        container: PerlValue,
12908        key: &str,
12909        line: usize,
12910    ) -> Result<PerlValue, FlowOrError> {
12911        if let Some(r) = container.as_hash_ref() {
12912            let h = r.read();
12913            return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12914        }
12915        if let Some(name) = container.as_hash_binding_name() {
12916            self.touch_env_hash(&name);
12917            return Ok(self.scope.get_hash_element(&name, key));
12918        }
12919        if let Some(b) = container.as_blessed_ref() {
12920            let data = b.data.read();
12921            if let Some(v) = data.hash_get(key) {
12922                return Ok(v);
12923            }
12924            if let Some(r) = data.as_hash_ref() {
12925                let h = r.read();
12926                return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12927            }
12928            return Err(PerlError::runtime(
12929                "Can't access hash field on non-hash blessed ref",
12930                line,
12931            )
12932            .into());
12933        }
12934        // Struct field access via hash deref syntax: $struct->{field}
12935        if let Some(s) = container.as_struct_inst() {
12936            if let Some(idx) = s.def.field_index(key) {
12937                return Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF));
12938            }
12939            return Err(PerlError::runtime(
12940                format!("struct {} has no field `{}`", s.def.name, key),
12941                line,
12942            )
12943            .into());
12944        }
12945        // Class instance field access via hash deref: $obj->{field}
12946        if let Some(c) = container.as_class_inst() {
12947            if let Some(idx) = c.def.field_index(key) {
12948                return Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF));
12949            }
12950            return Err(PerlError::runtime(
12951                format!("class {} has no field `{}`", c.def.name, key),
12952                line,
12953            )
12954            .into());
12955        }
12956        Err(PerlError::runtime("Can't use arrow deref on non-hash-ref", line).into())
12957    }
12958
12959    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
12960    pub(crate) fn arrow_array_postfix(
12961        &mut self,
12962        container: PerlValue,
12963        idx: i64,
12964        decrement: bool,
12965        line: usize,
12966    ) -> Result<PerlValue, FlowOrError> {
12967        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
12968        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12969        self.assign_arrow_array_deref(container, idx, new_val, line)?;
12970        Ok(old)
12971    }
12972
12973    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
12974    pub(crate) fn arrow_hash_postfix(
12975        &mut self,
12976        container: PerlValue,
12977        key: String,
12978        decrement: bool,
12979        line: usize,
12980    ) -> Result<PerlValue, FlowOrError> {
12981        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
12982        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12983        self.assign_arrow_hash_deref(container, key, new_val, line)?;
12984        Ok(old)
12985    }
12986
12987    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` evaluation. If a nullary
12988    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
12989    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
12990    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
12991    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
12992    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
12993    /// subs" in use`).
12994    pub(crate) fn resolve_bareword_rvalue(
12995        &mut self,
12996        name: &str,
12997        want: WantarrayCtx,
12998        line: usize,
12999    ) -> Result<PerlValue, FlowOrError> {
13000        if name == "__PACKAGE__" {
13001            return Ok(PerlValue::string(self.current_package()));
13002        }
13003        if let Some(sub) = self.resolve_sub_by_name(name) {
13004            return self.call_sub(&sub, vec![], want, line);
13005        }
13006        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
13007        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
13008            return r.map_err(Into::into);
13009        }
13010        Ok(PerlValue::string(name.to_string()))
13011    }
13012
13013    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
13014    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
13015    /// compound / inc-dec / assign helpers below.
13016    pub(crate) fn arrow_array_slice_values(
13017        &mut self,
13018        container: PerlValue,
13019        indices: &[i64],
13020        line: usize,
13021    ) -> Result<PerlValue, FlowOrError> {
13022        let mut out = Vec::with_capacity(indices.len());
13023        for &idx in indices {
13024            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
13025            out.push(v);
13026        }
13027        Ok(PerlValue::array(out))
13028    }
13029
13030    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment for
13031    /// multi-index `ArrowDeref { Array, List }`. Shared by the VM
13032    /// [`crate::bytecode::Op::SetArrowArraySlice`].
13033    pub(crate) fn assign_arrow_array_slice(
13034        &mut self,
13035        container: PerlValue,
13036        indices: Vec<i64>,
13037        val: PerlValue,
13038        line: usize,
13039    ) -> Result<PerlValue, FlowOrError> {
13040        if indices.is_empty() {
13041            return Err(PerlError::runtime("assign to empty array slice", line).into());
13042        }
13043        let vals = val.to_list();
13044        for (i, idx) in indices.iter().enumerate() {
13045            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
13046            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
13047        }
13048        Ok(PerlValue::UNDEF)
13049    }
13050
13051    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
13052    pub(crate) fn flatten_array_slice_index_specs(
13053        &mut self,
13054        indices: &[Expr],
13055    ) -> Result<Vec<i64>, FlowOrError> {
13056        let mut out = Vec::new();
13057        for idx_expr in indices {
13058            let v = if matches!(
13059                idx_expr.kind,
13060                ExprKind::Range { .. } | ExprKind::SliceRange { .. }
13061            ) {
13062                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
13063            } else {
13064                self.eval_expr(idx_expr)?
13065            };
13066            if let Some(list) = v.as_array_vec() {
13067                for idx in list {
13068                    out.push(idx.to_int());
13069                }
13070            } else {
13071                out.push(v.to_int());
13072            }
13073        }
13074        Ok(out)
13075    }
13076
13077    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
13078    pub(crate) fn assign_named_array_slice(
13079        &mut self,
13080        stash_array_name: &str,
13081        indices: Vec<i64>,
13082        val: PerlValue,
13083        line: usize,
13084    ) -> Result<PerlValue, FlowOrError> {
13085        if indices.is_empty() {
13086            return Err(PerlError::runtime("assign to empty array slice", line).into());
13087        }
13088        let vals = val.to_list();
13089        for (i, idx) in indices.iter().enumerate() {
13090            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
13091            self.scope
13092                .set_array_element(stash_array_name, *idx, v)
13093                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13094        }
13095        Ok(PerlValue::UNDEF)
13096    }
13097
13098    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
13099    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
13100    pub(crate) fn compound_assign_arrow_array_slice(
13101        &mut self,
13102        container: PerlValue,
13103        indices: Vec<i64>,
13104        op: BinOp,
13105        rhs: PerlValue,
13106        line: usize,
13107    ) -> Result<PerlValue, FlowOrError> {
13108        if indices.is_empty() {
13109            return Err(PerlError::runtime("assign to empty array slice", line).into());
13110        }
13111        let last_idx = *indices.last().expect("non-empty indices");
13112        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
13113        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
13114        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
13115        Ok(new_val)
13116    }
13117
13118    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
13119    /// pre forms return the new value, post forms return the old **last** element.
13120    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
13121    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
13122    pub(crate) fn arrow_array_slice_inc_dec(
13123        &mut self,
13124        container: PerlValue,
13125        indices: Vec<i64>,
13126        kind: u8,
13127        line: usize,
13128    ) -> Result<PerlValue, FlowOrError> {
13129        if indices.is_empty() {
13130            return Err(
13131                PerlError::runtime("array slice increment needs at least one index", line).into(),
13132            );
13133        }
13134        let last_idx = *indices.last().expect("non-empty indices");
13135        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
13136        let new_val = if kind & 1 == 0 {
13137            PerlValue::integer(last_old.to_int() + 1)
13138        } else {
13139            PerlValue::integer(last_old.to_int() - 1)
13140        };
13141        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
13142        Ok(if kind < 2 { new_val } else { last_old })
13143    }
13144
13145    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
13146    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
13147    pub(crate) fn named_array_slice_inc_dec(
13148        &mut self,
13149        stash_array_name: &str,
13150        indices: Vec<i64>,
13151        kind: u8,
13152        line: usize,
13153    ) -> Result<PerlValue, FlowOrError> {
13154        let last_idx = *indices.last().ok_or_else(|| {
13155            PerlError::runtime("array slice increment needs at least one index", line)
13156        })?;
13157        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
13158        let new_val = if kind & 1 == 0 {
13159            PerlValue::integer(last_old.to_int() + 1)
13160        } else {
13161            PerlValue::integer(last_old.to_int() - 1)
13162        };
13163        self.scope
13164            .set_array_element(stash_array_name, last_idx, new_val.clone())
13165            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13166        Ok(if kind < 2 { new_val } else { last_old })
13167    }
13168
13169    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
13170    pub(crate) fn compound_assign_named_array_slice(
13171        &mut self,
13172        stash_array_name: &str,
13173        indices: Vec<i64>,
13174        op: BinOp,
13175        rhs: PerlValue,
13176        line: usize,
13177    ) -> Result<PerlValue, FlowOrError> {
13178        if indices.is_empty() {
13179            return Err(PerlError::runtime("assign to empty array slice", line).into());
13180        }
13181        let last_idx = *indices.last().expect("non-empty indices");
13182        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
13183        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
13184        self.scope
13185            .set_array_element(stash_array_name, last_idx, new_val.clone())
13186            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13187        Ok(new_val)
13188    }
13189
13190    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
13191    pub(crate) fn assign_arrow_array_deref(
13192        &mut self,
13193        container: PerlValue,
13194        idx: i64,
13195        val: PerlValue,
13196        line: usize,
13197    ) -> ExecResult {
13198        if let Some(a) = container.as_array_ref() {
13199            let mut arr = a.write();
13200            let i = if idx < 0 {
13201                (arr.len() as i64 + idx) as usize
13202            } else {
13203                idx as usize
13204            };
13205            if i >= arr.len() {
13206                arr.resize(i + 1, PerlValue::UNDEF);
13207            }
13208            arr[i] = val;
13209            return Ok(PerlValue::UNDEF);
13210        }
13211        if let Some(name) = container.as_array_binding_name() {
13212            self.scope
13213                .set_array_element(&name, idx, val)
13214                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13215            return Ok(PerlValue::UNDEF);
13216        }
13217        Err(PerlError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
13218    }
13219
13220    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
13221    pub(crate) fn assign_typeglob_value(
13222        &mut self,
13223        name: &str,
13224        val: PerlValue,
13225        line: usize,
13226    ) -> ExecResult {
13227        let sub = if let Some(c) = val.as_code_ref() {
13228            Some(c)
13229        } else if let Some(r) = val.as_scalar_ref() {
13230            r.read().as_code_ref().map(|c| Arc::clone(&c))
13231        } else {
13232            None
13233        };
13234        if let Some(sub) = sub {
13235            let lhs_sub = self.qualify_typeglob_sub_key(name);
13236            self.subs.insert(lhs_sub, sub);
13237            return Ok(PerlValue::UNDEF);
13238        }
13239        Err(PerlError::runtime(
13240            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
13241            line,
13242        )
13243        .into())
13244    }
13245
13246    fn assign_value(&mut self, target: &Expr, val: PerlValue) -> ExecResult {
13247        match &target.kind {
13248            ExprKind::ScalarVar(name) => {
13249                let stor = self.tree_scalar_storage_name(name);
13250                if self.scope.is_scalar_frozen(&stor) {
13251                    return Err(FlowOrError::Error(PerlError::runtime(
13252                        format!("Modification of a frozen value: ${}", name),
13253                        target.line,
13254                    )));
13255                }
13256                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
13257                    let class = obj
13258                        .as_blessed_ref()
13259                        .map(|b| b.class.clone())
13260                        .unwrap_or_default();
13261                    let full = format!("{}::STORE", class);
13262                    if let Some(sub) = self.subs.get(&full).cloned() {
13263                        let arg_vals = vec![obj, val];
13264                        return match self.call_sub(
13265                            &sub,
13266                            arg_vals,
13267                            WantarrayCtx::Scalar,
13268                            target.line,
13269                        ) {
13270                            Ok(_) => Ok(PerlValue::UNDEF),
13271                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13272                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13273                        };
13274                    }
13275                }
13276                self.set_special_var(&stor, &val)
13277                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
13278                Ok(PerlValue::UNDEF)
13279            }
13280            ExprKind::ArrayVar(name) => {
13281                if self.scope.is_array_frozen(name) {
13282                    return Err(PerlError::runtime(
13283                        format!("Modification of a frozen value: @{}", name),
13284                        target.line,
13285                    )
13286                    .into());
13287                }
13288                if self.strict_vars
13289                    && !name.contains("::")
13290                    && !self.scope.array_binding_exists(name)
13291                {
13292                    return Err(PerlError::runtime(
13293                        format!(
13294                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
13295                            name, name
13296                        ),
13297                        target.line,
13298                    )
13299                    .into());
13300                }
13301                self.scope.set_array(name, val.to_list())?;
13302                Ok(PerlValue::UNDEF)
13303            }
13304            ExprKind::HashVar(name) => {
13305                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
13306                {
13307                    return Err(PerlError::runtime(
13308                        format!(
13309                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13310                            name, name
13311                        ),
13312                        target.line,
13313                    )
13314                    .into());
13315                }
13316                let items = val.to_list();
13317                let mut map = IndexMap::new();
13318                let mut i = 0;
13319                while i + 1 < items.len() {
13320                    map.insert(items[i].to_string(), items[i + 1].clone());
13321                    i += 2;
13322                }
13323                self.scope.set_hash(name, map)?;
13324                Ok(PerlValue::UNDEF)
13325            }
13326            ExprKind::ArrayElement { array, index } => {
13327                if self.strict_vars
13328                    && !array.contains("::")
13329                    && !self.scope.array_binding_exists(array)
13330                {
13331                    return Err(PerlError::runtime(
13332                        format!(
13333                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
13334                            array, array
13335                        ),
13336                        target.line,
13337                    )
13338                    .into());
13339                }
13340                if self.scope.is_array_frozen(array) {
13341                    return Err(PerlError::runtime(
13342                        format!("Modification of a frozen value: @{}", array),
13343                        target.line,
13344                    )
13345                    .into());
13346                }
13347                let idx = self.eval_expr(index)?.to_int();
13348                let aname = self.stash_array_name_for_package(array);
13349                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
13350                    let class = obj
13351                        .as_blessed_ref()
13352                        .map(|b| b.class.clone())
13353                        .unwrap_or_default();
13354                    let full = format!("{}::STORE", class);
13355                    if let Some(sub) = self.subs.get(&full).cloned() {
13356                        let arg_vals = vec![obj, PerlValue::integer(idx), val];
13357                        return match self.call_sub(
13358                            &sub,
13359                            arg_vals,
13360                            WantarrayCtx::Scalar,
13361                            target.line,
13362                        ) {
13363                            Ok(_) => Ok(PerlValue::UNDEF),
13364                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13365                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13366                        };
13367                    }
13368                }
13369                self.scope.set_array_element(&aname, idx, val)?;
13370                Ok(PerlValue::UNDEF)
13371            }
13372            ExprKind::ArraySlice { array, indices } => {
13373                if indices.is_empty() {
13374                    return Err(
13375                        PerlError::runtime("assign to empty array slice", target.line).into(),
13376                    );
13377                }
13378                self.check_strict_array_var(array, target.line)?;
13379                if self.scope.is_array_frozen(array) {
13380                    return Err(PerlError::runtime(
13381                        format!("Modification of a frozen value: @{}", array),
13382                        target.line,
13383                    )
13384                    .into());
13385                }
13386                let aname = self.stash_array_name_for_package(array);
13387                let flat = self.flatten_array_slice_index_specs(indices)?;
13388                self.assign_named_array_slice(&aname, flat, val, target.line)
13389            }
13390            ExprKind::HashElement { hash, key } => {
13391                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
13392                {
13393                    return Err(PerlError::runtime(
13394                        format!(
13395                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13396                            hash, hash
13397                        ),
13398                        target.line,
13399                    )
13400                    .into());
13401                }
13402                if self.scope.is_hash_frozen(hash) {
13403                    return Err(PerlError::runtime(
13404                        format!("Modification of a frozen value: %%{}", hash),
13405                        target.line,
13406                    )
13407                    .into());
13408                }
13409                let k = self.eval_expr(key)?.to_string();
13410                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
13411                    let class = obj
13412                        .as_blessed_ref()
13413                        .map(|b| b.class.clone())
13414                        .unwrap_or_default();
13415                    let full = format!("{}::STORE", class);
13416                    if let Some(sub) = self.subs.get(&full).cloned() {
13417                        let arg_vals = vec![obj, PerlValue::string(k), val];
13418                        return match self.call_sub(
13419                            &sub,
13420                            arg_vals,
13421                            WantarrayCtx::Scalar,
13422                            target.line,
13423                        ) {
13424                            Ok(_) => Ok(PerlValue::UNDEF),
13425                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13426                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13427                        };
13428                    }
13429                }
13430                self.scope.set_hash_element(hash, &k, val)?;
13431                Ok(PerlValue::UNDEF)
13432            }
13433            ExprKind::HashSlice { hash, keys } => {
13434                if keys.is_empty() {
13435                    return Err(
13436                        PerlError::runtime("assign to empty hash slice", target.line).into(),
13437                    );
13438                }
13439                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
13440                {
13441                    return Err(PerlError::runtime(
13442                        format!(
13443                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13444                            hash, hash
13445                        ),
13446                        target.line,
13447                    )
13448                    .into());
13449                }
13450                if self.scope.is_hash_frozen(hash) {
13451                    return Err(PerlError::runtime(
13452                        format!("Modification of a frozen value: %%{}", hash),
13453                        target.line,
13454                    )
13455                    .into());
13456                }
13457                let mut key_vals = Vec::with_capacity(keys.len());
13458                for key_expr in keys {
13459                    let v = if matches!(
13460                        key_expr.kind,
13461                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
13462                    ) {
13463                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
13464                    } else {
13465                        self.eval_expr(key_expr)?
13466                    };
13467                    key_vals.push(v);
13468                }
13469                self.assign_named_hash_slice(hash, key_vals, val, target.line)
13470            }
13471            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
13472            ExprKind::TypeglobExpr(e) => {
13473                let name = self.eval_expr(e)?.to_string();
13474                let synthetic = Expr {
13475                    kind: ExprKind::Typeglob(name),
13476                    line: target.line,
13477                };
13478                self.assign_value(&synthetic, val)
13479            }
13480            ExprKind::AnonymousListSlice { source, indices } => {
13481                if let ExprKind::Deref {
13482                    expr: inner,
13483                    kind: Sigil::Array,
13484                } = &source.kind
13485                {
13486                    let container = self.eval_arrow_array_base(inner, target.line)?;
13487                    let vals = val.to_list();
13488                    let n = indices.len().min(vals.len());
13489                    for i in 0..n {
13490                        let idx = self.eval_expr(&indices[i])?.to_int();
13491                        self.assign_arrow_array_deref(
13492                            container.clone(),
13493                            idx,
13494                            vals[i].clone(),
13495                            target.line,
13496                        )?;
13497                    }
13498                    return Ok(PerlValue::UNDEF);
13499                }
13500                Err(
13501                    PerlError::runtime("assign to list slice: unsupported base", target.line)
13502                        .into(),
13503                )
13504            }
13505            ExprKind::ArrowDeref {
13506                expr,
13507                index,
13508                kind: DerefKind::Hash,
13509            } => {
13510                let key = self.eval_expr(index)?.to_string();
13511                let container = self.eval_expr(expr)?;
13512                self.assign_arrow_hash_deref(container, key, val, target.line)
13513            }
13514            ExprKind::ArrowDeref {
13515                expr,
13516                index,
13517                kind: DerefKind::Array,
13518            } => {
13519                let container = self.eval_arrow_array_base(expr, target.line)?;
13520                if let ExprKind::List(indices) = &index.kind {
13521                    let vals = val.to_list();
13522                    let n = indices.len().min(vals.len());
13523                    for i in 0..n {
13524                        let idx = self.eval_expr(&indices[i])?.to_int();
13525                        self.assign_arrow_array_deref(
13526                            container.clone(),
13527                            idx,
13528                            vals[i].clone(),
13529                            target.line,
13530                        )?;
13531                    }
13532                    return Ok(PerlValue::UNDEF);
13533                }
13534                let idx = self.eval_expr(index)?.to_int();
13535                self.assign_arrow_array_deref(container, idx, val, target.line)
13536            }
13537            ExprKind::HashSliceDeref { container, keys } => {
13538                let href = self.eval_expr(container)?;
13539                let mut key_vals = Vec::with_capacity(keys.len());
13540                for key_expr in keys {
13541                    key_vals.push(self.eval_expr(key_expr)?);
13542                }
13543                self.assign_hash_slice_deref(href, key_vals, val, target.line)
13544            }
13545            ExprKind::Deref {
13546                expr,
13547                kind: Sigil::Scalar,
13548            } => {
13549                let ref_val = self.eval_expr(expr)?;
13550                self.assign_scalar_ref_deref(ref_val, val, target.line)
13551            }
13552            ExprKind::Deref {
13553                expr,
13554                kind: Sigil::Array,
13555            } => {
13556                let ref_val = self.eval_expr(expr)?;
13557                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
13558            }
13559            ExprKind::Deref {
13560                expr,
13561                kind: Sigil::Hash,
13562            } => {
13563                let ref_val = self.eval_expr(expr)?;
13564                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
13565            }
13566            ExprKind::Deref {
13567                expr,
13568                kind: Sigil::Typeglob,
13569            } => {
13570                let ref_val = self.eval_expr(expr)?;
13571                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
13572            }
13573            ExprKind::Pos(inner) => {
13574                let key = match inner {
13575                    None => "_".to_string(),
13576                    Some(expr) => match &expr.kind {
13577                        ExprKind::ScalarVar(n) => n.clone(),
13578                        _ => self.eval_expr(expr)?.to_string(),
13579                    },
13580                };
13581                if val.is_undef() {
13582                    self.regex_pos.insert(key, None);
13583                } else {
13584                    let u = val.to_int().max(0) as usize;
13585                    self.regex_pos.insert(key, Some(u));
13586                }
13587                Ok(PerlValue::UNDEF)
13588            }
13589            // List assignment: `($a, $b, ...) = (val1, val2, ...)`
13590            // RHS is already fully evaluated — distribute elements to targets.
13591            ExprKind::List(targets) => {
13592                let items = val.to_list();
13593                for (i, t) in targets.iter().enumerate() {
13594                    let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
13595                    self.assign_value(t, v)?;
13596                }
13597                Ok(PerlValue::UNDEF)
13598            }
13599            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
13600            // write the substitution result back to the assignment target.
13601            ExprKind::Assign { target, .. } => self.assign_value(target, val),
13602            _ => Ok(PerlValue::UNDEF),
13603        }
13604    }
13605
13606    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
13607    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
13608        (name.starts_with('#') && name.len() > 1)
13609            || name.starts_with('^')
13610            || matches!(
13611                name,
13612                "$$" | "0"
13613                    | "!"
13614                    | "@"
13615                    | "/"
13616                    | "\\"
13617                    | ","
13618                    | "."
13619                    | "]"
13620                    | ";"
13621                    | "ARGV"
13622                    | "^I"
13623                    | "^D"
13624                    | "^P"
13625                    | "^S"
13626                    | "^W"
13627                    | "^O"
13628                    | "^T"
13629                    | "^V"
13630                    | "^E"
13631                    | "^H"
13632                    | "^WARNING_BITS"
13633                    | "^GLOBAL_PHASE"
13634                    | "^MATCH"
13635                    | "^PREMATCH"
13636                    | "^POSTMATCH"
13637                    | "^LAST_SUBMATCH_RESULT"
13638                    | "<"
13639                    | ">"
13640                    | "("
13641                    | ")"
13642                    | "?"
13643                    | "|"
13644                    | "\""
13645                    | "+"
13646                    | "%"
13647                    | "="
13648                    | "-"
13649                    | ":"
13650                    | "*"
13651                    | "INC"
13652            )
13653            || crate::english::is_known_alias(name)
13654    }
13655
13656    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
13657    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
13658    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
13659    /// [`Self::english_no_match_vars`] is set.
13660    #[inline]
13661    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
13662        if !self.english_enabled {
13663            return name;
13664        }
13665        if self
13666            .english_lexical_scalars
13667            .iter()
13668            .any(|s| s.contains(name))
13669        {
13670            return name;
13671        }
13672        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
13673            return short;
13674        }
13675        name
13676    }
13677
13678    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
13679    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
13680        name.starts_with('^')
13681            || matches!(
13682                name,
13683                "0" | "/"
13684                    | "\\"
13685                    | ","
13686                    | ";"
13687                    | "\""
13688                    | "%"
13689                    | "="
13690                    | "-"
13691                    | ":"
13692                    | "*"
13693                    | "INC"
13694                    | "^I"
13695                    | "^D"
13696                    | "^P"
13697                    | "^W"
13698                    | "^H"
13699                    | "^WARNING_BITS"
13700                    | "$$"
13701                    | "]"
13702                    | "^S"
13703                    | "ARGV"
13704                    | "|"
13705                    | "+"
13706                    | "?"
13707                    | "!"
13708                    | "@"
13709                    | "."
13710            )
13711            || crate::english::is_known_alias(name)
13712    }
13713
13714    pub(crate) fn get_special_var(&self, name: &str) -> PerlValue {
13715        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
13716        let name = if !crate::compat_mode() {
13717            match name {
13718                "NR" => ".",
13719                "RS" => "/",
13720                "OFS" => ",",
13721                "ORS" => "\\",
13722                "NF" => {
13723                    let len = self.scope.array_len("F");
13724                    return PerlValue::integer(len as i64);
13725                }
13726                _ => self.english_scalar_name(name),
13727            }
13728        } else {
13729            self.english_scalar_name(name)
13730        };
13731        match name {
13732            "$$" => PerlValue::integer(std::process::id() as i64),
13733            "_" => self.scope.get_scalar("_"),
13734            "^MATCH" => PerlValue::string(self.last_match.clone()),
13735            "^PREMATCH" => PerlValue::string(self.prematch.clone()),
13736            "^POSTMATCH" => PerlValue::string(self.postmatch.clone()),
13737            "^LAST_SUBMATCH_RESULT" => PerlValue::string(self.last_paren_match.clone()),
13738            "0" => PerlValue::string(self.program_name.clone()),
13739            "!" => PerlValue::errno_dual(self.errno_code, self.errno.clone()),
13740            "@" => {
13741                if let Some(ref v) = self.eval_error_value {
13742                    v.clone()
13743                } else {
13744                    PerlValue::errno_dual(self.eval_error_code, self.eval_error.clone())
13745                }
13746            }
13747            "/" => match &self.irs {
13748                Some(s) => PerlValue::string(s.clone()),
13749                None => PerlValue::UNDEF,
13750            },
13751            "\\" => PerlValue::string(self.ors.clone()),
13752            "," => PerlValue::string(self.ofs.clone()),
13753            "." => {
13754                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
13755                if self.last_readline_handle.is_empty() {
13756                    if self.line_number == 0 {
13757                        PerlValue::UNDEF
13758                    } else {
13759                        PerlValue::integer(self.line_number)
13760                    }
13761                } else {
13762                    PerlValue::integer(
13763                        *self
13764                            .handle_line_numbers
13765                            .get(&self.last_readline_handle)
13766                            .unwrap_or(&0),
13767                    )
13768                }
13769            }
13770            "]" => PerlValue::float(perl_bracket_version()),
13771            ";" => PerlValue::string(self.subscript_sep.clone()),
13772            "ARGV" => PerlValue::string(self.argv_current_file.clone()),
13773            "^I" => PerlValue::string(self.inplace_edit.clone()),
13774            "^D" => PerlValue::integer(self.debug_flags),
13775            "^P" => PerlValue::integer(self.perl_debug_flags),
13776            "^S" => PerlValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
13777            "^W" => PerlValue::integer(if self.warnings { 1 } else { 0 }),
13778            "^O" => PerlValue::string(perl_osname()),
13779            "^T" => PerlValue::integer(self.script_start_time),
13780            "^V" => PerlValue::string(perl_version_v_string()),
13781            "^E" => PerlValue::string(extended_os_error_string()),
13782            "^H" => PerlValue::integer(self.compile_hints),
13783            "^WARNING_BITS" => PerlValue::integer(self.warning_bits),
13784            "^GLOBAL_PHASE" => PerlValue::string(self.global_phase.clone()),
13785            "<" | ">" => PerlValue::integer(unix_id_for_special(name)),
13786            "(" | ")" => PerlValue::string(unix_group_list_for_special(name)),
13787            "?" => PerlValue::integer(self.child_exit_status),
13788            "|" => PerlValue::integer(if self.output_autoflush { 1 } else { 0 }),
13789            "\"" => PerlValue::string(self.list_separator.clone()),
13790            "+" => PerlValue::string(self.last_paren_match.clone()),
13791            "%" => PerlValue::integer(self.format_page_number),
13792            "=" => PerlValue::integer(self.format_lines_per_page),
13793            "-" => PerlValue::integer(self.format_lines_left),
13794            ":" => PerlValue::string(self.format_line_break_chars.clone()),
13795            "*" => PerlValue::integer(if self.multiline_match { 1 } else { 0 }),
13796            "^" => PerlValue::string(self.format_top_name.clone()),
13797            "INC" => PerlValue::integer(self.inc_hook_index),
13798            "^A" => PerlValue::string(self.accumulator_format.clone()),
13799            "^C" => PerlValue::integer(if self.sigint_pending_caret.replace(false) {
13800                1
13801            } else {
13802                0
13803            }),
13804            "^F" => PerlValue::integer(self.max_system_fd),
13805            "^L" => PerlValue::string(self.formfeed_string.clone()),
13806            "^M" => PerlValue::string(self.emergency_memory.clone()),
13807            "^N" => PerlValue::string(self.last_subpattern_name.clone()),
13808            "^X" => PerlValue::string(self.executable_path.clone()),
13809            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
13810            "^TAINT" | "^TAINTED" => PerlValue::integer(0),
13811            "^UNICODE" => PerlValue::integer(if self.utf8_pragma { 1 } else { 0 }),
13812            "^OPEN" => PerlValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
13813            "^UTF8LOCALE" => PerlValue::integer(0),
13814            "^UTF8CACHE" => PerlValue::integer(-1),
13815            _ if name.starts_with('^') && name.len() > 1 => self
13816                .special_caret_scalars
13817                .get(name)
13818                .cloned()
13819                .unwrap_or(PerlValue::UNDEF),
13820            _ if name.starts_with('#') && name.len() > 1 => {
13821                let arr = &name[1..];
13822                let aname = self.stash_array_name_for_package(arr);
13823                let len = self.scope.array_len(&aname);
13824                PerlValue::integer(len as i64 - 1)
13825            }
13826            _ => self.scope.get_scalar(name),
13827        }
13828    }
13829
13830    pub(crate) fn set_special_var(&mut self, name: &str, val: &PerlValue) -> Result<(), PerlError> {
13831        let name = self.english_scalar_name(name);
13832        match name {
13833            "!" => {
13834                let code = val.to_int() as i32;
13835                self.errno_code = code;
13836                self.errno = if code == 0 {
13837                    String::new()
13838                } else {
13839                    std::io::Error::from_raw_os_error(code).to_string()
13840                };
13841            }
13842            "@" => {
13843                if let Some((code, msg)) = val.errno_dual_parts() {
13844                    self.eval_error_code = code;
13845                    self.eval_error = msg;
13846                } else {
13847                    self.eval_error = val.to_string();
13848                    let mut code = val.to_int() as i32;
13849                    if code == 0 && !self.eval_error.is_empty() {
13850                        code = 1;
13851                    }
13852                    self.eval_error_code = code;
13853                }
13854            }
13855            "." => {
13856                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
13857                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
13858                let n = val.to_int();
13859                if self.last_readline_handle.is_empty() {
13860                    self.line_number = n;
13861                } else {
13862                    self.handle_line_numbers
13863                        .insert(self.last_readline_handle.clone(), n);
13864                }
13865            }
13866            "0" => self.program_name = val.to_string(),
13867            "/" => {
13868                self.irs = if val.is_undef() {
13869                    None
13870                } else {
13871                    Some(val.to_string())
13872                }
13873            }
13874            "\\" => self.ors = val.to_string(),
13875            "," => self.ofs = val.to_string(),
13876            ";" => self.subscript_sep = val.to_string(),
13877            "\"" => self.list_separator = val.to_string(),
13878            "%" => self.format_page_number = val.to_int(),
13879            "=" => self.format_lines_per_page = val.to_int(),
13880            "-" => self.format_lines_left = val.to_int(),
13881            ":" => self.format_line_break_chars = val.to_string(),
13882            "*" => self.multiline_match = val.to_int() != 0,
13883            "^" => self.format_top_name = val.to_string(),
13884            "INC" => self.inc_hook_index = val.to_int(),
13885            "^A" => self.accumulator_format = val.to_string(),
13886            "^F" => self.max_system_fd = val.to_int(),
13887            "^L" => self.formfeed_string = val.to_string(),
13888            "^M" => self.emergency_memory = val.to_string(),
13889            "^I" => self.inplace_edit = val.to_string(),
13890            "^D" => self.debug_flags = val.to_int(),
13891            "^P" => self.perl_debug_flags = val.to_int(),
13892            "^W" => self.warnings = val.to_int() != 0,
13893            "^H" => self.compile_hints = val.to_int(),
13894            "^WARNING_BITS" => self.warning_bits = val.to_int(),
13895            "|" => {
13896                self.output_autoflush = val.to_int() != 0;
13897                if self.output_autoflush {
13898                    let _ = io::stdout().flush();
13899                }
13900            }
13901            // Read-only or pid-backed
13902            "$$"
13903            | "]"
13904            | "^S"
13905            | "ARGV"
13906            | "?"
13907            | "^O"
13908            | "^T"
13909            | "^V"
13910            | "^E"
13911            | "^GLOBAL_PHASE"
13912            | "^MATCH"
13913            | "^PREMATCH"
13914            | "^POSTMATCH"
13915            | "^LAST_SUBMATCH_RESULT"
13916            | "^C"
13917            | "^N"
13918            | "^X"
13919            | "^TAINT"
13920            | "^TAINTED"
13921            | "^UNICODE"
13922            | "^UTF8LOCALE"
13923            | "^UTF8CACHE"
13924            | "+"
13925            | "<"
13926            | ">"
13927            | "("
13928            | ")" => {}
13929            _ if name.starts_with('^') && name.len() > 1 => {
13930                self.special_caret_scalars
13931                    .insert(name.to_string(), val.clone());
13932            }
13933            _ => self.scope.set_scalar(name, val.clone())?,
13934        }
13935        Ok(())
13936    }
13937
13938    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
13939        match &expr.kind {
13940            ExprKind::ArrayVar(name) => Ok(name.clone()),
13941            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
13942            _ => Err(PerlError::runtime("Expected array", expr.line).into()),
13943        }
13944    }
13945
13946    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
13947    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
13948        match &expr.kind {
13949            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
13950            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
13951            _ => expr,
13952        }
13953    }
13954
13955    /// `@$aref` / `@{...}` after optional peeling — for `SpliceExpr` / `pop` operations.
13956    fn try_eval_array_deref_container(
13957        &mut self,
13958        expr: &Expr,
13959    ) -> Result<Option<PerlValue>, FlowOrError> {
13960        let e = Self::peel_array_builtin_operand(expr);
13961        if let ExprKind::Deref {
13962            expr: inner,
13963            kind: Sigil::Array,
13964        } = &e.kind
13965        {
13966            return Ok(Some(self.eval_expr(inner)?));
13967        }
13968        Ok(None)
13969    }
13970
13971    /// Current package (`main` when `__PACKAGE__` is unset or empty).
13972    fn current_package(&self) -> String {
13973        let s = self.scope.get_scalar("__PACKAGE__").to_string();
13974        if s.is_empty() {
13975            "main".to_string()
13976        } else {
13977            s
13978        }
13979    }
13980
13981    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
13982    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
13983    pub(crate) fn package_version_scalar(
13984        &mut self,
13985        package: &str,
13986    ) -> PerlResult<Option<PerlValue>> {
13987        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
13988        let _ = self
13989            .scope
13990            .set_scalar("__PACKAGE__", PerlValue::string(package.to_string()));
13991        let ver = self.get_special_var("VERSION");
13992        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
13993        Ok(if ver.is_undef() { None } else { Some(ver) })
13994    }
13995
13996    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
13997    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<PerlSub>> {
13998        let root = if start_package.is_empty() {
13999            "main"
14000        } else {
14001            start_package
14002        };
14003        for pkg in self.mro_linearize(root) {
14004            let key = if pkg == "main" {
14005                "AUTOLOAD".to_string()
14006            } else {
14007                format!("{}::AUTOLOAD", pkg)
14008            };
14009            if let Some(s) = self.subs.get(&key) {
14010                return Some(s.clone());
14011            }
14012        }
14013        None
14014    }
14015
14016    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
14017    /// qualified missing sub or method name and invoke the handler (same argument list as the
14018    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
14019    /// the package prefix of the missing name (or current package).
14020    pub(crate) fn try_autoload_call(
14021        &mut self,
14022        missing_name: &str,
14023        args: Vec<PerlValue>,
14024        line: usize,
14025        want: WantarrayCtx,
14026        method_invocant_class: Option<&str>,
14027    ) -> Option<ExecResult> {
14028        let pkg = self.current_package();
14029        let full = if missing_name.contains("::") {
14030            missing_name.to_string()
14031        } else {
14032            format!("{}::{}", pkg, missing_name)
14033        };
14034        let start_pkg = method_invocant_class.unwrap_or_else(|| {
14035            full.rsplit_once("::")
14036                .map(|(p, _)| p)
14037                .filter(|p| !p.is_empty())
14038                .unwrap_or("main")
14039        });
14040        let sub = self.resolve_autoload_sub(start_pkg)?;
14041        if let Err(e) = self
14042            .scope
14043            .set_scalar("AUTOLOAD", PerlValue::string(full.clone()))
14044        {
14045            return Some(Err(e.into()));
14046        }
14047        Some(self.call_sub(&sub, args, want, line))
14048    }
14049
14050    pub(crate) fn with_topic_default_args(&self, args: Vec<PerlValue>) -> Vec<PerlValue> {
14051        if args.is_empty() {
14052            vec![self.scope.get_scalar("_").clone()]
14053        } else {
14054            args
14055        }
14056    }
14057
14058    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
14059    /// and [`crate::bytecode::Op::IndirectCall`].
14060    pub(crate) fn dispatch_indirect_call(
14061        &mut self,
14062        target: PerlValue,
14063        arg_vals: Vec<PerlValue>,
14064        want: WantarrayCtx,
14065        line: usize,
14066    ) -> ExecResult {
14067        if let Some(sub) = target.as_code_ref() {
14068            return self.call_sub(&sub, arg_vals, want, line);
14069        }
14070        if let Some(name) = target.as_str() {
14071            return self.call_named_sub(&name, arg_vals, line, want);
14072        }
14073        Err(PerlError::runtime("Can't use non-code reference as a subroutine", line).into())
14074    }
14075
14076    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
14077    /// Bare-name dispatch for stryke list builtins (`sum`, `min`, `uniq`, `reduce`, `zip`, …).
14078    /// Resolves short aliases (`uq`, `shuf`, `chk`, `win`, `fst`, `rd`, `med`, `std`, `var`, …)
14079    /// and forwards to [`crate::list_builtins::dispatch_by_name`].
14080    pub(crate) fn call_bare_list_builtin(
14081        &mut self,
14082        name: &str,
14083        args: Vec<PerlValue>,
14084        line: usize,
14085        want: WantarrayCtx,
14086    ) -> ExecResult {
14087        let canonical = match name {
14088            "distinct" | "uq" => "uniq",
14089            "shuf" => "shuffle",
14090            "chk" => "chunked",
14091            "win" => "windowed",
14092            "zp" => "zip",
14093            "fst" => "first",
14094            "rd" => "reduce",
14095            "med" => "median",
14096            "std" => "stddev",
14097            "var" => "variance",
14098            other => other,
14099        };
14100        // List builtins like `sum`, `min`, `uniq` operate on a list — an empty
14101        // input must aggregate to the identity (0/undef), NOT default to $_.
14102        // `sum(@empty_after_grep)` was returning $_ before this; that produced
14103        // surprising results downstream (e.g. `… |> grep {0} |> sum` = topic).
14104        match crate::list_builtins::dispatch_by_name(self, canonical, &args, want) {
14105            Some(r) => r,
14106            None => Err(PerlError::runtime(
14107                format!("internal: not a stryke list builtin: {name}"),
14108                line,
14109            )
14110            .into()),
14111        }
14112    }
14113
14114    fn call_named_sub(
14115        &mut self,
14116        name: &str,
14117        args: Vec<PerlValue>,
14118        line: usize,
14119        want: WantarrayCtx,
14120    ) -> ExecResult {
14121        if let Some(sub) = self.resolve_sub_by_name(name) {
14122            let args = self.with_topic_default_args(args);
14123            return self.call_sub(&sub, args, want, line);
14124        }
14125        match name {
14126            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
14127            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
14128            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
14129            | "none" | "notall" | "first" | "fst" | "reduce" | "rd" | "reductions" | "sum"
14130            | "sum0" | "product" | "min" | "max" | "minstr" | "maxstr" | "mean" | "median"
14131            | "med" | "mode" | "stddev" | "std" | "variance" | "var" | "pairs" | "unpairs"
14132            | "pairkeys" | "pairvalues" | "pairgrep" | "pairmap" | "pairfirst" | "blessed"
14133            | "refaddr" | "reftype" | "weaken" | "unweaken" | "isweak" | "set_subname"
14134            | "subname" | "unicode_to_native" => {
14135                self.call_bare_list_builtin(name, args, line, want)
14136            }
14137            "deque" => {
14138                if !args.is_empty() {
14139                    return Err(PerlError::runtime("deque() takes no arguments", line).into());
14140                }
14141                Ok(PerlValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
14142            }
14143            "defer__internal" => {
14144                if args.len() != 1 {
14145                    return Err(PerlError::runtime(
14146                        "defer__internal expects one coderef argument",
14147                        line,
14148                    )
14149                    .into());
14150                }
14151                self.scope.push_defer(args[0].clone());
14152                Ok(PerlValue::UNDEF)
14153            }
14154            "heap" => {
14155                if args.len() != 1 {
14156                    return Err(
14157                        PerlError::runtime("heap() expects one comparator sub", line).into(),
14158                    );
14159                }
14160                if let Some(sub) = args[0].as_code_ref() {
14161                    Ok(PerlValue::heap(Arc::new(Mutex::new(PerlHeap {
14162                        items: Vec::new(),
14163                        cmp: Arc::clone(&sub),
14164                    }))))
14165                } else {
14166                    Err(PerlError::runtime("heap() requires a code reference", line).into())
14167                }
14168            }
14169            "pipeline" => {
14170                let mut items = Vec::new();
14171                for v in args {
14172                    if let Some(a) = v.as_array_vec() {
14173                        items.extend(a);
14174                    } else {
14175                        items.push(v);
14176                    }
14177                }
14178                Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
14179                    source: items,
14180                    ops: Vec::new(),
14181                    has_scalar_terminal: false,
14182                    par_stream: false,
14183                    streaming: false,
14184                    streaming_workers: 0,
14185                    streaming_buffer: 256,
14186                }))))
14187            }
14188            "par_pipeline" => {
14189                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
14190                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
14191                        .map_err(Into::into);
14192                }
14193                Ok(self.builtin_par_pipeline_stream(&args, line)?)
14194            }
14195            "par_pipeline_stream" => {
14196                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
14197                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
14198                        .map_err(Into::into);
14199                }
14200                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
14201            }
14202            "ppool" => {
14203                if args.len() != 1 {
14204                    return Err(PerlError::runtime(
14205                        "ppool() expects one argument (worker count)",
14206                        line,
14207                    )
14208                    .into());
14209                }
14210                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
14211            }
14212            "barrier" => {
14213                if args.len() != 1 {
14214                    return Err(PerlError::runtime(
14215                        "barrier() expects one argument (party count)",
14216                        line,
14217                    )
14218                    .into());
14219                }
14220                let n = args[0].to_int().max(1) as usize;
14221                Ok(PerlValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
14222            }
14223            "cluster" => {
14224                let items = if args.len() == 1 {
14225                    args[0].to_list()
14226                } else {
14227                    args.to_vec()
14228                };
14229                let c = RemoteCluster::from_list_args(&items)
14230                    .map_err(|msg| PerlError::runtime(msg, line))?;
14231                Ok(PerlValue::remote_cluster(Arc::new(c)))
14232            }
14233            _ => {
14234                // Late static binding: static::method() resolves to runtime class of $self
14235                if let Some(method_name) = name.strip_prefix("static::") {
14236                    let self_val = self.scope.get_scalar("self");
14237                    if let Some(c) = self_val.as_class_inst() {
14238                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
14239                            if let Some(ref body) = m.body {
14240                                let params = m.params.clone();
14241                                let mut call_args = vec![self_val.clone()];
14242                                call_args.extend(args);
14243                                return match self.call_class_method(body, &params, call_args, line)
14244                                {
14245                                    Ok(v) => Ok(v),
14246                                    Err(FlowOrError::Error(e)) => Err(e.into()),
14247                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14248                                    Err(e) => Err(e),
14249                                };
14250                            }
14251                        }
14252                        return Err(PerlError::runtime(
14253                            format!(
14254                                "static::{} — method not found on class {}",
14255                                method_name, c.def.name
14256                            ),
14257                            line,
14258                        )
14259                        .into());
14260                    }
14261                    return Err(PerlError::runtime(
14262                        "static:: can only be used inside a class method",
14263                        line,
14264                    )
14265                    .into());
14266                }
14267                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
14268                if let Some(def) = self.struct_defs.get(name).cloned() {
14269                    return self.struct_construct(&def, args, line);
14270                }
14271                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
14272                if let Some(def) = self.class_defs.get(name).cloned() {
14273                    return self.class_construct(&def, args, line);
14274                }
14275                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
14276                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
14277                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
14278                        return self.enum_construct(&def, variant_name, args, line);
14279                    }
14280                }
14281                // Check for static class method or static field: Math::add(...) / Counter::count()
14282                if let Some((class_name, member_name)) = name.rsplit_once("::") {
14283                    if let Some(def) = self.class_defs.get(class_name).cloned() {
14284                        // Static method
14285                        if let Some(m) = def.method(member_name) {
14286                            if m.is_static {
14287                                if let Some(ref body) = m.body {
14288                                    let params = m.params.clone();
14289                                    return match self.call_static_class_method(
14290                                        body,
14291                                        &params,
14292                                        args.clone(),
14293                                        line,
14294                                    ) {
14295                                        Ok(v) => Ok(v),
14296                                        Err(FlowOrError::Error(e)) => Err(e.into()),
14297                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14298                                        Err(e) => Err(e),
14299                                    };
14300                                }
14301                            }
14302                        }
14303                        // Static field access: getter (0 args) or setter (1 arg)
14304                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
14305                            let key = format!("{}::{}", class_name, member_name);
14306                            match args.len() {
14307                                0 => {
14308                                    let val = self.scope.get_scalar(&key);
14309                                    return Ok(val);
14310                                }
14311                                1 => {
14312                                    let _ = self.scope.set_scalar(&key, args[0].clone());
14313                                    return Ok(args[0].clone());
14314                                }
14315                                _ => {
14316                                    return Err(PerlError::runtime(
14317                                        format!(
14318                                            "static field `{}::{}` takes 0 or 1 arguments",
14319                                            class_name, member_name
14320                                        ),
14321                                        line,
14322                                    )
14323                                    .into());
14324                                }
14325                            }
14326                        }
14327                    }
14328                }
14329                let args = self.with_topic_default_args(args);
14330                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
14331                    return r;
14332                }
14333                Err(PerlError::runtime(self.undefined_subroutine_call_message(name), line).into())
14334            }
14335        }
14336    }
14337
14338    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
14339    pub(crate) fn struct_construct(
14340        &mut self,
14341        def: &Arc<StructDef>,
14342        args: Vec<PerlValue>,
14343        line: usize,
14344    ) -> ExecResult {
14345        // Detect if args are named (key => value pairs) or positional
14346        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
14347        let is_named = args.len() >= 2
14348            && args.len().is_multiple_of(2)
14349            && args.iter().step_by(2).all(|v| {
14350                let s = v.to_string();
14351                def.field_index(&s).is_some()
14352            });
14353
14354        let provided = if is_named {
14355            // Named construction: Point(x => 1, y => 2)
14356            let mut pairs = Vec::new();
14357            let mut i = 0;
14358            while i + 1 < args.len() {
14359                let k = args[i].to_string();
14360                let v = args[i + 1].clone();
14361                pairs.push((k, v));
14362                i += 2;
14363            }
14364            pairs
14365        } else {
14366            // Positional construction: Point(1, 2) fills fields in declaration order
14367            def.fields
14368                .iter()
14369                .zip(args.iter())
14370                .map(|(f, v)| (f.name.clone(), v.clone()))
14371                .collect()
14372        };
14373
14374        // Evaluate default expressions
14375        let mut defaults = Vec::with_capacity(def.fields.len());
14376        for field in &def.fields {
14377            if let Some(ref expr) = field.default {
14378                let val = self.eval_expr(expr)?;
14379                defaults.push(Some(val));
14380            } else {
14381                defaults.push(None);
14382            }
14383        }
14384
14385        Ok(crate::native_data::struct_new_with_defaults(
14386            def, &provided, &defaults, line,
14387        )?)
14388    }
14389
14390    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
14391    pub(crate) fn class_construct(
14392        &mut self,
14393        def: &Arc<ClassDef>,
14394        args: Vec<PerlValue>,
14395        _line: usize,
14396    ) -> ExecResult {
14397        use crate::value::ClassInstance;
14398
14399        // Prevent instantiation of abstract classes
14400        if def.is_abstract {
14401            return Err(PerlError::runtime(
14402                format!("cannot instantiate abstract class `{}`", def.name),
14403                _line,
14404            )
14405            .into());
14406        }
14407
14408        // Collect all fields from inheritance chain (parent fields first)
14409        let all_fields = self.collect_class_fields(def);
14410
14411        // Check if args are named
14412        let is_named = args.len() >= 2
14413            && args.len().is_multiple_of(2)
14414            && args.iter().step_by(2).all(|v| {
14415                let s = v.to_string();
14416                all_fields.iter().any(|(name, _, _)| name == &s)
14417            });
14418
14419        let provided: Vec<(String, PerlValue)> = if is_named {
14420            let mut pairs = Vec::new();
14421            let mut i = 0;
14422            while i + 1 < args.len() {
14423                let k = args[i].to_string();
14424                let v = args[i + 1].clone();
14425                pairs.push((k, v));
14426                i += 2;
14427            }
14428            pairs
14429        } else {
14430            all_fields
14431                .iter()
14432                .zip(args.iter())
14433                .map(|((name, _, _), v)| (name.clone(), v.clone()))
14434                .collect()
14435        };
14436
14437        // Build values array for all fields (inherited + own) with type checking
14438        let mut values = Vec::with_capacity(all_fields.len());
14439        for (name, default, ty) in &all_fields {
14440            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
14441                val.clone()
14442            } else if let Some(ref expr) = default {
14443                self.eval_expr(expr)?
14444            } else {
14445                PerlValue::UNDEF
14446            };
14447            ty.check_value(&val).map_err(|msg| {
14448                PerlError::type_error(
14449                    format!("class {} field `{}`: {}", def.name, name, msg),
14450                    _line,
14451                )
14452            })?;
14453            values.push(val);
14454        }
14455
14456        // Compute full ISA chain for type checking
14457        let isa_chain = self.mro_linearize(&def.name);
14458        let instance = PerlValue::class_inst(Arc::new(ClassInstance::new_with_isa(
14459            Arc::clone(def),
14460            values,
14461            isa_chain,
14462        )));
14463
14464        // Call BUILD hooks: parent BUILD first, then child BUILD
14465        let build_chain = self.collect_build_chain(def);
14466        if !build_chain.is_empty() {
14467            for (body, params) in &build_chain {
14468                let call_args = vec![instance.clone()];
14469                match self.call_class_method(body, params, call_args, _line) {
14470                    Ok(_) => {}
14471                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
14472                    Err(e) => return Err(e),
14473                }
14474            }
14475        }
14476
14477        Ok(instance)
14478    }
14479
14480    /// Collect BUILD methods from parent to child order.
14481    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14482        let mut chain = Vec::new();
14483        // Parent BUILD first
14484        for parent_name in &def.extends {
14485            if let Some(parent_def) = self.class_defs.get(parent_name) {
14486                chain.extend(self.collect_build_chain(parent_def));
14487            }
14488        }
14489        // Own BUILD
14490        if let Some(m) = def.method("BUILD") {
14491            if let Some(ref body) = m.body {
14492                chain.push((body.clone(), m.params.clone()));
14493            }
14494        }
14495        chain
14496    }
14497
14498    /// Collect all fields from a class and its parent hierarchy (parent fields first).
14499    /// Returns (name, default, type, visibility, owning_class_name).
14500    fn collect_class_fields(
14501        &self,
14502        def: &ClassDef,
14503    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
14504        self.collect_class_fields_full(def)
14505            .into_iter()
14506            .map(|(name, default, ty, _, _)| (name, default, ty))
14507            .collect()
14508    }
14509
14510    /// Like collect_class_fields but includes visibility and owning class name.
14511    fn collect_class_fields_full(
14512        &self,
14513        def: &ClassDef,
14514    ) -> Vec<(
14515        String,
14516        Option<Expr>,
14517        crate::ast::PerlTypeName,
14518        crate::ast::Visibility,
14519        String,
14520    )> {
14521        let mut all_fields = Vec::new();
14522
14523        for parent_name in &def.extends {
14524            if let Some(parent_def) = self.class_defs.get(parent_name) {
14525                let parent_fields = self.collect_class_fields_full(parent_def);
14526                all_fields.extend(parent_fields);
14527            }
14528        }
14529
14530        for field in &def.fields {
14531            all_fields.push((
14532                field.name.clone(),
14533                field.default.clone(),
14534                field.ty.clone(),
14535                field.visibility,
14536                def.name.clone(),
14537            ));
14538        }
14539
14540        all_fields
14541    }
14542
14543    /// Collect all method names from class and parents (deduplicates, child overrides parent).
14544    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
14545        // Parent methods first
14546        for parent_name in &def.extends {
14547            if let Some(parent_def) = self.class_defs.get(parent_name) {
14548                self.collect_class_method_names(parent_def, names);
14549            }
14550        }
14551        // Own methods (add if not already present — child overrides parent name)
14552        for m in &def.methods {
14553            if !m.is_static && !names.contains(&m.name) {
14554                names.push(m.name.clone());
14555            }
14556        }
14557    }
14558
14559    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
14560    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14561        let mut chain = Vec::new();
14562        // Own DESTROY first
14563        if let Some(m) = def.method("DESTROY") {
14564            if let Some(ref body) = m.body {
14565                chain.push((body.clone(), m.params.clone()));
14566            }
14567        }
14568        // Then parent DESTROY
14569        for parent_name in &def.extends {
14570            if let Some(parent_def) = self.class_defs.get(parent_name) {
14571                chain.extend(self.collect_destroy_chain(parent_def));
14572            }
14573        }
14574        chain
14575    }
14576
14577    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
14578    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
14579        if let Some(def) = self.class_defs.get(child) {
14580            for parent in &def.extends {
14581                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
14582                    return true;
14583                }
14584            }
14585        }
14586        false
14587    }
14588
14589    /// Find a method in a class or its parent hierarchy (child methods override parent).
14590    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
14591        // First check the current class
14592        if let Some(m) = def.method(method) {
14593            return Some((m.clone(), def.name.clone()));
14594        }
14595        // Then check parent classes
14596        for parent_name in &def.extends {
14597            if let Some(parent_def) = self.class_defs.get(parent_name) {
14598                if let Some(result) = self.find_class_method(parent_def, method) {
14599                    return Some(result);
14600                }
14601            }
14602        }
14603        None
14604    }
14605
14606    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
14607    pub(crate) fn enum_construct(
14608        &mut self,
14609        def: &Arc<EnumDef>,
14610        variant_name: &str,
14611        args: Vec<PerlValue>,
14612        line: usize,
14613    ) -> ExecResult {
14614        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
14615            FlowOrError::Error(PerlError::runtime(
14616                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
14617                line,
14618            ))
14619        })?;
14620        let variant = &def.variants[variant_idx];
14621        let data = if variant.ty.is_some() {
14622            if args.is_empty() {
14623                return Err(PerlError::runtime(
14624                    format!(
14625                        "enum variant `{}::{}` requires data",
14626                        def.name, variant_name
14627                    ),
14628                    line,
14629                )
14630                .into());
14631            }
14632            if args.len() == 1 {
14633                args.into_iter().next().unwrap()
14634            } else {
14635                PerlValue::array(args)
14636            }
14637        } else {
14638            if !args.is_empty() {
14639                return Err(PerlError::runtime(
14640                    format!(
14641                        "enum variant `{}::{}` does not take data",
14642                        def.name, variant_name
14643                    ),
14644                    line,
14645                )
14646                .into());
14647            }
14648            PerlValue::UNDEF
14649        };
14650        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
14651        Ok(PerlValue::enum_inst(Arc::new(inst)))
14652    }
14653
14654    /// True if `name` is a registered or standard process-global handle.
14655    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
14656        matches!(name, "STDIN" | "STDOUT" | "STDERR")
14657            || self.input_handles.contains_key(name)
14658            || self.output_handles.contains_key(name)
14659            || self.io_file_slots.contains_key(name)
14660            || self.pipe_children.contains_key(name)
14661    }
14662
14663    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
14664    pub(crate) fn io_handle_method(
14665        &mut self,
14666        name: &str,
14667        method: &str,
14668        args: &[PerlValue],
14669        line: usize,
14670    ) -> PerlResult<PerlValue> {
14671        match method {
14672            "print" => self.io_handle_print(name, args, false, line),
14673            "say" => self.io_handle_print(name, args, true, line),
14674            "printf" => self.io_handle_printf(name, args, line),
14675            "getline" | "readline" => {
14676                if !args.is_empty() {
14677                    return Err(PerlError::runtime(
14678                        format!("{}: too many arguments", method),
14679                        line,
14680                    ));
14681                }
14682                self.readline_builtin_execute(Some(name))
14683            }
14684            "close" => {
14685                if !args.is_empty() {
14686                    return Err(PerlError::runtime("close: too many arguments", line));
14687                }
14688                self.close_builtin_execute(name.to_string())
14689            }
14690            "eof" => {
14691                if !args.is_empty() {
14692                    return Err(PerlError::runtime("eof: too many arguments", line));
14693                }
14694                let at_eof = !self.has_input_handle(name);
14695                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
14696            }
14697            "getc" => {
14698                if !args.is_empty() {
14699                    return Err(PerlError::runtime("getc: too many arguments", line));
14700                }
14701                match crate::builtins::try_builtin(
14702                    self,
14703                    "getc",
14704                    &[PerlValue::string(name.to_string())],
14705                    line,
14706                ) {
14707                    Some(r) => r,
14708                    None => Err(PerlError::runtime("getc: not available", line)),
14709                }
14710            }
14711            "binmode" => match crate::builtins::try_builtin(
14712                self,
14713                "binmode",
14714                &[PerlValue::string(name.to_string())],
14715                line,
14716            ) {
14717                Some(r) => r,
14718                None => Err(PerlError::runtime("binmode: not available", line)),
14719            },
14720            "fileno" => match crate::builtins::try_builtin(
14721                self,
14722                "fileno",
14723                &[PerlValue::string(name.to_string())],
14724                line,
14725            ) {
14726                Some(r) => r,
14727                None => Err(PerlError::runtime("fileno: not available", line)),
14728            },
14729            "flush" => {
14730                if !args.is_empty() {
14731                    return Err(PerlError::runtime("flush: too many arguments", line));
14732                }
14733                self.io_handle_flush(name, line)
14734            }
14735            _ => Err(PerlError::runtime(
14736                format!("Unknown method for filehandle: {}", method),
14737                line,
14738            )),
14739        }
14740    }
14741
14742    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> PerlResult<PerlValue> {
14743        match handle_name {
14744            "STDOUT" => {
14745                let _ = IoWrite::flush(&mut io::stdout());
14746            }
14747            "STDERR" => {
14748                let _ = IoWrite::flush(&mut io::stderr());
14749            }
14750            name => {
14751                if let Some(writer) = self.output_handles.get_mut(name) {
14752                    let _ = IoWrite::flush(&mut *writer);
14753                } else {
14754                    return Err(PerlError::runtime(
14755                        format!("flush on unopened filehandle {}", name),
14756                        line,
14757                    ));
14758                }
14759            }
14760        }
14761        Ok(PerlValue::integer(1))
14762    }
14763
14764    fn io_handle_print(
14765        &mut self,
14766        handle_name: &str,
14767        args: &[PerlValue],
14768        newline: bool,
14769        line: usize,
14770    ) -> PerlResult<PerlValue> {
14771        if newline && (self.feature_bits & FEAT_SAY) == 0 {
14772            return Err(PerlError::runtime(
14773                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
14774                line,
14775            ));
14776        }
14777        let mut output = String::new();
14778        if args.is_empty() {
14779            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
14780            output.push_str(&self.scope.get_scalar("_").to_string());
14781        } else {
14782            for (i, val) in args.iter().enumerate() {
14783                if i > 0 && !self.ofs.is_empty() {
14784                    output.push_str(&self.ofs);
14785                }
14786                output.push_str(&val.to_string());
14787            }
14788        }
14789        if newline {
14790            output.push('\n');
14791        }
14792        output.push_str(&self.ors);
14793
14794        self.write_formatted_print(handle_name, &output, line)?;
14795        Ok(PerlValue::integer(1))
14796    }
14797
14798    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
14799    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
14800    pub(crate) fn write_formatted_print(
14801        &mut self,
14802        handle_name: &str,
14803        output: &str,
14804        line: usize,
14805    ) -> PerlResult<()> {
14806        match handle_name {
14807            "STDOUT" => {
14808                if !self.suppress_stdout {
14809                    print!("{}", output);
14810                    if self.output_autoflush {
14811                        let _ = io::stdout().flush();
14812                    }
14813                }
14814            }
14815            "STDERR" => {
14816                eprint!("{}", output);
14817                let _ = io::stderr().flush();
14818            }
14819            name => {
14820                if let Some(writer) = self.output_handles.get_mut(name) {
14821                    let _ = writer.write_all(output.as_bytes());
14822                    if self.output_autoflush {
14823                        let _ = writer.flush();
14824                    }
14825                } else {
14826                    return Err(PerlError::runtime(
14827                        format!("print on unopened filehandle {}", name),
14828                        line,
14829                    ));
14830                }
14831            }
14832        }
14833        Ok(())
14834    }
14835
14836    fn io_handle_printf(
14837        &mut self,
14838        handle_name: &str,
14839        args: &[PerlValue],
14840        line: usize,
14841    ) -> PerlResult<PerlValue> {
14842        let (fmt, rest): (String, &[PerlValue]) = if args.is_empty() {
14843            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
14844                Ok(s) => s,
14845                Err(FlowOrError::Error(e)) => return Err(e),
14846                Err(FlowOrError::Flow(_)) => {
14847                    return Err(PerlError::runtime(
14848                        "printf: unexpected control flow in sprintf",
14849                        line,
14850                    ));
14851                }
14852            };
14853            (s, &[])
14854        } else {
14855            (args[0].to_string(), &args[1..])
14856        };
14857        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
14858            Ok(s) => s,
14859            Err(FlowOrError::Error(e)) => return Err(e),
14860            Err(FlowOrError::Flow(_)) => {
14861                return Err(PerlError::runtime(
14862                    "printf: unexpected control flow in sprintf",
14863                    line,
14864                ));
14865            }
14866        };
14867        match handle_name {
14868            "STDOUT" => {
14869                if !self.suppress_stdout {
14870                    print!("{}", output);
14871                    if self.output_autoflush {
14872                        let _ = IoWrite::flush(&mut io::stdout());
14873                    }
14874                }
14875            }
14876            "STDERR" => {
14877                eprint!("{}", output);
14878                let _ = IoWrite::flush(&mut io::stderr());
14879            }
14880            name => {
14881                if let Some(writer) = self.output_handles.get_mut(name) {
14882                    let _ = writer.write_all(output.as_bytes());
14883                    if self.output_autoflush {
14884                        let _ = writer.flush();
14885                    }
14886                } else {
14887                    return Err(PerlError::runtime(
14888                        format!("printf on unopened filehandle {}", name),
14889                        line,
14890                    ));
14891                }
14892            }
14893        }
14894        Ok(PerlValue::integer(1))
14895    }
14896
14897    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
14898    pub(crate) fn try_native_method(
14899        &mut self,
14900        receiver: &PerlValue,
14901        method: &str,
14902        args: &[PerlValue],
14903        line: usize,
14904    ) -> Option<PerlResult<PerlValue>> {
14905        if let Some(name) = receiver.as_io_handle_name() {
14906            return Some(self.io_handle_method(&name, method, args, line));
14907        }
14908        if let Some(ref s) = receiver.as_str() {
14909            if self.is_bound_handle(s) {
14910                return Some(self.io_handle_method(s, method, args, line));
14911            }
14912        }
14913        if let Some(c) = receiver.as_sqlite_conn() {
14914            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
14915        }
14916        if let Some(s) = receiver.as_struct_inst() {
14917            // Field access: $p->x or $p->x(value)
14918            if let Some(idx) = s.def.field_index(method) {
14919                match args.len() {
14920                    0 => {
14921                        return Some(Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF)));
14922                    }
14923                    1 => {
14924                        let field = &s.def.fields[idx];
14925                        let new_val = args[0].clone();
14926                        if let Err(msg) = field.ty.check_value(&new_val) {
14927                            return Some(Err(PerlError::type_error(
14928                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
14929                                line,
14930                            )));
14931                        }
14932                        s.set_field(idx, new_val.clone());
14933                        return Some(Ok(new_val));
14934                    }
14935                    _ => {
14936                        return Some(Err(PerlError::runtime(
14937                            format!(
14938                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
14939                                method,
14940                                args.len()
14941                            ),
14942                            line,
14943                        )));
14944                    }
14945                }
14946            }
14947            // Built-in struct methods
14948            match method {
14949                "with" => {
14950                    // Functional update: $p->with(x => 5) returns new instance with changed field
14951                    let mut new_values = s.get_values();
14952                    let mut i = 0;
14953                    while i + 1 < args.len() {
14954                        let k = args[i].to_string();
14955                        let v = args[i + 1].clone();
14956                        if let Some(idx) = s.def.field_index(&k) {
14957                            let field = &s.def.fields[idx];
14958                            if let Err(msg) = field.ty.check_value(&v) {
14959                                return Some(Err(PerlError::type_error(
14960                                    format!(
14961                                        "struct {} field `{}`: {}",
14962                                        s.def.name, field.name, msg
14963                                    ),
14964                                    line,
14965                                )));
14966                            }
14967                            new_values[idx] = v;
14968                        } else {
14969                            return Some(Err(PerlError::runtime(
14970                                format!("struct {}: unknown field `{}`", s.def.name, k),
14971                                line,
14972                            )));
14973                        }
14974                        i += 2;
14975                    }
14976                    return Some(Ok(PerlValue::struct_inst(Arc::new(
14977                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
14978                    ))));
14979                }
14980                "to_hash" => {
14981                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
14982                    if !args.is_empty() {
14983                        return Some(Err(PerlError::runtime(
14984                            "struct to_hash takes no arguments",
14985                            line,
14986                        )));
14987                    }
14988                    let mut map = IndexMap::new();
14989                    let values = s.get_values();
14990                    for (i, field) in s.def.fields.iter().enumerate() {
14991                        map.insert(field.name.clone(), values[i].clone());
14992                    }
14993                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
14994                }
14995                "fields" => {
14996                    // Field list: $p->fields returns field names
14997                    if !args.is_empty() {
14998                        return Some(Err(PerlError::runtime(
14999                            "struct fields takes no arguments",
15000                            line,
15001                        )));
15002                    }
15003                    let names: Vec<PerlValue> = s
15004                        .def
15005                        .fields
15006                        .iter()
15007                        .map(|f| PerlValue::string(f.name.clone()))
15008                        .collect();
15009                    return Some(Ok(PerlValue::array(names)));
15010                }
15011                "clone" => {
15012                    // Clone: $p->clone deep copies
15013                    if !args.is_empty() {
15014                        return Some(Err(PerlError::runtime(
15015                            "struct clone takes no arguments",
15016                            line,
15017                        )));
15018                    }
15019                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
15020                    return Some(Ok(PerlValue::struct_inst(Arc::new(
15021                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
15022                    ))));
15023                }
15024                _ => {}
15025            }
15026            // User-defined struct method
15027            if let Some(m) = s.def.method(method) {
15028                let body = m.body.clone();
15029                let params = m.params.clone();
15030                // Build args: $self is the receiver, then the passed args
15031                let mut call_args = vec![receiver.clone()];
15032                call_args.extend(args.iter().cloned());
15033                return Some(
15034                    match self.call_struct_method(&body, &params, call_args, line) {
15035                        Ok(v) => Ok(v),
15036                        Err(FlowOrError::Error(e)) => Err(e),
15037                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15038                        Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
15039                            "unexpected control flow in struct method",
15040                            line,
15041                        )),
15042                    },
15043                );
15044            }
15045            return None;
15046        }
15047        // Class instance method dispatch
15048        if let Some(c) = receiver.as_class_inst() {
15049            // Collect all fields from inheritance chain (with visibility)
15050            let all_fields_full = self.collect_class_fields_full(&c.def);
15051            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
15052                .iter()
15053                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
15054                .collect();
15055
15056            // Field access: $obj->name or $obj->name(value)
15057            if let Some(idx) = all_fields_full
15058                .iter()
15059                .position(|(name, _, _, _, _)| name == method)
15060            {
15061                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
15062
15063                // Enforce field visibility
15064                match vis {
15065                    crate::ast::Visibility::Private => {
15066                        // Only accessible from within the owning class's methods
15067                        let caller_class = self
15068                            .scope
15069                            .get_scalar("self")
15070                            .as_class_inst()
15071                            .map(|ci| ci.def.name.clone());
15072                        if caller_class.as_deref() != Some(owner_class.as_str()) {
15073                            return Some(Err(PerlError::runtime(
15074                                format!("field `{}` of class {} is private", method, owner_class),
15075                                line,
15076                            )));
15077                        }
15078                    }
15079                    crate::ast::Visibility::Protected => {
15080                        // Accessible from owning class or subclasses
15081                        let caller_class = self
15082                            .scope
15083                            .get_scalar("self")
15084                            .as_class_inst()
15085                            .map(|ci| ci.def.name.clone());
15086                        let allowed = caller_class.as_deref().is_some_and(|caller| {
15087                            caller == owner_class || self.class_inherits_from(caller, owner_class)
15088                        });
15089                        if !allowed {
15090                            return Some(Err(PerlError::runtime(
15091                                format!("field `{}` of class {} is protected", method, owner_class),
15092                                line,
15093                            )));
15094                        }
15095                    }
15096                    crate::ast::Visibility::Public => {}
15097                }
15098
15099                match args.len() {
15100                    0 => {
15101                        return Some(Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF)));
15102                    }
15103                    1 => {
15104                        let new_val = args[0].clone();
15105                        if let Err(msg) = ty.check_value(&new_val) {
15106                            return Some(Err(PerlError::type_error(
15107                                format!("class {} field `{}`: {}", c.def.name, method, msg),
15108                                line,
15109                            )));
15110                        }
15111                        c.set_field(idx, new_val.clone());
15112                        return Some(Ok(new_val));
15113                    }
15114                    _ => {
15115                        return Some(Err(PerlError::runtime(
15116                            format!(
15117                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
15118                                method,
15119                                args.len()
15120                            ),
15121                            line,
15122                        )));
15123                    }
15124                }
15125            }
15126            // Built-in class methods (use all_fields for inheritance)
15127            match method {
15128                "with" => {
15129                    let mut new_values = c.get_values();
15130                    let mut i = 0;
15131                    while i + 1 < args.len() {
15132                        let k = args[i].to_string();
15133                        let v = args[i + 1].clone();
15134                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
15135                            let (_, _, ref ty) = all_fields[idx];
15136                            if let Err(msg) = ty.check_value(&v) {
15137                                return Some(Err(PerlError::type_error(
15138                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
15139                                    line,
15140                                )));
15141                            }
15142                            new_values[idx] = v;
15143                        } else {
15144                            return Some(Err(PerlError::runtime(
15145                                format!("class {}: unknown field `{}`", c.def.name, k),
15146                                line,
15147                            )));
15148                        }
15149                        i += 2;
15150                    }
15151                    return Some(Ok(PerlValue::class_inst(Arc::new(
15152                        crate::value::ClassInstance::new_with_isa(
15153                            Arc::clone(&c.def),
15154                            new_values,
15155                            c.isa_chain.clone(),
15156                        ),
15157                    ))));
15158                }
15159                "to_hash" => {
15160                    if !args.is_empty() {
15161                        return Some(Err(PerlError::runtime(
15162                            "class to_hash takes no arguments",
15163                            line,
15164                        )));
15165                    }
15166                    let mut map = IndexMap::new();
15167                    let values = c.get_values();
15168                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
15169                        if let Some(v) = values.get(i) {
15170                            map.insert(name.clone(), v.clone());
15171                        }
15172                    }
15173                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
15174                }
15175                "fields" => {
15176                    if !args.is_empty() {
15177                        return Some(Err(PerlError::runtime(
15178                            "class fields takes no arguments",
15179                            line,
15180                        )));
15181                    }
15182                    let names: Vec<PerlValue> = all_fields
15183                        .iter()
15184                        .map(|(name, _, _)| PerlValue::string(name.clone()))
15185                        .collect();
15186                    return Some(Ok(PerlValue::array(names)));
15187                }
15188                "clone" => {
15189                    if !args.is_empty() {
15190                        return Some(Err(PerlError::runtime(
15191                            "class clone takes no arguments",
15192                            line,
15193                        )));
15194                    }
15195                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
15196                    return Some(Ok(PerlValue::class_inst(Arc::new(
15197                        crate::value::ClassInstance::new_with_isa(
15198                            Arc::clone(&c.def),
15199                            new_values,
15200                            c.isa_chain.clone(),
15201                        ),
15202                    ))));
15203                }
15204                "isa" => {
15205                    if args.len() != 1 {
15206                        return Some(Err(PerlError::runtime("isa requires one argument", line)));
15207                    }
15208                    let class_name = args[0].to_string();
15209                    let is_a = c.isa(&class_name);
15210                    return Some(Ok(if is_a {
15211                        PerlValue::integer(1)
15212                    } else {
15213                        PerlValue::string(String::new())
15214                    }));
15215                }
15216                "does" => {
15217                    if args.len() != 1 {
15218                        return Some(Err(PerlError::runtime("does requires one argument", line)));
15219                    }
15220                    let trait_name = args[0].to_string();
15221                    let implements = c.def.implements.contains(&trait_name);
15222                    return Some(Ok(if implements {
15223                        PerlValue::integer(1)
15224                    } else {
15225                        PerlValue::string(String::new())
15226                    }));
15227                }
15228                "methods" => {
15229                    if !args.is_empty() {
15230                        return Some(Err(PerlError::runtime("methods takes no arguments", line)));
15231                    }
15232                    let mut names = Vec::new();
15233                    self.collect_class_method_names(&c.def, &mut names);
15234                    let values: Vec<PerlValue> = names.into_iter().map(PerlValue::string).collect();
15235                    return Some(Ok(PerlValue::array(values)));
15236                }
15237                "superclass" => {
15238                    if !args.is_empty() {
15239                        return Some(Err(PerlError::runtime(
15240                            "superclass takes no arguments",
15241                            line,
15242                        )));
15243                    }
15244                    let parents: Vec<PerlValue> = c
15245                        .def
15246                        .extends
15247                        .iter()
15248                        .map(|s| PerlValue::string(s.clone()))
15249                        .collect();
15250                    return Some(Ok(PerlValue::array(parents)));
15251                }
15252                "destroy" => {
15253                    // Explicit destructor call — runs DESTROY chain child-first
15254                    let destroy_chain = self.collect_destroy_chain(&c.def);
15255                    for (body, params) in &destroy_chain {
15256                        let call_args = vec![receiver.clone()];
15257                        match self.call_class_method(body, params, call_args, line) {
15258                            Ok(_) => {}
15259                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
15260                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
15261                            Err(_) => {}
15262                        }
15263                    }
15264                    return Some(Ok(PerlValue::UNDEF));
15265                }
15266                _ => {}
15267            }
15268            // User-defined class method (search inheritance chain)
15269            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
15270                // Check visibility
15271                match m.visibility {
15272                    crate::ast::Visibility::Private => {
15273                        let caller_class = self
15274                            .scope
15275                            .get_scalar("self")
15276                            .as_class_inst()
15277                            .map(|ci| ci.def.name.clone());
15278                        if caller_class.as_deref() != Some(owner_class.as_str()) {
15279                            return Some(Err(PerlError::runtime(
15280                                format!("method `{}` of class {} is private", method, owner_class),
15281                                line,
15282                            )));
15283                        }
15284                    }
15285                    crate::ast::Visibility::Protected => {
15286                        let caller_class = self
15287                            .scope
15288                            .get_scalar("self")
15289                            .as_class_inst()
15290                            .map(|ci| ci.def.name.clone());
15291                        let allowed = caller_class.as_deref().is_some_and(|caller| {
15292                            caller == owner_class.as_str()
15293                                || self.class_inherits_from(caller, owner_class)
15294                        });
15295                        if !allowed {
15296                            return Some(Err(PerlError::runtime(
15297                                format!(
15298                                    "method `{}` of class {} is protected",
15299                                    method, owner_class
15300                                ),
15301                                line,
15302                            )));
15303                        }
15304                    }
15305                    crate::ast::Visibility::Public => {}
15306                }
15307                if let Some(ref body) = m.body {
15308                    let params = m.params.clone();
15309                    let mut call_args = vec![receiver.clone()];
15310                    call_args.extend(args.iter().cloned());
15311                    return Some(
15312                        match self.call_class_method(body, &params, call_args, line) {
15313                            Ok(v) => Ok(v),
15314                            Err(FlowOrError::Error(e)) => Err(e),
15315                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15316                            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
15317                                "unexpected control flow in class method",
15318                                line,
15319                            )),
15320                        },
15321                    );
15322                }
15323            }
15324            return None;
15325        }
15326        if let Some(d) = receiver.as_dataframe() {
15327            return Some(self.dataframe_method(d, method, args, line));
15328        }
15329        if let Some(s) = crate::value::set_payload(receiver) {
15330            return Some(self.set_method(s, method, args, line));
15331        }
15332        if let Some(d) = receiver.as_deque() {
15333            return Some(self.deque_method(d, method, args, line));
15334        }
15335        if let Some(h) = receiver.as_heap_pq() {
15336            return Some(self.heap_method(h, method, args, line));
15337        }
15338        if let Some(p) = receiver.as_pipeline() {
15339            return Some(self.pipeline_method(p, method, args, line));
15340        }
15341        if let Some(c) = receiver.as_capture() {
15342            return Some(self.capture_method(c, method, args, line));
15343        }
15344        if let Some(p) = receiver.as_ppool() {
15345            return Some(self.ppool_method(p, method, args, line));
15346        }
15347        if let Some(b) = receiver.as_barrier() {
15348            return Some(self.barrier_method(b, method, args, line));
15349        }
15350        if let Some(g) = receiver.as_generator() {
15351            if method == "next" {
15352                if !args.is_empty() {
15353                    return Some(Err(PerlError::runtime(
15354                        "generator->next takes no arguments",
15355                        line,
15356                    )));
15357                }
15358                return Some(self.generator_next(&g));
15359            }
15360            return None;
15361        }
15362        if let Some(arc) = receiver.as_atomic_arc() {
15363            let inner = arc.lock().clone();
15364            if let Some(d) = inner.as_deque() {
15365                return Some(self.deque_method(d, method, args, line));
15366            }
15367            if let Some(h) = inner.as_heap_pq() {
15368                return Some(self.heap_method(h, method, args, line));
15369            }
15370        }
15371        None
15372    }
15373
15374    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
15375    fn dataframe_method(
15376        &mut self,
15377        d: Arc<Mutex<PerlDataFrame>>,
15378        method: &str,
15379        args: &[PerlValue],
15380        line: usize,
15381    ) -> PerlResult<PerlValue> {
15382        match method {
15383            "nrow" | "nrows" => {
15384                if !args.is_empty() {
15385                    return Err(PerlError::runtime(
15386                        format!("dataframe {} takes no arguments", method),
15387                        line,
15388                    ));
15389                }
15390                Ok(PerlValue::integer(d.lock().nrows() as i64))
15391            }
15392            "ncol" | "ncols" => {
15393                if !args.is_empty() {
15394                    return Err(PerlError::runtime(
15395                        format!("dataframe {} takes no arguments", method),
15396                        line,
15397                    ));
15398                }
15399                Ok(PerlValue::integer(d.lock().ncols() as i64))
15400            }
15401            "filter" => {
15402                if args.len() != 1 {
15403                    return Err(PerlError::runtime(
15404                        "dataframe filter expects 1 argument (sub)",
15405                        line,
15406                    ));
15407                }
15408                let Some(sub) = args[0].as_code_ref() else {
15409                    return Err(PerlError::runtime(
15410                        "dataframe filter expects a code reference",
15411                        line,
15412                    ));
15413                };
15414                let df_guard = d.lock();
15415                let n = df_guard.nrows();
15416                let mut keep = vec![false; n];
15417                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
15418                    let row = df_guard.row_hashref(r);
15419                    self.scope_push_hook();
15420                    self.scope.set_topic(row);
15421                    if let Some(ref env) = sub.closure_env {
15422                        self.scope.restore_capture(env);
15423                    }
15424                    let pass = match self.exec_block_no_scope(&sub.body) {
15425                        Ok(v) => v.is_true(),
15426                        Err(_) => false,
15427                    };
15428                    self.scope_pop_hook();
15429                    *row_keep = pass;
15430                }
15431                let columns = df_guard.columns.clone();
15432                let cols: Vec<Vec<PerlValue>> = (0..df_guard.ncols())
15433                    .map(|i| {
15434                        let mut out = Vec::new();
15435                        for (r, pass_row) in keep.iter().enumerate().take(n) {
15436                            if *pass_row {
15437                                out.push(df_guard.cols[i][r].clone());
15438                            }
15439                        }
15440                        out
15441                    })
15442                    .collect();
15443                let group_by = df_guard.group_by.clone();
15444                drop(df_guard);
15445                let new_df = PerlDataFrame {
15446                    columns,
15447                    cols,
15448                    group_by,
15449                };
15450                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
15451            }
15452            "group_by" => {
15453                if args.len() != 1 {
15454                    return Err(PerlError::runtime(
15455                        "dataframe group_by expects 1 column name",
15456                        line,
15457                    ));
15458                }
15459                let key = args[0].to_string();
15460                let inner = d.lock();
15461                if inner.col_index(&key).is_none() {
15462                    return Err(PerlError::runtime(
15463                        format!("dataframe group_by: unknown column \"{}\"", key),
15464                        line,
15465                    ));
15466                }
15467                let new_df = PerlDataFrame {
15468                    columns: inner.columns.clone(),
15469                    cols: inner.cols.clone(),
15470                    group_by: Some(key),
15471                };
15472                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
15473            }
15474            "sum" => {
15475                if args.len() != 1 {
15476                    return Err(PerlError::runtime(
15477                        "dataframe sum expects 1 column name",
15478                        line,
15479                    ));
15480                }
15481                let col_name = args[0].to_string();
15482                let inner = d.lock();
15483                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
15484                    PerlError::runtime(
15485                        format!("dataframe sum: unknown column \"{}\"", col_name),
15486                        line,
15487                    )
15488                })?;
15489                match &inner.group_by {
15490                    Some(gcol) => {
15491                        let gi = inner.col_index(gcol).ok_or_else(|| {
15492                            PerlError::runtime(
15493                                format!("dataframe sum: unknown group column \"{}\"", gcol),
15494                                line,
15495                            )
15496                        })?;
15497                        let mut acc: IndexMap<String, f64> = IndexMap::new();
15498                        for r in 0..inner.nrows() {
15499                            let k = inner.cols[gi][r].to_string();
15500                            let v = inner.cols[val_idx][r].to_number();
15501                            *acc.entry(k).or_insert(0.0) += v;
15502                        }
15503                        let keys: Vec<String> = acc.keys().cloned().collect();
15504                        let sums: Vec<f64> = acc.values().copied().collect();
15505                        let cols = vec![
15506                            keys.into_iter().map(PerlValue::string).collect(),
15507                            sums.into_iter().map(PerlValue::float).collect(),
15508                        ];
15509                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
15510                        let out = PerlDataFrame {
15511                            columns,
15512                            cols,
15513                            group_by: None,
15514                        };
15515                        Ok(PerlValue::dataframe(Arc::new(Mutex::new(out))))
15516                    }
15517                    None => {
15518                        let total: f64 = (0..inner.nrows())
15519                            .map(|r| inner.cols[val_idx][r].to_number())
15520                            .sum();
15521                        Ok(PerlValue::float(total))
15522                    }
15523                }
15524            }
15525            _ => Err(PerlError::runtime(
15526                format!("Unknown method for dataframe: {}", method),
15527                line,
15528            )),
15529        }
15530    }
15531
15532    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
15533    fn set_method(
15534        &self,
15535        s: Arc<crate::value::PerlSet>,
15536        method: &str,
15537        args: &[PerlValue],
15538        line: usize,
15539    ) -> PerlResult<PerlValue> {
15540        match method {
15541            "has" | "contains" | "member" => {
15542                if args.len() != 1 {
15543                    return Err(PerlError::runtime(
15544                        "set->has expects one argument (element)",
15545                        line,
15546                    ));
15547                }
15548                let k = crate::value::set_member_key(&args[0]);
15549                Ok(PerlValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
15550            }
15551            "size" | "len" | "count" => {
15552                if !args.is_empty() {
15553                    return Err(PerlError::runtime("set->size takes no arguments", line));
15554                }
15555                Ok(PerlValue::integer(s.len() as i64))
15556            }
15557            "values" | "list" | "elements" => {
15558                if !args.is_empty() {
15559                    return Err(PerlError::runtime("set->values takes no arguments", line));
15560                }
15561                Ok(PerlValue::array(s.values().cloned().collect()))
15562            }
15563            _ => Err(PerlError::runtime(
15564                format!("Unknown method for set: {}", method),
15565                line,
15566            )),
15567        }
15568    }
15569
15570    fn deque_method(
15571        &mut self,
15572        d: Arc<Mutex<VecDeque<PerlValue>>>,
15573        method: &str,
15574        args: &[PerlValue],
15575        line: usize,
15576    ) -> PerlResult<PerlValue> {
15577        match method {
15578            "push_back" => {
15579                if args.len() != 1 {
15580                    return Err(PerlError::runtime("push_back expects 1 argument", line));
15581                }
15582                d.lock().push_back(args[0].clone());
15583                Ok(PerlValue::integer(d.lock().len() as i64))
15584            }
15585            "push_front" => {
15586                if args.len() != 1 {
15587                    return Err(PerlError::runtime("push_front expects 1 argument", line));
15588                }
15589                d.lock().push_front(args[0].clone());
15590                Ok(PerlValue::integer(d.lock().len() as i64))
15591            }
15592            "pop_back" => Ok(d.lock().pop_back().unwrap_or(PerlValue::UNDEF)),
15593            "pop_front" => Ok(d.lock().pop_front().unwrap_or(PerlValue::UNDEF)),
15594            "size" | "len" => Ok(PerlValue::integer(d.lock().len() as i64)),
15595            _ => Err(PerlError::runtime(
15596                format!("Unknown method for deque: {}", method),
15597                line,
15598            )),
15599        }
15600    }
15601
15602    fn heap_method(
15603        &mut self,
15604        h: Arc<Mutex<PerlHeap>>,
15605        method: &str,
15606        args: &[PerlValue],
15607        line: usize,
15608    ) -> PerlResult<PerlValue> {
15609        match method {
15610            "push" => {
15611                if args.len() != 1 {
15612                    return Err(PerlError::runtime("heap push expects 1 argument", line));
15613                }
15614                let mut g = h.lock();
15615                let n = g.items.len();
15616                g.items.push(args[0].clone());
15617                let cmp = g.cmp.clone();
15618                drop(g);
15619                let mut g = h.lock();
15620                self.heap_sift_up(&mut g.items, &cmp, n);
15621                Ok(PerlValue::integer(g.items.len() as i64))
15622            }
15623            "pop" => {
15624                let mut g = h.lock();
15625                if g.items.is_empty() {
15626                    return Ok(PerlValue::UNDEF);
15627                }
15628                let cmp = g.cmp.clone();
15629                let n = g.items.len();
15630                g.items.swap(0, n - 1);
15631                let v = g.items.pop().unwrap();
15632                if !g.items.is_empty() {
15633                    self.heap_sift_down(&mut g.items, &cmp, 0);
15634                }
15635                Ok(v)
15636            }
15637            "peek" => Ok(h.lock().items.first().cloned().unwrap_or(PerlValue::UNDEF)),
15638            _ => Err(PerlError::runtime(
15639                format!("Unknown method for heap: {}", method),
15640                line,
15641            )),
15642        }
15643    }
15644
15645    fn ppool_method(
15646        &mut self,
15647        pool: PerlPpool,
15648        method: &str,
15649        args: &[PerlValue],
15650        line: usize,
15651    ) -> PerlResult<PerlValue> {
15652        match method {
15653            "submit" => pool.submit(self, args, line),
15654            "collect" => {
15655                if !args.is_empty() {
15656                    return Err(PerlError::runtime("collect() takes no arguments", line));
15657                }
15658                pool.collect(line)
15659            }
15660            _ => Err(PerlError::runtime(
15661                format!("Unknown method for ppool: {}", method),
15662                line,
15663            )),
15664        }
15665    }
15666
15667    fn barrier_method(
15668        &self,
15669        barrier: PerlBarrier,
15670        method: &str,
15671        args: &[PerlValue],
15672        line: usize,
15673    ) -> PerlResult<PerlValue> {
15674        match method {
15675            "wait" => {
15676                if !args.is_empty() {
15677                    return Err(PerlError::runtime("wait() takes no arguments", line));
15678                }
15679                let _ = barrier.0.wait();
15680                Ok(PerlValue::integer(1))
15681            }
15682            _ => Err(PerlError::runtime(
15683                format!("Unknown method for barrier: {}", method),
15684                line,
15685            )),
15686        }
15687    }
15688
15689    fn capture_method(
15690        &self,
15691        c: Arc<CaptureResult>,
15692        method: &str,
15693        args: &[PerlValue],
15694        line: usize,
15695    ) -> PerlResult<PerlValue> {
15696        if !args.is_empty() {
15697            return Err(PerlError::runtime(
15698                format!("capture: {} takes no arguments", method),
15699                line,
15700            ));
15701        }
15702        match method {
15703            "stdout" => Ok(PerlValue::string(c.stdout.clone())),
15704            "stderr" => Ok(PerlValue::string(c.stderr.clone())),
15705            "exitcode" => Ok(PerlValue::integer(c.exitcode)),
15706            "failed" => Ok(PerlValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
15707            _ => Err(PerlError::runtime(
15708                format!("Unknown method for capture: {}", method),
15709                line,
15710            )),
15711        }
15712    }
15713
15714    pub(crate) fn builtin_par_pipeline_stream(
15715        &mut self,
15716        args: &[PerlValue],
15717        _line: usize,
15718    ) -> PerlResult<PerlValue> {
15719        let mut items = Vec::new();
15720        for v in args {
15721            if let Some(a) = v.as_array_vec() {
15722                items.extend(a);
15723            } else {
15724                items.push(v.clone());
15725            }
15726        }
15727        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15728            source: items,
15729            ops: Vec::new(),
15730            has_scalar_terminal: false,
15731            par_stream: true,
15732            streaming: false,
15733            streaming_workers: 0,
15734            streaming_buffer: 256,
15735        }))))
15736    }
15737
15738    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
15739    /// that wires ops through bounded channels on `collect()`.
15740    pub(crate) fn builtin_par_pipeline_stream_new(
15741        &mut self,
15742        args: &[PerlValue],
15743        _line: usize,
15744    ) -> PerlResult<PerlValue> {
15745        let mut items = Vec::new();
15746        let mut workers: usize = 0;
15747        let mut buffer: usize = 256;
15748        // Separate list items from keyword args (workers => N, buffer => N).
15749        let mut i = 0;
15750        while i < args.len() {
15751            let s = args[i].to_string();
15752            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
15753                let val = args[i + 1].to_int().max(1) as usize;
15754                if s == "workers" {
15755                    workers = val;
15756                } else {
15757                    buffer = val;
15758                }
15759                i += 2;
15760            } else if let Some(a) = args[i].as_array_vec() {
15761                items.extend(a);
15762                i += 1;
15763            } else {
15764                items.push(args[i].clone());
15765                i += 1;
15766            }
15767        }
15768        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15769            source: items,
15770            ops: Vec::new(),
15771            has_scalar_terminal: false,
15772            par_stream: false,
15773            streaming: true,
15774            streaming_workers: workers,
15775            streaming_buffer: buffer,
15776        }))))
15777    }
15778
15779    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
15780    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<PerlSub> {
15781        let line = 1usize;
15782        let body = vec![Statement {
15783            label: None,
15784            kind: StmtKind::Expression(Expr {
15785                kind: ExprKind::BinOp {
15786                    left: Box::new(Expr {
15787                        kind: ExprKind::ScalarVar("_".into()),
15788                        line,
15789                    }),
15790                    op: BinOp::Mul,
15791                    right: Box::new(Expr {
15792                        kind: ExprKind::Integer(k),
15793                        line,
15794                    }),
15795                },
15796                line,
15797            }),
15798            line,
15799        }];
15800        Arc::new(PerlSub {
15801            name: "__pipeline_int_mul__".into(),
15802            params: vec![],
15803            body,
15804            closure_env: None,
15805            prototype: None,
15806            fib_like: None,
15807        })
15808    }
15809
15810    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<PerlSub> {
15811        let captured = self.scope.capture();
15812        Arc::new(PerlSub {
15813            name: "__ANON__".into(),
15814            params: vec![],
15815            body: block.clone(),
15816            closure_env: Some(captured),
15817            prototype: None,
15818            fib_like: None,
15819        })
15820    }
15821
15822    pub(crate) fn builtin_collect_execute(
15823        &mut self,
15824        args: &[PerlValue],
15825        line: usize,
15826    ) -> PerlResult<PerlValue> {
15827        if args.is_empty() {
15828            return Err(PerlError::runtime(
15829                "collect() expects at least one argument",
15830                line,
15831            ));
15832        }
15833        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
15834        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
15835        if args.len() == 1 {
15836            if let Some(p) = args[0].as_pipeline() {
15837                return self.pipeline_collect(&p, line);
15838            }
15839            return Ok(PerlValue::array(args[0].to_list()));
15840        }
15841        Ok(PerlValue::array(args.to_vec()))
15842    }
15843
15844    pub(crate) fn pipeline_push(
15845        &self,
15846        p: &Arc<Mutex<PipelineInner>>,
15847        op: PipelineOp,
15848        line: usize,
15849    ) -> PerlResult<()> {
15850        let mut g = p.lock();
15851        if g.has_scalar_terminal {
15852            return Err(PerlError::runtime(
15853                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
15854                line,
15855            ));
15856        }
15857        if matches!(
15858            &op,
15859            PipelineOp::PReduce { .. }
15860                | PipelineOp::PReduceInit { .. }
15861                | PipelineOp::PMapReduce { .. }
15862        ) {
15863            g.has_scalar_terminal = true;
15864        }
15865        g.ops.push(op);
15866        Ok(())
15867    }
15868
15869    fn pipeline_parse_sub_progress(
15870        args: &[PerlValue],
15871        line: usize,
15872        name: &str,
15873    ) -> PerlResult<(Arc<PerlSub>, bool)> {
15874        if args.is_empty() {
15875            return Err(PerlError::runtime(
15876                format!("pipeline {}: expects at least 1 argument (code ref)", name),
15877                line,
15878            ));
15879        }
15880        let Some(sub) = args[0].as_code_ref() else {
15881            return Err(PerlError::runtime(
15882                format!("pipeline {}: first argument must be a code reference", name),
15883                line,
15884            ));
15885        };
15886        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
15887        if args.len() > 2 {
15888            return Err(PerlError::runtime(
15889                format!(
15890                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
15891                    name
15892                ),
15893                line,
15894            ));
15895        }
15896        Ok((sub, progress))
15897    }
15898
15899    pub(crate) fn pipeline_method(
15900        &mut self,
15901        p: Arc<Mutex<PipelineInner>>,
15902        method: &str,
15903        args: &[PerlValue],
15904        line: usize,
15905    ) -> PerlResult<PerlValue> {
15906        match method {
15907            "filter" | "f" | "grep" => {
15908                if args.len() != 1 {
15909                    return Err(PerlError::runtime(
15910                        "pipeline filter/grep expects 1 argument (sub)",
15911                        line,
15912                    ));
15913                }
15914                let Some(sub) = args[0].as_code_ref() else {
15915                    return Err(PerlError::runtime(
15916                        "pipeline filter/grep expects a code reference",
15917                        line,
15918                    ));
15919                };
15920                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
15921                Ok(PerlValue::pipeline(Arc::clone(&p)))
15922            }
15923            "map" => {
15924                if args.len() != 1 {
15925                    return Err(PerlError::runtime(
15926                        "pipeline map expects 1 argument (sub)",
15927                        line,
15928                    ));
15929                }
15930                let Some(sub) = args[0].as_code_ref() else {
15931                    return Err(PerlError::runtime(
15932                        "pipeline map expects a code reference",
15933                        line,
15934                    ));
15935                };
15936                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
15937                Ok(PerlValue::pipeline(Arc::clone(&p)))
15938            }
15939            "tap" | "peek" => {
15940                if args.len() != 1 {
15941                    return Err(PerlError::runtime(
15942                        "pipeline tap/peek expects 1 argument (sub)",
15943                        line,
15944                    ));
15945                }
15946                let Some(sub) = args[0].as_code_ref() else {
15947                    return Err(PerlError::runtime(
15948                        "pipeline tap/peek expects a code reference",
15949                        line,
15950                    ));
15951                };
15952                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
15953                Ok(PerlValue::pipeline(Arc::clone(&p)))
15954            }
15955            "take" => {
15956                if args.len() != 1 {
15957                    return Err(PerlError::runtime("pipeline take expects 1 argument", line));
15958                }
15959                let n = args[0].to_int();
15960                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
15961                Ok(PerlValue::pipeline(Arc::clone(&p)))
15962            }
15963            "pmap" => {
15964                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
15965                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
15966                Ok(PerlValue::pipeline(Arc::clone(&p)))
15967            }
15968            "pgrep" => {
15969                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
15970                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
15971                Ok(PerlValue::pipeline(Arc::clone(&p)))
15972            }
15973            "pfor" => {
15974                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
15975                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
15976                Ok(PerlValue::pipeline(Arc::clone(&p)))
15977            }
15978            "pmap_chunked" => {
15979                if args.len() < 2 {
15980                    return Err(PerlError::runtime(
15981                        "pipeline pmap_chunked expects chunk size and a code reference",
15982                        line,
15983                    ));
15984                }
15985                let chunk = args[0].to_int().max(1);
15986                let Some(sub) = args[1].as_code_ref() else {
15987                    return Err(PerlError::runtime(
15988                        "pipeline pmap_chunked: second argument must be a code reference",
15989                        line,
15990                    ));
15991                };
15992                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
15993                if args.len() > 3 {
15994                    return Err(PerlError::runtime(
15995                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
15996                        line,
15997                    ));
15998                }
15999                self.pipeline_push(
16000                    &p,
16001                    PipelineOp::PMapChunked {
16002                        chunk,
16003                        sub,
16004                        progress,
16005                    },
16006                    line,
16007                )?;
16008                Ok(PerlValue::pipeline(Arc::clone(&p)))
16009            }
16010            "psort" => {
16011                let (cmp, progress) = match args.len() {
16012                    0 => (None, false),
16013                    1 => {
16014                        if let Some(s) = args[0].as_code_ref() {
16015                            (Some(s), false)
16016                        } else {
16017                            (None, args[0].is_true())
16018                        }
16019                    }
16020                    2 => {
16021                        let Some(s) = args[0].as_code_ref() else {
16022                            return Err(PerlError::runtime(
16023                                "pipeline psort: with two arguments, the first must be a comparator sub",
16024                                line,
16025                            ));
16026                        };
16027                        (Some(s), args[1].is_true())
16028                    }
16029                    _ => {
16030                        return Err(PerlError::runtime(
16031                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
16032                            line,
16033                        ));
16034                    }
16035                };
16036                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
16037                Ok(PerlValue::pipeline(Arc::clone(&p)))
16038            }
16039            "pcache" => {
16040                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
16041                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
16042                Ok(PerlValue::pipeline(Arc::clone(&p)))
16043            }
16044            "preduce" => {
16045                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
16046                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
16047                Ok(PerlValue::pipeline(Arc::clone(&p)))
16048            }
16049            "preduce_init" => {
16050                if args.len() < 2 {
16051                    return Err(PerlError::runtime(
16052                        "pipeline preduce_init expects init value and a code reference",
16053                        line,
16054                    ));
16055                }
16056                let init = args[0].clone();
16057                let Some(sub) = args[1].as_code_ref() else {
16058                    return Err(PerlError::runtime(
16059                        "pipeline preduce_init: second argument must be a code reference",
16060                        line,
16061                    ));
16062                };
16063                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
16064                if args.len() > 3 {
16065                    return Err(PerlError::runtime(
16066                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
16067                        line,
16068                    ));
16069                }
16070                self.pipeline_push(
16071                    &p,
16072                    PipelineOp::PReduceInit {
16073                        init,
16074                        sub,
16075                        progress,
16076                    },
16077                    line,
16078                )?;
16079                Ok(PerlValue::pipeline(Arc::clone(&p)))
16080            }
16081            "pmap_reduce" => {
16082                if args.len() < 2 {
16083                    return Err(PerlError::runtime(
16084                        "pipeline pmap_reduce expects map sub and reduce sub",
16085                        line,
16086                    ));
16087                }
16088                let Some(map) = args[0].as_code_ref() else {
16089                    return Err(PerlError::runtime(
16090                        "pipeline pmap_reduce: first argument must be a code reference (map)",
16091                        line,
16092                    ));
16093                };
16094                let Some(reduce) = args[1].as_code_ref() else {
16095                    return Err(PerlError::runtime(
16096                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
16097                        line,
16098                    ));
16099                };
16100                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
16101                if args.len() > 3 {
16102                    return Err(PerlError::runtime(
16103                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
16104                        line,
16105                    ));
16106                }
16107                self.pipeline_push(
16108                    &p,
16109                    PipelineOp::PMapReduce {
16110                        map,
16111                        reduce,
16112                        progress,
16113                    },
16114                    line,
16115                )?;
16116                Ok(PerlValue::pipeline(Arc::clone(&p)))
16117            }
16118            "collect" => {
16119                if !args.is_empty() {
16120                    return Err(PerlError::runtime(
16121                        "pipeline collect takes no arguments",
16122                        line,
16123                    ));
16124                }
16125                self.pipeline_collect(&p, line)
16126            }
16127            _ => {
16128                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
16129                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
16130                if let Some(sub) = self.resolve_sub_by_name(method) {
16131                    if !args.is_empty() {
16132                        return Err(PerlError::runtime(
16133                            format!(
16134                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
16135                                method
16136                            ),
16137                            line,
16138                        ));
16139                    }
16140                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
16141                    Ok(PerlValue::pipeline(Arc::clone(&p)))
16142                } else {
16143                    Err(PerlError::runtime(
16144                        format!("Unknown method for pipeline: {}", method),
16145                        line,
16146                    ))
16147                }
16148            }
16149        }
16150    }
16151
16152    fn pipeline_parallel_map(
16153        &mut self,
16154        items: Vec<PerlValue>,
16155        sub: &Arc<PerlSub>,
16156        progress: bool,
16157    ) -> Vec<PerlValue> {
16158        let subs = self.subs.clone();
16159        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16160        let pmap_progress = PmapProgress::new(progress, items.len());
16161        let results: Vec<PerlValue> = items
16162            .into_par_iter()
16163            .map(|item| {
16164                let mut local_interp = Interpreter::new();
16165                local_interp.subs = subs.clone();
16166                local_interp.scope.restore_capture(&scope_capture);
16167                local_interp
16168                    .scope
16169                    .restore_atomics(&atomic_arrays, &atomic_hashes);
16170                local_interp.enable_parallel_guard();
16171                local_interp.scope.set_topic(item);
16172                local_interp.scope_push_hook();
16173                let val = match local_interp.exec_block_no_scope(&sub.body) {
16174                    Ok(val) => val,
16175                    Err(_) => PerlValue::UNDEF,
16176                };
16177                local_interp.scope_pop_hook();
16178                pmap_progress.tick();
16179                val
16180            })
16181            .collect();
16182        pmap_progress.finish();
16183        results
16184    }
16185
16186    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
16187    fn pipeline_par_stream_filter(
16188        &mut self,
16189        items: Vec<PerlValue>,
16190        sub: &Arc<PerlSub>,
16191    ) -> Vec<PerlValue> {
16192        if items.is_empty() {
16193            return items;
16194        }
16195        let subs = self.subs.clone();
16196        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16197        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
16198        let mut kept: Vec<(usize, PerlValue)> = indexed
16199            .into_par_iter()
16200            .filter_map(|(i, item)| {
16201                let mut local_interp = Interpreter::new();
16202                local_interp.subs = subs.clone();
16203                local_interp.scope.restore_capture(&scope_capture);
16204                local_interp
16205                    .scope
16206                    .restore_atomics(&atomic_arrays, &atomic_hashes);
16207                local_interp.enable_parallel_guard();
16208                local_interp.scope.set_topic(item.clone());
16209                local_interp.scope_push_hook();
16210                let keep = match local_interp.exec_block_no_scope(&sub.body) {
16211                    Ok(val) => val.is_true(),
16212                    Err(_) => false,
16213                };
16214                local_interp.scope_pop_hook();
16215                if keep {
16216                    Some((i, item))
16217                } else {
16218                    None
16219                }
16220            })
16221            .collect();
16222        kept.sort_by_key(|(i, _)| *i);
16223        kept.into_iter().map(|(_, x)| x).collect()
16224    }
16225
16226    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
16227    fn pipeline_par_stream_map(
16228        &mut self,
16229        items: Vec<PerlValue>,
16230        sub: &Arc<PerlSub>,
16231    ) -> Vec<PerlValue> {
16232        if items.is_empty() {
16233            return items;
16234        }
16235        let subs = self.subs.clone();
16236        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16237        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
16238        let mut mapped: Vec<(usize, PerlValue)> = indexed
16239            .into_par_iter()
16240            .map(|(i, item)| {
16241                let mut local_interp = Interpreter::new();
16242                local_interp.subs = subs.clone();
16243                local_interp.scope.restore_capture(&scope_capture);
16244                local_interp
16245                    .scope
16246                    .restore_atomics(&atomic_arrays, &atomic_hashes);
16247                local_interp.enable_parallel_guard();
16248                local_interp.scope.set_topic(item);
16249                local_interp.scope_push_hook();
16250                let val = match local_interp.exec_block_no_scope(&sub.body) {
16251                    Ok(val) => val,
16252                    Err(_) => PerlValue::UNDEF,
16253                };
16254                local_interp.scope_pop_hook();
16255                (i, val)
16256            })
16257            .collect();
16258        mapped.sort_by_key(|(i, _)| *i);
16259        mapped.into_iter().map(|(_, x)| x).collect()
16260    }
16261
16262    fn pipeline_collect(
16263        &mut self,
16264        p: &Arc<Mutex<PipelineInner>>,
16265        line: usize,
16266    ) -> PerlResult<PerlValue> {
16267        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
16268            let g = p.lock();
16269            (
16270                g.source.clone(),
16271                g.ops.clone(),
16272                g.par_stream,
16273                g.streaming,
16274                g.streaming_workers,
16275                g.streaming_buffer,
16276            )
16277        };
16278        if streaming {
16279            return self.pipeline_collect_streaming(
16280                v,
16281                &ops,
16282                streaming_workers,
16283                streaming_buffer,
16284                line,
16285            );
16286        }
16287        for op in ops {
16288            match op {
16289                PipelineOp::Filter(sub) => {
16290                    if par_stream {
16291                        v = self.pipeline_par_stream_filter(v, &sub);
16292                    } else {
16293                        let mut out = Vec::new();
16294                        for item in v {
16295                            self.scope_push_hook();
16296                            self.scope.set_topic(item.clone());
16297                            if let Some(ref env) = sub.closure_env {
16298                                self.scope.restore_capture(env);
16299                            }
16300                            let keep = match self.exec_block_no_scope(&sub.body) {
16301                                Ok(val) => val.is_true(),
16302                                Err(_) => false,
16303                            };
16304                            self.scope_pop_hook();
16305                            if keep {
16306                                out.push(item);
16307                            }
16308                        }
16309                        v = out;
16310                    }
16311                }
16312                PipelineOp::Map(sub) => {
16313                    if par_stream {
16314                        v = self.pipeline_par_stream_map(v, &sub);
16315                    } else {
16316                        let mut out = Vec::new();
16317                        for item in v {
16318                            self.scope_push_hook();
16319                            self.scope.set_topic(item);
16320                            if let Some(ref env) = sub.closure_env {
16321                                self.scope.restore_capture(env);
16322                            }
16323                            let mapped = match self.exec_block_no_scope(&sub.body) {
16324                                Ok(val) => val,
16325                                Err(_) => PerlValue::UNDEF,
16326                            };
16327                            self.scope_pop_hook();
16328                            out.push(mapped);
16329                        }
16330                        v = out;
16331                    }
16332                }
16333                PipelineOp::Tap(sub) => {
16334                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
16335                        Ok(_) => {}
16336                        Err(FlowOrError::Error(e)) => return Err(e),
16337                        Err(FlowOrError::Flow(_)) => {
16338                            return Err(PerlError::runtime(
16339                                "tap: unsupported control flow in block",
16340                                line,
16341                            ));
16342                        }
16343                    }
16344                }
16345                PipelineOp::Take(n) => {
16346                    let n = n.max(0) as usize;
16347                    if v.len() > n {
16348                        v.truncate(n);
16349                    }
16350                }
16351                PipelineOp::PMap { sub, progress } => {
16352                    v = self.pipeline_parallel_map(v, &sub, progress);
16353                }
16354                PipelineOp::PGrep { sub, progress } => {
16355                    let subs = self.subs.clone();
16356                    let (scope_capture, atomic_arrays, atomic_hashes) =
16357                        self.scope.capture_with_atomics();
16358                    let pmap_progress = PmapProgress::new(progress, v.len());
16359                    v = v
16360                        .into_par_iter()
16361                        .filter_map(|item| {
16362                            let mut local_interp = Interpreter::new();
16363                            local_interp.subs = subs.clone();
16364                            local_interp.scope.restore_capture(&scope_capture);
16365                            local_interp
16366                                .scope
16367                                .restore_atomics(&atomic_arrays, &atomic_hashes);
16368                            local_interp.enable_parallel_guard();
16369                            local_interp.scope.set_topic(item.clone());
16370                            local_interp.scope_push_hook();
16371                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
16372                                Ok(val) => val.is_true(),
16373                                Err(_) => false,
16374                            };
16375                            local_interp.scope_pop_hook();
16376                            pmap_progress.tick();
16377                            if keep {
16378                                Some(item)
16379                            } else {
16380                                None
16381                            }
16382                        })
16383                        .collect();
16384                    pmap_progress.finish();
16385                }
16386                PipelineOp::PFor { sub, progress } => {
16387                    let subs = self.subs.clone();
16388                    let (scope_capture, atomic_arrays, atomic_hashes) =
16389                        self.scope.capture_with_atomics();
16390                    let pmap_progress = PmapProgress::new(progress, v.len());
16391                    let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
16392                    v.clone().into_par_iter().for_each(|item| {
16393                        if first_err.lock().is_some() {
16394                            return;
16395                        }
16396                        let mut local_interp = Interpreter::new();
16397                        local_interp.subs = subs.clone();
16398                        local_interp.scope.restore_capture(&scope_capture);
16399                        local_interp
16400                            .scope
16401                            .restore_atomics(&atomic_arrays, &atomic_hashes);
16402                        local_interp.enable_parallel_guard();
16403                        local_interp.scope.set_topic(item);
16404                        local_interp.scope_push_hook();
16405                        match local_interp.exec_block_no_scope(&sub.body) {
16406                            Ok(_) => {}
16407                            Err(e) => {
16408                                let stryke = match e {
16409                                    FlowOrError::Error(stryke) => stryke,
16410                                    FlowOrError::Flow(_) => PerlError::runtime(
16411                                        "return/last/next/redo not supported inside pipeline pfor block",
16412                                        line,
16413                                    ),
16414                                };
16415                                let mut g = first_err.lock();
16416                                if g.is_none() {
16417                                    *g = Some(stryke);
16418                                }
16419                            }
16420                        }
16421                        local_interp.scope_pop_hook();
16422                        pmap_progress.tick();
16423                    });
16424                    pmap_progress.finish();
16425                    let pfor_err = first_err.lock().take();
16426                    if let Some(e) = pfor_err {
16427                        return Err(e);
16428                    }
16429                }
16430                PipelineOp::PMapChunked {
16431                    chunk,
16432                    sub,
16433                    progress,
16434                } => {
16435                    let chunk_n = chunk.max(1) as usize;
16436                    let subs = self.subs.clone();
16437                    let (scope_capture, atomic_arrays, atomic_hashes) =
16438                        self.scope.capture_with_atomics();
16439                    let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = v
16440                        .chunks(chunk_n)
16441                        .enumerate()
16442                        .map(|(i, c)| (i, c.to_vec()))
16443                        .collect();
16444                    let n_chunks = indexed_chunks.len();
16445                    let pmap_progress = PmapProgress::new(progress, n_chunks);
16446                    let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
16447                        .into_par_iter()
16448                        .map(|(chunk_idx, chunk)| {
16449                            let mut local_interp = Interpreter::new();
16450                            local_interp.subs = subs.clone();
16451                            local_interp.scope.restore_capture(&scope_capture);
16452                            local_interp
16453                                .scope
16454                                .restore_atomics(&atomic_arrays, &atomic_hashes);
16455                            local_interp.enable_parallel_guard();
16456                            let mut out = Vec::with_capacity(chunk.len());
16457                            for item in chunk {
16458                                local_interp.scope.set_topic(item);
16459                                local_interp.scope_push_hook();
16460                                match local_interp.exec_block_no_scope(&sub.body) {
16461                                    Ok(val) => {
16462                                        local_interp.scope_pop_hook();
16463                                        out.push(val);
16464                                    }
16465                                    Err(_) => {
16466                                        local_interp.scope_pop_hook();
16467                                        out.push(PerlValue::UNDEF);
16468                                    }
16469                                }
16470                            }
16471                            pmap_progress.tick();
16472                            (chunk_idx, out)
16473                        })
16474                        .collect();
16475                    pmap_progress.finish();
16476                    chunk_results.sort_by_key(|(i, _)| *i);
16477                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
16478                }
16479                PipelineOp::PSort { cmp, progress } => {
16480                    let pmap_progress = PmapProgress::new(progress, 2);
16481                    pmap_progress.tick();
16482                    match cmp {
16483                        Some(cmp_block) => {
16484                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
16485                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
16486                            } else {
16487                                let subs = self.subs.clone();
16488                                let scope_capture = self.scope.capture();
16489                                v.par_sort_by(|a, b| {
16490                                    let mut local_interp = Interpreter::new();
16491                                    local_interp.subs = subs.clone();
16492                                    local_interp.scope.restore_capture(&scope_capture);
16493                                    local_interp.enable_parallel_guard();
16494                                    let _ = local_interp.scope.set_scalar("a", a.clone());
16495                                    let _ = local_interp.scope.set_scalar("b", b.clone());
16496                                    let _ = local_interp.scope.set_scalar("_0", a.clone());
16497                                    let _ = local_interp.scope.set_scalar("_1", b.clone());
16498                                    local_interp.scope_push_hook();
16499                                    let ord =
16500                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
16501                                            Ok(v) => {
16502                                                let n = v.to_int();
16503                                                if n < 0 {
16504                                                    std::cmp::Ordering::Less
16505                                                } else if n > 0 {
16506                                                    std::cmp::Ordering::Greater
16507                                                } else {
16508                                                    std::cmp::Ordering::Equal
16509                                                }
16510                                            }
16511                                            Err(_) => std::cmp::Ordering::Equal,
16512                                        };
16513                                    local_interp.scope_pop_hook();
16514                                    ord
16515                                });
16516                            }
16517                        }
16518                        None => {
16519                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
16520                        }
16521                    }
16522                    pmap_progress.tick();
16523                    pmap_progress.finish();
16524                }
16525                PipelineOp::PCache { sub, progress } => {
16526                    let subs = self.subs.clone();
16527                    let scope_capture = self.scope.capture();
16528                    let cache = &*crate::pcache::GLOBAL_PCACHE;
16529                    let pmap_progress = PmapProgress::new(progress, v.len());
16530                    v = v
16531                        .into_par_iter()
16532                        .map(|item| {
16533                            let k = crate::pcache::cache_key(&item);
16534                            if let Some(cached) = cache.get(&k) {
16535                                pmap_progress.tick();
16536                                return cached.clone();
16537                            }
16538                            let mut local_interp = Interpreter::new();
16539                            local_interp.subs = subs.clone();
16540                            local_interp.scope.restore_capture(&scope_capture);
16541                            local_interp.enable_parallel_guard();
16542                            local_interp.scope.set_topic(item.clone());
16543                            local_interp.scope_push_hook();
16544                            let val = match local_interp.exec_block_no_scope(&sub.body) {
16545                                Ok(v) => v,
16546                                Err(_) => PerlValue::UNDEF,
16547                            };
16548                            local_interp.scope_pop_hook();
16549                            cache.insert(k, val.clone());
16550                            pmap_progress.tick();
16551                            val
16552                        })
16553                        .collect();
16554                    pmap_progress.finish();
16555                }
16556                PipelineOp::PReduce { sub, progress } => {
16557                    if v.is_empty() {
16558                        return Ok(PerlValue::UNDEF);
16559                    }
16560                    if v.len() == 1 {
16561                        return Ok(v.into_iter().next().unwrap());
16562                    }
16563                    let block = sub.body.clone();
16564                    let subs = self.subs.clone();
16565                    let scope_capture = self.scope.capture();
16566                    let pmap_progress = PmapProgress::new(progress, v.len());
16567                    let result = v
16568                        .into_par_iter()
16569                        .map(|x| {
16570                            pmap_progress.tick();
16571                            x
16572                        })
16573                        .reduce_with(|a, b| {
16574                            let mut local_interp = Interpreter::new();
16575                            local_interp.subs = subs.clone();
16576                            local_interp.scope.restore_capture(&scope_capture);
16577                            local_interp.enable_parallel_guard();
16578                            let _ = local_interp.scope.set_scalar("a", a.clone());
16579                            let _ = local_interp.scope.set_scalar("b", b.clone());
16580                            let _ = local_interp.scope.set_scalar("_0", a);
16581                            let _ = local_interp.scope.set_scalar("_1", b);
16582                            match local_interp.exec_block(&block) {
16583                                Ok(val) => val,
16584                                Err(_) => PerlValue::UNDEF,
16585                            }
16586                        });
16587                    pmap_progress.finish();
16588                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16589                }
16590                PipelineOp::PReduceInit {
16591                    init,
16592                    sub,
16593                    progress,
16594                } => {
16595                    if v.is_empty() {
16596                        return Ok(init);
16597                    }
16598                    let block = sub.body.clone();
16599                    let subs = self.subs.clone();
16600                    let scope_capture = self.scope.capture();
16601                    let cap: &[(String, PerlValue)] = scope_capture.as_slice();
16602                    if v.len() == 1 {
16603                        return Ok(fold_preduce_init_step(
16604                            &subs,
16605                            cap,
16606                            &block,
16607                            preduce_init_fold_identity(&init),
16608                            v.into_iter().next().unwrap(),
16609                        ));
16610                    }
16611                    let pmap_progress = PmapProgress::new(progress, v.len());
16612                    let result = v
16613                        .into_par_iter()
16614                        .fold(
16615                            || preduce_init_fold_identity(&init),
16616                            |acc, item| {
16617                                pmap_progress.tick();
16618                                fold_preduce_init_step(&subs, cap, &block, acc, item)
16619                            },
16620                        )
16621                        .reduce(
16622                            || preduce_init_fold_identity(&init),
16623                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
16624                        );
16625                    pmap_progress.finish();
16626                    return Ok(result);
16627                }
16628                PipelineOp::PMapReduce {
16629                    map,
16630                    reduce,
16631                    progress,
16632                } => {
16633                    if v.is_empty() {
16634                        return Ok(PerlValue::UNDEF);
16635                    }
16636                    let map_block = map.body.clone();
16637                    let reduce_block = reduce.body.clone();
16638                    let subs = self.subs.clone();
16639                    let scope_capture = self.scope.capture();
16640                    if v.len() == 1 {
16641                        let mut local_interp = Interpreter::new();
16642                        local_interp.subs = subs.clone();
16643                        local_interp.scope.restore_capture(&scope_capture);
16644                        local_interp.scope.set_topic(v[0].clone());
16645                        return match local_interp.exec_block_no_scope(&map_block) {
16646                            Ok(val) => Ok(val),
16647                            Err(_) => Ok(PerlValue::UNDEF),
16648                        };
16649                    }
16650                    let pmap_progress = PmapProgress::new(progress, v.len());
16651                    let result = v
16652                        .into_par_iter()
16653                        .map(|item| {
16654                            let mut local_interp = Interpreter::new();
16655                            local_interp.subs = subs.clone();
16656                            local_interp.scope.restore_capture(&scope_capture);
16657                            local_interp.scope.set_topic(item);
16658                            let val = match local_interp.exec_block_no_scope(&map_block) {
16659                                Ok(val) => val,
16660                                Err(_) => PerlValue::UNDEF,
16661                            };
16662                            pmap_progress.tick();
16663                            val
16664                        })
16665                        .reduce_with(|a, b| {
16666                            let mut local_interp = Interpreter::new();
16667                            local_interp.subs = subs.clone();
16668                            local_interp.scope.restore_capture(&scope_capture);
16669                            let _ = local_interp.scope.set_scalar("a", a.clone());
16670                            let _ = local_interp.scope.set_scalar("b", b.clone());
16671                            let _ = local_interp.scope.set_scalar("_0", a);
16672                            let _ = local_interp.scope.set_scalar("_1", b);
16673                            match local_interp.exec_block_no_scope(&reduce_block) {
16674                                Ok(val) => val,
16675                                Err(_) => PerlValue::UNDEF,
16676                            }
16677                        });
16678                    pmap_progress.finish();
16679                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16680                }
16681            }
16682        }
16683        Ok(PerlValue::array(v))
16684    }
16685
16686    /// Streaming collect: wire pipeline ops through bounded channels so items flow
16687    /// between stages concurrently.  Order is **not** preserved.
16688    fn pipeline_collect_streaming(
16689        &mut self,
16690        source: Vec<PerlValue>,
16691        ops: &[PipelineOp],
16692        workers_per_stage: usize,
16693        buffer: usize,
16694        line: usize,
16695    ) -> PerlResult<PerlValue> {
16696        use crossbeam::channel::{bounded, Receiver, Sender};
16697
16698        // Validate: reject ops that require all items (can't stream).
16699        for op in ops {
16700            match op {
16701                PipelineOp::PSort { .. }
16702                | PipelineOp::PReduce { .. }
16703                | PipelineOp::PReduceInit { .. }
16704                | PipelineOp::PMapReduce { .. }
16705                | PipelineOp::PMapChunked { .. } => {
16706                    return Err(PerlError::runtime(
16707                        format!(
16708                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
16709                            std::mem::discriminant(op)
16710                        ),
16711                        line,
16712                    ));
16713                }
16714                _ => {}
16715            }
16716        }
16717
16718        // Filter out non-streamable ops and collect streamable ones.
16719        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
16720        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
16721        if streamable_ops.is_empty() {
16722            return Ok(PerlValue::array(source));
16723        }
16724
16725        let n_stages = streamable_ops.len();
16726        let wn = if workers_per_stage > 0 {
16727            workers_per_stage
16728        } else {
16729            self.parallel_thread_count()
16730        };
16731        let subs = self.subs.clone();
16732        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16733
16734        // Build channels: one between each pair of stages, plus one for output.
16735        // channel[0]: source → stage 0
16736        // channel[i]: stage i-1 → stage i
16737        // channel[n_stages]: stage n_stages-1 → collector
16738        let mut channels: Vec<(Sender<PerlValue>, Receiver<PerlValue>)> =
16739            (0..=n_stages).map(|_| bounded(buffer)).collect();
16740
16741        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
16742        let take_done: Arc<std::sync::atomic::AtomicBool> =
16743            Arc::new(std::sync::atomic::AtomicBool::new(false));
16744
16745        // Collect senders/receivers for each stage.
16746        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
16747        let source_tx = channels[0].0.clone();
16748        let result_rx = channels[n_stages].1.clone();
16749        let results: Arc<Mutex<Vec<PerlValue>>> = Arc::new(Mutex::new(Vec::new()));
16750
16751        std::thread::scope(|scope| {
16752            // Collector thread: drain results concurrently to avoid deadlock
16753            // when bounded channels fill up.
16754            let result_rx_c = result_rx.clone();
16755            let results_c = Arc::clone(&results);
16756            scope.spawn(move || {
16757                while let Ok(item) = result_rx_c.recv() {
16758                    results_c.lock().push(item);
16759                }
16760            });
16761
16762            // Source feeder thread.
16763            let err_s = Arc::clone(&err);
16764            let take_done_s = Arc::clone(&take_done);
16765            scope.spawn(move || {
16766                for item in source {
16767                    if err_s.lock().is_some()
16768                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
16769                    {
16770                        break;
16771                    }
16772                    if source_tx.send(item).is_err() {
16773                        break;
16774                    }
16775                }
16776            });
16777
16778            // Spawn workers for each stage.
16779            for (stage_idx, op) in streamable_ops.iter().enumerate() {
16780                let rx = channels[stage_idx].1.clone();
16781                let tx = channels[stage_idx + 1].0.clone();
16782
16783                for _ in 0..wn {
16784                    let rx = rx.clone();
16785                    let tx = tx.clone();
16786                    let subs = subs.clone();
16787                    let capture = capture.clone();
16788                    let atomic_arrays = atomic_arrays.clone();
16789                    let atomic_hashes = atomic_hashes.clone();
16790                    let err_w = Arc::clone(&err);
16791                    let take_done_w = Arc::clone(&take_done);
16792
16793                    match *op {
16794                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
16795                            let sub = Arc::clone(sub);
16796                            scope.spawn(move || {
16797                                while let Ok(item) = rx.recv() {
16798                                    if err_w.lock().is_some() {
16799                                        break;
16800                                    }
16801                                    let mut interp = Interpreter::new();
16802                                    interp.subs = subs.clone();
16803                                    interp.scope.restore_capture(&capture);
16804                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16805                                    interp.enable_parallel_guard();
16806                                    interp.scope.set_topic(item.clone());
16807                                    interp.scope_push_hook();
16808                                    let keep = match interp.exec_block_no_scope(&sub.body) {
16809                                        Ok(val) => val.is_true(),
16810                                        Err(_) => false,
16811                                    };
16812                                    interp.scope_pop_hook();
16813                                    if keep && tx.send(item).is_err() {
16814                                        break;
16815                                    }
16816                                }
16817                            });
16818                        }
16819                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
16820                            let sub = Arc::clone(sub);
16821                            scope.spawn(move || {
16822                                while let Ok(item) = rx.recv() {
16823                                    if err_w.lock().is_some() {
16824                                        break;
16825                                    }
16826                                    let mut interp = Interpreter::new();
16827                                    interp.subs = subs.clone();
16828                                    interp.scope.restore_capture(&capture);
16829                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16830                                    interp.enable_parallel_guard();
16831                                    interp.scope.set_topic(item);
16832                                    interp.scope_push_hook();
16833                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
16834                                        Ok(val) => val,
16835                                        Err(_) => PerlValue::UNDEF,
16836                                    };
16837                                    interp.scope_pop_hook();
16838                                    if tx.send(mapped).is_err() {
16839                                        break;
16840                                    }
16841                                }
16842                            });
16843                        }
16844                        PipelineOp::Take(n) => {
16845                            let limit = (*n).max(0) as usize;
16846                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
16847                            let count_w = Arc::clone(&count);
16848                            scope.spawn(move || {
16849                                while let Ok(item) = rx.recv() {
16850                                    let prev =
16851                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
16852                                    if prev >= limit {
16853                                        take_done_w
16854                                            .store(true, std::sync::atomic::Ordering::Relaxed);
16855                                        break;
16856                                    }
16857                                    if tx.send(item).is_err() {
16858                                        break;
16859                                    }
16860                                }
16861                            });
16862                            // Take only needs 1 worker; skip remaining worker spawns.
16863                            break;
16864                        }
16865                        PipelineOp::PFor { ref sub, .. } => {
16866                            let sub = Arc::clone(sub);
16867                            scope.spawn(move || {
16868                                while let Ok(item) = rx.recv() {
16869                                    if err_w.lock().is_some() {
16870                                        break;
16871                                    }
16872                                    let mut interp = Interpreter::new();
16873                                    interp.subs = subs.clone();
16874                                    interp.scope.restore_capture(&capture);
16875                                    interp
16876                                        .scope
16877                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16878                                    interp.enable_parallel_guard();
16879                                    interp.scope.set_topic(item.clone());
16880                                    interp.scope_push_hook();
16881                                    match interp.exec_block_no_scope(&sub.body) {
16882                                        Ok(_) => {}
16883                                        Err(e) => {
16884                                            let msg = match e {
16885                                                FlowOrError::Error(stryke) => stryke.to_string(),
16886                                                FlowOrError::Flow(_) => {
16887                                                    "unexpected control flow in par_pipeline_stream pfor".into()
16888                                                }
16889                                            };
16890                                            let mut g = err_w.lock();
16891                                            if g.is_none() {
16892                                                *g = Some(msg);
16893                                            }
16894                                            interp.scope_pop_hook();
16895                                            break;
16896                                        }
16897                                    }
16898                                    interp.scope_pop_hook();
16899                                    if tx.send(item).is_err() {
16900                                        break;
16901                                    }
16902                                }
16903                            });
16904                        }
16905                        PipelineOp::Tap(ref sub) => {
16906                            let sub = Arc::clone(sub);
16907                            scope.spawn(move || {
16908                                while let Ok(item) = rx.recv() {
16909                                    if err_w.lock().is_some() {
16910                                        break;
16911                                    }
16912                                    let mut interp = Interpreter::new();
16913                                    interp.subs = subs.clone();
16914                                    interp.scope.restore_capture(&capture);
16915                                    interp
16916                                        .scope
16917                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16918                                    interp.enable_parallel_guard();
16919                                    match interp.call_sub(
16920                                        &sub,
16921                                        vec![item.clone()],
16922                                        WantarrayCtx::Void,
16923                                        line,
16924                                    )
16925                                    {
16926                                        Ok(_) => {}
16927                                        Err(e) => {
16928                                            let msg = match e {
16929                                                FlowOrError::Error(stryke) => stryke.to_string(),
16930                                                FlowOrError::Flow(_) => {
16931                                                    "unexpected control flow in par_pipeline_stream tap"
16932                                                        .into()
16933                                                }
16934                                            };
16935                                            let mut g = err_w.lock();
16936                                            if g.is_none() {
16937                                                *g = Some(msg);
16938                                            }
16939                                            break;
16940                                        }
16941                                    }
16942                                    if tx.send(item).is_err() {
16943                                        break;
16944                                    }
16945                                }
16946                            });
16947                        }
16948                        PipelineOp::PCache { ref sub, .. } => {
16949                            let sub = Arc::clone(sub);
16950                            scope.spawn(move || {
16951                                while let Ok(item) = rx.recv() {
16952                                    if err_w.lock().is_some() {
16953                                        break;
16954                                    }
16955                                    let k = crate::pcache::cache_key(&item);
16956                                    let val = if let Some(cached) =
16957                                        crate::pcache::GLOBAL_PCACHE.get(&k)
16958                                    {
16959                                        cached.clone()
16960                                    } else {
16961                                        let mut interp = Interpreter::new();
16962                                        interp.subs = subs.clone();
16963                                        interp.scope.restore_capture(&capture);
16964                                        interp
16965                                            .scope
16966                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
16967                                        interp.enable_parallel_guard();
16968                                        interp.scope.set_topic(item);
16969                                        interp.scope_push_hook();
16970                                        let v = match interp.exec_block_no_scope(&sub.body) {
16971                                            Ok(v) => v,
16972                                            Err(_) => PerlValue::UNDEF,
16973                                        };
16974                                        interp.scope_pop_hook();
16975                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
16976                                        v
16977                                    };
16978                                    if tx.send(val).is_err() {
16979                                        break;
16980                                    }
16981                                }
16982                            });
16983                        }
16984                        // Non-streaming ops already rejected above.
16985                        _ => unreachable!(),
16986                    }
16987                }
16988            }
16989
16990            // Drop our copies of intermediate senders/receivers so channels disconnect
16991            // when workers finish.  Also drop result_rx so the collector thread exits
16992            // once all stage workers are done.
16993            channels.clear();
16994            drop(result_rx);
16995        });
16996
16997        if let Some(msg) = err.lock().take() {
16998            return Err(PerlError::runtime(msg, line));
16999        }
17000
17001        let results = std::mem::take(&mut *results.lock());
17002        Ok(PerlValue::array(results))
17003    }
17004
17005    fn heap_compare(&mut self, cmp: &Arc<PerlSub>, a: &PerlValue, b: &PerlValue) -> Ordering {
17006        self.scope_push_hook();
17007        if let Some(ref env) = cmp.closure_env {
17008            self.scope.restore_capture(env);
17009        }
17010        let _ = self.scope.set_scalar("a", a.clone());
17011        let _ = self.scope.set_scalar("b", b.clone());
17012        let _ = self.scope.set_scalar("_0", a.clone());
17013        let _ = self.scope.set_scalar("_1", b.clone());
17014        let ord = match self.exec_block_no_scope(&cmp.body) {
17015            Ok(v) => {
17016                let n = v.to_int();
17017                if n < 0 {
17018                    Ordering::Less
17019                } else if n > 0 {
17020                    Ordering::Greater
17021                } else {
17022                    Ordering::Equal
17023                }
17024            }
17025            Err(_) => Ordering::Equal,
17026        };
17027        self.scope_pop_hook();
17028        ord
17029    }
17030
17031    fn heap_sift_up(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
17032        while i > 0 {
17033            let p = (i - 1) / 2;
17034            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
17035                break;
17036            }
17037            items.swap(i, p);
17038            i = p;
17039        }
17040    }
17041
17042    fn heap_sift_down(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
17043        let n = items.len();
17044        loop {
17045            let mut sm = i;
17046            let l = 2 * i + 1;
17047            let r = 2 * i + 2;
17048            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
17049                sm = l;
17050            }
17051            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
17052                sm = r;
17053            }
17054            if sm == i {
17055                break;
17056            }
17057            items.swap(i, sm);
17058            i = sm;
17059        }
17060    }
17061
17062    fn hash_for_signature_destruct(
17063        &mut self,
17064        v: &PerlValue,
17065        line: usize,
17066    ) -> PerlResult<IndexMap<String, PerlValue>> {
17067        let Some(m) = self.match_subject_as_hash(v) else {
17068            return Err(PerlError::runtime(
17069                format!(
17070                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
17071                    v.ref_type()
17072                ),
17073                line,
17074            ));
17075        };
17076        Ok(m)
17077    }
17078
17079    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
17080    pub(crate) fn apply_sub_signature(
17081        &mut self,
17082        sub: &PerlSub,
17083        argv: &[PerlValue],
17084        line: usize,
17085    ) -> PerlResult<()> {
17086        if sub.params.is_empty() {
17087            return Ok(());
17088        }
17089        let mut i = 0usize;
17090        for p in &sub.params {
17091            match p {
17092                SubSigParam::Scalar(name, ty, default) => {
17093                    let val = if i < argv.len() {
17094                        argv[i].clone()
17095                    } else if let Some(default_expr) = default {
17096                        match self.eval_expr(default_expr) {
17097                            Ok(v) => v,
17098                            Err(FlowOrError::Error(e)) => return Err(e),
17099                            Err(FlowOrError::Flow(_)) => {
17100                                return Err(PerlError::runtime(
17101                                    "unexpected control flow in parameter default",
17102                                    line,
17103                                ))
17104                            }
17105                        }
17106                    } else {
17107                        PerlValue::UNDEF
17108                    };
17109                    i += 1;
17110                    if let Some(t) = ty {
17111                        if let Err(e) = t.check_value(&val) {
17112                            return Err(PerlError::runtime(
17113                                format!("sub parameter ${}: {}", name, e),
17114                                line,
17115                            ));
17116                        }
17117                    }
17118                    let n = self.english_scalar_name(name);
17119                    self.scope.declare_scalar(n, val);
17120                }
17121                SubSigParam::Array(name, default) => {
17122                    let rest: Vec<PerlValue> = if i < argv.len() {
17123                        let r = argv[i..].to_vec();
17124                        i = argv.len();
17125                        r
17126                    } else if let Some(default_expr) = default {
17127                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17128                            Ok(v) => v,
17129                            Err(FlowOrError::Error(e)) => return Err(e),
17130                            Err(FlowOrError::Flow(_)) => {
17131                                return Err(PerlError::runtime(
17132                                    "unexpected control flow in parameter default",
17133                                    line,
17134                                ))
17135                            }
17136                        };
17137                        val.to_list()
17138                    } else {
17139                        vec![]
17140                    };
17141                    let aname = self.stash_array_name_for_package(name);
17142                    self.scope.declare_array(&aname, rest);
17143                }
17144                SubSigParam::Hash(name, default) => {
17145                    let rest: Vec<PerlValue> = if i < argv.len() {
17146                        let r = argv[i..].to_vec();
17147                        i = argv.len();
17148                        r
17149                    } else if let Some(default_expr) = default {
17150                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17151                            Ok(v) => v,
17152                            Err(FlowOrError::Error(e)) => return Err(e),
17153                            Err(FlowOrError::Flow(_)) => {
17154                                return Err(PerlError::runtime(
17155                                    "unexpected control flow in parameter default",
17156                                    line,
17157                                ))
17158                            }
17159                        };
17160                        val.to_list()
17161                    } else {
17162                        vec![]
17163                    };
17164                    let mut map = IndexMap::new();
17165                    let mut j = 0;
17166                    while j + 1 < rest.len() {
17167                        map.insert(rest[j].to_string(), rest[j + 1].clone());
17168                        j += 2;
17169                    }
17170                    self.scope.declare_hash(name, map);
17171                }
17172                SubSigParam::ArrayDestruct(elems) => {
17173                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17174                    i += 1;
17175                    let Some(arr) = self.match_subject_as_array(&arg) else {
17176                        return Err(PerlError::runtime(
17177                            format!(
17178                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
17179                                arg.ref_type()
17180                            ),
17181                            line,
17182                        ));
17183                    };
17184                    let binds = self
17185                        .match_array_pattern_elems(&arr, elems, line)
17186                        .map_err(|e| match e {
17187                            FlowOrError::Error(stryke) => stryke,
17188                            FlowOrError::Flow(_) => PerlError::runtime(
17189                                "unexpected flow in sub signature array destruct",
17190                                line,
17191                            ),
17192                        })?;
17193                    let Some(binds) = binds else {
17194                        return Err(PerlError::runtime(
17195                            "sub signature array destruct: length or element mismatch",
17196                            line,
17197                        ));
17198                    };
17199                    for b in binds {
17200                        match b {
17201                            PatternBinding::Scalar(name, v) => {
17202                                let n = self.english_scalar_name(&name);
17203                                self.scope.declare_scalar(n, v);
17204                            }
17205                            PatternBinding::Array(name, elems) => {
17206                                self.scope.declare_array(&name, elems);
17207                            }
17208                        }
17209                    }
17210                }
17211                SubSigParam::HashDestruct(pairs) => {
17212                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17213                    i += 1;
17214                    let map = self.hash_for_signature_destruct(&arg, line)?;
17215                    for (key, varname) in pairs {
17216                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
17217                        let n = self.english_scalar_name(varname);
17218                        self.scope.declare_scalar(n, v);
17219                    }
17220                }
17221            }
17222        }
17223        Ok(())
17224    }
17225
17226    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
17227    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
17228    /// These are `PerlSub`s with empty bodies and magic keys in `closure_env`.
17229    pub(crate) fn try_hof_dispatch(
17230        &mut self,
17231        sub: &PerlSub,
17232        args: &[PerlValue],
17233        want: WantarrayCtx,
17234        line: usize,
17235    ) -> Option<ExecResult> {
17236        let env = sub.closure_env.as_ref()?;
17237        fn env_get<'a>(env: &'a [(String, PerlValue)], key: &str) -> Option<&'a PerlValue> {
17238            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
17239        }
17240
17241        match sub.name.as_str() {
17242            // ── compose: right-to-left function application ──
17243            "__comp__" => {
17244                let fns = env_get(env, "__comp_fns__")?.to_list();
17245                let mut val = args.first().cloned().unwrap_or(PerlValue::UNDEF);
17246                for f in fns.iter().rev() {
17247                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
17248                        Ok(v) => val = v,
17249                        Err(e) => return Some(Err(e)),
17250                    }
17251                }
17252                Some(Ok(val))
17253            }
17254            // ── constantly: always return the captured value ──
17255            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
17256            // ── juxt: call each fn with same args, collect results ──
17257            "__juxt__" => {
17258                let fns = env_get(env, "__juxt_fns__")?.to_list();
17259                let mut results = Vec::with_capacity(fns.len());
17260                for f in &fns {
17261                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
17262                        Ok(v) => results.push(v),
17263                        Err(e) => return Some(Err(e)),
17264                    }
17265                }
17266                Some(Ok(PerlValue::array(results)))
17267            }
17268            // ── partial: prepend bound args ──
17269            "__partial__" => {
17270                let fn_val = env_get(env, "__partial_fn__")?.clone();
17271                let bound = env_get(env, "__partial_args__")?.to_list();
17272                let mut all_args = bound;
17273                all_args.extend_from_slice(args);
17274                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
17275            }
17276            // ── complement: negate the result ──
17277            "__complement__" => {
17278                let fn_val = env_get(env, "__complement_fn__")?.clone();
17279                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17280                    Ok(v) => Some(Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }))),
17281                    Err(e) => Some(Err(e)),
17282                }
17283            }
17284            // ── fnil: replace undef args with defaults ──
17285            "__fnil__" => {
17286                let fn_val = env_get(env, "__fnil_fn__")?.clone();
17287                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
17288                let mut patched = args.to_vec();
17289                for (i, d) in defaults.iter().enumerate() {
17290                    if i < patched.len() {
17291                        if patched[i].is_undef() {
17292                            patched[i] = d.clone();
17293                        }
17294                    } else {
17295                        patched.push(d.clone());
17296                    }
17297                }
17298                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
17299            }
17300            // ── memoize: cache by stringified args ──
17301            "__memoize__" => {
17302                let fn_val = env_get(env, "__memoize_fn__")?.clone();
17303                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
17304                let key = args
17305                    .iter()
17306                    .map(|a| a.to_string())
17307                    .collect::<Vec<_>>()
17308                    .join("\x00");
17309                if let Some(href) = cache_ref.as_hash_ref() {
17310                    if let Some(cached) = href.read().get(&key) {
17311                        return Some(Ok(cached.clone()));
17312                    }
17313                }
17314                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17315                    Ok(v) => {
17316                        if let Some(href) = cache_ref.as_hash_ref() {
17317                            href.write().insert(key, v.clone());
17318                        }
17319                        Some(Ok(v))
17320                    }
17321                    Err(e) => Some(Err(e)),
17322                }
17323            }
17324            // ── curry: accumulate args until arity reached ──
17325            "__curry__" => {
17326                let fn_val = env_get(env, "__curry_fn__")?.clone();
17327                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
17328                let bound = env_get(env, "__curry_bound__")?.to_list();
17329                let mut all = bound;
17330                all.extend_from_slice(args);
17331                if all.len() >= arity {
17332                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
17333                } else {
17334                    let curry_sub = PerlSub {
17335                        name: "__curry__".to_string(),
17336                        params: vec![],
17337                        body: vec![],
17338                        closure_env: Some(vec![
17339                            ("__curry_fn__".to_string(), fn_val),
17340                            (
17341                                "__curry_arity__".to_string(),
17342                                PerlValue::integer(arity as i64),
17343                            ),
17344                            ("__curry_bound__".to_string(), PerlValue::array(all)),
17345                        ]),
17346                        prototype: None,
17347                        fib_like: None,
17348                    };
17349                    Some(Ok(PerlValue::code_ref(Arc::new(curry_sub))))
17350                }
17351            }
17352            // ── once: call once, cache forever ──
17353            "__once__" => {
17354                let cache_ref = env_get(env, "__once_cache__")?.clone();
17355                if let Some(href) = cache_ref.as_hash_ref() {
17356                    let r = href.read();
17357                    if r.contains_key("done") {
17358                        return Some(Ok(r.get("val").cloned().unwrap_or(PerlValue::UNDEF)));
17359                    }
17360                }
17361                let fn_val = env_get(env, "__once_fn__")?.clone();
17362                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17363                    Ok(v) => {
17364                        if let Some(href) = cache_ref.as_hash_ref() {
17365                            let mut w = href.write();
17366                            w.insert("done".to_string(), PerlValue::integer(1));
17367                            w.insert("val".to_string(), v.clone());
17368                        }
17369                        Some(Ok(v))
17370                    }
17371                    Err(e) => Some(Err(e)),
17372                }
17373            }
17374            _ => None,
17375        }
17376    }
17377
17378    pub(crate) fn call_sub(
17379        &mut self,
17380        sub: &PerlSub,
17381        args: Vec<PerlValue>,
17382        want: WantarrayCtx,
17383        _line: usize,
17384    ) -> ExecResult {
17385        // Push current sub for __SUB__ access
17386        self.current_sub_stack.push(Arc::new(sub.clone()));
17387
17388        // Single frame for both @_ and the block's local variables —
17389        // avoids the double push_frame/pop_frame overhead per call.
17390        self.scope_push_hook();
17391        self.scope.declare_array("_", args.clone());
17392        if let Some(ref env) = sub.closure_env {
17393            self.scope.restore_capture(env);
17394        }
17395        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
17396        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
17397        // Must be AFTER restore_capture so we don't get shadowed by captured $_
17398        self.scope.set_closure_args(&args);
17399        // Move `@_` out so `fib_like` / hof dispatch take `&[PerlValue]` without cloning.
17400        let argv = self.scope.take_sub_underscore().unwrap_or_default();
17401        self.apply_sub_signature(sub, &argv, _line)?;
17402        let saved = self.wantarray_kind;
17403        self.wantarray_kind = want;
17404        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
17405            self.wantarray_kind = saved;
17406            self.scope_pop_hook();
17407            self.current_sub_stack.pop();
17408            return match r {
17409                Ok(v) => Ok(v),
17410                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17411                Err(e) => Err(e),
17412            };
17413        }
17414        if let Some(pat) = sub.fib_like.as_ref() {
17415            if argv.len() == 1 {
17416                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
17417                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
17418                    if let Some(p) = &mut self.profiler {
17419                        p.enter_sub(&sub.name);
17420                    }
17421                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
17422                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
17423                        p.exit_sub(t0.elapsed());
17424                    }
17425                    self.wantarray_kind = saved;
17426                    self.scope_pop_hook();
17427                    self.current_sub_stack.pop();
17428                    return Ok(PerlValue::integer(n));
17429                }
17430            }
17431        }
17432        self.scope.declare_array("_", argv.clone());
17433        // Note: set_closure_args was already called at line 15077; don't call it again
17434        // as that would incorrectly shift the outer topic stack a second time.
17435        let t0 = self.profiler.is_some().then(std::time::Instant::now);
17436        if let Some(p) = &mut self.profiler {
17437            p.enter_sub(&sub.name);
17438        }
17439        // Always evaluate the function body's last expression in List context so
17440        // `@array` returns the array contents, not the count. The caller adapts the
17441        // return value to their own wantarray context after receiving it.
17442        let result = self.exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List);
17443        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
17444            p.exit_sub(t0.elapsed());
17445        }
17446        // For goto &sub, capture @_ before popping the frame
17447        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
17448            Some(self.scope.get_array("_"))
17449        } else {
17450            None
17451        };
17452        self.wantarray_kind = saved;
17453        self.scope_pop_hook();
17454        self.current_sub_stack.pop();
17455        match result {
17456            Ok(v) => Ok(v),
17457            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17458            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
17459                // goto &sub — tail call: look up target and call with same @_
17460                let goto_args = goto_args.unwrap_or_default();
17461                let fqn = if target_name.contains("::") {
17462                    target_name.clone()
17463                } else {
17464                    format!("{}::{}", self.current_package(), target_name)
17465                };
17466                if let Some(target_sub) = self
17467                    .subs
17468                    .get(&fqn)
17469                    .cloned()
17470                    .or_else(|| self.subs.get(&target_name).cloned())
17471                {
17472                    self.call_sub(&target_sub, goto_args, want, _line)
17473                } else {
17474                    Err(
17475                        PerlError::runtime(format!("Undefined subroutine &{}", target_name), _line)
17476                            .into(),
17477                    )
17478                }
17479            }
17480            Err(FlowOrError::Flow(Flow::Yield(_))) => {
17481                Err(PerlError::runtime("yield is only valid inside gen { }", 0).into())
17482            }
17483            Err(e) => Err(e),
17484        }
17485    }
17486
17487    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
17488    fn call_struct_method(
17489        &mut self,
17490        body: &Block,
17491        params: &[SubSigParam],
17492        args: Vec<PerlValue>,
17493        line: usize,
17494    ) -> ExecResult {
17495        self.scope_push_hook();
17496        self.scope.declare_array("_", args.clone());
17497        // Bind $self to first arg (the receiver)
17498        if let Some(self_val) = args.first() {
17499            self.scope.declare_scalar("self", self_val.clone());
17500        }
17501        // Set $_0, $_1, etc. for all args
17502        self.scope.set_closure_args(&args);
17503        // Apply signature if provided - skip the first arg ($self) for user params
17504        let user_args: Vec<PerlValue> = args.iter().skip(1).cloned().collect();
17505        self.apply_params_to_argv(params, &user_args, line)?;
17506        let result = self.exec_block_no_scope(body);
17507        self.scope_pop_hook();
17508        match result {
17509            Ok(v) => Ok(v),
17510            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17511            Err(e) => Err(e),
17512        }
17513    }
17514
17515    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
17516    pub(crate) fn call_class_method(
17517        &mut self,
17518        body: &Block,
17519        params: &[SubSigParam],
17520        args: Vec<PerlValue>,
17521        line: usize,
17522    ) -> ExecResult {
17523        self.call_class_method_inner(body, params, args, line, false)
17524    }
17525
17526    /// Call a static class method: `Math::add(...)`.
17527    pub(crate) fn call_static_class_method(
17528        &mut self,
17529        body: &Block,
17530        params: &[SubSigParam],
17531        args: Vec<PerlValue>,
17532        line: usize,
17533    ) -> ExecResult {
17534        self.call_class_method_inner(body, params, args, line, true)
17535    }
17536
17537    fn call_class_method_inner(
17538        &mut self,
17539        body: &Block,
17540        params: &[SubSigParam],
17541        args: Vec<PerlValue>,
17542        line: usize,
17543        is_static: bool,
17544    ) -> ExecResult {
17545        self.scope_push_hook();
17546        self.scope.declare_array("_", args.clone());
17547        if !is_static {
17548            // Bind $self to first arg (the receiver) for instance methods
17549            if let Some(self_val) = args.first() {
17550                self.scope.declare_scalar("self", self_val.clone());
17551            }
17552        }
17553        // Set $_0, $_1, etc. for all args
17554        self.scope.set_closure_args(&args);
17555        // Apply signature: skip first arg ($self) only for instance methods
17556        let user_args: Vec<PerlValue> = if is_static {
17557            args.clone()
17558        } else {
17559            args.iter().skip(1).cloned().collect()
17560        };
17561        self.apply_params_to_argv(params, &user_args, line)?;
17562        let result = self.exec_block_no_scope(body);
17563        self.scope_pop_hook();
17564        match result {
17565            Ok(v) => Ok(v),
17566            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17567            Err(e) => Err(e),
17568        }
17569    }
17570
17571    /// Apply SubSigParam bindings without the full PerlSub machinery.
17572    fn apply_params_to_argv(
17573        &mut self,
17574        params: &[SubSigParam],
17575        argv: &[PerlValue],
17576        line: usize,
17577    ) -> PerlResult<()> {
17578        let mut i = 0;
17579        for param in params {
17580            match param {
17581                SubSigParam::Scalar(name, ty_opt, default) => {
17582                    let v = if i < argv.len() {
17583                        argv[i].clone()
17584                    } else if let Some(default_expr) = default {
17585                        match self.eval_expr(default_expr) {
17586                            Ok(v) => v,
17587                            Err(FlowOrError::Error(e)) => return Err(e),
17588                            Err(FlowOrError::Flow(_)) => {
17589                                return Err(PerlError::runtime(
17590                                    "unexpected control flow in parameter default",
17591                                    line,
17592                                ))
17593                            }
17594                        }
17595                    } else {
17596                        PerlValue::UNDEF
17597                    };
17598                    i += 1;
17599                    if let Some(ty) = ty_opt {
17600                        ty.check_value(&v).map_err(|msg| {
17601                            PerlError::type_error(
17602                                format!("method parameter ${}: {}", name, msg),
17603                                line,
17604                            )
17605                        })?;
17606                    }
17607                    let n = self.english_scalar_name(name);
17608                    self.scope.declare_scalar(n, v);
17609                }
17610                SubSigParam::Array(name, default) => {
17611                    let rest: Vec<PerlValue> = if i < argv.len() {
17612                        let r = argv[i..].to_vec();
17613                        i = argv.len();
17614                        r
17615                    } else if let Some(default_expr) = default {
17616                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17617                            Ok(v) => v,
17618                            Err(FlowOrError::Error(e)) => return Err(e),
17619                            Err(FlowOrError::Flow(_)) => {
17620                                return Err(PerlError::runtime(
17621                                    "unexpected control flow in parameter default",
17622                                    line,
17623                                ))
17624                            }
17625                        };
17626                        val.to_list()
17627                    } else {
17628                        vec![]
17629                    };
17630                    let aname = self.stash_array_name_for_package(name);
17631                    self.scope.declare_array(&aname, rest);
17632                }
17633                SubSigParam::Hash(name, default) => {
17634                    let rest: Vec<PerlValue> = if i < argv.len() {
17635                        let r = argv[i..].to_vec();
17636                        i = argv.len();
17637                        r
17638                    } else if let Some(default_expr) = default {
17639                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17640                            Ok(v) => v,
17641                            Err(FlowOrError::Error(e)) => return Err(e),
17642                            Err(FlowOrError::Flow(_)) => {
17643                                return Err(PerlError::runtime(
17644                                    "unexpected control flow in parameter default",
17645                                    line,
17646                                ))
17647                            }
17648                        };
17649                        val.to_list()
17650                    } else {
17651                        vec![]
17652                    };
17653                    let mut map = IndexMap::new();
17654                    let mut j = 0;
17655                    while j + 1 < rest.len() {
17656                        map.insert(rest[j].to_string(), rest[j + 1].clone());
17657                        j += 2;
17658                    }
17659                    self.scope.declare_hash(name, map);
17660                }
17661                SubSigParam::ArrayDestruct(elems) => {
17662                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17663                    i += 1;
17664                    let Some(arr) = self.match_subject_as_array(&arg) else {
17665                        return Err(PerlError::runtime(
17666                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
17667                            line,
17668                        ));
17669                    };
17670                    let binds = self
17671                        .match_array_pattern_elems(&arr, elems, line)
17672                        .map_err(|e| match e {
17673                            FlowOrError::Error(stryke) => stryke,
17674                            FlowOrError::Flow(_) => {
17675                                PerlError::runtime("unexpected flow in method array destruct", line)
17676                            }
17677                        })?;
17678                    let Some(binds) = binds else {
17679                        return Err(PerlError::runtime(
17680                            format!(
17681                                "method parameter: array destructure failed at position {}",
17682                                i
17683                            ),
17684                            line,
17685                        ));
17686                    };
17687                    for b in binds {
17688                        match b {
17689                            PatternBinding::Scalar(name, v) => {
17690                                let n = self.english_scalar_name(&name);
17691                                self.scope.declare_scalar(n, v);
17692                            }
17693                            PatternBinding::Array(name, elems) => {
17694                                self.scope.declare_array(&name, elems);
17695                            }
17696                        }
17697                    }
17698                }
17699                SubSigParam::HashDestruct(pairs) => {
17700                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17701                    i += 1;
17702                    let map = self.hash_for_signature_destruct(&arg, line)?;
17703                    for (key, varname) in pairs {
17704                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
17705                        let n = self.english_scalar_name(varname);
17706                        self.scope.declare_scalar(n, v);
17707                    }
17708                }
17709            }
17710        }
17711        Ok(())
17712    }
17713
17714    fn builtin_new(&mut self, class: &str, args: Vec<PerlValue>, line: usize) -> ExecResult {
17715        if class == "Set" {
17716            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
17717        }
17718        if let Some(def) = self.struct_defs.get(class).cloned() {
17719            let mut provided = Vec::new();
17720            let mut i = 1;
17721            while i + 1 < args.len() {
17722                let k = args[i].to_string();
17723                let v = args[i + 1].clone();
17724                provided.push((k, v));
17725                i += 2;
17726            }
17727            let mut defaults = Vec::with_capacity(def.fields.len());
17728            for field in &def.fields {
17729                if let Some(ref expr) = field.default {
17730                    let val = self.eval_expr(expr)?;
17731                    defaults.push(Some(val));
17732                } else {
17733                    defaults.push(None);
17734                }
17735            }
17736            return Ok(crate::native_data::struct_new_with_defaults(
17737                &def, &provided, &defaults, line,
17738            )?);
17739        }
17740        // Default OO constructor: Class->new(%args) → bless {%args}, class
17741        let mut map = IndexMap::new();
17742        let mut i = 1; // skip $self (first arg is class name)
17743        while i + 1 < args.len() {
17744            let k = args[i].to_string();
17745            let v = args[i + 1].clone();
17746            map.insert(k, v);
17747            i += 2;
17748        }
17749        Ok(PerlValue::blessed(Arc::new(
17750            crate::value::BlessedRef::new_blessed(class.to_string(), PerlValue::hash(map)),
17751        )))
17752    }
17753
17754    fn exec_print(
17755        &mut self,
17756        handle: Option<&str>,
17757        args: &[Expr],
17758        newline: bool,
17759        line: usize,
17760    ) -> ExecResult {
17761        if newline && (self.feature_bits & FEAT_SAY) == 0 {
17762            return Err(PerlError::runtime(
17763                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
17764                line,
17765            )
17766            .into());
17767        }
17768        let mut output = String::new();
17769        if args.is_empty() {
17770            // Perl: print with no LIST prints $_ (same for say).
17771            let topic = self.scope.get_scalar("_").clone();
17772            let s = self.stringify_value(topic, line)?;
17773            output.push_str(&s);
17774        } else {
17775            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
17776            // between those top-level expressions only (not between elements of an expanded `@arr`).
17777            for (i, a) in args.iter().enumerate() {
17778                if i > 0 {
17779                    output.push_str(&self.ofs);
17780                }
17781                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17782                for item in val.to_list() {
17783                    let s = self.stringify_value(item, line)?;
17784                    output.push_str(&s);
17785                }
17786            }
17787        }
17788        if newline {
17789            output.push('\n');
17790        }
17791        output.push_str(&self.ors);
17792
17793        let handle_name =
17794            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17795        self.write_formatted_print(handle_name.as_str(), &output, line)?;
17796        Ok(PerlValue::integer(1))
17797    }
17798
17799    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
17800        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
17801            // Perl: printf with no args uses $_ as the format string.
17802            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
17803            (s, &[])
17804        } else {
17805            (self.eval_expr(&args[0])?.to_string(), &args[1..])
17806        };
17807        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
17808        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
17809        // ranges to flip-flop values, so go through list-context eval and splat.
17810        let mut arg_vals = Vec::new();
17811        for a in rest {
17812            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17813            if let Some(items) = v.as_array_vec() {
17814                arg_vals.extend(items);
17815            } else {
17816                arg_vals.push(v);
17817            }
17818        }
17819        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
17820        let handle_name =
17821            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17822        match handle_name.as_str() {
17823            "STDOUT" => {
17824                if !self.suppress_stdout {
17825                    print!("{}", output);
17826                    if self.output_autoflush {
17827                        let _ = io::stdout().flush();
17828                    }
17829                }
17830            }
17831            "STDERR" => {
17832                eprint!("{}", output);
17833                let _ = io::stderr().flush();
17834            }
17835            name => {
17836                if let Some(writer) = self.output_handles.get_mut(name) {
17837                    let _ = writer.write_all(output.as_bytes());
17838                    if self.output_autoflush {
17839                        let _ = writer.flush();
17840                    }
17841                }
17842            }
17843        }
17844        Ok(PerlValue::integer(1))
17845    }
17846
17847    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
17848    pub(crate) fn eval_substr_expr(
17849        &mut self,
17850        string: &Expr,
17851        offset: &Expr,
17852        length: Option<&Expr>,
17853        replacement: Option<&Expr>,
17854        _line: usize,
17855    ) -> Result<PerlValue, FlowOrError> {
17856        let s = self.eval_expr(string)?.to_string();
17857        let off = self.eval_expr(offset)?.to_int();
17858        let start = if off < 0 {
17859            (s.len() as i64 + off).max(0) as usize
17860        } else {
17861            off as usize
17862        };
17863        let len = if let Some(l) = length {
17864            let len_val = self.eval_expr(l)?.to_int();
17865            if len_val < 0 {
17866                // Negative length: count from end of string
17867                let remaining = s.len().saturating_sub(start) as i64;
17868                (remaining + len_val).max(0) as usize
17869            } else {
17870                len_val as usize
17871            }
17872        } else {
17873            s.len().saturating_sub(start)
17874        };
17875        let end = start.saturating_add(len).min(s.len());
17876        let result = s.get(start..end).unwrap_or("").to_string();
17877        if let Some(rep) = replacement {
17878            let rep_s = self.eval_expr(rep)?.to_string();
17879            let mut new_s = String::new();
17880            new_s.push_str(&s[..start]);
17881            new_s.push_str(&rep_s);
17882            new_s.push_str(&s[end..]);
17883            self.assign_value(string, PerlValue::string(new_s))?;
17884        }
17885        Ok(PerlValue::string(result))
17886    }
17887
17888    pub(crate) fn eval_push_expr(
17889        &mut self,
17890        array: &Expr,
17891        values: &[Expr],
17892        line: usize,
17893    ) -> Result<PerlValue, FlowOrError> {
17894        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17895            for v in values {
17896                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17897                self.push_array_deref_value(aref.clone(), val, line)?;
17898            }
17899            let len = self.array_deref_len(aref, line)?;
17900            return Ok(PerlValue::integer(len));
17901        }
17902        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17903        if self.scope.is_array_frozen(&arr_name) {
17904            return Err(PerlError::runtime(
17905                format!("Modification of a frozen value: @{}", arr_name),
17906                line,
17907            )
17908            .into());
17909        }
17910        for v in values {
17911            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17912            if let Some(items) = val.as_array_vec() {
17913                for item in items {
17914                    self.scope
17915                        .push_to_array(&arr_name, item)
17916                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17917                }
17918            } else {
17919                self.scope
17920                    .push_to_array(&arr_name, val)
17921                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17922            }
17923        }
17924        let len = self.scope.array_len(&arr_name);
17925        Ok(PerlValue::integer(len as i64))
17926    }
17927
17928    pub(crate) fn eval_pop_expr(
17929        &mut self,
17930        array: &Expr,
17931        line: usize,
17932    ) -> Result<PerlValue, FlowOrError> {
17933        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17934            return self.pop_array_deref(aref, line);
17935        }
17936        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17937        self.scope
17938            .pop_from_array(&arr_name)
17939            .map_err(|e| FlowOrError::Error(e.at_line(line)))
17940    }
17941
17942    pub(crate) fn eval_shift_expr(
17943        &mut self,
17944        array: &Expr,
17945        line: usize,
17946    ) -> Result<PerlValue, FlowOrError> {
17947        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17948            return self.shift_array_deref(aref, line);
17949        }
17950        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17951        self.scope
17952            .shift_from_array(&arr_name)
17953            .map_err(|e| FlowOrError::Error(e.at_line(line)))
17954    }
17955
17956    pub(crate) fn eval_unshift_expr(
17957        &mut self,
17958        array: &Expr,
17959        values: &[Expr],
17960        line: usize,
17961    ) -> Result<PerlValue, FlowOrError> {
17962        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17963            let mut vals = Vec::new();
17964            for v in values {
17965                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17966                if let Some(items) = val.as_array_vec() {
17967                    vals.extend(items);
17968                } else {
17969                    vals.push(val);
17970                }
17971            }
17972            let len = self.unshift_array_deref_multi(aref, vals, line)?;
17973            return Ok(PerlValue::integer(len));
17974        }
17975        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17976        let mut vals = Vec::new();
17977        for v in values {
17978            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17979            if let Some(items) = val.as_array_vec() {
17980                vals.extend(items);
17981            } else {
17982                vals.push(val);
17983            }
17984        }
17985        let arr = self
17986            .scope
17987            .get_array_mut(&arr_name)
17988            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17989        for (i, v) in vals.into_iter().enumerate() {
17990            arr.insert(i, v);
17991        }
17992        let len = arr.len();
17993        Ok(PerlValue::integer(len as i64))
17994    }
17995
17996    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
17997    pub(crate) fn push_array_deref_value(
17998        &mut self,
17999        arr_ref: PerlValue,
18000        val: PerlValue,
18001        line: usize,
18002    ) -> Result<(), FlowOrError> {
18003        // Resolve binding refs in the value being stored so they snapshot
18004        // the current scope data and survive scope pop.
18005        let val = self.scope.resolve_container_binding_ref(val);
18006        if let Some(r) = arr_ref.as_array_ref() {
18007            let mut w = r.write();
18008            if let Some(items) = val.as_array_vec() {
18009                w.extend(items.iter().cloned());
18010            } else {
18011                w.push(val);
18012            }
18013            return Ok(());
18014        }
18015        if let Some(name) = arr_ref.as_array_binding_name() {
18016            if let Some(items) = val.as_array_vec() {
18017                for item in items {
18018                    self.scope
18019                        .push_to_array(&name, item)
18020                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18021                }
18022            } else {
18023                self.scope
18024                    .push_to_array(&name, val)
18025                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18026            }
18027            return Ok(());
18028        }
18029        if let Some(s) = arr_ref.as_str() {
18030            if self.strict_refs {
18031                return Err(PerlError::runtime(
18032                    format!(
18033                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18034                        s
18035                    ),
18036                    line,
18037                )
18038                .into());
18039            }
18040            let name = s.to_string();
18041            if let Some(items) = val.as_array_vec() {
18042                for item in items {
18043                    self.scope
18044                        .push_to_array(&name, item)
18045                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18046                }
18047            } else {
18048                self.scope
18049                    .push_to_array(&name, val)
18050                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18051            }
18052            return Ok(());
18053        }
18054        Err(PerlError::runtime("push argument is not an ARRAY reference", line).into())
18055    }
18056
18057    pub(crate) fn array_deref_len(
18058        &self,
18059        arr_ref: PerlValue,
18060        line: usize,
18061    ) -> Result<i64, FlowOrError> {
18062        if let Some(r) = arr_ref.as_array_ref() {
18063            return Ok(r.read().len() as i64);
18064        }
18065        if let Some(name) = arr_ref.as_array_binding_name() {
18066            return Ok(self.scope.array_len(&name) as i64);
18067        }
18068        if let Some(s) = arr_ref.as_str() {
18069            if self.strict_refs {
18070                return Err(PerlError::runtime(
18071                    format!(
18072                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18073                        s
18074                    ),
18075                    line,
18076                )
18077                .into());
18078            }
18079            return Ok(self.scope.array_len(&s) as i64);
18080        }
18081        Err(PerlError::runtime("argument is not an ARRAY reference", line).into())
18082    }
18083
18084    pub(crate) fn pop_array_deref(
18085        &mut self,
18086        arr_ref: PerlValue,
18087        line: usize,
18088    ) -> Result<PerlValue, FlowOrError> {
18089        if let Some(r) = arr_ref.as_array_ref() {
18090            let mut w = r.write();
18091            return Ok(w.pop().unwrap_or(PerlValue::UNDEF));
18092        }
18093        if let Some(name) = arr_ref.as_array_binding_name() {
18094            return self
18095                .scope
18096                .pop_from_array(&name)
18097                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18098        }
18099        if let Some(s) = arr_ref.as_str() {
18100            if self.strict_refs {
18101                return Err(PerlError::runtime(
18102                    format!(
18103                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18104                        s
18105                    ),
18106                    line,
18107                )
18108                .into());
18109            }
18110            return self
18111                .scope
18112                .pop_from_array(&s)
18113                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18114        }
18115        Err(PerlError::runtime("pop argument is not an ARRAY reference", line).into())
18116    }
18117
18118    pub(crate) fn shift_array_deref(
18119        &mut self,
18120        arr_ref: PerlValue,
18121        line: usize,
18122    ) -> Result<PerlValue, FlowOrError> {
18123        if let Some(r) = arr_ref.as_array_ref() {
18124            let mut w = r.write();
18125            return Ok(if w.is_empty() {
18126                PerlValue::UNDEF
18127            } else {
18128                w.remove(0)
18129            });
18130        }
18131        if let Some(name) = arr_ref.as_array_binding_name() {
18132            return self
18133                .scope
18134                .shift_from_array(&name)
18135                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18136        }
18137        if let Some(s) = arr_ref.as_str() {
18138            if self.strict_refs {
18139                return Err(PerlError::runtime(
18140                    format!(
18141                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18142                        s
18143                    ),
18144                    line,
18145                )
18146                .into());
18147            }
18148            return self
18149                .scope
18150                .shift_from_array(&s)
18151                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18152        }
18153        Err(PerlError::runtime("shift argument is not an ARRAY reference", line).into())
18154    }
18155
18156    pub(crate) fn unshift_array_deref_multi(
18157        &mut self,
18158        arr_ref: PerlValue,
18159        vals: Vec<PerlValue>,
18160        line: usize,
18161    ) -> Result<i64, FlowOrError> {
18162        let mut flat: Vec<PerlValue> = Vec::new();
18163        for v in vals {
18164            if let Some(items) = v.as_array_vec() {
18165                flat.extend(items);
18166            } else {
18167                flat.push(v);
18168            }
18169        }
18170        if let Some(r) = arr_ref.as_array_ref() {
18171            let mut w = r.write();
18172            for (i, v) in flat.into_iter().enumerate() {
18173                w.insert(i, v);
18174            }
18175            return Ok(w.len() as i64);
18176        }
18177        if let Some(name) = arr_ref.as_array_binding_name() {
18178            let arr = self
18179                .scope
18180                .get_array_mut(&name)
18181                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18182            for (i, v) in flat.into_iter().enumerate() {
18183                arr.insert(i, v);
18184            }
18185            return Ok(arr.len() as i64);
18186        }
18187        if let Some(s) = arr_ref.as_str() {
18188            if self.strict_refs {
18189                return Err(PerlError::runtime(
18190                    format!(
18191                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18192                        s
18193                    ),
18194                    line,
18195                )
18196                .into());
18197            }
18198            let name = s.to_string();
18199            let arr = self
18200                .scope
18201                .get_array_mut(&name)
18202                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18203            for (i, v) in flat.into_iter().enumerate() {
18204                arr.insert(i, v);
18205            }
18206            return Ok(arr.len() as i64);
18207        }
18208        Err(PerlError::runtime("unshift argument is not an ARRAY reference", line).into())
18209    }
18210
18211    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
18212    /// / compiler wraps `splice` like other context-sensitive builtins).
18213    pub(crate) fn splice_array_deref(
18214        &mut self,
18215        aref: PerlValue,
18216        offset_val: PerlValue,
18217        length_val: PerlValue,
18218        rep_vals: Vec<PerlValue>,
18219        line: usize,
18220    ) -> Result<PerlValue, FlowOrError> {
18221        let ctx = self.wantarray_kind;
18222        if let Some(r) = aref.as_array_ref() {
18223            let arr_len = r.read().len();
18224            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18225            let mut w = r.write();
18226            let removed: Vec<PerlValue> = w.drain(off..end).collect();
18227            for (i, v) in rep_vals.into_iter().enumerate() {
18228                w.insert(off + i, v);
18229            }
18230            return Ok(match ctx {
18231                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18232                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18233            });
18234        }
18235        if let Some(name) = aref.as_array_binding_name() {
18236            let arr_len = self.scope.array_len(&name);
18237            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18238            let arr = self
18239                .scope
18240                .get_array_mut(&name)
18241                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18242            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
18243            for (i, v) in rep_vals.into_iter().enumerate() {
18244                arr.insert(off + i, v);
18245            }
18246            return Ok(match ctx {
18247                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18248                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18249            });
18250        }
18251        if let Some(s) = aref.as_str() {
18252            if self.strict_refs {
18253                return Err(PerlError::runtime(
18254                    format!(
18255                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18256                        s
18257                    ),
18258                    line,
18259                )
18260                .into());
18261            }
18262            let arr_len = self.scope.array_len(&s);
18263            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18264            let arr = self
18265                .scope
18266                .get_array_mut(&s)
18267                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18268            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
18269            for (i, v) in rep_vals.into_iter().enumerate() {
18270                arr.insert(off + i, v);
18271            }
18272            return Ok(match ctx {
18273                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18274                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18275            });
18276        }
18277        Err(PerlError::runtime("splice argument is not an ARRAY reference", line).into())
18278    }
18279
18280    pub(crate) fn eval_splice_expr(
18281        &mut self,
18282        array: &Expr,
18283        offset: Option<&Expr>,
18284        length: Option<&Expr>,
18285        replacement: &[Expr],
18286        ctx: WantarrayCtx,
18287        line: usize,
18288    ) -> Result<PerlValue, FlowOrError> {
18289        if let Some(aref) = self.try_eval_array_deref_container(array)? {
18290            let offset_val = if let Some(o) = offset {
18291                self.eval_expr(o)?
18292            } else {
18293                PerlValue::integer(0)
18294            };
18295            let length_val = if let Some(l) = length {
18296                self.eval_expr(l)?
18297            } else {
18298                PerlValue::UNDEF
18299            };
18300            let mut rep_vals = Vec::new();
18301            for r in replacement {
18302                rep_vals.push(self.eval_expr(r)?);
18303            }
18304            let saved = self.wantarray_kind;
18305            self.wantarray_kind = ctx;
18306            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
18307            self.wantarray_kind = saved;
18308            return out;
18309        }
18310        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
18311        let arr_len = self.scope.array_len(&arr_name);
18312        let offset_val = if let Some(o) = offset {
18313            self.eval_expr(o)?
18314        } else {
18315            PerlValue::integer(0)
18316        };
18317        let length_val = if let Some(l) = length {
18318            self.eval_expr(l)?
18319        } else {
18320            PerlValue::UNDEF
18321        };
18322        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18323        let mut rep_vals = Vec::new();
18324        for r in replacement {
18325            rep_vals.push(self.eval_expr(r)?);
18326        }
18327        let arr = self
18328            .scope
18329            .get_array_mut(&arr_name)
18330            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18331        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
18332        for (i, v) in rep_vals.into_iter().enumerate() {
18333            arr.insert(off + i, v);
18334        }
18335        Ok(match ctx {
18336            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18337            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18338        })
18339    }
18340
18341    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
18342    pub(crate) fn keys_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
18343        if let Some(h) = val.as_hash_map() {
18344            Ok(PerlValue::array(
18345                h.keys().map(|k| PerlValue::string(k.clone())).collect(),
18346            ))
18347        } else if let Some(r) = val.as_hash_ref() {
18348            Ok(PerlValue::array(
18349                r.read()
18350                    .keys()
18351                    .map(|k| PerlValue::string(k.clone()))
18352                    .collect(),
18353            ))
18354        } else {
18355            Err(PerlError::runtime("keys requires hash", line).into())
18356        }
18357    }
18358
18359    pub(crate) fn eval_keys_expr(
18360        &mut self,
18361        expr: &Expr,
18362        line: usize,
18363    ) -> Result<PerlValue, FlowOrError> {
18364        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
18365        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
18366        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
18367        Self::keys_from_value(val, line)
18368    }
18369
18370    /// Result of `values EXPR` after `EXPR` has been evaluated.
18371    pub(crate) fn values_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
18372        if let Some(h) = val.as_hash_map() {
18373            Ok(PerlValue::array(h.values().cloned().collect()))
18374        } else if let Some(r) = val.as_hash_ref() {
18375            Ok(PerlValue::array(r.read().values().cloned().collect()))
18376        } else {
18377            Err(PerlError::runtime("values requires hash", line).into())
18378        }
18379    }
18380
18381    pub(crate) fn eval_values_expr(
18382        &mut self,
18383        expr: &Expr,
18384        line: usize,
18385    ) -> Result<PerlValue, FlowOrError> {
18386        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
18387        Self::values_from_value(val, line)
18388    }
18389
18390    pub(crate) fn eval_delete_operand(
18391        &mut self,
18392        expr: &Expr,
18393        line: usize,
18394    ) -> Result<PerlValue, FlowOrError> {
18395        match &expr.kind {
18396            ExprKind::HashElement { hash, key } => {
18397                let k = self.eval_expr(key)?.to_string();
18398                self.touch_env_hash(hash);
18399                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
18400                    let class = obj
18401                        .as_blessed_ref()
18402                        .map(|b| b.class.clone())
18403                        .unwrap_or_default();
18404                    let full = format!("{}::DELETE", class);
18405                    if let Some(sub) = self.subs.get(&full).cloned() {
18406                        return self.call_sub(
18407                            &sub,
18408                            vec![obj, PerlValue::string(k)],
18409                            WantarrayCtx::Scalar,
18410                            line,
18411                        );
18412                    }
18413                }
18414                self.scope
18415                    .delete_hash_element(hash, &k)
18416                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
18417            }
18418            ExprKind::ArrayElement { array, index } => {
18419                self.check_strict_array_var(array, line)?;
18420                let idx = self.eval_expr(index)?.to_int();
18421                let aname = self.stash_array_name_for_package(array);
18422                self.scope
18423                    .delete_array_element(&aname, idx)
18424                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
18425            }
18426            ExprKind::ArrowDeref {
18427                expr: inner,
18428                index,
18429                kind: DerefKind::Hash,
18430            } => {
18431                let k = self.eval_expr(index)?.to_string();
18432                let container = self.eval_expr(inner)?;
18433                self.delete_arrow_hash_element(container, &k, line)
18434                    .map_err(Into::into)
18435            }
18436            ExprKind::ArrowDeref {
18437                expr: inner,
18438                index,
18439                kind: DerefKind::Array,
18440            } => {
18441                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
18442                    return Err(PerlError::runtime(
18443                        "delete on array element needs scalar subscript",
18444                        line,
18445                    )
18446                    .into());
18447                }
18448                let container = self.eval_expr(inner)?;
18449                let idx = self.eval_expr(index)?.to_int();
18450                self.delete_arrow_array_element(container, idx, line)
18451                    .map_err(Into::into)
18452            }
18453            _ => Err(PerlError::runtime("delete requires hash or array element", line).into()),
18454        }
18455    }
18456
18457    pub(crate) fn eval_exists_operand(
18458        &mut self,
18459        expr: &Expr,
18460        line: usize,
18461    ) -> Result<PerlValue, FlowOrError> {
18462        match &expr.kind {
18463            ExprKind::HashElement { hash, key } => {
18464                let k = self.eval_expr(key)?.to_string();
18465                self.touch_env_hash(hash);
18466                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
18467                    let class = obj
18468                        .as_blessed_ref()
18469                        .map(|b| b.class.clone())
18470                        .unwrap_or_default();
18471                    let full = format!("{}::EXISTS", class);
18472                    if let Some(sub) = self.subs.get(&full).cloned() {
18473                        return self.call_sub(
18474                            &sub,
18475                            vec![obj, PerlValue::string(k)],
18476                            WantarrayCtx::Scalar,
18477                            line,
18478                        );
18479                    }
18480                }
18481                Ok(PerlValue::integer(
18482                    if self.scope.exists_hash_element(hash, &k) {
18483                        1
18484                    } else {
18485                        0
18486                    },
18487                ))
18488            }
18489            ExprKind::ArrayElement { array, index } => {
18490                self.check_strict_array_var(array, line)?;
18491                let idx = self.eval_expr(index)?.to_int();
18492                let aname = self.stash_array_name_for_package(array);
18493                Ok(PerlValue::integer(
18494                    if self.scope.exists_array_element(&aname, idx) {
18495                        1
18496                    } else {
18497                        0
18498                    },
18499                ))
18500            }
18501            ExprKind::ArrowDeref {
18502                expr: inner,
18503                index,
18504                kind: DerefKind::Hash,
18505            } => {
18506                let k = self.eval_expr(index)?.to_string();
18507                let container = self.eval_expr(inner)?;
18508                let yes = self.exists_arrow_hash_element(container, &k, line)?;
18509                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
18510            }
18511            ExprKind::ArrowDeref {
18512                expr: inner,
18513                index,
18514                kind: DerefKind::Array,
18515            } => {
18516                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
18517                    return Err(PerlError::runtime(
18518                        "exists on array element needs scalar subscript",
18519                        line,
18520                    )
18521                    .into());
18522                }
18523                let container = self.eval_expr(inner)?;
18524                let idx = self.eval_expr(index)?.to_int();
18525                let yes = self.exists_arrow_array_element(container, idx, line)?;
18526                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
18527            }
18528            _ => Err(PerlError::runtime("exists requires hash or array element", line).into()),
18529        }
18530    }
18531
18532    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
18533    ///
18534    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
18535    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
18536    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
18537    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
18538    /// the new path amortizes the handshake across the whole map.
18539    pub(crate) fn eval_pmap_remote(
18540        &mut self,
18541        cluster_pv: PerlValue,
18542        list_pv: PerlValue,
18543        show_progress: bool,
18544        block: &Block,
18545        flat_outputs: bool,
18546        line: usize,
18547    ) -> Result<PerlValue, FlowOrError> {
18548        let Some(cluster) = cluster_pv.as_remote_cluster() else {
18549            return Err(PerlError::runtime("pmap_on: expected cluster(...) value", line).into());
18550        };
18551        let items = list_pv.to_list();
18552        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18553        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
18554            return Err(PerlError::runtime(
18555                "pmap_on: mysync/atomic capture is not supported for remote workers",
18556                line,
18557            )
18558            .into());
18559        }
18560        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
18561            .map_err(|e| PerlError::runtime(e, line))?;
18562        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
18563        let block_src = crate::fmt::format_block(block);
18564        let item_jsons =
18565            crate::cluster::perl_items_to_json(&items).map_err(|e| PerlError::runtime(e, line))?;
18566
18567        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
18568        // synchronous from the caller's POV, so we drive the bar before/after the call.
18569        let pmap_progress = PmapProgress::new(show_progress, items.len());
18570        let result_values =
18571            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
18572                .map_err(|e| PerlError::runtime(format!("pmap_on remote: {e}"), line))?;
18573        for _ in 0..result_values.len() {
18574            pmap_progress.tick();
18575        }
18576        pmap_progress.finish();
18577
18578        if flat_outputs {
18579            let flattened: Vec<PerlValue> = result_values
18580                .into_iter()
18581                .flat_map(|v| v.map_flatten_outputs(true))
18582                .collect();
18583            Ok(PerlValue::array(flattened))
18584        } else {
18585            Ok(PerlValue::array(result_values))
18586        }
18587    }
18588
18589    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
18590    pub(crate) fn eval_par_lines_expr(
18591        &mut self,
18592        path: &Expr,
18593        callback: &Expr,
18594        progress: Option<&Expr>,
18595        line: usize,
18596    ) -> Result<PerlValue, FlowOrError> {
18597        let show_progress = progress
18598            .map(|p| self.eval_expr(p))
18599            .transpose()?
18600            .map(|v| v.is_true())
18601            .unwrap_or(false);
18602        let path_s = self.eval_expr(path)?.to_string();
18603        let cb_val = self.eval_expr(callback)?;
18604        let sub = if let Some(s) = cb_val.as_code_ref() {
18605            s
18606        } else {
18607            return Err(PerlError::runtime(
18608                "par_lines: second argument must be a code reference",
18609                line,
18610            )
18611            .into());
18612        };
18613        let subs = self.subs.clone();
18614        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18615        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
18616            FlowOrError::Error(PerlError::runtime(format!("par_lines: {}", e), line))
18617        })?;
18618        let mmap = unsafe {
18619            memmap2::Mmap::map(&file).map_err(|e| {
18620                FlowOrError::Error(PerlError::runtime(format!("par_lines: mmap: {}", e), line))
18621            })?
18622        };
18623        let data: &[u8] = &mmap;
18624        if data.is_empty() {
18625            return Ok(PerlValue::UNDEF);
18626        }
18627        let line_total = crate::par_lines::line_count_bytes(data);
18628        let pmap_progress = PmapProgress::new(show_progress, line_total);
18629        if self.num_threads == 0 {
18630            self.num_threads = rayon::current_num_threads();
18631        }
18632        let num_chunks = self.num_threads.saturating_mul(8).max(1);
18633        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
18634        chunks.into_par_iter().try_for_each(|(start, end)| {
18635            let slice = &data[start..end];
18636            let mut s = 0usize;
18637            while s < slice.len() {
18638                let e = slice[s..]
18639                    .iter()
18640                    .position(|&b| b == b'\n')
18641                    .map(|p| s + p)
18642                    .unwrap_or(slice.len());
18643                let line_bytes = &slice[s..e];
18644                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
18645                let mut local_interp = Interpreter::new();
18646                local_interp.subs = subs.clone();
18647                local_interp.scope.restore_capture(&scope_capture);
18648                local_interp
18649                    .scope
18650                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18651                local_interp.enable_parallel_guard();
18652                local_interp.scope.set_topic(PerlValue::string(line_str));
18653                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
18654                    Ok(_) => {}
18655                    Err(e) => return Err(e),
18656                }
18657                pmap_progress.tick();
18658                if e >= slice.len() {
18659                    break;
18660                }
18661                s = e + 1;
18662            }
18663            Ok(())
18664        })?;
18665        pmap_progress.finish();
18666        Ok(PerlValue::UNDEF)
18667    }
18668
18669    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
18670    pub(crate) fn eval_par_walk_expr(
18671        &mut self,
18672        path: &Expr,
18673        callback: &Expr,
18674        progress: Option<&Expr>,
18675        line: usize,
18676    ) -> Result<PerlValue, FlowOrError> {
18677        let show_progress = progress
18678            .map(|p| self.eval_expr(p))
18679            .transpose()?
18680            .map(|v| v.is_true())
18681            .unwrap_or(false);
18682        let path_val = self.eval_expr(path)?;
18683        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
18684            arr.into_iter()
18685                .map(|v| PathBuf::from(v.to_string()))
18686                .collect()
18687        } else {
18688            vec![PathBuf::from(path_val.to_string())]
18689        };
18690        let cb_val = self.eval_expr(callback)?;
18691        let sub = if let Some(s) = cb_val.as_code_ref() {
18692            s
18693        } else {
18694            return Err(PerlError::runtime(
18695                "par_walk: second argument must be a code reference",
18696                line,
18697            )
18698            .into());
18699        };
18700        let subs = self.subs.clone();
18701        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18702
18703        if show_progress {
18704            let paths = crate::par_walk::collect_paths(&roots);
18705            let pmap_progress = PmapProgress::new(true, paths.len());
18706            paths.into_par_iter().try_for_each(|p| {
18707                let s = p.to_string_lossy().into_owned();
18708                let mut local_interp = Interpreter::new();
18709                local_interp.subs = subs.clone();
18710                local_interp.scope.restore_capture(&scope_capture);
18711                local_interp
18712                    .scope
18713                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18714                local_interp.enable_parallel_guard();
18715                local_interp.scope.set_topic(PerlValue::string(s));
18716                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
18717                    Ok(_) => {}
18718                    Err(e) => return Err(e),
18719                }
18720                pmap_progress.tick();
18721                Ok(())
18722            })?;
18723            pmap_progress.finish();
18724        } else {
18725            for r in &roots {
18726                par_walk_recursive(
18727                    r.as_path(),
18728                    &sub,
18729                    &subs,
18730                    &scope_capture,
18731                    &atomic_arrays,
18732                    &atomic_hashes,
18733                    line,
18734                )?;
18735            }
18736        }
18737        Ok(PerlValue::UNDEF)
18738    }
18739
18740    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
18741    pub(crate) fn builtin_par_sed(
18742        &mut self,
18743        args: &[PerlValue],
18744        line: usize,
18745        has_progress: bool,
18746    ) -> PerlResult<PerlValue> {
18747        let show_progress = if has_progress {
18748            args.last().map(|v| v.is_true()).unwrap_or(false)
18749        } else {
18750            false
18751        };
18752        let slice = if has_progress {
18753            &args[..args.len().saturating_sub(1)]
18754        } else {
18755            args
18756        };
18757        if slice.len() < 3 {
18758            return Err(PerlError::runtime(
18759                "par_sed: need pattern, replacement, and at least one file path",
18760                line,
18761            ));
18762        }
18763        let pat_val = &slice[0];
18764        let repl = slice[1].to_string();
18765        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
18766
18767        let re = if let Some(rx) = pat_val.as_regex() {
18768            rx
18769        } else {
18770            let pattern = pat_val.to_string();
18771            match self.compile_regex(&pattern, "g", line) {
18772                Ok(r) => r,
18773                Err(FlowOrError::Error(e)) => return Err(e),
18774                Err(FlowOrError::Flow(f)) => {
18775                    return Err(PerlError::runtime(format!("par_sed: {:?}", f), line))
18776                }
18777            }
18778        };
18779
18780        let pmap = PmapProgress::new(show_progress, files.len());
18781        let touched = AtomicUsize::new(0);
18782        files.par_iter().try_for_each(|path| {
18783            let content = read_file_text_perl_compat(path)
18784                .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18785            let new_s = re.replace_all(&content, &repl);
18786            if new_s != content {
18787                std::fs::write(path, new_s.as_bytes())
18788                    .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18789                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
18790            }
18791            pmap.tick();
18792            Ok(())
18793        })?;
18794        pmap.finish();
18795        Ok(PerlValue::integer(
18796            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
18797        ))
18798    }
18799
18800    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
18801    pub(crate) fn eval_pwatch_expr(
18802        &mut self,
18803        path: &Expr,
18804        callback: &Expr,
18805        line: usize,
18806    ) -> Result<PerlValue, FlowOrError> {
18807        let pattern_s = self.eval_expr(path)?.to_string();
18808        let cb_val = self.eval_expr(callback)?;
18809        let sub = if let Some(s) = cb_val.as_code_ref() {
18810            s
18811        } else {
18812            return Err(PerlError::runtime(
18813                "pwatch: second argument must be a code reference",
18814                line,
18815            )
18816            .into());
18817        };
18818        let subs = self.subs.clone();
18819        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18820        crate::pwatch::run_pwatch(
18821            &pattern_s,
18822            sub,
18823            subs,
18824            scope_capture,
18825            atomic_arrays,
18826            atomic_hashes,
18827            line,
18828        )
18829        .map_err(FlowOrError::Error)
18830    }
18831
18832    /// Interpolate `$var` in s/// replacement strings, preserving numeric backrefs ($1, $2, etc.).
18833    fn interpolate_replacement_string(&self, replacement: &str) -> String {
18834        let mut out = String::with_capacity(replacement.len());
18835        let chars: Vec<char> = replacement.chars().collect();
18836        let mut i = 0;
18837        while i < chars.len() {
18838            if chars[i] == '\\' && i + 1 < chars.len() {
18839                out.push(chars[i]);
18840                out.push(chars[i + 1]);
18841                i += 2;
18842                continue;
18843            }
18844            if chars[i] == '$' && i + 1 < chars.len() {
18845                let start = i;
18846                i += 1;
18847                if chars[i].is_ascii_digit() {
18848                    out.push('$');
18849                    while i < chars.len() && chars[i].is_ascii_digit() {
18850                        out.push(chars[i]);
18851                        i += 1;
18852                    }
18853                    continue;
18854                }
18855                if chars[i] == '&' || chars[i] == '`' || chars[i] == '\'' {
18856                    out.push('$');
18857                    out.push(chars[i]);
18858                    i += 1;
18859                    continue;
18860                }
18861                if !chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{' {
18862                    out.push('$');
18863                    continue;
18864                }
18865                let mut name = String::new();
18866                if chars[i] == '{' {
18867                    i += 1;
18868                    while i < chars.len() && chars[i] != '}' {
18869                        name.push(chars[i]);
18870                        i += 1;
18871                    }
18872                    if i < chars.len() {
18873                        i += 1;
18874                    }
18875                } else {
18876                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18877                        name.push(chars[i]);
18878                        i += 1;
18879                    }
18880                }
18881                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
18882                    let val = self.scope.get_scalar(&name);
18883                    out.push_str(&val.to_string());
18884                } else if !name.is_empty() {
18885                    out.push_str(&replacement[start..i]);
18886                } else {
18887                    out.push('$');
18888                }
18889                continue;
18890            }
18891            out.push(chars[i]);
18892            i += 1;
18893        }
18894        out
18895    }
18896
18897    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
18898    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
18899        let mut out = String::with_capacity(pattern.len());
18900        let chars: Vec<char> = pattern.chars().collect();
18901        let mut i = 0;
18902        while i < chars.len() {
18903            if chars[i] == '\\' && i + 1 < chars.len() {
18904                // Preserve escape sequences (including \$ which is literal $)
18905                out.push(chars[i]);
18906                out.push(chars[i + 1]);
18907                i += 2;
18908                continue;
18909            }
18910            if chars[i] == '$' && i + 1 < chars.len() {
18911                i += 1;
18912                // `$` at end of pattern is an anchor, not a variable
18913                if i >= chars.len()
18914                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
18915                {
18916                    out.push('$');
18917                    continue;
18918                }
18919                let mut name = String::new();
18920                if chars[i] == '{' {
18921                    i += 1;
18922                    while i < chars.len() && chars[i] != '}' {
18923                        name.push(chars[i]);
18924                        i += 1;
18925                    }
18926                    if i < chars.len() {
18927                        i += 1;
18928                    } // skip }
18929                } else {
18930                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18931                        name.push(chars[i]);
18932                        i += 1;
18933                    }
18934                }
18935                if !name.is_empty() {
18936                    let val = self.scope.get_scalar(&name);
18937                    out.push_str(&val.to_string());
18938                } else {
18939                    out.push('$');
18940                }
18941                continue;
18942            }
18943            out.push(chars[i]);
18944            i += 1;
18945        }
18946        out
18947    }
18948
18949    pub(crate) fn compile_regex(
18950        &mut self,
18951        pattern: &str,
18952        flags: &str,
18953        line: usize,
18954    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
18955        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
18956        let pattern = if pattern.contains('$') || pattern.contains('@') {
18957            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
18958        } else {
18959            std::borrow::Cow::Borrowed(pattern)
18960        };
18961        let pattern = pattern.as_ref();
18962        // Fast path: same regex as last call (common in loops).
18963        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
18964        let multiline = self.multiline_match;
18965        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
18966            if lp == pattern && lf == flags && *lm == multiline {
18967                return Ok(lr.clone());
18968            }
18969        }
18970        // Slow path: HashMap lookup
18971        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
18972        if let Some(cached) = self.regex_cache.get(&key) {
18973            self.regex_last = Some((
18974                pattern.to_string(),
18975                flags.to_string(),
18976                multiline,
18977                cached.clone(),
18978            ));
18979            return Ok(cached.clone());
18980        }
18981        let expanded = expand_perl_regex_quotemeta(pattern);
18982        let expanded = expand_perl_regex_octal_escapes(&expanded);
18983        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
18984        let mut re_str = String::new();
18985        if flags.contains('i') {
18986            re_str.push_str("(?i)");
18987        }
18988        if flags.contains('s') {
18989            re_str.push_str("(?s)");
18990        }
18991        if flags.contains('m') {
18992            re_str.push_str("(?m)");
18993        }
18994        if flags.contains('x') {
18995            re_str.push_str("(?x)");
18996        }
18997        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
18998        if multiline {
18999            re_str.push_str("(?s)");
19000        }
19001        re_str.push_str(&expanded);
19002        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
19003            FlowOrError::Error(PerlError::runtime(
19004                format!("Invalid regex /{}/: {}", pattern, e),
19005                line,
19006            ))
19007        })?;
19008        let arc = re;
19009        self.regex_last = Some((
19010            pattern.to_string(),
19011            flags.to_string(),
19012            multiline,
19013            arc.clone(),
19014        ));
19015        self.regex_cache.insert(key, arc.clone());
19016        Ok(arc)
19017    }
19018
19019    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
19020    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
19021        if self.last_readline_handle.is_empty() {
19022            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
19023        }
19024        let n = *self
19025            .handle_line_numbers
19026            .get(&self.last_readline_handle)
19027            .unwrap_or(&0);
19028        if n <= 0 {
19029            return None;
19030        }
19031        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
19032        {
19033            return Some(("<>".to_string(), n));
19034        }
19035        if self.last_readline_handle == "STDIN" {
19036            return Some((self.last_stdin_die_bracket.clone(), n));
19037        }
19038        Some((format!("<{}>", self.last_readline_handle), n))
19039    }
19040
19041    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
19042    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
19043        let mut s = format!(" at {} line {}", self.file, source_line);
19044        if let Some((bracket, n)) = self.die_warn_io_annotation() {
19045            s.push_str(&format!(", {} line {}.", bracket, n));
19046        } else {
19047            s.push('.');
19048        }
19049        s
19050    }
19051
19052    /// Process a line in -n/-p mode via the VM.
19053    ///
19054    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
19055    /// file so `eof` with no arguments matches Perl behavior on that line.
19056    pub fn process_line(
19057        &mut self,
19058        line_str: &str,
19059        _program: &Program,
19060        is_last_input_line: bool,
19061    ) -> PerlResult<Option<String>> {
19062        let chunk = self
19063            .line_mode_chunk
19064            .as_ref()
19065            .expect("process_line called without compiled chunk — execute() must run first")
19066            .clone();
19067        crate::run_line_body(&chunk, self, line_str, is_last_input_line)
19068    }
19069}
19070
19071fn par_walk_invoke_entry(
19072    path: &Path,
19073    sub: &Arc<PerlSub>,
19074    subs: &HashMap<String, Arc<PerlSub>>,
19075    scope_capture: &[(String, PerlValue)],
19076    atomic_arrays: &[(String, crate::scope::AtomicArray)],
19077    atomic_hashes: &[(String, crate::scope::AtomicHash)],
19078    line: usize,
19079) -> Result<(), FlowOrError> {
19080    let s = path.to_string_lossy().into_owned();
19081    let mut local_interp = Interpreter::new();
19082    local_interp.subs = subs.clone();
19083    local_interp.scope.restore_capture(scope_capture);
19084    local_interp
19085        .scope
19086        .restore_atomics(atomic_arrays, atomic_hashes);
19087    local_interp.enable_parallel_guard();
19088    local_interp.scope.set_topic(PerlValue::string(s));
19089    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
19090    Ok(())
19091}
19092
19093fn par_walk_recursive(
19094    path: &Path,
19095    sub: &Arc<PerlSub>,
19096    subs: &HashMap<String, Arc<PerlSub>>,
19097    scope_capture: &[(String, PerlValue)],
19098    atomic_arrays: &[(String, crate::scope::AtomicArray)],
19099    atomic_hashes: &[(String, crate::scope::AtomicHash)],
19100    line: usize,
19101) -> Result<(), FlowOrError> {
19102    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
19103        return par_walk_invoke_entry(
19104            path,
19105            sub,
19106            subs,
19107            scope_capture,
19108            atomic_arrays,
19109            atomic_hashes,
19110            line,
19111        );
19112    }
19113    if !path.is_dir() {
19114        return Ok(());
19115    }
19116    par_walk_invoke_entry(
19117        path,
19118        sub,
19119        subs,
19120        scope_capture,
19121        atomic_arrays,
19122        atomic_hashes,
19123        line,
19124    )?;
19125    let read = match std::fs::read_dir(path) {
19126        Ok(r) => r,
19127        Err(_) => return Ok(()),
19128    };
19129    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
19130    entries.par_iter().try_for_each(|e| {
19131        par_walk_recursive(
19132            &e.path(),
19133            sub,
19134            subs,
19135            scope_capture,
19136            atomic_arrays,
19137            atomic_hashes,
19138            line,
19139        )
19140    })?;
19141    Ok(())
19142}
19143
19144/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
19145pub(crate) fn perl_sprintf_format_with<F>(
19146    fmt: &str,
19147    args: &[PerlValue],
19148    mut string_for_s: F,
19149) -> Result<String, FlowOrError>
19150where
19151    F: FnMut(&PerlValue) -> Result<String, FlowOrError>,
19152{
19153    let mut result = String::new();
19154    let mut arg_idx = 0;
19155    let chars: Vec<char> = fmt.chars().collect();
19156    let mut i = 0;
19157
19158    while i < chars.len() {
19159        if chars[i] == '%' {
19160            i += 1;
19161            if i >= chars.len() {
19162                break;
19163            }
19164            if chars[i] == '%' {
19165                result.push('%');
19166                i += 1;
19167                continue;
19168            }
19169
19170            // Parse format specifier
19171            let mut flags = String::new();
19172            while i < chars.len() && "-+ #0".contains(chars[i]) {
19173                flags.push(chars[i]);
19174                i += 1;
19175            }
19176            let mut width = String::new();
19177            while i < chars.len() && chars[i].is_ascii_digit() {
19178                width.push(chars[i]);
19179                i += 1;
19180            }
19181            let mut precision = String::new();
19182            if i < chars.len() && chars[i] == '.' {
19183                i += 1;
19184                while i < chars.len() && chars[i].is_ascii_digit() {
19185                    precision.push(chars[i]);
19186                    i += 1;
19187                }
19188            }
19189            if i >= chars.len() {
19190                break;
19191            }
19192            let spec = chars[i];
19193            i += 1;
19194
19195            let arg = args.get(arg_idx).cloned().unwrap_or(PerlValue::UNDEF);
19196            arg_idx += 1;
19197
19198            let w: usize = width.parse().unwrap_or(0);
19199            let p: usize = precision.parse().unwrap_or(6);
19200
19201            let zero_pad = flags.contains('0') && !flags.contains('-');
19202            let left_align = flags.contains('-');
19203            let formatted = match spec {
19204                'd' | 'i' => {
19205                    if zero_pad {
19206                        format!("{:0width$}", arg.to_int(), width = w)
19207                    } else if left_align {
19208                        format!("{:<width$}", arg.to_int(), width = w)
19209                    } else {
19210                        format!("{:width$}", arg.to_int(), width = w)
19211                    }
19212                }
19213                'u' => {
19214                    if zero_pad {
19215                        format!("{:0width$}", arg.to_int() as u64, width = w)
19216                    } else {
19217                        format!("{:width$}", arg.to_int() as u64, width = w)
19218                    }
19219                }
19220                'f' => format!("{:width$.prec$}", arg.to_number(), width = w, prec = p),
19221                'e' => format!("{:width$.prec$e}", arg.to_number(), width = w, prec = p),
19222                'g' => {
19223                    let n = arg.to_number();
19224                    if n.abs() >= 1e-4 && n.abs() < 1e15 {
19225                        format!("{:width$.prec$}", n, width = w, prec = p)
19226                    } else {
19227                        format!("{:width$.prec$e}", n, width = w, prec = p)
19228                    }
19229                }
19230                's' => {
19231                    let s = string_for_s(&arg)?;
19232                    if !precision.is_empty() {
19233                        let truncated: String = s.chars().take(p).collect();
19234                        if flags.contains('-') {
19235                            format!("{:<width$}", truncated, width = w)
19236                        } else {
19237                            format!("{:>width$}", truncated, width = w)
19238                        }
19239                    } else if flags.contains('-') {
19240                        format!("{:<width$}", s, width = w)
19241                    } else {
19242                        format!("{:>width$}", s, width = w)
19243                    }
19244                }
19245                'x' => {
19246                    let v = arg.to_int();
19247                    if zero_pad && w > 0 {
19248                        format!("{:0width$x}", v, width = w)
19249                    } else if left_align {
19250                        format!("{:<width$x}", v, width = w)
19251                    } else if w > 0 {
19252                        format!("{:width$x}", v, width = w)
19253                    } else {
19254                        format!("{:x}", v)
19255                    }
19256                }
19257                'X' => {
19258                    let v = arg.to_int();
19259                    if zero_pad && w > 0 {
19260                        format!("{:0width$X}", v, width = w)
19261                    } else if left_align {
19262                        format!("{:<width$X}", v, width = w)
19263                    } else if w > 0 {
19264                        format!("{:width$X}", v, width = w)
19265                    } else {
19266                        format!("{:X}", v)
19267                    }
19268                }
19269                'o' => {
19270                    let v = arg.to_int();
19271                    if zero_pad && w > 0 {
19272                        format!("{:0width$o}", v, width = w)
19273                    } else if left_align {
19274                        format!("{:<width$o}", v, width = w)
19275                    } else if w > 0 {
19276                        format!("{:width$o}", v, width = w)
19277                    } else {
19278                        format!("{:o}", v)
19279                    }
19280                }
19281                'b' => {
19282                    let v = arg.to_int();
19283                    if zero_pad && w > 0 {
19284                        format!("{:0width$b}", v, width = w)
19285                    } else if left_align {
19286                        format!("{:<width$b}", v, width = w)
19287                    } else if w > 0 {
19288                        format!("{:width$b}", v, width = w)
19289                    } else {
19290                        format!("{:b}", v)
19291                    }
19292                }
19293                'c' => char::from_u32(arg.to_int() as u32)
19294                    .map(|c| c.to_string())
19295                    .unwrap_or_default(),
19296                _ => arg.to_string(),
19297            };
19298
19299            result.push_str(&formatted);
19300        } else {
19301            result.push(chars[i]);
19302            i += 1;
19303        }
19304    }
19305    Ok(result)
19306}
19307
19308#[cfg(test)]
19309mod regex_expand_tests {
19310    use super::Interpreter;
19311
19312    #[test]
19313    fn compile_regex_quotemeta_qe_matches_literal() {
19314        let mut i = Interpreter::new();
19315        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
19316        assert!(re.is_match("a.c"));
19317        assert!(!re.is_match("abc"));
19318    }
19319
19320    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
19321    /// stay literal (not rewritten to `(?:\n?\z)`).
19322    #[test]
19323    fn compile_regex_char_class_leading_close_bracket_is_literal() {
19324        let mut i = Interpreter::new();
19325        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
19326        assert!(re.is_match("$"));
19327        assert!(re.is_match("]"));
19328        assert!(!re.is_match("x"));
19329    }
19330}
19331
19332#[cfg(test)]
19333mod special_scalar_name_tests {
19334    use super::Interpreter;
19335
19336    #[test]
19337    fn special_scalar_name_for_get_matches_magic_globals() {
19338        assert!(Interpreter::is_special_scalar_name_for_get("0"));
19339        assert!(Interpreter::is_special_scalar_name_for_get("!"));
19340        assert!(Interpreter::is_special_scalar_name_for_get("^W"));
19341        assert!(Interpreter::is_special_scalar_name_for_get("^O"));
19342        assert!(Interpreter::is_special_scalar_name_for_get("^MATCH"));
19343        assert!(Interpreter::is_special_scalar_name_for_get("<"));
19344        assert!(Interpreter::is_special_scalar_name_for_get("?"));
19345        assert!(Interpreter::is_special_scalar_name_for_get("|"));
19346        assert!(Interpreter::is_special_scalar_name_for_get("^UNICODE"));
19347        assert!(Interpreter::is_special_scalar_name_for_get("\""));
19348        assert!(!Interpreter::is_special_scalar_name_for_get("foo"));
19349        assert!(!Interpreter::is_special_scalar_name_for_get("plainvar"));
19350    }
19351
19352    #[test]
19353    fn special_scalar_name_for_set_matches_set_special_var_arms() {
19354        assert!(Interpreter::is_special_scalar_name_for_set("0"));
19355        assert!(Interpreter::is_special_scalar_name_for_set("^D"));
19356        assert!(Interpreter::is_special_scalar_name_for_set("^H"));
19357        assert!(Interpreter::is_special_scalar_name_for_set("^WARNING_BITS"));
19358        assert!(Interpreter::is_special_scalar_name_for_set("ARGV"));
19359        assert!(Interpreter::is_special_scalar_name_for_set("|"));
19360        assert!(Interpreter::is_special_scalar_name_for_set("?"));
19361        assert!(Interpreter::is_special_scalar_name_for_set("^UNICODE"));
19362        assert!(Interpreter::is_special_scalar_name_for_set("."));
19363        assert!(!Interpreter::is_special_scalar_name_for_set("foo"));
19364        assert!(!Interpreter::is_special_scalar_name_for_set("__PACKAGE__"));
19365    }
19366
19367    #[test]
19368    fn caret_and_id_specials_roundtrip_get() {
19369        let i = Interpreter::new();
19370        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
19371        assert_eq!(
19372            i.get_special_var("^V").to_string(),
19373            format!("v{}", env!("CARGO_PKG_VERSION"))
19374        );
19375        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
19376        assert!(i.get_special_var("^T").to_int() >= 0);
19377        #[cfg(unix)]
19378        {
19379            assert!(i.get_special_var("<").to_int() >= 0);
19380        }
19381    }
19382
19383    #[test]
19384    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
19385        let mut i = Interpreter::new();
19386        i.last_readline_handle.clear();
19387        i.line_number = 3;
19388        i.prepare_flip_flop_vm_slots(1);
19389        assert_eq!(
19390            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
19391            1
19392        );
19393        assert!(i.flip_flop_active[0]);
19394        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
19395        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
19396        assert_eq!(
19397            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
19398            1
19399        );
19400        assert!(i.flip_flop_active[0]);
19401    }
19402
19403    #[test]
19404    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
19405        let mut i = Interpreter::new();
19406        i.last_readline_handle.clear();
19407        i.line_number = 2;
19408        i.prepare_flip_flop_vm_slots(1);
19409        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
19410        assert!(i.flip_flop_active[0]);
19411        i.line_number = 3;
19412        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
19413        assert!(!i.flip_flop_active[0]);
19414        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
19415    }
19416}