Skip to main content

stryke/
vm_helper.rs

1use std::cell::Cell;
2use std::cmp::Ordering;
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::fs::File;
5use std::io::{self, BufRead, BufReader, Cursor, Read, Write as IoWrite};
6#[cfg(unix)]
7use std::os::unix::process::ExitStatusExt;
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::atomic::AtomicUsize;
11use std::sync::Arc;
12use std::sync::{Barrier, OnceLock};
13use std::time::{Duration, Instant};
14
15use indexmap::IndexMap;
16use parking_lot::{Mutex, RwLock};
17use rand::rngs::StdRng;
18use rand::{Rng, SeedableRng};
19use rayon::prelude::*;
20
21use caseless::default_case_fold_str;
22
23use crate::ast::*;
24use crate::builtins::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 = VMHelper::new();
113    local_interp.subs = subs.clone();
114    local_interp.scope.restore_capture(scope_capture);
115    local_interp.enable_parallel_guard();
116    local_interp
117        .scope
118        .declare_array("_", vec![a.clone(), b.clone()]);
119    local_interp.scope.set_sort_pair(a, b);
120    match local_interp.exec_block(block) {
121        Ok(val) => val,
122        Err(_) => PerlValue::UNDEF,
123    }
124}
125
126/// Seed each parallel chunk from `init` without sharing mutable hashref storage (plain `clone` on
127/// `HashRef` reuses the same `Arc<RwLock<…>>`).
128pub(crate) fn preduce_init_fold_identity(init: &PerlValue) -> PerlValue {
129    if let Some(m) = init.as_hash_map() {
130        return PerlValue::hash(m.clone());
131    }
132    if let Some(r) = init.as_hash_ref() {
133        return PerlValue::hash_ref(Arc::new(RwLock::new(r.read().clone())));
134    }
135    init.clone()
136}
137
138pub(crate) fn fold_preduce_init_step(
139    subs: &HashMap<String, Arc<PerlSub>>,
140    scope_capture: &[(String, PerlValue)],
141    block: &Block,
142    acc: PerlValue,
143    item: PerlValue,
144) -> PerlValue {
145    let mut local_interp = VMHelper::new();
146    local_interp.subs = subs.clone();
147    local_interp.scope.restore_capture(scope_capture);
148    local_interp.enable_parallel_guard();
149    local_interp
150        .scope
151        .declare_array("_", vec![acc.clone(), item.clone()]);
152    local_interp.scope.set_sort_pair(acc, item);
153    match local_interp.exec_block(block) {
154        Ok(val) => val,
155        Err(_) => PerlValue::UNDEF,
156    }
157}
158
159/// `use feature 'say'`
160pub const FEAT_SAY: u64 = 1 << 0;
161/// `use feature 'state'`
162pub const FEAT_STATE: u64 = 1 << 1;
163/// `use feature 'switch'` (given/when when fully wired)
164pub const FEAT_SWITCH: u64 = 1 << 2;
165/// `use feature 'unicode_strings'`
166pub const FEAT_UNICODE_STRINGS: u64 = 1 << 3;
167
168/// Flow control signals propagated via Result.
169#[derive(Debug)]
170pub(crate) enum Flow {
171    Return(PerlValue),
172    Last(Option<String>),
173    Next(Option<String>),
174    Redo(Option<String>),
175    Yield(PerlValue),
176    /// `goto &sub` — tail-call: replace current sub with the named one, keeping @_.
177    GotoSub(String),
178}
179
180pub(crate) type ExecResult = Result<PerlValue, FlowOrError>;
181
182#[derive(Debug)]
183pub(crate) enum FlowOrError {
184    Flow(Flow),
185    Error(PerlError),
186}
187
188impl From<PerlError> for FlowOrError {
189    fn from(e: PerlError) -> Self {
190        FlowOrError::Error(e)
191    }
192}
193
194impl From<Flow> for FlowOrError {
195    fn from(f: Flow) -> Self {
196        FlowOrError::Flow(f)
197    }
198}
199
200/// Bindings introduced by a successful algebraic [`MatchPattern`] (scalar vs array).
201enum PatternBinding {
202    Scalar(String, PerlValue),
203    Array(String, Vec<PerlValue>),
204}
205
206/// Perl `$]` — numeric language level (`5 + minor/1000 + patch/1_000_000`).
207/// Emulated Perl 5.x level (not the `stryke` crate semver).
208pub fn perl_bracket_version() -> f64 {
209    const PERL_EMUL_MINOR: u32 = 38;
210    const PERL_EMUL_PATCH: u32 = 0;
211    5.0 + (PERL_EMUL_MINOR as f64) / 1000.0 + (PERL_EMUL_PATCH as f64) / 1_000_000.0
212}
213
214/// Cheap seed for [`StdRng`] at startup (avoids `getentropy` / blocking sources).
215#[inline]
216fn fast_rng_seed() -> u64 {
217    let local: u8 = 0;
218    let addr = &local as *const u8 as u64;
219    (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ addr
220}
221
222/// `$^X` — cache `current_exe()` once per process (tiny win on repeated `Interpreter::new`).
223fn cached_executable_path() -> String {
224    static CACHED: OnceLock<String> = OnceLock::new();
225    CACHED
226        .get_or_init(|| {
227            std::env::current_exe()
228                .map(|p| p.to_string_lossy().into_owned())
229                .unwrap_or_else(|_| "stryke".to_string())
230        })
231        .clone()
232}
233
234fn build_term_hash() -> IndexMap<String, PerlValue> {
235    let mut m = IndexMap::new();
236    m.insert(
237        "TERM".into(),
238        PerlValue::string(std::env::var("TERM").unwrap_or_default()),
239    );
240    m.insert(
241        "COLORTERM".into(),
242        PerlValue::string(std::env::var("COLORTERM").unwrap_or_default()),
243    );
244
245    let (rows, cols) = term_size();
246    m.insert("rows".into(), PerlValue::integer(rows));
247    m.insert("cols".into(), PerlValue::integer(cols));
248
249    #[cfg(unix)]
250    let is_tty = unsafe { libc::isatty(1) != 0 };
251    #[cfg(not(unix))]
252    let is_tty = false;
253    m.insert(
254        "is_tty".into(),
255        PerlValue::integer(if is_tty { 1 } else { 0 }),
256    );
257
258    m
259}
260
261fn term_size() -> (i64, i64) {
262    #[cfg(unix)]
263    {
264        unsafe {
265            let mut ws: libc::winsize = std::mem::zeroed();
266            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 {
267                return (ws.ws_row as i64, ws.ws_col as i64);
268            }
269        }
270    }
271    let rows = std::env::var("LINES")
272        .ok()
273        .and_then(|s| s.parse().ok())
274        .unwrap_or(24);
275    let cols = std::env::var("COLUMNS")
276        .ok()
277        .and_then(|s| s.parse().ok())
278        .unwrap_or(80);
279    (rows, cols)
280}
281
282#[cfg(unix)]
283fn build_uname_hash() -> IndexMap<String, PerlValue> {
284    fn uts_field(slice: &[libc::c_char]) -> String {
285        let n = slice.iter().take_while(|&&c| c != 0).count();
286        let bytes: Vec<u8> = slice[..n].iter().map(|&c| c as u8).collect();
287        String::from_utf8_lossy(&bytes).into_owned()
288    }
289    let mut m = IndexMap::new();
290    let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
291    if unsafe { libc::uname(&mut uts) } == 0 {
292        m.insert(
293            "sysname".into(),
294            PerlValue::string(uts_field(uts.sysname.as_slice())),
295        );
296        m.insert(
297            "nodename".into(),
298            PerlValue::string(uts_field(uts.nodename.as_slice())),
299        );
300        m.insert(
301            "release".into(),
302            PerlValue::string(uts_field(uts.release.as_slice())),
303        );
304        m.insert(
305            "version".into(),
306            PerlValue::string(uts_field(uts.version.as_slice())),
307        );
308        m.insert(
309            "machine".into(),
310            PerlValue::string(uts_field(uts.machine.as_slice())),
311        );
312    }
313    m
314}
315
316#[cfg(unix)]
317fn build_limits_hash() -> IndexMap<String, PerlValue> {
318    use libc::{getrlimit, rlimit, RLIM_INFINITY};
319    #[cfg(target_os = "linux")]
320    type RlimitResource = libc::__rlimit_resource_t;
321    #[cfg(not(target_os = "linux"))]
322    type RlimitResource = libc::c_int;
323    fn get_limit(resource: RlimitResource) -> (i64, i64) {
324        let mut rlim = rlimit {
325            rlim_cur: 0,
326            rlim_max: 0,
327        };
328        if unsafe { getrlimit(resource, &mut rlim) } == 0 {
329            let cur = if rlim.rlim_cur == RLIM_INFINITY {
330                -1
331            } else {
332                rlim.rlim_cur as i64
333            };
334            let max = if rlim.rlim_max == RLIM_INFINITY {
335                -1
336            } else {
337                rlim.rlim_max as i64
338            };
339            (cur, max)
340        } else {
341            (-1, -1)
342        }
343    }
344    let mut m = IndexMap::new();
345    let (cur, max) = get_limit(libc::RLIMIT_NOFILE);
346    m.insert("nofile".into(), PerlValue::integer(cur));
347    m.insert("nofile_max".into(), PerlValue::integer(max));
348    let (cur, max) = get_limit(libc::RLIMIT_STACK);
349    m.insert("stack".into(), PerlValue::integer(cur));
350    m.insert("stack_max".into(), PerlValue::integer(max));
351    let (cur, max) = get_limit(libc::RLIMIT_AS);
352    m.insert("as".into(), PerlValue::integer(cur));
353    m.insert("as_max".into(), PerlValue::integer(max));
354    let (cur, max) = get_limit(libc::RLIMIT_DATA);
355    m.insert("data".into(), PerlValue::integer(cur));
356    m.insert("data_max".into(), PerlValue::integer(max));
357    let (cur, max) = get_limit(libc::RLIMIT_FSIZE);
358    m.insert("fsize".into(), PerlValue::integer(cur));
359    m.insert("fsize_max".into(), PerlValue::integer(max));
360    let (cur, max) = get_limit(libc::RLIMIT_CORE);
361    m.insert("core".into(), PerlValue::integer(cur));
362    m.insert("core_max".into(), PerlValue::integer(max));
363    let (cur, max) = get_limit(libc::RLIMIT_CPU);
364    m.insert("cpu".into(), PerlValue::integer(cur));
365    m.insert("cpu_max".into(), PerlValue::integer(max));
366    let (cur, max) = get_limit(libc::RLIMIT_NPROC);
367    m.insert("nproc".into(), PerlValue::integer(cur));
368    m.insert("nproc_max".into(), PerlValue::integer(max));
369    #[cfg(target_os = "linux")]
370    {
371        let (cur, max) = get_limit(libc::RLIMIT_MEMLOCK);
372        m.insert("memlock".into(), PerlValue::integer(cur));
373        m.insert("memlock_max".into(), PerlValue::integer(max));
374    }
375    m
376}
377
378/// Context of the **current** subroutine call (`wantarray`).
379#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
380pub(crate) enum WantarrayCtx {
381    #[default]
382    Scalar,
383    List,
384    Void,
385}
386
387impl WantarrayCtx {
388    #[inline]
389    pub(crate) fn from_byte(b: u8) -> Self {
390        match b {
391            1 => Self::List,
392            2 => Self::Void,
393            _ => Self::Scalar,
394        }
395    }
396
397    #[inline]
398    pub(crate) fn as_byte(self) -> u8 {
399        match self {
400            Self::Scalar => 0,
401            Self::List => 1,
402            Self::Void => 2,
403        }
404    }
405}
406
407/// Minimum log level filter for `log_*` / `log_json` (trace = most verbose).
408#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
409pub(crate) enum LogLevelFilter {
410    Trace,
411    Debug,
412    Info,
413    Warn,
414    Error,
415}
416
417impl LogLevelFilter {
418    pub(crate) fn parse(s: &str) -> Option<Self> {
419        match s.trim().to_ascii_lowercase().as_str() {
420            "trace" => Some(Self::Trace),
421            "debug" => Some(Self::Debug),
422            "info" => Some(Self::Info),
423            "warn" | "warning" => Some(Self::Warn),
424            "error" => Some(Self::Error),
425            _ => None,
426        }
427    }
428
429    pub(crate) fn as_str(self) -> &'static str {
430        match self {
431            Self::Trace => "trace",
432            Self::Debug => "debug",
433            Self::Info => "info",
434            Self::Warn => "warn",
435            Self::Error => "error",
436        }
437    }
438}
439
440/// True when `@$aref->[IX]` / `IX` needs **list** context on the RHS of `=` (multi-slot slice).
441fn arrow_deref_array_assign_rhs_list_ctx(index: &Expr) -> bool {
442    match &index.kind {
443        ExprKind::Range { .. } | ExprKind::SliceRange { .. } => true,
444        ExprKind::QW(ws) => ws.len() > 1,
445        ExprKind::List(el) => {
446            if el.len() > 1 {
447                true
448            } else if el.len() == 1 {
449                arrow_deref_array_assign_rhs_list_ctx(&el[0])
450            } else {
451                false
452            }
453        }
454        _ => false,
455    }
456}
457
458/// Wantarray for the RHS of a plain `=` assignment — must match [`crate::compiler::Compiler`] lowering
459/// so `<>` / `readline` list-slurp matches Perl for `@a = <>` (not only `my`/`our`/`local` initializers).
460pub(crate) fn assign_rhs_wantarray(target: &Expr) -> WantarrayCtx {
461    match &target.kind {
462        ExprKind::ArrayVar(_) | ExprKind::HashVar(_) => WantarrayCtx::List,
463        ExprKind::ScalarVar(_) | ExprKind::ArrayElement { .. } | ExprKind::HashElement { .. } => {
464            WantarrayCtx::Scalar
465        }
466        ExprKind::Deref { kind, .. } => match kind {
467            Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
468            Sigil::Array | Sigil::Hash => WantarrayCtx::List,
469        },
470        ExprKind::ArrowDeref {
471            index,
472            kind: DerefKind::Array,
473            ..
474        } => {
475            if arrow_deref_array_assign_rhs_list_ctx(index) {
476                WantarrayCtx::List
477            } else {
478                WantarrayCtx::Scalar
479            }
480        }
481        ExprKind::ArrowDeref {
482            kind: DerefKind::Hash,
483            ..
484        }
485        | ExprKind::ArrowDeref {
486            kind: DerefKind::Call,
487            ..
488        } => WantarrayCtx::Scalar,
489        ExprKind::HashSliceDeref { .. }
490        | ExprKind::HashSlice { .. }
491        | ExprKind::HashKvSlice { .. } => WantarrayCtx::List,
492        ExprKind::ArraySlice { indices, .. } => {
493            if indices.len() > 1 {
494                WantarrayCtx::List
495            } else if indices.len() == 1 {
496                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
497                    WantarrayCtx::List
498                } else {
499                    WantarrayCtx::Scalar
500                }
501            } else {
502                WantarrayCtx::Scalar
503            }
504        }
505        ExprKind::AnonymousListSlice { indices, .. } => {
506            if indices.len() > 1 {
507                WantarrayCtx::List
508            } else if indices.len() == 1 {
509                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
510                    WantarrayCtx::List
511                } else {
512                    WantarrayCtx::Scalar
513                }
514            } else {
515                WantarrayCtx::Scalar
516            }
517        }
518        ExprKind::Typeglob(_) | ExprKind::TypeglobExpr(_) => WantarrayCtx::Scalar,
519        ExprKind::List(_) => WantarrayCtx::List,
520        _ => WantarrayCtx::Scalar,
521    }
522}
523
524/// Memoized inputs + result for a non-`g` `regex_match_execute` call. Populated on every
525/// successful match and consulted at the top of the next call; on exact-match (same pattern,
526/// flags, multiline, and haystack content) we skip regex execution + capture-var scope population
527/// entirely, replaying the stored `PerlValue` result. See [`VMHelper::regex_match_memo`].
528#[derive(Clone)]
529pub(crate) struct RegexMatchMemo {
530    pub pattern: String,
531    pub flags: String,
532    pub multiline: bool,
533    pub haystack: String,
534    pub result: PerlValue,
535}
536
537/// State for scalar `..` / `...` (key: `Expr` address).
538#[derive(Clone, Copy, Default)]
539struct FlipFlopTreeState {
540    active: bool,
541    /// Exclusive `...`: `$.` line where the left bound matched — right is only tested when `$.` is
542    /// strictly greater (Perl: do not test the right operand until the next evaluation; for numeric
543    /// `$.` that defers past the left-match line, including multiple evals on that line).
544    exclusive_left_line: Option<i64>,
545}
546
547/// `BufReader` / `print` / `sysread` / `tell` on the same handle share this [`File`] cursor.
548#[derive(Clone)]
549pub(crate) struct IoSharedFile(pub Arc<Mutex<File>>);
550
551impl Read for IoSharedFile {
552    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
553        self.0.lock().read(buf)
554    }
555}
556
557pub(crate) struct IoSharedFileWrite(pub Arc<Mutex<File>>);
558
559impl IoWrite for IoSharedFileWrite {
560    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
561        self.0.lock().write(buf)
562    }
563
564    fn flush(&mut self) -> io::Result<()> {
565        self.0.lock().flush()
566    }
567}
568
569/// There is no Tree walking Interpreter, this is Just a Virtual Machine helper struct
570pub struct VMHelper {
571    pub scope: Scope,
572    pub(crate) subs: HashMap<String, Arc<PerlSub>>,
573    /// AOP advice registry — populated by `Op::RegisterAdvice` from `before|after|around` decls.
574    pub(crate) intercepts: Vec<crate::aop::Intercept>,
575    /// Auto-incremented for the next registered intercept id (1-based; matches zshrs).
576    pub(crate) next_intercept_id: u32,
577    /// Stack of active around-advice contexts; `proceed()` reads the top frame.
578    pub(crate) intercept_ctx_stack: Vec<crate::aop::InterceptCtx>,
579    /// Re-entrancy guard: while running advice for a name, calling that same name from inside
580    /// the body skips advice and runs the original directly. Prevents infinite recursion when
581    /// an advice body uses the same sub it advises.
582    pub(crate) intercept_active_names: Vec<String>,
583    pub(crate) file: String,
584    /// File handles: name → writer
585    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
586    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
587    /// Output separator ($,)
588    pub ofs: String,
589    /// Output record separator ($\)
590    pub ors: String,
591    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
592    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
593    pub irs: Option<String>,
594    /// $! — last OS error
595    pub errno: String,
596    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
597    pub errno_code: i32,
598    /// $@ — last eval error (string)
599    pub eval_error: String,
600    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
601    pub eval_error_code: i32,
602    /// When `die` is called with a ref argument, the ref value is preserved here.
603    pub eval_error_value: Option<PerlValue>,
604    /// @ARGV
605    pub argv: Vec<String>,
606    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
607    pub env: IndexMap<String, PerlValue>,
608    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
609    pub env_materialized: bool,
610    /// $0
611    pub program_name: String,
612    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle)
613    pub line_number: i64,
614    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
615    pub last_readline_handle: String,
616    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
617    pub(crate) last_stdin_die_bracket: String,
618    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
619    pub handle_line_numbers: HashMap<String, i64>,
620    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
621    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
622    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
623    pub(crate) flip_flop_active: Vec<bool>,
624    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
625    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
626    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
627    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
628    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
629    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
630    pub(crate) flip_flop_sequence: Vec<i64>,
631    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
632    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
633    /// range on one line return the same sequence number).
634    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
635    /// Scalar `..` / `...` flip-flop state (key: `Expr` address).
636    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
637    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
638    pub sigint_pending_caret: Cell<bool>,
639    /// Auto-split mode (-a)
640    pub auto_split: bool,
641    /// Field separator for -F
642    pub field_separator: Option<String>,
643    /// BEGIN blocks
644    begin_blocks: Vec<Block>,
645    /// `UNITCHECK` blocks (LIFO at run)
646    unit_check_blocks: Vec<Block>,
647    /// `CHECK` blocks (LIFO at run)
648    check_blocks: Vec<Block>,
649    /// `INIT` blocks (FIFO at run)
650    init_blocks: Vec<Block>,
651    /// END blocks
652    end_blocks: Vec<Block>,
653    /// -w warnings / `use warnings` / `$^W`
654    pub warnings: bool,
655    /// Output autoflush (`$|`).
656    pub output_autoflush: bool,
657    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
658    pub default_print_handle: String,
659    /// Suppress stdout output (fan workers with progress bars).
660    pub suppress_stdout: bool,
661    /// Per-instance test counters for `assert_*` / `test_run` / `test_skip` (stryke
662    /// `.stk` test framework). Atomics so the immutable-ref builtin signature
663    /// (`fn(&VMHelper, ...)`) can mutate without changing every call site. Replaces
664    /// the previous `AtomicUsize` process-globals which leaked counts across runs
665    /// in a single process — embedders running multiple `.stk` programs in one
666    /// `VMHelper` would see the previous run's counts contaminate the next.
667    pub test_pass_count: std::sync::atomic::AtomicUsize,
668    pub test_fail_count: std::sync::atomic::AtomicUsize,
669    pub test_skip_count: std::sync::atomic::AtomicUsize,
670    /// Set to `true` by `test_run` when any assertion failed during the run.
671    /// CLI driver (`main.rs`) reads this after `execute` returns and exits with
672    /// code 1. Replaces the previous in-VM `std::process::exit(1)` which made
673    /// embedding (running a `.stk` program from a Rust harness) impossible —
674    /// any failing test would kill the host process.
675    pub test_run_failed: std::sync::atomic::AtomicBool,
676    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
677    pub child_exit_status: i64,
678    /// Last successful match (`$&`, `${^MATCH}`).
679    pub last_match: String,
680    /// Before match (`` $` ``, `${^PREMATCH}`).
681    pub prematch: String,
682    /// After match (`$'`, `${^POSTMATCH}`).
683    pub postmatch: String,
684    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
685    pub last_paren_match: String,
686    /// List separator for array stringification in concatenation / interpolation (`$"`).
687    pub list_separator: String,
688    /// Script start time (`$^T`) — seconds since Unix epoch.
689    pub script_start_time: i64,
690    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
691    pub compile_hints: i64,
692    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
693    pub warning_bits: i64,
694    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
695    pub global_phase: String,
696    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
697    pub subscript_sep: String,
698    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
699    /// The `stryke` driver sets this from `-i` / `-i.ext`.
700    pub inplace_edit: String,
701    /// `$^D` — debugging flags (integer; mostly ignored).
702    pub debug_flags: i64,
703    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
704    pub perl_debug_flags: i64,
705    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
706    pub eval_nesting: u32,
707    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
708    pub argv_current_file: String,
709    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
710    pub(crate) diamond_next_idx: usize,
711    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
712    pub(crate) diamond_reader: Option<BufReader<File>>,
713    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
714    pub strict_refs: bool,
715    pub strict_subs: bool,
716    pub strict_vars: bool,
717    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
718    pub utf8_pragma: bool,
719    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
720    pub open_pragma_utf8: bool,
721    /// `use feature` — bit flags (`FEAT_*`).
722    pub feature_bits: u64,
723    /// Number of parallel threads
724    pub num_threads: usize,
725    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
726    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
727    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
728    /// Third flag: `$*` multiline (prepends `(?s)` when true).
729    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
730    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
731    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
732    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
733    /// scope population entirely on cache hit.
734    ///
735    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
736    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
737    /// capture-var side-effect replay is forced on the next hit.
738    regex_match_memo: Option<RegexMatchMemo>,
739    /// False when the user (or some non-regex code path) has written to one of the capture
740    /// variables since the last `apply_regex_captures` call. The memoized match result is still
741    /// valid, but the scope side effects need to be reapplied on the next hit.
742    regex_capture_scope_fresh: bool,
743    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
744    pub(crate) regex_pos: HashMap<String, Option<usize>>,
745    /// Persistent storage for `state` variables, keyed by "line:name".
746    pub(crate) state_vars: HashMap<String, PerlValue>,
747    /// Per-frame tracking of state variable bindings: (var_name, state_key).
748    pub(crate) state_bindings_stack: Vec<Vec<(String, String)>>,
749    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
750    pub(crate) rand_rng: StdRng,
751    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
752    pub(crate) dir_handles: HashMap<String, DirHandleState>,
753    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
754    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
755    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
756    pub(crate) pipe_children: HashMap<String, Child>,
757    /// Sockets from `socket` / `accept` / `connect`.
758    pub(crate) socket_handles: HashMap<String, PerlSocket>,
759    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
760    pub(crate) wantarray_kind: WantarrayCtx,
761    /// `struct Name { ... }` definitions (merged from VM chunks).
762    pub struct_defs: HashMap<String, Arc<StructDef>>,
763    /// `enum Name { ... }` definitions (merged from VM chunks).
764    pub enum_defs: HashMap<String, Arc<EnumDef>>,
765    /// `class Name extends ... impl ... { ... }` definitions.
766    pub class_defs: HashMap<String, Arc<ClassDef>>,
767    /// `trait Name { ... }` definitions.
768    pub trait_defs: HashMap<String, Arc<TraitDef>>,
769    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
770    /// call/return (JIT disabled); per-statement lines and subs.
771    pub profiler: Option<Profiler>,
772    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
773    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
774    /// Virtual modules: path → source (for AOT bundles). Checked before filesystem in `require`.
775    pub(crate) virtual_modules: HashMap<String, String>,
776    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
777    pub(crate) tied_hashes: HashMap<String, PerlValue>,
778    /// `tie $name` — TIESCALAR object for FETCH/STORE.
779    pub(crate) tied_scalars: HashMap<String, PerlValue>,
780    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
781    pub(crate) tied_arrays: HashMap<String, PerlValue>,
782    /// `use overload` — class → Perl overload key → short method name in that package.
783    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
784    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
785    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
786    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
787    pub(crate) special_caret_scalars: HashMap<String, PerlValue>,
788    /// `$%` — format output page number.
789    pub format_page_number: i64,
790    /// `$=` — format lines per page.
791    pub format_lines_per_page: i64,
792    /// `$-` — lines remaining on format page.
793    pub format_lines_left: i64,
794    /// `$:` — characters to break format lines (Perl default `\n`).
795    pub format_line_break_chars: String,
796    /// `$^` — top-of-form format name.
797    pub format_top_name: String,
798    /// `$^A` — format write accumulator.
799    pub accumulator_format: String,
800    /// `$^F` — max system file descriptor (Perl default 2).
801    pub max_system_fd: i64,
802    /// `$^M` — emergency memory buffer (no-op pool in stryke).
803    pub emergency_memory: String,
804    /// `$^N` — last opened named regexp capture name.
805    pub last_subpattern_name: String,
806    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
807    pub inc_hook_index: i64,
808    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
809    pub multiline_match: bool,
810    /// `$^X` — path to this executable (cached).
811    pub executable_path: String,
812    /// `$^L` — formfeed string for formats (Perl default `\f`).
813    pub formfeed_string: String,
814    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
815    pub(crate) glob_handle_alias: HashMap<String, String>,
816    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
817    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
818    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
819    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
820    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
821    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
822    pub(crate) special_var_restore_frames: Vec<Vec<(String, PerlValue)>>,
823    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
824    /// Lazy-init flag: reflection hashes (`%b`, `%stryke::builtins`, etc.)
825    /// are only built on first access to avoid startup cost.
826    pub(crate) reflection_hashes_ready: bool,
827    pub(crate) english_enabled: bool,
828    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
829    pub(crate) english_no_match_vars: bool,
830    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
831    /// available for the rest of the program — Perl exports them into the caller's namespace
832    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
833    pub(crate) english_match_vars_ever_enabled: bool,
834    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
835    english_lexical_scalars: Vec<HashSet<String>>,
836    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
837    our_lexical_scalars: Vec<HashSet<String>>,
838    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
839    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
840    pub vm_jit_enabled: bool,
841    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
842    pub disasm_bytecode: bool,
843    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from a bytecode cache hit. When
844    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
845    /// (`.take()`) on first read so re-entry compiles normally.
846    pub cached_chunk: Option<crate::bytecode::Chunk>,
847    /// Sideband: script path for bytecode cache save after compilation (mtime-based).
848    pub cache_script_path: Option<std::path::PathBuf>,
849    /// Set while stepping a `gen { }` body (`yield`).
850    pub(crate) in_generator: bool,
851    /// `-n`/`-p` driver: prelude only; body runs per line in [`Self::process_line_vm`].
852    pub line_mode_skip_main: bool,
853    /// Pre-compiled chunk for `-n`/`-p` line mode. Stored after the prelude `execute()` call
854    /// so `process_line_vm` can re-execute the body portion per input line.
855    pub line_mode_chunk: Option<crate::bytecode::Chunk>,
856    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
857    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
858    /// matches Perl (true on the last line of that source).
859    pub(crate) line_mode_eof_pending: bool,
860    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
861    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
862    pub line_mode_stdin_pending: VecDeque<String>,
863    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
864    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
865    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
866    pub(crate) log_level_override: Option<LogLevelFilter>,
867    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
868    /// Pushed on `call_sub` entry, popped on exit.
869    pub(crate) current_sub_stack: Vec<Arc<PerlSub>>,
870    /// Interactive debugger state (`-d` flag).
871    pub debugger: Option<crate::debugger::Debugger>,
872    /// Call stack for debugger: (sub_name, call_line).
873    pub(crate) debug_call_stack: Vec<(String, usize)>,
874}
875
876/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
877#[derive(Debug, Clone, Default)]
878pub struct ReplCompletionSnapshot {
879    pub subs: Vec<String>,
880    pub blessed_scalars: HashMap<String, String>,
881    pub isa_for_class: HashMap<String, Vec<String>>,
882}
883
884impl ReplCompletionSnapshot {
885    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
886    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
887        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
888        let mro = linearize_c3(class, &parents, 0);
889        let mut names = HashSet::new();
890        for pkg in &mro {
891            if pkg == "UNIVERSAL" {
892                continue;
893            }
894            let prefix = format!("{}::", pkg);
895            for k in &self.subs {
896                if k.starts_with(&prefix) {
897                    let rest = &k[prefix.len()..];
898                    if !rest.contains("::") {
899                        names.insert(rest.to_string());
900                    }
901                }
902            }
903        }
904        for k in &self.subs {
905            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
906                if !rest.contains("::") {
907                    names.insert(rest.to_string());
908                }
909            }
910        }
911        let mut v: Vec<String> = names.into_iter().collect();
912        v.sort();
913        v
914    }
915}
916
917fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
918    let left = left.trim_end();
919    if left.is_empty() {
920        return None;
921    }
922    if let Some(i) = left.rfind('$') {
923        let name = left[i + 1..].trim();
924        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
925            return state.blessed_scalars.get(name).cloned();
926        }
927    }
928    let tok = left.split_whitespace().last()?;
929    if tok.contains("::") {
930        return Some(tok.to_string());
931    }
932    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
933        return Some(tok.to_string());
934    }
935    None
936}
937
938/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
939pub fn repl_arrow_method_completions(
940    state: &ReplCompletionSnapshot,
941    line: &str,
942    pos: usize,
943) -> Option<(usize, Vec<String>)> {
944    let pos = pos.min(line.len());
945    let before = &line[..pos];
946    let arrow_idx = before.rfind("->")?;
947    let after_arrow = &before[arrow_idx + 2..];
948    let rest = after_arrow.trim_start();
949    let ws_len = after_arrow.len() - rest.len();
950    let method_start = arrow_idx + 2 + ws_len;
951    let method_prefix = &line[method_start..pos];
952    if !method_prefix
953        .chars()
954        .all(|c| c.is_alphanumeric() || c == '_')
955    {
956        return None;
957    }
958    let left = line[..arrow_idx].trim_end();
959    let class = repl_resolve_class_for_arrow(state, left)?;
960    let mut methods = state.methods_for_class(&class);
961    methods.retain(|m| m.starts_with(method_prefix));
962    Some((method_start, methods))
963}
964
965/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
966#[derive(Debug, Clone, Default)]
967pub(crate) struct ModuleExportLists {
968    /// Default imports for `use Module` with no list.
969    pub export: Vec<String>,
970    /// Extra symbols allowed in `use Module qw(name)`.
971    pub export_ok: Vec<String>,
972}
973
974/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
975fn piped_shell_command(cmd: &str) -> Command {
976    if cfg!(windows) {
977        let mut c = Command::new("cmd");
978        c.arg("/C").arg(cmd);
979        c
980    } else {
981        let mut c = Command::new("sh");
982        c.arg("-c").arg(cmd);
983        c
984    }
985}
986
987/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
988/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
989/// so the Rust `regex` crate can match them.
990/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
991/// so the Rust regex crate can match NUL and other octal-specified bytes.
992/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
993fn expand_perl_regex_octal_escapes(pat: &str) -> String {
994    let mut out = String::with_capacity(pat.len());
995    let mut it = pat.chars().peekable();
996    while let Some(c) = it.next() {
997        if c == '\\' {
998            if let Some(&'0') = it.peek() {
999                // Collect up to 3 octal digits starting with '0'
1000                let mut oct = String::new();
1001                while oct.len() < 3 {
1002                    if let Some(&d) = it.peek() {
1003                        if ('0'..='7').contains(&d) {
1004                            oct.push(d);
1005                            it.next();
1006                        } else {
1007                            break;
1008                        }
1009                    } else {
1010                        break;
1011                    }
1012                }
1013                if let Ok(val) = u8::from_str_radix(&oct, 8) {
1014                    out.push_str(&format!("\\x{:02x}", val));
1015                } else {
1016                    out.push('\\');
1017                    out.push_str(&oct);
1018                }
1019                continue;
1020            }
1021        }
1022        out.push(c);
1023    }
1024    out
1025}
1026
1027fn expand_perl_regex_quotemeta(pat: &str) -> String {
1028    let mut out = String::with_capacity(pat.len().saturating_mul(2));
1029    let mut it = pat.chars().peekable();
1030    let mut in_q = false;
1031    while let Some(c) = it.next() {
1032        if in_q {
1033            if c == '\\' && it.peek() == Some(&'E') {
1034                it.next();
1035                in_q = false;
1036                continue;
1037            }
1038            out.push_str(&perl_quotemeta(&c.to_string()));
1039            continue;
1040        }
1041        if c == '\\' && it.peek() == Some(&'Q') {
1042            it.next();
1043            in_q = true;
1044            continue;
1045        }
1046        out.push(c);
1047    }
1048    out
1049}
1050
1051/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
1052///
1053/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
1054/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
1055///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
1056pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
1057    let mut out = String::with_capacity(replacement.len() + 8);
1058    let mut it = replacement.chars().peekable();
1059    while let Some(c) = it.next() {
1060        if c == '\\' {
1061            match it.peek() {
1062                Some(&d) if d.is_ascii_digit() => {
1063                    it.next();
1064                    out.push_str("${");
1065                    out.push(d);
1066                    while let Some(&d2) = it.peek() {
1067                        if !d2.is_ascii_digit() {
1068                            break;
1069                        }
1070                        it.next();
1071                        out.push(d2);
1072                    }
1073                    out.push('}');
1074                }
1075                Some(&'\\') => {
1076                    it.next();
1077                    out.push('\\');
1078                }
1079                _ => out.push('\\'),
1080            }
1081        } else if c == '$' {
1082            match it.peek() {
1083                Some(&d) if d.is_ascii_digit() => {
1084                    it.next();
1085                    out.push_str("${");
1086                    out.push(d);
1087                    while let Some(&d2) = it.peek() {
1088                        if !d2.is_ascii_digit() {
1089                            break;
1090                        }
1091                        it.next();
1092                        out.push(d2);
1093                    }
1094                    out.push('}');
1095                }
1096                Some(&'{') => {
1097                    // already braced — pass through as-is
1098                    out.push('$');
1099                }
1100                _ => out.push('$'),
1101            }
1102        } else {
1103            out.push(c);
1104        }
1105    }
1106    out
1107}
1108
1109/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
1110/// past the closing `]`.
1111fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
1112    debug_assert_eq!(chars.get(i), Some(&'['));
1113    out.push('[');
1114    i += 1;
1115    if i < chars.len() && chars[i] == '^' {
1116        out.push('^');
1117        i += 1;
1118    }
1119    if i >= chars.len() {
1120        return i;
1121    }
1122    // `]` as the first class character is literal iff another unescaped `]` closes the class
1123    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
1124    // this `]`.
1125    if chars[i] == ']' {
1126        if i + 1 < chars.len() && chars[i + 1] == ']' {
1127            // `[]]` / `[^]]`: literal `]` then the closing `]`.
1128            out.push(']');
1129            i += 1;
1130        } else {
1131            let mut scan = i + 1;
1132            let mut found_closing = false;
1133            while scan < chars.len() {
1134                if chars[scan] == '\\' && scan + 1 < chars.len() {
1135                    scan += 2;
1136                    continue;
1137                }
1138                if chars[scan] == ']' {
1139                    found_closing = true;
1140                    break;
1141                }
1142                scan += 1;
1143            }
1144            if found_closing {
1145                out.push(']');
1146                i += 1;
1147            } else {
1148                out.push(']');
1149                return i + 1;
1150            }
1151        }
1152    }
1153    while i < chars.len() && chars[i] != ']' {
1154        if chars[i] == '\\' && i + 1 < chars.len() {
1155            out.push(chars[i]);
1156            out.push(chars[i + 1]);
1157            i += 2;
1158            continue;
1159        }
1160        out.push(chars[i]);
1161        i += 1;
1162    }
1163    if i < chars.len() {
1164        out.push(']');
1165        i += 1;
1166    }
1167    i
1168}
1169
1170/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
1171/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
1172/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1173/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1174fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1175    if multiline_flag {
1176        return pat.to_string();
1177    }
1178    let chars: Vec<char> = pat.chars().collect();
1179    let mut out = String::with_capacity(pat.len().saturating_add(16));
1180    let mut i = 0usize;
1181    while i < chars.len() {
1182        let c = chars[i];
1183        if c == '\\' && i + 1 < chars.len() {
1184            out.push(c);
1185            out.push(chars[i + 1]);
1186            i += 2;
1187            continue;
1188        }
1189        if c == '[' {
1190            i = copy_regex_char_class(&chars, i, &mut out);
1191            continue;
1192        }
1193        if c == '$' {
1194            if let Some(&next) = chars.get(i + 1) {
1195                if next.is_ascii_digit() {
1196                    out.push(c);
1197                    i += 1;
1198                    continue;
1199                }
1200                if next == '{' {
1201                    out.push(c);
1202                    i += 1;
1203                    continue;
1204                }
1205                if next.is_ascii_alphanumeric() || next == '_' {
1206                    out.push(c);
1207                    i += 1;
1208                    continue;
1209                }
1210            }
1211            out.push_str("(?=\\n?\\z)");
1212            i += 1;
1213            continue;
1214        }
1215        out.push(c);
1216        i += 1;
1217    }
1218    out
1219}
1220
1221/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1222#[derive(Debug, Clone)]
1223pub(crate) struct DirHandleState {
1224    pub entries: Vec<String>,
1225    pub pos: usize,
1226}
1227
1228/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1229pub(crate) fn perl_osname() -> String {
1230    match std::env::consts::OS {
1231        "linux" => "linux".to_string(),
1232        "macos" => "darwin".to_string(),
1233        "windows" => "MSWin32".to_string(),
1234        other => other.to_string(),
1235    }
1236}
1237
1238fn perl_version_v_string() -> String {
1239    format!("v{}", env!("CARGO_PKG_VERSION"))
1240}
1241
1242fn extended_os_error_string() -> String {
1243    std::io::Error::last_os_error().to_string()
1244}
1245
1246#[cfg(unix)]
1247fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1248    unsafe {
1249        (
1250            libc::getuid() as i64,
1251            libc::geteuid() as i64,
1252            libc::getgid() as i64,
1253            libc::getegid() as i64,
1254        )
1255    }
1256}
1257
1258#[cfg(not(unix))]
1259fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1260    (0, 0, 0, 0)
1261}
1262
1263fn unix_id_for_special(name: &str) -> i64 {
1264    let (r, e, _, _) = unix_real_effective_ids();
1265    match name {
1266        "<" => r,
1267        ">" => e,
1268        _ => 0,
1269    }
1270}
1271
1272#[cfg(unix)]
1273fn unix_group_list_string(primary: libc::gid_t) -> String {
1274    let mut buf = vec![0 as libc::gid_t; 256];
1275    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1276    if n <= 0 {
1277        return format!("{}", primary);
1278    }
1279    let mut parts = vec![format!("{}", primary)];
1280    for g in buf.iter().take(n as usize) {
1281        parts.push(format!("{}", g));
1282    }
1283    parts.join(" ")
1284}
1285
1286/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1287#[cfg(unix)]
1288fn unix_group_list_for_special(name: &str) -> String {
1289    let (_, _, gid, egid) = unix_real_effective_ids();
1290    match name {
1291        "(" => unix_group_list_string(gid as libc::gid_t),
1292        ")" => unix_group_list_string(egid as libc::gid_t),
1293        _ => String::new(),
1294    }
1295}
1296
1297#[cfg(not(unix))]
1298fn unix_group_list_for_special(_name: &str) -> String {
1299    String::new()
1300}
1301
1302/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1303/// `~/.ssh/config` and keys).
1304#[cfg(unix)]
1305fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1306    use libc::{getpwuid_r, getuid};
1307    use std::ffi::CStr;
1308    use std::os::unix::ffi::OsStringExt;
1309    let uid = unsafe { getuid() };
1310    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1311    let mut result: *mut libc::passwd = std::ptr::null_mut();
1312    let mut buf = vec![0u8; 16_384];
1313    let rc = unsafe {
1314        getpwuid_r(
1315            uid,
1316            &mut pw,
1317            buf.as_mut_ptr().cast::<libc::c_char>(),
1318            buf.len(),
1319            &mut result,
1320        )
1321    };
1322    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1323        return None;
1324    }
1325    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1326    if bytes.is_empty() {
1327        return None;
1328    }
1329    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1330}
1331
1332/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1333#[cfg(unix)]
1334fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1335    use libc::getpwnam_r;
1336    use std::ffi::{CStr, CString};
1337    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1338    let bytes = login.as_bytes();
1339    if bytes.is_empty() || bytes.contains(&0) {
1340        return None;
1341    }
1342    let cname = CString::new(bytes).ok()?;
1343    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1344    let mut result: *mut libc::passwd = std::ptr::null_mut();
1345    let mut buf = vec![0u8; 16_384];
1346    let rc = unsafe {
1347        getpwnam_r(
1348            cname.as_ptr(),
1349            &mut pw,
1350            buf.as_mut_ptr().cast::<libc::c_char>(),
1351            buf.len(),
1352            &mut result,
1353        )
1354    };
1355    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1356        return None;
1357    }
1358    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1359    if dir_bytes.is_empty() {
1360        return None;
1361    }
1362    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1363}
1364
1365impl Default for VMHelper {
1366    fn default() -> Self {
1367        Self::new()
1368    }
1369}
1370
1371/// How [`VMHelper::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1372#[derive(Clone, Copy)]
1373pub(crate) enum CaptureAllMode {
1374    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1375    Empty,
1376    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1377    Append,
1378    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1379    Skip,
1380}
1381
1382impl VMHelper {
1383    pub fn new() -> Self {
1384        let mut scope = Scope::new();
1385        scope.declare_array("INC", vec![PerlValue::string(".".to_string())]);
1386        scope.declare_hash("INC", IndexMap::new());
1387        scope.declare_array("ARGV", vec![]);
1388        scope.declare_array("_", vec![]);
1389
1390        // @path / @p — $PATH split by OS path separator, frozen (immutable)
1391        let path_vec: Vec<PerlValue> = std::env::var("PATH")
1392            .unwrap_or_default()
1393            .split(if cfg!(windows) { ';' } else { ':' })
1394            .filter(|s| !s.is_empty())
1395            .map(|p| PerlValue::string(p.to_string()))
1396            .collect();
1397        scope.declare_array_frozen("path", path_vec.clone(), true);
1398        scope.declare_array_frozen("p", path_vec, true);
1399
1400        // @fpath / @f — $FPATH (zsh function path) split by ':', frozen
1401        let fpath_vec: Vec<PerlValue> = std::env::var("FPATH")
1402            .unwrap_or_default()
1403            .split(':')
1404            .filter(|s| !s.is_empty())
1405            .map(|p| PerlValue::string(p.to_string()))
1406            .collect();
1407        scope.declare_array_frozen("fpath", fpath_vec.clone(), true);
1408        scope.declare_array_frozen("f", fpath_vec, true);
1409        scope.declare_hash("ENV", IndexMap::new());
1410        scope.declare_hash("SIG", IndexMap::new());
1411
1412        // %term — terminal info (frozen)
1413        let term_map = build_term_hash();
1414        scope.declare_hash_global_frozen("term", term_map);
1415
1416        // %uname — system identification (frozen, Unix only)
1417        #[cfg(unix)]
1418        {
1419            let uname_map = build_uname_hash();
1420            scope.declare_hash_global_frozen("uname", uname_map);
1421        }
1422        #[cfg(not(unix))]
1423        {
1424            scope.declare_hash_global_frozen("uname", IndexMap::new());
1425        }
1426
1427        // %limits — resource limits (frozen, Unix only)
1428        #[cfg(unix)]
1429        {
1430            let limits_map = build_limits_hash();
1431            scope.declare_hash_global_frozen("limits", limits_map);
1432        }
1433        #[cfg(not(unix))]
1434        {
1435            scope.declare_hash_global_frozen("limits", IndexMap::new());
1436        }
1437
1438        // Reflection hashes — populated from `build.rs`-generated tables so
1439        // they track the real parser/dispatcher/LSP without hand-maintenance.
1440        // Seven hashes; all lookups are O(1). Forward maps:
1441        //   %b  / %stryke::builtins      — name → category ("parallel", "string", …)
1442        //   %pc / %stryke::perl_compats  — subset: Perl 5 core only
1443        //   %e  / %stryke::extensions    — subset: stryke-only
1444        //   %a  / %stryke::aliases       — alias → primary
1445        //   %d  / %stryke::descriptions  — name → LSP one-liner (sparse)
1446        // Inverted indexes for constant-time reverse queries:
1447        //   %c  / %stryke::categories    — category → arrayref of names
1448        //   %p  / %stryke::primaries     — primary → arrayref of aliases
1449        //
1450        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1451        // together they cover `keys %builtins`. Short aliases use the
1452        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1453        // Reflection hashes are lazily initialized on first access
1454        // (see `ensure_reflection_hashes`). Only declare the version scalar
1455        // eagerly since it's trivial.
1456        scope.declare_scalar(
1457            "stryke::VERSION",
1458            PerlValue::string(env!("CARGO_PKG_VERSION").to_string()),
1459        );
1460        scope.declare_array("-", vec![]);
1461        scope.declare_array("+", vec![]);
1462        scope.declare_array("^CAPTURE", vec![]);
1463        scope.declare_array("^CAPTURE_ALL", vec![]);
1464        scope.declare_hash("^HOOK", IndexMap::new());
1465        scope.declare_scalar("~", PerlValue::string("STDOUT".to_string()));
1466
1467        let script_start_time = std::time::SystemTime::now()
1468            .duration_since(std::time::UNIX_EPOCH)
1469            .map(|d| d.as_secs() as i64)
1470            .unwrap_or(0);
1471
1472        let executable_path = cached_executable_path();
1473
1474        let mut special_caret_scalars: HashMap<String, PerlValue> = HashMap::new();
1475        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1476            special_caret_scalars.insert(format!("^{}", name), PerlValue::UNDEF);
1477        }
1478
1479        let mut s = Self {
1480            scope,
1481            subs: HashMap::new(),
1482            intercepts: Vec::new(),
1483            next_intercept_id: 1,
1484            intercept_ctx_stack: Vec::new(),
1485            intercept_active_names: Vec::new(),
1486            struct_defs: HashMap::new(),
1487            enum_defs: HashMap::new(),
1488            class_defs: HashMap::new(),
1489            trait_defs: HashMap::new(),
1490            file: "-e".to_string(),
1491            output_handles: HashMap::new(),
1492            input_handles: HashMap::new(),
1493            ofs: String::new(),
1494            ors: String::new(),
1495            irs: Some("\n".to_string()),
1496            errno: String::new(),
1497            errno_code: 0,
1498            eval_error: String::new(),
1499            eval_error_code: 0,
1500            eval_error_value: None,
1501            argv: Vec::new(),
1502            env: IndexMap::new(),
1503            env_materialized: false,
1504            program_name: "stryke".to_string(),
1505            line_number: 0,
1506            last_readline_handle: String::new(),
1507            last_stdin_die_bracket: "<STDIN>".to_string(),
1508            handle_line_numbers: HashMap::new(),
1509            flip_flop_active: Vec::new(),
1510            flip_flop_exclusive_left_line: Vec::new(),
1511            flip_flop_sequence: Vec::new(),
1512            flip_flop_last_dot: Vec::new(),
1513            flip_flop_tree: HashMap::new(),
1514            sigint_pending_caret: Cell::new(false),
1515            auto_split: false,
1516            field_separator: None,
1517            begin_blocks: Vec::new(),
1518            unit_check_blocks: Vec::new(),
1519            check_blocks: Vec::new(),
1520            init_blocks: Vec::new(),
1521            end_blocks: Vec::new(),
1522            warnings: false,
1523            output_autoflush: false,
1524            default_print_handle: "STDOUT".to_string(),
1525            suppress_stdout: false,
1526            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1527            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1528            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1529            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1530            child_exit_status: 0,
1531            last_match: String::new(),
1532            prematch: String::new(),
1533            postmatch: String::new(),
1534            last_paren_match: String::new(),
1535            list_separator: " ".to_string(),
1536            script_start_time,
1537            compile_hints: 0,
1538            warning_bits: 0,
1539            global_phase: "RUN".to_string(),
1540            subscript_sep: "\x1c".to_string(),
1541            inplace_edit: String::new(),
1542            debug_flags: 0,
1543            perl_debug_flags: 0,
1544            eval_nesting: 0,
1545            argv_current_file: String::new(),
1546            diamond_next_idx: 0,
1547            diamond_reader: None,
1548            strict_refs: false,
1549            strict_subs: false,
1550            strict_vars: false,
1551            utf8_pragma: false,
1552            open_pragma_utf8: false,
1553            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1554            feature_bits: FEAT_SAY,
1555            num_threads: 0, // lazily read from rayon on first parallel op
1556            regex_cache: HashMap::new(),
1557            regex_last: None,
1558            regex_match_memo: None,
1559            regex_capture_scope_fresh: false,
1560            regex_pos: HashMap::new(),
1561            state_vars: HashMap::new(),
1562            state_bindings_stack: Vec::new(),
1563            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1564            dir_handles: HashMap::new(),
1565            io_file_slots: HashMap::new(),
1566            pipe_children: HashMap::new(),
1567            socket_handles: HashMap::new(),
1568            wantarray_kind: WantarrayCtx::Scalar,
1569            profiler: None,
1570            module_export_lists: HashMap::new(),
1571            virtual_modules: HashMap::new(),
1572            tied_hashes: HashMap::new(),
1573            tied_scalars: HashMap::new(),
1574            tied_arrays: HashMap::new(),
1575            overload_table: HashMap::new(),
1576            format_templates: HashMap::new(),
1577            special_caret_scalars,
1578            format_page_number: 0,
1579            format_lines_per_page: 60,
1580            format_lines_left: 0,
1581            format_line_break_chars: "\n".to_string(),
1582            format_top_name: String::new(),
1583            accumulator_format: String::new(),
1584            max_system_fd: 2,
1585            emergency_memory: String::new(),
1586            last_subpattern_name: String::new(),
1587            inc_hook_index: 0,
1588            multiline_match: false,
1589            executable_path,
1590            formfeed_string: "\x0c".to_string(),
1591            glob_handle_alias: HashMap::new(),
1592            glob_restore_frames: vec![Vec::new()],
1593            special_var_restore_frames: vec![Vec::new()],
1594            reflection_hashes_ready: false,
1595            english_enabled: false,
1596            english_no_match_vars: false,
1597            english_match_vars_ever_enabled: false,
1598            english_lexical_scalars: vec![HashSet::new()],
1599            our_lexical_scalars: vec![HashSet::new()],
1600            vm_jit_enabled: !matches!(
1601                std::env::var("STRYKE_NO_JIT"),
1602                Ok(v)
1603                    if v == "1"
1604                        || v.eq_ignore_ascii_case("true")
1605                        || v.eq_ignore_ascii_case("yes")
1606            ),
1607            disasm_bytecode: false,
1608            cached_chunk: None,
1609            cache_script_path: None,
1610            in_generator: false,
1611            line_mode_skip_main: false,
1612            line_mode_chunk: None,
1613            line_mode_eof_pending: false,
1614            line_mode_stdin_pending: VecDeque::new(),
1615            rate_limit_slots: Vec::new(),
1616            log_level_override: None,
1617            current_sub_stack: Vec::new(),
1618            debugger: None,
1619            debug_call_stack: Vec::new(),
1620        };
1621        s.install_overload_pragma_stubs();
1622        s
1623    }
1624
1625    /// Lazily populate the reflection hashes (`%b`, `%stryke::builtins`, etc.)
1626    /// on first access. This avoids building ~12k hash entries on startup for
1627    /// one-liners that never touch introspection.
1628    pub(crate) fn ensure_reflection_hashes(&mut self) {
1629        if self.reflection_hashes_ready {
1630            return;
1631        }
1632        self.reflection_hashes_ready = true;
1633        // Package stashes (`%main::` / `%Pkg::`) are Perl-spec, install in
1634        // every mode — `--compat` does not turn off the symbol table.
1635        self.refresh_package_stashes();
1636        // Everything below is stryke-only. `--compat` skips the entire block
1637        // so a Perl 5 script sees no extension hashes and can use `%all` /
1638        // `%b` / `%parameters` / `%stryke::*` etc. as ordinary user hashes.
1639        if crate::compat_mode() {
1640            return;
1641        }
1642        let builtins_map = crate::builtins::builtins_hash_map();
1643        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1644        let extensions_map = crate::builtins::extensions_hash_map();
1645        let aliases_map = crate::builtins::aliases_hash_map();
1646        let descriptions_map = crate::builtins::descriptions_hash_map();
1647        let categories_map = crate::builtins::categories_hash_map();
1648        let primaries_map = crate::builtins::primaries_hash_map();
1649        let keywords_map = crate::builtins::keywords_hash_map();
1650        let all_map = crate::builtins::all_hash_map();
1651        self.scope
1652            .declare_hash_global_frozen("stryke::builtins", builtins_map.clone());
1653        self.scope
1654            .declare_hash_global_frozen("stryke::perl_compats", perl_compats_map.clone());
1655        self.scope
1656            .declare_hash_global_frozen("stryke::extensions", extensions_map.clone());
1657        self.scope
1658            .declare_hash_global_frozen("stryke::aliases", aliases_map.clone());
1659        self.scope
1660            .declare_hash_global_frozen("stryke::descriptions", descriptions_map.clone());
1661        self.scope
1662            .declare_hash_global_frozen("stryke::categories", categories_map.clone());
1663        self.scope
1664            .declare_hash_global_frozen("stryke::primaries", primaries_map.clone());
1665        self.scope
1666            .declare_hash_global_frozen("stryke::keywords", keywords_map.clone());
1667        self.scope
1668            .declare_hash_global_frozen("stryke::all", all_map.clone());
1669        // Short aliases: only declare if no user-declared hash with that name
1670        // exists, to avoid overwriting `my %e` etc.
1671        for (name, val) in [
1672            ("b", builtins_map),
1673            ("pc", perl_compats_map),
1674            ("e", extensions_map),
1675            ("a", aliases_map),
1676            ("d", descriptions_map),
1677            ("c", categories_map),
1678            ("p", primaries_map),
1679            ("k", keywords_map),
1680            ("all", all_map),
1681        ] {
1682            if !self.scope.any_frame_has_hash(name) {
1683                self.scope.declare_hash_global_frozen(name, val);
1684            }
1685        }
1686        // Initial install of `%parameters` (zsh-`$parameters` analogue).
1687        // Refreshed automatically on every read via `touch_env_hash`.
1688        if !self.scope.has_lexical_hash("parameters") {
1689            self.refresh_parameters_hash();
1690        }
1691    }
1692
1693    /// Rebuild `%parameters` (zsh-`$parameters` analogue) from the current
1694    /// scope. Maps every live sigil-prefixed name (`$x`, `@a`, `%h`, …) to its
1695    /// kind string (`"scalar"`, `"array"`, `"hash"`, `"atomic_array"`,
1696    /// `"atomic_hash"`, `"shared_array"`, `"shared_hash"`). Installed as a
1697    /// frozen global hash so user code can read it but not assign into it
1698    /// (parallel to `%all` / `%b` / `%stryke::*`). Refreshed automatically on
1699    /// every `%parameters` read via the `touch_env_hash` hook, so the snapshot
1700    /// is always current — the user never needs to call this directly.
1701    pub fn refresh_parameters_hash(&mut self) {
1702        let pairs = self.scope.parameters_pairs();
1703        let mut h: indexmap::IndexMap<String, PerlValue> =
1704            indexmap::IndexMap::with_capacity(pairs.len());
1705        for (name, kind) in pairs {
1706            h.insert(name, PerlValue::string(kind.to_string()));
1707        }
1708        // declare_hash_global_frozen overwrites unconditionally, so each
1709        // refresh replaces the prior snapshot.
1710        self.scope.declare_hash_global_frozen("parameters", h);
1711    }
1712
1713    /// Populate `%main::` / `%Foo::` package stashes with current symbol-table
1714    /// state so `keys %main::` and `keys %Foo::` enumerate live names. Maps
1715    /// each unqualified name → its kind string (`"scalar"`, `"array"`,
1716    /// `"hash"`, `"sub"`). Stryke has no real Perl typeglob layer; the kind
1717    /// string is the most useful per-symbol value we can offer.
1718    ///
1719    /// Callable repeatedly — overwrites prior stashes — so the REPL refreshes
1720    /// after every line and scripts can call it explicitly via the
1721    /// `refresh_stashes()` builtin if they want post-eval visibility.
1722    pub fn refresh_package_stashes(&mut self) {
1723        use indexmap::IndexMap;
1724
1725        let mut by_pkg: std::collections::HashMap<String, IndexMap<String, PerlValue>> =
1726            std::collections::HashMap::new();
1727
1728        let record =
1729            |pkg: &str,
1730             sym: &str,
1731             kind: &str,
1732             map: &mut std::collections::HashMap<String, IndexMap<String, PerlValue>>| {
1733                map.entry(pkg.to_string())
1734                    .or_default()
1735                    .insert(sym.to_string(), PerlValue::string(kind.to_string()));
1736            };
1737
1738        // Subs: keys like "main::foo" / "Foo::Bar::baz".
1739        for key in self.subs.keys() {
1740            if let Some(idx) = key.rfind("::") {
1741                let (pkg, rest) = key.split_at(idx);
1742                let sym = &rest[2..];
1743                if pkg.is_empty() || sym.is_empty() {
1744                    continue;
1745                }
1746                record(pkg, sym, "sub", &mut by_pkg);
1747            } else {
1748                // Bare-name sub (no package qualifier) lives in main::.
1749                record("main", key, "sub", &mut by_pkg);
1750            }
1751        }
1752
1753        // Package-qualified scalars / arrays / hashes from every frame.
1754        for frame in self.scope.frames_for_introspection() {
1755            let (scalars, arrays, hashes) = frame;
1756            for name in scalars {
1757                if let Some(idx) = name.rfind("::") {
1758                    let (pkg, rest) = name.split_at(idx);
1759                    let sym = &rest[2..];
1760                    if !pkg.is_empty() && !sym.is_empty() {
1761                        record(pkg, sym, "scalar", &mut by_pkg);
1762                    }
1763                }
1764            }
1765            for name in arrays {
1766                if let Some(idx) = name.rfind("::") {
1767                    let (pkg, rest) = name.split_at(idx);
1768                    let sym = &rest[2..];
1769                    if !pkg.is_empty() && !sym.is_empty() {
1770                        record(pkg, sym, "array", &mut by_pkg);
1771                    }
1772                }
1773            }
1774            for name in hashes {
1775                if let Some(idx) = name.rfind("::") {
1776                    let (pkg, rest) = name.split_at(idx);
1777                    let sym = &rest[2..];
1778                    if !pkg.is_empty() && !sym.is_empty() {
1779                        record(pkg, sym, "hash", &mut by_pkg);
1780                    }
1781                }
1782            }
1783        }
1784
1785        // Install each `%Pkg::` in the global frame. Lexer emits the trailing
1786        // `::` as part of the name, so the stash hash lives under that exact
1787        // key. `declare_hash_global_frozen` overwrites any prior copy.
1788        for (pkg, mut entries) in by_pkg {
1789            entries.sort_keys();
1790            let key = format!("{}::", pkg);
1791            self.scope.declare_hash_global_frozen(&key, entries);
1792        }
1793    }
1794
1795    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1796    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1797    /// strict subs and to satisfy `use overload ();` call sites.
1798    fn install_overload_pragma_stubs(&mut self) {
1799        let empty: Block = vec![];
1800        for key in ["overload::import", "overload::unimport"] {
1801            let name = key.to_string();
1802            self.subs.insert(
1803                name.clone(),
1804                Arc::new(PerlSub {
1805                    name,
1806                    params: vec![],
1807                    body: empty.clone(),
1808                    prototype: None,
1809                    closure_env: None,
1810                    fib_like: None,
1811                }),
1812            );
1813        }
1814    }
1815
1816    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1817    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1818    pub fn line_mode_worker_clone(&self) -> VMHelper {
1819        VMHelper {
1820            scope: self.scope.clone(),
1821            subs: self.subs.clone(),
1822            intercepts: self.intercepts.clone(),
1823            next_intercept_id: self.next_intercept_id,
1824            intercept_ctx_stack: self.intercept_ctx_stack.clone(),
1825            intercept_active_names: self.intercept_active_names.clone(),
1826            struct_defs: self.struct_defs.clone(),
1827            enum_defs: self.enum_defs.clone(),
1828            class_defs: self.class_defs.clone(),
1829            trait_defs: self.trait_defs.clone(),
1830            file: self.file.clone(),
1831            output_handles: HashMap::new(),
1832            input_handles: HashMap::new(),
1833            ofs: self.ofs.clone(),
1834            ors: self.ors.clone(),
1835            irs: self.irs.clone(),
1836            errno: self.errno.clone(),
1837            errno_code: self.errno_code,
1838            eval_error: self.eval_error.clone(),
1839            eval_error_code: self.eval_error_code,
1840            eval_error_value: self.eval_error_value.clone(),
1841            argv: self.argv.clone(),
1842            env: self.env.clone(),
1843            env_materialized: self.env_materialized,
1844            program_name: self.program_name.clone(),
1845            line_number: 0,
1846            last_readline_handle: String::new(),
1847            last_stdin_die_bracket: "<STDIN>".to_string(),
1848            handle_line_numbers: HashMap::new(),
1849            flip_flop_active: Vec::new(),
1850            flip_flop_exclusive_left_line: Vec::new(),
1851            flip_flop_sequence: Vec::new(),
1852            flip_flop_last_dot: Vec::new(),
1853            flip_flop_tree: HashMap::new(),
1854            sigint_pending_caret: Cell::new(false),
1855            auto_split: self.auto_split,
1856            field_separator: self.field_separator.clone(),
1857            begin_blocks: self.begin_blocks.clone(),
1858            unit_check_blocks: self.unit_check_blocks.clone(),
1859            check_blocks: self.check_blocks.clone(),
1860            init_blocks: self.init_blocks.clone(),
1861            end_blocks: self.end_blocks.clone(),
1862            warnings: self.warnings,
1863            output_autoflush: self.output_autoflush,
1864            default_print_handle: self.default_print_handle.clone(),
1865            suppress_stdout: self.suppress_stdout,
1866            // Workers start with fresh test counters — they don't share with the
1867            // parent. The parent is responsible for aggregating across workers if
1868            // it cares (none of the current parallel callers do).
1869            test_pass_count: std::sync::atomic::AtomicUsize::new(0),
1870            test_fail_count: std::sync::atomic::AtomicUsize::new(0),
1871            test_skip_count: std::sync::atomic::AtomicUsize::new(0),
1872            test_run_failed: std::sync::atomic::AtomicBool::new(false),
1873            child_exit_status: self.child_exit_status,
1874            last_match: self.last_match.clone(),
1875            prematch: self.prematch.clone(),
1876            postmatch: self.postmatch.clone(),
1877            last_paren_match: self.last_paren_match.clone(),
1878            list_separator: self.list_separator.clone(),
1879            script_start_time: self.script_start_time,
1880            compile_hints: self.compile_hints,
1881            warning_bits: self.warning_bits,
1882            global_phase: self.global_phase.clone(),
1883            subscript_sep: self.subscript_sep.clone(),
1884            inplace_edit: self.inplace_edit.clone(),
1885            debug_flags: self.debug_flags,
1886            perl_debug_flags: self.perl_debug_flags,
1887            eval_nesting: self.eval_nesting,
1888            argv_current_file: String::new(),
1889            diamond_next_idx: 0,
1890            diamond_reader: None,
1891            strict_refs: self.strict_refs,
1892            strict_subs: self.strict_subs,
1893            strict_vars: self.strict_vars,
1894            utf8_pragma: self.utf8_pragma,
1895            open_pragma_utf8: self.open_pragma_utf8,
1896            feature_bits: self.feature_bits,
1897            num_threads: 0,
1898            regex_cache: self.regex_cache.clone(),
1899            regex_last: self.regex_last.clone(),
1900            regex_match_memo: self.regex_match_memo.clone(),
1901            regex_capture_scope_fresh: false,
1902            regex_pos: self.regex_pos.clone(),
1903            state_vars: self.state_vars.clone(),
1904            state_bindings_stack: Vec::new(),
1905            rand_rng: self.rand_rng.clone(),
1906            dir_handles: HashMap::new(),
1907            io_file_slots: HashMap::new(),
1908            pipe_children: HashMap::new(),
1909            socket_handles: HashMap::new(),
1910            wantarray_kind: self.wantarray_kind,
1911            profiler: None,
1912            module_export_lists: self.module_export_lists.clone(),
1913            virtual_modules: self.virtual_modules.clone(),
1914            tied_hashes: self.tied_hashes.clone(),
1915            tied_scalars: self.tied_scalars.clone(),
1916            tied_arrays: self.tied_arrays.clone(),
1917            overload_table: self.overload_table.clone(),
1918            format_templates: self.format_templates.clone(),
1919            special_caret_scalars: self.special_caret_scalars.clone(),
1920            format_page_number: self.format_page_number,
1921            format_lines_per_page: self.format_lines_per_page,
1922            format_lines_left: self.format_lines_left,
1923            format_line_break_chars: self.format_line_break_chars.clone(),
1924            format_top_name: self.format_top_name.clone(),
1925            accumulator_format: self.accumulator_format.clone(),
1926            max_system_fd: self.max_system_fd,
1927            emergency_memory: self.emergency_memory.clone(),
1928            last_subpattern_name: self.last_subpattern_name.clone(),
1929            inc_hook_index: self.inc_hook_index,
1930            multiline_match: self.multiline_match,
1931            executable_path: self.executable_path.clone(),
1932            formfeed_string: self.formfeed_string.clone(),
1933            glob_handle_alias: self.glob_handle_alias.clone(),
1934            glob_restore_frames: self.glob_restore_frames.clone(),
1935            special_var_restore_frames: self.special_var_restore_frames.clone(),
1936            reflection_hashes_ready: self.reflection_hashes_ready,
1937            english_enabled: self.english_enabled,
1938            english_no_match_vars: self.english_no_match_vars,
1939            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
1940            english_lexical_scalars: self.english_lexical_scalars.clone(),
1941            our_lexical_scalars: self.our_lexical_scalars.clone(),
1942            vm_jit_enabled: self.vm_jit_enabled,
1943            disasm_bytecode: self.disasm_bytecode,
1944            // Sideband cache fields belong to the top-level driver, not line-mode workers.
1945            cached_chunk: None,
1946            cache_script_path: None,
1947            in_generator: false,
1948            line_mode_skip_main: false,
1949            line_mode_chunk: self.line_mode_chunk.clone(),
1950            line_mode_eof_pending: false,
1951            line_mode_stdin_pending: VecDeque::new(),
1952            rate_limit_slots: Vec::new(),
1953            log_level_override: self.log_level_override,
1954            current_sub_stack: Vec::new(),
1955            debugger: None,
1956            debug_call_stack: Vec::new(),
1957        }
1958    }
1959
1960    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
1961    pub(crate) fn parallel_thread_count(&mut self) -> usize {
1962        if self.num_threads == 0 {
1963            self.num_threads = rayon::current_num_threads();
1964        }
1965        self.num_threads
1966    }
1967
1968    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
1969    pub(crate) fn eval_par_list_call(
1970        &mut self,
1971        name: &str,
1972        args: &[PerlValue],
1973        ctx: WantarrayCtx,
1974        line: usize,
1975    ) -> PerlResult<PerlValue> {
1976        match name {
1977            "puniq" => {
1978                let (list_src, show_prog) = match args.len() {
1979                    0 => return Err(PerlError::runtime("puniq: expected LIST", line)),
1980                    1 => (&args[0], false),
1981                    2 => (&args[0], args[1].is_true()),
1982                    _ => {
1983                        return Err(PerlError::runtime(
1984                            "puniq: expected LIST [, progress => EXPR]",
1985                            line,
1986                        ));
1987                    }
1988                };
1989                let list = list_src.to_list();
1990                let n_threads = self.parallel_thread_count();
1991                let pmap_progress = PmapProgress::new(show_prog, list.len());
1992                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
1993                pmap_progress.finish();
1994                if ctx == WantarrayCtx::List {
1995                    Ok(PerlValue::array(out))
1996                } else {
1997                    Ok(PerlValue::integer(out.len() as i64))
1998                }
1999            }
2000            "pfirst" => {
2001                let (code_val, list_src, show_prog) = match args.len() {
2002                    2 => (&args[0], &args[1], false),
2003                    3 => (&args[0], &args[1], args[2].is_true()),
2004                    _ => {
2005                        return Err(PerlError::runtime(
2006                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
2007                            line,
2008                        ));
2009                    }
2010                };
2011                let Some(sub) = code_val.as_code_ref() else {
2012                    return Err(PerlError::runtime(
2013                        "pfirst: first argument must be a code reference",
2014                        line,
2015                    ));
2016                };
2017                let sub = sub.clone();
2018                let list = list_src.to_list();
2019                if list.is_empty() {
2020                    return Ok(PerlValue::UNDEF);
2021                }
2022                let pmap_progress = PmapProgress::new(show_prog, list.len());
2023                let subs = self.subs.clone();
2024                let (scope_capture, atomic_arrays, atomic_hashes) =
2025                    self.scope.capture_with_atomics();
2026                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
2027                    let mut local_interp = VMHelper::new();
2028                    local_interp.subs = subs.clone();
2029                    local_interp.scope.restore_capture(&scope_capture);
2030                    local_interp
2031                        .scope
2032                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2033                    local_interp.enable_parallel_guard();
2034                    local_interp.scope.set_topic(item);
2035                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2036                        Ok(v) => v.is_true(),
2037                        Err(_) => false,
2038                    }
2039                });
2040                pmap_progress.finish();
2041                Ok(out.unwrap_or(PerlValue::UNDEF))
2042            }
2043            "pany" => {
2044                let (code_val, list_src, show_prog) = match args.len() {
2045                    2 => (&args[0], &args[1], false),
2046                    3 => (&args[0], &args[1], args[2].is_true()),
2047                    _ => {
2048                        return Err(PerlError::runtime(
2049                            "pany: expected BLOCK, LIST [, progress => EXPR]",
2050                            line,
2051                        ));
2052                    }
2053                };
2054                let Some(sub) = code_val.as_code_ref() else {
2055                    return Err(PerlError::runtime(
2056                        "pany: first argument must be a code reference",
2057                        line,
2058                    ));
2059                };
2060                let sub = sub.clone();
2061                let list = list_src.to_list();
2062                let pmap_progress = PmapProgress::new(show_prog, list.len());
2063                let subs = self.subs.clone();
2064                let (scope_capture, atomic_arrays, atomic_hashes) =
2065                    self.scope.capture_with_atomics();
2066                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
2067                    let mut local_interp = VMHelper::new();
2068                    local_interp.subs = subs.clone();
2069                    local_interp.scope.restore_capture(&scope_capture);
2070                    local_interp
2071                        .scope
2072                        .restore_atomics(&atomic_arrays, &atomic_hashes);
2073                    local_interp.enable_parallel_guard();
2074                    local_interp.scope.set_topic(item);
2075                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
2076                        Ok(v) => v.is_true(),
2077                        Err(_) => false,
2078                    }
2079                });
2080                pmap_progress.finish();
2081                Ok(PerlValue::integer(if b { 1 } else { 0 }))
2082            }
2083            _ => Err(PerlError::runtime(
2084                format!("internal: unknown par_list builtin {name}"),
2085                line,
2086            )),
2087        }
2088    }
2089
2090    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
2091        #[cfg(unix)]
2092        if let Some(sig) = s.signal() {
2093            return sig as i64 & 0x7f;
2094        }
2095        let code = s.code().unwrap_or(0) as i64;
2096        code << 8
2097    }
2098
2099    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
2100        self.child_exit_status = self.encode_exit_status(s);
2101    }
2102
2103    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
2104    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
2105        // Perl's $! is the bare description ("No such file or directory"),
2106        // not Rust's "<desc> (os error N)" form. Strip the trailing parenthetical.
2107        let s = e.to_string();
2108        let stripped = s
2109            .rfind(" (os error ")
2110            .map(|i| s[..i].to_string())
2111            .unwrap_or(s);
2112        self.errno = stripped;
2113        self.errno_code = e.raw_os_error().unwrap_or(0);
2114    }
2115
2116    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
2117    ///
2118    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
2119    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
2120    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
2121    ///
2122    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
2123    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
2124    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
2125    /// `~/.ssh/config` and keys apply.
2126    pub(crate) fn ssh_builtin_execute(&mut self, args: &[PerlValue]) -> PerlResult<PerlValue> {
2127        use std::process::Command;
2128        let mut cmd = Command::new("ssh");
2129        #[cfg(unix)]
2130        {
2131            use libc::geteuid;
2132            let home_for_ssh = if unsafe { geteuid() } == 0 {
2133                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
2134            } else {
2135                None
2136            };
2137            if let Some(h) = home_for_ssh {
2138                cmd.env("HOME", h);
2139            } else if std::env::var_os("HOME").is_none() {
2140                if let Some(h) = pw_home_dir_for_current_uid() {
2141                    cmd.env("HOME", h);
2142                }
2143            }
2144        }
2145        for a in args {
2146            cmd.arg(a.to_string());
2147        }
2148        match cmd.status() {
2149            Ok(s) => {
2150                self.record_child_exit_status(s);
2151                Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
2152            }
2153            Err(e) => {
2154                self.apply_io_error_to_errno(&e);
2155                Ok(PerlValue::integer(-1))
2156            }
2157        }
2158    }
2159
2160    /// Set `$@` message; numeric side is `0` if empty, else `1`.
2161    pub(crate) fn set_eval_error(&mut self, msg: String) {
2162        self.eval_error = msg;
2163        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2164        self.eval_error_value = None;
2165    }
2166
2167    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &PerlError) {
2168        self.eval_error = e.to_string();
2169        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2170        self.eval_error_value = e.die_value.clone();
2171    }
2172
2173    pub(crate) fn clear_eval_error(&mut self) {
2174        self.eval_error = String::new();
2175        self.eval_error_code = 0;
2176        self.eval_error_value = None;
2177    }
2178
2179    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
2180    fn bump_line_for_handle(&mut self, handle_key: &str) {
2181        self.last_readline_handle = handle_key.to_string();
2182        *self
2183            .handle_line_numbers
2184            .entry(handle_key.to_string())
2185            .or_insert(0) += 1;
2186    }
2187
2188    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
2189    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
2190        if name.starts_with('^') {
2191            return name.to_string();
2192        }
2193        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
2194            let pkg = self.current_package();
2195            if !pkg.is_empty() && pkg != "main" {
2196                return format!("{}::{}", pkg, name);
2197            }
2198        }
2199        name.to_string()
2200    }
2201
2202    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
2203    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
2204        if name.contains("::") {
2205            return name.to_string();
2206        }
2207        let pkg = self.current_package();
2208        if pkg.is_empty() || pkg == "main" {
2209            format!("main::{}", name)
2210        } else {
2211            format!("{}::{}", pkg, name)
2212        }
2213    }
2214
2215    /// Bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
2216    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
2217        if name.contains("::") {
2218            return name.to_string();
2219        }
2220        for (lex, our) in self
2221            .english_lexical_scalars
2222            .iter()
2223            .zip(self.our_lexical_scalars.iter())
2224            .rev()
2225        {
2226            if lex.contains(name) {
2227                if our.contains(name) {
2228                    return self.stash_scalar_name_for_package(name);
2229                }
2230                return name.to_string();
2231            }
2232        }
2233        name.to_string()
2234    }
2235
2236    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
2237    pub(crate) fn tie_execute(
2238        &mut self,
2239        target_kind: u8,
2240        target_name: &str,
2241        class_and_args: Vec<PerlValue>,
2242        line: usize,
2243    ) -> PerlResult<PerlValue> {
2244        let mut it = class_and_args.into_iter();
2245        let class = it.next().unwrap_or(PerlValue::UNDEF);
2246        let pkg = class.to_string();
2247        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
2248        let tie_ctor = match target_kind {
2249            0 => "TIESCALAR",
2250            1 => "TIEARRAY",
2251            2 => "TIEHASH",
2252            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
2253        };
2254        let tie_fn = format!("{}::{}", pkg, tie_ctor);
2255        let sub = self
2256            .subs
2257            .get(&tie_fn)
2258            .cloned()
2259            .ok_or_else(|| PerlError::runtime(format!("tie: cannot find &{}", tie_fn), line))?;
2260        let mut call_args = vec![PerlValue::string(pkg.clone())];
2261        call_args.extend(it);
2262        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
2263            Ok(v) => v,
2264            Err(FlowOrError::Flow(_)) => PerlValue::UNDEF,
2265            Err(FlowOrError::Error(e)) => return Err(e),
2266        };
2267        match target_kind {
2268            0 => {
2269                self.tied_scalars.insert(target_name.to_string(), obj);
2270            }
2271            1 => {
2272                let key = self.stash_array_name_for_package(target_name);
2273                self.tied_arrays.insert(key, obj);
2274            }
2275            2 => {
2276                self.tied_hashes.insert(target_name.to_string(), obj);
2277            }
2278            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
2279        }
2280        Ok(PerlValue::UNDEF)
2281    }
2282
2283    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
2284    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
2285        let key = format!("{}::ISA", class);
2286        self.scope
2287            .get_array(&key)
2288            .into_iter()
2289            .map(|v| v.to_string())
2290            .collect()
2291    }
2292
2293    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
2294        let p = |c: &str| self.parents_of_class(c);
2295        linearize_c3(class, &p, 0)
2296    }
2297
2298    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
2299    pub(crate) fn resolve_method_full_name(
2300        &self,
2301        invocant_class: &str,
2302        method: &str,
2303        super_mode: bool,
2304    ) -> Option<String> {
2305        let mro = self.mro_linearize(invocant_class);
2306        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
2307        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
2308        // even when running `C::meth`.
2309        let start = if super_mode {
2310            mro.iter()
2311                .position(|p| p == invocant_class)
2312                .map(|i| i + 1)
2313                // If the class string does not appear in MRO (should be rare), skip the first
2314                // entry so we still search parents before giving up.
2315                .unwrap_or(1)
2316        } else {
2317            0
2318        };
2319        for pkg in mro.iter().skip(start) {
2320            if pkg == "UNIVERSAL" {
2321                continue;
2322            }
2323            let fq = format!("{}::{}", pkg, method);
2324            if self.subs.contains_key(&fq) {
2325                return Some(fq);
2326            }
2327        }
2328        mro.iter()
2329            .skip(start)
2330            .find(|p| *p != "UNIVERSAL")
2331            .map(|pkg| format!("{}::{}", pkg, method))
2332    }
2333
2334    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
2335        if let Some(alias) = self.glob_handle_alias.get(name) {
2336            return alias.clone();
2337        }
2338        // `print $fh …` stores the handle as "$varname"; resolve it by
2339        // reading the scalar variable which holds the IO handle name.
2340        if let Some(var_name) = name.strip_prefix('$') {
2341            let val = self.scope.get_scalar(var_name);
2342            let s = val.to_string();
2343            if !s.is_empty() {
2344                return self.resolve_io_handle_name(&s);
2345            }
2346        }
2347        name.to_string()
2348    }
2349
2350    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
2351    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
2352        if name.contains("::") {
2353            name.to_string()
2354        } else {
2355            self.qualify_sub_key(name)
2356        }
2357    }
2358
2359    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
2360    pub(crate) fn copy_typeglob_slots(
2361        &mut self,
2362        lhs: &str,
2363        rhs: &str,
2364        line: usize,
2365    ) -> PerlResult<()> {
2366        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
2367        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
2368        match self.subs.get(&rhs_sub).cloned() {
2369            Some(s) => {
2370                self.subs.insert(lhs_sub, s);
2371            }
2372            None => {
2373                self.subs.remove(&lhs_sub);
2374            }
2375        }
2376        let sv = self.scope.get_scalar(rhs);
2377        self.scope
2378            .set_scalar(lhs, sv.clone())
2379            .map_err(|e| e.at_line(line))?;
2380        let lhs_an = self.stash_array_name_for_package(lhs);
2381        let rhs_an = self.stash_array_name_for_package(rhs);
2382        let av = self.scope.get_array(&rhs_an);
2383        self.scope
2384            .set_array(&lhs_an, av.clone())
2385            .map_err(|e| e.at_line(line))?;
2386        let hv = self.scope.get_hash(rhs);
2387        self.scope
2388            .set_hash(lhs, hv.clone())
2389            .map_err(|e| e.at_line(line))?;
2390        match self.glob_handle_alias.get(rhs).cloned() {
2391            Some(t) => {
2392                self.glob_handle_alias.insert(lhs.to_string(), t);
2393            }
2394            None => {
2395                self.glob_handle_alias.remove(lhs);
2396            }
2397        }
2398        Ok(())
2399    }
2400
2401    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2402    pub(crate) fn install_format_decl(
2403        &mut self,
2404        basename: &str,
2405        lines: &[String],
2406        line: usize,
2407    ) -> PerlResult<()> {
2408        let pkg = self.current_package();
2409        let key = format!("{}::{}", pkg, basename);
2410        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2411        self.format_templates.insert(key, Arc::new(tmpl));
2412        Ok(())
2413    }
2414
2415    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2416    /// Anonymous overload handlers are emitted by the parser as a synthetic
2417    /// `__overload_anon_N` SubDecl at the top of the program (registered under
2418    /// `main::`); re-bind a clone under the current package so the dispatch
2419    /// `Pkg::__overload_anon_N` lookup at runtime resolves. (PARITY-012)
2420    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2421        let pkg = self.current_package();
2422        for (_, v) in pairs {
2423            if v.starts_with("__overload_anon_") {
2424                // Synthetic anon-overload subs are emitted at the top of the
2425                // program, before any user `package N` statement, so they're
2426                // registered under the bare name (qualify_sub_key returns the
2427                // unqualified form for the `main` package). Re-bind a clone
2428                // under `Pkg::name` so the dispatch lookup `Pkg::sub_short`
2429                // resolves.
2430                let pkg_key = format!("{}::{}", pkg, v);
2431                if !self.subs.contains_key(&pkg_key) {
2432                    let src = if let Some(s) = self.subs.get(v) {
2433                        Some(s.clone())
2434                    } else {
2435                        self.subs.get(&format!("main::{}", v)).cloned()
2436                    };
2437                    if let Some(sub) = src {
2438                        self.subs.insert(pkg_key, sub);
2439                    }
2440                }
2441            }
2442        }
2443        let ent = self.overload_table.entry(pkg).or_default();
2444        for (k, v) in pairs {
2445            ent.insert(k.clone(), v.clone());
2446        }
2447    }
2448
2449    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2450    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2451    pub(crate) fn local_declare_typeglob(
2452        &mut self,
2453        lhs: &str,
2454        rhs: Option<&str>,
2455        line: usize,
2456    ) -> PerlResult<()> {
2457        let old = self.glob_handle_alias.remove(lhs);
2458        let Some(frame) = self.glob_restore_frames.last_mut() else {
2459            return Err(PerlError::runtime(
2460                "internal: no glob restore frame for local *GLOB",
2461                line,
2462            ));
2463        };
2464        frame.push((lhs.to_string(), old));
2465        if let Some(r) = rhs {
2466            self.glob_handle_alias
2467                .insert(lhs.to_string(), r.to_string());
2468        }
2469        Ok(())
2470    }
2471
2472    pub(crate) fn scope_push_hook(&mut self) {
2473        self.scope.push_frame();
2474        self.glob_restore_frames.push(Vec::new());
2475        self.special_var_restore_frames.push(Vec::new());
2476        self.english_lexical_scalars.push(HashSet::new());
2477        self.our_lexical_scalars.push(HashSet::new());
2478        self.state_bindings_stack.push(Vec::new());
2479    }
2480
2481    #[inline]
2482    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2483        if let Some(s) = self.english_lexical_scalars.last_mut() {
2484            s.insert(name.to_string());
2485        }
2486    }
2487
2488    /// Snapshot the `english_lexical_scalars` stack for parallel worker spawn (rayon
2489    /// closures need owned `Vec<HashSet<String>>` they can `clone()` per-worker).
2490    #[inline]
2491    pub(crate) fn english_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2492        self.english_lexical_scalars.clone()
2493    }
2494
2495    /// Snapshot the `our_lexical_scalars` stack — companion to
2496    /// [`Self::english_lexical_scalars_clone`].
2497    #[inline]
2498    pub(crate) fn our_lexical_scalars_clone(&self) -> Vec<HashSet<String>> {
2499        self.our_lexical_scalars.clone()
2500    }
2501
2502    /// Replace `english_lexical_scalars` wholesale (parallel-worker setup).
2503    #[inline]
2504    pub(crate) fn set_english_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2505        self.english_lexical_scalars = v;
2506    }
2507
2508    /// Replace `our_lexical_scalars` wholesale (parallel-worker setup).
2509    #[inline]
2510    pub(crate) fn set_our_lexical_scalars(&mut self, v: Vec<HashSet<String>>) {
2511        self.our_lexical_scalars = v;
2512    }
2513
2514    #[inline]
2515    fn note_our_scalar(&mut self, bare_name: &str) {
2516        if let Some(s) = self.our_lexical_scalars.last_mut() {
2517            s.insert(bare_name.to_string());
2518        }
2519    }
2520
2521    /// Public wrapper for [`Self::english_note_lexical_scalar`] — used by bytecode
2522    /// `Op::DeclareOurSync*` to register bare names so worker `tree_scalar_storage_name`
2523    /// reads rewrite to `Pkg::x`.
2524    #[inline]
2525    pub(crate) fn english_note_lexical_scalar_pub(&mut self, name: &str) {
2526        self.english_note_lexical_scalar(name);
2527    }
2528
2529    /// Public wrapper for [`Self::note_our_scalar`] — see [`Self::english_note_lexical_scalar_pub`].
2530    #[inline]
2531    pub(crate) fn note_our_scalar_pub(&mut self, bare_name: &str) {
2532        self.note_our_scalar(bare_name);
2533    }
2534
2535    pub(crate) fn scope_pop_hook(&mut self) {
2536        if !self.scope.can_pop_frame() {
2537            return;
2538        }
2539        // Execute deferred blocks in LIFO order before popping the frame.
2540        // Important: defer blocks run in the CURRENT scope (not a new frame),
2541        // so they can modify variables in the enclosing scope.
2542        let defers = self.scope.take_defers();
2543        for coderef in defers {
2544            if let Some(sub) = coderef.as_code_ref() {
2545                // Execute the defer block body directly in the current scope,
2546                // without creating a new frame or restoring closure captures.
2547                // This allows defer { $x = 100 } to modify the outer $x.
2548                let saved_wa = self.wantarray_kind;
2549                self.wantarray_kind = WantarrayCtx::Void;
2550                let _ = self.exec_block_no_scope(&sub.body);
2551                self.wantarray_kind = saved_wa;
2552            }
2553        }
2554        // Save state variable values back before popping the frame
2555        if let Some(bindings) = self.state_bindings_stack.pop() {
2556            for (var_name, state_key) in &bindings {
2557                let val = self.scope.get_scalar(var_name).clone();
2558                self.state_vars.insert(state_key.clone(), val);
2559            }
2560        }
2561        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2562        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2563        if let Some(entries) = self.special_var_restore_frames.pop() {
2564            for (name, old) in entries.into_iter().rev() {
2565                let _ = self.set_special_var(&name, &old);
2566            }
2567        }
2568        if let Some(entries) = self.glob_restore_frames.pop() {
2569            for (name, old) in entries.into_iter().rev() {
2570                match old {
2571                    Some(s) => {
2572                        self.glob_handle_alias.insert(name, s);
2573                    }
2574                    None => {
2575                        self.glob_handle_alias.remove(&name);
2576                    }
2577                }
2578            }
2579        }
2580        self.scope.pop_frame();
2581        let _ = self.english_lexical_scalars.pop();
2582        let _ = self.our_lexical_scalars.pop();
2583    }
2584
2585    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2586    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2587    #[inline]
2588    pub(crate) fn enable_parallel_guard(&mut self) {
2589        self.scope.set_parallel_guard(true);
2590    }
2591
2592    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues after VM compilation.
2593    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2594        self.begin_blocks.clear();
2595        self.unit_check_blocks.clear();
2596        self.check_blocks.clear();
2597        self.init_blocks.clear();
2598        self.end_blocks.clear();
2599    }
2600
2601    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2602    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2603    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2604    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2605    /// there because it only calls [`Scope::pop_frame`].
2606    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2607        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2608            self.scope_pop_hook();
2609        }
2610    }
2611
2612    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2613    ///
2614    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2615    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2616    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2617    /// workers that call `perl_signal::poll`).
2618    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> PerlResult<()> {
2619        self.touch_env_hash("SIG");
2620        let v = self.scope.get_hash_element("SIG", sig);
2621        if v.is_undef() {
2622            return Self::default_sig_action(sig);
2623        }
2624        if let Some(s) = v.as_str() {
2625            if s == "IGNORE" {
2626                return Ok(());
2627            }
2628            if s == "DEFAULT" {
2629                return Self::default_sig_action(sig);
2630            }
2631        }
2632        if let Some(sub) = v.as_code_ref() {
2633            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2634                Ok(_) => Ok(()),
2635                Err(FlowOrError::Flow(_)) => Ok(()),
2636                Err(FlowOrError::Error(e)) => Err(e),
2637            }
2638        } else {
2639            Self::default_sig_action(sig)
2640        }
2641    }
2642
2643    /// Dispatch `$SIG{__WARN__}` if a coderef is installed; fall back to stderr.
2644    /// Recursion is guarded by temporarily clearing the slot during dispatch so
2645    /// a `__WARN__` handler that itself calls `warn` does not loop.
2646    pub(crate) fn fire_pseudosig_warn(&mut self, msg: &str, line: usize) -> PerlResult<()> {
2647        self.touch_env_hash("SIG");
2648        let slot = self.scope.get_hash_element("SIG", "__WARN__");
2649        if let Some(sub) = slot.as_code_ref() {
2650            let prev = slot;
2651            let _ = self
2652                .scope
2653                .set_hash_element("SIG", "__WARN__", PerlValue::UNDEF);
2654            let arg = PerlValue::string(msg.to_string());
2655            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2656            let _ = self.scope.set_hash_element("SIG", "__WARN__", prev);
2657            return match r {
2658                Ok(_) => Ok(()),
2659                Err(FlowOrError::Flow(_)) => Ok(()),
2660                Err(FlowOrError::Error(e)) => Err(e),
2661            };
2662        }
2663        eprint!("{}", msg);
2664        Ok(())
2665    }
2666
2667    /// Dispatch `$SIG{__DIE__}` if a coderef is installed. Perl semantics:
2668    /// the handler runs even when the die is going to be caught by an `eval`,
2669    /// and the die still propagates afterwards. If the handler itself dies,
2670    /// that error replaces the original. Recursion is guarded by temporarily
2671    /// clearing the slot during dispatch.
2672    pub(crate) fn fire_pseudosig_die(&mut self, msg: &str, line: usize) -> PerlResult<()> {
2673        self.touch_env_hash("SIG");
2674        let slot = self.scope.get_hash_element("SIG", "__DIE__");
2675        if let Some(sub) = slot.as_code_ref() {
2676            let prev = slot;
2677            let _ = self
2678                .scope
2679                .set_hash_element("SIG", "__DIE__", PerlValue::UNDEF);
2680            let arg = PerlValue::string(msg.to_string());
2681            let r = self.call_sub(&sub, vec![arg], WantarrayCtx::Void, line);
2682            let _ = self.scope.set_hash_element("SIG", "__DIE__", prev);
2683            return match r {
2684                Ok(_) => Ok(()),
2685                Err(FlowOrError::Flow(_)) => Ok(()),
2686                Err(FlowOrError::Error(e)) => Err(e),
2687            };
2688        }
2689        Ok(())
2690    }
2691
2692    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2693    #[inline]
2694    fn default_sig_action(sig: &str) -> PerlResult<()> {
2695        match sig {
2696            // 128 + signal number (common shell convention)
2697            "INT" => std::process::exit(130),
2698            "TERM" => std::process::exit(143),
2699            "ALRM" => std::process::exit(142),
2700            // Default for SIGCHLD is ignore
2701            "CHLD" => Ok(()),
2702            _ => Ok(()),
2703        }
2704    }
2705
2706    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2707    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2708    pub fn materialize_env_if_needed(&mut self) {
2709        if self.env_materialized {
2710            return;
2711        }
2712        self.env = std::env::vars()
2713            .map(|(k, v)| (k, PerlValue::string(v)))
2714            .collect();
2715        self.scope
2716            .set_hash("ENV", self.env.clone())
2717            .expect("set %ENV");
2718        self.env_materialized = true;
2719    }
2720
2721    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2722    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2723        self.materialize_env_if_needed();
2724        if let Some(x) = self.log_level_override {
2725            return x;
2726        }
2727        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2728        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2729    }
2730
2731    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2732    pub(crate) fn no_color_effective(&mut self) -> bool {
2733        self.materialize_env_if_needed();
2734        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2735        if v.is_undef() {
2736            return false;
2737        }
2738        !v.to_string().is_empty()
2739    }
2740
2741    #[inline]
2742    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2743        // `%main::ENV` ≡ `%ENV`, `%main::parameters` ≡ `%parameters`,
2744        // `%main::a` ≡ `%a`, etc. Strip the `main::` qualifier so the
2745        // lazy-materialize / reflection-hash branches fire on the
2746        // canonical bare name. Without this, `exists $main::ENV{PATH}`
2747        // returns 0 on a fresh interpreter because ENV never gets
2748        // materialized.
2749        let hash_name: &str = crate::scope::strip_main_prefix(hash_name).unwrap_or(hash_name);
2750        if hash_name == "ENV" {
2751            self.materialize_env_if_needed();
2752        } else if hash_name == "parameters"
2753            && !crate::compat_mode()
2754            && !self.scope.has_lexical_hash("parameters")
2755        {
2756            // `%parameters` (zsh `$parameters` analogue) — rebuild on every
2757            // read so it always reflects current scope state. Frozen install,
2758            // so user code can read but not assign into it. Stryke-only;
2759            // `--compat` skips the auto-refresh so Perl 5 scripts that use
2760            // `%parameters` for their own purposes are unaffected.
2761            self.ensure_reflection_hashes();
2762            self.refresh_parameters_hash();
2763        } else if hash_name.ends_with("::") && hash_name.len() > 2 {
2764            // `%main::` / `%Foo::` — repopulate from current symbol table on
2765            // every read so newly-defined subs / `our` vars become visible
2766            // without an explicit `refresh_stashes()` call. Cheap: walks
2767            // `subs` keys + frame name lists, no value cloning.
2768            self.refresh_package_stashes();
2769        } else if !self.reflection_hashes_ready && !self.scope.has_lexical_hash(hash_name) {
2770            match hash_name {
2771                "b"
2772                | "pc"
2773                | "e"
2774                | "a"
2775                | "d"
2776                | "c"
2777                | "p"
2778                | "k"
2779                | "all"
2780                | "stryke::builtins"
2781                | "stryke::perl_compats"
2782                | "stryke::extensions"
2783                | "stryke::aliases"
2784                | "stryke::descriptions"
2785                | "stryke::categories"
2786                | "stryke::primaries"
2787                | "stryke::keywords"
2788                | "stryke::all" => {
2789                    self.ensure_reflection_hashes();
2790                }
2791                _ => {}
2792            }
2793        }
2794    }
2795
2796    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2797    pub(crate) fn exists_arrow_hash_element(
2798        &self,
2799        container: PerlValue,
2800        key: &str,
2801        line: usize,
2802    ) -> PerlResult<bool> {
2803        if let Some(r) = container.as_hash_ref() {
2804            return Ok(r.read().contains_key(key));
2805        }
2806        if let Some(b) = container.as_blessed_ref() {
2807            let data = b.data.read();
2808            if let Some(r) = data.as_hash_ref() {
2809                return Ok(r.read().contains_key(key));
2810            }
2811            if let Some(hm) = data.as_hash_map() {
2812                return Ok(hm.contains_key(key));
2813            }
2814            return Err(PerlError::runtime(
2815                "exists argument is not a HASH reference",
2816                line,
2817            ));
2818        }
2819        // `exists $h{x}{y}` when `$h{x}` is undef OR a non-hash scalar: Perl
2820        // returns false for the deepest test without erroring. Stryke
2821        // previously errored on the intermediate. Match Perl. (BUG-009)
2822        let _ = line;
2823        Ok(false)
2824    }
2825
2826    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
2827    pub(crate) fn delete_arrow_hash_element(
2828        &self,
2829        container: PerlValue,
2830        key: &str,
2831        line: usize,
2832    ) -> PerlResult<PerlValue> {
2833        if let Some(r) = container.as_hash_ref() {
2834            return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2835        }
2836        if let Some(b) = container.as_blessed_ref() {
2837            let mut data = b.data.write();
2838            if let Some(r) = data.as_hash_ref() {
2839                return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2840            }
2841            if let Some(mut map) = data.as_hash_map() {
2842                let v = map.shift_remove(key).unwrap_or(PerlValue::UNDEF);
2843                *data = PerlValue::hash(map);
2844                return Ok(v);
2845            }
2846            return Err(PerlError::runtime(
2847                "delete argument is not a HASH reference",
2848                line,
2849            ));
2850        }
2851        Err(PerlError::runtime(
2852            "delete argument is not a HASH reference",
2853            line,
2854        ))
2855    }
2856
2857    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
2858    pub(crate) fn exists_arrow_array_element(
2859        &self,
2860        container: PerlValue,
2861        idx: i64,
2862        line: usize,
2863    ) -> PerlResult<bool> {
2864        if let Some(a) = container.as_array_ref() {
2865            let arr = a.read();
2866            let i = if idx < 0 {
2867                (arr.len() as i64 + idx) as usize
2868            } else {
2869                idx as usize
2870            };
2871            return Ok(i < arr.len());
2872        }
2873        // `exists $a[5][0]` when `$a[5]` is missing OR a non-array scalar:
2874        // Perl returns false at the deepest test without erroring. (BUG-009)
2875        let _ = line;
2876        Ok(false)
2877    }
2878
2879    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
2880    pub(crate) fn delete_arrow_array_element(
2881        &self,
2882        container: PerlValue,
2883        idx: i64,
2884        line: usize,
2885    ) -> PerlResult<PerlValue> {
2886        if let Some(a) = container.as_array_ref() {
2887            let mut arr = a.write();
2888            let i = if idx < 0 {
2889                (arr.len() as i64 + idx) as usize
2890            } else {
2891                idx as usize
2892            };
2893            if i >= arr.len() {
2894                return Ok(PerlValue::UNDEF);
2895            }
2896            let old = arr.get(i).cloned().unwrap_or(PerlValue::UNDEF);
2897            arr[i] = PerlValue::UNDEF;
2898            return Ok(old);
2899        }
2900        Err(PerlError::runtime(
2901            "delete argument is not an ARRAY reference",
2902            line,
2903        ))
2904    }
2905
2906    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
2907    pub(crate) fn inc_directories(&self) -> Vec<String> {
2908        let mut v: Vec<String> = self
2909            .scope
2910            .get_array("INC")
2911            .into_iter()
2912            .map(|x| x.to_string())
2913            .filter(|s| !s.is_empty())
2914            .collect();
2915        if v.is_empty() {
2916            v.push(".".to_string());
2917        }
2918        v
2919    }
2920
2921    #[inline]
2922    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
2923        matches!(
2924            name,
2925            "_" | "0"
2926                | "!"
2927                | "@"
2928                | "/"
2929                | "\\"
2930                | ","
2931                | "."
2932                | "__PACKAGE__"
2933                | "$$"
2934                | "|"
2935                | "?"
2936                | "\""
2937                | "&"
2938                | "`"
2939                | "'"
2940                | "+"
2941                | "<"
2942                | ">"
2943                | "("
2944                | ")"
2945                | "]"
2946                | ";"
2947                | "ARGV"
2948                | "%"
2949                | "="
2950                | "-"
2951                | ":"
2952                | "*"
2953                | "INC"
2954                // sort/reduce comparator slots — predefined package globals
2955                // ($main::a, $main::b). Perl exempts them globally, not just
2956                // inside sort blocks, so any reference compiles cleanly.
2957                | "a"
2958                | "b"
2959        ) || name.chars().all(|c| c.is_ascii_digit())
2960            || name.starts_with('^')
2961            || (name.starts_with('#') && name.len() > 1)
2962            // Stryke implicit closure-param slots (`$_0`, `$_1`, …, `$_99`).
2963            // These are auto-bound inside any block that takes positional
2964            // arguments (sort comparators, reduce blocks, sub bodies, map/
2965            // grep blocks). Treat them like the digit-only match groups —
2966            // exempt globally so a strict-vars check inside a `preduce {
2967            // $_0 + $_1 }` block doesn't reject them as undeclared.
2968            || (name.starts_with('_')
2969                && name.len() > 1
2970                && name[1..].chars().all(|c| c.is_ascii_digit()))
2971    }
2972
2973    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2974        if !self.strict_vars
2975            || Self::strict_scalar_exempt(name)
2976            || name.contains("::")
2977            || self.scope.scalar_binding_exists(name)
2978        {
2979            return Ok(());
2980        }
2981        Err(PerlError::runtime(
2982            format!(
2983                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
2984                name, name
2985            ),
2986            line,
2987        )
2988        .into())
2989    }
2990
2991    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2992        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
2993            return Ok(());
2994        }
2995        Err(PerlError::runtime(
2996            format!(
2997                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
2998                name, name
2999            ),
3000            line,
3001        )
3002        .into())
3003    }
3004
3005    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
3006        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
3007        if !self.strict_vars
3008            || name.contains("::")
3009            || self.scope.hash_binding_exists(name)
3010            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
3011        {
3012            return Ok(());
3013        }
3014        Err(PerlError::runtime(
3015            format!(
3016                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
3017                name, name
3018            ),
3019            line,
3020        )
3021        .into())
3022    }
3023
3024    fn looks_like_version_only(spec: &str) -> bool {
3025        let t = spec.trim();
3026        !t.is_empty()
3027            && !t.contains('/')
3028            && !t.contains('\\')
3029            && !t.contains("::")
3030            && t.chars()
3031                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
3032            && t.chars().any(|c| c.is_ascii_digit())
3033    }
3034
3035    fn module_spec_to_relpath(spec: &str) -> String {
3036        let t = spec.trim();
3037        if t.contains("::") {
3038            format!("{}.pm", t.replace("::", "/"))
3039        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
3040            t.replace('\\', "/")
3041        } else {
3042            format!("{}.pm", t)
3043        }
3044    }
3045
3046    /// Lockfile-driven module resolution (RFC §"Module Resolution"). Walks up from
3047    /// `cwd` for `stryke.toml`, then asks [`crate::pkg::commands::resolve_module`]
3048    /// to find the module either in `lib/` or in the lockfile-pinned store. The
3049    /// `relpath` arg is the `@INC`-style path (`Foo/Bar.pm`) used elsewhere in
3050    /// `require`; it is converted to a logical name (`Foo::Bar`) for the resolver.
3051    /// Both `.pm` and `.stk` variants are tried — stryke source uses `.stk`.
3052    fn try_resolve_via_lockfile(relpath: &str) -> Option<std::path::PathBuf> {
3053        let cwd = std::env::current_dir().ok()?;
3054        let project_root = crate::pkg::commands::find_project_root(&cwd)?;
3055
3056        // Convert "Foo/Bar.pm" → "Foo::Bar". Drop the trailing extension so
3057        // `resolve_module` (which appends `.stk`) builds the right path.
3058        let stem = relpath
3059            .strip_suffix(".pm")
3060            .or_else(|| relpath.strip_suffix(".pl"))
3061            .or_else(|| relpath.strip_suffix(".stk"))
3062            .unwrap_or(relpath);
3063        let logical = stem.replace('/', "::");
3064
3065        crate::pkg::commands::resolve_module(&project_root, &logical).unwrap_or_default()
3066    }
3067
3068    /// `sub name` in `package P` → stash key `P::name`. `sub Q::name { }` is already fully
3069    /// qualified — do not prepend the current package. Unqualified names in `main` are stored
3070    /// **bare** (`name`), matching the compiler's `Op::Call` interning so the VM's
3071    /// `sub_for_closure_restore` lookup hits in one step.
3072    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
3073        if name.contains("::") {
3074            return name.to_string();
3075        }
3076        let pkg = self.current_package();
3077        if pkg.is_empty() || pkg == "main" {
3078            name.to_string()
3079        } else {
3080            format!("{}::{}", pkg, name)
3081        }
3082    }
3083
3084    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
3085    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
3086        let mut msg = format!("Undefined subroutine &{}", name);
3087        if self.strict_subs {
3088            msg.push_str(
3089                " (strict subs: declare the sub or use a fully qualified name before calling)",
3090            );
3091        }
3092        msg
3093    }
3094
3095    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
3096    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
3097        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
3098        if self.strict_subs {
3099            msg.push_str(
3100                " (strict subs: declare the sub or use a fully qualified name before calling)",
3101            );
3102        }
3103        msg
3104    }
3105
3106    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
3107    fn import_alias_key(&self, short: &str) -> String {
3108        self.qualify_sub_key(short)
3109    }
3110
3111    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
3112    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
3113        if imports.len() == 1 {
3114            match &imports[0].kind {
3115                ExprKind::QW(ws) => return ws.is_empty(),
3116                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
3117                ExprKind::List(xs) => return xs.is_empty(),
3118                _ => {}
3119            }
3120        }
3121        false
3122    }
3123
3124    /// After `require`, copy `Module::export` → caller stash per `use` list.
3125    fn apply_module_import(
3126        &mut self,
3127        module: &str,
3128        imports: &[Expr],
3129        line: usize,
3130    ) -> PerlResult<()> {
3131        if imports.is_empty() {
3132            return self.import_all_from_module(module, line);
3133        }
3134        if Self::is_explicit_empty_import_list(imports) {
3135            return Ok(());
3136        }
3137        let names = Self::pragma_import_strings(imports, line)?;
3138        if names.is_empty() {
3139            return Ok(());
3140        }
3141        for name in names {
3142            self.import_one_symbol(module, &name, line)?;
3143        }
3144        Ok(())
3145    }
3146
3147    fn import_all_from_module(&mut self, module: &str, line: usize) -> PerlResult<()> {
3148        if let Some(lists) = self.module_export_lists.get(module) {
3149            let export: Vec<String> = lists.export.clone();
3150            for short in export {
3151                self.import_named_sub(module, &short, line)?;
3152            }
3153            return Ok(());
3154        }
3155        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
3156        let prefix = format!("{}::", module);
3157        let keys: Vec<String> = self
3158            .subs
3159            .keys()
3160            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
3161            .cloned()
3162            .collect();
3163        for k in keys {
3164            let short = k[prefix.len()..].to_string();
3165            if let Some(sub) = self.subs.get(&k).cloned() {
3166                let alias = self.import_alias_key(&short);
3167                self.subs.insert(alias, sub);
3168            }
3169        }
3170        Ok(())
3171    }
3172
3173    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
3174    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> PerlResult<()> {
3175        let qual = format!("{}::{}", module, short);
3176        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
3177            PerlError::runtime(
3178                format!(
3179                    "`{}` is not defined in module `{}` (expected `{}`)",
3180                    short, module, qual
3181                ),
3182                line,
3183            )
3184        })?;
3185        let alias = self.import_alias_key(short);
3186        self.subs.insert(alias, sub);
3187        Ok(())
3188    }
3189
3190    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> PerlResult<()> {
3191        if let Some(lists) = self.module_export_lists.get(module) {
3192            let allowed: HashSet<&str> = lists
3193                .export
3194                .iter()
3195                .map(|s| s.as_str())
3196                .chain(lists.export_ok.iter().map(|s| s.as_str()))
3197                .collect();
3198            if !allowed.contains(export) {
3199                return Err(PerlError::runtime(
3200                    format!(
3201                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
3202                        export, module
3203                    ),
3204                    line,
3205                ));
3206            }
3207        }
3208        self.import_named_sub(module, export, line)
3209    }
3210
3211    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
3212    fn record_exporter_our_array_name(&mut self, name: &str, items: &[PerlValue]) {
3213        if name != "EXPORT" && name != "EXPORT_OK" {
3214            return;
3215        }
3216        let pkg = self.current_package();
3217        if pkg.is_empty() || pkg == "main" {
3218            return;
3219        }
3220        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
3221        let ent = self.module_export_lists.entry(pkg).or_default();
3222        if name == "EXPORT" {
3223            ent.export = names;
3224        } else {
3225            ent.export_ok = names;
3226        }
3227    }
3228
3229    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
3230    /// Refresh [`PerlSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
3231    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
3232    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
3233        let key = self.qualify_sub_key(name);
3234        let Some(sub) = self.subs.get(&key).cloned() else {
3235            return;
3236        };
3237        let captured = self.scope.capture();
3238        let closure_env = if captured.is_empty() {
3239            None
3240        } else {
3241            Some(captured)
3242        };
3243        let mut new_sub = (*sub).clone();
3244        new_sub.closure_env = closure_env;
3245        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
3246        self.subs.insert(key, Arc::new(new_sub));
3247    }
3248
3249    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<PerlSub>> {
3250        if let Some(s) = self.subs.get(name) {
3251            return Some(s.clone());
3252        }
3253        if !name.contains("::") {
3254            // Non-`main` packages store subs at `Pkg::name`; resolve bare callers there.
3255            let pkg = self.current_package();
3256            if !pkg.is_empty() && pkg != "main" {
3257                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
3258                q.push_str(&pkg);
3259                q.push_str("::");
3260                q.push_str(name);
3261                return self.subs.get(&q).cloned();
3262            }
3263            return None;
3264        }
3265        // `\&main::greet` / `defined &main::greet`: subs in `main` are stored bare so the
3266        // compiler's `Op::Call("greet", ...)` and the runtime stash lookup share a key.
3267        // Strip the `main::` qualifier and try the bare form so explicit qualified callers
3268        // still resolve to the same sub.
3269        if let Some(rest) = name.strip_prefix("main::") {
3270            if !rest.contains("::") {
3271                return self.subs.get(rest).cloned();
3272            }
3273        }
3274        None
3275    }
3276
3277    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
3278    /// before calling `import`).
3279    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
3280        if let Some(first) = imports.first() {
3281            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
3282                return &imports[1..];
3283            }
3284        }
3285        imports
3286    }
3287
3288    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
3289    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> PerlResult<Vec<String>> {
3290        let mut out = Vec::new();
3291        for e in imports {
3292            match &e.kind {
3293                ExprKind::String(s) => out.push(s.clone()),
3294                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
3295                ExprKind::Integer(n) => out.push(n.to_string()),
3296                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
3297                // a single interpolated variable.  Reconstruct the sigil+name form.
3298                ExprKind::InterpolatedString(parts) => {
3299                    let mut s = String::new();
3300                    for p in parts {
3301                        match p {
3302                            StringPart::Literal(l) => s.push_str(l),
3303                            StringPart::ScalarVar(v) => {
3304                                s.push('$');
3305                                s.push_str(v);
3306                            }
3307                            StringPart::ArrayVar(v) => {
3308                                s.push('@');
3309                                s.push_str(v);
3310                            }
3311                            _ => {
3312                                return Err(PerlError::runtime(
3313                                    "pragma import must be a compile-time string, qw(), or integer",
3314                                    e.line.max(default_line),
3315                                ));
3316                            }
3317                        }
3318                    }
3319                    out.push(s);
3320                }
3321                _ => {
3322                    return Err(PerlError::runtime(
3323                        "pragma import must be a compile-time string, qw(), or integer",
3324                        e.line.max(default_line),
3325                    ));
3326                }
3327            }
3328        }
3329        Ok(out)
3330    }
3331
3332    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3333        if imports.is_empty() {
3334            self.strict_refs = true;
3335            self.strict_subs = true;
3336            self.strict_vars = true;
3337            return Ok(());
3338        }
3339        let names = Self::pragma_import_strings(imports, line)?;
3340        for name in names {
3341            match name.as_str() {
3342                "refs" => self.strict_refs = true,
3343                "subs" => self.strict_subs = true,
3344                "vars" => self.strict_vars = true,
3345                _ => {
3346                    return Err(PerlError::runtime(
3347                        format!("Unknown strict mode `{}`", name),
3348                        line,
3349                    ));
3350                }
3351            }
3352        }
3353        Ok(())
3354    }
3355
3356    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3357        if imports.is_empty() {
3358            self.strict_refs = false;
3359            self.strict_subs = false;
3360            self.strict_vars = false;
3361            return Ok(());
3362        }
3363        let names = Self::pragma_import_strings(imports, line)?;
3364        for name in names {
3365            match name.as_str() {
3366                "refs" => self.strict_refs = false,
3367                "subs" => self.strict_subs = false,
3368                "vars" => self.strict_vars = false,
3369                _ => {
3370                    return Err(PerlError::runtime(
3371                        format!("Unknown strict mode `{}`", name),
3372                        line,
3373                    ));
3374                }
3375            }
3376        }
3377        Ok(())
3378    }
3379
3380    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3381        let items = Self::pragma_import_strings(imports, line)?;
3382        if items.is_empty() {
3383            return Err(PerlError::runtime(
3384                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
3385                line,
3386            ));
3387        }
3388        for item in items {
3389            let s = item.trim();
3390            if let Some(rest) = s.strip_prefix(':') {
3391                self.apply_feature_bundle(rest, line)?;
3392            } else {
3393                self.apply_feature_name(s, true, line)?;
3394            }
3395        }
3396        Ok(())
3397    }
3398
3399    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3400        if imports.is_empty() {
3401            self.feature_bits = 0;
3402            return Ok(());
3403        }
3404        let items = Self::pragma_import_strings(imports, line)?;
3405        for item in items {
3406            let s = item.trim();
3407            if let Some(rest) = s.strip_prefix(':') {
3408                self.clear_feature_bundle(rest);
3409            } else {
3410                self.apply_feature_name(s, false, line)?;
3411            }
3412        }
3413        Ok(())
3414    }
3415
3416    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> PerlResult<()> {
3417        let key = v.trim();
3418        match key {
3419            "5.10" | "5.010" | "5.10.0" => {
3420                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3421            }
3422            "5.12" | "5.012" | "5.12.0" => {
3423                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3424            }
3425            _ => {
3426                return Err(PerlError::runtime(
3427                    format!("unsupported feature bundle :{}", key),
3428                    line,
3429                ));
3430            }
3431        }
3432        Ok(())
3433    }
3434
3435    fn clear_feature_bundle(&mut self, v: &str) {
3436        let key = v.trim();
3437        if matches!(
3438            key,
3439            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
3440        ) {
3441            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
3442        }
3443    }
3444
3445    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> PerlResult<()> {
3446        let bit = match name {
3447            "say" => FEAT_SAY,
3448            "state" => FEAT_STATE,
3449            "switch" => FEAT_SWITCH,
3450            "unicode_strings" => FEAT_UNICODE_STRINGS,
3451            // Features that stryke accepts as known but tracks no separate bit for —
3452            // either always-on, always-off, or syntactic sugar already enabled.
3453            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
3454            "postderef"
3455            | "postderef_qq"
3456            | "evalbytes"
3457            | "current_sub"
3458            | "fc"
3459            | "lexical_subs"
3460            | "signatures"
3461            | "refaliasing"
3462            | "bitwise"
3463            | "isa"
3464            | "indirect"
3465            | "multidimensional"
3466            | "bareword_filehandles"
3467            | "try"
3468            | "defer"
3469            | "extra_paired_delimiters"
3470            | "module_true"
3471            | "class"
3472            | "array_base" => return Ok(()),
3473            _ => {
3474                return Err(PerlError::runtime(
3475                    format!("unknown feature `{}`", name),
3476                    line,
3477                ));
3478            }
3479        };
3480        if enable {
3481            self.feature_bits |= bit;
3482        } else {
3483            self.feature_bits &= !bit;
3484        }
3485        Ok(())
3486    }
3487
3488    /// `require EXPR` — load once, record `%INC`, return `1` on success.
3489    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> PerlResult<PerlValue> {
3490        let t = spec.trim();
3491        if t.is_empty() {
3492            return Err(PerlError::runtime("require: empty argument", line));
3493        }
3494        match t {
3495            "strict" => {
3496                self.apply_use_strict(&[], line)?;
3497                return Ok(PerlValue::integer(1));
3498            }
3499            "utf8" => {
3500                self.utf8_pragma = true;
3501                return Ok(PerlValue::integer(1));
3502            }
3503            "feature" | "v5" => {
3504                return Ok(PerlValue::integer(1));
3505            }
3506            "warnings" => {
3507                self.warnings = true;
3508                return Ok(PerlValue::integer(1));
3509            }
3510            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
3511                return Ok(PerlValue::integer(1));
3512            }
3513            _ => {}
3514        }
3515        let p = Path::new(t);
3516        if p.is_absolute() {
3517            return self.require_absolute_path(p, line);
3518        }
3519        if t.starts_with("./") || t.starts_with("../") {
3520            return self.require_relative_path(p, line);
3521        }
3522        if Self::looks_like_version_only(t) {
3523            return Ok(PerlValue::integer(1));
3524        }
3525        let relpath = Self::module_spec_to_relpath(t);
3526        self.require_from_inc(&relpath, line)
3527    }
3528
3529    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
3530    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> PerlResult<()> {
3531        let v = self.scope.get_hash_element("^HOOK", key);
3532        if v.is_undef() {
3533            return Ok(());
3534        }
3535        let Some(sub) = v.as_code_ref() else {
3536            return Ok(());
3537        };
3538        let r = self.call_sub(
3539            sub.as_ref(),
3540            vec![PerlValue::string(path.to_string())],
3541            WantarrayCtx::Scalar,
3542            line,
3543        );
3544        match r {
3545            Ok(_) => Ok(()),
3546            Err(FlowOrError::Error(e)) => Err(e),
3547            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
3548            Err(FlowOrError::Flow(other)) => Err(PerlError::runtime(
3549                format!(
3550                    "require hook {:?} returned unexpected control flow: {:?}",
3551                    key, other
3552                ),
3553                line,
3554            )),
3555        }
3556    }
3557
3558    fn require_absolute_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3559        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
3560        let key = canon.to_string_lossy().into_owned();
3561        if self.scope.exists_hash_element("INC", &key) {
3562            return Ok(PerlValue::integer(1));
3563        }
3564        self.invoke_require_hook("require__before", &key, line)?;
3565        let code = read_file_text_perl_compat(&canon).map_err(|e| {
3566            PerlError::runtime(
3567                format!("Can't open {} for reading: {}", canon.display(), e),
3568                line,
3569            )
3570        })?;
3571        let code = crate::data_section::strip_perl_end_marker(&code);
3572        self.scope
3573            .set_hash_element("INC", &key, PerlValue::string(key.clone()))?;
3574        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3575        let r = crate::parse_and_run_module_in_file(code, self, &key);
3576        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3577        r?;
3578        self.invoke_require_hook("require__after", &key, line)?;
3579        Ok(PerlValue::integer(1))
3580    }
3581
3582    fn require_relative_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3583        if !path.exists() {
3584            return Err(PerlError::runtime(
3585                format!(
3586                    "Can't locate {} (relative path does not exist)",
3587                    path.display()
3588                ),
3589                line,
3590            ));
3591        }
3592        self.require_absolute_path(path, line)
3593    }
3594
3595    fn require_from_inc(&mut self, relpath: &str, line: usize) -> PerlResult<PerlValue> {
3596        if self.scope.exists_hash_element("INC", relpath) {
3597            return Ok(PerlValue::integer(1));
3598        }
3599        self.invoke_require_hook("require__before", relpath, line)?;
3600
3601        // Lockfile-driven module resolution. When the cwd is inside a stryke
3602        // project (`stryke.toml` reachable), `use Foo::Bar` first looks at
3603        // `lib/Foo/Bar.stk` and then at lockfile-pinned store entries before
3604        // falling through to `@INC`. See docs/PACKAGE_REGISTRY.md §"Module
3605        // Resolution".
3606        if let Some(found) = Self::try_resolve_via_lockfile(relpath) {
3607            let code = read_file_text_perl_compat(&found).map_err(|e| {
3608                PerlError::runtime(
3609                    format!("Can't open {} for reading: {}", found.display(), e),
3610                    line,
3611                )
3612            })?;
3613            let code = crate::data_section::strip_perl_end_marker(&code);
3614            let abs = found.canonicalize().unwrap_or(found);
3615            let abs_s = abs.to_string_lossy().into_owned();
3616            self.scope
3617                .set_hash_element("INC", relpath, PerlValue::string(abs_s.clone()))?;
3618            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3619            let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3620            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3621            r?;
3622            self.invoke_require_hook("require__after", relpath, line)?;
3623            return Ok(PerlValue::integer(1));
3624        }
3625
3626        // Check virtual modules first (AOT bundles).
3627        if let Some(code) = self.virtual_modules.get(relpath).cloned() {
3628            let code = crate::data_section::strip_perl_end_marker(&code);
3629            self.scope.set_hash_element(
3630                "INC",
3631                relpath,
3632                PerlValue::string(format!("(virtual)/{}", relpath)),
3633            )?;
3634            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3635            let r = crate::parse_and_run_module_in_file(code, self, relpath);
3636            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3637            r?;
3638            self.invoke_require_hook("require__after", relpath, line)?;
3639            return Ok(PerlValue::integer(1));
3640        }
3641
3642        for dir in self.inc_directories() {
3643            let full = Path::new(&dir).join(relpath);
3644            if full.is_file() {
3645                let code = read_file_text_perl_compat(&full).map_err(|e| {
3646                    PerlError::runtime(
3647                        format!("Can't open {} for reading: {}", full.display(), e),
3648                        line,
3649                    )
3650                })?;
3651                let code = crate::data_section::strip_perl_end_marker(&code);
3652                let abs = full.canonicalize().unwrap_or(full);
3653                let abs_s = abs.to_string_lossy().into_owned();
3654                self.scope
3655                    .set_hash_element("INC", relpath, PerlValue::string(abs_s.clone()))?;
3656                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3657                let r = crate::parse_and_run_module_in_file(code, self, &abs_s);
3658                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3659                r?;
3660                self.invoke_require_hook("require__after", relpath, line)?;
3661                return Ok(PerlValue::integer(1));
3662            }
3663        }
3664        Err(PerlError::runtime(
3665            format!(
3666                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
3667                relpath
3668            ),
3669            line,
3670        ))
3671    }
3672
3673    /// Register a virtual module (for AOT bundles). Path should be relative like "lib/foo.stk".
3674    pub fn register_virtual_module(&mut self, path: String, source: String) {
3675        self.virtual_modules.insert(path, source);
3676    }
3677
3678    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
3679    pub(crate) fn exec_use_stmt(
3680        &mut self,
3681        module: &str,
3682        imports: &[Expr],
3683        line: usize,
3684    ) -> PerlResult<()> {
3685        match module {
3686            "strict" => self.apply_use_strict(imports, line),
3687            "utf8" => {
3688                if !imports.is_empty() {
3689                    return Err(PerlError::runtime("use utf8 takes no arguments", line));
3690                }
3691                self.utf8_pragma = true;
3692                Ok(())
3693            }
3694            "feature" => self.apply_use_feature(imports, line),
3695            "v5" => Ok(()),
3696            "warnings" => {
3697                self.warnings = true;
3698                Ok(())
3699            }
3700            "English" => {
3701                self.english_enabled = true;
3702                let args = Self::pragma_import_strings(imports, line)?;
3703                let no_match = args.iter().any(|a| a == "-no_match_vars");
3704                // Once match vars are exported (use English without -no_match_vars),
3705                // they stay available for the rest of the program — Perl exports them
3706                // into the caller's namespace and later pragmas cannot un-export them.
3707                if !no_match {
3708                    self.english_match_vars_ever_enabled = true;
3709                }
3710                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
3711                Ok(())
3712            }
3713            "Env" => self.apply_use_env(imports, line),
3714            "open" => self.apply_use_open(imports, line),
3715            "constant" => self.apply_use_constant(imports, line),
3716            "bigint" | "bignum" | "bigrat" => {
3717                // Activate BigInt promotion for `**` (and any other op
3718                // that consults the bigint pragma). `bignum` and
3719                // `bigrat` are routed here too — stryke doesn't yet
3720                // distinguish them from `bigint` for arithmetic, but
3721                // accepting them prevents the default-load path from
3722                // searching @INC for a CPAN module that won't parse.
3723                crate::set_bigint_pragma(true);
3724                Ok(())
3725            }
3726            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3727            _ => {
3728                self.require_execute(module, line)?;
3729                let imports = Self::imports_after_leading_use_version(imports);
3730                self.apply_module_import(module, imports, line)?;
3731                Ok(())
3732            }
3733        }
3734    }
3735
3736    /// `no strict 'refs'`, `no warnings`, `no feature`, …
3737    pub(crate) fn exec_no_stmt(
3738        &mut self,
3739        module: &str,
3740        imports: &[Expr],
3741        line: usize,
3742    ) -> PerlResult<()> {
3743        match module {
3744            "strict" => self.apply_no_strict(imports, line),
3745            "utf8" => {
3746                if !imports.is_empty() {
3747                    return Err(PerlError::runtime("no utf8 takes no arguments", line));
3748                }
3749                self.utf8_pragma = false;
3750                Ok(())
3751            }
3752            "feature" => self.apply_no_feature(imports, line),
3753            "v5" => Ok(()),
3754            "warnings" => {
3755                self.warnings = false;
3756                Ok(())
3757            }
3758            "English" => {
3759                self.english_enabled = false;
3760                // Don't reset no_match_vars here — if match vars were ever enabled,
3761                // they persist (Perl's export cannot be un-exported).
3762                if !self.english_match_vars_ever_enabled {
3763                    self.english_no_match_vars = false;
3764                }
3765                Ok(())
3766            }
3767            "open" => {
3768                self.open_pragma_utf8 = false;
3769                Ok(())
3770            }
3771            "bigint" | "bignum" | "bigrat" => {
3772                crate::set_bigint_pragma(false);
3773                Ok(())
3774            }
3775            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3776            _ => Ok(()),
3777        }
3778    }
3779
3780    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
3781    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3782        let names = Self::pragma_import_strings(imports, line)?;
3783        for n in names {
3784            let key = n.trim_start_matches('@');
3785            if key.eq_ignore_ascii_case("PATH") {
3786                let path_env = std::env::var("PATH").unwrap_or_default();
3787                let path_vec: Vec<PerlValue> = std::env::split_paths(&path_env)
3788                    .map(|p| PerlValue::string(p.to_string_lossy().into_owned()))
3789                    .collect();
3790                let aname = self.stash_array_name_for_package("PATH");
3791                self.scope.declare_array(&aname, path_vec);
3792            }
3793        }
3794        Ok(())
3795    }
3796
3797    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
3798    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3799        let items = Self::pragma_import_strings(imports, line)?;
3800        for item in items {
3801            let s = item.trim();
3802            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
3803                self.open_pragma_utf8 = true;
3804                continue;
3805            }
3806            if let Some(rest) = s.strip_prefix(":encoding(") {
3807                if let Some(inner) = rest.strip_suffix(')') {
3808                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
3809                        self.open_pragma_utf8 = true;
3810                    }
3811                }
3812            }
3813        }
3814        Ok(())
3815    }
3816
3817    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
3818    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3819        if imports.is_empty() {
3820            return Ok(());
3821        }
3822        // `use constant 1.03;` — version check only (ignored here).
3823        if imports.len() == 1 {
3824            match &imports[0].kind {
3825                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
3826                _ => {}
3827            }
3828        }
3829        for imp in imports {
3830            match &imp.kind {
3831                ExprKind::List(items) => {
3832                    if items.len() % 2 != 0 {
3833                        return Err(PerlError::runtime(
3834                            format!(
3835                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
3836                                items.len()
3837                            ),
3838                            line,
3839                        ));
3840                    }
3841                    let mut i = 0;
3842                    while i < items.len() {
3843                        let name = match &items[i].kind {
3844                            ExprKind::String(s) => s.clone(),
3845                            _ => {
3846                                return Err(PerlError::runtime(
3847                                    "use constant: constant name must be a string literal",
3848                                    line,
3849                                ));
3850                            }
3851                        };
3852                        let val = match self.eval_expr(&items[i + 1]) {
3853                            Ok(v) => v,
3854                            Err(FlowOrError::Error(e)) => return Err(e),
3855                            Err(FlowOrError::Flow(_)) => {
3856                                return Err(PerlError::runtime(
3857                                    "use constant: unexpected control flow in initializer",
3858                                    line,
3859                                ));
3860                            }
3861                        };
3862                        self.install_constant_sub(&name, &val, line)?;
3863                        i += 2;
3864                    }
3865                }
3866                _ => {
3867                    return Err(PerlError::runtime(
3868                        "use constant: expected list of NAME => VALUE pairs",
3869                        line,
3870                    ));
3871                }
3872            }
3873        }
3874        Ok(())
3875    }
3876
3877    fn install_constant_sub(&mut self, name: &str, val: &PerlValue, line: usize) -> PerlResult<()> {
3878        let key = self.qualify_sub_key(name);
3879        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
3880        let body = vec![Statement {
3881            label: None,
3882            kind: StmtKind::Return(Some(ret_expr)),
3883            line,
3884        }];
3885        self.subs.insert(
3886            key.clone(),
3887            Arc::new(PerlSub {
3888                name: key,
3889                params: vec![],
3890                body,
3891                prototype: None,
3892                closure_env: None,
3893                fib_like: None,
3894            }),
3895        );
3896        Ok(())
3897    }
3898
3899    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
3900    fn perl_value_to_const_literal_expr(&self, v: &PerlValue, line: usize) -> PerlResult<Expr> {
3901        if v.is_undef() {
3902            return Ok(Expr {
3903                kind: ExprKind::Undef,
3904                line,
3905            });
3906        }
3907        if let Some(n) = v.as_integer() {
3908            return Ok(Expr {
3909                kind: ExprKind::Integer(n),
3910                line,
3911            });
3912        }
3913        if let Some(f) = v.as_float() {
3914            return Ok(Expr {
3915                kind: ExprKind::Float(f),
3916                line,
3917            });
3918        }
3919        if let Some(s) = v.as_str() {
3920            return Ok(Expr {
3921                kind: ExprKind::String(s),
3922                line,
3923            });
3924        }
3925        if let Some(arr) = v.as_array_vec() {
3926            let mut elems = Vec::with_capacity(arr.len());
3927            for e in &arr {
3928                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3929            }
3930            return Ok(Expr {
3931                kind: ExprKind::ArrayRef(elems),
3932                line,
3933            });
3934        }
3935        if let Some(h) = v.as_hash_map() {
3936            let mut pairs = Vec::with_capacity(h.len());
3937            for (k, vv) in h.iter() {
3938                pairs.push((
3939                    Expr {
3940                        kind: ExprKind::String(k.clone()),
3941                        line,
3942                    },
3943                    self.perl_value_to_const_literal_expr(vv, line)?,
3944                ));
3945            }
3946            return Ok(Expr {
3947                kind: ExprKind::HashRef(pairs),
3948                line,
3949            });
3950        }
3951        if let Some(aref) = v.as_array_ref() {
3952            let arr = aref.read();
3953            let mut elems = Vec::with_capacity(arr.len());
3954            for e in arr.iter() {
3955                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3956            }
3957            return Ok(Expr {
3958                kind: ExprKind::ArrayRef(elems),
3959                line,
3960            });
3961        }
3962        if let Some(href) = v.as_hash_ref() {
3963            let h = href.read();
3964            let mut pairs = Vec::with_capacity(h.len());
3965            for (k, vv) in h.iter() {
3966                pairs.push((
3967                    Expr {
3968                        kind: ExprKind::String(k.clone()),
3969                        line,
3970                    },
3971                    self.perl_value_to_const_literal_expr(vv, line)?,
3972                ));
3973            }
3974            return Ok(Expr {
3975                kind: ExprKind::HashRef(pairs),
3976                line,
3977            });
3978        }
3979        Err(PerlError::runtime(
3980            format!("use constant: unsupported value type ({v:?})"),
3981            line,
3982        ))
3983    }
3984
3985    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
3986    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> PerlResult<()> {
3987        // Reset per-interpreter pragma flags. Each new program scan starts
3988        // clean; pragmas activate only when the program contains `use utf8;`
3989        // / `use bigint;` etc. (Globals like `BIGINT_PRAGMA` stay sticky
3990        // across runs in the same process — bigint tests that need
3991        // isolation use subprocess invocation.)
3992        self.utf8_pragma = false;
3993        for stmt in &program.statements {
3994            match &stmt.kind {
3995                StmtKind::Package { name } => {
3996                    let _ = self
3997                        .scope
3998                        .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
3999                }
4000                StmtKind::SubDecl {
4001                    name,
4002                    params,
4003                    body,
4004                    prototype,
4005                } => {
4006                    let key = self.qualify_sub_key(name);
4007                    let mut sub = PerlSub {
4008                        name: name.clone(),
4009                        params: params.clone(),
4010                        body: body.clone(),
4011                        closure_env: None,
4012                        prototype: prototype.clone(),
4013                        fib_like: None,
4014                    };
4015                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
4016                    self.subs.insert(key, Arc::new(sub));
4017                }
4018                StmtKind::UsePerlVersion { .. } => {}
4019                StmtKind::Use { module, imports } => {
4020                    self.exec_use_stmt(module, imports, stmt.line)?;
4021                }
4022                StmtKind::UseOverload { pairs } => {
4023                    self.install_use_overload_pairs(pairs);
4024                }
4025                StmtKind::FormatDecl { name, lines } => {
4026                    self.install_format_decl(name, lines, stmt.line)?;
4027                }
4028                StmtKind::No { module, imports } => {
4029                    self.exec_no_stmt(module, imports, stmt.line)?;
4030                }
4031                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
4032                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
4033                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
4034                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
4035                StmtKind::End(block) => self.end_blocks.push(block.clone()),
4036                _ => {}
4037            }
4038        }
4039        Ok(())
4040    }
4041
4042    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
4043    pub fn install_data_handle(&mut self, data: Vec<u8>) {
4044        self.input_handles.insert(
4045            "DATA".to_string(),
4046            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
4047        );
4048    }
4049
4050    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
4051    ///
4052    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
4053    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
4054    /// [`piped_shell_command`]).
4055    pub(crate) fn open_builtin_execute(
4056        &mut self,
4057        handle_name: String,
4058        mode_s: String,
4059        file_opt: Option<String>,
4060        line: usize,
4061    ) -> PerlResult<PerlValue> {
4062        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
4063        // - leading `|`  → pipe to command (write to child's stdin)
4064        // - trailing `|` → pipe from command (read child's stdout)
4065        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
4066        let (actual_mode, path) = if let Some(f) = file_opt {
4067            (mode_s, f)
4068        } else {
4069            let trimmed = mode_s.trim();
4070            if let Some(rest) = trimmed.strip_prefix('|') {
4071                ("|-".to_string(), rest.trim_start().to_string())
4072            } else if trimmed.ends_with('|') {
4073                let mut cmd = trimmed.to_string();
4074                cmd.pop(); // trailing `|` that selects pipe-from-command
4075                ("-|".to_string(), cmd.trim_end().to_string())
4076            } else if let Some(rest) = trimmed.strip_prefix(">>") {
4077                (">>".to_string(), rest.trim().to_string())
4078            } else if let Some(rest) = trimmed.strip_prefix('>') {
4079                (">".to_string(), rest.trim().to_string())
4080            } else if let Some(rest) = trimmed.strip_prefix('<') {
4081                ("<".to_string(), rest.trim().to_string())
4082            } else {
4083                ("<".to_string(), trimmed.to_string())
4084            }
4085        };
4086        let handle_return = handle_name.clone();
4087        match actual_mode.as_str() {
4088            "-|" => {
4089                let mut cmd = piped_shell_command(&path);
4090                cmd.stdout(Stdio::piped());
4091                let mut child = cmd.spawn().map_err(|e| {
4092                    self.apply_io_error_to_errno(&e);
4093                    PerlError::runtime(format!("Can't open pipe from command: {}", e), line)
4094                })?;
4095                let stdout = child
4096                    .stdout
4097                    .take()
4098                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdout", line))?;
4099                self.input_handles
4100                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
4101                self.pipe_children.insert(handle_name, child);
4102            }
4103            "|-" => {
4104                let mut cmd = piped_shell_command(&path);
4105                cmd.stdin(Stdio::piped());
4106                let mut child = cmd.spawn().map_err(|e| {
4107                    self.apply_io_error_to_errno(&e);
4108                    PerlError::runtime(format!("Can't open pipe to command: {}", e), line)
4109                })?;
4110                let stdin = child
4111                    .stdin
4112                    .take()
4113                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdin", line))?;
4114                self.output_handles
4115                    .insert(handle_name.clone(), Box::new(stdin));
4116                self.pipe_children.insert(handle_name, child);
4117            }
4118            "<" => {
4119                let file = match std::fs::File::open(&path) {
4120                    Ok(f) => f,
4121                    Err(e) => {
4122                        self.apply_io_error_to_errno(&e);
4123                        return Ok(PerlValue::integer(0));
4124                    }
4125                };
4126                let shared = Arc::new(Mutex::new(file));
4127                self.io_file_slots
4128                    .insert(handle_name.clone(), Arc::clone(&shared));
4129                self.input_handles.insert(
4130                    handle_name.clone(),
4131                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
4132                );
4133            }
4134            ">" => {
4135                let file = match std::fs::File::create(&path) {
4136                    Ok(f) => f,
4137                    Err(e) => {
4138                        self.apply_io_error_to_errno(&e);
4139                        return Ok(PerlValue::integer(0));
4140                    }
4141                };
4142                let shared = Arc::new(Mutex::new(file));
4143                self.io_file_slots
4144                    .insert(handle_name.clone(), Arc::clone(&shared));
4145                self.output_handles.insert(
4146                    handle_name.clone(),
4147                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4148                );
4149            }
4150            ">>" => {
4151                let file = match std::fs::OpenOptions::new()
4152                    .append(true)
4153                    .create(true)
4154                    .open(&path)
4155                {
4156                    Ok(f) => f,
4157                    Err(e) => {
4158                        self.apply_io_error_to_errno(&e);
4159                        return Ok(PerlValue::integer(0));
4160                    }
4161                };
4162                let shared = Arc::new(Mutex::new(file));
4163                self.io_file_slots
4164                    .insert(handle_name.clone(), Arc::clone(&shared));
4165                self.output_handles.insert(
4166                    handle_name.clone(),
4167                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
4168                );
4169            }
4170            _ => {
4171                return Err(PerlError::runtime(
4172                    format!("Unknown open mode '{}'", actual_mode),
4173                    line,
4174                ));
4175            }
4176        }
4177        Ok(PerlValue::io_handle(handle_return))
4178    }
4179
4180    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
4181    /// matches the previous key under [`PerlValue::str_eq`]. Returns a list of arrayrefs
4182    /// (same outer shape as `chunked`).
4183    pub(crate) fn eval_chunk_by_builtin(
4184        &mut self,
4185        key_spec: &Expr,
4186        list_expr: &Expr,
4187        ctx: WantarrayCtx,
4188        line: usize,
4189    ) -> ExecResult {
4190        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
4191        let chunks = match &key_spec.kind {
4192            ExprKind::CodeRef { .. } => {
4193                let cr = self.eval_expr(key_spec)?;
4194                let Some(sub) = cr.as_code_ref() else {
4195                    return Err(PerlError::runtime(
4196                        "group_by/chunk_by: first argument must be { BLOCK }",
4197                        line,
4198                    )
4199                    .into());
4200                };
4201                let sub = sub.clone();
4202                let mut chunks: Vec<PerlValue> = Vec::new();
4203                let mut run: Vec<PerlValue> = Vec::new();
4204                let mut prev_key: Option<PerlValue> = None;
4205                for item in list {
4206                    self.scope.set_topic(item.clone());
4207                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
4208                        Ok(k) => k,
4209                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
4210                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
4211                        Err(_) => PerlValue::UNDEF,
4212                    };
4213                    match &prev_key {
4214                        None => {
4215                            run.push(item);
4216                            prev_key = Some(key);
4217                        }
4218                        Some(pk) => {
4219                            if key.str_eq(pk) {
4220                                run.push(item);
4221                            } else {
4222                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
4223                                    std::mem::take(&mut run),
4224                                ))));
4225                                run.push(item);
4226                                prev_key = Some(key);
4227                            }
4228                        }
4229                    }
4230                }
4231                if !run.is_empty() {
4232                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
4233                }
4234                chunks
4235            }
4236            _ => {
4237                let mut chunks: Vec<PerlValue> = Vec::new();
4238                let mut run: Vec<PerlValue> = Vec::new();
4239                let mut prev_key: Option<PerlValue> = None;
4240                for item in list {
4241                    self.scope.set_topic(item.clone());
4242                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
4243                    match &prev_key {
4244                        None => {
4245                            run.push(item);
4246                            prev_key = Some(key);
4247                        }
4248                        Some(pk) => {
4249                            if key.str_eq(pk) {
4250                                run.push(item);
4251                            } else {
4252                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
4253                                    std::mem::take(&mut run),
4254                                ))));
4255                                run.push(item);
4256                                prev_key = Some(key);
4257                            }
4258                        }
4259                    }
4260                }
4261                if !run.is_empty() {
4262                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
4263                }
4264                chunks
4265            }
4266        };
4267        Ok(match ctx {
4268            WantarrayCtx::List => PerlValue::array(chunks),
4269            WantarrayCtx::Scalar => PerlValue::integer(chunks.len() as i64),
4270            WantarrayCtx::Void => PerlValue::UNDEF,
4271        })
4272    }
4273
4274    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
4275    pub(crate) fn list_higher_order_block_builtin(
4276        &mut self,
4277        name: &str,
4278        args: &[PerlValue],
4279        line: usize,
4280    ) -> PerlResult<PerlValue> {
4281        match self.list_higher_order_block_builtin_exec(name, args, line) {
4282            Ok(v) => Ok(v),
4283            Err(FlowOrError::Error(e)) => Err(e),
4284            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
4285            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
4286                format!("{name}: unsupported control flow in block"),
4287                line,
4288            )),
4289        }
4290    }
4291
4292    fn list_higher_order_block_builtin_exec(
4293        &mut self,
4294        name: &str,
4295        args: &[PerlValue],
4296        line: usize,
4297    ) -> ExecResult {
4298        if args.is_empty() {
4299            return Err(
4300                PerlError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
4301            );
4302        }
4303        let Some(sub) = args[0].as_code_ref() else {
4304            return Err(PerlError::runtime(
4305                format!("{name}: first argument must be {{ BLOCK }}"),
4306                line,
4307            )
4308            .into());
4309        };
4310        let sub = sub.clone();
4311        let items: Vec<PerlValue> = args[1..].to_vec();
4312        if matches!(name, "tap" | "peek") && items.len() == 1 {
4313            if let Some(p) = items[0].as_pipeline() {
4314                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
4315                return Ok(PerlValue::pipeline(Arc::clone(&p)));
4316            }
4317            let v = &items[0];
4318            if v.is_iterator() || v.as_array_vec().is_some() {
4319                let source = crate::map_stream::into_pull_iter(v.clone());
4320                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
4321                return Ok(PerlValue::iterator(Arc::new(
4322                    crate::map_stream::TapIterator::new(
4323                        source,
4324                        sub,
4325                        self.subs.clone(),
4326                        capture,
4327                        atomic_arrays,
4328                        atomic_hashes,
4329                    ),
4330                )));
4331            }
4332        }
4333        // Streaming optimization disabled for these functions because the pre-captured
4334        // coderef from args[0] has its closure_env populated at parse time, which causes
4335        // $_ to get stale values on subsequent calls. These functions work correctly in
4336        // the non-streaming eager path below.
4337        let wa = self.wantarray_kind;
4338        match name {
4339            "take_while" => {
4340                let mut out = Vec::new();
4341                for item in items {
4342                    // `call_sub` binds the item to @_/positional params (so
4343                    // stryke lambdas work) and to `$_` via `set_closure_args`
4344                    // (so `_`-using blocks work). Replaces the old
4345                    // `exec_block(&sub.body)` path which only set the topic.
4346                    self.scope.set_topic(item.clone());
4347                    let pred =
4348                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4349                    if !pred.is_true() {
4350                        break;
4351                    }
4352                    out.push(item);
4353                }
4354                Ok(match wa {
4355                    WantarrayCtx::List => PerlValue::array(out),
4356                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4357                    WantarrayCtx::Void => PerlValue::UNDEF,
4358                })
4359            }
4360            "drop_while" | "skip_while" => {
4361                let mut i = 0usize;
4362                while i < items.len() {
4363                    let it = items[i].clone();
4364                    self.scope.set_topic(it.clone());
4365                    let pred = self.call_sub(&sub, vec![it], WantarrayCtx::Scalar, line)?;
4366                    if !pred.is_true() {
4367                        break;
4368                    }
4369                    i += 1;
4370                }
4371                let rest = items[i..].to_vec();
4372                Ok(match wa {
4373                    WantarrayCtx::List => PerlValue::array(rest),
4374                    WantarrayCtx::Scalar => PerlValue::integer(rest.len() as i64),
4375                    WantarrayCtx::Void => PerlValue::UNDEF,
4376                })
4377            }
4378            "reject" | "grepv" => {
4379                let mut out = Vec::new();
4380                for item in items {
4381                    self.scope.set_topic(item.clone());
4382                    let pred =
4383                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4384                    if !pred.is_true() {
4385                        out.push(item);
4386                    }
4387                }
4388                Ok(match wa {
4389                    WantarrayCtx::List => PerlValue::array(out),
4390                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4391                    WantarrayCtx::Void => PerlValue::UNDEF,
4392                })
4393            }
4394            "tap" | "peek" => {
4395                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
4396                Ok(match wa {
4397                    WantarrayCtx::List => PerlValue::array(items),
4398                    WantarrayCtx::Scalar => PerlValue::integer(items.len() as i64),
4399                    WantarrayCtx::Void => PerlValue::UNDEF,
4400                })
4401            }
4402            "partition" => {
4403                let mut yes = Vec::new();
4404                let mut no = Vec::new();
4405                for item in items {
4406                    self.scope.set_topic(item.clone());
4407                    let pred =
4408                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4409                    if pred.is_true() {
4410                        yes.push(item);
4411                    } else {
4412                        no.push(item);
4413                    }
4414                }
4415                let yes_ref = PerlValue::array_ref(Arc::new(RwLock::new(yes)));
4416                let no_ref = PerlValue::array_ref(Arc::new(RwLock::new(no)));
4417                Ok(match wa {
4418                    WantarrayCtx::List => PerlValue::array(vec![yes_ref, no_ref]),
4419                    WantarrayCtx::Scalar => PerlValue::integer(2),
4420                    WantarrayCtx::Void => PerlValue::UNDEF,
4421                })
4422            }
4423            "min_by" => {
4424                let mut best: Option<(PerlValue, PerlValue)> = None;
4425                for item in items {
4426                    self.scope.set_topic(item.clone());
4427                    let key =
4428                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4429                    best = Some(match best {
4430                        None => (item, key),
4431                        Some((bv, bk)) => {
4432                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
4433                                (item, key)
4434                            } else {
4435                                (bv, bk)
4436                            }
4437                        }
4438                    });
4439                }
4440                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
4441            }
4442            "max_by" => {
4443                let mut best: Option<(PerlValue, PerlValue)> = None;
4444                for item in items {
4445                    self.scope.set_topic(item.clone());
4446                    let key =
4447                        self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?;
4448                    best = Some(match best {
4449                        None => (item, key),
4450                        Some((bv, bk)) => {
4451                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
4452                                (item, key)
4453                            } else {
4454                                (bv, bk)
4455                            }
4456                        }
4457                    });
4458                }
4459                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
4460            }
4461            "zip_with" => {
4462                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
4463                // Flatten items, then treat each array ref/binding as a separate list.
4464                let flat: Vec<PerlValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
4465                let refs: Vec<Vec<PerlValue>> = flat
4466                    .iter()
4467                    .map(|el| {
4468                        if let Some(ar) = el.as_array_ref() {
4469                            ar.read().clone()
4470                        } else if let Some(name) = el.as_array_binding_name() {
4471                            self.scope.get_array(&name)
4472                        } else {
4473                            vec![el.clone()]
4474                        }
4475                    })
4476                    .collect();
4477                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
4478                let mut out = Vec::with_capacity(max_len);
4479                for i in 0..max_len {
4480                    let pair: Vec<PerlValue> = refs
4481                        .iter()
4482                        .map(|l| l.get(i).cloned().unwrap_or(PerlValue::UNDEF))
4483                        .collect();
4484                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
4485                    out.push(result);
4486                }
4487                Ok(match wa {
4488                    WantarrayCtx::List => PerlValue::array(out),
4489                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4490                    WantarrayCtx::Void => PerlValue::UNDEF,
4491                })
4492            }
4493            "count_by" => {
4494                let mut counts = indexmap::IndexMap::new();
4495                for item in items {
4496                    self.scope.set_topic(item.clone());
4497                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4498                    let k = key.to_string();
4499                    let entry = counts.entry(k).or_insert(PerlValue::integer(0));
4500                    *entry = PerlValue::integer(entry.to_int() + 1);
4501                }
4502                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(counts))))
4503            }
4504            _ => Err(PerlError::runtime(
4505                format!("internal: unknown list block builtin `{name}`"),
4506                line,
4507            )
4508            .into()),
4509        }
4510    }
4511
4512    /// `rmdir LIST` — remove empty directories; returns count removed.
4513    pub(crate) fn builtin_rmdir_execute(
4514        &mut self,
4515        args: &[PerlValue],
4516        _line: usize,
4517    ) -> PerlResult<PerlValue> {
4518        let mut count = 0i64;
4519        for a in args {
4520            let p = a.to_string();
4521            if p.is_empty() {
4522                continue;
4523            }
4524            if std::fs::remove_dir(&p).is_ok() {
4525                count += 1;
4526            }
4527        }
4528        Ok(PerlValue::integer(count))
4529    }
4530
4531    /// `touch FILE, ...` — create if absent, update timestamps to now.
4532    pub(crate) fn builtin_touch_execute(
4533        &mut self,
4534        args: &[PerlValue],
4535        _line: usize,
4536    ) -> PerlResult<PerlValue> {
4537        let paths: Vec<String> = args.iter().map(|v| v.to_string()).collect();
4538        Ok(PerlValue::integer(crate::perl_fs::touch_paths(&paths)))
4539    }
4540
4541    /// `utime ATIME, MTIME, LIST`
4542    pub(crate) fn builtin_utime_execute(
4543        &mut self,
4544        args: &[PerlValue],
4545        line: usize,
4546    ) -> PerlResult<PerlValue> {
4547        if args.len() < 3 {
4548            return Err(PerlError::runtime(
4549                "utime requires at least three arguments (atime, mtime, files...)",
4550                line,
4551            ));
4552        }
4553        let at = args[0].to_int();
4554        let mt = args[1].to_int();
4555        let paths: Vec<String> = args.iter().skip(2).map(|v| v.to_string()).collect();
4556        let n = crate::perl_fs::utime_paths(at, mt, &paths);
4557        #[cfg(not(unix))]
4558        if !paths.is_empty() && n == 0 {
4559            return Err(PerlError::runtime(
4560                "utime is not supported on this platform",
4561                line,
4562            ));
4563        }
4564        Ok(PerlValue::integer(n))
4565    }
4566
4567    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
4568    pub(crate) fn builtin_umask_execute(
4569        &mut self,
4570        args: &[PerlValue],
4571        line: usize,
4572    ) -> PerlResult<PerlValue> {
4573        #[cfg(unix)]
4574        {
4575            let _ = line;
4576            if args.is_empty() {
4577                let cur = unsafe { libc::umask(0) };
4578                unsafe { libc::umask(cur) };
4579                return Ok(PerlValue::integer(cur as i64));
4580            }
4581            let new_m = args[0].to_int() as libc::mode_t;
4582            let old = unsafe { libc::umask(new_m) };
4583            Ok(PerlValue::integer(old as i64))
4584        }
4585        #[cfg(not(unix))]
4586        {
4587            let _ = args;
4588            Err(PerlError::runtime(
4589                "umask is not supported on this platform",
4590                line,
4591            ))
4592        }
4593    }
4594
4595    /// `getcwd` — current directory or undef on failure.
4596    pub(crate) fn builtin_getcwd_execute(
4597        &mut self,
4598        args: &[PerlValue],
4599        line: usize,
4600    ) -> PerlResult<PerlValue> {
4601        if !args.is_empty() {
4602            return Err(PerlError::runtime("getcwd takes no arguments", line));
4603        }
4604        match std::env::current_dir() {
4605            Ok(p) => Ok(PerlValue::string(p.to_string_lossy().into_owned())),
4606            Err(e) => {
4607                self.apply_io_error_to_errno(&e);
4608                Ok(PerlValue::UNDEF)
4609            }
4610        }
4611    }
4612
4613    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
4614    pub(crate) fn builtin_realpath_execute(
4615        &mut self,
4616        args: &[PerlValue],
4617        line: usize,
4618    ) -> PerlResult<PerlValue> {
4619        let path = args
4620            .first()
4621            .ok_or_else(|| PerlError::runtime("realpath: need path", line))?
4622            .to_string();
4623        if path.is_empty() {
4624            return Err(PerlError::runtime("realpath: need path", line));
4625        }
4626        match crate::perl_fs::realpath_resolved(&path) {
4627            Ok(s) => Ok(PerlValue::string(s)),
4628            Err(e) => {
4629                self.apply_io_error_to_errno(&e);
4630                Ok(PerlValue::UNDEF)
4631            }
4632        }
4633    }
4634
4635    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
4636    pub(crate) fn builtin_pipe_execute(
4637        &mut self,
4638        args: &[PerlValue],
4639        line: usize,
4640    ) -> PerlResult<PerlValue> {
4641        if args.len() != 2 {
4642            return Err(PerlError::runtime(
4643                "pipe requires exactly two arguments",
4644                line,
4645            ));
4646        }
4647        #[cfg(unix)]
4648        {
4649            use std::fs::File;
4650            use std::os::unix::io::FromRawFd;
4651
4652            let read_name = args[0].to_string();
4653            let write_name = args[1].to_string();
4654            if read_name.is_empty() || write_name.is_empty() {
4655                return Err(PerlError::runtime("pipe: invalid handle name", line));
4656            }
4657            let mut fds = [0i32; 2];
4658            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
4659                let e = std::io::Error::last_os_error();
4660                self.apply_io_error_to_errno(&e);
4661                return Ok(PerlValue::integer(0));
4662            }
4663            let read_file = unsafe { File::from_raw_fd(fds[0]) };
4664            let write_file = unsafe { File::from_raw_fd(fds[1]) };
4665
4666            let read_shared = Arc::new(Mutex::new(read_file));
4667            let write_shared = Arc::new(Mutex::new(write_file));
4668
4669            self.close_builtin_execute(read_name.clone()).ok();
4670            self.close_builtin_execute(write_name.clone()).ok();
4671
4672            self.io_file_slots
4673                .insert(read_name.clone(), Arc::clone(&read_shared));
4674            self.input_handles.insert(
4675                read_name,
4676                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
4677            );
4678
4679            self.io_file_slots
4680                .insert(write_name.clone(), Arc::clone(&write_shared));
4681            self.output_handles
4682                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
4683
4684            Ok(PerlValue::integer(1))
4685        }
4686        #[cfg(not(unix))]
4687        {
4688            let _ = args;
4689            Err(PerlError::runtime(
4690                "pipe is not supported on this platform",
4691                line,
4692            ))
4693        }
4694    }
4695
4696    pub(crate) fn close_builtin_execute(&mut self, name: String) -> PerlResult<PerlValue> {
4697        self.output_handles.remove(&name);
4698        self.input_handles.remove(&name);
4699        self.io_file_slots.remove(&name);
4700        if let Some(mut child) = self.pipe_children.remove(&name) {
4701            if let Ok(st) = child.wait() {
4702                self.record_child_exit_status(st);
4703            }
4704        }
4705        Ok(PerlValue::integer(1))
4706    }
4707
4708    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
4709        self.input_handles.contains_key(name)
4710    }
4711
4712    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
4713    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
4714    /// readline-level EOF tracking exists.
4715    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
4716        self.line_mode_eof_pending
4717    }
4718
4719    /// `eof` / `eof()` / `eof FH` — shared by [`crate::vm::VM`] and
4720    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
4721    /// not [`ExprKind::Eof`]).
4722    pub(crate) fn eof_builtin_execute(
4723        &self,
4724        args: &[PerlValue],
4725        line: usize,
4726    ) -> PerlResult<PerlValue> {
4727        match args.len() {
4728            0 => Ok(PerlValue::integer(if self.eof_without_arg_is_true() {
4729                1
4730            } else {
4731                0
4732            })),
4733            1 => {
4734                let name = args[0].to_string();
4735                let at_eof = !self.has_input_handle(&name);
4736                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
4737            }
4738            _ => Err(PerlError::runtime("eof: too many arguments", line)),
4739        }
4740    }
4741
4742    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
4743    /// `0`, stringifies to `""`) for `""`.
4744    pub(crate) fn study_return_value(s: &str) -> PerlValue {
4745        if s.is_empty() {
4746            PerlValue::string(String::new())
4747        } else {
4748            PerlValue::integer(1)
4749        }
4750    }
4751
4752    pub(crate) fn readline_builtin_execute(
4753        &mut self,
4754        handle: Option<&str>,
4755    ) -> PerlResult<PerlValue> {
4756        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
4757        if handle.is_none() {
4758            let argv = self.scope.get_array("ARGV");
4759            if !argv.is_empty() {
4760                loop {
4761                    if self.diamond_reader.is_none() {
4762                        while self.diamond_next_idx < argv.len() {
4763                            let path = argv[self.diamond_next_idx].to_string();
4764                            self.diamond_next_idx += 1;
4765                            match File::open(&path) {
4766                                Ok(f) => {
4767                                    self.argv_current_file = path;
4768                                    self.diamond_reader = Some(BufReader::new(f));
4769                                    break;
4770                                }
4771                                Err(e) => {
4772                                    self.apply_io_error_to_errno(&e);
4773                                }
4774                            }
4775                        }
4776                        if self.diamond_reader.is_none() {
4777                            return Ok(PerlValue::UNDEF);
4778                        }
4779                    }
4780                    let mut line_str = String::new();
4781                    let read_result: Result<usize, io::Error> =
4782                        if let Some(reader) = self.diamond_reader.as_mut() {
4783                            if self.open_pragma_utf8 {
4784                                let mut buf = Vec::new();
4785                                reader.read_until(b'\n', &mut buf).inspect(|n| {
4786                                    if *n > 0 {
4787                                        line_str = String::from_utf8_lossy(&buf).into_owned();
4788                                    }
4789                                })
4790                            } else {
4791                                let mut buf = Vec::new();
4792                                match reader.read_until(b'\n', &mut buf) {
4793                                    Ok(n) => {
4794                                        if n > 0 {
4795                                            line_str =
4796                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
4797                                                &buf,
4798                                            );
4799                                        }
4800                                        Ok(n)
4801                                    }
4802                                    Err(e) => Err(e),
4803                                }
4804                            }
4805                        } else {
4806                            unreachable!()
4807                        };
4808                    match read_result {
4809                        Ok(0) => {
4810                            self.diamond_reader = None;
4811                            continue;
4812                        }
4813                        Ok(_) => {
4814                            self.bump_line_for_handle(&self.argv_current_file.clone());
4815                            return Ok(PerlValue::string(line_str));
4816                        }
4817                        Err(e) => {
4818                            self.apply_io_error_to_errno(&e);
4819                            self.diamond_reader = None;
4820                            continue;
4821                        }
4822                    }
4823                }
4824            } else {
4825                self.argv_current_file.clear();
4826            }
4827        }
4828
4829        let handle_name = handle.unwrap_or("STDIN");
4830        let mut line_str = String::new();
4831        if handle_name == "STDIN" {
4832            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
4833                self.last_stdin_die_bracket = if handle.is_none() {
4834                    "<>".to_string()
4835                } else {
4836                    "<STDIN>".to_string()
4837                };
4838                self.bump_line_for_handle("STDIN");
4839                return Ok(PerlValue::string(queued));
4840            }
4841            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4842                let mut buf = Vec::new();
4843                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
4844                    if *n > 0 {
4845                        line_str = String::from_utf8_lossy(&buf).into_owned();
4846                    }
4847                })
4848            } else {
4849                let mut buf = Vec::new();
4850                let mut lock = io::stdin().lock();
4851                match lock.read_until(b'\n', &mut buf) {
4852                    Ok(n) => {
4853                        if n > 0 {
4854                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4855                        }
4856                        Ok(n)
4857                    }
4858                    Err(e) => Err(e),
4859                }
4860            };
4861            match r {
4862                Ok(0) => Ok(PerlValue::UNDEF),
4863                Ok(_) => {
4864                    self.last_stdin_die_bracket = if handle.is_none() {
4865                        "<>".to_string()
4866                    } else {
4867                        "<STDIN>".to_string()
4868                    };
4869                    self.bump_line_for_handle("STDIN");
4870                    Ok(PerlValue::string(line_str))
4871                }
4872                Err(e) => {
4873                    self.apply_io_error_to_errno(&e);
4874                    Ok(PerlValue::UNDEF)
4875                }
4876            }
4877        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
4878            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4879                let mut buf = Vec::new();
4880                reader.read_until(b'\n', &mut buf).inspect(|n| {
4881                    if *n > 0 {
4882                        line_str = String::from_utf8_lossy(&buf).into_owned();
4883                    }
4884                })
4885            } else {
4886                let mut buf = Vec::new();
4887                match reader.read_until(b'\n', &mut buf) {
4888                    Ok(n) => {
4889                        if n > 0 {
4890                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4891                        }
4892                        Ok(n)
4893                    }
4894                    Err(e) => Err(e),
4895                }
4896            };
4897            match r {
4898                Ok(0) => Ok(PerlValue::UNDEF),
4899                Ok(_) => {
4900                    self.bump_line_for_handle(handle_name);
4901                    Ok(PerlValue::string(line_str))
4902                }
4903                Err(e) => {
4904                    self.apply_io_error_to_errno(&e);
4905                    Ok(PerlValue::UNDEF)
4906                }
4907            }
4908        } else {
4909            Ok(PerlValue::UNDEF)
4910        }
4911    }
4912
4913    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
4914    pub(crate) fn readline_builtin_execute_list(
4915        &mut self,
4916        handle: Option<&str>,
4917    ) -> PerlResult<PerlValue> {
4918        let mut lines = Vec::new();
4919        loop {
4920            let v = self.readline_builtin_execute(handle)?;
4921            if v.is_undef() {
4922                break;
4923            }
4924            lines.push(v);
4925        }
4926        Ok(PerlValue::array(lines))
4927    }
4928
4929    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> PerlValue {
4930        match std::fs::read_dir(path) {
4931            Ok(rd) => {
4932                let entries: Vec<String> = rd
4933                    .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
4934                    .collect();
4935                self.dir_handles
4936                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
4937                PerlValue::integer(1)
4938            }
4939            Err(e) => {
4940                self.apply_io_error_to_errno(&e);
4941                PerlValue::integer(0)
4942            }
4943        }
4944    }
4945
4946    pub(crate) fn readdir_handle(&mut self, handle: &str) -> PerlValue {
4947        if let Some(dh) = self.dir_handles.get_mut(handle) {
4948            if dh.pos < dh.entries.len() {
4949                let s = dh.entries[dh.pos].clone();
4950                dh.pos += 1;
4951                PerlValue::string(s)
4952            } else {
4953                PerlValue::UNDEF
4954            }
4955        } else {
4956            PerlValue::UNDEF
4957        }
4958    }
4959
4960    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
4961    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> PerlValue {
4962        if let Some(dh) = self.dir_handles.get_mut(handle) {
4963            let rest: Vec<PerlValue> = dh.entries[dh.pos..]
4964                .iter()
4965                .cloned()
4966                .map(PerlValue::string)
4967                .collect();
4968            dh.pos = dh.entries.len();
4969            PerlValue::array(rest)
4970        } else {
4971            PerlValue::array(Vec::new())
4972        }
4973    }
4974
4975    pub(crate) fn closedir_handle(&mut self, handle: &str) -> PerlValue {
4976        PerlValue::integer(if self.dir_handles.remove(handle).is_some() {
4977            1
4978        } else {
4979            0
4980        })
4981    }
4982
4983    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> PerlValue {
4984        if let Some(dh) = self.dir_handles.get_mut(handle) {
4985            dh.pos = 0;
4986            PerlValue::integer(1)
4987        } else {
4988            PerlValue::integer(0)
4989        }
4990    }
4991
4992    pub(crate) fn telldir_handle(&mut self, handle: &str) -> PerlValue {
4993        self.dir_handles
4994            .get(handle)
4995            .map(|dh| PerlValue::integer(dh.pos as i64))
4996            .unwrap_or(PerlValue::UNDEF)
4997    }
4998
4999    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> PerlValue {
5000        if let Some(dh) = self.dir_handles.get_mut(handle) {
5001            dh.pos = pos.min(dh.entries.len());
5002            PerlValue::integer(1)
5003        } else {
5004            PerlValue::integer(0)
5005        }
5006    }
5007
5008    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
5009    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
5010    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
5011    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
5012    #[inline]
5013    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
5014        crate::special_vars::is_regex_match_scalar_name(name)
5015    }
5016
5017    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
5018    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
5019    /// `apply_regex_captures` instead of short-circuiting.
5020    #[inline]
5021    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
5022        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
5023            self.regex_capture_scope_fresh = false;
5024        }
5025    }
5026
5027    pub(crate) fn apply_regex_captures(
5028        &mut self,
5029        haystack: &str,
5030        offset: usize,
5031        re: &PerlCompiledRegex,
5032        caps: &PerlCaptures<'_>,
5033        capture_all: CaptureAllMode,
5034    ) -> Result<(), FlowOrError> {
5035        let m0 = caps.get(0).expect("regex capture 0");
5036        let s0 = offset + m0.start;
5037        let e0 = offset + m0.end;
5038        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
5039        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
5040        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
5041        let mut last_paren = String::new();
5042        for i in 1..caps.len() {
5043            if let Some(m) = caps.get(i) {
5044                last_paren = m.text.to_string();
5045            }
5046        }
5047        self.last_paren_match = last_paren;
5048        self.last_subpattern_name = String::new();
5049        for n in re.capture_names().flatten() {
5050            if caps.name(n).is_some() {
5051                self.last_subpattern_name = n.to_string();
5052            }
5053        }
5054        self.scope
5055            .set_scalar("&", PerlValue::string(self.last_match.clone()))?;
5056        self.scope
5057            .set_scalar("`", PerlValue::string(self.prematch.clone()))?;
5058        self.scope
5059            .set_scalar("'", PerlValue::string(self.postmatch.clone()))?;
5060        self.scope
5061            .set_scalar("+", PerlValue::string(self.last_paren_match.clone()))?;
5062        for i in 1..caps.len() {
5063            if let Some(m) = caps.get(i) {
5064                self.scope
5065                    .set_scalar(&i.to_string(), PerlValue::string(m.text.to_string()))?;
5066            }
5067        }
5068        let mut start_arr = vec![PerlValue::integer(s0 as i64)];
5069        let mut end_arr = vec![PerlValue::integer(e0 as i64)];
5070        for i in 1..caps.len() {
5071            if let Some(m) = caps.get(i) {
5072                start_arr.push(PerlValue::integer((offset + m.start) as i64));
5073                end_arr.push(PerlValue::integer((offset + m.end) as i64));
5074            } else {
5075                start_arr.push(PerlValue::integer(-1));
5076                end_arr.push(PerlValue::integer(-1));
5077            }
5078        }
5079        self.scope.set_array("-", start_arr)?;
5080        self.scope.set_array("+", end_arr)?;
5081        let mut named = IndexMap::new();
5082        for name in re.capture_names().flatten() {
5083            if let Some(m) = caps.name(name) {
5084                named.insert(name.to_string(), PerlValue::string(m.text.to_string()));
5085            }
5086        }
5087        self.scope.set_hash("+", named.clone())?;
5088        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
5089        let mut named_minus = IndexMap::new();
5090        for (name, val) in &named {
5091            named_minus.insert(
5092                name.clone(),
5093                PerlValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
5094            );
5095        }
5096        self.scope.set_hash("-", named_minus)?;
5097        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
5098        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
5099        match capture_all {
5100            CaptureAllMode::Empty => {
5101                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5102            }
5103            CaptureAllMode::Append => {
5104                let mut rows = self.scope.get_array("^CAPTURE_ALL");
5105                rows.push(PerlValue::array(cap_flat));
5106                self.scope.set_array("^CAPTURE_ALL", rows)?;
5107            }
5108            CaptureAllMode::Skip => {}
5109        }
5110        Ok(())
5111    }
5112
5113    pub(crate) fn clear_flip_flop_state(&mut self) {
5114        self.flip_flop_active.clear();
5115        self.flip_flop_exclusive_left_line.clear();
5116        self.flip_flop_sequence.clear();
5117        self.flip_flop_last_dot.clear();
5118        self.flip_flop_tree.clear();
5119    }
5120
5121    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
5122        self.flip_flop_active.resize(slots as usize, false);
5123        self.flip_flop_active.fill(false);
5124        self.flip_flop_exclusive_left_line
5125            .resize(slots as usize, None);
5126        self.flip_flop_exclusive_left_line.fill(None);
5127        self.flip_flop_sequence.resize(slots as usize, 0);
5128        self.flip_flop_sequence.fill(0);
5129        self.flip_flop_last_dot.resize(slots as usize, None);
5130        self.flip_flop_last_dot.fill(None);
5131    }
5132
5133    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
5134    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
5135    /// [`Self::handle_line_numbers`]).
5136    #[inline]
5137    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
5138        if self.last_readline_handle.is_empty() {
5139            self.line_number
5140        } else {
5141            *self
5142                .handle_line_numbers
5143                .get(&self.last_readline_handle)
5144                .unwrap_or(&0)
5145        }
5146    }
5147
5148    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
5149    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
5150    ///
5151    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
5152    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
5153    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
5154    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
5155    /// get `0` / `N` from [`PerlValue::to_int`].
5156    pub(crate) fn scalar_flip_flop_eval(
5157        &mut self,
5158        left: i64,
5159        right: i64,
5160        slot: usize,
5161        exclusive: bool,
5162    ) -> PerlResult<PerlValue> {
5163        if self.flip_flop_active.len() <= slot {
5164            self.flip_flop_active.resize(slot + 1, false);
5165        }
5166        if self.flip_flop_exclusive_left_line.len() <= slot {
5167            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5168        }
5169        if self.flip_flop_sequence.len() <= slot {
5170            self.flip_flop_sequence.resize(slot + 1, 0);
5171        }
5172        if self.flip_flop_last_dot.len() <= slot {
5173            self.flip_flop_last_dot.resize(slot + 1, None);
5174        }
5175        let dot = self.scalar_flipflop_dot_line();
5176        let active = &mut self.flip_flop_active[slot];
5177        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5178        let seq = &mut self.flip_flop_sequence[slot];
5179        let last_dot = &mut self.flip_flop_last_dot[slot];
5180        if !*active {
5181            if dot == left {
5182                *active = true;
5183                *seq = 1;
5184                *last_dot = Some(dot);
5185                if exclusive {
5186                    *excl_left = Some(dot);
5187                } else {
5188                    *excl_left = None;
5189                    if dot == right {
5190                        *active = false;
5191                        return Ok(PerlValue::string(format!("{}E0", *seq)));
5192                    }
5193                }
5194                return Ok(PerlValue::string(seq.to_string()));
5195            }
5196            *last_dot = Some(dot);
5197            return Ok(PerlValue::string(String::new()));
5198        }
5199        // Already active: increment the sequence once per new `$.`, so a second evaluation on
5200        // the same line reads the same number (matches Perl `pp_flop`).
5201        if *last_dot != Some(dot) {
5202            *seq += 1;
5203            *last_dot = Some(dot);
5204        }
5205        let cur_seq = *seq;
5206        if let Some(ll) = *excl_left {
5207            if dot == right && dot > ll {
5208                *active = false;
5209                *excl_left = None;
5210                *seq = 0;
5211                return Ok(PerlValue::string(format!("{}E0", cur_seq)));
5212            }
5213        } else if dot == right {
5214            *active = false;
5215            *seq = 0;
5216            return Ok(PerlValue::string(format!("{}E0", cur_seq)));
5217        }
5218        Ok(PerlValue::string(cur_seq.to_string()))
5219    }
5220
5221    fn regex_flip_flop_transition(
5222        active: &mut bool,
5223        excl_left: &mut Option<i64>,
5224        exclusive: bool,
5225        dot: i64,
5226        left_m: bool,
5227        right_m: bool,
5228    ) -> i64 {
5229        if !*active {
5230            if left_m {
5231                *active = true;
5232                if exclusive {
5233                    *excl_left = Some(dot);
5234                } else {
5235                    *excl_left = None;
5236                    if right_m {
5237                        *active = false;
5238                    }
5239                }
5240                return 1;
5241            }
5242            return 0;
5243        }
5244        if let Some(ll) = *excl_left {
5245            if right_m && dot > ll {
5246                *active = false;
5247                *excl_left = None;
5248            }
5249        } else if right_m {
5250            *active = false;
5251        }
5252        1
5253    }
5254
5255    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
5256    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
5257    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
5258    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
5259    pub(crate) fn regex_flip_flop_eval(
5260        &mut self,
5261        left_pat: &str,
5262        left_flags: &str,
5263        right_pat: &str,
5264        right_flags: &str,
5265        slot: usize,
5266        exclusive: bool,
5267        line: usize,
5268    ) -> PerlResult<PerlValue> {
5269        let dot = self.scalar_flipflop_dot_line();
5270        let subject = self.scope.get_scalar("_").to_string();
5271        let left_re = self
5272            .compile_regex(left_pat, left_flags, line)
5273            .map_err(|e| match e {
5274                FlowOrError::Error(err) => err,
5275                FlowOrError::Flow(_) => {
5276                    PerlError::runtime("unexpected flow in regex flip-flop", line)
5277                }
5278            })?;
5279        let right_re = self
5280            .compile_regex(right_pat, right_flags, line)
5281            .map_err(|e| match e {
5282                FlowOrError::Error(err) => err,
5283                FlowOrError::Flow(_) => {
5284                    PerlError::runtime("unexpected flow in regex flip-flop", line)
5285                }
5286            })?;
5287        let left_m = left_re.is_match(&subject);
5288        let right_m = right_re.is_match(&subject);
5289        if self.flip_flop_active.len() <= slot {
5290            self.flip_flop_active.resize(slot + 1, false);
5291        }
5292        if self.flip_flop_exclusive_left_line.len() <= slot {
5293            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5294        }
5295        let active = &mut self.flip_flop_active[slot];
5296        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5297        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
5298            active, excl_left, exclusive, dot, left_m, right_m,
5299        )))
5300    }
5301
5302    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
5303    pub(crate) fn regex_flip_flop_eval_dynamic_right(
5304        &mut self,
5305        left_pat: &str,
5306        left_flags: &str,
5307        slot: usize,
5308        exclusive: bool,
5309        line: usize,
5310        right_m: bool,
5311    ) -> PerlResult<PerlValue> {
5312        let dot = self.scalar_flipflop_dot_line();
5313        let subject = self.scope.get_scalar("_").to_string();
5314        let left_re = self
5315            .compile_regex(left_pat, left_flags, line)
5316            .map_err(|e| match e {
5317                FlowOrError::Error(err) => err,
5318                FlowOrError::Flow(_) => {
5319                    PerlError::runtime("unexpected flow in regex flip-flop", line)
5320                }
5321            })?;
5322        let left_m = left_re.is_match(&subject);
5323        if self.flip_flop_active.len() <= slot {
5324            self.flip_flop_active.resize(slot + 1, false);
5325        }
5326        if self.flip_flop_exclusive_left_line.len() <= slot {
5327            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5328        }
5329        let active = &mut self.flip_flop_active[slot];
5330        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5331        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
5332            active, excl_left, exclusive, dot, left_m, right_m,
5333        )))
5334    }
5335
5336    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
5337    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
5338        &mut self,
5339        left_pat: &str,
5340        left_flags: &str,
5341        slot: usize,
5342        exclusive: bool,
5343        line: usize,
5344        rhs_line: i64,
5345    ) -> PerlResult<PerlValue> {
5346        let dot = self.scalar_flipflop_dot_line();
5347        let subject = self.scope.get_scalar("_").to_string();
5348        let left_re = self
5349            .compile_regex(left_pat, left_flags, line)
5350            .map_err(|e| match e {
5351                FlowOrError::Error(err) => err,
5352                FlowOrError::Flow(_) => {
5353                    PerlError::runtime("unexpected flow in regex flip-flop", line)
5354                }
5355            })?;
5356        let left_m = left_re.is_match(&subject);
5357        let right_m = dot == rhs_line;
5358        if self.flip_flop_active.len() <= slot {
5359            self.flip_flop_active.resize(slot + 1, false);
5360        }
5361        if self.flip_flop_exclusive_left_line.len() <= slot {
5362            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5363        }
5364        let active = &mut self.flip_flop_active[slot];
5365        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5366        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
5367            active, excl_left, exclusive, dot, left_m, right_m,
5368        )))
5369    }
5370
5371    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
5372    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
5373    /// right test until `$.` is strictly past the line where the left regex matched (same as
5374    /// [`Self::regex_flip_flop_eval`]).
5375    pub(crate) fn regex_eof_flip_flop_eval(
5376        &mut self,
5377        left_pat: &str,
5378        left_flags: &str,
5379        slot: usize,
5380        exclusive: bool,
5381        line: usize,
5382    ) -> PerlResult<PerlValue> {
5383        let dot = self.scalar_flipflop_dot_line();
5384        let subject = self.scope.get_scalar("_").to_string();
5385        let left_re = self
5386            .compile_regex(left_pat, left_flags, line)
5387            .map_err(|e| match e {
5388                FlowOrError::Error(err) => err,
5389                FlowOrError::Flow(_) => {
5390                    PerlError::runtime("unexpected flow in regex/eof flip-flop", line)
5391                }
5392            })?;
5393        let left_m = left_re.is_match(&subject);
5394        let right_m = self.eof_without_arg_is_true();
5395        if self.flip_flop_active.len() <= slot {
5396            self.flip_flop_active.resize(slot + 1, false);
5397        }
5398        if self.flip_flop_exclusive_left_line.len() <= slot {
5399            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5400        }
5401        let active = &mut self.flip_flop_active[slot];
5402        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5403        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
5404            active, excl_left, exclusive, dot, left_m, right_m,
5405        )))
5406    }
5407
5408    /// Shared `chomp` implementation (mutates `target`).
5409    /// `read(FH, $buf, LEN)` — read from filehandle into named variable.
5410    /// Returns bytes read count (or error). Called from VM's ReadIntoVar op.
5411    pub(crate) fn builtin_read_into(
5412        &mut self,
5413        fh_val: PerlValue,
5414        var_name: &str,
5415        length: usize,
5416        line: usize,
5417    ) -> ExecResult {
5418        use std::io::Read;
5419        let fh = fh_val
5420            .as_io_handle_name()
5421            .unwrap_or_else(|| fh_val.to_string());
5422        let mut buf = vec![0u8; length];
5423        let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
5424            slot.lock().read(&mut buf).unwrap_or(0)
5425        } else if fh == "STDIN" {
5426            std::io::stdin().read(&mut buf).unwrap_or(0)
5427        } else {
5428            return Err(PerlError::runtime(format!("read: unopened handle {}", fh), line).into());
5429        };
5430        buf.truncate(n);
5431        let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
5432        let _ = self.scope.set_scalar(var_name, PerlValue::string(read_str));
5433        Ok(PerlValue::integer(n as i64))
5434    }
5435
5436    pub(crate) fn chomp_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
5437        // Perl's `chomp` on `@arr` / `%hash` iterates and chomps every
5438        // element in place, returning the *total count* of newlines
5439        // removed. Pre-fix this collapsed the array/hash to its
5440        // stringified form, chomped that, and reassigned a scalar
5441        // back — silently destroying the container.
5442        match &target.kind {
5443            ExprKind::ArrayVar(name) => {
5444                let arr = self.scope.get_array(name);
5445                let mut total = 0i64;
5446                let mut new_arr = Vec::with_capacity(arr.len());
5447                for v in arr {
5448                    let mut s = v.to_string();
5449                    if s.ends_with('\n') {
5450                        s.pop();
5451                        total += 1;
5452                    }
5453                    new_arr.push(PerlValue::string(s));
5454                }
5455                self.scope
5456                    .set_array(name, new_arr)
5457                    .map_err(FlowOrError::Error)?;
5458                return Ok(PerlValue::integer(total));
5459            }
5460            ExprKind::HashVar(name) => {
5461                let h = self.scope.get_hash(name);
5462                let mut total = 0i64;
5463                let mut new_h: indexmap::IndexMap<String, PerlValue> =
5464                    indexmap::IndexMap::with_capacity(h.len());
5465                for (k, v) in h {
5466                    let mut s = v.to_string();
5467                    if s.ends_with('\n') {
5468                        s.pop();
5469                        total += 1;
5470                    }
5471                    new_h.insert(k, PerlValue::string(s));
5472                }
5473                self.scope
5474                    .set_hash(name, new_h)
5475                    .map_err(FlowOrError::Error)?;
5476                return Ok(PerlValue::integer(total));
5477            }
5478            _ => {}
5479        }
5480        let mut s = val.to_string();
5481        let removed = if s.ends_with('\n') {
5482            s.pop();
5483            1i64
5484        } else {
5485            0i64
5486        };
5487        self.assign_value(target, PerlValue::string(s))?;
5488        Ok(PerlValue::integer(removed))
5489    }
5490
5491    /// Shared `chop` implementation (mutates `target`).
5492    pub(crate) fn chop_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
5493        // Perl's `chop @arr` / `chop %hash` chops every element in
5494        // place and returns the *last character chopped*. Without
5495        // this branch the call stringified the whole container,
5496        // chopped one byte off the joined form, and reassigned a
5497        // scalar back — destroying the array.
5498        match &target.kind {
5499            ExprKind::ArrayVar(name) => {
5500                let arr = self.scope.get_array(name);
5501                let mut last = PerlValue::UNDEF;
5502                let mut new_arr = Vec::with_capacity(arr.len());
5503                for v in arr {
5504                    let mut s = v.to_string();
5505                    if let Some(c) = s.pop() {
5506                        last = PerlValue::string(c.to_string());
5507                    }
5508                    new_arr.push(PerlValue::string(s));
5509                }
5510                self.scope
5511                    .set_array(name, new_arr)
5512                    .map_err(FlowOrError::Error)?;
5513                return Ok(last);
5514            }
5515            ExprKind::HashVar(name) => {
5516                let h = self.scope.get_hash(name);
5517                let mut last = PerlValue::UNDEF;
5518                let mut new_h: indexmap::IndexMap<String, PerlValue> =
5519                    indexmap::IndexMap::with_capacity(h.len());
5520                for (k, v) in h {
5521                    let mut s = v.to_string();
5522                    if let Some(c) = s.pop() {
5523                        last = PerlValue::string(c.to_string());
5524                    }
5525                    new_h.insert(k, PerlValue::string(s));
5526                }
5527                self.scope
5528                    .set_hash(name, new_h)
5529                    .map_err(FlowOrError::Error)?;
5530                return Ok(last);
5531            }
5532            _ => {}
5533        }
5534        let mut s = val.to_string();
5535        let chopped = s
5536            .pop()
5537            .map(|c| PerlValue::string(c.to_string()))
5538            .unwrap_or(PerlValue::UNDEF);
5539        self.assign_value(target, PerlValue::string(s))?;
5540        Ok(chopped)
5541    }
5542
5543    /// Shared regex match implementation (`pos` is updated for scalar `/g`).
5544    pub(crate) fn regex_match_execute(
5545        &mut self,
5546        s: String,
5547        pattern: &str,
5548        flags: &str,
5549        scalar_g: bool,
5550        pos_key: &str,
5551        line: usize,
5552    ) -> ExecResult {
5553        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
5554        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
5555        // also keep per-pattern `pos()` state that the memo doesn't track.
5556        //
5557        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
5558        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
5559        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
5560        if !flags.contains('g') && !scalar_g {
5561            let memo_hit = {
5562                if let Some(ref mem) = self.regex_match_memo {
5563                    mem.pattern == pattern
5564                        && mem.flags == flags
5565                        && mem.multiline == self.multiline_match
5566                        && mem.haystack == s
5567                } else {
5568                    false
5569                }
5570            };
5571            if memo_hit {
5572                if self.regex_capture_scope_fresh {
5573                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
5574                }
5575                // Memo hit but scope side effects were invalidated. Re-apply captures
5576                // from the memoized haystack + a fresh compiled regex.
5577                let (memo_s, memo_result) = {
5578                    let mem = self.regex_match_memo.as_ref().expect("memo");
5579                    (mem.haystack.clone(), mem.result.clone())
5580                };
5581                let re = self.compile_regex(pattern, flags, line)?;
5582                if let Some(caps) = re.captures(&memo_s) {
5583                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
5584                }
5585                self.regex_capture_scope_fresh = true;
5586                return Ok(memo_result);
5587            }
5588        }
5589        let re = self.compile_regex(pattern, flags, line)?;
5590        if flags.contains('g') && scalar_g {
5591            let key = pos_key.to_string();
5592            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
5593            if start == 0 {
5594                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5595            }
5596            if start > s.len() {
5597                self.regex_pos.insert(key, None);
5598                return Ok(PerlValue::integer(0));
5599            }
5600            let sub = s.get(start..).unwrap_or("");
5601            if let Some(caps) = re.captures(sub) {
5602                let overall = caps.get(0).expect("capture 0");
5603                let abs_end = start + overall.end;
5604                self.regex_pos.insert(key, Some(abs_end));
5605                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
5606                Ok(PerlValue::integer(1))
5607            } else {
5608                self.regex_pos.insert(key, None);
5609                Ok(PerlValue::integer(0))
5610            }
5611        } else if flags.contains('g') {
5612            let mut rows = Vec::new();
5613            let mut last_caps: Option<PerlCaptures<'_>> = None;
5614            for caps in re.captures_iter(&s) {
5615                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5616                    &caps,
5617                )));
5618                last_caps = Some(caps);
5619            }
5620            self.scope.set_array("^CAPTURE_ALL", rows)?;
5621            let matches: Vec<PerlValue> = match &*re {
5622                PerlCompiledRegex::Rust(r) => r
5623                    .find_iter(&s)
5624                    .map(|m| PerlValue::string(m.as_str().to_string()))
5625                    .collect(),
5626                PerlCompiledRegex::Fancy(r) => r
5627                    .find_iter(&s)
5628                    .filter_map(|m| m.ok())
5629                    .map(|m| PerlValue::string(m.as_str().to_string()))
5630                    .collect(),
5631                PerlCompiledRegex::Pcre2(r) => r
5632                    .find_iter(s.as_bytes())
5633                    .filter_map(|m| m.ok())
5634                    .map(|m| {
5635                        let t = s.get(m.start()..m.end()).unwrap_or("");
5636                        PerlValue::string(t.to_string())
5637                    })
5638                    .collect(),
5639            };
5640            if matches.is_empty() {
5641                Ok(PerlValue::integer(0))
5642            } else {
5643                if let Some(caps) = last_caps {
5644                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
5645                }
5646                Ok(PerlValue::array(matches))
5647            }
5648        } else if let Some(caps) = re.captures(&s) {
5649            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
5650            let result = PerlValue::integer(1);
5651            self.regex_match_memo = Some(RegexMatchMemo {
5652                pattern: pattern.to_string(),
5653                flags: flags.to_string(),
5654                multiline: self.multiline_match,
5655                haystack: s,
5656                result: result.clone(),
5657            });
5658            self.regex_capture_scope_fresh = true;
5659            Ok(result)
5660        } else {
5661            let result = PerlValue::integer(0);
5662            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
5663            self.regex_match_memo = Some(RegexMatchMemo {
5664                pattern: pattern.to_string(),
5665                flags: flags.to_string(),
5666                multiline: self.multiline_match,
5667                haystack: s,
5668                result: result.clone(),
5669            });
5670            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
5671            // the last successful match (if any) set them to. Don't flip the flag.
5672            Ok(result)
5673        }
5674    }
5675
5676    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
5677    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
5678    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
5679    pub(crate) fn expand_env_braces_in_subst(
5680        &mut self,
5681        raw: &str,
5682        line: usize,
5683    ) -> PerlResult<String> {
5684        self.materialize_env_if_needed();
5685        let mut out = String::new();
5686        let mut rest = raw;
5687        while let Some(idx) = rest.find("$ENV{") {
5688            out.push_str(&rest[..idx]);
5689            let after = &rest[idx + 5..];
5690            let end = after
5691                .find('}')
5692                .ok_or_else(|| PerlError::runtime("Unclosed $ENV{...} in s///", line))?;
5693            let key = &after[..end];
5694            let val = self.scope.get_hash_element("ENV", key);
5695            out.push_str(&val.to_string());
5696            rest = &after[end + 1..];
5697        }
5698        out.push_str(rest);
5699        Ok(out)
5700    }
5701
5702    /// Shared `s///` implementation.
5703    ///
5704    /// Perl replacement strings accept both `\1` and `$1` for back-references.
5705    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
5706    /// understand `$N`, so we normalise here.
5707    pub(crate) fn regex_subst_execute(
5708        &mut self,
5709        s: String,
5710        pattern: &str,
5711        replacement: &str,
5712        flags: &str,
5713        target: &Expr,
5714        line: usize,
5715    ) -> ExecResult {
5716        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
5717        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
5718        let re = self.compile_regex(&pattern, &re_flags, line)?;
5719        if flags.contains('e') {
5720            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
5721        }
5722        let replacement = self.expand_env_braces_in_subst(replacement, line)?;
5723        let replacement = self.interpolate_replacement_string(&replacement);
5724        let replacement = normalize_replacement_backrefs(&replacement);
5725        let last_caps = if flags.contains('g') {
5726            let mut rows = Vec::new();
5727            let mut last = None;
5728            for caps in re.captures_iter(&s) {
5729                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5730                    &caps,
5731                )));
5732                last = Some(caps);
5733            }
5734            self.scope.set_array("^CAPTURE_ALL", rows)?;
5735            last
5736        } else {
5737            re.captures(&s)
5738        };
5739        if let Some(caps) = last_caps {
5740            let mode = if flags.contains('g') {
5741                CaptureAllMode::Skip
5742            } else {
5743                CaptureAllMode::Empty
5744            };
5745            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
5746        }
5747        let (new_s, count) = if flags.contains('g') {
5748            let count = re.find_iter_count(&s);
5749            (re.replace_all(&s, replacement.as_str()), count)
5750        } else {
5751            let count = if re.is_match(&s) { 1 } else { 0 };
5752            (re.replace(&s, replacement.as_str()), count)
5753        };
5754        if flags.contains('r') {
5755            // /r — non-destructive: return the modified string, leave target unchanged
5756            Ok(PerlValue::string(new_s))
5757        } else {
5758            self.assign_value(target, PerlValue::string(new_s))?;
5759            Ok(PerlValue::integer(count as i64))
5760        }
5761    }
5762
5763    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
5764    /// and executes the string; the next round uses [`PerlValue::to_string`] of the prior value).
5765    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
5766        let prep_source = |raw: &str| -> String {
5767            let mut code = raw.trim().to_string();
5768            if !code.ends_with(';') {
5769                code.push(';');
5770            }
5771            code
5772        };
5773        let mut cur = prep_source(replacement);
5774        let mut last = PerlValue::UNDEF;
5775        for round in 0..e_count {
5776            last = crate::parse_and_run_string(&cur, self)?;
5777            if round + 1 < e_count {
5778                cur = prep_source(&last.to_string());
5779            }
5780        }
5781        Ok(last)
5782    }
5783
5784    fn regex_subst_execute_eval(
5785        &mut self,
5786        s: String,
5787        re: &PerlCompiledRegex,
5788        replacement: &str,
5789        flags: &str,
5790        target: &Expr,
5791        line: usize,
5792    ) -> ExecResult {
5793        let e_count = flags.chars().filter(|c| *c == 'e').count();
5794        if e_count == 0 {
5795            return Err(PerlError::runtime("s///e: internal error (no e flag)", line).into());
5796        }
5797
5798        if flags.contains('g') {
5799            let mut rows = Vec::new();
5800            let mut out = String::new();
5801            let mut last = 0usize;
5802            let mut count = 0usize;
5803            for caps in re.captures_iter(&s) {
5804                let m0 = caps.get(0).expect("regex capture 0");
5805                out.push_str(&s[last..m0.start]);
5806                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5807                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5808                out.push_str(&repl_val.to_string());
5809                last = m0.end;
5810                count += 1;
5811                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5812                    &caps,
5813                )));
5814            }
5815            self.scope.set_array("^CAPTURE_ALL", rows)?;
5816            out.push_str(&s[last..]);
5817            if flags.contains('r') {
5818                return Ok(PerlValue::string(out));
5819            }
5820            self.assign_value(target, PerlValue::string(out))?;
5821            return Ok(PerlValue::integer(count as i64));
5822        }
5823        if let Some(caps) = re.captures(&s) {
5824            let m0 = caps.get(0).expect("regex capture 0");
5825            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5826            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5827            let mut out = String::new();
5828            out.push_str(&s[..m0.start]);
5829            out.push_str(&repl_val.to_string());
5830            out.push_str(&s[m0.end..]);
5831            if flags.contains('r') {
5832                return Ok(PerlValue::string(out));
5833            }
5834            self.assign_value(target, PerlValue::string(out))?;
5835            return Ok(PerlValue::integer(1));
5836        }
5837        if flags.contains('r') {
5838            return Ok(PerlValue::string(s));
5839        }
5840        self.assign_value(target, PerlValue::string(s))?;
5841        Ok(PerlValue::integer(0))
5842    }
5843
5844    /// Shared `tr///` implementation.
5845    pub(crate) fn regex_transliterate_execute(
5846        &mut self,
5847        s: String,
5848        from: &str,
5849        to: &str,
5850        flags: &str,
5851        target: &Expr,
5852        line: usize,
5853    ) -> ExecResult {
5854        let _ = line;
5855        let from_chars = Self::tr_expand_ranges(from);
5856        let to_chars = Self::tr_expand_ranges(to);
5857        let delete_mode = flags.contains('d');
5858        let mut count = 0i64;
5859        let new_s: String = s
5860            .chars()
5861            .filter_map(|c| {
5862                if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
5863                    count += 1;
5864                    if delete_mode {
5865                        // /d — delete characters that match but have no replacement
5866                        if pos < to_chars.len() {
5867                            Some(to_chars[pos])
5868                        } else {
5869                            None // delete this character
5870                        }
5871                    } else {
5872                        // Normal mode: use last char in to_chars if pos exceeds, or keep original
5873                        Some(to_chars.get(pos).or(to_chars.last()).copied().unwrap_or(c))
5874                    }
5875                } else {
5876                    Some(c)
5877                }
5878            })
5879            .collect();
5880        if flags.contains('r') {
5881            // /r — non-destructive: return the modified string, leave target unchanged
5882            Ok(PerlValue::string(new_s))
5883        } else {
5884            self.assign_value(target, PerlValue::string(new_s))?;
5885            Ok(PerlValue::integer(count))
5886        }
5887    }
5888
5889    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
5890    /// A literal `-` at the start or end of the spec is kept as-is.
5891    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
5892        let raw: Vec<char> = spec.chars().collect();
5893        let mut out = Vec::with_capacity(raw.len());
5894        let mut i = 0;
5895        while i < raw.len() {
5896            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
5897                let start = raw[i] as u32;
5898                let end = raw[i + 2] as u32;
5899                for code in start..=end {
5900                    if let Some(c) = char::from_u32(code) {
5901                        out.push(c);
5902                    }
5903                }
5904                i += 3;
5905            } else {
5906                out.push(raw[i]);
5907                i += 1;
5908            }
5909        }
5910        out
5911    }
5912
5913    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
5914    pub(crate) fn splice_builtin_execute(
5915        &mut self,
5916        args: &[PerlValue],
5917        line: usize,
5918    ) -> PerlResult<PerlValue> {
5919        if args.is_empty() {
5920            return Err(PerlError::runtime("splice: missing array", line));
5921        }
5922        let arr_name = args[0].to_string();
5923        let arr_len = self.scope.array_len(&arr_name);
5924        let offset_val = args
5925            .get(1)
5926            .cloned()
5927            .unwrap_or_else(|| PerlValue::integer(0));
5928        let length_val = match args.get(2) {
5929            None => PerlValue::UNDEF,
5930            Some(v) => v.clone(),
5931        };
5932        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
5933        let rep_vals: Vec<PerlValue> = args.iter().skip(3).cloned().collect();
5934        let removed = self.scope.splice_in_place(&arr_name, off, end, rep_vals)?;
5935        Ok(match self.wantarray_kind {
5936            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
5937            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
5938        })
5939    }
5940
5941    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
5942    pub(crate) fn unshift_builtin_execute(
5943        &mut self,
5944        args: &[PerlValue],
5945        line: usize,
5946    ) -> PerlResult<PerlValue> {
5947        if args.is_empty() {
5948            return Err(PerlError::runtime("unshift: missing array", line));
5949        }
5950        let arr_name = args[0].to_string();
5951        let mut flat_vals: Vec<PerlValue> = Vec::new();
5952        for a in args.iter().skip(1) {
5953            if let Some(items) = a.as_array_vec() {
5954                flat_vals.extend(items);
5955            } else {
5956                flat_vals.push(a.clone());
5957            }
5958        }
5959        let arr = self.scope.get_array_mut(&arr_name)?;
5960        for (i, v) in flat_vals.into_iter().enumerate() {
5961            arr.insert(i, v);
5962        }
5963        Ok(PerlValue::integer(arr.len() as i64))
5964    }
5965
5966    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
5967    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
5968    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
5969        if upper == 0.0 {
5970            self.rand_rng.gen_range(0.0..1.0)
5971        } else if upper > 0.0 {
5972            self.rand_rng.gen_range(0.0..upper)
5973        } else {
5974            self.rand_rng.gen_range(upper..0.0)
5975        }
5976    }
5977
5978    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
5979    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
5980        let n = if let Some(s) = seed {
5981            s as i64
5982        } else {
5983            std::time::SystemTime::now()
5984                .duration_since(std::time::UNIX_EPOCH)
5985                .map(|d| d.as_secs() as i64)
5986                .unwrap_or(1)
5987        };
5988        let mag = n.unsigned_abs();
5989        self.rand_rng = StdRng::seed_from_u64(mag);
5990        n.abs()
5991    }
5992
5993    pub fn set_file(&mut self, file: &str) {
5994        self.file = file.to_string();
5995    }
5996
5997    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
5998    pub fn repl_completion_names(&self) -> Vec<String> {
5999        let mut v = self.scope.repl_binding_names();
6000        v.extend(self.subs.keys().cloned());
6001        v.sort();
6002        v.dedup();
6003        v
6004    }
6005
6006    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
6007    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
6008        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
6009        subs.sort();
6010        let mut classes: HashSet<String> = HashSet::new();
6011        for k in &subs {
6012            if let Some((pkg, rest)) = k.split_once("::") {
6013                if !rest.contains("::") {
6014                    classes.insert(pkg.to_string());
6015                }
6016            }
6017        }
6018        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
6019        for bn in self.scope.repl_binding_names() {
6020            if let Some(r) = bn.strip_prefix('$') {
6021                let v = self.scope.get_scalar(r);
6022                if let Some(b) = v.as_blessed_ref() {
6023                    blessed_scalars.insert(r.to_string(), b.class.clone());
6024                    classes.insert(b.class.clone());
6025                }
6026            }
6027        }
6028        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
6029        for c in classes {
6030            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
6031        }
6032        ReplCompletionSnapshot {
6033            subs,
6034            blessed_scalars,
6035            isa_for_class,
6036        }
6037    }
6038
6039    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
6040        if n == 0 {
6041            return Err(FlowOrError::Error(PerlError::runtime(
6042                "bench: iteration count must be positive",
6043                line,
6044            )));
6045        }
6046        let mut samples = Vec::with_capacity(n);
6047        for _ in 0..n {
6048            let start = std::time::Instant::now();
6049            self.exec_block(body)?;
6050            samples.push(start.elapsed().as_secs_f64() * 1000.0);
6051        }
6052        let mut sorted = samples.clone();
6053        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
6054        let min_ms = sorted[0];
6055        let mean = samples.iter().sum::<f64>() / n as f64;
6056        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
6057            .saturating_sub(1)
6058            .min(n - 1);
6059        let p99_ms = sorted[p99_idx];
6060        Ok(PerlValue::string(format!(
6061            "bench: n={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
6062            n, min_ms, mean, p99_ms
6063        )))
6064    }
6065
6066    pub fn execute(&mut self, program: &Program) -> PerlResult<PerlValue> {
6067        // Snapshot the (possibly empty) class registry into the
6068        // thread-local that the free-function serializers consult, so
6069        // that `to_json($obj)` can resolve inheritance fields without
6070        // taking a `&VMHelper`. Done unconditionally — cheap clone of
6071        // an Arc<HashMap>-shaped structure.
6072        crate::serialize_normalize::install_class_defs(self.class_defs.clone());
6073        // `-n`/`-p`: compile and run only the prelude, store chunk for per-line re-execution.
6074        if self.line_mode_skip_main {
6075            crate::compile_and_run_prelude(program, self)?;
6076            return Ok(PerlValue::UNDEF);
6077        }
6078        crate::try_vm_execute(program, self)
6079            .expect("VM compilation must succeed — all execution is VM-only")
6080    }
6081
6082    /// Run `END` blocks (after `-n`/`-p` line loop when prelude used [`Self::line_mode_skip_main`]).
6083    pub fn run_end_blocks(&mut self) -> PerlResult<()> {
6084        self.global_phase = "END".to_string();
6085        let ends = std::mem::take(&mut self.end_blocks);
6086        for block in &ends {
6087            self.exec_block(block).map_err(|e| match e {
6088                FlowOrError::Error(e) => e,
6089                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in END", 0),
6090            })?;
6091        }
6092        Ok(())
6093    }
6094
6095    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
6096    /// and drain remaining `DESTROY` callbacks.
6097    pub fn run_global_teardown(&mut self) -> PerlResult<()> {
6098        self.global_phase = "DESTRUCT".to_string();
6099        self.drain_pending_destroys(0)
6100    }
6101
6102    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
6103    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> PerlResult<()> {
6104        loop {
6105            let batch = crate::pending_destroy::take_queue();
6106            if batch.is_empty() {
6107                break;
6108            }
6109            for (class, payload) in batch {
6110                let fq = format!("{}::DESTROY", class);
6111                let Some(sub) = self.subs.get(&fq).cloned() else {
6112                    continue;
6113                };
6114                let inv = PerlValue::blessed(Arc::new(
6115                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
6116                ));
6117                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
6118                    Ok(_) => {}
6119                    Err(FlowOrError::Error(e)) => return Err(e),
6120                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
6121                    Err(FlowOrError::Flow(other)) => {
6122                        return Err(PerlError::runtime(
6123                            format!("DESTROY: unexpected control flow ({other:?})"),
6124                            line,
6125                        ));
6126                    }
6127                }
6128            }
6129        }
6130        Ok(())
6131    }
6132
6133    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
6134        self.exec_block_with_tail(block, WantarrayCtx::Void)
6135    }
6136
6137    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
6138    /// Non-final statements stay void context.
6139    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6140        let uses_goto = block
6141            .iter()
6142            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
6143        if uses_goto {
6144            self.scope_push_hook();
6145            let r = self.exec_block_with_goto_tail(block, tail);
6146            self.scope_pop_hook();
6147            r
6148        } else {
6149            self.scope_push_hook();
6150            let result = self.exec_block_no_scope_with_tail(block, tail);
6151            self.scope_pop_hook();
6152            result
6153        }
6154    }
6155
6156    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
6157        let mut map: HashMap<String, usize> = HashMap::new();
6158        for (i, s) in block.iter().enumerate() {
6159            if let Some(l) = &s.label {
6160                map.insert(l.clone(), i);
6161            }
6162        }
6163        let mut pc = 0usize;
6164        let mut last = PerlValue::UNDEF;
6165        let last_idx = block.len().saturating_sub(1);
6166        while pc < block.len() {
6167            if let StmtKind::Goto { target } = &block[pc].kind {
6168                let line = block[pc].line;
6169                let name = self.eval_expr(target)?.to_string();
6170                pc = *map.get(&name).ok_or_else(|| {
6171                    FlowOrError::Error(PerlError::runtime(
6172                        format!("goto: unknown label {}", name),
6173                        line,
6174                    ))
6175                })?;
6176                continue;
6177            }
6178            let v = if pc == last_idx {
6179                match &block[pc].kind {
6180                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
6181                    _ => self.exec_statement(&block[pc])?,
6182                }
6183            } else {
6184                self.exec_statement(&block[pc])?
6185            };
6186            last = v;
6187            pc += 1;
6188        }
6189        Ok(last)
6190    }
6191
6192    /// Execute block statements without pushing/popping a scope frame.
6193    /// Used internally by loops and the VM for sub calls.
6194    #[inline]
6195    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
6196        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
6197    }
6198
6199    pub(crate) fn exec_block_no_scope_with_tail(
6200        &mut self,
6201        block: &Block,
6202        tail: WantarrayCtx,
6203    ) -> ExecResult {
6204        if block.is_empty() {
6205            return Ok(PerlValue::UNDEF);
6206        }
6207        let last_i = block.len() - 1;
6208        for (i, stmt) in block.iter().enumerate() {
6209            if i < last_i {
6210                self.exec_statement(stmt)?;
6211            } else {
6212                return match &stmt.kind {
6213                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
6214                    _ => self.exec_statement(stmt),
6215                };
6216            }
6217        }
6218        Ok(PerlValue::UNDEF)
6219    }
6220
6221    /// Spawn `block` on a worker thread; returns an [`PerlValue::AsyncTask`] handle (`async { }` / `spawn { }`).
6222    pub(crate) fn spawn_async_block(&self, block: &Block) -> PerlValue {
6223        use parking_lot::Mutex as ParkMutex;
6224
6225        let block = block.clone();
6226        let subs = self.subs.clone();
6227        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6228        let result = Arc::new(ParkMutex::new(None));
6229        let join = Arc::new(ParkMutex::new(None));
6230        let result2 = result.clone();
6231        let h = std::thread::spawn(move || {
6232            let mut interp = VMHelper::new();
6233            interp.subs = subs;
6234            interp.scope.restore_capture(&scalars);
6235            interp.scope.restore_atomics(&aar, &ahash);
6236            interp.enable_parallel_guard();
6237            let r = match interp.exec_block(&block) {
6238                Ok(v) => Ok(v),
6239                Err(FlowOrError::Error(e)) => Err(e),
6240                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6241                    Err(PerlError::runtime("yield inside async/spawn block", 0))
6242                }
6243                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
6244            };
6245            *result2.lock() = Some(r);
6246        });
6247        *join.lock() = Some(h);
6248        PerlValue::async_task(Arc::new(PerlAsyncTask { result, join }))
6249    }
6250
6251    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
6252    pub(crate) fn eval_timeout_block(
6253        &mut self,
6254        body: &Block,
6255        secs: f64,
6256        line: usize,
6257    ) -> ExecResult {
6258        use std::sync::mpsc::channel;
6259        use std::time::Duration;
6260
6261        let block = body.clone();
6262        let subs = self.subs.clone();
6263        let struct_defs = self.struct_defs.clone();
6264        let enum_defs = self.enum_defs.clone();
6265        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
6266        self.materialize_env_if_needed();
6267        let env = self.env.clone();
6268        let argv = self.argv.clone();
6269        let inc = self.scope.get_array("INC");
6270        let (tx, rx) = channel::<PerlResult<PerlValue>>();
6271        let _handle = std::thread::spawn(move || {
6272            let mut interp = VMHelper::new();
6273            interp.subs = subs;
6274            interp.struct_defs = struct_defs;
6275            interp.enum_defs = enum_defs;
6276            interp.env = env.clone();
6277            interp.argv = argv.clone();
6278            interp.scope.declare_array(
6279                "ARGV",
6280                argv.iter().map(|s| PerlValue::string(s.clone())).collect(),
6281            );
6282            for (k, v) in env {
6283                interp
6284                    .scope
6285                    .set_hash_element("ENV", &k, v)
6286                    .expect("set ENV in timeout thread");
6287            }
6288            interp.scope.declare_array("INC", inc);
6289            interp.scope.restore_capture(&scalars);
6290            interp.scope.restore_atomics(&aar, &ahash);
6291            interp.enable_parallel_guard();
6292            let out: PerlResult<PerlValue> = match interp.exec_block(&block) {
6293                Ok(v) => Ok(v),
6294                Err(FlowOrError::Error(e)) => Err(e),
6295                Err(FlowOrError::Flow(Flow::Yield(_))) => {
6296                    Err(PerlError::runtime("yield inside eval_timeout block", 0))
6297                }
6298                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
6299            };
6300            let _ = tx.send(out);
6301        });
6302        let dur = Duration::from_secs_f64(secs.max(0.0));
6303        match rx.recv_timeout(dur) {
6304            Ok(Ok(v)) => Ok(v),
6305            Ok(Err(e)) => Err(FlowOrError::Error(e)),
6306            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(PerlError::runtime(
6307                format!(
6308                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
6309                    secs
6310                ),
6311                line,
6312            )
6313            .into()),
6314            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(PerlError::runtime(
6315                "eval_timeout: worker thread panicked or disconnected",
6316                line,
6317            )
6318            .into()),
6319        }
6320    }
6321
6322    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
6323        let mut last = PerlValue::UNDEF;
6324        for stmt in body {
6325            match &stmt.kind {
6326                StmtKind::When { cond, body: wb } => {
6327                    if self.when_matches(cond)? {
6328                        return self.exec_block_smart(wb);
6329                    }
6330                }
6331                StmtKind::DefaultCase { body: db } => {
6332                    return self.exec_block_smart(db);
6333                }
6334                _ => {
6335                    last = self.exec_statement(stmt)?;
6336                }
6337            }
6338        }
6339        Ok(last)
6340    }
6341
6342    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
6343    pub(crate) fn exec_given_with_topic_value(
6344        &mut self,
6345        topic: PerlValue,
6346        body: &Block,
6347    ) -> ExecResult {
6348        self.scope_push_hook();
6349        self.scope.declare_scalar("_", topic);
6350        self.english_note_lexical_scalar("_");
6351        let r = self.exec_given_body(body);
6352        self.scope_pop_hook();
6353        r
6354    }
6355
6356    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
6357        let t = self.eval_expr(topic)?;
6358        self.exec_given_with_topic_value(t, body)
6359    }
6360
6361    /// `when (COND)` — topic is `$_` (set by `given`).
6362    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6363        let topic = self.scope.get_scalar("_");
6364        let line = cond.line;
6365        match &cond.kind {
6366            ExprKind::Regex(pattern, flags) => {
6367                let re = self.compile_regex(pattern, flags, line)?;
6368                let s = topic.to_string();
6369                Ok(re.is_match(&s))
6370            }
6371            ExprKind::String(s) => Ok(topic.to_string() == *s),
6372            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
6373            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
6374            _ => {
6375                let c = self.eval_expr(cond)?;
6376                Ok(self.smartmatch_when(&topic, &c))
6377            }
6378        }
6379    }
6380
6381    fn smartmatch_when(&self, topic: &PerlValue, c: &PerlValue) -> bool {
6382        if let Some(re) = c.as_regex() {
6383            return re.is_match(&topic.to_string());
6384        }
6385        // ARRAY / array-ref RHS: smartmatch is "any element matches the topic"
6386        // (`$x ~~ @list` → `grep { $x ~~ $_ } @list` per `perlop`).
6387        // Without this branch, `when ([2, 3, 5, 7])` always falls through to
6388        // `default` because the array stringified ("23 5 7"-ish) won't equal
6389        // the scalar. Recurse so nested arrays / regexes inside the array
6390        // still work.
6391        if let Some(arr) = c.as_array_ref() {
6392            let arr = arr.read();
6393            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
6394        }
6395        if let Some(arr) = c.as_array_vec() {
6396            return arr.iter().any(|elem| self.smartmatch_when(topic, elem));
6397        }
6398        // HASH / hash-ref RHS: "topic is a key" (`$x ~~ %h` → `exists $h{$x}`).
6399        if let Some(href) = c.as_hash_ref() {
6400            return href.read().contains_key(&topic.to_string());
6401        }
6402        if let Some(h) = c.as_hash_map() {
6403            return h.contains_key(&topic.to_string());
6404        }
6405        // Coderef RHS: call it with the topic, treat truthy result as match.
6406        if let Some(sub) = c.as_code_ref() {
6407            // smartmatch_when is `&self`; we can't `call_sub` (needs `&mut`).
6408            // For now, fall through to string equality. Future: hoist
6409            // when_matches to use `&mut self` so coderef RHS can fire.
6410            let _ = sub;
6411        }
6412        // Numeric equality if both sides parse as numbers.
6413        if let (Some(a), Some(b)) = (topic.as_integer(), c.as_integer()) {
6414            return a == b;
6415        }
6416        topic.to_string() == c.to_string()
6417    }
6418
6419    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
6420    pub(crate) fn eval_boolean_rvalue_condition(
6421        &mut self,
6422        cond: &Expr,
6423    ) -> Result<bool, FlowOrError> {
6424        match &cond.kind {
6425            ExprKind::Regex(pattern, flags) => {
6426                let topic = self.scope.get_scalar("_");
6427                let line = cond.line;
6428                let s = topic.to_string();
6429                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
6430                Ok(v.is_true())
6431            }
6432            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
6433            ExprKind::ReadLine(_) => {
6434                let v = self.eval_expr(cond)?;
6435                self.scope.set_topic(v.clone());
6436                Ok(!v.is_undef())
6437            }
6438            _ => {
6439                let v = self.eval_expr(cond)?;
6440                Ok(v.is_true())
6441            }
6442        }
6443    }
6444
6445    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
6446    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6447        self.eval_boolean_rvalue_condition(cond)
6448    }
6449
6450    pub(crate) fn eval_algebraic_match(
6451        &mut self,
6452        subject: &Expr,
6453        arms: &[MatchArm],
6454        line: usize,
6455    ) -> ExecResult {
6456        let val = self.eval_algebraic_match_subject(subject, line)?;
6457        self.eval_algebraic_match_with_subject_value(val, arms, line)
6458    }
6459
6460    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
6461    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
6462        match &subject.kind {
6463            ExprKind::ArrayVar(name) => {
6464                self.check_strict_array_var(name, line)?;
6465                let aname = self.stash_array_name_for_package(name);
6466                Ok(PerlValue::array_binding_ref(aname))
6467            }
6468            ExprKind::HashVar(name) => {
6469                self.check_strict_hash_var(name, line)?;
6470                self.touch_env_hash(name);
6471                Ok(PerlValue::hash_binding_ref(name.clone()))
6472            }
6473            _ => self.eval_expr(subject),
6474        }
6475    }
6476
6477    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
6478    pub(crate) fn eval_algebraic_match_with_subject_value(
6479        &mut self,
6480        val: PerlValue,
6481        arms: &[MatchArm],
6482        line: usize,
6483    ) -> ExecResult {
6484        // Exhaustive enum match: check variant coverage before matching
6485        if let Some(e) = val.as_enum_inst() {
6486            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
6487            if !has_catchall {
6488                let covered: Vec<String> = arms
6489                    .iter()
6490                    .filter_map(|a| {
6491                        if let MatchPattern::Value(expr) = &a.pattern {
6492                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
6493                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
6494                            }
6495                        }
6496                        None
6497                    })
6498                    .collect();
6499                let missing: Vec<&str> = e
6500                    .def
6501                    .variants
6502                    .iter()
6503                    .filter(|v| !covered.contains(&v.name))
6504                    .map(|v| v.name.as_str())
6505                    .collect();
6506                if !missing.is_empty() {
6507                    return Err(PerlError::runtime(
6508                        format!(
6509                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
6510                            e.def.name,
6511                            missing.join(", ")
6512                        ),
6513                        line,
6514                    )
6515                    .into());
6516                }
6517            }
6518        }
6519        for arm in arms {
6520            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
6521                let re = self.compile_regex(pattern, flags, line)?;
6522                let s = val.to_string();
6523                if let Some(caps) = re.captures(&s) {
6524                    self.scope_push_hook();
6525                    self.scope.declare_scalar("_", val.clone());
6526                    self.english_note_lexical_scalar("_");
6527                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
6528                    let guard_ok = if let Some(g) = &arm.guard {
6529                        self.eval_expr(g)?.is_true()
6530                    } else {
6531                        true
6532                    };
6533                    if !guard_ok {
6534                        self.scope_pop_hook();
6535                        continue;
6536                    }
6537                    let out = self.eval_expr(&arm.body);
6538                    self.scope_pop_hook();
6539                    return out;
6540                }
6541                continue;
6542            }
6543            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
6544                self.scope_push_hook();
6545                self.scope.declare_scalar("_", val.clone());
6546                self.english_note_lexical_scalar("_");
6547                for b in bindings {
6548                    match b {
6549                        PatternBinding::Scalar(name, v) => {
6550                            self.scope.declare_scalar(&name, v);
6551                            self.english_note_lexical_scalar(&name);
6552                        }
6553                        PatternBinding::Array(name, elems) => {
6554                            self.scope.declare_array(&name, elems);
6555                        }
6556                    }
6557                }
6558                let guard_ok = if let Some(g) = &arm.guard {
6559                    self.eval_expr(g)?.is_true()
6560                } else {
6561                    true
6562                };
6563                if !guard_ok {
6564                    self.scope_pop_hook();
6565                    continue;
6566                }
6567                let out = self.eval_expr(&arm.body);
6568                self.scope_pop_hook();
6569                return out;
6570            }
6571        }
6572        Err(PerlError::runtime(
6573            "match: no arm matched the value (add a `_` catch-all)",
6574            line,
6575        )
6576        .into())
6577    }
6578
6579    fn parse_duration_seconds(pv: &PerlValue) -> Option<f64> {
6580        let s = pv.to_string();
6581        let s = s.trim();
6582        if let Some(rest) = s.strip_suffix("ms") {
6583            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
6584        }
6585        if let Some(rest) = s.strip_suffix('s') {
6586            return rest.trim().parse::<f64>().ok();
6587        }
6588        if let Some(rest) = s.strip_suffix('m') {
6589            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
6590        }
6591        s.parse::<f64>().ok()
6592    }
6593
6594    fn eval_retry_block(
6595        &mut self,
6596        body: &Block,
6597        times: &Expr,
6598        backoff: RetryBackoff,
6599        _line: usize,
6600    ) -> ExecResult {
6601        let max = self.eval_expr(times)?.to_int().max(1) as usize;
6602        let base_ms: u64 = 10;
6603        let mut attempt = 0usize;
6604        loop {
6605            attempt += 1;
6606            match self.exec_block(body) {
6607                Ok(v) => return Ok(v),
6608                Err(FlowOrError::Error(e)) => {
6609                    if attempt >= max {
6610                        return Err(FlowOrError::Error(e));
6611                    }
6612                    let delay_ms = match backoff {
6613                        RetryBackoff::None => 0,
6614                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
6615                        RetryBackoff::Exponential => {
6616                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
6617                        }
6618                    };
6619                    if delay_ms > 0 {
6620                        std::thread::sleep(Duration::from_millis(delay_ms));
6621                    }
6622                }
6623                Err(e) => return Err(e),
6624            }
6625        }
6626    }
6627
6628    fn eval_rate_limit_block(
6629        &mut self,
6630        slot: u32,
6631        max: &Expr,
6632        window: &Expr,
6633        body: &Block,
6634        _line: usize,
6635    ) -> ExecResult {
6636        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
6637        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
6638            .filter(|s| *s > 0.0)
6639            .unwrap_or(1.0);
6640        let window_d = Duration::from_secs_f64(window_sec);
6641        let slot = slot as usize;
6642        while self.rate_limit_slots.len() <= slot {
6643            self.rate_limit_slots.push(VecDeque::new());
6644        }
6645        {
6646            let dq = &mut self.rate_limit_slots[slot];
6647            loop {
6648                let now = Instant::now();
6649                while let Some(t0) = dq.front().copied() {
6650                    if now.duration_since(t0) >= window_d {
6651                        dq.pop_front();
6652                    } else {
6653                        break;
6654                    }
6655                }
6656                if dq.len() < max_n || max_n == 0 {
6657                    break;
6658                }
6659                let t0 = dq.front().copied().unwrap();
6660                let wait = window_d.saturating_sub(now.duration_since(t0));
6661                if wait.is_zero() {
6662                    dq.pop_front();
6663                    continue;
6664                }
6665                std::thread::sleep(wait);
6666            }
6667            dq.push_back(Instant::now());
6668        }
6669        self.exec_block(body)
6670    }
6671
6672    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
6673        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
6674            .filter(|s| *s > 0.0)
6675            .unwrap_or(1.0);
6676        loop {
6677            match self.exec_block(body) {
6678                Ok(_) => {}
6679                Err(e) => return Err(e),
6680            }
6681            std::thread::sleep(Duration::from_secs_f64(sec));
6682        }
6683    }
6684
6685    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
6686    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> PerlResult<PerlValue> {
6687        let pair = |value: PerlValue, more: i64| {
6688            PerlValue::array_ref(Arc::new(RwLock::new(vec![value, PerlValue::integer(more)])))
6689        };
6690        let mut exhausted = gen.exhausted.lock();
6691        if *exhausted {
6692            return Ok(pair(PerlValue::UNDEF, 0));
6693        }
6694        let mut pc = gen.pc.lock();
6695        let mut scope_started = gen.scope_started.lock();
6696        if *pc >= gen.block.len() {
6697            if *scope_started {
6698                self.scope_pop_hook();
6699                *scope_started = false;
6700            }
6701            *exhausted = true;
6702            return Ok(pair(PerlValue::UNDEF, 0));
6703        }
6704        if !*scope_started {
6705            self.scope_push_hook();
6706            *scope_started = true;
6707        }
6708        self.in_generator = true;
6709        while *pc < gen.block.len() {
6710            let stmt = &gen.block[*pc];
6711            match self.exec_statement(stmt) {
6712                Ok(_) => {
6713                    *pc += 1;
6714                }
6715                Err(FlowOrError::Flow(Flow::Yield(v))) => {
6716                    *pc += 1;
6717                    self.in_generator = false;
6718                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
6719                    // binds in the caller block, not inside a frame left across yield.
6720                    if *scope_started {
6721                        self.scope_pop_hook();
6722                        *scope_started = false;
6723                    }
6724                    return Ok(pair(v, 1));
6725                }
6726                Err(e) => {
6727                    self.in_generator = false;
6728                    if *scope_started {
6729                        self.scope_pop_hook();
6730                        *scope_started = false;
6731                    }
6732                    return Err(match e {
6733                        FlowOrError::Error(ee) => ee,
6734                        FlowOrError::Flow(Flow::Yield(_)) => {
6735                            unreachable!("yield handled above")
6736                        }
6737                        FlowOrError::Flow(flow) => PerlError::runtime(
6738                            format!("unexpected control flow in generator: {:?}", flow),
6739                            0,
6740                        ),
6741                    });
6742                }
6743            }
6744        }
6745        self.in_generator = false;
6746        if *scope_started {
6747            self.scope_pop_hook();
6748            *scope_started = false;
6749        }
6750        *exhausted = true;
6751        Ok(pair(PerlValue::UNDEF, 0))
6752    }
6753
6754    fn match_pattern_try(
6755        &mut self,
6756        subject: &PerlValue,
6757        pattern: &MatchPattern,
6758        line: usize,
6759    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6760        match pattern {
6761            MatchPattern::Any => Ok(Some(vec![])),
6762            MatchPattern::Regex { .. } => {
6763                unreachable!("regex arms are handled in eval_algebraic_match")
6764            }
6765            MatchPattern::Value(expr) => {
6766                if self.match_pattern_value_alternation(subject, expr, line)? {
6767                    Ok(Some(vec![]))
6768                } else {
6769                    Ok(None)
6770                }
6771            }
6772            MatchPattern::Array(elems) => {
6773                let Some(arr) = self.match_subject_as_array(subject) else {
6774                    return Ok(None);
6775                };
6776                self.match_array_pattern_elems(&arr, elems, line)
6777            }
6778            MatchPattern::Hash(pairs) => {
6779                let Some(h) = self.match_subject_as_hash(subject) else {
6780                    return Ok(None);
6781                };
6782                self.match_hash_pattern_pairs(&h, pairs, line)
6783            }
6784            MatchPattern::OptionSome(name) => {
6785                let Some(arr) = self.match_subject_as_array(subject) else {
6786                    return Ok(None);
6787                };
6788                if arr.len() < 2 {
6789                    return Ok(None);
6790                }
6791                if !arr[1].is_true() {
6792                    return Ok(None);
6793                }
6794                Ok(Some(vec![PatternBinding::Scalar(
6795                    name.clone(),
6796                    arr[0].clone(),
6797                )]))
6798            }
6799        }
6800    }
6801
6802    /// Handle pattern alternation (e.g., `"foo" | "bar" | "baz"`) in match patterns.
6803    /// If the expression is a BitOr chain, recursively check if subject matches any alternative.
6804    fn match_pattern_value_alternation(
6805        &mut self,
6806        subject: &PerlValue,
6807        expr: &Expr,
6808        _line: usize,
6809    ) -> Result<bool, FlowOrError> {
6810        if let ExprKind::BinOp {
6811            left,
6812            op: BinOp::BitOr,
6813            right,
6814        } = &expr.kind
6815        {
6816            if self.match_pattern_value_alternation(subject, left, _line)? {
6817                return Ok(true);
6818            }
6819            return self.match_pattern_value_alternation(subject, right, _line);
6820        }
6821        let pv = self.eval_expr(expr)?;
6822        Ok(self.smartmatch_when(subject, &pv))
6823    }
6824
6825    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
6826    fn match_subject_as_array(&self, v: &PerlValue) -> Option<Vec<PerlValue>> {
6827        if let Some(a) = v.as_array_vec() {
6828            return Some(a);
6829        }
6830        if let Some(r) = v.as_array_ref() {
6831            return Some(r.read().clone());
6832        }
6833        if let Some(name) = v.as_array_binding_name() {
6834            return Some(self.scope.get_array(&name));
6835        }
6836        None
6837    }
6838
6839    fn match_subject_as_hash(&mut self, v: &PerlValue) -> Option<IndexMap<String, PerlValue>> {
6840        if let Some(h) = v.as_hash_map() {
6841            return Some(h);
6842        }
6843        if let Some(r) = v.as_hash_ref() {
6844            return Some(r.read().clone());
6845        }
6846        if let Some(name) = v.as_hash_binding_name() {
6847            self.touch_env_hash(&name);
6848            return Some(self.scope.get_hash(&name));
6849        }
6850        None
6851    }
6852
6853    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
6854    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
6855    pub(crate) fn hash_slice_deref_values(
6856        &mut self,
6857        container: &PerlValue,
6858        key_values: &[PerlValue],
6859        line: usize,
6860    ) -> Result<PerlValue, FlowOrError> {
6861        let h = if let Some(m) = self.match_subject_as_hash(container) {
6862            m
6863        } else {
6864            return Err(PerlError::runtime(
6865                "Hash slice dereference needs a hash or hash reference value",
6866                line,
6867            )
6868            .into());
6869        };
6870        let mut result = Vec::new();
6871        for kv in key_values {
6872            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6873                vv.iter().map(|x| x.to_string()).collect()
6874            } else {
6875                vec![kv.to_string()]
6876            };
6877            for k in key_strings {
6878                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6879            }
6880        }
6881        Ok(PerlValue::array(result))
6882    }
6883
6884    /// Single-key write for a hash slice container (hash ref or package hash name).
6885    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
6886    pub(crate) fn assign_hash_slice_one_key(
6887        &mut self,
6888        container: PerlValue,
6889        key: &str,
6890        val: PerlValue,
6891        line: usize,
6892    ) -> Result<PerlValue, FlowOrError> {
6893        if let Some(r) = container.as_hash_ref() {
6894            r.write().insert(key.to_string(), val);
6895            return Ok(PerlValue::UNDEF);
6896        }
6897        if let Some(name) = container.as_hash_binding_name() {
6898            self.touch_env_hash(&name);
6899            self.scope
6900                .set_hash_element(&name, key, val)
6901                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6902            return Ok(PerlValue::UNDEF);
6903        }
6904        if let Some(s) = container.as_str() {
6905            self.touch_env_hash(&s);
6906            if self.strict_refs {
6907                return Err(PerlError::runtime(
6908                    format!(
6909                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6910                        s
6911                    ),
6912                    line,
6913                )
6914                .into());
6915            }
6916            self.scope
6917                .set_hash_element(&s, key, val)
6918                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6919            return Ok(PerlValue::UNDEF);
6920        }
6921        Err(PerlError::runtime(
6922            "Hash slice assignment needs a hash or hash reference value",
6923            line,
6924        )
6925        .into())
6926    }
6927
6928    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
6929    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
6930    pub(crate) fn assign_named_hash_slice(
6931        &mut self,
6932        hash: &str,
6933        key_values: Vec<PerlValue>,
6934        val: PerlValue,
6935        line: usize,
6936    ) -> Result<PerlValue, FlowOrError> {
6937        self.touch_env_hash(hash);
6938        let mut ks: Vec<String> = Vec::new();
6939        for kv in key_values {
6940            if let Some(vv) = kv.as_array_vec() {
6941                ks.extend(vv.iter().map(|x| x.to_string()));
6942            } else {
6943                ks.push(kv.to_string());
6944            }
6945        }
6946        if ks.is_empty() {
6947            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6948        }
6949        let items = val.to_list();
6950        for (i, k) in ks.iter().enumerate() {
6951            let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6952            self.scope
6953                .set_hash_element(hash, k, v)
6954                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6955        }
6956        Ok(PerlValue::UNDEF)
6957    }
6958
6959    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
6960    pub(crate) fn assign_hash_slice_deref(
6961        &mut self,
6962        container: PerlValue,
6963        key_values: Vec<PerlValue>,
6964        val: PerlValue,
6965        line: usize,
6966    ) -> Result<PerlValue, FlowOrError> {
6967        let mut ks: Vec<String> = Vec::new();
6968        for kv in key_values {
6969            if let Some(vv) = kv.as_array_vec() {
6970                ks.extend(vv.iter().map(|x| x.to_string()));
6971            } else {
6972                ks.push(kv.to_string());
6973            }
6974        }
6975        if ks.is_empty() {
6976            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6977        }
6978        let items = val.to_list();
6979        if let Some(r) = container.as_hash_ref() {
6980            let mut h = r.write();
6981            for (i, k) in ks.iter().enumerate() {
6982                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6983                h.insert(k.clone(), v);
6984            }
6985            return Ok(PerlValue::UNDEF);
6986        }
6987        if let Some(name) = container.as_hash_binding_name() {
6988            self.touch_env_hash(&name);
6989            for (i, k) in ks.iter().enumerate() {
6990                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6991                self.scope
6992                    .set_hash_element(&name, k, v)
6993                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6994            }
6995            return Ok(PerlValue::UNDEF);
6996        }
6997        if let Some(s) = container.as_str() {
6998            if self.strict_refs {
6999                return Err(PerlError::runtime(
7000                    format!(
7001                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7002                        s
7003                    ),
7004                    line,
7005                )
7006                .into());
7007            }
7008            self.touch_env_hash(&s);
7009            for (i, k) in ks.iter().enumerate() {
7010                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
7011                self.scope
7012                    .set_hash_element(&s, k, v)
7013                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
7014            }
7015            return Ok(PerlValue::UNDEF);
7016        }
7017        Err(PerlError::runtime(
7018            "Hash slice assignment needs a hash or hash reference value",
7019            line,
7020        )
7021        .into())
7022    }
7023
7024    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
7025    /// Perl 5 applies the compound op only to the **last** slice element.
7026    pub(crate) fn compound_assign_hash_slice_deref(
7027        &mut self,
7028        container: PerlValue,
7029        key_values: Vec<PerlValue>,
7030        op: BinOp,
7031        rhs: PerlValue,
7032        line: usize,
7033    ) -> Result<PerlValue, FlowOrError> {
7034        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7035        let last_old = old_list
7036            .to_list()
7037            .last()
7038            .cloned()
7039            .unwrap_or(PerlValue::UNDEF);
7040        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7041        let mut ks: Vec<String> = Vec::new();
7042        for kv in &key_values {
7043            if let Some(vv) = kv.as_array_vec() {
7044                ks.extend(vv.iter().map(|x| x.to_string()));
7045            } else {
7046                ks.push(kv.to_string());
7047            }
7048        }
7049        if ks.is_empty() {
7050            return Err(PerlError::runtime("assign to empty hash slice", line).into());
7051        }
7052        let last_key = ks.last().expect("non-empty ks");
7053        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7054        Ok(new_val)
7055    }
7056
7057    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
7058    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
7059    /// the **old** value of that last element.
7060    ///
7061    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
7062    pub(crate) fn hash_slice_deref_inc_dec(
7063        &mut self,
7064        container: PerlValue,
7065        key_values: Vec<PerlValue>,
7066        kind: u8,
7067        line: usize,
7068    ) -> Result<PerlValue, FlowOrError> {
7069        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
7070        let last_old = old_list
7071            .to_list()
7072            .last()
7073            .cloned()
7074            .unwrap_or(PerlValue::UNDEF);
7075        let new_val = if kind & 1 == 0 {
7076            PerlValue::integer(last_old.to_int() + 1)
7077        } else {
7078            PerlValue::integer(last_old.to_int() - 1)
7079        };
7080        let mut ks: Vec<String> = Vec::new();
7081        for kv in &key_values {
7082            if let Some(vv) = kv.as_array_vec() {
7083                ks.extend(vv.iter().map(|x| x.to_string()));
7084            } else {
7085                ks.push(kv.to_string());
7086            }
7087        }
7088        let last_key = ks.last().ok_or_else(|| {
7089            PerlError::runtime("Hash slice increment needs at least one key", line)
7090        })?;
7091        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7092        Ok(if kind < 2 { new_val } else { last_old })
7093    }
7094
7095    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[PerlValue]) -> PerlValue {
7096        self.touch_env_hash(hash);
7097        let h = self.scope.get_hash(hash);
7098        let mut result = Vec::new();
7099        for kv in key_values {
7100            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
7101                vv.iter().map(|x| x.to_string()).collect()
7102            } else {
7103                vec![kv.to_string()]
7104            };
7105            for k in key_strings {
7106                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
7107            }
7108        }
7109        PerlValue::array(result)
7110    }
7111
7112    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
7113    pub(crate) fn compound_assign_named_hash_slice(
7114        &mut self,
7115        hash: &str,
7116        key_values: Vec<PerlValue>,
7117        op: BinOp,
7118        rhs: PerlValue,
7119        line: usize,
7120    ) -> Result<PerlValue, FlowOrError> {
7121        let old_list = self.hash_slice_named_values(hash, &key_values);
7122        let last_old = old_list
7123            .to_list()
7124            .last()
7125            .cloned()
7126            .unwrap_or(PerlValue::UNDEF);
7127        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
7128        let mut ks: Vec<String> = Vec::new();
7129        for kv in &key_values {
7130            if let Some(vv) = kv.as_array_vec() {
7131                ks.extend(vv.iter().map(|x| x.to_string()));
7132            } else {
7133                ks.push(kv.to_string());
7134            }
7135        }
7136        if ks.is_empty() {
7137            return Err(PerlError::runtime("assign to empty hash slice", line).into());
7138        }
7139        let last_key = ks.last().expect("non-empty ks");
7140        let container = PerlValue::string(hash.to_string());
7141        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7142        Ok(new_val)
7143    }
7144
7145    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
7146    pub(crate) fn named_hash_slice_inc_dec(
7147        &mut self,
7148        hash: &str,
7149        key_values: Vec<PerlValue>,
7150        kind: u8,
7151        line: usize,
7152    ) -> Result<PerlValue, FlowOrError> {
7153        let old_list = self.hash_slice_named_values(hash, &key_values);
7154        let last_old = old_list
7155            .to_list()
7156            .last()
7157            .cloned()
7158            .unwrap_or(PerlValue::UNDEF);
7159        let new_val = if kind & 1 == 0 {
7160            PerlValue::integer(last_old.to_int() + 1)
7161        } else {
7162            PerlValue::integer(last_old.to_int() - 1)
7163        };
7164        let mut ks: Vec<String> = Vec::new();
7165        for kv in &key_values {
7166            if let Some(vv) = kv.as_array_vec() {
7167                ks.extend(vv.iter().map(|x| x.to_string()));
7168            } else {
7169                ks.push(kv.to_string());
7170            }
7171        }
7172        let last_key = ks.last().ok_or_else(|| {
7173            PerlError::runtime("Hash slice increment needs at least one key", line)
7174        })?;
7175        let container = PerlValue::string(hash.to_string());
7176        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
7177        Ok(if kind < 2 { new_val } else { last_old })
7178    }
7179
7180    fn match_array_pattern_elems(
7181        &mut self,
7182        arr: &[PerlValue],
7183        elems: &[MatchArrayElem],
7184        line: usize,
7185    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7186        let has_rest = elems
7187            .iter()
7188            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
7189        let mut binds: Vec<PatternBinding> = Vec::new();
7190        let mut idx = 0usize;
7191        for (i, elem) in elems.iter().enumerate() {
7192            match elem {
7193                MatchArrayElem::Rest => {
7194                    if i != elems.len() - 1 {
7195                        return Err(PerlError::runtime(
7196                            "internal: `*` must be last in array match pattern",
7197                            line,
7198                        )
7199                        .into());
7200                    }
7201                    return Ok(Some(binds));
7202                }
7203                MatchArrayElem::RestBind(name) => {
7204                    if i != elems.len() - 1 {
7205                        return Err(PerlError::runtime(
7206                            "internal: `@name` rest bind must be last in array match pattern",
7207                            line,
7208                        )
7209                        .into());
7210                    }
7211                    let tail = arr[idx..].to_vec();
7212                    binds.push(PatternBinding::Array(name.clone(), tail));
7213                    return Ok(Some(binds));
7214                }
7215                MatchArrayElem::CaptureScalar(name) => {
7216                    if idx >= arr.len() {
7217                        return Ok(None);
7218                    }
7219                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
7220                    idx += 1;
7221                }
7222                MatchArrayElem::Expr(e) => {
7223                    if idx >= arr.len() {
7224                        return Ok(None);
7225                    }
7226                    let expected = self.eval_expr(e)?;
7227                    if !self.smartmatch_when(&arr[idx], &expected) {
7228                        return Ok(None);
7229                    }
7230                    idx += 1;
7231                }
7232            }
7233        }
7234        if !has_rest && idx != arr.len() {
7235            return Ok(None);
7236        }
7237        Ok(Some(binds))
7238    }
7239
7240    fn match_hash_pattern_pairs(
7241        &mut self,
7242        h: &IndexMap<String, PerlValue>,
7243        pairs: &[MatchHashPair],
7244        _line: usize,
7245    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
7246        let mut binds = Vec::new();
7247        for pair in pairs {
7248            match pair {
7249                MatchHashPair::KeyOnly { key } => {
7250                    let ks = self.eval_expr(key)?.to_string();
7251                    if !h.contains_key(&ks) {
7252                        return Ok(None);
7253                    }
7254                }
7255                MatchHashPair::Capture { key, name } => {
7256                    let ks = self.eval_expr(key)?.to_string();
7257                    let Some(v) = h.get(&ks) else {
7258                        return Ok(None);
7259                    };
7260                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
7261                }
7262            }
7263        }
7264        Ok(Some(binds))
7265    }
7266
7267    /// Check if a block declares variables (needs its own scope frame).
7268    #[inline]
7269    fn block_needs_scope(block: &Block) -> bool {
7270        block.iter().any(|s| match &s.kind {
7271            StmtKind::My(_)
7272            | StmtKind::Our(_)
7273            | StmtKind::Local(_)
7274            | StmtKind::State(_)
7275            | StmtKind::LocalExpr { .. } => true,
7276            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
7277            _ => false,
7278        })
7279    }
7280
7281    /// Execute block, only pushing a scope frame if needed.
7282    #[inline]
7283    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
7284        if Self::block_needs_scope(block) {
7285            self.exec_block(block)
7286        } else {
7287            self.exec_block_no_scope(block)
7288        }
7289    }
7290
7291    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
7292        let t0 = self.profiler.is_some().then(std::time::Instant::now);
7293        let r = self.exec_statement_inner(stmt);
7294        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
7295            prof.on_line(&self.file, stmt.line, t0.elapsed());
7296        }
7297        r
7298    }
7299
7300    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
7301        if let Err(e) = crate::perl_signal::poll(self) {
7302            return Err(FlowOrError::Error(e));
7303        }
7304        if let Err(e) = self.drain_pending_destroys(stmt.line) {
7305            return Err(FlowOrError::Error(e));
7306        }
7307        match &stmt.kind {
7308            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
7309            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
7310            StmtKind::If {
7311                condition,
7312                body,
7313                elsifs,
7314                else_block,
7315            } => {
7316                if self.eval_boolean_rvalue_condition(condition)? {
7317                    return self.exec_block(body);
7318                }
7319                for (c, b) in elsifs {
7320                    if self.eval_boolean_rvalue_condition(c)? {
7321                        return self.exec_block(b);
7322                    }
7323                }
7324                if let Some(eb) = else_block {
7325                    return self.exec_block(eb);
7326                }
7327                Ok(PerlValue::UNDEF)
7328            }
7329            StmtKind::Unless {
7330                condition,
7331                body,
7332                else_block,
7333            } => {
7334                if !self.eval_boolean_rvalue_condition(condition)? {
7335                    return self.exec_block(body);
7336                }
7337                if let Some(eb) = else_block {
7338                    return self.exec_block(eb);
7339                }
7340                Ok(PerlValue::UNDEF)
7341            }
7342            StmtKind::While {
7343                condition,
7344                body,
7345                label,
7346                continue_block,
7347            } => {
7348                'outer: loop {
7349                    if !self.eval_boolean_rvalue_condition(condition)? {
7350                        break;
7351                    }
7352                    'inner: loop {
7353                        match self.exec_block_smart(body) {
7354                            Ok(_) => break 'inner,
7355                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7356                                if l == label || l.is_none() =>
7357                            {
7358                                break 'outer;
7359                            }
7360                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7361                                if l == label || l.is_none() =>
7362                            {
7363                                if let Some(cb) = continue_block {
7364                                    let _ = self.exec_block_smart(cb);
7365                                }
7366                                continue 'outer;
7367                            }
7368                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7369                                if l == label || l.is_none() =>
7370                            {
7371                                continue 'inner;
7372                            }
7373                            Err(e) => return Err(e),
7374                        }
7375                    }
7376                    if let Some(cb) = continue_block {
7377                        let _ = self.exec_block_smart(cb);
7378                    }
7379                }
7380                Ok(PerlValue::UNDEF)
7381            }
7382            StmtKind::Until {
7383                condition,
7384                body,
7385                label,
7386                continue_block,
7387            } => {
7388                'outer: loop {
7389                    if self.eval_boolean_rvalue_condition(condition)? {
7390                        break;
7391                    }
7392                    'inner: loop {
7393                        match self.exec_block(body) {
7394                            Ok(_) => break 'inner,
7395                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7396                                if l == label || l.is_none() =>
7397                            {
7398                                break 'outer;
7399                            }
7400                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7401                                if l == label || l.is_none() =>
7402                            {
7403                                if let Some(cb) = continue_block {
7404                                    let _ = self.exec_block_smart(cb);
7405                                }
7406                                continue 'outer;
7407                            }
7408                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7409                                if l == label || l.is_none() =>
7410                            {
7411                                continue 'inner;
7412                            }
7413                            Err(e) => return Err(e),
7414                        }
7415                    }
7416                    if let Some(cb) = continue_block {
7417                        let _ = self.exec_block_smart(cb);
7418                    }
7419                }
7420                Ok(PerlValue::UNDEF)
7421            }
7422            StmtKind::DoWhile { body, condition } => {
7423                loop {
7424                    self.exec_block(body)?;
7425                    if !self.eval_boolean_rvalue_condition(condition)? {
7426                        break;
7427                    }
7428                }
7429                Ok(PerlValue::UNDEF)
7430            }
7431            StmtKind::For {
7432                init,
7433                condition,
7434                step,
7435                body,
7436                label,
7437                continue_block,
7438            } => {
7439                self.scope_push_hook();
7440                if let Some(init) = init {
7441                    self.exec_statement(init)?;
7442                }
7443                'outer: loop {
7444                    if let Some(cond) = condition {
7445                        if !self.eval_boolean_rvalue_condition(cond)? {
7446                            break;
7447                        }
7448                    }
7449                    'inner: loop {
7450                        match self.exec_block_smart(body) {
7451                            Ok(_) => break 'inner,
7452                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7453                                if l == label || l.is_none() =>
7454                            {
7455                                break 'outer;
7456                            }
7457                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7458                                if l == label || l.is_none() =>
7459                            {
7460                                if let Some(cb) = continue_block {
7461                                    let _ = self.exec_block_smart(cb);
7462                                }
7463                                if let Some(step) = step {
7464                                    self.eval_expr(step)?;
7465                                }
7466                                continue 'outer;
7467                            }
7468                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7469                                if l == label || l.is_none() =>
7470                            {
7471                                continue 'inner;
7472                            }
7473                            Err(e) => {
7474                                self.scope_pop_hook();
7475                                return Err(e);
7476                            }
7477                        }
7478                    }
7479                    if let Some(cb) = continue_block {
7480                        let _ = self.exec_block_smart(cb);
7481                    }
7482                    if let Some(step) = step {
7483                        self.eval_expr(step)?;
7484                    }
7485                }
7486                self.scope_pop_hook();
7487                Ok(PerlValue::UNDEF)
7488            }
7489            StmtKind::Foreach {
7490                var,
7491                list,
7492                body,
7493                label,
7494                continue_block,
7495            } => {
7496                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
7497                let items = list_val.to_list();
7498                self.scope_push_hook();
7499                self.scope.declare_scalar(var, PerlValue::UNDEF);
7500                self.english_note_lexical_scalar(var);
7501                let mut i = 0usize;
7502                'outer: while i < items.len() {
7503                    self.scope
7504                        .set_scalar(var, items[i].clone())
7505                        .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
7506                    'inner: loop {
7507                        match self.exec_block_smart(body) {
7508                            Ok(_) => break 'inner,
7509                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7510                                if l == label || l.is_none() =>
7511                            {
7512                                break 'outer;
7513                            }
7514                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7515                                if l == label || l.is_none() =>
7516                            {
7517                                if let Some(cb) = continue_block {
7518                                    let _ = self.exec_block_smart(cb);
7519                                }
7520                                i += 1;
7521                                continue 'outer;
7522                            }
7523                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7524                                if l == label || l.is_none() =>
7525                            {
7526                                continue 'inner;
7527                            }
7528                            Err(e) => {
7529                                self.scope_pop_hook();
7530                                return Err(e);
7531                            }
7532                        }
7533                    }
7534                    if let Some(cb) = continue_block {
7535                        let _ = self.exec_block_smart(cb);
7536                    }
7537                    i += 1;
7538                }
7539                self.scope_pop_hook();
7540                Ok(PerlValue::UNDEF)
7541            }
7542            StmtKind::SubDecl {
7543                name,
7544                params,
7545                body,
7546                prototype,
7547            } => {
7548                let key = self.qualify_sub_key(name);
7549                let captured = self.scope.capture();
7550                let closure_env = if captured.is_empty() {
7551                    None
7552                } else {
7553                    Some(captured)
7554                };
7555                let mut sub = PerlSub {
7556                    name: name.clone(),
7557                    params: params.clone(),
7558                    body: body.clone(),
7559                    closure_env,
7560                    prototype: prototype.clone(),
7561                    fib_like: None,
7562                };
7563                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
7564                self.subs.insert(key, Arc::new(sub));
7565                Ok(PerlValue::UNDEF)
7566            }
7567            StmtKind::StructDecl { def } => {
7568                if self.struct_defs.contains_key(&def.name) {
7569                    return Err(PerlError::runtime(
7570                        format!("duplicate struct `{}`", def.name),
7571                        stmt.line,
7572                    )
7573                    .into());
7574                }
7575                self.struct_defs
7576                    .insert(def.name.clone(), Arc::new(def.clone()));
7577                Ok(PerlValue::UNDEF)
7578            }
7579            StmtKind::EnumDecl { def } => {
7580                if self.enum_defs.contains_key(&def.name) {
7581                    return Err(PerlError::runtime(
7582                        format!("duplicate enum `{}`", def.name),
7583                        stmt.line,
7584                    )
7585                    .into());
7586                }
7587                self.enum_defs
7588                    .insert(def.name.clone(), Arc::new(def.clone()));
7589                Ok(PerlValue::UNDEF)
7590            }
7591            StmtKind::ClassDecl { def } => {
7592                if self.class_defs.contains_key(&def.name) {
7593                    return Err(PerlError::runtime(
7594                        format!("duplicate class `{}`", def.name),
7595                        stmt.line,
7596                    )
7597                    .into());
7598                }
7599                // Final class enforcement: prevent subclassing
7600                for parent_name in &def.extends {
7601                    if let Some(parent_def) = self.class_defs.get(parent_name) {
7602                        if parent_def.is_final {
7603                            return Err(PerlError::runtime(
7604                                format!("cannot extend final class `{}`", parent_name),
7605                                stmt.line,
7606                            )
7607                            .into());
7608                        }
7609                        // Final method enforcement: prevent overriding
7610                        for m in &def.methods {
7611                            if let Some(parent_method) = parent_def.method(&m.name) {
7612                                if parent_method.is_final {
7613                                    return Err(PerlError::runtime(
7614                                        format!(
7615                                            "cannot override final method `{}` from class `{}`",
7616                                            m.name, parent_name
7617                                        ),
7618                                        stmt.line,
7619                                    )
7620                                    .into());
7621                                }
7622                            }
7623                        }
7624                    }
7625                }
7626                // Trait contract enforcement + default method inheritance
7627                let mut def = def.clone();
7628                for trait_name in &def.implements.clone() {
7629                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
7630                        for required in trait_def.required_methods() {
7631                            let has_method = def.methods.iter().any(|m| m.name == required.name);
7632                            if !has_method {
7633                                return Err(PerlError::runtime(
7634                                    format!(
7635                                        "class `{}` implements trait `{}` but does not define required method `{}`",
7636                                        def.name, trait_name, required.name
7637                                    ),
7638                                    stmt.line,
7639                                )
7640                                .into());
7641                            }
7642                        }
7643                        // Inherit default methods from trait (methods with bodies)
7644                        for tm in &trait_def.methods {
7645                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
7646                                def.methods.push(tm.clone());
7647                            }
7648                        }
7649                    }
7650                }
7651                // Abstract method enforcement: concrete subclasses must implement
7652                // all abstract methods (body-less methods) from abstract parents
7653                if !def.is_abstract {
7654                    for parent_name in &def.extends.clone() {
7655                        if let Some(parent_def) = self.class_defs.get(parent_name) {
7656                            if parent_def.is_abstract {
7657                                for m in &parent_def.methods {
7658                                    if m.body.is_none()
7659                                        && !def.methods.iter().any(|dm| dm.name == m.name)
7660                                    {
7661                                        return Err(PerlError::runtime(
7662                                            format!(
7663                                                "class `{}` must implement abstract method `{}` from `{}`",
7664                                                def.name, m.name, parent_name
7665                                            ),
7666                                            stmt.line,
7667                                        )
7668                                        .into());
7669                                    }
7670                                }
7671                            }
7672                        }
7673                    }
7674                }
7675                // Initialize static fields
7676                for sf in &def.static_fields {
7677                    let val = if let Some(ref expr) = sf.default {
7678                        self.eval_expr(expr)?
7679                    } else {
7680                        PerlValue::UNDEF
7681                    };
7682                    let key = format!("{}::{}", def.name, sf.name);
7683                    self.scope.declare_scalar(&key, val);
7684                }
7685                // Register class methods into self.subs so method dispatch finds them.
7686                for m in &def.methods {
7687                    if let Some(ref body) = m.body {
7688                        let fq = format!("{}::{}", def.name, m.name);
7689                        let sub = Arc::new(PerlSub {
7690                            name: fq.clone(),
7691                            params: m.params.clone(),
7692                            body: body.clone(),
7693                            closure_env: None,
7694                            prototype: None,
7695                            fib_like: None,
7696                        });
7697                        self.subs.insert(fq, sub);
7698                    }
7699                }
7700                // Set @ClassName::ISA so MRO/isa resolution works.
7701                if !def.extends.is_empty() {
7702                    let isa_key = format!("{}::ISA", def.name);
7703                    let parents: Vec<PerlValue> = def
7704                        .extends
7705                        .iter()
7706                        .map(|p| PerlValue::string(p.clone()))
7707                        .collect();
7708                    self.scope.declare_array(&isa_key, parents);
7709                }
7710                let arc_def = Arc::new(def);
7711                self.class_defs
7712                    .insert(arc_def.name.clone(), Arc::clone(&arc_def));
7713                // Mirror the new class into the serializer-visible
7714                // thread-local registry so `to_json($obj)` etc. can walk
7715                // its inheritance chain.
7716                crate::serialize_normalize::register_class_def(arc_def);
7717                Ok(PerlValue::UNDEF)
7718            }
7719            StmtKind::TraitDecl { def } => {
7720                if self.trait_defs.contains_key(&def.name) {
7721                    return Err(PerlError::runtime(
7722                        format!("duplicate trait `{}`", def.name),
7723                        stmt.line,
7724                    )
7725                    .into());
7726                }
7727                self.trait_defs
7728                    .insert(def.name.clone(), Arc::new(def.clone()));
7729                Ok(PerlValue::UNDEF)
7730            }
7731            StmtKind::My(decls) | StmtKind::Our(decls) => {
7732                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
7733                // For list assignment my ($a, $b) = (10, 20), distribute elements.
7734                // All decls share the same initializer in the AST (parser clones it).
7735                if decls.len() > 1 && decls[0].initializer.is_some() {
7736                    let val = self.eval_expr_ctx(
7737                        decls[0].initializer.as_ref().unwrap(),
7738                        WantarrayCtx::List,
7739                    )?;
7740                    let items = val.to_list();
7741                    let mut idx = 0;
7742                    for decl in decls {
7743                        match decl.sigil {
7744                            Sigil::Scalar => {
7745                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7746                                let skey = if is_our {
7747                                    self.stash_scalar_name_for_package(&decl.name)
7748                                } else {
7749                                    decl.name.clone()
7750                                };
7751                                self.scope.declare_scalar_frozen(
7752                                    &skey,
7753                                    v,
7754                                    decl.frozen,
7755                                    decl.type_annotation.clone(),
7756                                )?;
7757                                self.english_note_lexical_scalar(&decl.name);
7758                                if is_our {
7759                                    self.note_our_scalar(&decl.name);
7760                                }
7761                                idx += 1;
7762                            }
7763                            Sigil::Array => {
7764                                // Array slurps remaining elements
7765                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7766                                idx = items.len();
7767                                if is_our {
7768                                    self.record_exporter_our_array_name(&decl.name, &rest);
7769                                }
7770                                let aname = self.stash_array_name_for_package(&decl.name);
7771                                self.scope.declare_array(&aname, rest);
7772                            }
7773                            Sigil::Hash => {
7774                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7775                                idx = items.len();
7776                                let mut map = IndexMap::new();
7777                                let mut i = 0;
7778                                while i + 1 < rest.len() {
7779                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7780                                    i += 2;
7781                                }
7782                                self.scope.declare_hash(&decl.name, map);
7783                            }
7784                            Sigil::Typeglob => {
7785                                return Err(PerlError::runtime(
7786                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
7787                                    stmt.line,
7788                                )
7789                                .into());
7790                            }
7791                        }
7792                    }
7793                } else {
7794                    // Single decl or no initializer
7795                    for decl in decls {
7796                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
7797                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
7798                        // compound op reads the lhs (see system Exporter.pm).
7799                        let compound_init = decl
7800                            .initializer
7801                            .as_ref()
7802                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
7803
7804                        if compound_init {
7805                            match decl.sigil {
7806                                Sigil::Typeglob => {
7807                                    return Err(PerlError::runtime(
7808                                        "compound assignment on typeglob declaration is not supported",
7809                                        stmt.line,
7810                                    )
7811                                    .into());
7812                                }
7813                                Sigil::Scalar => {
7814                                    let skey = if is_our {
7815                                        self.stash_scalar_name_for_package(&decl.name)
7816                                    } else {
7817                                        decl.name.clone()
7818                                    };
7819                                    self.scope.declare_scalar_frozen(
7820                                        &skey,
7821                                        PerlValue::UNDEF,
7822                                        decl.frozen,
7823                                        decl.type_annotation.clone(),
7824                                    )?;
7825                                    self.english_note_lexical_scalar(&decl.name);
7826                                    if is_our {
7827                                        self.note_our_scalar(&decl.name);
7828                                    }
7829                                    let init = decl.initializer.as_ref().unwrap();
7830                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7831                                }
7832                                Sigil::Array => {
7833                                    let aname = self.stash_array_name_for_package(&decl.name);
7834                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
7835                                    let init = decl.initializer.as_ref().unwrap();
7836                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7837                                    if is_our {
7838                                        let items = self.scope.get_array(&aname);
7839                                        self.record_exporter_our_array_name(&decl.name, &items);
7840                                    }
7841                                }
7842                                Sigil::Hash => {
7843                                    self.scope.declare_hash_frozen(
7844                                        &decl.name,
7845                                        IndexMap::new(),
7846                                        decl.frozen,
7847                                    );
7848                                    let init = decl.initializer.as_ref().unwrap();
7849                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7850                                }
7851                            }
7852                            continue;
7853                        }
7854
7855                        let val = if let Some(init) = &decl.initializer {
7856                            let ctx = match decl.sigil {
7857                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7858                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7859                            };
7860                            self.eval_expr_ctx(init, ctx)?
7861                        } else {
7862                            PerlValue::UNDEF
7863                        };
7864                        match decl.sigil {
7865                            Sigil::Typeglob => {
7866                                return Err(PerlError::runtime(
7867                                    "`my *FH` / typeglob declaration is not supported",
7868                                    stmt.line,
7869                                )
7870                                .into());
7871                            }
7872                            Sigil::Scalar => {
7873                                let skey = if is_our {
7874                                    self.stash_scalar_name_for_package(&decl.name)
7875                                } else {
7876                                    decl.name.clone()
7877                                };
7878                                self.scope.declare_scalar_frozen(
7879                                    &skey,
7880                                    val,
7881                                    decl.frozen,
7882                                    decl.type_annotation.clone(),
7883                                )?;
7884                                self.english_note_lexical_scalar(&decl.name);
7885                                if is_our {
7886                                    self.note_our_scalar(&decl.name);
7887                                }
7888                            }
7889                            Sigil::Array => {
7890                                let items = val.to_list();
7891                                if is_our {
7892                                    self.record_exporter_our_array_name(&decl.name, &items);
7893                                }
7894                                let aname = self.stash_array_name_for_package(&decl.name);
7895                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
7896                            }
7897                            Sigil::Hash => {
7898                                let items = val.to_list();
7899                                let mut map = IndexMap::new();
7900                                let mut i = 0;
7901                                while i + 1 < items.len() {
7902                                    let k = items[i].to_string();
7903                                    let v = items[i + 1].clone();
7904                                    map.insert(k, v);
7905                                    i += 2;
7906                                }
7907                                self.scope.declare_hash_frozen(&decl.name, map, decl.frozen);
7908                            }
7909                        }
7910                    }
7911                }
7912                Ok(PerlValue::UNDEF)
7913            }
7914            StmtKind::State(decls) => {
7915                // `state` variables persist across subroutine calls.
7916                // Key by source line + name for uniqueness.
7917                for decl in decls {
7918                    let state_key = format!("{}:{}", stmt.line, decl.name);
7919                    match decl.sigil {
7920                        Sigil::Scalar => {
7921                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
7922                                // Already initialized — declare with persisted value
7923                                self.scope.declare_scalar(&decl.name, prev);
7924                            } else {
7925                                // First encounter — evaluate initializer
7926                                let val = if let Some(init) = &decl.initializer {
7927                                    self.eval_expr(init)?
7928                                } else {
7929                                    PerlValue::UNDEF
7930                                };
7931                                self.state_vars.insert(state_key.clone(), val.clone());
7932                                self.scope.declare_scalar(&decl.name, val);
7933                            }
7934                            // Register for save-back when scope pops
7935                            if let Some(frame) = self.state_bindings_stack.last_mut() {
7936                                frame.push((decl.name.clone(), state_key));
7937                            }
7938                        }
7939                        _ => {
7940                            // For arrays/hashes, fall back to simple my-like behavior
7941                            let val = if let Some(init) = &decl.initializer {
7942                                self.eval_expr(init)?
7943                            } else {
7944                                PerlValue::UNDEF
7945                            };
7946                            match decl.sigil {
7947                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
7948                                Sigil::Hash => {
7949                                    let items = val.to_list();
7950                                    let mut map = IndexMap::new();
7951                                    let mut i = 0;
7952                                    while i + 1 < items.len() {
7953                                        map.insert(items[i].to_string(), items[i + 1].clone());
7954                                        i += 2;
7955                                    }
7956                                    self.scope.declare_hash(&decl.name, map);
7957                                }
7958                                _ => {}
7959                            }
7960                        }
7961                    }
7962                }
7963                Ok(PerlValue::UNDEF)
7964            }
7965            StmtKind::Local(decls) => {
7966                if decls.len() > 1 && decls[0].initializer.is_some() {
7967                    let val = self.eval_expr_ctx(
7968                        decls[0].initializer.as_ref().unwrap(),
7969                        WantarrayCtx::List,
7970                    )?;
7971                    let items = val.to_list();
7972                    let mut idx = 0;
7973                    for decl in decls {
7974                        match decl.sigil {
7975                            Sigil::Scalar => {
7976                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7977                                idx += 1;
7978                                self.scope.local_set_scalar(&decl.name, v)?;
7979                            }
7980                            Sigil::Array => {
7981                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7982                                idx = items.len();
7983                                self.scope.local_set_array(&decl.name, rest)?;
7984                            }
7985                            Sigil::Hash => {
7986                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7987                                idx = items.len();
7988                                if decl.name == "ENV" {
7989                                    self.materialize_env_if_needed();
7990                                }
7991                                let mut map = IndexMap::new();
7992                                let mut i = 0;
7993                                while i + 1 < rest.len() {
7994                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7995                                    i += 2;
7996                                }
7997                                self.scope.local_set_hash(&decl.name, map)?;
7998                            }
7999                            Sigil::Typeglob => {
8000                                return Err(PerlError::runtime(
8001                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
8002                                    stmt.line,
8003                                )
8004                                .into());
8005                            }
8006                        }
8007                    }
8008                    Ok(val)
8009                } else {
8010                    let mut last_val = PerlValue::UNDEF;
8011                    for decl in decls {
8012                        let val = if let Some(init) = &decl.initializer {
8013                            let ctx = match decl.sigil {
8014                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
8015                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
8016                            };
8017                            self.eval_expr_ctx(init, ctx)?
8018                        } else {
8019                            PerlValue::UNDEF
8020                        };
8021                        last_val = val.clone();
8022                        match decl.sigil {
8023                            Sigil::Typeglob => {
8024                                let old = self.glob_handle_alias.remove(&decl.name);
8025                                if let Some(frame) = self.glob_restore_frames.last_mut() {
8026                                    frame.push((decl.name.clone(), old));
8027                                }
8028                                if let Some(init) = &decl.initializer {
8029                                    if let ExprKind::Typeglob(rhs) = &init.kind {
8030                                        self.glob_handle_alias
8031                                            .insert(decl.name.clone(), rhs.clone());
8032                                    } else {
8033                                        return Err(PerlError::runtime(
8034                                            "local *GLOB = *OTHER — right side must be a typeglob",
8035                                            stmt.line,
8036                                        )
8037                                        .into());
8038                                    }
8039                                }
8040                            }
8041                            Sigil::Scalar => {
8042                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
8043                                // must update the interpreter's backing field too — these are
8044                                // not stored in `Scope`. Save the prior value for restoration
8045                                // on `scope_pop_hook` so the block-exit restore is visible to
8046                                // print/I/O code.
8047                                if Self::is_special_scalar_name_for_set(&decl.name) {
8048                                    let old = self.get_special_var(&decl.name);
8049                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
8050                                    {
8051                                        frame.push((decl.name.clone(), old));
8052                                    }
8053                                    self.set_special_var(&decl.name, &val)
8054                                        .map_err(|e| e.at_line(stmt.line))?;
8055                                }
8056                                self.scope.local_set_scalar(&decl.name, val)?;
8057                            }
8058                            Sigil::Array => {
8059                                self.scope.local_set_array(&decl.name, val.to_list())?;
8060                            }
8061                            Sigil::Hash => {
8062                                if decl.name == "ENV" {
8063                                    self.materialize_env_if_needed();
8064                                }
8065                                let items = val.to_list();
8066                                let mut map = IndexMap::new();
8067                                let mut i = 0;
8068                                while i + 1 < items.len() {
8069                                    let k = items[i].to_string();
8070                                    let v = items[i + 1].clone();
8071                                    map.insert(k, v);
8072                                    i += 2;
8073                                }
8074                                self.scope.local_set_hash(&decl.name, map)?;
8075                            }
8076                        }
8077                    }
8078                    Ok(last_val)
8079                }
8080            }
8081            StmtKind::LocalExpr {
8082                target,
8083                initializer,
8084            } => {
8085                let rhs_name = |init: &Expr| -> PerlResult<Option<String>> {
8086                    match &init.kind {
8087                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
8088                        _ => Err(PerlError::runtime(
8089                            "local *GLOB = *OTHER — right side must be a typeglob",
8090                            stmt.line,
8091                        )),
8092                    }
8093                };
8094                match &target.kind {
8095                    ExprKind::Typeglob(name) => {
8096                        let rhs = if let Some(init) = initializer {
8097                            rhs_name(init)?
8098                        } else {
8099                            None
8100                        };
8101                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
8102                        return Ok(PerlValue::UNDEF);
8103                    }
8104                    ExprKind::Deref {
8105                        expr,
8106                        kind: Sigil::Typeglob,
8107                    } => {
8108                        let lhs = self.eval_expr(expr)?.to_string();
8109                        let rhs = if let Some(init) = initializer {
8110                            rhs_name(init)?
8111                        } else {
8112                            None
8113                        };
8114                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8115                        return Ok(PerlValue::UNDEF);
8116                    }
8117                    ExprKind::TypeglobExpr(e) => {
8118                        let lhs = self.eval_expr(e)?.to_string();
8119                        let rhs = if let Some(init) = initializer {
8120                            rhs_name(init)?
8121                        } else {
8122                            None
8123                        };
8124                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
8125                        return Ok(PerlValue::UNDEF);
8126                    }
8127                    _ => {}
8128                }
8129                let val = if let Some(init) = initializer {
8130                    let ctx = match &target.kind {
8131                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
8132                        _ => WantarrayCtx::Scalar,
8133                    };
8134                    self.eval_expr_ctx(init, ctx)?
8135                } else {
8136                    PerlValue::UNDEF
8137                };
8138                match &target.kind {
8139                    ExprKind::ScalarVar(name) => {
8140                        // `local $X = …` on a special var — see twin block in
8141                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
8142                        if Self::is_special_scalar_name_for_set(name) {
8143                            let old = self.get_special_var(name);
8144                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
8145                                frame.push((name.clone(), old));
8146                            }
8147                            self.set_special_var(name, &val)
8148                                .map_err(|e| e.at_line(stmt.line))?;
8149                        }
8150                        self.scope.local_set_scalar(name, val.clone())?;
8151                    }
8152                    ExprKind::ArrayVar(name) => {
8153                        self.scope.local_set_array(name, val.to_list())?;
8154                    }
8155                    ExprKind::HashVar(name) => {
8156                        if name == "ENV" {
8157                            self.materialize_env_if_needed();
8158                        }
8159                        let items = val.to_list();
8160                        let mut map = IndexMap::new();
8161                        let mut i = 0;
8162                        while i + 1 < items.len() {
8163                            map.insert(items[i].to_string(), items[i + 1].clone());
8164                            i += 2;
8165                        }
8166                        self.scope.local_set_hash(name, map)?;
8167                    }
8168                    ExprKind::HashElement { hash, key } => {
8169                        let ks = self.eval_expr(key)?.to_string();
8170                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
8171                    }
8172                    ExprKind::ArrayElement { array, index } => {
8173                        self.check_strict_array_var(array, stmt.line)?;
8174                        let aname = self.stash_array_name_for_package(array);
8175                        let idx = self.eval_expr(index)?.to_int();
8176                        self.scope
8177                            .local_set_array_element(&aname, idx, val.clone())?;
8178                    }
8179                    _ => {
8180                        return Err(PerlError::runtime(
8181                            format!(
8182                                "local on this lvalue is not supported yet ({:?})",
8183                                target.kind
8184                            ),
8185                            stmt.line,
8186                        )
8187                        .into());
8188                    }
8189                }
8190                Ok(val)
8191            }
8192            StmtKind::MySync(decls) => {
8193                for decl in decls {
8194                    let val = if let Some(init) = &decl.initializer {
8195                        self.eval_expr(init)?
8196                    } else {
8197                        PerlValue::UNDEF
8198                    };
8199                    match decl.sigil {
8200                        Sigil::Typeglob => {
8201                            return Err(PerlError::runtime(
8202                                "`mysync` does not support typeglob variables",
8203                                stmt.line,
8204                            )
8205                            .into());
8206                        }
8207                        Sigil::Scalar => {
8208                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
8209                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
8210                            let stored = if val.is_mysync_deque_or_heap() {
8211                                val
8212                            } else {
8213                                PerlValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(val)))
8214                            };
8215                            self.scope.declare_scalar(&decl.name, stored);
8216                        }
8217                        Sigil::Array => {
8218                            self.scope.declare_atomic_array(&decl.name, val.to_list());
8219                        }
8220                        Sigil::Hash => {
8221                            let items = val.to_list();
8222                            let mut map = IndexMap::new();
8223                            let mut i = 0;
8224                            while i + 1 < items.len() {
8225                                map.insert(items[i].to_string(), items[i + 1].clone());
8226                                i += 2;
8227                            }
8228                            self.scope.declare_atomic_hash(&decl.name, map);
8229                        }
8230                    }
8231                }
8232                Ok(PerlValue::UNDEF)
8233            }
8234            StmtKind::OurSync(decls) => {
8235                // The fan/pmap/pfor workers execute closure bodies via this tree-walker
8236                // (`exec_block_no_scope`), not the bytecode VM — so `oursync` MUST register
8237                // each declared name in `english_lexical_scalars` + `our_lexical_scalars`
8238                // for `tree_scalar_storage_name` to rewrite later `$x` reads to `Pkg::x`.
8239                // Without this, worker `$x` reads see UNDEF (the qualified key isn't
8240                // queried) even though capture/restore brought the cell across.
8241                for decl in decls {
8242                    let val = if let Some(init) = &decl.initializer {
8243                        self.eval_expr(init)?
8244                    } else {
8245                        PerlValue::UNDEF
8246                    };
8247                    match decl.sigil {
8248                        Sigil::Typeglob => {
8249                            return Err(PerlError::runtime(
8250                                "`oursync` does not support typeglob variables",
8251                                stmt.line,
8252                            )
8253                            .into());
8254                        }
8255                        Sigil::Scalar => {
8256                            let stash = self.stash_scalar_name_for_package(&decl.name);
8257                            let stored = if val.is_mysync_deque_or_heap() {
8258                                val
8259                            } else {
8260                                PerlValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(val)))
8261                            };
8262                            self.scope.declare_scalar(&stash, stored);
8263                            self.english_note_lexical_scalar(&decl.name);
8264                            self.note_our_scalar(&decl.name);
8265                        }
8266                        Sigil::Array => {
8267                            let stash = self.stash_array_name_for_package(&decl.name);
8268                            self.scope.declare_atomic_array(&stash, val.to_list());
8269                            self.english_note_lexical_scalar(&decl.name);
8270                            self.note_our_scalar(&decl.name);
8271                        }
8272                        Sigil::Hash => {
8273                            let items = val.to_list();
8274                            let mut map = IndexMap::new();
8275                            let mut i = 0;
8276                            while i + 1 < items.len() {
8277                                map.insert(items[i].to_string(), items[i + 1].clone());
8278                                i += 2;
8279                            }
8280                            // Match `our %h` convention: bare hash name (existing
8281                            // cross-package quirk).
8282                            self.scope.declare_atomic_hash(&decl.name, map);
8283                            self.english_note_lexical_scalar(&decl.name);
8284                            self.note_our_scalar(&decl.name);
8285                        }
8286                    }
8287                }
8288                Ok(PerlValue::UNDEF)
8289            }
8290            StmtKind::Package { name } => {
8291                // Minimal package support — just set a variable
8292                let _ = self
8293                    .scope
8294                    .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
8295                Ok(PerlValue::UNDEF)
8296            }
8297            StmtKind::UsePerlVersion { .. } => Ok(PerlValue::UNDEF),
8298            StmtKind::Use { .. } => {
8299                // Handled in `prepare_program_top_level` before BEGIN / main.
8300                Ok(PerlValue::UNDEF)
8301            }
8302            StmtKind::UseOverload { pairs } => {
8303                self.install_use_overload_pairs(pairs);
8304                Ok(PerlValue::UNDEF)
8305            }
8306            StmtKind::No { .. } => {
8307                // Handled in `prepare_program_top_level` (same phase as `use`).
8308                Ok(PerlValue::UNDEF)
8309            }
8310            StmtKind::Return(val) => {
8311                let v = if let Some(e) = val {
8312                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
8313                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
8314                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
8315                    self.eval_expr_ctx(e, self.wantarray_kind)?
8316                } else {
8317                    PerlValue::UNDEF
8318                };
8319                Err(Flow::Return(v).into())
8320            }
8321            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
8322            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
8323            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
8324            StmtKind::Block(block) => self.exec_block(block),
8325            StmtKind::Begin(_)
8326            | StmtKind::UnitCheck(_)
8327            | StmtKind::Check(_)
8328            | StmtKind::Init(_)
8329            | StmtKind::End(_) => Ok(PerlValue::UNDEF),
8330            StmtKind::Empty => Ok(PerlValue::UNDEF),
8331            StmtKind::Goto { target } => {
8332                // goto &sub — tail call
8333                if let ExprKind::SubroutineRef(name) = &target.kind {
8334                    return Err(Flow::GotoSub(name.clone()).into());
8335                }
8336                Err(PerlError::runtime("goto reached outside goto-aware block", stmt.line).into())
8337            }
8338            StmtKind::EvalTimeout { timeout, body } => {
8339                let secs = self.eval_expr(timeout)?.to_number();
8340                self.eval_timeout_block(body, secs, stmt.line)
8341            }
8342            StmtKind::Tie {
8343                target,
8344                class,
8345                args,
8346            } => {
8347                let kind = match &target {
8348                    TieTarget::Scalar(_) => 0u8,
8349                    TieTarget::Array(_) => 1u8,
8350                    TieTarget::Hash(_) => 2u8,
8351                };
8352                let name = match &target {
8353                    TieTarget::Scalar(s) => s.as_str(),
8354                    TieTarget::Array(a) => a.as_str(),
8355                    TieTarget::Hash(h) => h.as_str(),
8356                };
8357                let mut vals = vec![self.eval_expr(class)?];
8358                for a in args {
8359                    vals.push(self.eval_expr(a)?);
8360                }
8361                self.tie_execute(kind, name, vals, stmt.line)
8362                    .map_err(Into::into)
8363            }
8364            StmtKind::TryCatch {
8365                try_block,
8366                catch_var,
8367                catch_block,
8368                finally_block,
8369            } => match self.exec_block(try_block) {
8370                Ok(v) => {
8371                    if let Some(fb) = finally_block {
8372                        self.exec_block(fb)?;
8373                    }
8374                    Ok(v)
8375                }
8376                Err(FlowOrError::Error(e)) => {
8377                    if matches!(e.kind, ErrorKind::Exit(_)) {
8378                        return Err(FlowOrError::Error(e));
8379                    }
8380                    self.scope_push_hook();
8381                    self.scope
8382                        .declare_scalar(catch_var, PerlValue::string(e.to_string()));
8383                    self.english_note_lexical_scalar(catch_var);
8384                    let r = self.exec_block(catch_block);
8385                    self.scope_pop_hook();
8386                    if let Some(fb) = finally_block {
8387                        self.exec_block(fb)?;
8388                    }
8389                    r
8390                }
8391                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
8392            },
8393            StmtKind::Given { topic, body } => self.exec_given(topic, body),
8394            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(PerlError::runtime(
8395                "when/default may only appear inside a given block",
8396                stmt.line,
8397            )
8398            .into()),
8399            StmtKind::FormatDecl { .. } => {
8400                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
8401                Ok(PerlValue::UNDEF)
8402            }
8403            StmtKind::AdviceDecl {
8404                kind,
8405                pattern,
8406                body,
8407            } => {
8408                // Tree-walker registration path: only reached if bytecode compilation
8409                // bailed out (extremely rare — production programs always run through
8410                // the VM). The body has no compiled bytecode region, so we tag it with
8411                // `u16::MAX` and `dispatch_with_advice` will refuse to fire it rather
8412                // than silently fall back to the AST tree-walker.
8413                let id = self.next_intercept_id;
8414                self.next_intercept_id = id.saturating_add(1);
8415                self.intercepts.push(crate::aop::Intercept {
8416                    id,
8417                    kind: *kind,
8418                    pattern: pattern.clone(),
8419                    body: body.clone(),
8420                    body_block_idx: u16::MAX,
8421                });
8422                Ok(PerlValue::UNDEF)
8423            }
8424            StmtKind::Continue(block) => self.exec_block_smart(block),
8425        }
8426    }
8427
8428    #[inline]
8429    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
8430        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
8431    }
8432
8433    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
8434    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
8435    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
8436    pub(crate) fn scalar_compound_assign_scalar_target(
8437        &mut self,
8438        name: &str,
8439        op: BinOp,
8440        rhs: PerlValue,
8441    ) -> Result<PerlValue, PerlError> {
8442        if op == BinOp::Concat {
8443            return self.scope.scalar_concat_inplace(name, &rhs);
8444        }
8445        self.scope
8446            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs))
8447    }
8448
8449    fn compound_scalar_binop(old: &PerlValue, op: BinOp, rhs: &PerlValue) -> PerlValue {
8450        match op {
8451            BinOp::Add => {
8452                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8453                    PerlValue::integer(a.wrapping_add(b))
8454                } else {
8455                    PerlValue::float(old.to_number() + rhs.to_number())
8456                }
8457            }
8458            BinOp::Sub => {
8459                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8460                    PerlValue::integer(a.wrapping_sub(b))
8461                } else {
8462                    PerlValue::float(old.to_number() - rhs.to_number())
8463                }
8464            }
8465            BinOp::Mul => {
8466                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8467                    PerlValue::integer(a.wrapping_mul(b))
8468                } else {
8469                    PerlValue::float(old.to_number() * rhs.to_number())
8470                }
8471            }
8472            BinOp::BitAnd => {
8473                if let Some(s) = crate::value::set_intersection(old, rhs) {
8474                    s
8475                } else {
8476                    PerlValue::integer(old.to_int() & rhs.to_int())
8477                }
8478            }
8479            BinOp::BitOr => {
8480                if let Some(s) = crate::value::set_union(old, rhs) {
8481                    s
8482                } else {
8483                    PerlValue::integer(old.to_int() | rhs.to_int())
8484                }
8485            }
8486            BinOp::BitXor => PerlValue::integer(old.to_int() ^ rhs.to_int()),
8487            BinOp::ShiftLeft => PerlValue::integer(old.to_int() << rhs.to_int()),
8488            BinOp::ShiftRight => PerlValue::integer(old.to_int() >> rhs.to_int()),
8489            BinOp::Div => PerlValue::float(old.to_number() / rhs.to_number()),
8490            BinOp::Mod => {
8491                // Return 0 on b==0 silently — this helper is the
8492                // `$x OP= rhs` atomic-mutate path which can't propagate
8493                // errors. The non-compound `%` path (eval_binop) raises
8494                // `ErrorKind::DivisionByZero`.
8495                let b = rhs.to_int();
8496                if b == 0 {
8497                    PerlValue::integer(0)
8498                } else {
8499                    PerlValue::integer(crate::value::perl_mod_i64(old.to_int(), b))
8500                }
8501            }
8502            BinOp::Pow => PerlValue::float(old.to_number().powf(rhs.to_number())),
8503            BinOp::LogOr => {
8504                if old.is_true() {
8505                    old.clone()
8506                } else {
8507                    rhs.clone()
8508                }
8509            }
8510            BinOp::DefinedOr => {
8511                if !old.is_undef() {
8512                    old.clone()
8513                } else {
8514                    rhs.clone()
8515                }
8516            }
8517            BinOp::LogAnd => {
8518                if old.is_true() {
8519                    rhs.clone()
8520                } else {
8521                    old.clone()
8522                }
8523            }
8524            _ => PerlValue::float(old.to_number() + rhs.to_number()),
8525        }
8526    }
8527
8528    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
8529    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
8530    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
8531    fn eval_hash_slice_key_components(
8532        &mut self,
8533        key_expr: &Expr,
8534    ) -> Result<Vec<String>, FlowOrError> {
8535        let v = if matches!(
8536            key_expr.kind,
8537            ExprKind::Range { .. } | ExprKind::SliceRange { .. }
8538        ) {
8539            self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
8540        } else {
8541            self.eval_expr(key_expr)?
8542        };
8543        if let Some(vv) = v.as_array_vec() {
8544            Ok(vv.iter().map(|x| x.to_string()).collect())
8545        } else {
8546            Ok(vec![v.to_string()])
8547        }
8548    }
8549
8550    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
8551    pub(crate) fn symbolic_deref(
8552        &mut self,
8553        val: PerlValue,
8554        kind: Sigil,
8555        line: usize,
8556    ) -> ExecResult {
8557        match kind {
8558            Sigil::Scalar => {
8559                if let Some(name) = val.as_scalar_binding_name() {
8560                    return Ok(self.get_special_var(&name));
8561                }
8562                if let Some(r) = val.as_scalar_ref() {
8563                    return Ok(r.read().clone());
8564                }
8565                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
8566                if let Some(r) = val.as_array_ref() {
8567                    return Ok(PerlValue::array(r.read().clone()));
8568                }
8569                if let Some(name) = val.as_array_binding_name() {
8570                    return Ok(PerlValue::array(self.scope.get_array(&name)));
8571                }
8572                if let Some(r) = val.as_hash_ref() {
8573                    return Ok(PerlValue::hash(r.read().clone()));
8574                }
8575                if let Some(name) = val.as_hash_binding_name() {
8576                    self.touch_env_hash(&name);
8577                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
8578                }
8579                if let Some(s) = val.as_str() {
8580                    if self.strict_refs {
8581                        return Err(PerlError::runtime(
8582                            format!(
8583                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
8584                                s
8585                            ),
8586                            line,
8587                        )
8588                        .into());
8589                    }
8590                    return Ok(self.get_special_var(&s));
8591                }
8592                Err(PerlError::runtime("Can't dereference non-reference as scalar", line).into())
8593            }
8594            Sigil::Array => {
8595                if let Some(r) = val.as_array_ref() {
8596                    return Ok(PerlValue::array(r.read().clone()));
8597                }
8598                if let Some(name) = val.as_array_binding_name() {
8599                    return Ok(PerlValue::array(self.scope.get_array(&name)));
8600                }
8601                if val.is_undef() {
8602                    if self.strict_refs {
8603                        return Err(PerlError::runtime(
8604                            "Can't use an undefined value as an ARRAY reference",
8605                            line,
8606                        )
8607                        .into());
8608                    }
8609                    return Ok(PerlValue::array(vec![]));
8610                }
8611                // Plain primitive scalar (int, float, string): under no-strict, perl
8612                // treats this as a symbolic ref `@{$val_as_string}` and silently
8613                // returns the (likely empty) named array. Under strict refs, error.
8614                // Heap objects (Pair, Generator, blessed-non-ref) fall through to
8615                // the dereference-error so we don't silently swallow real bugs.
8616                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
8617                    let s = val.to_string();
8618                    if self.strict_refs {
8619                        return Err(PerlError::runtime(
8620                            format!(
8621                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8622                                s
8623                            ),
8624                            line,
8625                        )
8626                        .into());
8627                    }
8628                    return Ok(PerlValue::array(self.scope.get_array(&s)));
8629                }
8630                Err(PerlError::runtime("Can't dereference non-reference as array", line).into())
8631            }
8632            Sigil::Hash => {
8633                if let Some(r) = val.as_hash_ref() {
8634                    return Ok(PerlValue::hash(r.read().clone()));
8635                }
8636                if let Some(name) = val.as_hash_binding_name() {
8637                    self.touch_env_hash(&name);
8638                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
8639                }
8640                // Stryke `class C { ... }` instances answer to `%$obj` by
8641                // flattening their field name/value pairs — the same shape
8642                // a Perl-style blessed hashref produces. This keeps the
8643                // canonical introspection idiom (`keys %$obj`, `values
8644                // %$obj`) working for stryke-native OO too. Order matches
8645                // the inheritance-collected field order from
8646                // `collect_class_fields_full`.
8647                if let Some(c) = val.as_class_inst() {
8648                    let all_fields = self.collect_class_fields_full(&c.def);
8649                    let values = c.get_values();
8650                    let mut map = IndexMap::new();
8651                    for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
8652                        if let Some(v) = values.get(i) {
8653                            map.insert(name.clone(), v.clone());
8654                        }
8655                    }
8656                    return Ok(PerlValue::hash(map));
8657                }
8658                // Same for stryke `struct S { ... }` instances — keep them
8659                // introspectable through the Perl-style hash-deref idiom.
8660                if let Some(s) = val.as_struct_inst() {
8661                    let values = s.get_values();
8662                    let mut map = IndexMap::new();
8663                    for (i, field) in s.def.fields.iter().enumerate() {
8664                        if let Some(v) = values.get(i) {
8665                            map.insert(field.name.clone(), v.clone());
8666                        }
8667                    }
8668                    return Ok(PerlValue::hash(map));
8669                }
8670                // Blessed-ref escape hatch: when the inner data is a hash,
8671                // unwrap and treat the deref as if it targeted the inner
8672                // hash. Old Perl OO code that wrote `%$self` on a blessed
8673                // hashref keeps working without an extra unbless step.
8674                if let Some(b) = val.as_blessed_ref() {
8675                    let inner = b.data.read().clone();
8676                    if let Some(r) = inner.as_hash_ref() {
8677                        return Ok(PerlValue::hash(r.read().clone()));
8678                    }
8679                    if let Some(h) = inner.as_hash_map() {
8680                        return Ok(PerlValue::hash(h));
8681                    }
8682                }
8683                if val.is_undef() {
8684                    if self.strict_refs {
8685                        return Err(PerlError::runtime(
8686                            "Can't use an undefined value as a HASH reference",
8687                            line,
8688                        )
8689                        .into());
8690                    }
8691                    return Ok(PerlValue::hash(IndexMap::new()));
8692                }
8693                if val.is_integer_like() || val.is_float_like() || val.is_string_like() {
8694                    let s = val.to_string();
8695                    if self.strict_refs {
8696                        return Err(PerlError::runtime(
8697                            format!(
8698                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8699                                s
8700                            ),
8701                            line,
8702                        )
8703                        .into());
8704                    }
8705                    self.touch_env_hash(&s);
8706                    return Ok(PerlValue::hash(self.scope.get_hash(&s)));
8707                }
8708                Err(PerlError::runtime("Can't dereference non-reference as hash", line).into())
8709            }
8710            Sigil::Typeglob => {
8711                if let Some(s) = val.as_str() {
8712                    return Ok(PerlValue::string(self.resolve_io_handle_name(&s)));
8713                }
8714                Err(PerlError::runtime("Can't dereference non-reference as typeglob", line).into())
8715            }
8716        }
8717    }
8718
8719    /// `qq` list join expects a plain array; if a bare [`PerlValue::array_ref`] reaches join, peel
8720    /// one level so elements stringify like Perl (`"@$r"`).
8721    #[inline]
8722    pub(crate) fn peel_array_ref_for_list_join(&self, v: PerlValue) -> PerlValue {
8723        if let Some(r) = v.as_array_ref() {
8724            return PerlValue::array(r.read().clone());
8725        }
8726        v
8727    }
8728
8729    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
8730    pub(crate) fn make_array_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
8731        if let Some(a) = val.as_array_ref() {
8732            return Ok(PerlValue::array_ref(Arc::clone(&a)));
8733        }
8734        if let Some(name) = val.as_array_binding_name() {
8735            return Ok(PerlValue::array_binding_ref(name));
8736        }
8737        if let Some(s) = val.as_str() {
8738            if self.strict_refs {
8739                return Err(PerlError::runtime(
8740                    format!(
8741                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8742                        s
8743                    ),
8744                    line,
8745                )
8746                .into());
8747            }
8748            return Ok(PerlValue::array_binding_ref(s.to_string()));
8749        }
8750        if let Some(r) = val.as_scalar_ref() {
8751            let inner = r.read().clone();
8752            return self.make_array_ref_alias(inner, line);
8753        }
8754        Err(PerlError::runtime("Can't make array reference from value", line).into())
8755    }
8756
8757    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
8758    pub(crate) fn make_hash_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
8759        if let Some(h) = val.as_hash_ref() {
8760            return Ok(PerlValue::hash_ref(Arc::clone(&h)));
8761        }
8762        if let Some(name) = val.as_hash_binding_name() {
8763            return Ok(PerlValue::hash_binding_ref(name));
8764        }
8765        if let Some(s) = val.as_str() {
8766            if self.strict_refs {
8767                return Err(PerlError::runtime(
8768                    format!(
8769                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8770                        s
8771                    ),
8772                    line,
8773                )
8774                .into());
8775            }
8776            return Ok(PerlValue::hash_binding_ref(s.to_string()));
8777        }
8778        if let Some(r) = val.as_scalar_ref() {
8779            let inner = r.read().clone();
8780            return self.make_hash_ref_alias(inner, line);
8781        }
8782        Err(PerlError::runtime("Can't make hash reference from value", line).into())
8783    }
8784
8785    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
8786    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
8787    pub(crate) fn process_case_escapes(s: &str) -> String {
8788        // Quick check: if no backslash, nothing to do
8789        if !s.contains('\\') {
8790            return s.to_string();
8791        }
8792        let mut result = String::with_capacity(s.len());
8793        let mut chars = s.chars().peekable();
8794        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
8795        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
8796
8797        while let Some(c) = chars.next() {
8798            if c == '\\' {
8799                match chars.peek() {
8800                    Some(&'U') => {
8801                        chars.next();
8802                        mode = Some('U');
8803                        continue;
8804                    }
8805                    Some(&'L') => {
8806                        chars.next();
8807                        mode = Some('L');
8808                        continue;
8809                    }
8810                    Some(&'Q') => {
8811                        chars.next();
8812                        mode = Some('Q');
8813                        continue;
8814                    }
8815                    Some(&'E') => {
8816                        chars.next();
8817                        mode = None;
8818                        next_char_mod = None;
8819                        continue;
8820                    }
8821                    Some(&'u') => {
8822                        chars.next();
8823                        next_char_mod = Some('u');
8824                        continue;
8825                    }
8826                    Some(&'l') => {
8827                        chars.next();
8828                        next_char_mod = Some('l');
8829                        continue;
8830                    }
8831                    _ => {}
8832                }
8833            }
8834
8835            let ch = c;
8836
8837            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
8838            if let Some(m) = next_char_mod.take() {
8839                let transformed = match m {
8840                    'u' => ch.to_uppercase().next().unwrap_or(ch),
8841                    'l' => ch.to_lowercase().next().unwrap_or(ch),
8842                    _ => ch,
8843                };
8844                result.push(transformed);
8845            } else {
8846                // Apply ongoing mode
8847                match mode {
8848                    Some('U') => {
8849                        for uc in ch.to_uppercase() {
8850                            result.push(uc);
8851                        }
8852                    }
8853                    Some('L') => {
8854                        for lc in ch.to_lowercase() {
8855                            result.push(lc);
8856                        }
8857                    }
8858                    Some('Q') => {
8859                        if !ch.is_ascii_alphanumeric() && ch != '_' {
8860                            result.push('\\');
8861                        }
8862                        result.push(ch);
8863                    }
8864                    None | Some(_) => {
8865                        result.push(ch);
8866                    }
8867                }
8868            }
8869        }
8870        result
8871    }
8872
8873    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
8874        let line = expr.line;
8875        match &expr.kind {
8876            ExprKind::Integer(n) => Ok(PerlValue::integer(*n)),
8877            ExprKind::Float(f) => Ok(PerlValue::float(*f)),
8878            ExprKind::String(s) => {
8879                let processed = Self::process_case_escapes(s);
8880                Ok(PerlValue::string(processed))
8881            }
8882            ExprKind::Bareword(s) => {
8883                if s == "__PACKAGE__" {
8884                    return Ok(PerlValue::string(self.current_package()));
8885                }
8886                if let Some(sub) = self.resolve_sub_by_name(s) {
8887                    return self.call_sub(&sub, vec![], ctx, line);
8888                }
8889                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
8890                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
8891                    return r.map_err(Into::into);
8892                }
8893                Ok(PerlValue::string(s.clone()))
8894            }
8895            ExprKind::Undef => Ok(PerlValue::UNDEF),
8896            ExprKind::MagicConst(MagicConstKind::File) => Ok(PerlValue::string(self.file.clone())),
8897            ExprKind::MagicConst(MagicConstKind::Line) => Ok(PerlValue::integer(expr.line as i64)),
8898            ExprKind::MagicConst(MagicConstKind::Sub) => {
8899                if let Some(sub) = self.current_sub_stack.last().cloned() {
8900                    Ok(PerlValue::code_ref(sub))
8901                } else {
8902                    Ok(PerlValue::UNDEF)
8903                }
8904            }
8905            ExprKind::Regex(pattern, flags) => {
8906                if ctx == WantarrayCtx::Void {
8907                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
8908                    let topic = self.scope.get_scalar("_");
8909                    let s = topic.to_string();
8910                    self.regex_match_execute(s, pattern, flags, false, "_", line)
8911                } else {
8912                    let re = self.compile_regex(pattern, flags, line)?;
8913                    Ok(PerlValue::regex(re, pattern.clone(), flags.clone()))
8914                }
8915            }
8916            ExprKind::QW(words) => Ok(PerlValue::array(
8917                words.iter().map(|w| PerlValue::string(w.clone())).collect(),
8918            )),
8919
8920            // Interpolated strings
8921            ExprKind::InterpolatedString(parts) => {
8922                let mut raw_result = String::new();
8923                for part in parts {
8924                    match part {
8925                        StringPart::Literal(s) => raw_result.push_str(s),
8926                        StringPart::ScalarVar(name) => {
8927                            self.check_strict_scalar_var(name, line)?;
8928                            let val = self.get_special_var(name);
8929                            let s = self.stringify_value(val, line)?;
8930                            raw_result.push_str(&s);
8931                        }
8932                        StringPart::ArrayVar(name) => {
8933                            self.check_strict_array_var(name, line)?;
8934                            let aname = self.stash_array_name_for_package(name);
8935                            let arr = self.scope.get_array(&aname);
8936                            let mut parts = Vec::with_capacity(arr.len());
8937                            for v in &arr {
8938                                parts.push(self.stringify_value(v.clone(), line)?);
8939                            }
8940                            let sep = self.list_separator.clone();
8941                            raw_result.push_str(&parts.join(&sep));
8942                        }
8943                        StringPart::Expr(e) => {
8944                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
8945                                self.check_strict_array_var(array, line)?;
8946                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8947                                let val = self.peel_array_ref_for_list_join(val);
8948                                let list = val.to_list();
8949                                let sep = self.list_separator.clone();
8950                                let mut parts = Vec::with_capacity(list.len());
8951                                for v in list {
8952                                    parts.push(self.stringify_value(v, line)?);
8953                                }
8954                                raw_result.push_str(&parts.join(&sep));
8955                            } else if let ExprKind::Deref {
8956                                kind: Sigil::Array, ..
8957                            } = &e.kind
8958                            {
8959                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8960                                let val = self.peel_array_ref_for_list_join(val);
8961                                let list = val.to_list();
8962                                let sep = self.list_separator.clone();
8963                                let mut parts = Vec::with_capacity(list.len());
8964                                for v in list {
8965                                    parts.push(self.stringify_value(v, line)?);
8966                                }
8967                                raw_result.push_str(&parts.join(&sep));
8968                            } else {
8969                                let val = self.eval_expr(e)?;
8970                                let s = self.stringify_value(val, line)?;
8971                                raw_result.push_str(&s);
8972                            }
8973                        }
8974                    }
8975                }
8976                let result = Self::process_case_escapes(&raw_result);
8977                Ok(PerlValue::string(result))
8978            }
8979
8980            // Variables
8981            ExprKind::ScalarVar(name) => {
8982                self.check_strict_scalar_var(name, line)?;
8983                let stor = self.tree_scalar_storage_name(name);
8984                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
8985                    let class = obj
8986                        .as_blessed_ref()
8987                        .map(|b| b.class.clone())
8988                        .unwrap_or_default();
8989                    let full = format!("{}::FETCH", class);
8990                    if let Some(sub) = self.subs.get(&full).cloned() {
8991                        return self.call_sub(&sub, vec![obj], ctx, line);
8992                    }
8993                }
8994                Ok(self.get_special_var(&stor))
8995            }
8996            ExprKind::ArrayVar(name) => {
8997                self.check_strict_array_var(name, line)?;
8998                let aname = self.stash_array_name_for_package(name);
8999                let arr = self.scope.get_array(&aname);
9000                if ctx == WantarrayCtx::List {
9001                    Ok(PerlValue::array(arr))
9002                } else {
9003                    Ok(PerlValue::integer(arr.len() as i64))
9004                }
9005            }
9006            ExprKind::HashVar(name) => {
9007                self.check_strict_hash_var(name, line)?;
9008                self.touch_env_hash(name);
9009                let h = self.scope.get_hash(name);
9010                let pv = PerlValue::hash(h);
9011                if ctx == WantarrayCtx::List {
9012                    Ok(pv)
9013                } else {
9014                    Ok(pv.scalar_context())
9015                }
9016            }
9017            ExprKind::Typeglob(name) => {
9018                let n = self.resolve_io_handle_name(name);
9019                Ok(PerlValue::string(n))
9020            }
9021            ExprKind::TypeglobExpr(e) => {
9022                let name = self.eval_expr(e)?.to_string();
9023                let n = self.resolve_io_handle_name(&name);
9024                Ok(PerlValue::string(n))
9025            }
9026            ExprKind::ArrayElement { array, index } => {
9027                // Stryke string-index sugar: bareword `_[N]` parses to an
9028                // ArrayElement with a `__topicstr__N` synthetic name. Strip
9029                // the prefix and treat as substr-of-topic. Differs from
9030                // `$_[N]` (sigil form) which keeps Perl's @_-access.
9031                if let Some(real) = array.strip_prefix("__topicstr__") {
9032                    let s = self.scope.get_scalar(real).to_string();
9033                    if let ExprKind::Range {
9034                        from,
9035                        to,
9036                        exclusive,
9037                        step,
9038                    } = &index.kind
9039                    {
9040                        let n = s.chars().count() as i64;
9041                        let mut from_i = self.eval_expr(from)?.to_int();
9042                        let mut to_i = self.eval_expr(to)?.to_int();
9043                        let step_i = match step {
9044                            Some(e) => self.eval_expr(e)?.to_int(),
9045                            None => 1,
9046                        };
9047                        if from_i < 0 {
9048                            from_i += n
9049                        }
9050                        if to_i < 0 {
9051                            to_i += n
9052                        }
9053                        if *exclusive {
9054                            to_i -= 1
9055                        }
9056                        let chars: Vec<char> = s.chars().collect();
9057                        let mut out = String::new();
9058                        if step_i > 0 {
9059                            let mut i = from_i;
9060                            while i <= to_i && i < n {
9061                                if i >= 0 {
9062                                    out.push(chars[i as usize]);
9063                                }
9064                                i += step_i;
9065                            }
9066                        } else if step_i < 0 {
9067                            let mut i = from_i;
9068                            while i >= to_i && i >= 0 {
9069                                if i < n {
9070                                    out.push(chars[i as usize]);
9071                                }
9072                                i += step_i;
9073                            }
9074                        }
9075                        return Ok(PerlValue::string(out));
9076                    }
9077                    let idx = self.eval_expr(index)?.to_int();
9078                    let n = s.chars().count() as i64;
9079                    let i = if idx < 0 { idx + n } else { idx };
9080                    return Ok(if i >= 0 && i < n {
9081                        s.chars()
9082                            .nth(i as usize)
9083                            .map(|c| PerlValue::string(c.to_string()))
9084                            .unwrap_or(PerlValue::UNDEF)
9085                    } else {
9086                        PerlValue::UNDEF
9087                    });
9088                }
9089                self.check_strict_array_var(array, line)?;
9090                // Stryke (non-compat) string-slice sugar: when the index is
9091                // a `from:to[:step]` range AND the target is a string, return
9092                // a substring with optional step. Mirrors Python `s[1:10:2]`.
9093                // Detect this BEFORE collapsing the range to an int.
9094                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9095                    if let ExprKind::Range {
9096                        from,
9097                        to,
9098                        exclusive,
9099                        step,
9100                    } = &index.kind
9101                    {
9102                        let aname_check = self.stash_array_name_for_package(array);
9103                        let prefer_scalar =
9104                            array == "_" || self.scope.get_array(&aname_check).is_empty();
9105                        if prefer_scalar {
9106                            let s = self.scope.get_scalar(array).to_string();
9107                            if !s.is_empty() {
9108                                let n = s.chars().count() as i64;
9109                                let mut from_i = self.eval_expr(from)?.to_int();
9110                                let mut to_i = self.eval_expr(to)?.to_int();
9111                                let step_i = match step {
9112                                    Some(e) => self.eval_expr(e)?.to_int(),
9113                                    None => 1,
9114                                };
9115                                if from_i < 0 {
9116                                    from_i += n
9117                                }
9118                                if to_i < 0 {
9119                                    to_i += n
9120                                }
9121                                if *exclusive {
9122                                    to_i -= 1
9123                                }
9124                                let chars: Vec<char> = s.chars().collect();
9125                                let mut out = String::new();
9126                                if step_i > 0 {
9127                                    let mut i = from_i;
9128                                    while i <= to_i && i < n {
9129                                        if i >= 0 {
9130                                            out.push(chars[i as usize]);
9131                                        }
9132                                        i += step_i;
9133                                    }
9134                                } else if step_i < 0 {
9135                                    let mut i = from_i;
9136                                    while i >= to_i && i >= 0 {
9137                                        if i < n {
9138                                            out.push(chars[i as usize]);
9139                                        }
9140                                        i += step_i;
9141                                    }
9142                                }
9143                                return Ok(PerlValue::string(out));
9144                            }
9145                        }
9146                    }
9147                }
9148                let idx = self.eval_expr(index)?.to_int();
9149                let aname = self.stash_array_name_for_package(array);
9150                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
9151                    let class = obj
9152                        .as_blessed_ref()
9153                        .map(|b| b.class.clone())
9154                        .unwrap_or_default();
9155                    let full = format!("{}::FETCH", class);
9156                    if let Some(sub) = self.subs.get(&full).cloned() {
9157                        let arg_vals = vec![obj, PerlValue::integer(idx)];
9158                        return self.call_sub(&sub, arg_vals, ctx, line);
9159                    }
9160                }
9161                // Stryke (non-compat) sugar: `$name[i]` indexes by Unicode
9162                // char when `@name` is missing/empty but `$name` is a
9163                // non-empty string. So `$s[0]` is the first grapheme of
9164                // `$s`. NB: `$_[0]` keeps Perl's `@_`-access semantics
9165                // because `@_` is populated inside any sub call; the
9166                // bareword `_[0]` parses to the same AST node, so both
9167                // forms behave alike — use `substr(_, 0, 1)` for char-of-
9168                // topic when inside a sub. Compat mode = Perl semantics.
9169                if !crate::compat_mode() && self.scope.scalar_binding_exists(array) {
9170                    let prefer_scalar = self.scope.get_array(&aname).is_empty();
9171                    if prefer_scalar {
9172                        let s = self.scope.get_scalar(array).to_string();
9173                        if !s.is_empty() {
9174                            let n = s.chars().count() as i64;
9175                            let i = if idx < 0 { idx + n } else { idx };
9176                            if i >= 0 && i < n {
9177                                if let Some(c) = s.chars().nth(i as usize) {
9178                                    return Ok(PerlValue::string(c.to_string()));
9179                                }
9180                            }
9181                            return Ok(PerlValue::UNDEF);
9182                        }
9183                    }
9184                }
9185                Ok(self.scope.get_array_element(&aname, idx))
9186            }
9187            ExprKind::HashElement { hash, key } => {
9188                self.check_strict_hash_var(hash, line)?;
9189                let k = self.eval_expr(key)?.to_string();
9190                self.touch_env_hash(hash);
9191                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
9192                    let class = obj
9193                        .as_blessed_ref()
9194                        .map(|b| b.class.clone())
9195                        .unwrap_or_default();
9196                    let full = format!("{}::FETCH", class);
9197                    if let Some(sub) = self.subs.get(&full).cloned() {
9198                        let arg_vals = vec![obj, PerlValue::string(k)];
9199                        return self.call_sub(&sub, arg_vals, ctx, line);
9200                    }
9201                }
9202                Ok(self.scope.get_hash_element(hash, &k))
9203            }
9204            ExprKind::ArraySlice { array, indices } => {
9205                self.check_strict_array_var(array, line)?;
9206                let aname = self.stash_array_name_for_package(array);
9207                let flat = self.flatten_array_slice_index_specs(indices)?;
9208                let mut result = Vec::with_capacity(flat.len());
9209                for idx in flat {
9210                    result.push(self.scope.get_array_element(&aname, idx));
9211                }
9212                Ok(PerlValue::array(result))
9213            }
9214            ExprKind::HashSlice { hash, keys } => {
9215                self.check_strict_hash_var(hash, line)?;
9216                self.touch_env_hash(hash);
9217                let mut result = Vec::new();
9218                for key_expr in keys {
9219                    for k in self.eval_hash_slice_key_components(key_expr)? {
9220                        result.push(self.scope.get_hash_element(hash, &k));
9221                    }
9222                }
9223                Ok(PerlValue::array(result))
9224            }
9225            ExprKind::HashKvSlice { hash, keys } => {
9226                // `%h{KEYS}` — Perl 5.20+ key-value slice. Returns a flat
9227                // (key, value, key, value, ...) list. (BUG-008)
9228                self.check_strict_hash_var(hash, line)?;
9229                self.touch_env_hash(hash);
9230                let mut result = Vec::new();
9231                for key_expr in keys {
9232                    for k in self.eval_hash_slice_key_components(key_expr)? {
9233                        let v = self.scope.get_hash_element(hash, &k);
9234                        result.push(PerlValue::string(k));
9235                        result.push(v);
9236                    }
9237                }
9238                Ok(PerlValue::array(result))
9239            }
9240            ExprKind::HashSliceDeref { container, keys } => {
9241                let hv = self.eval_expr(container)?;
9242                let mut key_vals = Vec::with_capacity(keys.len());
9243                for key_expr in keys {
9244                    let v = if matches!(
9245                        key_expr.kind,
9246                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
9247                    ) {
9248                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
9249                    } else {
9250                        self.eval_expr(key_expr)?
9251                    };
9252                    key_vals.push(v);
9253                }
9254                self.hash_slice_deref_values(&hv, &key_vals, line)
9255            }
9256            ExprKind::AnonymousListSlice { source, indices } => {
9257                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
9258                let items = list_val.to_list();
9259                let flat = self.flatten_array_slice_index_specs(indices)?;
9260                let mut out = Vec::with_capacity(flat.len());
9261                for idx in flat {
9262                    let i = if idx < 0 {
9263                        (items.len() as i64 + idx) as usize
9264                    } else {
9265                        idx as usize
9266                    };
9267                    out.push(items.get(i).cloned().unwrap_or(PerlValue::UNDEF));
9268                }
9269                let arr = PerlValue::array(out);
9270                if ctx != WantarrayCtx::List {
9271                    let v = arr.to_list();
9272                    Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF))
9273                } else {
9274                    Ok(arr)
9275                }
9276            }
9277
9278            // References
9279            ExprKind::ScalarRef(inner) => match &inner.kind {
9280                ExprKind::ScalarVar(name) => Ok(PerlValue::scalar_binding_ref(name.clone())),
9281                ExprKind::ArrayVar(name) => {
9282                    self.check_strict_array_var(name, line)?;
9283                    let aname = self.stash_array_name_for_package(name);
9284                    // Promote the scope's array to shared Arc-backed storage.
9285                    // Both the scope and the returned ref share the same Arc.
9286                    let arc = self.scope.promote_array_to_shared(&aname);
9287                    Ok(PerlValue::array_ref(arc))
9288                }
9289                ExprKind::HashVar(name) => {
9290                    self.check_strict_hash_var(name, line)?;
9291                    let arc = self.scope.promote_hash_to_shared(name);
9292                    Ok(PerlValue::hash_ref(arc))
9293                }
9294                ExprKind::Deref {
9295                    expr: e,
9296                    kind: Sigil::Array,
9297                } => {
9298                    let v = self.eval_expr(e)?;
9299                    self.make_array_ref_alias(v, line)
9300                }
9301                ExprKind::Deref {
9302                    expr: e,
9303                    kind: Sigil::Hash,
9304                } => {
9305                    let v = self.eval_expr(e)?;
9306                    self.make_hash_ref_alias(v, line)
9307                }
9308                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
9309                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
9310                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
9311                }
9312                ExprKind::HashSliceDeref { .. } => {
9313                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
9314                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
9315                }
9316                _ => {
9317                    let val = self.eval_expr(inner)?;
9318                    Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
9319                }
9320            },
9321            ExprKind::ArrayRef(elems) => {
9322                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
9323                // variables flatten into the ref rather than collapsing to a scalar count /
9324                // flip-flop value.
9325                let mut arr = Vec::with_capacity(elems.len());
9326                for e in elems {
9327                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
9328                    let v = self.scope.resolve_container_binding_ref(v);
9329                    if let Some(vec) = v.as_array_vec() {
9330                        arr.extend(vec);
9331                    } else {
9332                        arr.push(v);
9333                    }
9334                }
9335                Ok(PerlValue::array_ref(Arc::new(RwLock::new(arr))))
9336            }
9337            ExprKind::HashRef(pairs) => {
9338                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
9339                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
9340                let mut map = IndexMap::new();
9341                for (k, v) in pairs {
9342                    let key_str = self.eval_expr(k)?.to_string();
9343                    if key_str == "__HASH_SPREAD__" {
9344                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
9345                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
9346                        let items = spread.to_list();
9347                        let mut i = 0;
9348                        while i + 1 < items.len() {
9349                            map.insert(items[i].to_string(), items[i + 1].clone());
9350                            i += 2;
9351                        }
9352                    } else {
9353                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
9354                        map.insert(key_str, val);
9355                    }
9356                }
9357                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map))))
9358            }
9359            ExprKind::CodeRef { params, body } => {
9360                let captured = self.scope.capture();
9361                Ok(PerlValue::code_ref(Arc::new(PerlSub {
9362                    name: "__ANON__".to_string(),
9363                    params: params.clone(),
9364                    body: body.clone(),
9365                    closure_env: Some(captured),
9366                    prototype: None,
9367                    fib_like: None,
9368                })))
9369            }
9370            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
9371            ExprKind::SubroutineCodeRef(name) => {
9372                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
9373                    PerlError::runtime(self.undefined_subroutine_resolve_message(name), line)
9374                })?;
9375                Ok(PerlValue::code_ref(sub))
9376            }
9377            ExprKind::DynamicSubCodeRef(expr) => {
9378                let name = self.eval_expr(expr)?.to_string();
9379                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
9380                    PerlError::runtime(self.undefined_subroutine_resolve_message(&name), line)
9381                })?;
9382                Ok(PerlValue::code_ref(sub))
9383            }
9384            ExprKind::Deref { expr, kind } => {
9385                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
9386                    let val = self.eval_expr(expr)?;
9387                    let n = self.array_deref_len(val, line)?;
9388                    return Ok(PerlValue::integer(n));
9389                }
9390                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
9391                    let val = self.eval_expr(expr)?;
9392                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
9393                    return Ok(h.scalar_context());
9394                }
9395                let val = self.eval_expr(expr)?;
9396                self.symbolic_deref(val, *kind, line)
9397            }
9398            ExprKind::ArrowDeref { expr, index, kind } => {
9399                match kind {
9400                    DerefKind::Array => {
9401                        let container = self.eval_arrow_array_base(expr, line)?;
9402                        if let ExprKind::List(indices) = &index.kind {
9403                            let mut out = Vec::with_capacity(indices.len());
9404                            for ix in indices {
9405                                let idx = self.eval_expr(ix)?.to_int();
9406                                out.push(self.read_arrow_array_element(
9407                                    container.clone(),
9408                                    idx,
9409                                    line,
9410                                )?);
9411                            }
9412                            let arr = PerlValue::array(out);
9413                            if ctx != WantarrayCtx::List {
9414                                let v = arr.to_list();
9415                                return Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF));
9416                            }
9417                            return Ok(arr);
9418                        }
9419                        let idx = self.eval_expr(index)?.to_int();
9420                        self.read_arrow_array_element(container, idx, line)
9421                    }
9422                    DerefKind::Hash => {
9423                        let val = self.eval_arrow_hash_base(expr, line)?;
9424                        let key = self.eval_expr(index)?.to_string();
9425                        self.read_arrow_hash_element(val, key.as_str(), line)
9426                    }
9427                    DerefKind::Call => {
9428                        // $coderef->(args)
9429                        let val = self.eval_expr(expr)?;
9430                        if let ExprKind::List(ref arg_exprs) = index.kind {
9431                            let mut args = Vec::new();
9432                            for a in arg_exprs {
9433                                args.push(self.eval_expr(a)?);
9434                            }
9435                            // Auto-deref ScalarRef for closure self-reference: $f->()
9436                            let callable = if let Some(inner) = val.as_scalar_ref() {
9437                                inner.read().clone()
9438                            } else {
9439                                val
9440                            };
9441                            if let Some(sub) = callable.as_code_ref() {
9442                                return self.call_sub(&sub, args, ctx, line);
9443                            }
9444                            Err(PerlError::runtime("Not a code reference", line).into())
9445                        } else {
9446                            Err(PerlError::runtime("Invalid call deref", line).into())
9447                        }
9448                    }
9449                }
9450            }
9451
9452            // Binary operators
9453            ExprKind::BinOp { left, op, right } => {
9454                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
9455                match op {
9456                    BinOp::BindMatch => {
9457                        let lv = self.eval_expr(left)?;
9458                        let rv = self.eval_expr(right)?;
9459                        let s = lv.to_string();
9460                        let pat = rv.to_string();
9461                        return self.regex_match_execute(s, &pat, "", false, "_", line);
9462                    }
9463                    BinOp::BindNotMatch => {
9464                        let lv = self.eval_expr(left)?;
9465                        let rv = self.eval_expr(right)?;
9466                        let s = lv.to_string();
9467                        let pat = rv.to_string();
9468                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
9469                        return Ok(PerlValue::integer(if m.is_true() { 0 } else { 1 }));
9470                    }
9471                    BinOp::LogAnd | BinOp::LogAndWord => {
9472                        match &left.kind {
9473                            ExprKind::Regex(_, _) => {
9474                                if !self.eval_boolean_rvalue_condition(left)? {
9475                                    return Ok(PerlValue::string(String::new()));
9476                                }
9477                            }
9478                            _ => {
9479                                let lv = self.eval_expr(left)?;
9480                                if !lv.is_true() {
9481                                    return Ok(lv);
9482                                }
9483                            }
9484                        }
9485                        return match &right.kind {
9486                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
9487                                if self.eval_boolean_rvalue_condition(right)? {
9488                                    1
9489                                } else {
9490                                    0
9491                                },
9492                            )),
9493                            _ => self.eval_expr(right),
9494                        };
9495                    }
9496                    BinOp::LogOr | BinOp::LogOrWord => {
9497                        match &left.kind {
9498                            ExprKind::Regex(_, _) => {
9499                                if self.eval_boolean_rvalue_condition(left)? {
9500                                    return Ok(PerlValue::integer(1));
9501                                }
9502                            }
9503                            _ => {
9504                                let lv = self.eval_expr(left)?;
9505                                if lv.is_true() {
9506                                    return Ok(lv);
9507                                }
9508                            }
9509                        }
9510                        return match &right.kind {
9511                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
9512                                if self.eval_boolean_rvalue_condition(right)? {
9513                                    1
9514                                } else {
9515                                    0
9516                                },
9517                            )),
9518                            _ => self.eval_expr(right),
9519                        };
9520                    }
9521                    BinOp::DefinedOr => {
9522                        let lv = self.eval_expr(left)?;
9523                        if !lv.is_undef() {
9524                            return Ok(lv);
9525                        }
9526                        return self.eval_expr(right);
9527                    }
9528                    _ => {}
9529                }
9530                let lv = self.eval_expr(left)?;
9531                let rv = self.eval_expr(right)?;
9532                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
9533                    return r;
9534                }
9535                self.eval_binop(*op, &lv, &rv, line)
9536            }
9537
9538            // Unary
9539            ExprKind::UnaryOp { op, expr } => match op {
9540                UnaryOp::PreIncrement => {
9541                    if let ExprKind::ScalarVar(name) = &expr.kind {
9542                        self.check_strict_scalar_var(name, line)?;
9543                        let n = self.resolved_scalar_storage_name(name);
9544                        return Ok(self
9545                            .scope
9546                            .atomic_mutate(&n, perl_inc)
9547                            .map_err(|e| e.at_line(line))?);
9548                    }
9549                    if let ExprKind::Deref { kind, .. } = &expr.kind {
9550                        if matches!(kind, Sigil::Array | Sigil::Hash) {
9551                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
9552                                *kind, true, true, line,
9553                            ));
9554                        }
9555                    }
9556                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
9557                        let href = self.eval_expr(container)?;
9558                        let mut key_vals = Vec::with_capacity(keys.len());
9559                        for key_expr in keys {
9560                            key_vals.push(self.eval_expr(key_expr)?);
9561                        }
9562                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
9563                    }
9564                    if let ExprKind::ArrowDeref {
9565                        expr: arr_expr,
9566                        index,
9567                        kind: DerefKind::Array,
9568                    } = &expr.kind
9569                    {
9570                        if let ExprKind::List(indices) = &index.kind {
9571                            let container = self.eval_arrow_array_base(arr_expr, line)?;
9572                            let mut idxs = Vec::with_capacity(indices.len());
9573                            for ix in indices {
9574                                idxs.push(self.eval_expr(ix)?.to_int());
9575                            }
9576                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
9577                        }
9578                    }
9579                    let val = self.eval_expr(expr)?;
9580                    let new_val = perl_inc(&val);
9581                    self.assign_value(expr, new_val.clone())?;
9582                    Ok(new_val)
9583                }
9584                UnaryOp::PreDecrement => {
9585                    if let ExprKind::ScalarVar(name) = &expr.kind {
9586                        self.check_strict_scalar_var(name, line)?;
9587                        let n = self.resolved_scalar_storage_name(name);
9588                        return Ok(self
9589                            .scope
9590                            .atomic_mutate(&n, |v| PerlValue::integer(v.to_int() - 1))
9591                            .map_err(|e| e.at_line(line))?);
9592                    }
9593                    if let ExprKind::Deref { kind, .. } = &expr.kind {
9594                        if matches!(kind, Sigil::Array | Sigil::Hash) {
9595                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
9596                                *kind, true, false, line,
9597                            ));
9598                        }
9599                    }
9600                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
9601                        let href = self.eval_expr(container)?;
9602                        let mut key_vals = Vec::with_capacity(keys.len());
9603                        for key_expr in keys {
9604                            key_vals.push(self.eval_expr(key_expr)?);
9605                        }
9606                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
9607                    }
9608                    if let ExprKind::ArrowDeref {
9609                        expr: arr_expr,
9610                        index,
9611                        kind: DerefKind::Array,
9612                    } = &expr.kind
9613                    {
9614                        if let ExprKind::List(indices) = &index.kind {
9615                            let container = self.eval_arrow_array_base(arr_expr, line)?;
9616                            let mut idxs = Vec::with_capacity(indices.len());
9617                            for ix in indices {
9618                                idxs.push(self.eval_expr(ix)?.to_int());
9619                            }
9620                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
9621                        }
9622                    }
9623                    let val = self.eval_expr(expr)?;
9624                    let new_val = PerlValue::integer(val.to_int() - 1);
9625                    self.assign_value(expr, new_val.clone())?;
9626                    Ok(new_val)
9627                }
9628                _ => {
9629                    match op {
9630                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
9631                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
9632                                let topic = self.scope.get_scalar("_");
9633                                let rl = expr.line;
9634                                let s = topic.to_string();
9635                                let v =
9636                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
9637                                return Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }));
9638                            }
9639                        }
9640                        _ => {}
9641                    }
9642                    let val = self.eval_expr(expr)?;
9643                    match op {
9644                        UnaryOp::Negate => {
9645                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
9646                                return r;
9647                            }
9648                            if let Some(n) = val.as_integer() {
9649                                Ok(PerlValue::integer(-n))
9650                            } else {
9651                                Ok(PerlValue::float(-val.to_number()))
9652                            }
9653                        }
9654                        UnaryOp::LogNot => {
9655                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
9656                                let pv = r?;
9657                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
9658                            }
9659                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
9660                        }
9661                        UnaryOp::BitNot => Ok(PerlValue::integer(!val.to_int())),
9662                        UnaryOp::LogNotWord => {
9663                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
9664                                let pv = r?;
9665                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
9666                            }
9667                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
9668                        }
9669                        UnaryOp::Ref => {
9670                            if let ExprKind::ScalarVar(name) = &expr.kind {
9671                                return Ok(PerlValue::scalar_binding_ref(name.clone()));
9672                            }
9673                            Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
9674                        }
9675                        _ => unreachable!(),
9676                    }
9677                }
9678            },
9679
9680            ExprKind::PostfixOp { expr, op } => {
9681                // For scalar variables, use atomic_mutate_post to hold the lock
9682                // for the entire read-modify-write (critical for mysync).
9683                if let ExprKind::ScalarVar(name) = &expr.kind {
9684                    self.check_strict_scalar_var(name, line)?;
9685                    let n = self.resolved_scalar_storage_name(name);
9686                    let f: fn(&PerlValue) -> PerlValue = match op {
9687                        PostfixOp::Increment => |v| perl_inc(v),
9688                        PostfixOp::Decrement => |v| PerlValue::integer(v.to_int() - 1),
9689                    };
9690                    return Ok(self
9691                        .scope
9692                        .atomic_mutate_post(&n, f)
9693                        .map_err(|e| e.at_line(line))?);
9694                }
9695                if let ExprKind::Deref { kind, .. } = &expr.kind {
9696                    if matches!(kind, Sigil::Array | Sigil::Hash) {
9697                        let is_inc = matches!(op, PostfixOp::Increment);
9698                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
9699                            *kind, false, is_inc, line,
9700                        ));
9701                    }
9702                }
9703                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
9704                    let href = self.eval_expr(container)?;
9705                    let mut key_vals = Vec::with_capacity(keys.len());
9706                    for key_expr in keys {
9707                        key_vals.push(self.eval_expr(key_expr)?);
9708                    }
9709                    let kind_byte = match op {
9710                        PostfixOp::Increment => 2u8,
9711                        PostfixOp::Decrement => 3u8,
9712                    };
9713                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
9714                }
9715                if let ExprKind::ArrowDeref {
9716                    expr: arr_expr,
9717                    index,
9718                    kind: DerefKind::Array,
9719                } = &expr.kind
9720                {
9721                    if let ExprKind::List(indices) = &index.kind {
9722                        let container = self.eval_arrow_array_base(arr_expr, line)?;
9723                        let mut idxs = Vec::with_capacity(indices.len());
9724                        for ix in indices {
9725                            idxs.push(self.eval_expr(ix)?.to_int());
9726                        }
9727                        let kind_byte = match op {
9728                            PostfixOp::Increment => 2u8,
9729                            PostfixOp::Decrement => 3u8,
9730                        };
9731                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
9732                    }
9733                }
9734                let val = self.eval_expr(expr)?;
9735                let old = val.clone();
9736                let new_val = match op {
9737                    PostfixOp::Increment => perl_inc(&val),
9738                    PostfixOp::Decrement => PerlValue::integer(val.to_int() - 1),
9739                };
9740                self.assign_value(expr, new_val)?;
9741                Ok(old)
9742            }
9743
9744            // Assignment
9745            ExprKind::Assign { target, value } => {
9746                if let ExprKind::Typeglob(lhs) = &target.kind {
9747                    if let ExprKind::Typeglob(rhs) = &value.kind {
9748                        self.copy_typeglob_slots(lhs, rhs, line)?;
9749                        return self.eval_expr(value);
9750                    }
9751                }
9752                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
9753                self.assign_value(target, val.clone())?;
9754                Ok(val)
9755            }
9756            ExprKind::CompoundAssign { target, op, value } => {
9757                // For scalar targets, use atomic_mutate to hold the lock.
9758                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
9759                if let ExprKind::ScalarVar(name) = &target.kind {
9760                    self.check_strict_scalar_var(name, line)?;
9761                    let n = self.resolved_scalar_storage_name(name);
9762                    let op = *op;
9763                    let rhs = match op {
9764                        BinOp::LogOr => {
9765                            let old = self.scope.get_scalar(&n);
9766                            if old.is_true() {
9767                                return Ok(old);
9768                            }
9769                            self.eval_expr(value)?
9770                        }
9771                        BinOp::DefinedOr => {
9772                            let old = self.scope.get_scalar(&n);
9773                            if !old.is_undef() {
9774                                return Ok(old);
9775                            }
9776                            self.eval_expr(value)?
9777                        }
9778                        BinOp::LogAnd => {
9779                            let old = self.scope.get_scalar(&n);
9780                            if !old.is_true() {
9781                                return Ok(old);
9782                            }
9783                            self.eval_expr(value)?
9784                        }
9785                        _ => self.eval_expr(value)?,
9786                    };
9787                    return Ok(self.scalar_compound_assign_scalar_target(&n, op, rhs)?);
9788                }
9789                let rhs = self.eval_expr(value)?;
9790                // For hash element targets: $h{key} += 1
9791                if let ExprKind::HashElement { hash, key } = &target.kind {
9792                    self.check_strict_hash_var(hash, line)?;
9793                    let k = self.eval_expr(key)?.to_string();
9794                    let op = *op;
9795                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
9796                        BinOp::Add => {
9797                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9798                                PerlValue::integer(a.wrapping_add(b))
9799                            } else {
9800                                PerlValue::float(old.to_number() + rhs.to_number())
9801                            }
9802                        }
9803                        BinOp::Sub => {
9804                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9805                                PerlValue::integer(a.wrapping_sub(b))
9806                            } else {
9807                                PerlValue::float(old.to_number() - rhs.to_number())
9808                            }
9809                        }
9810                        BinOp::Concat => {
9811                            let mut s = old.to_string();
9812                            rhs.append_to(&mut s);
9813                            PerlValue::string(s)
9814                        }
9815                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
9816                    })?);
9817                }
9818                // For array element targets: $a[i] += 1
9819                if let ExprKind::ArrayElement { array, index } = &target.kind {
9820                    self.check_strict_array_var(array, line)?;
9821                    let idx = self.eval_expr(index)?.to_int();
9822                    let op = *op;
9823                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
9824                        BinOp::Add => {
9825                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9826                                PerlValue::integer(a.wrapping_add(b))
9827                            } else {
9828                                PerlValue::float(old.to_number() + rhs.to_number())
9829                            }
9830                        }
9831                        BinOp::Sub => {
9832                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9833                                PerlValue::integer(a.wrapping_sub(b))
9834                            } else {
9835                                PerlValue::float(old.to_number() - rhs.to_number())
9836                            }
9837                        }
9838                        BinOp::Mul => {
9839                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9840                                PerlValue::integer(a.wrapping_mul(b))
9841                            } else {
9842                                PerlValue::float(old.to_number() * rhs.to_number())
9843                            }
9844                        }
9845                        BinOp::Div => PerlValue::float(old.to_number() / rhs.to_number()),
9846                        BinOp::Mod => {
9847                            // Perl `%` is floored-division (sign-of-divisor),
9848                            // not Rust's `%` (sign-of-dividend) nor
9849                            // `rem_euclid` (always non-negative). Truncate
9850                            // float operands to int first, matching Perl 5.
9851                            let a = old.to_int();
9852                            let b = rhs.to_int();
9853                            if b == 0 {
9854                                PerlValue::integer(0)
9855                            } else {
9856                                PerlValue::integer(crate::value::perl_mod_i64(a, b))
9857                            }
9858                        }
9859                        BinOp::Concat => {
9860                            let mut s = old.to_string();
9861                            rhs.append_to(&mut s);
9862                            PerlValue::string(s)
9863                        }
9864                        BinOp::Pow => PerlValue::float(old.to_number().powf(rhs.to_number())),
9865                        BinOp::BitAnd => PerlValue::integer(old.to_int() & rhs.to_int()),
9866                        BinOp::BitOr => PerlValue::integer(old.to_int() | rhs.to_int()),
9867                        BinOp::BitXor => PerlValue::integer(old.to_int() ^ rhs.to_int()),
9868                        BinOp::ShiftLeft => PerlValue::integer(old.to_int() << rhs.to_int()),
9869                        BinOp::ShiftRight => PerlValue::integer(old.to_int() >> rhs.to_int()),
9870                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
9871                    })?);
9872                }
9873                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
9874                    let href = self.eval_expr(container)?;
9875                    let mut key_vals = Vec::with_capacity(keys.len());
9876                    for key_expr in keys {
9877                        key_vals.push(self.eval_expr(key_expr)?);
9878                    }
9879                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
9880                }
9881                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
9882                    if let ExprKind::Deref {
9883                        expr: inner,
9884                        kind: Sigil::Array,
9885                    } = &source.kind
9886                    {
9887                        let container = self.eval_arrow_array_base(inner, line)?;
9888                        let idxs = self.flatten_array_slice_index_specs(indices)?;
9889                        return self
9890                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
9891                    }
9892                }
9893                if let ExprKind::ArrowDeref {
9894                    expr: arr_expr,
9895                    index,
9896                    kind: DerefKind::Array,
9897                } = &target.kind
9898                {
9899                    if let ExprKind::List(indices) = &index.kind {
9900                        let container = self.eval_arrow_array_base(arr_expr, line)?;
9901                        let mut idxs = Vec::with_capacity(indices.len());
9902                        for ix in indices {
9903                            idxs.push(self.eval_expr(ix)?.to_int());
9904                        }
9905                        return self
9906                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
9907                    }
9908                }
9909                let old = self.eval_expr(target)?;
9910                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
9911                self.assign_value(target, new_val.clone())?;
9912                Ok(new_val)
9913            }
9914
9915            // Ternary — propagate wantarray context to both branches so
9916            // `($a, $b) = $c ? (1, 2) : (3, 4)` evaluates the chosen branch
9917            // in list context.
9918            ExprKind::Ternary {
9919                condition,
9920                then_expr,
9921                else_expr,
9922            } => {
9923                if self.eval_boolean_rvalue_condition(condition)? {
9924                    self.eval_expr_ctx(then_expr, ctx)
9925                } else {
9926                    self.eval_expr_ctx(else_expr, ctx)
9927                }
9928            }
9929
9930            // Range
9931            ExprKind::Range {
9932                from,
9933                to,
9934                exclusive,
9935                step,
9936            } => {
9937                if ctx == WantarrayCtx::List {
9938                    let f = self.eval_expr(from)?;
9939                    let t = self.eval_expr(to)?;
9940                    if let Some(s) = step {
9941                        let step_val = self.eval_expr(s)?.to_int();
9942                        let from_i = f.to_int();
9943                        let to_i = t.to_int();
9944                        let list = if step_val == 0 {
9945                            vec![]
9946                        } else if step_val > 0 {
9947                            (from_i..=to_i)
9948                                .step_by(step_val as usize)
9949                                .map(PerlValue::integer)
9950                                .collect()
9951                        } else {
9952                            std::iter::successors(Some(from_i), |&x| {
9953                                let next = x - step_val.abs();
9954                                if next >= to_i {
9955                                    Some(next)
9956                                } else {
9957                                    None
9958                                }
9959                            })
9960                            .map(PerlValue::integer)
9961                            .collect()
9962                        };
9963                        Ok(PerlValue::array(list))
9964                    } else {
9965                        let list = perl_list_range_expand(f, t);
9966                        Ok(PerlValue::array(list))
9967                    }
9968                } else {
9969                    let key = std::ptr::from_ref(expr) as usize;
9970                    match (&from.kind, &to.kind) {
9971                        (
9972                            ExprKind::Regex(left_pat, left_flags),
9973                            ExprKind::Regex(right_pat, right_flags),
9974                        ) => {
9975                            let dot = self.scalar_flipflop_dot_line();
9976                            let subject = self.scope.get_scalar("_").to_string();
9977                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9978                                |e| match e {
9979                                    FlowOrError::Error(err) => err,
9980                                    FlowOrError::Flow(_) => PerlError::runtime(
9981                                        "unexpected flow in regex flip-flop",
9982                                        line,
9983                                    ),
9984                                },
9985                            )?;
9986                            let right_re = self
9987                                .compile_regex(right_pat, right_flags, line)
9988                                .map_err(|e| match e {
9989                                    FlowOrError::Error(err) => err,
9990                                    FlowOrError::Flow(_) => PerlError::runtime(
9991                                        "unexpected flow in regex flip-flop",
9992                                        line,
9993                                    ),
9994                                })?;
9995                            let left_m = left_re.is_match(&subject);
9996                            let right_m = right_re.is_match(&subject);
9997                            let st = self.flip_flop_tree.entry(key).or_default();
9998                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9999                                &mut st.active,
10000                                &mut st.exclusive_left_line,
10001                                *exclusive,
10002                                dot,
10003                                left_m,
10004                                right_m,
10005                            )))
10006                        }
10007                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
10008                            let dot = self.scalar_flipflop_dot_line();
10009                            let subject = self.scope.get_scalar("_").to_string();
10010                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10011                                |e| match e {
10012                                    FlowOrError::Error(err) => err,
10013                                    FlowOrError::Flow(_) => PerlError::runtime(
10014                                        "unexpected flow in regex/eof flip-flop",
10015                                        line,
10016                                    ),
10017                                },
10018                            )?;
10019                            let left_m = left_re.is_match(&subject);
10020                            let right_m = self.eof_without_arg_is_true();
10021                            let st = self.flip_flop_tree.entry(key).or_default();
10022                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
10023                                &mut st.active,
10024                                &mut st.exclusive_left_line,
10025                                *exclusive,
10026                                dot,
10027                                left_m,
10028                                right_m,
10029                            )))
10030                        }
10031                        (
10032                            ExprKind::Regex(left_pat, left_flags),
10033                            ExprKind::Integer(_) | ExprKind::Float(_),
10034                        ) => {
10035                            let dot = self.scalar_flipflop_dot_line();
10036                            let right = self.eval_expr(to)?.to_int();
10037                            let subject = self.scope.get_scalar("_").to_string();
10038                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10039                                |e| match e {
10040                                    FlowOrError::Error(err) => err,
10041                                    FlowOrError::Flow(_) => PerlError::runtime(
10042                                        "unexpected flow in regex flip-flop",
10043                                        line,
10044                                    ),
10045                                },
10046                            )?;
10047                            let left_m = left_re.is_match(&subject);
10048                            let right_m = dot == right;
10049                            let st = self.flip_flop_tree.entry(key).or_default();
10050                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
10051                                &mut st.active,
10052                                &mut st.exclusive_left_line,
10053                                *exclusive,
10054                                dot,
10055                                left_m,
10056                                right_m,
10057                            )))
10058                        }
10059                        (ExprKind::Regex(left_pat, left_flags), _) => {
10060                            if let ExprKind::Eof(Some(_)) = &to.kind {
10061                                return Err(FlowOrError::Error(PerlError::runtime(
10062                                    "regex flip-flop with eof(HANDLE) is not supported",
10063                                    line,
10064                                )));
10065                            }
10066                            let dot = self.scalar_flipflop_dot_line();
10067                            let subject = self.scope.get_scalar("_").to_string();
10068                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
10069                                |e| match e {
10070                                    FlowOrError::Error(err) => err,
10071                                    FlowOrError::Flow(_) => PerlError::runtime(
10072                                        "unexpected flow in regex flip-flop",
10073                                        line,
10074                                    ),
10075                                },
10076                            )?;
10077                            let left_m = left_re.is_match(&subject);
10078                            let right_m = self.eval_boolean_rvalue_condition(to)?;
10079                            let st = self.flip_flop_tree.entry(key).or_default();
10080                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
10081                                &mut st.active,
10082                                &mut st.exclusive_left_line,
10083                                *exclusive,
10084                                dot,
10085                                left_m,
10086                                right_m,
10087                            )))
10088                        }
10089                        _ => {
10090                            let left = self.eval_expr(from)?.to_int();
10091                            let right = self.eval_expr(to)?.to_int();
10092                            let dot = self.scalar_flipflop_dot_line();
10093                            let st = self.flip_flop_tree.entry(key).or_default();
10094                            if !st.active {
10095                                if dot == left {
10096                                    st.active = true;
10097                                    if *exclusive {
10098                                        st.exclusive_left_line = Some(dot);
10099                                    } else {
10100                                        st.exclusive_left_line = None;
10101                                        if dot == right {
10102                                            st.active = false;
10103                                        }
10104                                    }
10105                                    return Ok(PerlValue::integer(1));
10106                                }
10107                                return Ok(PerlValue::integer(0));
10108                            }
10109                            if let Some(ll) = st.exclusive_left_line {
10110                                if dot == right && dot > ll {
10111                                    st.active = false;
10112                                    st.exclusive_left_line = None;
10113                                }
10114                            } else if dot == right {
10115                                st.active = false;
10116                            }
10117                            Ok(PerlValue::integer(1))
10118                        }
10119                    }
10120                }
10121            }
10122
10123            // SliceRange — open-ended Python-style slice expansion. Reachable from the
10124            // tree-walker when slice subscripts are evaluated outside the VM (rare; VM is
10125            // the primary execution engine). Only closed forms (`from:to[:step]`) can be
10126            // expanded here without container length context; open ends require a slice
10127            // op (`Op::ArraySliceRange` / `Op::HashSliceRange`) which knows the container.
10128            ExprKind::SliceRange { from, to, step } => {
10129                let f = match from {
10130                    Some(e) => self.eval_expr(e)?,
10131                    None => {
10132                        return Err(PerlError::runtime(
10133                            "open-ended slice range cannot be evaluated outside slice subscript",
10134                            line,
10135                        )
10136                        .into());
10137                    }
10138                };
10139                let t = match to {
10140                    Some(e) => self.eval_expr(e)?,
10141                    None => {
10142                        return Err(PerlError::runtime(
10143                            "open-ended slice range cannot be evaluated outside slice subscript",
10144                            line,
10145                        )
10146                        .into());
10147                    }
10148                };
10149                let list = if let Some(s) = step {
10150                    let sv = self.eval_expr(s)?;
10151                    crate::value::perl_list_range_expand_stepped(f, t, sv)
10152                } else {
10153                    perl_list_range_expand(f, t)
10154                };
10155                Ok(PerlValue::array(list))
10156            }
10157
10158            // Repeat — see `ast.rs` `ExprKind::Repeat` for the list/scalar split.
10159            ExprKind::Repeat {
10160                expr,
10161                count,
10162                list_repeat,
10163            } => {
10164                let n = self.eval_expr(count)?.to_int().max(0) as usize;
10165                if *list_repeat {
10166                    // `(LIST) x N` — evaluate the LHS in list context, replicate.
10167                    let saved = self.wantarray_kind;
10168                    self.wantarray_kind = WantarrayCtx::List;
10169                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10170                    self.wantarray_kind = saved;
10171                    let items: Vec<PerlValue> = val.as_array_vec().unwrap_or_else(|| vec![val]);
10172                    let mut result = Vec::with_capacity(items.len() * n);
10173                    for _ in 0..n {
10174                        result.extend(items.iter().cloned());
10175                    }
10176                    Ok(PerlValue::array(result))
10177                } else {
10178                    // `EXPR x N` — scalar string repetition.
10179                    let val = self.eval_expr(expr)?;
10180                    Ok(PerlValue::string(val.to_string().repeat(n)))
10181                }
10182            }
10183
10184            // `my $x = …` / `our` / `state` / `local` used as an expression
10185            // (e.g. `if (my $line = readline)`).  Declare each variable in the
10186            // current scope, evaluate the initializer (if any), and return the
10187            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
10188            ExprKind::MyExpr { keyword, decls } => {
10189                // Build a temporary statement and dispatch to the canonical
10190                // statement handler so behavior matches `my $x = …;` exactly.
10191                let stmt_kind = match keyword.as_str() {
10192                    "my" => StmtKind::My(decls.clone()),
10193                    "our" => StmtKind::Our(decls.clone()),
10194                    "state" => StmtKind::State(decls.clone()),
10195                    "local" => StmtKind::Local(decls.clone()),
10196                    _ => StmtKind::My(decls.clone()),
10197                };
10198                let stmt = Statement {
10199                    label: None,
10200                    kind: stmt_kind,
10201                    line,
10202                };
10203                self.exec_statement(&stmt)?;
10204                // Return the value of the (first) declared variable so the
10205                // surrounding expression sees the assigned value, matching
10206                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
10207                let first = decls.first().ok_or_else(|| {
10208                    FlowOrError::Error(PerlError::runtime("MyExpr: empty decl list", line))
10209                })?;
10210                Ok(match first.sigil {
10211                    Sigil::Scalar => self.scope.get_scalar(&first.name),
10212                    Sigil::Array => PerlValue::array(self.scope.get_array(&first.name)),
10213                    Sigil::Hash => {
10214                        let h = self.scope.get_hash(&first.name);
10215                        let mut flat: Vec<PerlValue> = Vec::with_capacity(h.len() * 2);
10216                        for (k, v) in h {
10217                            flat.push(PerlValue::string(k));
10218                            flat.push(v);
10219                        }
10220                        PerlValue::array(flat)
10221                    }
10222                    Sigil::Typeglob => PerlValue::UNDEF,
10223                })
10224            }
10225
10226            // Function calls
10227            ExprKind::FuncCall { name, args } => {
10228                // Stryke builtins are unprefixed; `CORE::name` callers route back to the
10229                // bare-name dispatch so the matches below stay flat.
10230                let dispatch_name: &str = name.strip_prefix("CORE::").unwrap_or(name.as_str());
10231                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
10232                if matches!(dispatch_name, "read") && args.len() >= 3 {
10233                    let fh_val = self.eval_expr(&args[0])?;
10234                    let fh = fh_val
10235                        .as_io_handle_name()
10236                        .unwrap_or_else(|| fh_val.to_string());
10237                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
10238                    let offset = if args.len() > 3 {
10239                        self.eval_expr(&args[3])?.to_int().max(0) as usize
10240                    } else {
10241                        0
10242                    };
10243                    // Extract the variable name from the AST
10244                    let var_name = match &args[1].kind {
10245                        ExprKind::ScalarVar(n) => n.clone(),
10246                        _ => self.eval_expr(&args[1])?.to_string(),
10247                    };
10248                    let mut buf = vec![0u8; len];
10249                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
10250                        slot.lock().read(&mut buf).unwrap_or(0)
10251                    } else if fh == "STDIN" {
10252                        std::io::stdin().read(&mut buf).unwrap_or(0)
10253                    } else {
10254                        return Err(PerlError::runtime(
10255                            format!("read: unopened handle {}", fh),
10256                            line,
10257                        )
10258                        .into());
10259                    };
10260                    buf.truncate(n);
10261                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
10262                    if offset > 0 {
10263                        let mut existing = self.scope.get_scalar(&var_name).to_string();
10264                        while existing.len() < offset {
10265                            existing.push('\0');
10266                        }
10267                        existing.push_str(&read_str);
10268                        let _ = self
10269                            .scope
10270                            .set_scalar(&var_name, PerlValue::string(existing));
10271                    } else {
10272                        let _ = self
10273                            .scope
10274                            .set_scalar(&var_name, PerlValue::string(read_str));
10275                    }
10276                    return Ok(PerlValue::integer(n as i64));
10277                }
10278                if matches!(dispatch_name, "group_by" | "chunk_by") {
10279                    if args.len() != 2 {
10280                        return Err(PerlError::runtime(
10281                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
10282                            line,
10283                        )
10284                        .into());
10285                    }
10286                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
10287                }
10288                if matches!(dispatch_name, "puniq" | "pfirst" | "pany") {
10289                    let mut arg_vals = Vec::with_capacity(args.len());
10290                    for a in args {
10291                        arg_vals.push(self.eval_expr(a)?);
10292                    }
10293                    let saved_wa = self.wantarray_kind;
10294                    self.wantarray_kind = ctx;
10295                    let r = self.eval_par_list_call(dispatch_name, &arg_vals, ctx, line);
10296                    self.wantarray_kind = saved_wa;
10297                    return r.map_err(Into::into);
10298                }
10299                let arg_vals = if matches!(dispatch_name, "any" | "all" | "none" | "first")
10300                    || matches!(
10301                        dispatch_name,
10302                        "take_while"
10303                            | "drop_while"
10304                            | "skip_while"
10305                            | "reject"
10306                            | "grepv"
10307                            | "tap"
10308                            | "peek"
10309                    )
10310                    || matches!(
10311                        dispatch_name,
10312                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
10313                    ) {
10314                    if args.len() != 2 {
10315                        return Err(PerlError::runtime(
10316                            format!("{}: expected BLOCK, LIST", name),
10317                            line,
10318                        )
10319                        .into());
10320                    }
10321                    let cr = self.eval_expr(&args[0])?;
10322                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
10323                    let mut v = vec![cr];
10324                    v.extend(list_src.to_list());
10325                    v
10326                } else if matches!(
10327                    dispatch_name,
10328                    "zip"
10329                        | "zip_longest"
10330                        | "zip_shortest"
10331                        | "mesh"
10332                        | "mesh_longest"
10333                        | "mesh_shortest"
10334                ) {
10335                    let mut v = Vec::with_capacity(args.len());
10336                    for a in args {
10337                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
10338                    }
10339                    v
10340                } else if matches!(
10341                    dispatch_name,
10342                    "uniq"
10343                        | "distinct"
10344                        | "uniqstr"
10345                        | "uniqint"
10346                        | "uniqnum"
10347                        | "flatten"
10348                        | "set"
10349                        | "list_count"
10350                        | "list_size"
10351                        | "count"
10352                        | "size"
10353                        | "cnt"
10354                        | "with_index"
10355                        | "shuffle"
10356                        | "sum"
10357                        | "sum0"
10358                        | "product"
10359                        | "min"
10360                        | "max"
10361                        | "minstr"
10362                        | "maxstr"
10363                        | "mean"
10364                        | "median"
10365                        | "mode"
10366                        | "stddev"
10367                        | "variance"
10368                        | "pairs"
10369                        | "unpairs"
10370                        | "pairkeys"
10371                        | "pairvalues"
10372                ) {
10373                    // Slurpy list `(@)`: one list expr (`uniq @x`) or multiple actuals
10374                    // (`uniq(1, 1, 2)`). Each actual is evaluated in list context so
10375                    // `@a, @b` flattens.
10376                    let mut list_out = Vec::new();
10377                    if args.len() == 1 {
10378                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
10379                    } else {
10380                        for a in args {
10381                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
10382                        }
10383                    }
10384                    list_out
10385                } else if matches!(dispatch_name, "take" | "head" | "tail" | "drop") {
10386                    if args.is_empty() {
10387                        return Err(PerlError::runtime(
10388                            "take/head/tail/drop: need LIST..., N or unary N",
10389                            line,
10390                        )
10391                        .into());
10392                    }
10393                    let mut arg_vals = Vec::with_capacity(args.len());
10394                    if args.len() == 1 {
10395                        // head @l == head @l, 1 — evaluate in list context
10396                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10397                    } else {
10398                        for a in &args[..args.len() - 1] {
10399                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
10400                        }
10401                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
10402                    }
10403                    arg_vals
10404                } else if matches!(dispatch_name, "chunked" | "windowed") {
10405                    let mut list_out = Vec::new();
10406                    match args.len() {
10407                        0 => {
10408                            return Err(PerlError::runtime(
10409                                format!("{name}: expected (LIST, N) or unary N after |>"),
10410                                line,
10411                            )
10412                            .into());
10413                        }
10414                        1 => {
10415                            // chunked @l / windowed @l — evaluate in list context, default size
10416                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
10417                        }
10418                        2 => {
10419                            list_out.extend(
10420                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
10421                            );
10422                            list_out.push(self.eval_expr(&args[1])?);
10423                        }
10424                        _ => {
10425                            return Err(PerlError::runtime(
10426                                format!(
10427                                    "{name}: expected exactly (LIST, N); use one list expression then size"
10428                                ),
10429                                line,
10430                            )
10431                            .into());
10432                        }
10433                    }
10434                    list_out
10435                } else {
10436                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
10437                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
10438                    let mut arg_vals = Vec::with_capacity(args.len());
10439                    for a in args {
10440                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
10441                        if let Some(items) = v.as_array_vec() {
10442                            arg_vals.extend(items);
10443                        } else {
10444                            arg_vals.push(v);
10445                        }
10446                    }
10447                    arg_vals
10448                };
10449                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
10450                let saved_wa = self.wantarray_kind;
10451                self.wantarray_kind = ctx;
10452                // Builtins first — immune to monkey-patching (matches VM dispatch order).
10453                // In compat mode, user subs shadow builtins (Perl 5 semantics).
10454                if !crate::compat_mode() {
10455                    if matches!(
10456                        dispatch_name,
10457                        "take_while"
10458                            | "drop_while"
10459                            | "skip_while"
10460                            | "reject"
10461                            | "grepv"
10462                            | "tap"
10463                            | "peek"
10464                    ) {
10465                        let r =
10466                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
10467                        self.wantarray_kind = saved_wa;
10468                        return r.map_err(Into::into);
10469                    }
10470                    if let Some(r) =
10471                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
10472                    {
10473                        self.wantarray_kind = saved_wa;
10474                        return r.map_err(Into::into);
10475                    }
10476                }
10477                if let Some(sub) = self.resolve_sub_by_name(name) {
10478                    self.wantarray_kind = saved_wa;
10479                    let args = self.with_topic_default_args(arg_vals);
10480                    let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
10481                    return self.call_sub_with_package(&sub, args, ctx, line, pkg);
10482                }
10483                // Compat mode: check builtins after user subs (Perl 5 semantics).
10484                if crate::compat_mode() {
10485                    if matches!(
10486                        dispatch_name,
10487                        "take_while"
10488                            | "drop_while"
10489                            | "skip_while"
10490                            | "reject"
10491                            | "grepv"
10492                            | "tap"
10493                            | "peek"
10494                    ) {
10495                        let r =
10496                            self.list_higher_order_block_builtin(dispatch_name, &arg_vals, line);
10497                        self.wantarray_kind = saved_wa;
10498                        return r.map_err(Into::into);
10499                    }
10500                    if let Some(r) =
10501                        crate::builtins::try_builtin(self, dispatch_name, &arg_vals, line)
10502                    {
10503                        self.wantarray_kind = saved_wa;
10504                        return r.map_err(Into::into);
10505                    }
10506                }
10507                self.wantarray_kind = saved_wa;
10508                self.call_named_sub(name, arg_vals, line, ctx)
10509            }
10510            ExprKind::IndirectCall {
10511                target,
10512                args,
10513                ampersand: _,
10514                pass_caller_arglist,
10515            } => {
10516                let tval = self.eval_expr(target)?;
10517                let arg_vals = if *pass_caller_arglist {
10518                    self.scope.get_array("_")
10519                } else {
10520                    let mut v = Vec::with_capacity(args.len());
10521                    for a in args {
10522                        v.push(self.eval_expr(a)?);
10523                    }
10524                    v
10525                };
10526                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
10527            }
10528            ExprKind::MethodCall {
10529                object,
10530                method,
10531                args,
10532                super_call,
10533            } => {
10534                let obj = self.eval_expr(object)?;
10535                let mut arg_vals = vec![obj.clone()];
10536                for a in args {
10537                    arg_vals.push(self.eval_expr(a)?);
10538                }
10539                if let Some(r) =
10540                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
10541                {
10542                    return r.map_err(Into::into);
10543                }
10544                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
10545                    return r.map_err(Into::into);
10546                }
10547                // Get class name
10548                let class = if let Some(b) = obj.as_blessed_ref() {
10549                    b.class.clone()
10550                } else if let Some(s) = obj.as_str() {
10551                    s // Class->method()
10552                } else {
10553                    return Err(PerlError::runtime("Can't call method on non-object", line).into());
10554                };
10555                if method == "VERSION" && !*super_call {
10556                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
10557                        return Ok(ver);
10558                    }
10559                }
10560                // UNIVERSAL methods: isa, can, DOES
10561                if !*super_call {
10562                    match method.as_str() {
10563                        "isa" => {
10564                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
10565                            let mro = self.mro_linearize(&class);
10566                            let result = mro.iter().any(|c| c == &target);
10567                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
10568                        }
10569                        "can" => {
10570                            let target_method =
10571                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
10572                            let found = self
10573                                .resolve_method_full_name(&class, &target_method, false)
10574                                .and_then(|fq| self.subs.get(&fq))
10575                                .is_some();
10576                            if found {
10577                                return Ok(PerlValue::code_ref(Arc::new(PerlSub {
10578                                    name: target_method,
10579                                    params: vec![],
10580                                    body: vec![],
10581                                    closure_env: None,
10582                                    prototype: None,
10583                                    fib_like: None,
10584                                })));
10585                            } else {
10586                                return Ok(PerlValue::UNDEF);
10587                            }
10588                        }
10589                        "DOES" => {
10590                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
10591                            let mro = self.mro_linearize(&class);
10592                            let result = mro.iter().any(|c| c == &target);
10593                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
10594                        }
10595                        _ => {}
10596                    }
10597                }
10598                let full_name = self
10599                    .resolve_method_full_name(&class, method, *super_call)
10600                    .ok_or_else(|| {
10601                        PerlError::runtime(
10602                            format!(
10603                                "Can't locate method \"{}\" for invocant \"{}\"",
10604                                method, class
10605                            ),
10606                            line,
10607                        )
10608                    })?;
10609                if let Some(sub) = self.subs.get(&full_name).cloned() {
10610                    self.call_sub(&sub, arg_vals, ctx, line)
10611                } else if method == "new" && !*super_call {
10612                    // Default constructor
10613                    self.builtin_new(&class, arg_vals, line)
10614                } else if let Some(r) =
10615                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
10616                {
10617                    r
10618                } else {
10619                    Err(PerlError::runtime(
10620                        format!(
10621                            "Can't locate method \"{}\" in package \"{}\"",
10622                            method, class
10623                        ),
10624                        line,
10625                    )
10626                    .into())
10627                }
10628            }
10629
10630            // Print/Say/Printf
10631            ExprKind::Print { handle, args } => {
10632                self.exec_print(handle.as_deref(), args, false, line)
10633            }
10634            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
10635            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
10636            ExprKind::Die(args) => {
10637                if args.is_empty() {
10638                    // `die` with no args: re-die with current $@ or "Died"
10639                    let current = self.scope.get_scalar("@");
10640                    let msg = if current.is_undef() || current.to_string().is_empty() {
10641                        let mut m = "Died".to_string();
10642                        m.push_str(&self.die_warn_at_suffix(line));
10643                        m.push('\n');
10644                        m
10645                    } else {
10646                        current.to_string()
10647                    };
10648                    self.fire_pseudosig_die(&msg, line)?;
10649                    return Err(PerlError::die(msg, line).into());
10650                }
10651                // Single ref argument: store the ref value in $@
10652                if args.len() == 1 {
10653                    let v = self.eval_expr(&args[0])?;
10654                    if v.as_hash_ref().is_some()
10655                        || v.as_blessed_ref().is_some()
10656                        || v.as_array_ref().is_some()
10657                        || v.as_code_ref().is_some()
10658                    {
10659                        let msg = v.to_string();
10660                        self.fire_pseudosig_die(&msg, line)?;
10661                        return Err(PerlError::die_with_value(v, msg, line).into());
10662                    }
10663                }
10664                let mut msg = String::new();
10665                for a in args {
10666                    let v = self.eval_expr(a)?;
10667                    msg.push_str(&v.to_string());
10668                }
10669                if msg.is_empty() {
10670                    msg = "Died".to_string();
10671                }
10672                if !msg.ends_with('\n') {
10673                    msg.push_str(&self.die_warn_at_suffix(line));
10674                    msg.push('\n');
10675                }
10676                self.fire_pseudosig_die(&msg, line)?;
10677                Err(PerlError::die(msg, line).into())
10678            }
10679            ExprKind::Warn(args) => {
10680                let mut msg = String::new();
10681                for a in args {
10682                    let v = self.eval_expr(a)?;
10683                    msg.push_str(&v.to_string());
10684                }
10685                if msg.is_empty() {
10686                    msg = "Warning: something's wrong".to_string();
10687                }
10688                if !msg.ends_with('\n') {
10689                    msg.push_str(&self.die_warn_at_suffix(line));
10690                    msg.push('\n');
10691                }
10692                self.fire_pseudosig_warn(&msg, line)?;
10693                Ok(PerlValue::integer(1))
10694            }
10695
10696            // Regex
10697            ExprKind::Match {
10698                expr,
10699                pattern,
10700                flags,
10701                scalar_g,
10702                delim: _,
10703            } => {
10704                let val = self.eval_expr(expr)?;
10705                if val.is_iterator() {
10706                    let source = crate::map_stream::into_pull_iter(val);
10707                    let re = self.compile_regex(pattern, flags, line)?;
10708                    let global = flags.contains('g');
10709                    if global {
10710                        return Ok(PerlValue::iterator(std::sync::Arc::new(
10711                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
10712                        )));
10713                    } else {
10714                        return Ok(PerlValue::iterator(std::sync::Arc::new(
10715                            crate::map_stream::MatchStreamIterator::new(source, re),
10716                        )));
10717                    }
10718                }
10719                let s = val.to_string();
10720                let pos_key = match &expr.kind {
10721                    ExprKind::ScalarVar(n) => n.as_str(),
10722                    _ => "_",
10723                };
10724                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
10725            }
10726            ExprKind::Substitution {
10727                expr,
10728                pattern,
10729                replacement,
10730                flags,
10731                delim: _,
10732            } => {
10733                let val = self.eval_expr(expr)?;
10734                if val.is_iterator() {
10735                    let source = crate::map_stream::into_pull_iter(val);
10736                    let re = self.compile_regex(pattern, flags, line)?;
10737                    let global = flags.contains('g');
10738                    return Ok(PerlValue::iterator(std::sync::Arc::new(
10739                        crate::map_stream::SubstStreamIterator::new(
10740                            source,
10741                            re,
10742                            normalize_replacement_backrefs(replacement),
10743                            global,
10744                        ),
10745                    )));
10746                }
10747                let s = val.to_string();
10748                self.regex_subst_execute(
10749                    s,
10750                    pattern,
10751                    replacement.as_str(),
10752                    flags.as_str(),
10753                    expr,
10754                    line,
10755                )
10756            }
10757            ExprKind::Transliterate {
10758                expr,
10759                from,
10760                to,
10761                flags,
10762                delim: _,
10763            } => {
10764                let val = self.eval_expr(expr)?;
10765                if val.is_iterator() {
10766                    let source = crate::map_stream::into_pull_iter(val);
10767                    return Ok(PerlValue::iterator(std::sync::Arc::new(
10768                        crate::map_stream::TransliterateStreamIterator::new(
10769                            source, from, to, flags,
10770                        ),
10771                    )));
10772                }
10773                let s = val.to_string();
10774                self.regex_transliterate_execute(
10775                    s,
10776                    from.as_str(),
10777                    to.as_str(),
10778                    flags.as_str(),
10779                    expr,
10780                    line,
10781                )
10782            }
10783
10784            // List operations
10785            ExprKind::MapExpr {
10786                block,
10787                list,
10788                flatten_array_refs,
10789                stream,
10790            } => {
10791                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10792                if *stream {
10793                    let out =
10794                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
10795                    if ctx == WantarrayCtx::List {
10796                        return Ok(out);
10797                    }
10798                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10799                }
10800                let items = list_val.to_list();
10801                if items.len() == 1 {
10802                    if let Some(p) = items[0].as_pipeline() {
10803                        if *flatten_array_refs {
10804                            return Err(PerlError::runtime(
10805                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
10806                                line,
10807                            )
10808                            .into());
10809                        }
10810                        let sub = self.anon_coderef_from_block(block);
10811                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
10812                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
10813                    }
10814                }
10815                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
10816                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
10817                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
10818                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
10819                let mut result = Vec::new();
10820                for item in items {
10821                    self.scope.set_topic(item);
10822                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
10823                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
10824                }
10825                if ctx == WantarrayCtx::List {
10826                    Ok(PerlValue::array(result))
10827                } else {
10828                    Ok(PerlValue::integer(result.len() as i64))
10829                }
10830            }
10831            ExprKind::ForEachExpr { block, list } => {
10832                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10833                // Lazy: consume iterator one-at-a-time without materializing.
10834                if list_val.is_iterator() {
10835                    let iter = list_val.into_iterator();
10836                    let mut count = 0i64;
10837                    while let Some(item) = iter.next_item() {
10838                        count += 1;
10839                        self.scope.set_topic(item);
10840                        self.exec_block(block)?;
10841                    }
10842                    return Ok(PerlValue::integer(count));
10843                }
10844                let items = list_val.to_list();
10845                let count = items.len();
10846                for item in items {
10847                    self.scope.set_topic(item);
10848                    self.exec_block(block)?;
10849                }
10850                Ok(PerlValue::integer(count as i64))
10851            }
10852            ExprKind::MapExprComma {
10853                expr,
10854                list,
10855                flatten_array_refs,
10856                stream,
10857            } => {
10858                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10859                if *stream {
10860                    let out =
10861                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
10862                    if ctx == WantarrayCtx::List {
10863                        return Ok(out);
10864                    }
10865                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10866                }
10867                let items = list_val.to_list();
10868                let mut result = Vec::new();
10869                for item in items {
10870                    // EXPR-form: no `{}` block boundary, so don't shift the
10871                    // topic chain or zero slot 1+. Just rebind `$_` / `$_0`.
10872                    // This makes `map _1, @$_` read the surrounding fn's
10873                    // second arg per iter; block-form `map { ... }` still
10874                    // gets a full `set_topic` via its CodeRef call.
10875                    self.scope.set_topic_local(item.clone());
10876                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10877                    // Coderef-in-block-position: `map $f, @l` calls `$f($_)`
10878                    // when `$f` is a code reference. Skipped under `--compat`
10879                    // (Perl semantics: re-evaluate expr per iteration as value).
10880                    let val = if !crate::compat_mode() {
10881                        if let Some(sub) = val.as_code_ref() {
10882                            let sub = sub.clone();
10883                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::List, line)?
10884                        } else {
10885                            val
10886                        }
10887                    } else {
10888                        val
10889                    };
10890                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
10891                }
10892                if ctx == WantarrayCtx::List {
10893                    Ok(PerlValue::array(result))
10894                } else {
10895                    Ok(PerlValue::integer(result.len() as i64))
10896                }
10897            }
10898            ExprKind::GrepExpr {
10899                block,
10900                list,
10901                keyword,
10902            } => {
10903                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10904                if keyword.is_stream() {
10905                    let out = self.filter_stream_block_output(list_val, block, line)?;
10906                    if ctx == WantarrayCtx::List {
10907                        return Ok(out);
10908                    }
10909                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10910                }
10911                let items = list_val.to_list();
10912                if items.len() == 1 {
10913                    if let Some(p) = items[0].as_pipeline() {
10914                        let sub = self.anon_coderef_from_block(block);
10915                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
10916                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
10917                    }
10918                }
10919                let mut result = Vec::new();
10920                for item in items {
10921                    self.scope.set_topic(item.clone());
10922                    let val = self.exec_block(block)?;
10923                    // Bare regex in block → match against $_ (Perl: /pat/ in
10924                    // grep is `$_ =~ /pat/`, not a truthy regex object).
10925                    let keep = if let Some(re) = val.as_regex() {
10926                        re.is_match(&item.to_string())
10927                    } else {
10928                        val.is_true()
10929                    };
10930                    if keep {
10931                        result.push(item);
10932                    }
10933                }
10934                if ctx == WantarrayCtx::List {
10935                    Ok(PerlValue::array(result))
10936                } else {
10937                    Ok(PerlValue::integer(result.len() as i64))
10938                }
10939            }
10940            ExprKind::GrepExprComma {
10941                expr,
10942                list,
10943                keyword,
10944            } => {
10945                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10946                if keyword.is_stream() {
10947                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
10948                    if ctx == WantarrayCtx::List {
10949                        return Ok(out);
10950                    }
10951                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10952                }
10953                let items = list_val.to_list();
10954                let mut result = Vec::new();
10955                for item in items {
10956                    // EXPR-form: see comment in MapExprComma above. No block
10957                    // boundary, so no chain shift; `_1` reads the surrounding
10958                    // fn's second arg per iter rather than getting nuked.
10959                    self.scope.set_topic_local(item.clone());
10960                    let val = self.eval_expr(expr)?;
10961                    // Coderef-in-block-position: `grep $f, @l` calls `$f($_)`
10962                    // when `$f` is a code reference, then filters by truthiness
10963                    // of the call result. Skipped under `--compat`.
10964                    let val = if !crate::compat_mode() {
10965                        if let Some(sub) = val.as_code_ref() {
10966                            let sub = sub.clone();
10967                            self.call_sub(&sub, vec![item.clone()], WantarrayCtx::Scalar, line)?
10968                        } else {
10969                            val
10970                        }
10971                    } else {
10972                        val
10973                    };
10974                    let keep = if let Some(re) = val.as_regex() {
10975                        re.is_match(&item.to_string())
10976                    } else {
10977                        val.is_true()
10978                    };
10979                    if keep {
10980                        result.push(item);
10981                    }
10982                }
10983                if ctx == WantarrayCtx::List {
10984                    Ok(PerlValue::array(result))
10985                } else {
10986                    Ok(PerlValue::integer(result.len() as i64))
10987                }
10988            }
10989            ExprKind::SortExpr { cmp, list } => {
10990                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10991                let mut items = list_val.to_list();
10992                match cmp {
10993                    Some(SortComparator::Code(code_expr)) => {
10994                        let sub = self.eval_expr(code_expr)?;
10995                        let Some(sub) = sub.as_code_ref() else {
10996                            return Err(PerlError::runtime(
10997                                "sort: comparator must be a code reference",
10998                                line,
10999                            )
11000                            .into());
11001                        };
11002                        let sub = sub.clone();
11003                        items.sort_by(|a, b| {
11004                            // `set_sort_pair` keeps Perl-style `$a`/`$b` package-
11005                            // global access for `sub cmp { $a <=> $b }`. Passing
11006                            // `(a, b)` as positional args lets stryke lambdas
11007                            // `fn ($a, $b) { $b <=> $a }` receive them via @_.
11008                            self.scope.set_sort_pair(a.clone(), b.clone());
11009                            match self.call_sub(&sub, vec![a.clone(), b.clone()], ctx, line) {
11010                                Ok(v) => {
11011                                    let n = v.to_int();
11012                                    if n < 0 {
11013                                        Ordering::Less
11014                                    } else if n > 0 {
11015                                        Ordering::Greater
11016                                    } else {
11017                                        Ordering::Equal
11018                                    }
11019                                }
11020                                Err(_) => Ordering::Equal,
11021                            }
11022                        });
11023                    }
11024                    Some(SortComparator::Block(cmp_block)) => {
11025                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
11026                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
11027                        } else {
11028                            let cmp_block = cmp_block.clone();
11029                            items.sort_by(|a, b| {
11030                                self.scope.set_sort_pair(a.clone(), b.clone());
11031                                match self.exec_block(&cmp_block) {
11032                                    Ok(v) => {
11033                                        let n = v.to_int();
11034                                        if n < 0 {
11035                                            Ordering::Less
11036                                        } else if n > 0 {
11037                                            Ordering::Greater
11038                                        } else {
11039                                            Ordering::Equal
11040                                        }
11041                                    }
11042                                    Err(_) => Ordering::Equal,
11043                                }
11044                            });
11045                        }
11046                    }
11047                    None => {
11048                        items.sort_by_key(|a| a.to_string());
11049                    }
11050                }
11051                Ok(PerlValue::array(items))
11052            }
11053            ExprKind::Rev(expr) => {
11054                // Eval in scalar context first to preserve set/hash/array ref types
11055                let val = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
11056                if val.is_iterator() {
11057                    return Ok(PerlValue::iterator(Arc::new(
11058                        crate::value::RevIterator::new(val.into_iterator()),
11059                    )));
11060                }
11061                if let Some(s) = crate::value::set_payload(&val) {
11062                    let mut out = crate::value::PerlSet::new();
11063                    for (k, v) in s.iter().rev() {
11064                        out.insert(k.clone(), v.clone());
11065                    }
11066                    return Ok(PerlValue::set(Arc::new(out)));
11067                }
11068                if let Some(ar) = val.as_array_ref() {
11069                    let items: Vec<_> = ar.read().iter().rev().cloned().collect();
11070                    return Ok(PerlValue::array_ref(Arc::new(parking_lot::RwLock::new(
11071                        items,
11072                    ))));
11073                }
11074                if let Some(hr) = val.as_hash_ref() {
11075                    let mut out: indexmap::IndexMap<String, PerlValue> = indexmap::IndexMap::new();
11076                    for (k, v) in hr.read().iter() {
11077                        out.insert(v.to_string(), PerlValue::string(k.clone()));
11078                    }
11079                    return Ok(PerlValue::hash_ref(Arc::new(parking_lot::RwLock::new(out))));
11080                }
11081                // Re-eval in list context for bare arrays/hashes
11082                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11083                if let Some(hm) = val.as_hash_map() {
11084                    let mut out: indexmap::IndexMap<String, PerlValue> = indexmap::IndexMap::new();
11085                    for (k, v) in hm.iter() {
11086                        out.insert(v.to_string(), PerlValue::string(k.clone()));
11087                    }
11088                    return Ok(PerlValue::hash(out));
11089                }
11090                if val.as_array_vec().is_some() {
11091                    let mut items = val.to_list();
11092                    items.reverse();
11093                    Ok(PerlValue::array(items))
11094                } else {
11095                    let items = val.to_list();
11096                    if items.len() > 1 {
11097                        let mut items = items;
11098                        items.reverse();
11099                        Ok(PerlValue::array(items))
11100                    } else {
11101                        let s = val.to_string();
11102                        Ok(PerlValue::string(s.chars().rev().collect()))
11103                    }
11104                }
11105            }
11106            ExprKind::ReverseExpr(list) => {
11107                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11108                match ctx {
11109                    WantarrayCtx::List => {
11110                        let mut items = val.to_list();
11111                        items.reverse();
11112                        Ok(PerlValue::array(items))
11113                    }
11114                    _ => {
11115                        let items = val.to_list();
11116                        let s: String = items.iter().map(|v| v.to_string()).collect();
11117                        Ok(PerlValue::string(s.chars().rev().collect()))
11118                    }
11119                }
11120            }
11121
11122            // ── Parallel operations (rayon-powered) ──
11123            ExprKind::ParLinesExpr {
11124                path,
11125                callback,
11126                progress,
11127            } => self.eval_par_lines_expr(
11128                path.as_ref(),
11129                callback.as_ref(),
11130                progress.as_deref(),
11131                line,
11132            ),
11133            ExprKind::ParWalkExpr {
11134                path,
11135                callback,
11136                progress,
11137            } => {
11138                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
11139            }
11140            ExprKind::PwatchExpr { path, callback } => {
11141                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
11142            }
11143            ExprKind::PMapExpr {
11144                block,
11145                list,
11146                progress,
11147                flat_outputs,
11148                on_cluster,
11149                stream,
11150            } => {
11151                let show_progress = progress
11152                    .as_ref()
11153                    .map(|p| self.eval_expr(p))
11154                    .transpose()?
11155                    .map(|v| v.is_true())
11156                    .unwrap_or(false);
11157                let list_val = self.eval_expr(list)?;
11158                if let Some(cluster_e) = on_cluster {
11159                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
11160                    return self.eval_pmap_remote(
11161                        cluster_val,
11162                        list_val,
11163                        show_progress,
11164                        block,
11165                        *flat_outputs,
11166                        line,
11167                    );
11168                }
11169                if *stream {
11170                    let source = crate::map_stream::into_pull_iter(list_val);
11171                    let sub = self.anon_coderef_from_block(block);
11172                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
11173                    return Ok(PerlValue::iterator(Arc::new(
11174                        crate::map_stream::PMapStreamIterator::new(
11175                            source,
11176                            sub,
11177                            self.subs.clone(),
11178                            capture,
11179                            atomic_arrays,
11180                            atomic_hashes,
11181                            *flat_outputs,
11182                        ),
11183                    )));
11184                }
11185                let items = list_val.to_list();
11186                let block = block.clone();
11187                let subs = self.subs.clone();
11188                let (scope_capture, atomic_arrays, atomic_hashes) =
11189                    self.scope.capture_with_atomics();
11190                let pmap_progress = PmapProgress::new(show_progress, items.len());
11191
11192                if *flat_outputs {
11193                    let mut indexed: Vec<(usize, Vec<PerlValue>)> = items
11194                        .into_par_iter()
11195                        .enumerate()
11196                        .map(|(i, item)| {
11197                            let mut local_interp = VMHelper::new();
11198                            local_interp.subs = subs.clone();
11199                            local_interp.scope.restore_capture(&scope_capture);
11200                            local_interp
11201                                .scope
11202                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11203                            local_interp.enable_parallel_guard();
11204                            local_interp.scope.set_topic(item);
11205                            let val = match local_interp.exec_block(&block) {
11206                                Ok(val) => val,
11207                                Err(_) => PerlValue::UNDEF,
11208                            };
11209                            let chunk = val.map_flatten_outputs(true);
11210                            pmap_progress.tick();
11211                            (i, chunk)
11212                        })
11213                        .collect();
11214                    pmap_progress.finish();
11215                    indexed.sort_by_key(|(i, _)| *i);
11216                    let results: Vec<PerlValue> =
11217                        indexed.into_iter().flat_map(|(_, v)| v).collect();
11218                    Ok(PerlValue::array(results))
11219                } else {
11220                    let results: Vec<PerlValue> = items
11221                        .into_par_iter()
11222                        .map(|item| {
11223                            let mut local_interp = VMHelper::new();
11224                            local_interp.subs = subs.clone();
11225                            local_interp.scope.restore_capture(&scope_capture);
11226                            local_interp
11227                                .scope
11228                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11229                            local_interp.enable_parallel_guard();
11230                            local_interp.scope.set_topic(item);
11231                            let val = match local_interp.exec_block(&block) {
11232                                Ok(val) => val,
11233                                Err(_) => PerlValue::UNDEF,
11234                            };
11235                            pmap_progress.tick();
11236                            val
11237                        })
11238                        .collect();
11239                    pmap_progress.finish();
11240                    Ok(PerlValue::array(results))
11241                }
11242            }
11243            ExprKind::PMapChunkedExpr {
11244                chunk_size,
11245                block,
11246                list,
11247                progress,
11248            } => {
11249                let show_progress = progress
11250                    .as_ref()
11251                    .map(|p| self.eval_expr(p))
11252                    .transpose()?
11253                    .map(|v| v.is_true())
11254                    .unwrap_or(false);
11255                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
11256                let list_val = self.eval_expr(list)?;
11257                let items = list_val.to_list();
11258                let block = block.clone();
11259                let subs = self.subs.clone();
11260                let (scope_capture, atomic_arrays, atomic_hashes) =
11261                    self.scope.capture_with_atomics();
11262
11263                let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = items
11264                    .chunks(chunk_n)
11265                    .enumerate()
11266                    .map(|(i, c)| (i, c.to_vec()))
11267                    .collect();
11268
11269                let n_chunks = indexed_chunks.len();
11270                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
11271
11272                let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
11273                    .into_par_iter()
11274                    .map(|(chunk_idx, chunk)| {
11275                        let mut local_interp = VMHelper::new();
11276                        local_interp.subs = subs.clone();
11277                        local_interp.scope.restore_capture(&scope_capture);
11278                        local_interp
11279                            .scope
11280                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11281                        local_interp.enable_parallel_guard();
11282                        let mut out = Vec::with_capacity(chunk.len());
11283                        for item in chunk {
11284                            local_interp.scope.set_topic(item);
11285                            match local_interp.exec_block(&block) {
11286                                Ok(val) => out.push(val),
11287                                Err(_) => out.push(PerlValue::UNDEF),
11288                            }
11289                        }
11290                        pmap_progress.tick();
11291                        (chunk_idx, out)
11292                    })
11293                    .collect();
11294
11295                pmap_progress.finish();
11296                chunk_results.sort_by_key(|(i, _)| *i);
11297                let results: Vec<PerlValue> =
11298                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
11299                Ok(PerlValue::array(results))
11300            }
11301            ExprKind::PGrepExpr {
11302                block,
11303                list,
11304                progress,
11305                stream,
11306            } => {
11307                let show_progress = progress
11308                    .as_ref()
11309                    .map(|p| self.eval_expr(p))
11310                    .transpose()?
11311                    .map(|v| v.is_true())
11312                    .unwrap_or(false);
11313                let list_val = self.eval_expr(list)?;
11314                if *stream {
11315                    let source = crate::map_stream::into_pull_iter(list_val);
11316                    let sub = self.anon_coderef_from_block(block);
11317                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
11318                    return Ok(PerlValue::iterator(Arc::new(
11319                        crate::map_stream::PGrepStreamIterator::new(
11320                            source,
11321                            sub,
11322                            self.subs.clone(),
11323                            capture,
11324                            atomic_arrays,
11325                            atomic_hashes,
11326                        ),
11327                    )));
11328                }
11329                let items = list_val.to_list();
11330                let block = block.clone();
11331                let subs = self.subs.clone();
11332                let (scope_capture, atomic_arrays, atomic_hashes) =
11333                    self.scope.capture_with_atomics();
11334                let pmap_progress = PmapProgress::new(show_progress, items.len());
11335
11336                let results: Vec<PerlValue> = items
11337                    .into_par_iter()
11338                    .filter_map(|item| {
11339                        let mut local_interp = VMHelper::new();
11340                        local_interp.subs = subs.clone();
11341                        local_interp.scope.restore_capture(&scope_capture);
11342                        local_interp
11343                            .scope
11344                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11345                        local_interp.enable_parallel_guard();
11346                        local_interp.scope.set_topic(item.clone());
11347                        let keep = match local_interp.exec_block(&block) {
11348                            Ok(val) => val.is_true(),
11349                            Err(_) => false,
11350                        };
11351                        pmap_progress.tick();
11352                        if keep {
11353                            Some(item)
11354                        } else {
11355                            None
11356                        }
11357                    })
11358                    .collect();
11359                pmap_progress.finish();
11360                Ok(PerlValue::array(results))
11361            }
11362            ExprKind::ParExpr { block, list } => {
11363                // Generic parallel-chunk wrapper: split input on a sensible
11364                // boundary (UTF-8 char-aligned for strings, element-aligned
11365                // for arrays), evaluate the block per chunk in parallel
11366                // with `$_` bound to the chunk, then concatenate results.
11367                //
11368                // Chunk count is capped at min(n_threads, 8) because each
11369                // chunk pays a fixed `VMHelper::new()` setup cost (env-var
11370                // parsing, PATH/FPATH split, term-info ioctl, IndexMap
11371                // declarations). On 18-core machines, splitting 18 ways
11372                // makes setup overhead dominate the actual work.
11373                let list_val = self.eval_expr(list)?;
11374                let n_threads = rayon::current_num_threads().clamp(1, 8);
11375                let chunks = par_chunk_value(&list_val, n_threads);
11376                if chunks.len() < 2 {
11377                    // Below break-even (small input or unsupported value type):
11378                    // run the block once with the original input as `$_`.
11379                    self.scope.set_topic(list_val);
11380                    let v = self.exec_block(block)?;
11381                    return Ok(v);
11382                }
11383                let block_clone = block.clone();
11384                let subs = self.subs.clone();
11385                let (scope_capture, atomic_arrays, atomic_hashes) =
11386                    self.scope.capture_with_atomics();
11387                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
11388                let err_w = Arc::clone(&first_err);
11389                let per_chunk: Vec<Vec<PerlValue>> = chunks
11390                    .into_par_iter()
11391                    .map(|chunk| {
11392                        if err_w.lock().is_some() {
11393                            return Vec::new();
11394                        }
11395                        let mut local_interp = VMHelper::new();
11396                        local_interp.subs = subs.clone();
11397                        local_interp.scope.restore_capture(&scope_capture);
11398                        local_interp
11399                            .scope
11400                            .restore_atomics(&atomic_arrays, &atomic_hashes);
11401                        local_interp.enable_parallel_guard();
11402                        local_interp.scope.set_topic(chunk);
11403                        match local_interp.exec_block(&block_clone) {
11404                            Ok(v) => v.map_flatten_outputs(true),
11405                            Err(e) => {
11406                                let mut g = err_w.lock();
11407                                if g.is_none() {
11408                                    *g = Some(format!("par: {:?}", e));
11409                                }
11410                                Vec::new()
11411                            }
11412                        }
11413                    })
11414                    .collect();
11415                if let Some(msg) = first_err.lock().take() {
11416                    return Err(FlowOrError::Error(PerlError::runtime(msg, line)));
11417                }
11418                let total: usize = per_chunk.iter().map(|v| v.len()).sum();
11419                let mut out = Vec::with_capacity(total);
11420                for v in per_chunk {
11421                    out.extend(v);
11422                }
11423                Ok(PerlValue::array(out))
11424            }
11425            ExprKind::ParReduceExpr {
11426                extract_block,
11427                reduce_block,
11428                list,
11429            } => {
11430                // Chunk INPUT, run extract per chunk in parallel, then
11431                // reduce pairwise across chunks. With an explicit reduce
11432                // block the user controls merging via `$a` / `$b`. Without
11433                // one, the merger is auto-picked based on the first
11434                // chunk's result type:
11435                //   - hash<num>  → key-wise add (canonical histogram merge)
11436                //   - number     → numeric `+`
11437                //   - array/list → concat
11438                //   - string     → concat
11439                let list_val = self.eval_expr(list)?;
11440                let n_threads = rayon::current_num_threads().clamp(1, 8);
11441                let chunks = par_chunk_value(&list_val, n_threads);
11442                if chunks.len() < 2 {
11443                    // Single-chunk fallback: bind input as both $_ and $_[0]
11444                    // so blocks built via the pipe_rhs_wrap path (which uses
11445                    // `$_[0]`) work the same as topic-style blocks.
11446                    self.scope.declare_array("_", vec![list_val.clone()]);
11447                    self.scope.set_topic(list_val);
11448                    return self.exec_block(extract_block);
11449                }
11450                let extract = extract_block.clone();
11451                let subs = self.subs.clone();
11452                let (scope_capture, atomic_arrays, atomic_hashes) =
11453                    self.scope.capture_with_atomics();
11454                let first_err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
11455                let err_w = Arc::clone(&first_err);
11456                let per_chunk: Vec<PerlValue> = chunks
11457                    .into_par_iter()
11458                    .map(|chunk| {
11459                        if err_w.lock().is_some() {
11460                            return PerlValue::UNDEF;
11461                        }
11462                        let mut local = VMHelper::new();
11463                        local.subs = subs.clone();
11464                        local.scope.restore_capture(&scope_capture);
11465                        local.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
11466                        local.enable_parallel_guard();
11467                        // Bind chunk as both `$_` (topic) and `$_[0]`.
11468                        // The topic form is used by `par_reduce { letters
11469                        // |> freq }` style blocks; the `$_[0]` form is
11470                        // used when `~p>` lowers to `par_reduce` via the
11471                        // pipe_rhs_wrap path.
11472                        local.scope.declare_array("_", vec![chunk.clone()]);
11473                        local.scope.set_topic(chunk);
11474                        match local.exec_block(&extract) {
11475                            Ok(v) => v,
11476                            Err(e) => {
11477                                let mut g = err_w.lock();
11478                                if g.is_none() {
11479                                    *g = Some(format!("par_reduce: {:?}", e));
11480                                }
11481                                PerlValue::UNDEF
11482                            }
11483                        }
11484                    })
11485                    .collect();
11486                if let Some(msg) = first_err.lock().take() {
11487                    return Err(FlowOrError::Error(PerlError::runtime(msg, line)));
11488                }
11489                if per_chunk.is_empty() {
11490                    return Ok(PerlValue::UNDEF);
11491                }
11492                // Explicit reducer: pairwise via user block with $a/$b bound.
11493                if let Some(rb) = reduce_block {
11494                    let mut acc = per_chunk[0].clone();
11495                    for v in per_chunk.into_iter().skip(1) {
11496                        self.scope.declare_scalar("a", acc.clone());
11497                        self.scope.declare_scalar("b", v);
11498                        acc = self.exec_block(rb)?;
11499                    }
11500                    return Ok(acc);
11501                }
11502                // Auto-merge.
11503                Ok(par_reduce_auto_merge(per_chunk))
11504            }
11505            ExprKind::PForExpr {
11506                block,
11507                list,
11508                progress,
11509            } => {
11510                let show_progress = progress
11511                    .as_ref()
11512                    .map(|p| self.eval_expr(p))
11513                    .transpose()?
11514                    .map(|v| v.is_true())
11515                    .unwrap_or(false);
11516                let list_val = self.eval_expr(list)?;
11517                let items = list_val.to_list();
11518                let block = block.clone();
11519                let subs = self.subs.clone();
11520                let (scope_capture, atomic_arrays, atomic_hashes) =
11521                    self.scope.capture_with_atomics();
11522
11523                let pmap_progress = PmapProgress::new(show_progress, items.len());
11524                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
11525                items.into_par_iter().for_each(|item| {
11526                    if first_err.lock().is_some() {
11527                        return;
11528                    }
11529                    let mut local_interp = VMHelper::new();
11530                    local_interp.subs = subs.clone();
11531                    local_interp.scope.restore_capture(&scope_capture);
11532                    local_interp
11533                        .scope
11534                        .restore_atomics(&atomic_arrays, &atomic_hashes);
11535                    local_interp.enable_parallel_guard();
11536                    local_interp.scope.set_topic(item);
11537                    match local_interp.exec_block(&block) {
11538                        Ok(_) => {}
11539                        Err(e) => {
11540                            let stryke = match e {
11541                                FlowOrError::Error(stryke) => stryke,
11542                                FlowOrError::Flow(_) => PerlError::runtime(
11543                                    "return/last/next/redo not supported inside pfor block",
11544                                    line,
11545                                ),
11546                            };
11547                            let mut g = first_err.lock();
11548                            if g.is_none() {
11549                                *g = Some(stryke);
11550                            }
11551                        }
11552                    }
11553                    pmap_progress.tick();
11554                });
11555                pmap_progress.finish();
11556                if let Some(e) = first_err.lock().take() {
11557                    return Err(FlowOrError::Error(e));
11558                }
11559                Ok(PerlValue::UNDEF)
11560            }
11561            ExprKind::FanExpr {
11562                count,
11563                block,
11564                progress,
11565                capture,
11566            } => {
11567                let show_progress = progress
11568                    .as_ref()
11569                    .map(|p| self.eval_expr(p))
11570                    .transpose()?
11571                    .map(|v| v.is_true())
11572                    .unwrap_or(false);
11573                let n = match count {
11574                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
11575                    None => self.parallel_thread_count(),
11576                };
11577                let block = block.clone();
11578                let subs = self.subs.clone();
11579                let (scope_capture, atomic_arrays, atomic_hashes) =
11580                    self.scope.capture_with_atomics();
11581
11582                let fan_progress = FanProgress::new(show_progress, n);
11583                if *capture {
11584                    if n == 0 {
11585                        return Ok(PerlValue::array(Vec::new()));
11586                    }
11587                    let pairs: Vec<(usize, ExecResult)> = (0..n)
11588                        .into_par_iter()
11589                        .map(|i| {
11590                            fan_progress.start_worker(i);
11591                            let mut local_interp = VMHelper::new();
11592                            local_interp.subs = subs.clone();
11593                            local_interp.suppress_stdout = show_progress;
11594                            local_interp.scope.restore_capture(&scope_capture);
11595                            local_interp
11596                                .scope
11597                                .restore_atomics(&atomic_arrays, &atomic_hashes);
11598                            local_interp.enable_parallel_guard();
11599                            local_interp.scope.set_topic(PerlValue::integer(i as i64));
11600                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
11601                            let res = local_interp.exec_block(&block);
11602                            crate::parallel_trace::fan_worker_set_index(None);
11603                            fan_progress.finish_worker(i);
11604                            (i, res)
11605                        })
11606                        .collect();
11607                    fan_progress.finish();
11608                    let mut pairs = pairs;
11609                    pairs.sort_by_key(|(i, _)| *i);
11610                    let mut out = Vec::with_capacity(n);
11611                    for (_, r) in pairs {
11612                        match r {
11613                            Ok(v) => out.push(v),
11614                            Err(e) => return Err(e),
11615                        }
11616                    }
11617                    return Ok(PerlValue::array(out));
11618                }
11619                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
11620                (0..n).into_par_iter().for_each(|i| {
11621                    if first_err.lock().is_some() {
11622                        return;
11623                    }
11624                    fan_progress.start_worker(i);
11625                    let mut local_interp = VMHelper::new();
11626                    local_interp.subs = subs.clone();
11627                    local_interp.suppress_stdout = show_progress;
11628                    local_interp.scope.restore_capture(&scope_capture);
11629                    local_interp
11630                        .scope
11631                        .restore_atomics(&atomic_arrays, &atomic_hashes);
11632                    local_interp.enable_parallel_guard();
11633                    local_interp.scope.set_topic(PerlValue::integer(i as i64));
11634                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
11635                    match local_interp.exec_block(&block) {
11636                        Ok(_) => {}
11637                        Err(e) => {
11638                            let stryke = match e {
11639                                FlowOrError::Error(stryke) => stryke,
11640                                FlowOrError::Flow(_) => PerlError::runtime(
11641                                    "return/last/next/redo not supported inside fan block",
11642                                    line,
11643                                ),
11644                            };
11645                            let mut g = first_err.lock();
11646                            if g.is_none() {
11647                                *g = Some(stryke);
11648                            }
11649                        }
11650                    }
11651                    crate::parallel_trace::fan_worker_set_index(None);
11652                    fan_progress.finish_worker(i);
11653                });
11654                fan_progress.finish();
11655                if let Some(e) = first_err.lock().take() {
11656                    return Err(FlowOrError::Error(e));
11657                }
11658                Ok(PerlValue::UNDEF)
11659            }
11660            ExprKind::RetryBlock {
11661                body,
11662                times,
11663                backoff,
11664            } => self.eval_retry_block(body, times, *backoff, line),
11665            ExprKind::RateLimitBlock {
11666                slot,
11667                max,
11668                window,
11669                body,
11670            } => self.eval_rate_limit_block(*slot, max, window, body, line),
11671            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
11672            ExprKind::GenBlock { body } => {
11673                let g = Arc::new(PerlGenerator {
11674                    block: body.clone(),
11675                    pc: Mutex::new(0),
11676                    scope_started: Mutex::new(false),
11677                    exhausted: Mutex::new(false),
11678                });
11679                Ok(PerlValue::generator(g))
11680            }
11681            ExprKind::Yield(e) => {
11682                if !self.in_generator {
11683                    return Err(PerlError::runtime("yield outside gen block", line).into());
11684                }
11685                let v = self.eval_expr(e)?;
11686                Err(FlowOrError::Flow(Flow::Yield(v)))
11687            }
11688            ExprKind::AlgebraicMatch { subject, arms } => {
11689                self.eval_algebraic_match(subject, arms, line)
11690            }
11691            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
11692                Ok(self.spawn_async_block(body))
11693            }
11694            ExprKind::Trace { body } => {
11695                crate::parallel_trace::trace_enter();
11696                let out = self.exec_block(body);
11697                crate::parallel_trace::trace_leave();
11698                out
11699            }
11700            ExprKind::Spinner { message, body } => {
11701                use std::io::Write as _;
11702                let msg = self.eval_expr(message)?.to_string();
11703                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
11704                let done2 = done.clone();
11705                let handle = std::thread::spawn(move || {
11706                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
11707                    let mut i = 0;
11708                    let stderr = std::io::stderr();
11709                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
11710                        {
11711                            let stdout = std::io::stdout();
11712                            let _stdout_lock = stdout.lock();
11713                            let mut err = stderr.lock();
11714                            let _ = write!(
11715                                err,
11716                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
11717                                frames[i % frames.len()],
11718                                msg
11719                            );
11720                            let _ = err.flush();
11721                        }
11722                        std::thread::sleep(std::time::Duration::from_millis(80));
11723                        i += 1;
11724                    }
11725                    let mut err = stderr.lock();
11726                    let _ = write!(err, "\r\x1b[2K");
11727                    let _ = err.flush();
11728                });
11729                let result = self.exec_block(body);
11730                done.store(true, std::sync::atomic::Ordering::Relaxed);
11731                let _ = handle.join();
11732                result
11733            }
11734            ExprKind::Timer { body } => {
11735                let start = std::time::Instant::now();
11736                self.exec_block(body)?;
11737                let ms = start.elapsed().as_secs_f64() * 1000.0;
11738                Ok(PerlValue::float(ms))
11739            }
11740            ExprKind::Bench { body, times } => {
11741                let n = self.eval_expr(times)?.to_int();
11742                if n < 0 {
11743                    return Err(PerlError::runtime(
11744                        "bench: iteration count must be non-negative",
11745                        line,
11746                    )
11747                    .into());
11748                }
11749                self.run_bench_block(body, n as usize, line)
11750            }
11751            ExprKind::Await(expr) => {
11752                let v = self.eval_expr(expr)?;
11753                if let Some(t) = v.as_async_task() {
11754                    t.await_result().map_err(FlowOrError::from)
11755                } else {
11756                    Ok(v)
11757                }
11758            }
11759            ExprKind::Slurp(e) => {
11760                let path = self.eval_expr(e)?.to_string();
11761                crate::perl_fs::read_file_text_or_glob(&path)
11762                    .map(PerlValue::string)
11763                    .map_err(|e| {
11764                        FlowOrError::Error(PerlError::runtime(format!("slurp: {}", e), line))
11765                    })
11766            }
11767            ExprKind::Capture(e) => {
11768                let cmd = self.eval_expr(e)?.to_string();
11769                let output = Command::new("sh")
11770                    .arg("-c")
11771                    .arg(&cmd)
11772                    .output()
11773                    .map_err(|e| {
11774                        FlowOrError::Error(PerlError::runtime(format!("capture: {}", e), line))
11775                    })?;
11776                self.record_child_exit_status(output.status);
11777                let exitcode = output.status.code().unwrap_or(-1) as i64;
11778                let stdout = decode_utf8_or_latin1(&output.stdout);
11779                let stderr = decode_utf8_or_latin1(&output.stderr);
11780                Ok(PerlValue::capture(Arc::new(CaptureResult {
11781                    stdout,
11782                    stderr,
11783                    exitcode,
11784                })))
11785            }
11786            ExprKind::Qx(e) => {
11787                let cmd = self.eval_expr(e)?.to_string();
11788                crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)
11789            }
11790            ExprKind::FetchUrl(e) => {
11791                let url = self.eval_expr(e)?.to_string();
11792                ureq::get(&url)
11793                    .call()
11794                    .map_err(|e| {
11795                        FlowOrError::Error(PerlError::runtime(format!("fetch_url: {}", e), line))
11796                    })
11797                    .and_then(|r| {
11798                        r.into_string().map(PerlValue::string).map_err(|e| {
11799                            FlowOrError::Error(PerlError::runtime(
11800                                format!("fetch_url: {}", e),
11801                                line,
11802                            ))
11803                        })
11804                    })
11805            }
11806            ExprKind::Pchannel { capacity } => {
11807                if let Some(c) = capacity {
11808                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
11809                    Ok(crate::pchannel::create_bounded_pair(n))
11810                } else {
11811                    Ok(crate::pchannel::create_pair())
11812                }
11813            }
11814            ExprKind::PSortExpr {
11815                cmp,
11816                list,
11817                progress,
11818            } => {
11819                let show_progress = progress
11820                    .as_ref()
11821                    .map(|p| self.eval_expr(p))
11822                    .transpose()?
11823                    .map(|v| v.is_true())
11824                    .unwrap_or(false);
11825                let list_val = self.eval_expr(list)?;
11826                let mut items = list_val.to_list();
11827                let pmap_progress = PmapProgress::new(show_progress, 2);
11828                pmap_progress.tick();
11829                if let Some(cmp_block) = cmp {
11830                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
11831                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
11832                    } else {
11833                        let cmp_block = cmp_block.clone();
11834                        let subs = self.subs.clone();
11835                        let scope_capture = self.scope.capture();
11836                        items.par_sort_by(|a, b| {
11837                            let mut local_interp = VMHelper::new();
11838                            local_interp.subs = subs.clone();
11839                            local_interp.scope.restore_capture(&scope_capture);
11840                            local_interp.scope.set_sort_pair(a.clone(), b.clone());
11841                            match local_interp.exec_block(&cmp_block) {
11842                                Ok(v) => {
11843                                    let n = v.to_int();
11844                                    if n < 0 {
11845                                        std::cmp::Ordering::Less
11846                                    } else if n > 0 {
11847                                        std::cmp::Ordering::Greater
11848                                    } else {
11849                                        std::cmp::Ordering::Equal
11850                                    }
11851                                }
11852                                Err(_) => std::cmp::Ordering::Equal,
11853                            }
11854                        });
11855                    }
11856                } else {
11857                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
11858                }
11859                pmap_progress.tick();
11860                pmap_progress.finish();
11861                Ok(PerlValue::array(items))
11862            }
11863
11864            ExprKind::ReduceExpr { block, list } => {
11865                let list_val = self.eval_expr(list)?;
11866                let items = list_val.to_list();
11867                if items.is_empty() {
11868                    return Ok(PerlValue::UNDEF);
11869                }
11870                if items.len() == 1 {
11871                    return Ok(items.into_iter().next().unwrap());
11872                }
11873                let block = block.clone();
11874                let subs = self.subs.clone();
11875                let scope_capture = self.scope.capture();
11876                let mut acc = items[0].clone();
11877                for b in items.into_iter().skip(1) {
11878                    let mut local_interp = VMHelper::new();
11879                    local_interp.subs = subs.clone();
11880                    local_interp.scope.restore_capture(&scope_capture);
11881                    local_interp.scope.set_sort_pair(acc, b);
11882                    acc = match local_interp.exec_block(&block) {
11883                        Ok(val) => val,
11884                        Err(_) => PerlValue::UNDEF,
11885                    };
11886                }
11887                Ok(acc)
11888            }
11889
11890            ExprKind::PReduceExpr {
11891                block,
11892                list,
11893                progress,
11894            } => {
11895                let show_progress = progress
11896                    .as_ref()
11897                    .map(|p| self.eval_expr(p))
11898                    .transpose()?
11899                    .map(|v| v.is_true())
11900                    .unwrap_or(false);
11901                let list_val = self.eval_expr(list)?;
11902                let items = list_val.to_list();
11903                if items.is_empty() {
11904                    return Ok(PerlValue::UNDEF);
11905                }
11906                if items.len() == 1 {
11907                    return Ok(items.into_iter().next().unwrap());
11908                }
11909                let block = block.clone();
11910                let subs = self.subs.clone();
11911                let scope_capture = self.scope.capture();
11912                let pmap_progress = PmapProgress::new(show_progress, items.len());
11913
11914                let result = items
11915                    .into_par_iter()
11916                    .map(|x| {
11917                        pmap_progress.tick();
11918                        x
11919                    })
11920                    .reduce_with(|a, b| {
11921                        let mut local_interp = VMHelper::new();
11922                        local_interp.subs = subs.clone();
11923                        local_interp.scope.restore_capture(&scope_capture);
11924                        local_interp.scope.set_sort_pair(a, b);
11925                        match local_interp.exec_block(&block) {
11926                            Ok(val) => val,
11927                            Err(_) => PerlValue::UNDEF,
11928                        }
11929                    });
11930                pmap_progress.finish();
11931                Ok(result.unwrap_or(PerlValue::UNDEF))
11932            }
11933
11934            ExprKind::PReduceInitExpr {
11935                init,
11936                block,
11937                list,
11938                progress,
11939            } => {
11940                let show_progress = progress
11941                    .as_ref()
11942                    .map(|p| self.eval_expr(p))
11943                    .transpose()?
11944                    .map(|v| v.is_true())
11945                    .unwrap_or(false);
11946                let init_val = self.eval_expr(init)?;
11947                let list_val = self.eval_expr(list)?;
11948                let items = list_val.to_list();
11949                if items.is_empty() {
11950                    return Ok(init_val);
11951                }
11952                let block = block.clone();
11953                let subs = self.subs.clone();
11954                let scope_capture = self.scope.capture();
11955                let cap: &[(String, PerlValue)] = scope_capture.as_slice();
11956                if items.len() == 1 {
11957                    return Ok(fold_preduce_init_step(
11958                        &subs,
11959                        cap,
11960                        &block,
11961                        preduce_init_fold_identity(&init_val),
11962                        items.into_iter().next().unwrap(),
11963                    ));
11964                }
11965                let pmap_progress = PmapProgress::new(show_progress, items.len());
11966                let result = items
11967                    .into_par_iter()
11968                    .fold(
11969                        || preduce_init_fold_identity(&init_val),
11970                        |acc, item| {
11971                            pmap_progress.tick();
11972                            fold_preduce_init_step(&subs, cap, &block, acc, item)
11973                        },
11974                    )
11975                    .reduce(
11976                        || preduce_init_fold_identity(&init_val),
11977                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
11978                    );
11979                pmap_progress.finish();
11980                Ok(result)
11981            }
11982
11983            ExprKind::PMapReduceExpr {
11984                map_block,
11985                reduce_block,
11986                list,
11987                progress,
11988            } => {
11989                let show_progress = progress
11990                    .as_ref()
11991                    .map(|p| self.eval_expr(p))
11992                    .transpose()?
11993                    .map(|v| v.is_true())
11994                    .unwrap_or(false);
11995                let list_val = self.eval_expr(list)?;
11996                let items = list_val.to_list();
11997                if items.is_empty() {
11998                    return Ok(PerlValue::UNDEF);
11999                }
12000                let map_block = map_block.clone();
12001                let reduce_block = reduce_block.clone();
12002                let subs = self.subs.clone();
12003                let scope_capture = self.scope.capture();
12004                if items.len() == 1 {
12005                    let mut local_interp = VMHelper::new();
12006                    local_interp.subs = subs.clone();
12007                    local_interp.scope.restore_capture(&scope_capture);
12008                    local_interp.scope.set_topic(items[0].clone());
12009                    return match local_interp.exec_block_no_scope(&map_block) {
12010                        Ok(v) => Ok(v),
12011                        Err(_) => Ok(PerlValue::UNDEF),
12012                    };
12013                }
12014                let pmap_progress = PmapProgress::new(show_progress, items.len());
12015                let result = items
12016                    .into_par_iter()
12017                    .map(|item| {
12018                        let mut local_interp = VMHelper::new();
12019                        local_interp.subs = subs.clone();
12020                        local_interp.scope.restore_capture(&scope_capture);
12021                        local_interp.scope.set_topic(item);
12022                        let val = match local_interp.exec_block_no_scope(&map_block) {
12023                            Ok(val) => val,
12024                            Err(_) => PerlValue::UNDEF,
12025                        };
12026                        pmap_progress.tick();
12027                        val
12028                    })
12029                    .reduce_with(|a, b| {
12030                        let mut local_interp = VMHelper::new();
12031                        local_interp.subs = subs.clone();
12032                        local_interp.scope.restore_capture(&scope_capture);
12033                        local_interp.scope.set_sort_pair(a, b);
12034                        match local_interp.exec_block_no_scope(&reduce_block) {
12035                            Ok(val) => val,
12036                            Err(_) => PerlValue::UNDEF,
12037                        }
12038                    });
12039                pmap_progress.finish();
12040                Ok(result.unwrap_or(PerlValue::UNDEF))
12041            }
12042
12043            ExprKind::PcacheExpr {
12044                block,
12045                list,
12046                progress,
12047            } => {
12048                let show_progress = progress
12049                    .as_ref()
12050                    .map(|p| self.eval_expr(p))
12051                    .transpose()?
12052                    .map(|v| v.is_true())
12053                    .unwrap_or(false);
12054                let list_val = self.eval_expr(list)?;
12055                let items = list_val.to_list();
12056                let block = block.clone();
12057                let subs = self.subs.clone();
12058                let scope_capture = self.scope.capture();
12059                let cache = &*crate::pcache::GLOBAL_PCACHE;
12060                let pmap_progress = PmapProgress::new(show_progress, items.len());
12061                let results: Vec<PerlValue> = items
12062                    .into_par_iter()
12063                    .map(|item| {
12064                        let k = crate::pcache::cache_key(&item);
12065                        if let Some(v) = cache.get(&k) {
12066                            pmap_progress.tick();
12067                            return v.clone();
12068                        }
12069                        let mut local_interp = VMHelper::new();
12070                        local_interp.subs = subs.clone();
12071                        local_interp.scope.restore_capture(&scope_capture);
12072                        local_interp.scope.set_topic(item.clone());
12073                        let val = match local_interp.exec_block_no_scope(&block) {
12074                            Ok(v) => v,
12075                            Err(_) => PerlValue::UNDEF,
12076                        };
12077                        cache.insert(k, val.clone());
12078                        pmap_progress.tick();
12079                        val
12080                    })
12081                    .collect();
12082                pmap_progress.finish();
12083                Ok(PerlValue::array(results))
12084            }
12085
12086            ExprKind::PselectExpr { receivers, timeout } => {
12087                let mut rx_vals = Vec::with_capacity(receivers.len());
12088                for r in receivers {
12089                    rx_vals.push(self.eval_expr(r)?);
12090                }
12091                let dur = if let Some(t) = timeout.as_ref() {
12092                    Some(std::time::Duration::from_secs_f64(
12093                        self.eval_expr(t)?.to_number().max(0.0),
12094                    ))
12095                } else {
12096                    None
12097                };
12098                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
12099                    &rx_vals, dur, line,
12100                )?)
12101            }
12102
12103            // Array ops
12104            ExprKind::Push { array, values } => {
12105                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
12106            }
12107            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
12108            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
12109            ExprKind::Unshift { array, values } => {
12110                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
12111            }
12112            ExprKind::Splice {
12113                array,
12114                offset,
12115                length,
12116                replacement,
12117            } => self.eval_splice_expr(
12118                array.as_ref(),
12119                offset.as_deref(),
12120                length.as_deref(),
12121                replacement.as_slice(),
12122                ctx,
12123                line,
12124            ),
12125            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
12126            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
12127            ExprKind::Keys(expr) => {
12128                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
12129                let keys = Self::keys_from_value(val, line)?;
12130                if ctx == WantarrayCtx::List {
12131                    Ok(keys)
12132                } else {
12133                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
12134                    Ok(PerlValue::integer(n as i64))
12135                }
12136            }
12137            ExprKind::Values(expr) => {
12138                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
12139                let vals = Self::values_from_value(val, line)?;
12140                if ctx == WantarrayCtx::List {
12141                    Ok(vals)
12142                } else {
12143                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
12144                    Ok(PerlValue::integer(n as i64))
12145                }
12146            }
12147            ExprKind::Each(_) => {
12148                // Simplified: returns empty list (full iterator state would need more work)
12149                Ok(PerlValue::array(vec![]))
12150            }
12151
12152            // String ops
12153            ExprKind::Chomp(expr) => {
12154                let val = self.eval_expr(expr)?;
12155                self.chomp_inplace_execute(val, expr)
12156            }
12157            ExprKind::Chop(expr) => {
12158                let val = self.eval_expr(expr)?;
12159                self.chop_inplace_execute(val, expr)
12160            }
12161            ExprKind::Length(expr) => {
12162                let val = self.eval_expr(expr)?;
12163                Ok(if let Some(a) = val.as_array_vec() {
12164                    PerlValue::integer(a.len() as i64)
12165                } else if let Some(h) = val.as_hash_map() {
12166                    PerlValue::integer(h.len() as i64)
12167                } else if let Some(b) = val.as_bytes_arc() {
12168                    // Raw byte buffer: always byte count, regardless of utf8 pragma.
12169                    PerlValue::integer(b.len() as i64)
12170                } else {
12171                    let s = val.to_string();
12172                    let n = if self.utf8_pragma {
12173                        s.chars().count()
12174                    } else {
12175                        s.len()
12176                    };
12177                    PerlValue::integer(n as i64)
12178                })
12179            }
12180            ExprKind::Substr {
12181                string,
12182                offset,
12183                length,
12184                replacement,
12185            } => self.eval_substr_expr(
12186                string.as_ref(),
12187                offset.as_ref(),
12188                length.as_deref(),
12189                replacement.as_deref(),
12190                line,
12191            ),
12192            ExprKind::Index {
12193                string,
12194                substr,
12195                position,
12196            } => {
12197                let s = self.eval_expr(string)?.to_string();
12198                let sub = self.eval_expr(substr)?.to_string();
12199                let pos = if let Some(p) = position {
12200                    self.eval_expr(p)?.to_int() as usize
12201                } else {
12202                    0
12203                };
12204                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
12205                Ok(PerlValue::integer(result))
12206            }
12207            ExprKind::Rindex {
12208                string,
12209                substr,
12210                position,
12211            } => {
12212                let s = self.eval_expr(string)?.to_string();
12213                let sub = self.eval_expr(substr)?.to_string();
12214                let end = if let Some(p) = position {
12215                    self.eval_expr(p)?.to_int() as usize + sub.len()
12216                } else {
12217                    s.len()
12218                };
12219                let search = &s[..end.min(s.len())];
12220                let result = search.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
12221                Ok(PerlValue::integer(result))
12222            }
12223            ExprKind::Sprintf { format, args } => {
12224                let fmt = self.eval_expr(format)?.to_string();
12225                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
12226                // builtins into individual format arguments.
12227                let mut arg_vals = Vec::new();
12228                for a in args {
12229                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
12230                    if let Some(items) = v.as_array_vec() {
12231                        arg_vals.extend(items);
12232                    } else {
12233                        arg_vals.push(v);
12234                    }
12235                }
12236                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
12237                Ok(PerlValue::string(s))
12238            }
12239            ExprKind::JoinExpr { separator, list } => {
12240                let sep = self.eval_expr(separator)?.to_string();
12241                // Like Perl 5, arguments after the separator are evaluated in list context so
12242                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
12243                // expands `localtime` to nine fields.
12244                let items = if let ExprKind::List(exprs) = &list.kind {
12245                    let saved = self.wantarray_kind;
12246                    self.wantarray_kind = WantarrayCtx::List;
12247                    let mut vals = Vec::new();
12248                    for e in exprs {
12249                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
12250                        if let Some(items) = v.as_array_vec() {
12251                            vals.extend(items);
12252                        } else if v.is_iterator() {
12253                            // `join "", rev chars(...)` etc. — drain the
12254                            // lazy iterator into items so it joins the
12255                            // sequence instead of stringifying as "Iterator".
12256                            vals.extend(v.into_iterator().collect_all());
12257                        } else {
12258                            vals.push(v);
12259                        }
12260                    }
12261                    self.wantarray_kind = saved;
12262                    vals
12263                } else {
12264                    let saved = self.wantarray_kind;
12265                    self.wantarray_kind = WantarrayCtx::List;
12266                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
12267                    self.wantarray_kind = saved;
12268                    if let Some(items) = v.as_array_vec() {
12269                        items
12270                    } else if v.is_iterator() {
12271                        // `~> ... rev |> join ""` produces an Iterator from
12272                        // the lazy stages; drain it before joining, otherwise
12273                        // it stringifies as "Iterator".
12274                        v.into_iterator().collect_all()
12275                    } else {
12276                        vec![v]
12277                    }
12278                };
12279                let mut strs = Vec::with_capacity(items.len());
12280                for v in &items {
12281                    strs.push(self.stringify_value(v.clone(), line)?);
12282                }
12283                Ok(PerlValue::string(strs.join(&sep)))
12284            }
12285            ExprKind::SplitExpr {
12286                pattern,
12287                string,
12288                limit,
12289            } => {
12290                let pat_val = self.eval_expr(pattern)?;
12291                // For a regex value, pull the *source* (not the Display form,
12292                // which wraps an empty regex as `(?:)` and would defeat the
12293                // empty-pattern branch below).  Mirrors the VM's Split path.
12294                let pat = pat_val
12295                    .regex_src_and_flags()
12296                    .map(|(s, _)| s)
12297                    .unwrap_or_else(|| pat_val.to_string());
12298                let s = self.eval_expr(string)?.to_string();
12299                // Perl semantics for the limit field:
12300                //   omitted / 0  → no truncation, *strip* trailing empty fields.
12301                //   > 0          → at most LIMIT fields, keep empties up to limit.
12302                //   < 0          → no truncation, *keep* all empties.
12303                // Stryke previously parsed limit as `usize`, which folded a
12304                // user-supplied -1 into a giant positive number and made the
12305                // strip / keep decision ambiguous. Use `i64` so the sign is
12306                // preserved.
12307                let lim_opt: Option<i64> = limit
12308                    .as_ref()
12309                    .map(|l| self.eval_expr(l).map(|v| v.to_int()))
12310                    .transpose()?;
12311                let re = self.compile_regex(&pat, "", line)?;
12312                let mut parts: Vec<String> = match lim_opt {
12313                    Some(l) if l > 0 => re.splitn_strings(&s, l as usize),
12314                    _ => re.split_strings(&s),
12315                };
12316
12317                // Zero-width patterns (`split //, $s`) are defined by Perl as
12318                // "split between every character" — the regex engine, however,
12319                // also matches the empty string at position 0, producing a
12320                // spurious leading empty field that Perl does not emit. Strip
12321                // it before the trailing-empty rule kicks in.
12322                if pat.is_empty() && parts.first().is_some_and(|p| p.is_empty()) {
12323                    parts.remove(0);
12324                }
12325                // Trailing-empty strip: Perl strips ONLY when LIMIT is omitted
12326                // or zero. Positive LIMIT keeps trailing empties; negative
12327                // LIMIT also keeps them.
12328                let strip_trailing = matches!(lim_opt, None | Some(0));
12329                if strip_trailing {
12330                    while parts.last().is_some_and(|p| p.is_empty()) {
12331                        parts.pop();
12332                    }
12333                }
12334
12335                Ok(PerlValue::array(
12336                    parts.into_iter().map(PerlValue::string).collect(),
12337                ))
12338            }
12339
12340            // Numeric
12341            ExprKind::Abs(expr) => {
12342                let val = self.eval_expr(expr)?;
12343                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
12344                    return r;
12345                }
12346                Ok(PerlValue::float(val.to_number().abs()))
12347            }
12348            ExprKind::Int(expr) => {
12349                let val = self.eval_expr(expr)?;
12350                Ok(PerlValue::integer(val.to_number() as i64))
12351            }
12352            ExprKind::Sqrt(expr) => {
12353                let val = self.eval_expr(expr)?;
12354                Ok(PerlValue::float(val.to_number().sqrt()))
12355            }
12356            ExprKind::Sin(expr) => {
12357                let val = self.eval_expr(expr)?;
12358                Ok(PerlValue::float(val.to_number().sin()))
12359            }
12360            ExprKind::Cos(expr) => {
12361                let val = self.eval_expr(expr)?;
12362                Ok(PerlValue::float(val.to_number().cos()))
12363            }
12364            ExprKind::Atan2 { y, x } => {
12365                let yv = self.eval_expr(y)?.to_number();
12366                let xv = self.eval_expr(x)?.to_number();
12367                Ok(PerlValue::float(yv.atan2(xv)))
12368            }
12369            ExprKind::Exp(expr) => {
12370                let val = self.eval_expr(expr)?;
12371                Ok(PerlValue::float(val.to_number().exp()))
12372            }
12373            ExprKind::Log(expr) => {
12374                let val = self.eval_expr(expr)?;
12375                Ok(PerlValue::float(val.to_number().ln()))
12376            }
12377            ExprKind::Rand(upper) => {
12378                let u = match upper {
12379                    Some(e) => self.eval_expr(e)?.to_number(),
12380                    None => 1.0,
12381                };
12382                Ok(PerlValue::float(self.perl_rand(u)))
12383            }
12384            ExprKind::Srand(seed) => {
12385                let s = match seed {
12386                    Some(e) => Some(self.eval_expr(e)?.to_number()),
12387                    None => None,
12388                };
12389                Ok(PerlValue::integer(self.perl_srand(s)))
12390            }
12391            ExprKind::Hex(expr) => {
12392                let val = self.eval_expr(expr)?.to_string();
12393                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
12394                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
12395                Ok(PerlValue::integer(n))
12396            }
12397            ExprKind::Oct(expr) => {
12398                let val = self.eval_expr(expr)?.to_string();
12399                let s = val.trim();
12400                let n = if s.starts_with("0x") || s.starts_with("0X") {
12401                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
12402                } else if s.starts_with("0b") || s.starts_with("0B") {
12403                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
12404                } else if s.starts_with("0o") || s.starts_with("0O") {
12405                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
12406                } else {
12407                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
12408                };
12409                Ok(PerlValue::integer(n))
12410            }
12411
12412            // Case
12413            ExprKind::Lc(expr) => Ok(PerlValue::string(
12414                self.eval_expr(expr)?.to_string().to_lowercase(),
12415            )),
12416            ExprKind::Uc(expr) => Ok(PerlValue::string(
12417                self.eval_expr(expr)?.to_string().to_uppercase(),
12418            )),
12419            ExprKind::Lcfirst(expr) => {
12420                let s = self.eval_expr(expr)?.to_string();
12421                let mut chars = s.chars();
12422                let result = match chars.next() {
12423                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
12424                    None => String::new(),
12425                };
12426                Ok(PerlValue::string(result))
12427            }
12428            ExprKind::Ucfirst(expr) => {
12429                let s = self.eval_expr(expr)?.to_string();
12430                let mut chars = s.chars();
12431                let result = match chars.next() {
12432                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
12433                    None => String::new(),
12434                };
12435                Ok(PerlValue::string(result))
12436            }
12437            ExprKind::Fc(expr) => Ok(PerlValue::string(default_case_fold_str(
12438                &self.eval_expr(expr)?.to_string(),
12439            ))),
12440            ExprKind::Crypt { plaintext, salt } => {
12441                let p = self.eval_expr(plaintext)?.to_string();
12442                let sl = self.eval_expr(salt)?.to_string();
12443                Ok(PerlValue::string(perl_crypt(&p, &sl)))
12444            }
12445            ExprKind::Pos(e) => {
12446                let key = match e {
12447                    None => "_".to_string(),
12448                    Some(expr) => match &expr.kind {
12449                        ExprKind::ScalarVar(n) => n.clone(),
12450                        _ => self.eval_expr(expr)?.to_string(),
12451                    },
12452                };
12453                Ok(self
12454                    .regex_pos
12455                    .get(&key)
12456                    .copied()
12457                    .flatten()
12458                    .map(|p| PerlValue::integer(p as i64))
12459                    .unwrap_or(PerlValue::UNDEF))
12460            }
12461            ExprKind::Study(expr) => {
12462                let s = self.eval_expr(expr)?.to_string();
12463                Ok(Self::study_return_value(&s))
12464            }
12465
12466            // Type
12467            ExprKind::Defined(expr) => {
12468                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
12469                if let ExprKind::SubroutineRef(name) = &expr.kind {
12470                    let exists = self.resolve_sub_by_name(name).is_some();
12471                    return Ok(PerlValue::integer(if exists { 1 } else { 0 }));
12472                }
12473                let val = self.eval_expr(expr)?;
12474                Ok(PerlValue::integer(if val.is_undef() { 0 } else { 1 }))
12475            }
12476            ExprKind::Ref(expr) => {
12477                let val = self.eval_expr(expr)?;
12478                Ok(val.ref_type())
12479            }
12480            ExprKind::ScalarContext(expr) => {
12481                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
12482                Ok(v.scalar_context())
12483            }
12484
12485            // Char
12486            ExprKind::Chr(expr) => {
12487                let n = self.eval_expr(expr)?.to_int() as u32;
12488                Ok(PerlValue::string(
12489                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
12490                ))
12491            }
12492            ExprKind::Ord(expr) => {
12493                let s = self.eval_expr(expr)?.to_string();
12494                Ok(PerlValue::integer(
12495                    s.chars().next().map(|c| c as i64).unwrap_or(0),
12496                ))
12497            }
12498
12499            // I/O
12500            ExprKind::OpenMyHandle { .. } => Err(PerlError::runtime(
12501                "internal: `open my $fh` handle used outside open()",
12502                line,
12503            )
12504            .into()),
12505            ExprKind::Open { handle, mode, file } => {
12506                if let ExprKind::OpenMyHandle { name } = &handle.kind {
12507                    self.scope
12508                        .declare_scalar_frozen(name, PerlValue::UNDEF, false, None)?;
12509                    self.english_note_lexical_scalar(name);
12510                    let mode_s = self.eval_expr(mode)?.to_string();
12511                    let file_opt = if let Some(f) = file {
12512                        Some(self.eval_expr(f)?.to_string())
12513                    } else {
12514                        None
12515                    };
12516                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
12517                    self.scope.set_scalar(name, ret.clone())?;
12518                    return Ok(ret);
12519                }
12520                let handle_s = self.eval_expr(handle)?.to_string();
12521                let handle_name = self.resolve_io_handle_name(&handle_s);
12522                let mode_s = self.eval_expr(mode)?.to_string();
12523                let file_opt = if let Some(f) = file {
12524                    Some(self.eval_expr(f)?.to_string())
12525                } else {
12526                    None
12527                };
12528                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
12529                    .map_err(Into::into)
12530            }
12531            ExprKind::Close(expr) => {
12532                let s = self.eval_expr(expr)?.to_string();
12533                let name = self.resolve_io_handle_name(&s);
12534                self.close_builtin_execute(name).map_err(Into::into)
12535            }
12536            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
12537                self.readline_builtin_execute_list(handle.as_deref())
12538            } else {
12539                self.readline_builtin_execute(handle.as_deref())
12540            }
12541            .map_err(Into::into),
12542            ExprKind::Eof(expr) => match expr {
12543                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
12544                Some(e) => {
12545                    let name = self.eval_expr(e)?;
12546                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
12547                }
12548            },
12549
12550            ExprKind::Opendir { handle, path } => {
12551                let h = self.eval_expr(handle)?.to_string();
12552                let p = self.eval_expr(path)?.to_string();
12553                Ok(self.opendir_handle(&h, &p))
12554            }
12555            ExprKind::Readdir(e) => {
12556                let h = self.eval_expr(e)?.to_string();
12557                Ok(if ctx == WantarrayCtx::List {
12558                    self.readdir_handle_list(&h)
12559                } else {
12560                    self.readdir_handle(&h)
12561                })
12562            }
12563            ExprKind::Closedir(e) => {
12564                let h = self.eval_expr(e)?.to_string();
12565                Ok(self.closedir_handle(&h))
12566            }
12567            ExprKind::Rewinddir(e) => {
12568                let h = self.eval_expr(e)?.to_string();
12569                Ok(self.rewinddir_handle(&h))
12570            }
12571            ExprKind::Telldir(e) => {
12572                let h = self.eval_expr(e)?.to_string();
12573                Ok(self.telldir_handle(&h))
12574            }
12575            ExprKind::Seekdir { handle, position } => {
12576                let h = self.eval_expr(handle)?.to_string();
12577                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
12578                Ok(self.seekdir_handle(&h, pos))
12579            }
12580
12581            // File tests
12582            ExprKind::FileTest { op, expr } => {
12583                let path = self.eval_expr(expr)?.to_string();
12584                // -M, -A, -C return fractional days (float), not boolean
12585                if matches!(op, 'M' | 'A' | 'C') {
12586                    #[cfg(unix)]
12587                    {
12588                        return match crate::perl_fs::filetest_age_days(&path, *op) {
12589                            Some(days) => Ok(PerlValue::float(days)),
12590                            None => Ok(PerlValue::UNDEF),
12591                        };
12592                    }
12593                    #[cfg(not(unix))]
12594                    return Ok(PerlValue::UNDEF);
12595                }
12596                // -s returns file size (or undef on error)
12597                if *op == 's' {
12598                    return match std::fs::metadata(&path) {
12599                        Ok(m) => Ok(PerlValue::integer(m.len() as i64)),
12600                        Err(_) => Ok(PerlValue::UNDEF),
12601                    };
12602                }
12603                let result = match op {
12604                    'e' => std::path::Path::new(&path).exists(),
12605                    'f' => std::path::Path::new(&path).is_file(),
12606                    'd' => std::path::Path::new(&path).is_dir(),
12607                    'l' => std::path::Path::new(&path).is_symlink(),
12608                    #[cfg(unix)]
12609                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
12610                    #[cfg(not(unix))]
12611                    'r' => std::fs::metadata(&path).is_ok(),
12612                    #[cfg(unix)]
12613                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
12614                    #[cfg(not(unix))]
12615                    'w' => std::fs::metadata(&path).is_ok(),
12616                    #[cfg(unix)]
12617                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
12618                    #[cfg(not(unix))]
12619                    'x' => false,
12620                    #[cfg(unix)]
12621                    'o' => crate::perl_fs::filetest_owned_effective(&path),
12622                    #[cfg(not(unix))]
12623                    'o' => false,
12624                    #[cfg(unix)]
12625                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
12626                    #[cfg(not(unix))]
12627                    'R' => false,
12628                    #[cfg(unix)]
12629                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
12630                    #[cfg(not(unix))]
12631                    'W' => false,
12632                    #[cfg(unix)]
12633                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
12634                    #[cfg(not(unix))]
12635                    'X' => false,
12636                    #[cfg(unix)]
12637                    'O' => crate::perl_fs::filetest_owned_real(&path),
12638                    #[cfg(not(unix))]
12639                    'O' => false,
12640                    'z' => std::fs::metadata(&path)
12641                        .map(|m| m.len() == 0)
12642                        .unwrap_or(true),
12643                    't' => crate::perl_fs::filetest_is_tty(&path),
12644                    #[cfg(unix)]
12645                    'p' => crate::perl_fs::filetest_is_pipe(&path),
12646                    #[cfg(not(unix))]
12647                    'p' => false,
12648                    #[cfg(unix)]
12649                    'S' => crate::perl_fs::filetest_is_socket(&path),
12650                    #[cfg(not(unix))]
12651                    'S' => false,
12652                    #[cfg(unix)]
12653                    'b' => crate::perl_fs::filetest_is_block_device(&path),
12654                    #[cfg(not(unix))]
12655                    'b' => false,
12656                    #[cfg(unix)]
12657                    'c' => crate::perl_fs::filetest_is_char_device(&path),
12658                    #[cfg(not(unix))]
12659                    'c' => false,
12660                    #[cfg(unix)]
12661                    'u' => crate::perl_fs::filetest_is_setuid(&path),
12662                    #[cfg(not(unix))]
12663                    'u' => false,
12664                    #[cfg(unix)]
12665                    'g' => crate::perl_fs::filetest_is_setgid(&path),
12666                    #[cfg(not(unix))]
12667                    'g' => false,
12668                    #[cfg(unix)]
12669                    'k' => crate::perl_fs::filetest_is_sticky(&path),
12670                    #[cfg(not(unix))]
12671                    'k' => false,
12672                    'T' => crate::perl_fs::filetest_is_text(&path),
12673                    'B' => crate::perl_fs::filetest_is_binary(&path),
12674                    _ => false,
12675                };
12676                Ok(PerlValue::integer(if result { 1 } else { 0 }))
12677            }
12678
12679            // System
12680            ExprKind::System(args) => {
12681                let mut cmd_args = Vec::new();
12682                for a in args {
12683                    cmd_args.push(self.eval_expr(a)?.to_string());
12684                }
12685                if cmd_args.is_empty() {
12686                    return Ok(PerlValue::integer(-1));
12687                }
12688                let status = Command::new("sh")
12689                    .arg("-c")
12690                    .arg(cmd_args.join(" "))
12691                    .status();
12692                match status {
12693                    Ok(s) => {
12694                        self.record_child_exit_status(s);
12695                        Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
12696                    }
12697                    Err(e) => {
12698                        self.apply_io_error_to_errno(&e);
12699                        Ok(PerlValue::integer(-1))
12700                    }
12701                }
12702            }
12703            ExprKind::Exec(args) => {
12704                let mut cmd_args = Vec::new();
12705                for a in args {
12706                    cmd_args.push(self.eval_expr(a)?.to_string());
12707                }
12708                if cmd_args.is_empty() {
12709                    return Ok(PerlValue::integer(-1));
12710                }
12711                let status = Command::new("sh")
12712                    .arg("-c")
12713                    .arg(cmd_args.join(" "))
12714                    .status();
12715                match status {
12716                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
12717                    Err(e) => {
12718                        self.apply_io_error_to_errno(&e);
12719                        Ok(PerlValue::integer(-1))
12720                    }
12721                }
12722            }
12723            ExprKind::Eval(expr) => {
12724                self.eval_nesting += 1;
12725                let out = match &expr.kind {
12726                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
12727                        Ok(v) => {
12728                            self.clear_eval_error();
12729                            Ok(v)
12730                        }
12731                        Err(FlowOrError::Error(e)) => {
12732                            self.set_eval_error_from_perl_error(&e);
12733                            Ok(PerlValue::UNDEF)
12734                        }
12735                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
12736                    },
12737                    _ => {
12738                        let code = self.eval_expr(expr)?.to_string();
12739                        // Parse and execute the string as Perl code
12740                        match crate::parse_and_run_string(&code, self) {
12741                            Ok(v) => {
12742                                self.clear_eval_error();
12743                                Ok(v)
12744                            }
12745                            Err(e) => {
12746                                self.set_eval_error(e.to_string());
12747                                Ok(PerlValue::UNDEF)
12748                            }
12749                        }
12750                    }
12751                };
12752                self.eval_nesting -= 1;
12753                out
12754            }
12755            ExprKind::Do(expr) => match &expr.kind {
12756                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
12757                _ => {
12758                    let val = self.eval_expr(expr)?;
12759                    let filename = val.to_string();
12760                    match read_file_text_perl_compat(&filename) {
12761                        Ok(code) => {
12762                            let code = crate::data_section::strip_perl_end_marker(&code);
12763                            match crate::parse_and_run_string_in_file(code, self, &filename) {
12764                                Ok(v) => Ok(v),
12765                                Err(e) => {
12766                                    self.set_eval_error(e.to_string());
12767                                    Ok(PerlValue::UNDEF)
12768                                }
12769                            }
12770                        }
12771                        Err(e) => {
12772                            self.apply_io_error_to_errno(&e);
12773                            Ok(PerlValue::UNDEF)
12774                        }
12775                    }
12776                }
12777            },
12778            ExprKind::Require(expr) => {
12779                let spec = self.eval_expr(expr)?.to_string();
12780                self.require_execute(&spec, line)
12781                    .map_err(FlowOrError::Error)
12782            }
12783            ExprKind::Exit(code) => {
12784                let c = if let Some(e) = code {
12785                    self.eval_expr(e)?.to_int() as i32
12786                } else {
12787                    0
12788                };
12789                Err(PerlError::new(ErrorKind::Exit(c), "", line, &self.file).into())
12790            }
12791            ExprKind::Chdir(expr) => {
12792                let path = self.eval_expr(expr)?.to_string();
12793                match std::env::set_current_dir(&path) {
12794                    Ok(_) => Ok(PerlValue::integer(1)),
12795                    Err(e) => {
12796                        self.apply_io_error_to_errno(&e);
12797                        Ok(PerlValue::integer(0))
12798                    }
12799                }
12800            }
12801            ExprKind::Mkdir { path, mode: _ } => {
12802                let p = self.eval_expr(path)?.to_string();
12803                match std::fs::create_dir(&p) {
12804                    Ok(_) => Ok(PerlValue::integer(1)),
12805                    Err(e) => {
12806                        self.apply_io_error_to_errno(&e);
12807                        Ok(PerlValue::integer(0))
12808                    }
12809                }
12810            }
12811            ExprKind::Unlink(args) => {
12812                let mut count = 0i64;
12813                for a in args {
12814                    let path = self.eval_expr(a)?.to_string();
12815                    if std::fs::remove_file(&path).is_ok() {
12816                        count += 1;
12817                    }
12818                }
12819                Ok(PerlValue::integer(count))
12820            }
12821            ExprKind::Rename { old, new } => {
12822                let o = self.eval_expr(old)?.to_string();
12823                let n = self.eval_expr(new)?.to_string();
12824                Ok(crate::perl_fs::rename_paths(&o, &n))
12825            }
12826            ExprKind::Chmod(args) => {
12827                let mode = self.eval_expr(&args[0])?.to_int();
12828                let mut paths = Vec::new();
12829                for a in &args[1..] {
12830                    paths.push(self.eval_expr(a)?.to_string());
12831                }
12832                Ok(PerlValue::integer(crate::perl_fs::chmod_paths(
12833                    &paths, mode,
12834                )))
12835            }
12836            ExprKind::Chown(args) => {
12837                let uid = self.eval_expr(&args[0])?.to_int();
12838                let gid = self.eval_expr(&args[1])?.to_int();
12839                let mut paths = Vec::new();
12840                for a in &args[2..] {
12841                    paths.push(self.eval_expr(a)?.to_string());
12842                }
12843                Ok(PerlValue::integer(crate::perl_fs::chown_paths(
12844                    &paths, uid, gid,
12845                )))
12846            }
12847            ExprKind::Stat(e) => {
12848                let path = self.eval_expr(e)?.to_string();
12849                Ok(crate::perl_fs::stat_path(&path, false))
12850            }
12851            ExprKind::Lstat(e) => {
12852                let path = self.eval_expr(e)?.to_string();
12853                Ok(crate::perl_fs::stat_path(&path, true))
12854            }
12855            ExprKind::Link { old, new } => {
12856                let o = self.eval_expr(old)?.to_string();
12857                let n = self.eval_expr(new)?.to_string();
12858                Ok(crate::perl_fs::link_hard(&o, &n))
12859            }
12860            ExprKind::Symlink { old, new } => {
12861                let o = self.eval_expr(old)?.to_string();
12862                let n = self.eval_expr(new)?.to_string();
12863                Ok(crate::perl_fs::link_sym(&o, &n))
12864            }
12865            ExprKind::Readlink(e) => {
12866                let path = self.eval_expr(e)?.to_string();
12867                Ok(crate::perl_fs::read_link(&path))
12868            }
12869            ExprKind::Files(args) => {
12870                let dir = if args.is_empty() {
12871                    ".".to_string()
12872                } else {
12873                    self.eval_expr(&args[0])?.to_string()
12874                };
12875                Ok(crate::perl_fs::list_files(&dir))
12876            }
12877            ExprKind::Filesf(args) => {
12878                let dir = if args.is_empty() {
12879                    ".".to_string()
12880                } else {
12881                    self.eval_expr(&args[0])?.to_string()
12882                };
12883                Ok(crate::perl_fs::list_filesf(&dir))
12884            }
12885            ExprKind::FilesfRecursive(args) => {
12886                let dir = if args.is_empty() {
12887                    ".".to_string()
12888                } else {
12889                    self.eval_expr(&args[0])?.to_string()
12890                };
12891                Ok(PerlValue::iterator(Arc::new(
12892                    crate::value::FsWalkIterator::new(&dir, true),
12893                )))
12894            }
12895            ExprKind::Dirs(args) => {
12896                let dir = if args.is_empty() {
12897                    ".".to_string()
12898                } else {
12899                    self.eval_expr(&args[0])?.to_string()
12900                };
12901                Ok(crate::perl_fs::list_dirs(&dir))
12902            }
12903            ExprKind::DirsRecursive(args) => {
12904                let dir = if args.is_empty() {
12905                    ".".to_string()
12906                } else {
12907                    self.eval_expr(&args[0])?.to_string()
12908                };
12909                Ok(PerlValue::iterator(Arc::new(
12910                    crate::value::FsWalkIterator::new(&dir, false),
12911                )))
12912            }
12913            ExprKind::SymLinks(args) => {
12914                let dir = if args.is_empty() {
12915                    ".".to_string()
12916                } else {
12917                    self.eval_expr(&args[0])?.to_string()
12918                };
12919                Ok(crate::perl_fs::list_sym_links(&dir))
12920            }
12921            ExprKind::Sockets(args) => {
12922                let dir = if args.is_empty() {
12923                    ".".to_string()
12924                } else {
12925                    self.eval_expr(&args[0])?.to_string()
12926                };
12927                Ok(crate::perl_fs::list_sockets(&dir))
12928            }
12929            ExprKind::Pipes(args) => {
12930                let dir = if args.is_empty() {
12931                    ".".to_string()
12932                } else {
12933                    self.eval_expr(&args[0])?.to_string()
12934                };
12935                Ok(crate::perl_fs::list_pipes(&dir))
12936            }
12937            ExprKind::BlockDevices(args) => {
12938                let dir = if args.is_empty() {
12939                    ".".to_string()
12940                } else {
12941                    self.eval_expr(&args[0])?.to_string()
12942                };
12943                Ok(crate::perl_fs::list_block_devices(&dir))
12944            }
12945            ExprKind::CharDevices(args) => {
12946                let dir = if args.is_empty() {
12947                    ".".to_string()
12948                } else {
12949                    self.eval_expr(&args[0])?.to_string()
12950                };
12951                Ok(crate::perl_fs::list_char_devices(&dir))
12952            }
12953            ExprKind::Executables(args) => {
12954                let dir = if args.is_empty() {
12955                    ".".to_string()
12956                } else {
12957                    self.eval_expr(&args[0])?.to_string()
12958                };
12959                Ok(crate::perl_fs::list_executables(&dir))
12960            }
12961            ExprKind::Glob(args) => {
12962                let mut pats = Vec::new();
12963                for a in args {
12964                    pats.push(self.eval_expr(a)?.to_string());
12965                }
12966                Ok(crate::perl_fs::glob_patterns(&pats))
12967            }
12968            ExprKind::GlobPar { args, progress } => {
12969                let mut pats = Vec::new();
12970                for a in args {
12971                    pats.push(self.eval_expr(a)?.to_string());
12972                }
12973                let show_progress = progress
12974                    .as_ref()
12975                    .map(|p| self.eval_expr(p))
12976                    .transpose()?
12977                    .map(|v| v.is_true())
12978                    .unwrap_or(false);
12979                if show_progress {
12980                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
12981                } else {
12982                    Ok(crate::perl_fs::glob_par_patterns(&pats))
12983                }
12984            }
12985            ExprKind::ParSed { args, progress } => {
12986                let has_progress = progress.is_some();
12987                let mut vals: Vec<PerlValue> = Vec::new();
12988                for a in args {
12989                    vals.push(self.eval_expr(a)?);
12990                }
12991                if let Some(p) = progress {
12992                    vals.push(self.eval_expr(p.as_ref())?);
12993                }
12994                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
12995            }
12996            ExprKind::Bless { ref_expr, class } => {
12997                let val = self.eval_expr(ref_expr)?;
12998                let class_name = if let Some(c) = class {
12999                    self.eval_expr(c)?.to_string()
13000                } else {
13001                    self.scope.get_scalar("__PACKAGE__").to_string()
13002                };
13003                Ok(PerlValue::blessed(Arc::new(
13004                    crate::value::BlessedRef::new_blessed(class_name, val),
13005                )))
13006            }
13007            ExprKind::Caller(_) => {
13008                // Simplified: return package, file, line
13009                Ok(PerlValue::array(vec![
13010                    PerlValue::string("main".into()),
13011                    PerlValue::string(self.file.clone()),
13012                    PerlValue::integer(line as i64),
13013                ]))
13014            }
13015            ExprKind::Wantarray => Ok(match self.wantarray_kind {
13016                WantarrayCtx::Void => PerlValue::UNDEF,
13017                WantarrayCtx::Scalar => PerlValue::integer(0),
13018                WantarrayCtx::List => PerlValue::integer(1),
13019            }),
13020
13021            ExprKind::List(exprs) => {
13022                // In scalar context, the comma operator evaluates to the last element.
13023                if ctx == WantarrayCtx::Scalar {
13024                    if let Some(last) = exprs.last() {
13025                        // Evaluate earlier expressions for side effects
13026                        for e in &exprs[..exprs.len() - 1] {
13027                            self.eval_expr(e)?;
13028                        }
13029                        return self.eval_expr(last);
13030                    } else {
13031                        return Ok(PerlValue::UNDEF);
13032                    }
13033                }
13034                let mut vals = Vec::new();
13035                for e in exprs {
13036                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
13037                    if let Some(items) = v.as_array_vec() {
13038                        vals.extend(items);
13039                    } else {
13040                        vals.push(v);
13041                    }
13042                }
13043                if vals.len() == 1 {
13044                    Ok(vals.pop().unwrap())
13045                } else {
13046                    Ok(PerlValue::array(vals))
13047                }
13048            }
13049
13050            // Postfix modifiers
13051            ExprKind::PostfixIf { expr, condition } => {
13052                if self.eval_postfix_condition(condition)? {
13053                    self.eval_expr(expr)
13054                } else {
13055                    Ok(PerlValue::UNDEF)
13056                }
13057            }
13058            ExprKind::PostfixUnless { expr, condition } => {
13059                if !self.eval_postfix_condition(condition)? {
13060                    self.eval_expr(expr)
13061                } else {
13062                    Ok(PerlValue::UNDEF)
13063                }
13064            }
13065            ExprKind::PostfixWhile { expr, condition } => {
13066                // `do { ... } while (COND)` — body runs before the first condition check.
13067                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
13068                let is_do_block = matches!(
13069                    &expr.kind,
13070                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
13071                );
13072                let mut last = PerlValue::UNDEF;
13073                if is_do_block {
13074                    loop {
13075                        last = self.eval_expr(expr)?;
13076                        if !self.eval_postfix_condition(condition)? {
13077                            break;
13078                        }
13079                    }
13080                } else {
13081                    loop {
13082                        if !self.eval_postfix_condition(condition)? {
13083                            break;
13084                        }
13085                        last = self.eval_expr(expr)?;
13086                    }
13087                }
13088                Ok(last)
13089            }
13090            ExprKind::PostfixUntil { expr, condition } => {
13091                let is_do_block = matches!(
13092                    &expr.kind,
13093                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
13094                );
13095                let mut last = PerlValue::UNDEF;
13096                if is_do_block {
13097                    loop {
13098                        last = self.eval_expr(expr)?;
13099                        if self.eval_postfix_condition(condition)? {
13100                            break;
13101                        }
13102                    }
13103                } else {
13104                    loop {
13105                        if self.eval_postfix_condition(condition)? {
13106                            break;
13107                        }
13108                        last = self.eval_expr(expr)?;
13109                    }
13110                }
13111                Ok(last)
13112            }
13113            ExprKind::PostfixForeach { expr, list } => {
13114                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
13115                let mut last = PerlValue::UNDEF;
13116                for item in items {
13117                    self.scope.set_topic(item);
13118                    last = self.eval_expr(expr)?;
13119                }
13120                Ok(last)
13121            }
13122        }
13123    }
13124
13125    // ── Helpers ──
13126
13127    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
13128        match op {
13129            BinOp::Add => Some("+"),
13130            BinOp::Sub => Some("-"),
13131            BinOp::Mul => Some("*"),
13132            BinOp::Div => Some("/"),
13133            BinOp::Mod => Some("%"),
13134            BinOp::Pow => Some("**"),
13135            BinOp::Concat => Some("."),
13136            BinOp::StrEq => Some("eq"),
13137            BinOp::NumEq => Some("=="),
13138            BinOp::StrNe => Some("ne"),
13139            BinOp::NumNe => Some("!="),
13140            BinOp::StrLt => Some("lt"),
13141            BinOp::StrGt => Some("gt"),
13142            BinOp::StrLe => Some("le"),
13143            BinOp::StrGe => Some("ge"),
13144            BinOp::NumLt => Some("<"),
13145            BinOp::NumGt => Some(">"),
13146            BinOp::NumLe => Some("<="),
13147            BinOp::NumGe => Some(">="),
13148            BinOp::Spaceship => Some("<=>"),
13149            BinOp::StrCmp => Some("cmp"),
13150            _ => None,
13151        }
13152    }
13153
13154    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
13155    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
13156        map.get("").or_else(|| map.get("\"\""))
13157    }
13158
13159    /// String context for blessed objects with `overload '""'`.
13160    pub(crate) fn stringify_value(
13161        &mut self,
13162        v: PerlValue,
13163        line: usize,
13164    ) -> Result<String, FlowOrError> {
13165        if let Some(r) = self.try_overload_stringify(&v, line) {
13166            let pv = r?;
13167            return Ok(pv.to_string());
13168        }
13169        Ok(v.to_string())
13170    }
13171
13172    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
13173    pub(crate) fn perl_sprintf_stringify(
13174        &mut self,
13175        fmt: &str,
13176        args: &[PerlValue],
13177        line: usize,
13178    ) -> Result<String, FlowOrError> {
13179        // Step 1: build the output and collect any `%n` store-targets.
13180        let (out, pending_n) = {
13181            let mut stringify = |v: &PerlValue| -> Result<String, FlowOrError> {
13182                self.stringify_value(v.clone(), line)
13183            };
13184            perl_sprintf_format_full(fmt, args, &mut stringify)?
13185        };
13186        // Step 2: apply any `%n` writes through the proper scope path.
13187        for (target, count) in pending_n {
13188            self.assign_scalar_ref_deref(target, PerlValue::integer(count), line)?;
13189        }
13190        Ok(out)
13191    }
13192
13193    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
13194    pub(crate) fn render_format_template(
13195        &mut self,
13196        tmpl: &crate::format::FormatTemplate,
13197        line: usize,
13198    ) -> Result<String, FlowOrError> {
13199        use crate::format::{FormatRecord, PictureSegment};
13200        let mut buf = String::new();
13201        for rec in &tmpl.records {
13202            match rec {
13203                FormatRecord::Literal(s) => {
13204                    buf.push_str(s);
13205                    buf.push('\n');
13206                }
13207                FormatRecord::Picture { segments, exprs } => {
13208                    let mut vals: Vec<String> = Vec::new();
13209                    for e in exprs {
13210                        let v = self.eval_expr(e)?;
13211                        vals.push(self.stringify_value(v, line)?);
13212                    }
13213                    let mut vi = 0usize;
13214                    let mut line_out = String::new();
13215                    for seg in segments {
13216                        match seg {
13217                            PictureSegment::Literal(t) => line_out.push_str(t),
13218                            PictureSegment::Field {
13219                                width,
13220                                align,
13221                                kind: _,
13222                            } => {
13223                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
13224                                vi += 1;
13225                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
13226                            }
13227                        }
13228                    }
13229                    buf.push_str(line_out.trim_end());
13230                    buf.push('\n');
13231                }
13232            }
13233        }
13234        Ok(buf)
13235    }
13236
13237    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
13238    pub(crate) fn resolve_write_output_handle(
13239        &self,
13240        v: &PerlValue,
13241        line: usize,
13242    ) -> PerlResult<String> {
13243        if let Some(n) = v.as_io_handle_name() {
13244            let n = self.resolve_io_handle_name(&n);
13245            if self.is_bound_handle(&n) {
13246                return Ok(n);
13247            }
13248        }
13249        if let Some(s) = v.as_str() {
13250            if self.is_bound_handle(&s) {
13251                return Ok(self.resolve_io_handle_name(&s));
13252            }
13253        }
13254        let s = v.to_string();
13255        if self.is_bound_handle(&s) {
13256            return Ok(self.resolve_io_handle_name(&s));
13257        }
13258        Err(PerlError::runtime(
13259            format!("write: invalid or unopened filehandle {}", s),
13260            line,
13261        ))
13262    }
13263
13264    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
13265    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
13266    /// that handle like `write FH`.
13267    pub(crate) fn write_format_execute(
13268        &mut self,
13269        args: &[PerlValue],
13270        line: usize,
13271    ) -> PerlResult<PerlValue> {
13272        let handle_name = match args.len() {
13273            0 => self.default_print_handle.clone(),
13274            1 => self.resolve_write_output_handle(&args[0], line)?,
13275            _ => {
13276                return Err(PerlError::runtime("write: too many arguments", line));
13277            }
13278        };
13279        let pkg = self.current_package();
13280        let mut fmt_name = self.scope.get_scalar("~").to_string();
13281        if fmt_name.is_empty() {
13282            fmt_name = "STDOUT".to_string();
13283        }
13284        let key = format!("{}::{}", pkg, fmt_name);
13285        let tmpl = self
13286            .format_templates
13287            .get(&key)
13288            .map(Arc::clone)
13289            .ok_or_else(|| {
13290                PerlError::runtime(
13291                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
13292                    line,
13293                )
13294            })?;
13295        let out = self
13296            .render_format_template(&tmpl, line)
13297            .map_err(|e| match e {
13298                FlowOrError::Error(e) => e,
13299                FlowOrError::Flow(_) => PerlError::runtime("write: unexpected control flow", line),
13300            })?;
13301        self.write_formatted_print(handle_name.as_str(), &out, line)?;
13302        Ok(PerlValue::integer(1))
13303    }
13304
13305    pub(crate) fn try_overload_stringify(
13306        &mut self,
13307        v: &PerlValue,
13308        line: usize,
13309    ) -> Option<ExecResult> {
13310        // Native class instance: look for method named '""' or 'stringify'
13311        if let Some(c) = v.as_class_inst() {
13312            let method_name = c
13313                .def
13314                .method("stringify")
13315                .or_else(|| c.def.method("\"\""))
13316                .filter(|m| m.body.is_some())?;
13317            let body = method_name.body.clone().unwrap();
13318            let params = method_name.params.clone();
13319            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
13320        }
13321        let br = v.as_blessed_ref()?;
13322        let class = br.class.clone();
13323        let map = self.overload_table.get(&class)?;
13324        let sub_short = Self::overload_stringify_method(map)?;
13325        let fq = format!("{}::{}", class, sub_short);
13326        let sub = self.subs.get(&fq)?.clone();
13327        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
13328    }
13329
13330    /// Map overload operator key to native class method name.
13331    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
13332        match key {
13333            "+" => Some("op_add"),
13334            "-" => Some("op_sub"),
13335            "*" => Some("op_mul"),
13336            "/" => Some("op_div"),
13337            "%" => Some("op_mod"),
13338            "**" => Some("op_pow"),
13339            "." => Some("op_concat"),
13340            "==" => Some("op_eq"),
13341            "!=" => Some("op_ne"),
13342            "<" => Some("op_lt"),
13343            ">" => Some("op_gt"),
13344            "<=" => Some("op_le"),
13345            ">=" => Some("op_ge"),
13346            "<=>" => Some("op_spaceship"),
13347            "eq" => Some("op_str_eq"),
13348            "ne" => Some("op_str_ne"),
13349            "lt" => Some("op_str_lt"),
13350            "gt" => Some("op_str_gt"),
13351            "le" => Some("op_str_le"),
13352            "ge" => Some("op_str_ge"),
13353            "cmp" => Some("op_cmp"),
13354            _ => None,
13355        }
13356    }
13357
13358    pub(crate) fn try_overload_binop(
13359        &mut self,
13360        op: BinOp,
13361        lv: &PerlValue,
13362        rv: &PerlValue,
13363        line: usize,
13364    ) -> Option<ExecResult> {
13365        let key = Self::overload_key_for_binop(op)?;
13366        // Native class instance overloading
13367        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
13368            (Some(c.def.clone()), lv.clone(), rv.clone())
13369        } else if let Some(c) = rv.as_class_inst() {
13370            (Some(c.def.clone()), rv.clone(), lv.clone())
13371        } else {
13372            (None, lv.clone(), rv.clone())
13373        };
13374        if let Some(ref def) = ci_def {
13375            if let Some(method_name) = Self::overload_method_name_for_key(key) {
13376                if let Some((m, _)) = self.find_class_method(def, method_name) {
13377                    if let Some(ref body) = m.body {
13378                        let params = m.params.clone();
13379                        return Some(self.call_class_method(
13380                            body,
13381                            &params,
13382                            vec![invocant, other],
13383                            line,
13384                        ));
13385                    }
13386                }
13387            }
13388        }
13389        // Blessed ref overloading (existing path)
13390        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
13391            (br.class.clone(), lv.clone(), rv.clone())
13392        } else if let Some(br) = rv.as_blessed_ref() {
13393            (br.class.clone(), rv.clone(), lv.clone())
13394        } else {
13395            return None;
13396        };
13397        let map = self.overload_table.get(&class)?;
13398        let sub_short = if let Some(s) = map.get(key) {
13399            s.clone()
13400        } else if let Some(nm) = map.get("nomethod") {
13401            let fq = format!("{}::{}", class, nm);
13402            let sub = self.subs.get(&fq)?.clone();
13403            return Some(self.call_sub(
13404                &sub,
13405                vec![invocant, other, PerlValue::string(key.to_string())],
13406                WantarrayCtx::Scalar,
13407                line,
13408            ));
13409        } else {
13410            return None;
13411        };
13412        let fq = format!("{}::{}", class, sub_short);
13413        let sub = self.subs.get(&fq)?.clone();
13414        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
13415    }
13416
13417    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
13418    pub(crate) fn try_overload_unary_dispatch(
13419        &mut self,
13420        op_key: &str,
13421        val: &PerlValue,
13422        line: usize,
13423    ) -> Option<ExecResult> {
13424        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
13425        if let Some(c) = val.as_class_inst() {
13426            let method_name = match op_key {
13427                "neg" => "op_neg",
13428                "bool" => "op_bool",
13429                "abs" => "op_abs",
13430                "0+" => "op_numify",
13431                _ => return None,
13432            };
13433            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
13434                if let Some(ref body) = m.body {
13435                    let params = m.params.clone();
13436                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
13437                }
13438            }
13439            return None;
13440        }
13441        // Blessed ref path
13442        let br = val.as_blessed_ref()?;
13443        let class = br.class.clone();
13444        let map = self.overload_table.get(&class)?;
13445        if let Some(s) = map.get(op_key) {
13446            let fq = format!("{}::{}", class, s);
13447            let sub = self.subs.get(&fq)?.clone();
13448            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
13449        }
13450        if let Some(nm) = map.get("nomethod") {
13451            let fq = format!("{}::{}", class, nm);
13452            let sub = self.subs.get(&fq)?.clone();
13453            return Some(self.call_sub(
13454                &sub,
13455                vec![val.clone(), PerlValue::string(op_key.to_string())],
13456                WantarrayCtx::Scalar,
13457                line,
13458            ));
13459        }
13460        None
13461    }
13462
13463    #[inline]
13464    fn eval_binop(
13465        &mut self,
13466        op: BinOp,
13467        lv: &PerlValue,
13468        rv: &PerlValue,
13469        _line: usize,
13470    ) -> ExecResult {
13471        Ok(match op {
13472            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
13473            // Perl `+` is numeric addition only; string concatenation is `.`.
13474            BinOp::Add => {
13475                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13476                    PerlValue::integer(a.wrapping_add(b))
13477                } else {
13478                    PerlValue::float(lv.to_number() + rv.to_number())
13479                }
13480            }
13481            BinOp::Sub => {
13482                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13483                    PerlValue::integer(a.wrapping_sub(b))
13484                } else {
13485                    PerlValue::float(lv.to_number() - rv.to_number())
13486                }
13487            }
13488            BinOp::Mul => {
13489                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13490                    PerlValue::integer(a.wrapping_mul(b))
13491                } else {
13492                    PerlValue::float(lv.to_number() * rv.to_number())
13493                }
13494            }
13495            BinOp::Div => {
13496                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13497                    if b == 0 {
13498                        return Err(
13499                            PerlError::division_by_zero("Illegal division by zero", _line).into(),
13500                        );
13501                    }
13502                    if a % b == 0 {
13503                        PerlValue::integer(a / b)
13504                    } else {
13505                        PerlValue::float(a as f64 / b as f64)
13506                    }
13507                } else {
13508                    let d = rv.to_number();
13509                    if d == 0.0 {
13510                        return Err(
13511                            PerlError::division_by_zero("Illegal division by zero", _line).into(),
13512                        );
13513                    }
13514                    PerlValue::float(lv.to_number() / d)
13515                }
13516            }
13517            BinOp::Mod => {
13518                let d = rv.to_int();
13519                if d == 0 {
13520                    return Err(PerlError::division_by_zero("Illegal modulus zero", _line).into());
13521                }
13522                PerlValue::integer(crate::value::perl_mod_i64(lv.to_int(), d))
13523            }
13524            BinOp::Pow => {
13525                // Under `--compat` or `use bigint;`, `compat_pow` promotes
13526                // to `BigInt` on overflow; otherwise it falls back to f64
13527                // (matches Perl's default i64-overflow-to-NV behavior).
13528                if crate::compat_mode() || crate::bigint_pragma() {
13529                    crate::value::compat_pow(lv, rv)
13530                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13531                    let int_pow = (b >= 0)
13532                        .then(|| u32::try_from(b).ok())
13533                        .flatten()
13534                        .and_then(|bu| a.checked_pow(bu))
13535                        .map(PerlValue::integer);
13536                    int_pow.unwrap_or_else(|| PerlValue::float(lv.to_number().powf(rv.to_number())))
13537                } else {
13538                    PerlValue::float(lv.to_number().powf(rv.to_number()))
13539                }
13540            }
13541            BinOp::Concat => {
13542                let mut s = String::new();
13543                lv.append_to(&mut s);
13544                rv.append_to(&mut s);
13545                PerlValue::string(s)
13546            }
13547            BinOp::NumEq => {
13548                // Struct equality: compare all fields
13549                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
13550                    if a.def.name != b.def.name {
13551                        PerlValue::integer(0)
13552                    } else {
13553                        let av = a.get_values();
13554                        let bv = b.get_values();
13555                        let eq = av.len() == bv.len()
13556                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
13557                        PerlValue::integer(if eq { 1 } else { 0 })
13558                    }
13559                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13560                    PerlValue::integer(if a == b { 1 } else { 0 })
13561                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
13562                    // Stryke (non-compat) sugar: `==` falls back to string
13563                    // compare when both operands are non-numeric strings, so
13564                    // `"G" == "G"` is true (Perl's `0 == 0` numeric is also
13565                    // true here, but `"G" == "T"` is false in stryke vs
13566                    // also-true in Perl). See `Op::NumEq` in vm.rs.
13567                    PerlValue::integer(if lv.to_string() == rv.to_string() {
13568                        1
13569                    } else {
13570                        0
13571                    })
13572                } else {
13573                    PerlValue::integer(if lv.to_number() == rv.to_number() {
13574                        1
13575                    } else {
13576                        0
13577                    })
13578                }
13579            }
13580            BinOp::NumNe => {
13581                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13582                    PerlValue::integer(if a != b { 1 } else { 0 })
13583                } else if !crate::compat_mode() && both_non_numeric_strings_iv(lv, rv) {
13584                    PerlValue::integer(if lv.to_string() != rv.to_string() {
13585                        1
13586                    } else {
13587                        0
13588                    })
13589                } else {
13590                    PerlValue::integer(if lv.to_number() != rv.to_number() {
13591                        1
13592                    } else {
13593                        0
13594                    })
13595                }
13596            }
13597            BinOp::NumLt => {
13598                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13599                    PerlValue::integer(if a < b { 1 } else { 0 })
13600                } else {
13601                    PerlValue::integer(if lv.to_number() < rv.to_number() {
13602                        1
13603                    } else {
13604                        0
13605                    })
13606                }
13607            }
13608            BinOp::NumGt => {
13609                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13610                    PerlValue::integer(if a > b { 1 } else { 0 })
13611                } else {
13612                    PerlValue::integer(if lv.to_number() > rv.to_number() {
13613                        1
13614                    } else {
13615                        0
13616                    })
13617                }
13618            }
13619            BinOp::NumLe => {
13620                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13621                    PerlValue::integer(if a <= b { 1 } else { 0 })
13622                } else {
13623                    PerlValue::integer(if lv.to_number() <= rv.to_number() {
13624                        1
13625                    } else {
13626                        0
13627                    })
13628                }
13629            }
13630            BinOp::NumGe => {
13631                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13632                    PerlValue::integer(if a >= b { 1 } else { 0 })
13633                } else {
13634                    PerlValue::integer(if lv.to_number() >= rv.to_number() {
13635                        1
13636                    } else {
13637                        0
13638                    })
13639                }
13640            }
13641            BinOp::Spaceship => {
13642                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
13643                    PerlValue::integer(if a < b {
13644                        -1
13645                    } else if a > b {
13646                        1
13647                    } else {
13648                        0
13649                    })
13650                } else {
13651                    let a = lv.to_number();
13652                    let b = rv.to_number();
13653                    PerlValue::integer(if a < b {
13654                        -1
13655                    } else if a > b {
13656                        1
13657                    } else {
13658                        0
13659                    })
13660                }
13661            }
13662            BinOp::StrEq => PerlValue::integer(if lv.to_string() == rv.to_string() {
13663                1
13664            } else {
13665                0
13666            }),
13667            BinOp::StrNe => PerlValue::integer(if lv.to_string() != rv.to_string() {
13668                1
13669            } else {
13670                0
13671            }),
13672            BinOp::StrLt => PerlValue::integer(if lv.to_string() < rv.to_string() {
13673                1
13674            } else {
13675                0
13676            }),
13677            BinOp::StrGt => PerlValue::integer(if lv.to_string() > rv.to_string() {
13678                1
13679            } else {
13680                0
13681            }),
13682            BinOp::StrLe => PerlValue::integer(if lv.to_string() <= rv.to_string() {
13683                1
13684            } else {
13685                0
13686            }),
13687            BinOp::StrGe => PerlValue::integer(if lv.to_string() >= rv.to_string() {
13688                1
13689            } else {
13690                0
13691            }),
13692            BinOp::StrCmp => {
13693                let cmp = lv.to_string().cmp(&rv.to_string());
13694                PerlValue::integer(match cmp {
13695                    std::cmp::Ordering::Less => -1,
13696                    std::cmp::Ordering::Greater => 1,
13697                    std::cmp::Ordering::Equal => 0,
13698                })
13699            }
13700            BinOp::BitAnd => {
13701                if let Some(s) = crate::value::set_intersection(lv, rv) {
13702                    s
13703                } else {
13704                    PerlValue::integer(lv.to_int() & rv.to_int())
13705                }
13706            }
13707            BinOp::BitOr => {
13708                if let Some(s) = crate::value::set_union(lv, rv) {
13709                    s
13710                } else {
13711                    PerlValue::integer(lv.to_int() | rv.to_int())
13712                }
13713            }
13714            BinOp::BitXor => PerlValue::integer(lv.to_int() ^ rv.to_int()),
13715            BinOp::ShiftLeft => PerlValue::integer(lv.to_int() << rv.to_int()),
13716            BinOp::ShiftRight => PerlValue::integer(lv.to_int() >> rv.to_int()),
13717            // These should have been handled by short-circuit above
13718            BinOp::LogAnd
13719            | BinOp::LogOr
13720            | BinOp::DefinedOr
13721            | BinOp::LogAndWord
13722            | BinOp::LogOrWord => unreachable!(),
13723            BinOp::BindMatch | BinOp::BindNotMatch => {
13724                unreachable!("regex bind handled in eval_expr BinOp arm")
13725            }
13726        })
13727    }
13728
13729    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
13730    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
13731    /// length — that was silently wrong vs `perl`.
13732    fn err_modify_symbolic_aggregate_deref_inc_dec(
13733        kind: Sigil,
13734        is_pre: bool,
13735        is_inc: bool,
13736        line: usize,
13737    ) -> FlowOrError {
13738        let agg = match kind {
13739            Sigil::Array => "array",
13740            Sigil::Hash => "hash",
13741            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
13742        };
13743        let op = match (is_pre, is_inc) {
13744            (true, true) => "preincrement (++)",
13745            (true, false) => "predecrement (--)",
13746            (false, true) => "postincrement (++)",
13747            (false, false) => "postdecrement (--)",
13748        };
13749        FlowOrError::Error(PerlError::runtime(
13750            format!("Can't modify {agg} dereference in {op}"),
13751            line,
13752        ))
13753    }
13754
13755    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
13756    pub(crate) fn symbolic_scalar_ref_postfix(
13757        &mut self,
13758        ref_val: PerlValue,
13759        decrement: bool,
13760        line: usize,
13761    ) -> Result<PerlValue, FlowOrError> {
13762        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
13763        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
13764        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
13765        Ok(old)
13766    }
13767
13768    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
13769    /// [`Self::assign_value`] and the VM.
13770    pub(crate) fn assign_scalar_ref_deref(
13771        &mut self,
13772        ref_val: PerlValue,
13773        val: PerlValue,
13774        line: usize,
13775    ) -> ExecResult {
13776        if let Some(name) = ref_val.as_scalar_binding_name() {
13777            self.set_special_var(&name, &val)
13778                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13779            return Ok(PerlValue::UNDEF);
13780        }
13781        if let Some(r) = ref_val.as_scalar_ref() {
13782            *r.write() = val;
13783            return Ok(PerlValue::UNDEF);
13784        }
13785        // Plain primitive scalar value: under no-strict, perl symbolic-derefs
13786        // through the string. With `strict 'refs'`, emit perl's exact diagnostic.
13787        if ref_val.is_integer_like() || ref_val.is_float_like() || ref_val.is_string_like() {
13788            let s = ref_val.to_string();
13789            if self.strict_refs {
13790                return Err(PerlError::runtime(
13791                    format!(
13792                        "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
13793                        s
13794                    ),
13795                    line,
13796                )
13797                .into());
13798            }
13799            self.set_special_var(&s, &val)
13800                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13801            return Ok(PerlValue::UNDEF);
13802        }
13803        Err(PerlError::runtime("Can't assign to non-scalar reference", line).into())
13804    }
13805
13806    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
13807    pub(crate) fn assign_symbolic_array_ref_deref(
13808        &mut self,
13809        ref_val: PerlValue,
13810        val: PerlValue,
13811        line: usize,
13812    ) -> ExecResult {
13813        if let Some(a) = ref_val.as_array_ref() {
13814            *a.write() = val.to_list();
13815            return Ok(PerlValue::UNDEF);
13816        }
13817        if let Some(name) = ref_val.as_array_binding_name() {
13818            self.scope
13819                .set_array(&name, val.to_list())
13820                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13821            return Ok(PerlValue::UNDEF);
13822        }
13823        if let Some(s) = ref_val.as_str() {
13824            if self.strict_refs {
13825                return Err(PerlError::runtime(
13826                    format!(
13827                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
13828                        s
13829                    ),
13830                    line,
13831                )
13832                .into());
13833            }
13834            self.scope
13835                .set_array(&s, val.to_list())
13836                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13837            return Ok(PerlValue::UNDEF);
13838        }
13839        Err(PerlError::runtime("Can't assign to non-array reference", line).into())
13840    }
13841
13842    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
13843    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
13844    pub(crate) fn assign_symbolic_typeglob_ref_deref(
13845        &mut self,
13846        ref_val: PerlValue,
13847        val: PerlValue,
13848        line: usize,
13849    ) -> ExecResult {
13850        let lhs_name = if let Some(s) = ref_val.as_str() {
13851            if self.strict_refs {
13852                return Err(PerlError::runtime(
13853                    format!(
13854                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
13855                        s
13856                    ),
13857                    line,
13858                )
13859                .into());
13860            }
13861            s.to_string()
13862        } else {
13863            return Err(
13864                PerlError::runtime("Can't assign to non-glob symbolic reference", line).into(),
13865            );
13866        };
13867        let is_coderef = val.as_code_ref().is_some()
13868            || val
13869                .as_scalar_ref()
13870                .map(|r| r.read().as_code_ref().is_some())
13871                .unwrap_or(false);
13872        if is_coderef {
13873            return self.assign_typeglob_value(&lhs_name, val, line);
13874        }
13875        let rhs_key = val.to_string();
13876        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
13877            .map_err(FlowOrError::Error)?;
13878        Ok(PerlValue::UNDEF)
13879    }
13880
13881    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
13882    pub(crate) fn assign_symbolic_hash_ref_deref(
13883        &mut self,
13884        ref_val: PerlValue,
13885        val: PerlValue,
13886        line: usize,
13887    ) -> ExecResult {
13888        let items = val.to_list();
13889        let mut map = IndexMap::new();
13890        let mut i = 0;
13891        while i + 1 < items.len() {
13892            map.insert(items[i].to_string(), items[i + 1].clone());
13893            i += 2;
13894        }
13895        if let Some(h) = ref_val.as_hash_ref() {
13896            *h.write() = map;
13897            return Ok(PerlValue::UNDEF);
13898        }
13899        if let Some(name) = ref_val.as_hash_binding_name() {
13900            self.touch_env_hash(&name);
13901            self.scope
13902                .set_hash(&name, map)
13903                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13904            return Ok(PerlValue::UNDEF);
13905        }
13906        if let Some(s) = ref_val.as_str() {
13907            if self.strict_refs {
13908                return Err(PerlError::runtime(
13909                    format!(
13910                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
13911                        s
13912                    ),
13913                    line,
13914                )
13915                .into());
13916            }
13917            self.touch_env_hash(&s);
13918            self.scope
13919                .set_hash(&s, map)
13920                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13921            return Ok(PerlValue::UNDEF);
13922        }
13923        Err(PerlError::runtime("Can't assign to non-hash reference", line).into())
13924    }
13925
13926    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
13927    pub(crate) fn assign_arrow_hash_deref(
13928        &mut self,
13929        container: PerlValue,
13930        key: String,
13931        val: PerlValue,
13932        line: usize,
13933    ) -> ExecResult {
13934        if let Some(b) = container.as_blessed_ref() {
13935            let mut data = b.data.write();
13936            if let Some(r) = data.as_hash_ref() {
13937                r.write().insert(key, val);
13938                return Ok(PerlValue::UNDEF);
13939            }
13940            if let Some(mut map) = data.as_hash_map() {
13941                map.insert(key, val);
13942                *data = PerlValue::hash(map);
13943                return Ok(PerlValue::UNDEF);
13944            }
13945            return Err(PerlError::runtime("Can't assign into non-hash blessed ref", line).into());
13946        }
13947        if let Some(r) = container.as_hash_ref() {
13948            r.write().insert(key, val);
13949            return Ok(PerlValue::UNDEF);
13950        }
13951        if let Some(name) = container.as_hash_binding_name() {
13952            self.touch_env_hash(&name);
13953            self.scope
13954                .set_hash_element(&name, &key, val)
13955                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13956            return Ok(PerlValue::UNDEF);
13957        }
13958        Err(PerlError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
13959    }
13960
13961    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
13962    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
13963    pub(crate) fn eval_arrow_array_base(
13964        &mut self,
13965        expr: &Expr,
13966        _line: usize,
13967    ) -> Result<PerlValue, FlowOrError> {
13968        match &expr.kind {
13969            ExprKind::Deref {
13970                expr: inner,
13971                kind: Sigil::Array | Sigil::Scalar,
13972            } => self.eval_expr(inner),
13973            _ => self.eval_expr(expr),
13974        }
13975    }
13976
13977    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
13978    pub(crate) fn eval_arrow_hash_base(
13979        &mut self,
13980        expr: &Expr,
13981        _line: usize,
13982    ) -> Result<PerlValue, FlowOrError> {
13983        match &expr.kind {
13984            ExprKind::Deref {
13985                expr: inner,
13986                kind: Sigil::Scalar,
13987            } => self.eval_expr(inner),
13988            _ => self.eval_expr(expr),
13989        }
13990    }
13991
13992    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
13993    pub(crate) fn read_arrow_array_element(
13994        &self,
13995        container: PerlValue,
13996        idx: i64,
13997        line: usize,
13998    ) -> Result<PerlValue, FlowOrError> {
13999        if let Some(a) = container.as_array_ref() {
14000            let arr = a.read();
14001            let i = if idx < 0 {
14002                (arr.len() as i64 + idx) as usize
14003            } else {
14004                idx as usize
14005            };
14006            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
14007        }
14008        if let Some(name) = container.as_array_binding_name() {
14009            return Ok(self.scope.get_array_element(&name, idx));
14010        }
14011        if let Some(arr) = container.as_array_vec() {
14012            let i = if idx < 0 {
14013                (arr.len() as i64 + idx) as usize
14014            } else {
14015                idx as usize
14016            };
14017            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
14018        }
14019        // Blessed arrayref (e.g. `Pair`) — `pairs` returns blessed `Pair` objects that
14020        // can be indexed via `$_->[0]` / `$_->[1]`.
14021        if let Some(b) = container.as_blessed_ref() {
14022            let inner = b.data.read().clone();
14023            if let Some(a) = inner.as_array_ref() {
14024                let arr = a.read();
14025                let i = if idx < 0 {
14026                    (arr.len() as i64 + idx) as usize
14027                } else {
14028                    idx as usize
14029                };
14030                return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
14031            }
14032        }
14033        Err(PerlError::runtime("Can't use arrow deref on non-array-ref", line).into())
14034    }
14035
14036    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
14037    pub(crate) fn read_arrow_hash_element(
14038        &mut self,
14039        container: PerlValue,
14040        key: &str,
14041        line: usize,
14042    ) -> Result<PerlValue, FlowOrError> {
14043        if let Some(r) = container.as_hash_ref() {
14044            let h = r.read();
14045            return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
14046        }
14047        if let Some(name) = container.as_hash_binding_name() {
14048            self.touch_env_hash(&name);
14049            return Ok(self.scope.get_hash_element(&name, key));
14050        }
14051        if let Some(b) = container.as_blessed_ref() {
14052            let data = b.data.read();
14053            if let Some(v) = data.hash_get(key) {
14054                return Ok(v);
14055            }
14056            if let Some(r) = data.as_hash_ref() {
14057                let h = r.read();
14058                return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
14059            }
14060            return Err(PerlError::runtime(
14061                "Can't access hash field on non-hash blessed ref",
14062                line,
14063            )
14064            .into());
14065        }
14066        // Struct field access via hash deref syntax: $struct->{field}
14067        if let Some(s) = container.as_struct_inst() {
14068            if let Some(idx) = s.def.field_index(key) {
14069                return Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF));
14070            }
14071            return Err(PerlError::runtime(
14072                format!("struct {} has no field `{}`", s.def.name, key),
14073                line,
14074            )
14075            .into());
14076        }
14077        // Class instance field access via hash deref: $obj->{field}
14078        if let Some(c) = container.as_class_inst() {
14079            if let Some(idx) = c.def.field_index(key) {
14080                return Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF));
14081            }
14082            return Err(PerlError::runtime(
14083                format!("class {} has no field `{}`", c.def.name, key),
14084                line,
14085            )
14086            .into());
14087        }
14088        Err(PerlError::runtime("Can't use arrow deref on non-hash-ref", line).into())
14089    }
14090
14091    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
14092    pub(crate) fn arrow_array_postfix(
14093        &mut self,
14094        container: PerlValue,
14095        idx: i64,
14096        decrement: bool,
14097        line: usize,
14098    ) -> Result<PerlValue, FlowOrError> {
14099        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
14100        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14101        self.assign_arrow_array_deref(container, idx, new_val, line)?;
14102        Ok(old)
14103    }
14104
14105    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
14106    pub(crate) fn arrow_hash_postfix(
14107        &mut self,
14108        container: PerlValue,
14109        key: String,
14110        decrement: bool,
14111        line: usize,
14112    ) -> Result<PerlValue, FlowOrError> {
14113        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
14114        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
14115        self.assign_arrow_hash_deref(container, key, new_val, line)?;
14116        Ok(old)
14117    }
14118
14119    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` evaluation. If a nullary
14120    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
14121    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
14122    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
14123    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
14124    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
14125    /// subs" in use`).
14126    pub(crate) fn resolve_bareword_rvalue(
14127        &mut self,
14128        name: &str,
14129        want: WantarrayCtx,
14130        line: usize,
14131    ) -> Result<PerlValue, FlowOrError> {
14132        if name == "__PACKAGE__" {
14133            return Ok(PerlValue::string(self.current_package()));
14134        }
14135        if let Some(sub) = self.resolve_sub_by_name(name) {
14136            return self.call_sub(&sub, vec![], want, line);
14137        }
14138        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
14139        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
14140            return r.map_err(Into::into);
14141        }
14142        Ok(PerlValue::string(name.to_string()))
14143    }
14144
14145    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
14146    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
14147    /// compound / inc-dec / assign helpers below.
14148    pub(crate) fn arrow_array_slice_values(
14149        &mut self,
14150        container: PerlValue,
14151        indices: &[i64],
14152        line: usize,
14153    ) -> Result<PerlValue, FlowOrError> {
14154        let mut out = Vec::with_capacity(indices.len());
14155        for &idx in indices {
14156            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
14157            out.push(v);
14158        }
14159        Ok(PerlValue::array(out))
14160    }
14161
14162    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment for
14163    /// multi-index `ArrowDeref { Array, List }`. Shared by the VM
14164    /// [`crate::bytecode::Op::SetArrowArraySlice`].
14165    pub(crate) fn assign_arrow_array_slice(
14166        &mut self,
14167        container: PerlValue,
14168        indices: Vec<i64>,
14169        val: PerlValue,
14170        line: usize,
14171    ) -> Result<PerlValue, FlowOrError> {
14172        if indices.is_empty() {
14173            return Err(PerlError::runtime("assign to empty array slice", line).into());
14174        }
14175        let vals = val.to_list();
14176        for (i, idx) in indices.iter().enumerate() {
14177            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
14178            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
14179        }
14180        Ok(PerlValue::UNDEF)
14181    }
14182
14183    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
14184    pub(crate) fn flatten_array_slice_index_specs(
14185        &mut self,
14186        indices: &[Expr],
14187    ) -> Result<Vec<i64>, FlowOrError> {
14188        let mut out = Vec::new();
14189        for idx_expr in indices {
14190            let v = if matches!(
14191                idx_expr.kind,
14192                ExprKind::Range { .. } | ExprKind::SliceRange { .. }
14193            ) {
14194                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
14195            } else {
14196                self.eval_expr(idx_expr)?
14197            };
14198            if let Some(list) = v.as_array_vec() {
14199                for idx in list {
14200                    out.push(idx.to_int());
14201                }
14202            } else {
14203                out.push(v.to_int());
14204            }
14205        }
14206        Ok(out)
14207    }
14208
14209    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
14210    pub(crate) fn assign_named_array_slice(
14211        &mut self,
14212        stash_array_name: &str,
14213        indices: Vec<i64>,
14214        val: PerlValue,
14215        line: usize,
14216    ) -> Result<PerlValue, FlowOrError> {
14217        if indices.is_empty() {
14218            return Err(PerlError::runtime("assign to empty array slice", line).into());
14219        }
14220        let vals = val.to_list();
14221        for (i, idx) in indices.iter().enumerate() {
14222            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
14223            self.scope
14224                .set_array_element(stash_array_name, *idx, v)
14225                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14226        }
14227        Ok(PerlValue::UNDEF)
14228    }
14229
14230    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
14231    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
14232    pub(crate) fn compound_assign_arrow_array_slice(
14233        &mut self,
14234        container: PerlValue,
14235        indices: Vec<i64>,
14236        op: BinOp,
14237        rhs: PerlValue,
14238        line: usize,
14239    ) -> Result<PerlValue, FlowOrError> {
14240        if indices.is_empty() {
14241            return Err(PerlError::runtime("assign to empty array slice", line).into());
14242        }
14243        let last_idx = *indices.last().expect("non-empty indices");
14244        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
14245        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
14246        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
14247        Ok(new_val)
14248    }
14249
14250    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
14251    /// pre forms return the new value, post forms return the old **last** element.
14252    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
14253    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
14254    pub(crate) fn arrow_array_slice_inc_dec(
14255        &mut self,
14256        container: PerlValue,
14257        indices: Vec<i64>,
14258        kind: u8,
14259        line: usize,
14260    ) -> Result<PerlValue, FlowOrError> {
14261        if indices.is_empty() {
14262            return Err(
14263                PerlError::runtime("array slice increment needs at least one index", line).into(),
14264            );
14265        }
14266        let last_idx = *indices.last().expect("non-empty indices");
14267        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
14268        let new_val = if kind & 1 == 0 {
14269            PerlValue::integer(last_old.to_int() + 1)
14270        } else {
14271            PerlValue::integer(last_old.to_int() - 1)
14272        };
14273        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
14274        Ok(if kind < 2 { new_val } else { last_old })
14275    }
14276
14277    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
14278    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
14279    pub(crate) fn named_array_slice_inc_dec(
14280        &mut self,
14281        stash_array_name: &str,
14282        indices: Vec<i64>,
14283        kind: u8,
14284        line: usize,
14285    ) -> Result<PerlValue, FlowOrError> {
14286        let last_idx = *indices.last().ok_or_else(|| {
14287            PerlError::runtime("array slice increment needs at least one index", line)
14288        })?;
14289        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
14290        let new_val = if kind & 1 == 0 {
14291            PerlValue::integer(last_old.to_int() + 1)
14292        } else {
14293            PerlValue::integer(last_old.to_int() - 1)
14294        };
14295        self.scope
14296            .set_array_element(stash_array_name, last_idx, new_val.clone())
14297            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14298        Ok(if kind < 2 { new_val } else { last_old })
14299    }
14300
14301    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
14302    pub(crate) fn compound_assign_named_array_slice(
14303        &mut self,
14304        stash_array_name: &str,
14305        indices: Vec<i64>,
14306        op: BinOp,
14307        rhs: PerlValue,
14308        line: usize,
14309    ) -> Result<PerlValue, FlowOrError> {
14310        if indices.is_empty() {
14311            return Err(PerlError::runtime("assign to empty array slice", line).into());
14312        }
14313        let last_idx = *indices.last().expect("non-empty indices");
14314        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
14315        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
14316        self.scope
14317            .set_array_element(stash_array_name, last_idx, new_val.clone())
14318            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14319        Ok(new_val)
14320    }
14321
14322    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
14323    pub(crate) fn assign_arrow_array_deref(
14324        &mut self,
14325        container: PerlValue,
14326        idx: i64,
14327        val: PerlValue,
14328        line: usize,
14329    ) -> ExecResult {
14330        if let Some(a) = container.as_array_ref() {
14331            let mut arr = a.write();
14332            let i = if idx < 0 {
14333                (arr.len() as i64 + idx) as usize
14334            } else {
14335                idx as usize
14336            };
14337            if i >= arr.len() {
14338                arr.resize(i + 1, PerlValue::UNDEF);
14339            }
14340            arr[i] = val;
14341            return Ok(PerlValue::UNDEF);
14342        }
14343        if let Some(name) = container.as_array_binding_name() {
14344            self.scope
14345                .set_array_element(&name, idx, val)
14346                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
14347            return Ok(PerlValue::UNDEF);
14348        }
14349        Err(PerlError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
14350    }
14351
14352    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
14353    pub(crate) fn assign_typeglob_value(
14354        &mut self,
14355        name: &str,
14356        val: PerlValue,
14357        line: usize,
14358    ) -> ExecResult {
14359        let sub = if let Some(c) = val.as_code_ref() {
14360            Some(c)
14361        } else if let Some(r) = val.as_scalar_ref() {
14362            r.read().as_code_ref().map(|c| Arc::clone(&c))
14363        } else {
14364            None
14365        };
14366        if let Some(sub) = sub {
14367            let lhs_sub = self.qualify_typeglob_sub_key(name);
14368            self.subs.insert(lhs_sub, sub);
14369            return Ok(PerlValue::UNDEF);
14370        }
14371        Err(PerlError::runtime(
14372            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
14373            line,
14374        )
14375        .into())
14376    }
14377
14378    fn assign_value(&mut self, target: &Expr, val: PerlValue) -> ExecResult {
14379        match &target.kind {
14380            // `substr($s, $o, $l) = $rhs` — equivalent to the 4-arg form
14381            // `substr($s, $o, $l, $rhs)`. Evaluate the offset/length
14382            // sub-exprs, splice the new substring into the target, then
14383            // recursively call assign_value to write the modified string
14384            // through the original `string` lvalue.
14385            ExprKind::Substr {
14386                string,
14387                offset,
14388                length,
14389                replacement: None,
14390            } => {
14391                let s = self.eval_expr(string)?.to_string();
14392                let off = self.eval_expr(offset)?.to_int();
14393                let start = if off < 0 {
14394                    (s.len() as i64 + off).max(0) as usize
14395                } else {
14396                    (off as usize).min(s.len())
14397                };
14398                let len = if let Some(l) = length {
14399                    let lv = self.eval_expr(l)?.to_int();
14400                    if lv < 0 {
14401                        let remaining = s.len().saturating_sub(start) as i64;
14402                        (remaining + lv).max(0) as usize
14403                    } else {
14404                        lv as usize
14405                    }
14406                } else {
14407                    s.len().saturating_sub(start)
14408                };
14409                let end = start.saturating_add(len).min(s.len());
14410                let mut new_s = String::with_capacity(s.len());
14411                new_s.push_str(&s[..start]);
14412                new_s.push_str(&val.to_string());
14413                new_s.push_str(&s[end..]);
14414                self.assign_value(string, PerlValue::string(new_s))?;
14415                Ok(PerlValue::UNDEF)
14416            }
14417            // `(my $copy = $orig) =~ s/.../.../` — at this point the
14418            // MyExpr has already been evaluated as a side-effect of
14419            // `eval_expr(target)` upstream (so `$copy` has been declared
14420            // and initialized). The substitution / transliteration helpers
14421            // call back here to write the *new* string. Bind through the
14422            // declared name without re-running the initializer.
14423            ExprKind::MyExpr { decls, .. } => {
14424                let first = decls.first().ok_or_else(|| {
14425                    FlowOrError::Error(PerlError::runtime(
14426                        "assign_value: empty MyExpr decl list",
14427                        target.line,
14428                    ))
14429                })?;
14430                match first.sigil {
14431                    Sigil::Scalar => {
14432                        let stor = self.tree_scalar_storage_name(&first.name);
14433                        self.set_special_var(&stor, &val)
14434                            .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
14435                        Ok(PerlValue::UNDEF)
14436                    }
14437                    Sigil::Array => {
14438                        self.scope.set_array(&first.name, val.to_list())?;
14439                        Ok(PerlValue::UNDEF)
14440                    }
14441                    Sigil::Hash => {
14442                        let items = val.to_list();
14443                        let mut map = IndexMap::new();
14444                        let mut i = 0;
14445                        while i + 1 < items.len() {
14446                            map.insert(items[i].to_string(), items[i + 1].clone());
14447                            i += 2;
14448                        }
14449                        self.scope.set_hash(&first.name, map)?;
14450                        Ok(PerlValue::UNDEF)
14451                    }
14452                    Sigil::Typeglob => Ok(PerlValue::UNDEF),
14453                }
14454            }
14455            ExprKind::ScalarVar(name) => {
14456                let stor = self.tree_scalar_storage_name(name);
14457                if self.scope.is_scalar_frozen(&stor) {
14458                    return Err(FlowOrError::Error(PerlError::runtime(
14459                        format!("Modification of a frozen value: ${}", name),
14460                        target.line,
14461                    )));
14462                }
14463                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
14464                    let class = obj
14465                        .as_blessed_ref()
14466                        .map(|b| b.class.clone())
14467                        .unwrap_or_default();
14468                    let full = format!("{}::STORE", class);
14469                    if let Some(sub) = self.subs.get(&full).cloned() {
14470                        let arg_vals = vec![obj, val];
14471                        return match self.call_sub(
14472                            &sub,
14473                            arg_vals,
14474                            WantarrayCtx::Scalar,
14475                            target.line,
14476                        ) {
14477                            Ok(_) => Ok(PerlValue::UNDEF),
14478                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
14479                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
14480                        };
14481                    }
14482                }
14483                self.set_special_var(&stor, &val)
14484                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
14485                Ok(PerlValue::UNDEF)
14486            }
14487            ExprKind::ArrayVar(name) => {
14488                if self.scope.is_array_frozen(name) {
14489                    return Err(PerlError::runtime(
14490                        format!("Modification of a frozen value: @{}", name),
14491                        target.line,
14492                    )
14493                    .into());
14494                }
14495                if self.strict_vars
14496                    && !name.contains("::")
14497                    && !self.scope.array_binding_exists(name)
14498                {
14499                    return Err(PerlError::runtime(
14500                        format!(
14501                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
14502                            name, name
14503                        ),
14504                        target.line,
14505                    )
14506                    .into());
14507                }
14508                self.scope.set_array(name, val.to_list())?;
14509                Ok(PerlValue::UNDEF)
14510            }
14511            ExprKind::HashVar(name) => {
14512                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
14513                {
14514                    return Err(PerlError::runtime(
14515                        format!(
14516                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
14517                            name, name
14518                        ),
14519                        target.line,
14520                    )
14521                    .into());
14522                }
14523                let items = val.to_list();
14524                let mut map = IndexMap::new();
14525                let mut i = 0;
14526                while i + 1 < items.len() {
14527                    map.insert(items[i].to_string(), items[i + 1].clone());
14528                    i += 2;
14529                }
14530                self.scope.set_hash(name, map)?;
14531                Ok(PerlValue::UNDEF)
14532            }
14533            ExprKind::ArrayElement { array, index } => {
14534                if self.strict_vars
14535                    && !array.contains("::")
14536                    && !self.scope.array_binding_exists(array)
14537                {
14538                    return Err(PerlError::runtime(
14539                        format!(
14540                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
14541                            array, array
14542                        ),
14543                        target.line,
14544                    )
14545                    .into());
14546                }
14547                if self.scope.is_array_frozen(array) {
14548                    return Err(PerlError::runtime(
14549                        format!("Modification of a frozen value: @{}", array),
14550                        target.line,
14551                    )
14552                    .into());
14553                }
14554                let idx = self.eval_expr(index)?.to_int();
14555                let aname = self.stash_array_name_for_package(array);
14556                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
14557                    let class = obj
14558                        .as_blessed_ref()
14559                        .map(|b| b.class.clone())
14560                        .unwrap_or_default();
14561                    let full = format!("{}::STORE", class);
14562                    if let Some(sub) = self.subs.get(&full).cloned() {
14563                        let arg_vals = vec![obj, PerlValue::integer(idx), val];
14564                        return match self.call_sub(
14565                            &sub,
14566                            arg_vals,
14567                            WantarrayCtx::Scalar,
14568                            target.line,
14569                        ) {
14570                            Ok(_) => Ok(PerlValue::UNDEF),
14571                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
14572                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
14573                        };
14574                    }
14575                }
14576                self.scope.set_array_element(&aname, idx, val)?;
14577                Ok(PerlValue::UNDEF)
14578            }
14579            ExprKind::ArraySlice { array, indices } => {
14580                if indices.is_empty() {
14581                    return Err(
14582                        PerlError::runtime("assign to empty array slice", target.line).into(),
14583                    );
14584                }
14585                self.check_strict_array_var(array, target.line)?;
14586                if self.scope.is_array_frozen(array) {
14587                    return Err(PerlError::runtime(
14588                        format!("Modification of a frozen value: @{}", array),
14589                        target.line,
14590                    )
14591                    .into());
14592                }
14593                let aname = self.stash_array_name_for_package(array);
14594                let flat = self.flatten_array_slice_index_specs(indices)?;
14595                self.assign_named_array_slice(&aname, flat, val, target.line)
14596            }
14597            ExprKind::HashElement { hash, key } => {
14598                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
14599                {
14600                    return Err(PerlError::runtime(
14601                        format!(
14602                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
14603                            hash, hash
14604                        ),
14605                        target.line,
14606                    )
14607                    .into());
14608                }
14609                if self.scope.is_hash_frozen(hash) {
14610                    return Err(PerlError::runtime(
14611                        format!("Modification of a frozen value: %%{}", hash),
14612                        target.line,
14613                    )
14614                    .into());
14615                }
14616                let k = self.eval_expr(key)?.to_string();
14617                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
14618                    let class = obj
14619                        .as_blessed_ref()
14620                        .map(|b| b.class.clone())
14621                        .unwrap_or_default();
14622                    let full = format!("{}::STORE", class);
14623                    if let Some(sub) = self.subs.get(&full).cloned() {
14624                        let arg_vals = vec![obj, PerlValue::string(k), val];
14625                        return match self.call_sub(
14626                            &sub,
14627                            arg_vals,
14628                            WantarrayCtx::Scalar,
14629                            target.line,
14630                        ) {
14631                            Ok(_) => Ok(PerlValue::UNDEF),
14632                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
14633                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
14634                        };
14635                    }
14636                }
14637                self.scope.set_hash_element(hash, &k, val)?;
14638                Ok(PerlValue::UNDEF)
14639            }
14640            ExprKind::HashSlice { hash, keys } => {
14641                if keys.is_empty() {
14642                    return Err(
14643                        PerlError::runtime("assign to empty hash slice", target.line).into(),
14644                    );
14645                }
14646                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
14647                {
14648                    return Err(PerlError::runtime(
14649                        format!(
14650                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
14651                            hash, hash
14652                        ),
14653                        target.line,
14654                    )
14655                    .into());
14656                }
14657                if self.scope.is_hash_frozen(hash) {
14658                    return Err(PerlError::runtime(
14659                        format!("Modification of a frozen value: %%{}", hash),
14660                        target.line,
14661                    )
14662                    .into());
14663                }
14664                let mut key_vals = Vec::with_capacity(keys.len());
14665                for key_expr in keys {
14666                    let v = if matches!(
14667                        key_expr.kind,
14668                        ExprKind::Range { .. } | ExprKind::SliceRange { .. }
14669                    ) {
14670                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
14671                    } else {
14672                        self.eval_expr(key_expr)?
14673                    };
14674                    key_vals.push(v);
14675                }
14676                self.assign_named_hash_slice(hash, key_vals, val, target.line)
14677            }
14678            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
14679            ExprKind::TypeglobExpr(e) => {
14680                let name = self.eval_expr(e)?.to_string();
14681                let synthetic = Expr {
14682                    kind: ExprKind::Typeglob(name),
14683                    line: target.line,
14684                };
14685                self.assign_value(&synthetic, val)
14686            }
14687            ExprKind::AnonymousListSlice { source, indices } => {
14688                if let ExprKind::Deref {
14689                    expr: inner,
14690                    kind: Sigil::Array,
14691                } = &source.kind
14692                {
14693                    let container = self.eval_arrow_array_base(inner, target.line)?;
14694                    let vals = val.to_list();
14695                    let n = indices.len().min(vals.len());
14696                    for i in 0..n {
14697                        let idx = self.eval_expr(&indices[i])?.to_int();
14698                        self.assign_arrow_array_deref(
14699                            container.clone(),
14700                            idx,
14701                            vals[i].clone(),
14702                            target.line,
14703                        )?;
14704                    }
14705                    return Ok(PerlValue::UNDEF);
14706                }
14707                Err(
14708                    PerlError::runtime("assign to list slice: unsupported base", target.line)
14709                        .into(),
14710                )
14711            }
14712            ExprKind::ArrowDeref {
14713                expr,
14714                index,
14715                kind: DerefKind::Hash,
14716            } => {
14717                let key = self.eval_expr(index)?.to_string();
14718                let container = self.eval_expr(expr)?;
14719                self.assign_arrow_hash_deref(container, key, val, target.line)
14720            }
14721            ExprKind::ArrowDeref {
14722                expr,
14723                index,
14724                kind: DerefKind::Array,
14725            } => {
14726                let container = self.eval_arrow_array_base(expr, target.line)?;
14727                if let ExprKind::List(indices) = &index.kind {
14728                    let vals = val.to_list();
14729                    let n = indices.len().min(vals.len());
14730                    for i in 0..n {
14731                        let idx = self.eval_expr(&indices[i])?.to_int();
14732                        self.assign_arrow_array_deref(
14733                            container.clone(),
14734                            idx,
14735                            vals[i].clone(),
14736                            target.line,
14737                        )?;
14738                    }
14739                    return Ok(PerlValue::UNDEF);
14740                }
14741                let idx = self.eval_expr(index)?.to_int();
14742                self.assign_arrow_array_deref(container, idx, val, target.line)
14743            }
14744            ExprKind::HashSliceDeref { container, keys } => {
14745                let href = self.eval_expr(container)?;
14746                let mut key_vals = Vec::with_capacity(keys.len());
14747                for key_expr in keys {
14748                    key_vals.push(self.eval_expr(key_expr)?);
14749                }
14750                self.assign_hash_slice_deref(href, key_vals, val, target.line)
14751            }
14752            ExprKind::Deref {
14753                expr,
14754                kind: Sigil::Scalar,
14755            } => {
14756                let ref_val = self.eval_expr(expr)?;
14757                self.assign_scalar_ref_deref(ref_val, val, target.line)
14758            }
14759            ExprKind::Deref {
14760                expr,
14761                kind: Sigil::Array,
14762            } => {
14763                let ref_val = self.eval_expr(expr)?;
14764                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
14765            }
14766            ExprKind::Deref {
14767                expr,
14768                kind: Sigil::Hash,
14769            } => {
14770                let ref_val = self.eval_expr(expr)?;
14771                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
14772            }
14773            ExprKind::Deref {
14774                expr,
14775                kind: Sigil::Typeglob,
14776            } => {
14777                let ref_val = self.eval_expr(expr)?;
14778                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
14779            }
14780            ExprKind::Pos(inner) => {
14781                let key = match inner {
14782                    None => "_".to_string(),
14783                    Some(expr) => match &expr.kind {
14784                        ExprKind::ScalarVar(n) => n.clone(),
14785                        _ => self.eval_expr(expr)?.to_string(),
14786                    },
14787                };
14788                if val.is_undef() {
14789                    self.regex_pos.insert(key, None);
14790                } else {
14791                    let u = val.to_int().max(0) as usize;
14792                    self.regex_pos.insert(key, Some(u));
14793                }
14794                Ok(PerlValue::UNDEF)
14795            }
14796            // List assignment: `($a, $b, ...) = (val1, val2, ...)`
14797            // RHS is already fully evaluated — distribute elements to targets.
14798            ExprKind::List(targets) => {
14799                let items = val.to_list();
14800                for (i, t) in targets.iter().enumerate() {
14801                    let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
14802                    self.assign_value(t, v)?;
14803                }
14804                Ok(PerlValue::UNDEF)
14805            }
14806            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
14807            // write the substitution result back to the assignment target.
14808            ExprKind::Assign { target, .. } => self.assign_value(target, val),
14809            _ => Ok(PerlValue::UNDEF),
14810        }
14811    }
14812
14813    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
14814    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
14815        (name.starts_with('#') && name.len() > 1)
14816            || name.starts_with('^')
14817            || matches!(
14818                name,
14819                "$$" | "0"
14820                    | "!"
14821                    | "@"
14822                    | "/"
14823                    | "\\"
14824                    | ","
14825                    | "."
14826                    | "]"
14827                    | ";"
14828                    | "ARGV"
14829                    | "^I"
14830                    | "^D"
14831                    | "^P"
14832                    | "^S"
14833                    | "^W"
14834                    | "^O"
14835                    | "^T"
14836                    | "^V"
14837                    | "^E"
14838                    | "^H"
14839                    | "^WARNING_BITS"
14840                    | "^GLOBAL_PHASE"
14841                    | "^MATCH"
14842                    | "^PREMATCH"
14843                    | "^POSTMATCH"
14844                    | "^LAST_SUBMATCH_RESULT"
14845                    | "<"
14846                    | ">"
14847                    | "("
14848                    | ")"
14849                    | "?"
14850                    | "|"
14851                    | "\""
14852                    | "+"
14853                    | "%"
14854                    | "="
14855                    | "-"
14856                    | ":"
14857                    | "*"
14858                    | "INC"
14859            )
14860            || crate::english::is_known_alias(name)
14861    }
14862
14863    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
14864    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
14865    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
14866    /// [`Self::english_no_match_vars`] is set.
14867    #[inline]
14868    /// English alias resolution + `our`/`oursync` package qualification in one call.
14869    /// Returns the storage key the scope expects: `$ARG` → `_`, then `our $x` → `Pkg::x`.
14870    /// Use this for compound ops (`++`, `--`, `+=`, `||=`, etc.) so atomic-RMW lookups
14871    /// hit the package-qualified cell stored by `oursync`.
14872    pub(crate) fn resolved_scalar_storage_name(&self, name: &str) -> String {
14873        self.tree_scalar_storage_name(self.english_scalar_name(name))
14874    }
14875
14876    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
14877        if !self.english_enabled {
14878            return name;
14879        }
14880        if self
14881            .english_lexical_scalars
14882            .iter()
14883            .any(|s| s.contains(name))
14884        {
14885            return name;
14886        }
14887        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
14888            return short;
14889        }
14890        name
14891    }
14892
14893    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
14894    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
14895        // `$#name = N` resizes `@name` (Perl: setting the last index). The
14896        // bare-set path stores under literal `#name` as a separate scalar
14897        // and silently does nothing useful — match the read-side handling
14898        // by routing through `set_special_var`.
14899        (name.starts_with('#') && name.len() > 1)
14900            || name.starts_with('^')
14901            || matches!(
14902                name,
14903                "0" | "/"
14904                    | "\\"
14905                    | ","
14906                    | ";"
14907                    | "\""
14908                    | "%"
14909                    | "="
14910                    | "-"
14911                    | ":"
14912                    | "*"
14913                    | "INC"
14914                    | "^I"
14915                    | "^D"
14916                    | "^P"
14917                    | "^W"
14918                    | "^H"
14919                    | "^WARNING_BITS"
14920                    | "$$"
14921                    | "]"
14922                    | "^S"
14923                    | "ARGV"
14924                    | "|"
14925                    | "+"
14926                    | "?"
14927                    | "!"
14928                    | "@"
14929                    | "."
14930            )
14931            || crate::english::is_known_alias(name)
14932    }
14933
14934    pub(crate) fn get_special_var(&self, name: &str) -> PerlValue {
14935        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
14936        let name = if !crate::compat_mode() {
14937            match name {
14938                "NR" => ".",
14939                "RS" => "/",
14940                "OFS" => ",",
14941                "ORS" => "\\",
14942                "NF" => {
14943                    let len = self.scope.array_len("F");
14944                    return PerlValue::integer(len as i64);
14945                }
14946                _ => self.english_scalar_name(name),
14947            }
14948        } else {
14949            self.english_scalar_name(name)
14950        };
14951        match name {
14952            "$$" => PerlValue::integer(std::process::id() as i64),
14953            "_" => self.scope.get_scalar("_"),
14954            "^MATCH" => PerlValue::string(self.last_match.clone()),
14955            "^PREMATCH" => PerlValue::string(self.prematch.clone()),
14956            "^POSTMATCH" => PerlValue::string(self.postmatch.clone()),
14957            "^LAST_SUBMATCH_RESULT" => PerlValue::string(self.last_paren_match.clone()),
14958            "0" => PerlValue::string(self.program_name.clone()),
14959            "!" => PerlValue::errno_dual(self.errno_code, self.errno.clone()),
14960            "@" => {
14961                if let Some(ref v) = self.eval_error_value {
14962                    v.clone()
14963                } else {
14964                    PerlValue::errno_dual(self.eval_error_code, self.eval_error.clone())
14965                }
14966            }
14967            "/" => match &self.irs {
14968                Some(s) => PerlValue::string(s.clone()),
14969                None => PerlValue::UNDEF,
14970            },
14971            "\\" => PerlValue::string(self.ors.clone()),
14972            "," => PerlValue::string(self.ofs.clone()),
14973            "." => {
14974                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
14975                if self.last_readline_handle.is_empty() {
14976                    if self.line_number == 0 {
14977                        PerlValue::UNDEF
14978                    } else {
14979                        PerlValue::integer(self.line_number)
14980                    }
14981                } else {
14982                    PerlValue::integer(
14983                        *self
14984                            .handle_line_numbers
14985                            .get(&self.last_readline_handle)
14986                            .unwrap_or(&0),
14987                    )
14988                }
14989            }
14990            "]" => PerlValue::float(perl_bracket_version()),
14991            ";" => PerlValue::string(self.subscript_sep.clone()),
14992            "ARGV" => PerlValue::string(self.argv_current_file.clone()),
14993            "^I" => PerlValue::string(self.inplace_edit.clone()),
14994            "^D" => PerlValue::integer(self.debug_flags),
14995            "^P" => PerlValue::integer(self.perl_debug_flags),
14996            "^S" => PerlValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
14997            "^W" => PerlValue::integer(if self.warnings { 1 } else { 0 }),
14998            "^O" => PerlValue::string(perl_osname()),
14999            "^T" => PerlValue::integer(self.script_start_time),
15000            "^V" => PerlValue::string(perl_version_v_string()),
15001            "^E" => PerlValue::string(extended_os_error_string()),
15002            "^H" => PerlValue::integer(self.compile_hints),
15003            "^WARNING_BITS" => PerlValue::integer(self.warning_bits),
15004            "^GLOBAL_PHASE" => PerlValue::string(self.global_phase.clone()),
15005            "<" | ">" => PerlValue::integer(unix_id_for_special(name)),
15006            "(" | ")" => PerlValue::string(unix_group_list_for_special(name)),
15007            "?" => PerlValue::integer(self.child_exit_status),
15008            "|" => PerlValue::integer(if self.output_autoflush { 1 } else { 0 }),
15009            "\"" => PerlValue::string(self.list_separator.clone()),
15010            "+" => PerlValue::string(self.last_paren_match.clone()),
15011            "%" => PerlValue::integer(self.format_page_number),
15012            "=" => PerlValue::integer(self.format_lines_per_page),
15013            "-" => PerlValue::integer(self.format_lines_left),
15014            ":" => PerlValue::string(self.format_line_break_chars.clone()),
15015            "*" => PerlValue::integer(if self.multiline_match { 1 } else { 0 }),
15016            "^" => PerlValue::string(self.format_top_name.clone()),
15017            "INC" => PerlValue::integer(self.inc_hook_index),
15018            "^A" => PerlValue::string(self.accumulator_format.clone()),
15019            "^C" => PerlValue::integer(if self.sigint_pending_caret.replace(false) {
15020                1
15021            } else {
15022                0
15023            }),
15024            "^F" => PerlValue::integer(self.max_system_fd),
15025            "^L" => PerlValue::string(self.formfeed_string.clone()),
15026            "^M" => PerlValue::string(self.emergency_memory.clone()),
15027            "^N" => PerlValue::string(self.last_subpattern_name.clone()),
15028            "^X" => PerlValue::string(self.executable_path.clone()),
15029            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
15030            "^TAINT" | "^TAINTED" => PerlValue::integer(0),
15031            "^UNICODE" => PerlValue::integer(if self.utf8_pragma { 1 } else { 0 }),
15032            "^OPEN" => PerlValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
15033            "^UTF8LOCALE" => PerlValue::integer(0),
15034            "^UTF8CACHE" => PerlValue::integer(-1),
15035            _ if name.starts_with('^') && name.len() > 1 => self
15036                .special_caret_scalars
15037                .get(name)
15038                .cloned()
15039                .unwrap_or(PerlValue::UNDEF),
15040            _ if name.starts_with('#') && name.len() > 1 => {
15041                let arr = &name[1..];
15042                let aname = self.stash_array_name_for_package(arr);
15043                let len = self.scope.array_len(&aname);
15044                PerlValue::integer(len as i64 - 1)
15045            }
15046            _ => self.scope.get_scalar(name),
15047        }
15048    }
15049
15050    pub(crate) fn set_special_var(&mut self, name: &str, val: &PerlValue) -> Result<(), PerlError> {
15051        let name = self.english_scalar_name(name);
15052        match name {
15053            "!" => {
15054                let code = val.to_int() as i32;
15055                self.errno_code = code;
15056                self.errno = if code == 0 {
15057                    String::new()
15058                } else {
15059                    std::io::Error::from_raw_os_error(code).to_string()
15060                };
15061            }
15062            "@" => {
15063                if let Some((code, msg)) = val.errno_dual_parts() {
15064                    self.eval_error_code = code;
15065                    self.eval_error = msg;
15066                } else {
15067                    self.eval_error = val.to_string();
15068                    let mut code = val.to_int() as i32;
15069                    if code == 0 && !self.eval_error.is_empty() {
15070                        code = 1;
15071                    }
15072                    self.eval_error_code = code;
15073                }
15074            }
15075            "." => {
15076                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
15077                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
15078                let n = val.to_int();
15079                if self.last_readline_handle.is_empty() {
15080                    self.line_number = n;
15081                } else {
15082                    self.handle_line_numbers
15083                        .insert(self.last_readline_handle.clone(), n);
15084                }
15085            }
15086            "0" => self.program_name = val.to_string(),
15087            "/" => {
15088                self.irs = if val.is_undef() {
15089                    None
15090                } else {
15091                    Some(val.to_string())
15092                }
15093            }
15094            "\\" => self.ors = val.to_string(),
15095            "," => self.ofs = val.to_string(),
15096            ";" => self.subscript_sep = val.to_string(),
15097            "\"" => self.list_separator = val.to_string(),
15098            "%" => self.format_page_number = val.to_int(),
15099            "=" => self.format_lines_per_page = val.to_int(),
15100            "-" => self.format_lines_left = val.to_int(),
15101            ":" => self.format_line_break_chars = val.to_string(),
15102            "*" => self.multiline_match = val.to_int() != 0,
15103            "^" => self.format_top_name = val.to_string(),
15104            "INC" => self.inc_hook_index = val.to_int(),
15105            "^A" => self.accumulator_format = val.to_string(),
15106            "^F" => self.max_system_fd = val.to_int(),
15107            "^L" => self.formfeed_string = val.to_string(),
15108            "^M" => self.emergency_memory = val.to_string(),
15109            "^I" => self.inplace_edit = val.to_string(),
15110            "^D" => self.debug_flags = val.to_int(),
15111            "^P" => self.perl_debug_flags = val.to_int(),
15112            "^W" => self.warnings = val.to_int() != 0,
15113            "^H" => self.compile_hints = val.to_int(),
15114            "^WARNING_BITS" => self.warning_bits = val.to_int(),
15115            "|" => {
15116                self.output_autoflush = val.to_int() != 0;
15117                if self.output_autoflush {
15118                    let _ = io::stdout().flush();
15119                }
15120            }
15121            // Read-only or pid-backed
15122            "$$"
15123            | "]"
15124            | "^S"
15125            | "ARGV"
15126            | "?"
15127            | "^O"
15128            | "^T"
15129            | "^V"
15130            | "^E"
15131            | "^GLOBAL_PHASE"
15132            | "^MATCH"
15133            | "^PREMATCH"
15134            | "^POSTMATCH"
15135            | "^LAST_SUBMATCH_RESULT"
15136            | "^C"
15137            | "^N"
15138            | "^X"
15139            | "^TAINT"
15140            | "^TAINTED"
15141            | "^UNICODE"
15142            | "^UTF8LOCALE"
15143            | "^UTF8CACHE"
15144            | "+"
15145            | "<"
15146            | ">"
15147            | "("
15148            | ")" => {}
15149            _ if name.starts_with('^') && name.len() > 1 => {
15150                self.special_caret_scalars
15151                    .insert(name.to_string(), val.clone());
15152            }
15153            _ if name.starts_with('#') && name.len() > 1 => {
15154                // `$#name = N` resizes `@name` to length `N + 1`. Truncates
15155                // when N < current_last_idx, extends with `undef` otherwise.
15156                let arr = &name[1..];
15157                let aname = self.stash_array_name_for_package(arr);
15158                let new_last = val.to_int();
15159                let new_len = if new_last < 0 {
15160                    0
15161                } else {
15162                    (new_last as usize) + 1
15163                };
15164                let mut current = self.scope.get_array(&aname);
15165                current.resize(new_len, PerlValue::UNDEF);
15166                self.scope.set_array(&aname, current)?;
15167            }
15168            _ => self.scope.set_scalar(name, val.clone())?,
15169        }
15170        Ok(())
15171    }
15172
15173    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
15174        match &expr.kind {
15175            ExprKind::ArrayVar(name) => Ok(name.clone()),
15176            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
15177            _ => Err(PerlError::runtime("Expected array", expr.line).into()),
15178        }
15179    }
15180
15181    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
15182    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
15183        match &expr.kind {
15184            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
15185            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
15186            _ => expr,
15187        }
15188    }
15189
15190    /// `@$aref` / `@{...}` after optional peeling — for `SpliceExpr` / `pop` operations.
15191    fn try_eval_array_deref_container(
15192        &mut self,
15193        expr: &Expr,
15194    ) -> Result<Option<PerlValue>, FlowOrError> {
15195        let e = Self::peel_array_builtin_operand(expr);
15196        if let ExprKind::Deref {
15197            expr: inner,
15198            kind: Sigil::Array,
15199        } = &e.kind
15200        {
15201            return Ok(Some(self.eval_or_autoviv_array_ref(inner)?));
15202        }
15203        Ok(None)
15204    }
15205
15206    /// Evaluate `inner` and return an array ref, auto-vivifying when the result is undef
15207    /// and `inner` denotes a writable lvalue (scalar var, hash element, array element).
15208    /// Mirrors Perl 5: `push @{$h{k}}, $x` creates `$h{k}` as an arrayref on demand.
15209    fn eval_or_autoviv_array_ref(&mut self, inner: &Expr) -> Result<PerlValue, FlowOrError> {
15210        let line = inner.line;
15211        let val = self.eval_expr(inner)?;
15212        if !val.is_undef() {
15213            return Ok(val);
15214        }
15215        let new_ref = PerlValue::array_ref(Arc::new(RwLock::new(Vec::new())));
15216        match &inner.kind {
15217            ExprKind::ScalarVar(name) => {
15218                self.scope
15219                    .set_scalar(name, new_ref.clone())
15220                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15221                Ok(new_ref)
15222            }
15223            ExprKind::HashElement { hash, key } => {
15224                let k = self.eval_expr(key)?.to_string();
15225                self.scope
15226                    .set_hash_element(hash, &k, new_ref.clone())
15227                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15228                Ok(new_ref)
15229            }
15230            ExprKind::ArrayElement { array, index } => {
15231                let i = self.eval_expr(index)?.to_int();
15232                self.scope
15233                    .set_array_element(array, i, new_ref.clone())
15234                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
15235                Ok(new_ref)
15236            }
15237            _ => Ok(val),
15238        }
15239    }
15240
15241    /// Current package (`main` when `__PACKAGE__` is unset or empty).
15242    fn current_package(&self) -> String {
15243        let s = self.scope.get_scalar("__PACKAGE__").to_string();
15244        if s.is_empty() {
15245            "main".to_string()
15246        } else {
15247            s
15248        }
15249    }
15250
15251    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
15252    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
15253    pub(crate) fn package_version_scalar(
15254        &mut self,
15255        package: &str,
15256    ) -> PerlResult<Option<PerlValue>> {
15257        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
15258        let _ = self
15259            .scope
15260            .set_scalar("__PACKAGE__", PerlValue::string(package.to_string()));
15261        let ver = self.get_special_var("VERSION");
15262        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
15263        Ok(if ver.is_undef() { None } else { Some(ver) })
15264    }
15265
15266    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
15267    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<PerlSub>> {
15268        let root = if start_package.is_empty() {
15269            "main"
15270        } else {
15271            start_package
15272        };
15273        for pkg in self.mro_linearize(root) {
15274            let key = if pkg == "main" {
15275                "AUTOLOAD".to_string()
15276            } else {
15277                format!("{}::AUTOLOAD", pkg)
15278            };
15279            if let Some(s) = self.subs.get(&key) {
15280                return Some(s.clone());
15281            }
15282        }
15283        None
15284    }
15285
15286    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
15287    /// qualified missing sub or method name and invoke the handler (same argument list as the
15288    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
15289    /// the package prefix of the missing name (or current package).
15290    pub(crate) fn try_autoload_call(
15291        &mut self,
15292        missing_name: &str,
15293        args: Vec<PerlValue>,
15294        line: usize,
15295        want: WantarrayCtx,
15296        method_invocant_class: Option<&str>,
15297    ) -> Option<ExecResult> {
15298        let pkg = self.current_package();
15299        let full = if missing_name.contains("::") {
15300            missing_name.to_string()
15301        } else {
15302            format!("{}::{}", pkg, missing_name)
15303        };
15304        let start_pkg = method_invocant_class.unwrap_or_else(|| {
15305            full.rsplit_once("::")
15306                .map(|(p, _)| p)
15307                .filter(|p| !p.is_empty())
15308                .unwrap_or("main")
15309        });
15310        let sub = self.resolve_autoload_sub(start_pkg)?;
15311        if let Err(e) = self
15312            .scope
15313            .set_scalar("AUTOLOAD", PerlValue::string(full.clone()))
15314        {
15315            return Some(Err(e.into()));
15316        }
15317        Some(self.call_sub(&sub, args, want, line))
15318    }
15319
15320    pub(crate) fn with_topic_default_args(&self, args: Vec<PerlValue>) -> Vec<PerlValue> {
15321        if args.is_empty() {
15322            vec![self.scope.get_scalar("_").clone()]
15323        } else {
15324            args
15325        }
15326    }
15327
15328    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
15329    /// and [`crate::bytecode::Op::IndirectCall`].
15330    pub(crate) fn dispatch_indirect_call(
15331        &mut self,
15332        target: PerlValue,
15333        arg_vals: Vec<PerlValue>,
15334        want: WantarrayCtx,
15335        line: usize,
15336    ) -> ExecResult {
15337        if let Some(sub) = target.as_code_ref() {
15338            return self.call_sub(&sub, arg_vals, want, line);
15339        }
15340        if let Some(name) = target.as_str() {
15341            return self.call_named_sub(&name, arg_vals, line, want);
15342        }
15343        Err(PerlError::runtime("Can't use non-code reference as a subroutine", line).into())
15344    }
15345
15346    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
15347    /// Bare-name dispatch for stryke list builtins (`sum`, `min`, `uniq`, `reduce`, `zip`, …).
15348    /// Resolves short aliases (`uq`, `shuf`, `chk`, `win`, `fst`, `rd`, `med`, `std`, `var`, …)
15349    /// and forwards to [`crate::list_builtins::dispatch_by_name`].
15350    pub(crate) fn call_bare_list_builtin(
15351        &mut self,
15352        name: &str,
15353        args: Vec<PerlValue>,
15354        line: usize,
15355        want: WantarrayCtx,
15356    ) -> ExecResult {
15357        let canonical = match name {
15358            "distinct" | "uq" => "uniq",
15359            "shuf" => "shuffle",
15360            "chk" => "chunked",
15361            "win" => "windowed",
15362            "zp" => "zip",
15363            "fst" => "first",
15364            "rd" => "reduce",
15365            "med" => "median",
15366            "std" => "stddev",
15367            "var" => "variance",
15368            other => other,
15369        };
15370        // List builtins like `sum`, `min`, `uniq` operate on a list — an empty
15371        // input must aggregate to the identity (0/undef), NOT default to $_.
15372        // `sum(@empty_after_grep)` was returning $_ before this; that produced
15373        // surprising results downstream (e.g. `… |> grep {0} |> sum` = topic).
15374        match crate::list_builtins::dispatch_by_name(self, canonical, &args, want) {
15375            Some(r) => r,
15376            None => Err(PerlError::runtime(
15377                format!("internal: not a stryke list builtin: {name}"),
15378                line,
15379            )
15380            .into()),
15381        }
15382    }
15383
15384    fn call_named_sub(
15385        &mut self,
15386        name: &str,
15387        args: Vec<PerlValue>,
15388        line: usize,
15389        want: WantarrayCtx,
15390    ) -> ExecResult {
15391        if let Some(sub) = self.resolve_sub_by_name(name) {
15392            let args = self.with_topic_default_args(args);
15393            // The sub's home package is the qualifier from the resolved registry key.
15394            // `PerlSub.name` itself may be bare; pass an explicit override so call_sub can
15395            // switch `__PACKAGE__` for cross-package `our`/`oursync` qualification.
15396            let pkg = name.rsplit_once("::").map(|(p, _)| p.to_string());
15397            return self.call_sub_with_package(&sub, args, want, line, pkg);
15398        }
15399        match name {
15400            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
15401            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
15402            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
15403            | "none" | "notall" | "first" | "fst" | "reduce" | "rd" | "reductions" | "sum"
15404            | "sum0" | "product" | "min" | "max" | "minstr" | "maxstr" | "mean" | "median"
15405            | "med" | "mode" | "stddev" | "std" | "variance" | "var" | "pairs" | "unpairs"
15406            | "pairkeys" | "pairvalues" | "pairgrep" | "pairmap" | "pairfirst" | "blessed"
15407            | "refaddr" | "reftype" | "weaken" | "unweaken" | "isweak" | "set_subname"
15408            | "subname" | "unicode_to_native" => {
15409                self.call_bare_list_builtin(name, args, line, want)
15410            }
15411            "deque" => {
15412                if !args.is_empty() {
15413                    return Err(PerlError::runtime("deque() takes no arguments", line).into());
15414                }
15415                Ok(PerlValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
15416            }
15417            "defer__internal" => {
15418                if args.len() != 1 {
15419                    return Err(PerlError::runtime(
15420                        "defer__internal expects one coderef argument",
15421                        line,
15422                    )
15423                    .into());
15424                }
15425                self.scope.push_defer(args[0].clone());
15426                Ok(PerlValue::UNDEF)
15427            }
15428            "heap" => {
15429                if args.len() != 1 {
15430                    return Err(
15431                        PerlError::runtime("heap() expects one comparator sub", line).into(),
15432                    );
15433                }
15434                if let Some(sub) = args[0].as_code_ref() {
15435                    Ok(PerlValue::heap(Arc::new(Mutex::new(PerlHeap {
15436                        items: Vec::new(),
15437                        cmp: Arc::clone(&sub),
15438                    }))))
15439                } else {
15440                    Err(PerlError::runtime("heap() requires a code reference", line).into())
15441                }
15442            }
15443            "pipeline" => {
15444                let mut items = Vec::new();
15445                for v in args {
15446                    if let Some(a) = v.as_array_vec() {
15447                        items.extend(a);
15448                    } else {
15449                        items.push(v);
15450                    }
15451                }
15452                Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15453                    source: items,
15454                    ops: Vec::new(),
15455                    has_scalar_terminal: false,
15456                    par_stream: false,
15457                    streaming: false,
15458                    streaming_workers: 0,
15459                    streaming_buffer: 256,
15460                }))))
15461            }
15462            "par_pipeline" => {
15463                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
15464                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
15465                        .map_err(Into::into);
15466                }
15467                Ok(self.builtin_par_pipeline_stream(&args, line)?)
15468            }
15469            "par_pipeline_stream" => {
15470                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
15471                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
15472                        .map_err(Into::into);
15473                }
15474                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
15475            }
15476            "ppool" => {
15477                if args.len() != 1 {
15478                    return Err(PerlError::runtime(
15479                        "ppool() expects one argument (worker count)",
15480                        line,
15481                    )
15482                    .into());
15483                }
15484                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
15485            }
15486            "barrier" => {
15487                if args.len() != 1 {
15488                    return Err(PerlError::runtime(
15489                        "barrier() expects one argument (party count)",
15490                        line,
15491                    )
15492                    .into());
15493                }
15494                let n = args[0].to_int().max(1) as usize;
15495                Ok(PerlValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
15496            }
15497            "cluster" => {
15498                let items = if args.len() == 1 {
15499                    args[0].to_list()
15500                } else {
15501                    args.to_vec()
15502                };
15503                let c = RemoteCluster::from_list_args(&items)
15504                    .map_err(|msg| PerlError::runtime(msg, line))?;
15505                Ok(PerlValue::remote_cluster(Arc::new(c)))
15506            }
15507            _ => {
15508                // Late static binding: static::method() resolves to runtime class of $self
15509                if let Some(method_name) = name.strip_prefix("static::") {
15510                    let self_val = self.scope.get_scalar("self");
15511                    if let Some(c) = self_val.as_class_inst() {
15512                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
15513                            if let Some(ref body) = m.body {
15514                                let params = m.params.clone();
15515                                let mut call_args = vec![self_val.clone()];
15516                                call_args.extend(args);
15517                                return match self.call_class_method(body, &params, call_args, line)
15518                                {
15519                                    Ok(v) => Ok(v),
15520                                    Err(FlowOrError::Error(e)) => Err(e.into()),
15521                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15522                                    Err(e) => Err(e),
15523                                };
15524                            }
15525                        }
15526                        return Err(PerlError::runtime(
15527                            format!(
15528                                "static::{} — method not found on class {}",
15529                                method_name, c.def.name
15530                            ),
15531                            line,
15532                        )
15533                        .into());
15534                    }
15535                    return Err(PerlError::runtime(
15536                        "static:: can only be used inside a class method",
15537                        line,
15538                    )
15539                    .into());
15540                }
15541                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
15542                if let Some(def) = self.struct_defs.get(name).cloned() {
15543                    return self.struct_construct(&def, args, line);
15544                }
15545                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
15546                if let Some(def) = self.class_defs.get(name).cloned() {
15547                    return self.class_construct(&def, args, line);
15548                }
15549                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
15550                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
15551                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
15552                        return self.enum_construct(&def, variant_name, args, line);
15553                    }
15554                }
15555                // Check for static class method or static field: Math::add(...) / Counter::count()
15556                if let Some((class_name, member_name)) = name.rsplit_once("::") {
15557                    if let Some(def) = self.class_defs.get(class_name).cloned() {
15558                        // Static method
15559                        if let Some(m) = def.method(member_name) {
15560                            if m.is_static {
15561                                if let Some(ref body) = m.body {
15562                                    let params = m.params.clone();
15563                                    return match self.call_static_class_method(
15564                                        body,
15565                                        &params,
15566                                        args.clone(),
15567                                        line,
15568                                    ) {
15569                                        Ok(v) => Ok(v),
15570                                        Err(FlowOrError::Error(e)) => Err(e.into()),
15571                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15572                                        Err(e) => Err(e),
15573                                    };
15574                                }
15575                            }
15576                        }
15577                        // Static field access: getter (0 args) or setter (1 arg)
15578                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
15579                            let key = format!("{}::{}", class_name, member_name);
15580                            match args.len() {
15581                                0 => {
15582                                    let val = self.scope.get_scalar(&key);
15583                                    return Ok(val);
15584                                }
15585                                1 => {
15586                                    let _ = self.scope.set_scalar(&key, args[0].clone());
15587                                    return Ok(args[0].clone());
15588                                }
15589                                _ => {
15590                                    return Err(PerlError::runtime(
15591                                        format!(
15592                                            "static field `{}::{}` takes 0 or 1 arguments",
15593                                            class_name, member_name
15594                                        ),
15595                                        line,
15596                                    )
15597                                    .into());
15598                                }
15599                            }
15600                        }
15601                    }
15602                }
15603                let args = self.with_topic_default_args(args);
15604                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
15605                    return r;
15606                }
15607                Err(PerlError::runtime(self.undefined_subroutine_call_message(name), line).into())
15608            }
15609        }
15610    }
15611
15612    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
15613    pub(crate) fn struct_construct(
15614        &mut self,
15615        def: &Arc<StructDef>,
15616        args: Vec<PerlValue>,
15617        line: usize,
15618    ) -> ExecResult {
15619        // Detect if args are named (key => value pairs) or positional
15620        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
15621        let is_named = args.len() >= 2
15622            && args.len().is_multiple_of(2)
15623            && args.iter().step_by(2).all(|v| {
15624                let s = v.to_string();
15625                def.field_index(&s).is_some()
15626            });
15627
15628        let provided = if is_named {
15629            // Named construction: Point(x => 1, y => 2)
15630            let mut pairs = Vec::new();
15631            let mut i = 0;
15632            while i + 1 < args.len() {
15633                let k = args[i].to_string();
15634                let v = args[i + 1].clone();
15635                pairs.push((k, v));
15636                i += 2;
15637            }
15638            pairs
15639        } else {
15640            // Positional construction: Point(1, 2) fills fields in declaration order
15641            def.fields
15642                .iter()
15643                .zip(args.iter())
15644                .map(|(f, v)| (f.name.clone(), v.clone()))
15645                .collect()
15646        };
15647
15648        // Evaluate default expressions
15649        let mut defaults = Vec::with_capacity(def.fields.len());
15650        for field in &def.fields {
15651            if let Some(ref expr) = field.default {
15652                let val = self.eval_expr(expr)?;
15653                defaults.push(Some(val));
15654            } else {
15655                defaults.push(None);
15656            }
15657        }
15658
15659        Ok(crate::native_data::struct_new_with_defaults(
15660            def, &provided, &defaults, line,
15661        )?)
15662    }
15663
15664    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
15665    pub(crate) fn class_construct(
15666        &mut self,
15667        def: &Arc<ClassDef>,
15668        args: Vec<PerlValue>,
15669        _line: usize,
15670    ) -> ExecResult {
15671        use crate::value::ClassInstance;
15672
15673        // Prevent instantiation of abstract classes
15674        if def.is_abstract {
15675            return Err(PerlError::runtime(
15676                format!("cannot instantiate abstract class `{}`", def.name),
15677                _line,
15678            )
15679            .into());
15680        }
15681
15682        // Collect all fields from inheritance chain (parent fields first)
15683        let all_fields = self.collect_class_fields(def);
15684
15685        // Check if args are named
15686        let is_named = args.len() >= 2
15687            && args.len().is_multiple_of(2)
15688            && args.iter().step_by(2).all(|v| {
15689                let s = v.to_string();
15690                all_fields.iter().any(|(name, _, _)| name == &s)
15691            });
15692
15693        let provided: Vec<(String, PerlValue)> = if is_named {
15694            let mut pairs = Vec::new();
15695            let mut i = 0;
15696            while i + 1 < args.len() {
15697                let k = args[i].to_string();
15698                let v = args[i + 1].clone();
15699                pairs.push((k, v));
15700                i += 2;
15701            }
15702            pairs
15703        } else {
15704            all_fields
15705                .iter()
15706                .zip(args.iter())
15707                .map(|((name, _, _), v)| (name.clone(), v.clone()))
15708                .collect()
15709        };
15710
15711        // Build values array for all fields (inherited + own) with type checking
15712        let mut values = Vec::with_capacity(all_fields.len());
15713        for (name, default, ty) in &all_fields {
15714            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
15715                val.clone()
15716            } else if let Some(ref expr) = default {
15717                self.eval_expr(expr)?
15718            } else {
15719                PerlValue::UNDEF
15720            };
15721            ty.check_value(&val).map_err(|msg| {
15722                PerlError::type_error(
15723                    format!("class {} field `{}`: {}", def.name, name, msg),
15724                    _line,
15725                )
15726            })?;
15727            values.push(val);
15728        }
15729
15730        // Compute full ISA chain for type checking
15731        let isa_chain = self.mro_linearize(&def.name);
15732        let instance = PerlValue::class_inst(Arc::new(ClassInstance::new_with_isa(
15733            Arc::clone(def),
15734            values,
15735            isa_chain,
15736        )));
15737
15738        // Call BUILD hooks: parent BUILD first, then child BUILD
15739        let build_chain = self.collect_build_chain(def);
15740        if !build_chain.is_empty() {
15741            for (body, params) in &build_chain {
15742                let call_args = vec![instance.clone()];
15743                match self.call_class_method(body, params, call_args, _line) {
15744                    Ok(_) => {}
15745                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
15746                    Err(e) => return Err(e),
15747                }
15748            }
15749        }
15750
15751        Ok(instance)
15752    }
15753
15754    /// Collect BUILD methods from parent to child order.
15755    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
15756        let mut chain = Vec::new();
15757        // Parent BUILD first
15758        for parent_name in &def.extends {
15759            if let Some(parent_def) = self.class_defs.get(parent_name) {
15760                chain.extend(self.collect_build_chain(parent_def));
15761            }
15762        }
15763        // Own BUILD
15764        if let Some(m) = def.method("BUILD") {
15765            if let Some(ref body) = m.body {
15766                chain.push((body.clone(), m.params.clone()));
15767            }
15768        }
15769        chain
15770    }
15771
15772    /// Recursively flatten class/struct instances and the hashes/arrays
15773    /// they contain into a plain hashref tree. Atoms (numbers, strings,
15774    /// undef, code refs, regex refs, blessed-non-hash refs, …) round-trip
15775    /// unchanged. Used by `$obj->to_hash_rec` for both class and struct
15776    /// receivers.
15777    pub(crate) fn deep_to_hash_value(&self, v: &PerlValue) -> PerlValue {
15778        // Class instance: hashref of fields, recursing into each value.
15779        if let Some(c) = v.as_class_inst() {
15780            let all_fields = self.collect_class_fields_full(&c.def);
15781            let values = c.get_values();
15782            let mut map = IndexMap::new();
15783            for (i, (name, _, _, _, _)) in all_fields.iter().enumerate() {
15784                if let Some(elem) = values.get(i) {
15785                    map.insert(name.clone(), self.deep_to_hash_value(elem));
15786                }
15787            }
15788            return PerlValue::hash_ref(Arc::new(RwLock::new(map)));
15789        }
15790        // Struct instance: same shape, declaration order.
15791        if let Some(s) = v.as_struct_inst() {
15792            let values = s.get_values();
15793            let mut map = IndexMap::new();
15794            for (i, field) in s.def.fields.iter().enumerate() {
15795                if let Some(elem) = values.get(i) {
15796                    map.insert(field.name.clone(), self.deep_to_hash_value(elem));
15797                }
15798            }
15799            return PerlValue::hash_ref(Arc::new(RwLock::new(map)));
15800        }
15801        // Hashref: clone keys, recurse into values.
15802        if let Some(r) = v.as_hash_ref() {
15803            let inner = r.read().clone();
15804            let mut map = IndexMap::new();
15805            for (k, val) in inner.into_iter() {
15806                map.insert(k, self.deep_to_hash_value(&val));
15807            }
15808            return PerlValue::hash_ref(Arc::new(RwLock::new(map)));
15809        }
15810        // Arrayref: recurse into elements.
15811        if let Some(r) = v.as_array_ref() {
15812            let inner = r.read().clone();
15813            let out: Vec<PerlValue> = inner.iter().map(|e| self.deep_to_hash_value(e)).collect();
15814            return PerlValue::array_ref(Arc::new(RwLock::new(out)));
15815        }
15816        // Everything else (scalars, blessed refs, code refs, enums, …)
15817        // round-trips unchanged. Enum instances stringify naturally
15818        // through their existing `Display` so callers see a stable name.
15819        v.clone()
15820    }
15821
15822    /// Collect all fields from a class and its parent hierarchy (parent fields first).
15823    /// Returns (name, default, type, visibility, owning_class_name).
15824    fn collect_class_fields(
15825        &self,
15826        def: &ClassDef,
15827    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
15828        self.collect_class_fields_full(def)
15829            .into_iter()
15830            .map(|(name, default, ty, _, _)| (name, default, ty))
15831            .collect()
15832    }
15833
15834    /// Like collect_class_fields but includes visibility and owning class name.
15835    fn collect_class_fields_full(
15836        &self,
15837        def: &ClassDef,
15838    ) -> Vec<(
15839        String,
15840        Option<Expr>,
15841        crate::ast::PerlTypeName,
15842        crate::ast::Visibility,
15843        String,
15844    )> {
15845        let mut all_fields = Vec::new();
15846
15847        for parent_name in &def.extends {
15848            if let Some(parent_def) = self.class_defs.get(parent_name) {
15849                let parent_fields = self.collect_class_fields_full(parent_def);
15850                all_fields.extend(parent_fields);
15851            }
15852        }
15853
15854        for field in &def.fields {
15855            all_fields.push((
15856                field.name.clone(),
15857                field.default.clone(),
15858                field.ty.clone(),
15859                field.visibility,
15860                def.name.clone(),
15861            ));
15862        }
15863
15864        all_fields
15865    }
15866
15867    /// Collect all method names from class and parents (deduplicates, child overrides parent).
15868    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
15869        // Parent methods first
15870        for parent_name in &def.extends {
15871            if let Some(parent_def) = self.class_defs.get(parent_name) {
15872                self.collect_class_method_names(parent_def, names);
15873            }
15874        }
15875        // Own methods (add if not already present — child overrides parent name)
15876        for m in &def.methods {
15877            if !m.is_static && !names.contains(&m.name) {
15878                names.push(m.name.clone());
15879            }
15880        }
15881    }
15882
15883    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
15884    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
15885        let mut chain = Vec::new();
15886        // Own DESTROY first
15887        if let Some(m) = def.method("DESTROY") {
15888            if let Some(ref body) = m.body {
15889                chain.push((body.clone(), m.params.clone()));
15890            }
15891        }
15892        // Then parent DESTROY
15893        for parent_name in &def.extends {
15894            if let Some(parent_def) = self.class_defs.get(parent_name) {
15895                chain.extend(self.collect_destroy_chain(parent_def));
15896            }
15897        }
15898        chain
15899    }
15900
15901    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
15902    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
15903        if let Some(def) = self.class_defs.get(child) {
15904            for parent in &def.extends {
15905                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
15906                    return true;
15907                }
15908            }
15909        }
15910        false
15911    }
15912
15913    /// Find a method in a class or its parent hierarchy (child methods override parent).
15914    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
15915        // First check the current class
15916        if let Some(m) = def.method(method) {
15917            return Some((m.clone(), def.name.clone()));
15918        }
15919        // Then check parent classes
15920        for parent_name in &def.extends {
15921            if let Some(parent_def) = self.class_defs.get(parent_name) {
15922                if let Some(result) = self.find_class_method(parent_def, method) {
15923                    return Some(result);
15924                }
15925            }
15926        }
15927        None
15928    }
15929
15930    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
15931    pub(crate) fn enum_construct(
15932        &mut self,
15933        def: &Arc<EnumDef>,
15934        variant_name: &str,
15935        args: Vec<PerlValue>,
15936        line: usize,
15937    ) -> ExecResult {
15938        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
15939            FlowOrError::Error(PerlError::runtime(
15940                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
15941                line,
15942            ))
15943        })?;
15944        let variant = &def.variants[variant_idx];
15945        let data = if variant.ty.is_some() {
15946            if args.is_empty() {
15947                return Err(PerlError::runtime(
15948                    format!(
15949                        "enum variant `{}::{}` requires data",
15950                        def.name, variant_name
15951                    ),
15952                    line,
15953                )
15954                .into());
15955            }
15956            if args.len() == 1 {
15957                args.into_iter().next().unwrap()
15958            } else {
15959                PerlValue::array(args)
15960            }
15961        } else {
15962            if !args.is_empty() {
15963                return Err(PerlError::runtime(
15964                    format!(
15965                        "enum variant `{}::{}` does not take data",
15966                        def.name, variant_name
15967                    ),
15968                    line,
15969                )
15970                .into());
15971            }
15972            PerlValue::UNDEF
15973        };
15974        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
15975        Ok(PerlValue::enum_inst(Arc::new(inst)))
15976    }
15977
15978    /// True if `name` is a registered or standard process-global handle.
15979    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
15980        matches!(name, "STDIN" | "STDOUT" | "STDERR")
15981            || self.input_handles.contains_key(name)
15982            || self.output_handles.contains_key(name)
15983            || self.io_file_slots.contains_key(name)
15984            || self.pipe_children.contains_key(name)
15985    }
15986
15987    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
15988    pub(crate) fn io_handle_method(
15989        &mut self,
15990        name: &str,
15991        method: &str,
15992        args: &[PerlValue],
15993        line: usize,
15994    ) -> PerlResult<PerlValue> {
15995        match method {
15996            "print" => self.io_handle_print(name, args, false, line),
15997            "say" => self.io_handle_print(name, args, true, line),
15998            "printf" => self.io_handle_printf(name, args, line),
15999            "getline" | "readline" => {
16000                if !args.is_empty() {
16001                    return Err(PerlError::runtime(
16002                        format!("{}: too many arguments", method),
16003                        line,
16004                    ));
16005                }
16006                self.readline_builtin_execute(Some(name))
16007            }
16008            "close" => {
16009                if !args.is_empty() {
16010                    return Err(PerlError::runtime("close: too many arguments", line));
16011                }
16012                self.close_builtin_execute(name.to_string())
16013            }
16014            "eof" => {
16015                if !args.is_empty() {
16016                    return Err(PerlError::runtime("eof: too many arguments", line));
16017                }
16018                let at_eof = !self.has_input_handle(name);
16019                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
16020            }
16021            "getc" => {
16022                if !args.is_empty() {
16023                    return Err(PerlError::runtime("getc: too many arguments", line));
16024                }
16025                match crate::builtins::try_builtin(
16026                    self,
16027                    "getc",
16028                    &[PerlValue::string(name.to_string())],
16029                    line,
16030                ) {
16031                    Some(r) => r,
16032                    None => Err(PerlError::runtime("getc: not available", line)),
16033                }
16034            }
16035            "binmode" => match crate::builtins::try_builtin(
16036                self,
16037                "binmode",
16038                &[PerlValue::string(name.to_string())],
16039                line,
16040            ) {
16041                Some(r) => r,
16042                None => Err(PerlError::runtime("binmode: not available", line)),
16043            },
16044            "fileno" => match crate::builtins::try_builtin(
16045                self,
16046                "fileno",
16047                &[PerlValue::string(name.to_string())],
16048                line,
16049            ) {
16050                Some(r) => r,
16051                None => Err(PerlError::runtime("fileno: not available", line)),
16052            },
16053            "flush" => {
16054                if !args.is_empty() {
16055                    return Err(PerlError::runtime("flush: too many arguments", line));
16056                }
16057                self.io_handle_flush(name, line)
16058            }
16059            _ => Err(PerlError::runtime(
16060                format!("Unknown method for filehandle: {}", method),
16061                line,
16062            )),
16063        }
16064    }
16065
16066    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> PerlResult<PerlValue> {
16067        match handle_name {
16068            "STDOUT" => {
16069                let _ = IoWrite::flush(&mut io::stdout());
16070            }
16071            "STDERR" => {
16072                let _ = IoWrite::flush(&mut io::stderr());
16073            }
16074            name => {
16075                if let Some(writer) = self.output_handles.get_mut(name) {
16076                    let _ = IoWrite::flush(&mut *writer);
16077                } else {
16078                    return Err(PerlError::runtime(
16079                        format!("flush on unopened filehandle {}", name),
16080                        line,
16081                    ));
16082                }
16083            }
16084        }
16085        Ok(PerlValue::integer(1))
16086    }
16087
16088    fn io_handle_print(
16089        &mut self,
16090        handle_name: &str,
16091        args: &[PerlValue],
16092        newline: bool,
16093        line: usize,
16094    ) -> PerlResult<PerlValue> {
16095        if newline && (self.feature_bits & FEAT_SAY) == 0 {
16096            return Err(PerlError::runtime(
16097                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
16098                line,
16099            ));
16100        }
16101        let mut output = String::new();
16102        if args.is_empty() {
16103            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
16104            output.push_str(&self.scope.get_scalar("_").to_string());
16105        } else {
16106            for (i, val) in args.iter().enumerate() {
16107                if i > 0 && !self.ofs.is_empty() {
16108                    output.push_str(&self.ofs);
16109                }
16110                output.push_str(&val.to_string());
16111            }
16112        }
16113        if newline {
16114            output.push('\n');
16115        }
16116        output.push_str(&self.ors);
16117
16118        self.write_formatted_print(handle_name, &output, line)?;
16119        Ok(PerlValue::integer(1))
16120    }
16121
16122    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
16123    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
16124    pub(crate) fn write_formatted_print(
16125        &mut self,
16126        handle_name: &str,
16127        output: &str,
16128        line: usize,
16129    ) -> PerlResult<()> {
16130        match handle_name {
16131            "STDOUT" => {
16132                if !self.suppress_stdout {
16133                    print!("{}", output);
16134                    if self.output_autoflush {
16135                        let _ = io::stdout().flush();
16136                    }
16137                }
16138            }
16139            "STDERR" => {
16140                eprint!("{}", output);
16141                let _ = io::stderr().flush();
16142            }
16143            name => {
16144                if let Some(writer) = self.output_handles.get_mut(name) {
16145                    let _ = writer.write_all(output.as_bytes());
16146                    if self.output_autoflush {
16147                        let _ = writer.flush();
16148                    }
16149                } else {
16150                    return Err(PerlError::runtime(
16151                        format!("print on unopened filehandle {}", name),
16152                        line,
16153                    ));
16154                }
16155            }
16156        }
16157        Ok(())
16158    }
16159
16160    fn io_handle_printf(
16161        &mut self,
16162        handle_name: &str,
16163        args: &[PerlValue],
16164        line: usize,
16165    ) -> PerlResult<PerlValue> {
16166        let (fmt, rest): (String, &[PerlValue]) = if args.is_empty() {
16167            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
16168                Ok(s) => s,
16169                Err(FlowOrError::Error(e)) => return Err(e),
16170                Err(FlowOrError::Flow(_)) => {
16171                    return Err(PerlError::runtime(
16172                        "printf: unexpected control flow in sprintf",
16173                        line,
16174                    ));
16175                }
16176            };
16177            (s, &[])
16178        } else {
16179            (args[0].to_string(), &args[1..])
16180        };
16181        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
16182            Ok(s) => s,
16183            Err(FlowOrError::Error(e)) => return Err(e),
16184            Err(FlowOrError::Flow(_)) => {
16185                return Err(PerlError::runtime(
16186                    "printf: unexpected control flow in sprintf",
16187                    line,
16188                ));
16189            }
16190        };
16191        match handle_name {
16192            "STDOUT" => {
16193                if !self.suppress_stdout {
16194                    print!("{}", output);
16195                    if self.output_autoflush {
16196                        let _ = IoWrite::flush(&mut io::stdout());
16197                    }
16198                }
16199            }
16200            "STDERR" => {
16201                eprint!("{}", output);
16202                let _ = IoWrite::flush(&mut io::stderr());
16203            }
16204            name => {
16205                if let Some(writer) = self.output_handles.get_mut(name) {
16206                    let _ = writer.write_all(output.as_bytes());
16207                    if self.output_autoflush {
16208                        let _ = writer.flush();
16209                    }
16210                } else {
16211                    return Err(PerlError::runtime(
16212                        format!("printf on unopened filehandle {}", name),
16213                        line,
16214                    ));
16215                }
16216            }
16217        }
16218        Ok(PerlValue::integer(1))
16219    }
16220
16221    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
16222    pub(crate) fn try_native_method(
16223        &mut self,
16224        receiver: &PerlValue,
16225        method: &str,
16226        args: &[PerlValue],
16227        line: usize,
16228    ) -> Option<PerlResult<PerlValue>> {
16229        if let Some(name) = receiver.as_io_handle_name() {
16230            return Some(self.io_handle_method(&name, method, args, line));
16231        }
16232        if let Some(ref s) = receiver.as_str() {
16233            if self.is_bound_handle(s) {
16234                return Some(self.io_handle_method(s, method, args, line));
16235            }
16236        }
16237        if let Some(c) = receiver.as_sqlite_conn() {
16238            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
16239        }
16240        if let Some(s) = receiver.as_struct_inst() {
16241            // Field access: $p->x or $p->x(value)
16242            if let Some(idx) = s.def.field_index(method) {
16243                match args.len() {
16244                    0 => {
16245                        return Some(Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF)));
16246                    }
16247                    1 => {
16248                        let field = &s.def.fields[idx];
16249                        let new_val = args[0].clone();
16250                        if let Err(msg) = field.ty.check_value(&new_val) {
16251                            return Some(Err(PerlError::type_error(
16252                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
16253                                line,
16254                            )));
16255                        }
16256                        s.set_field(idx, new_val.clone());
16257                        return Some(Ok(new_val));
16258                    }
16259                    _ => {
16260                        return Some(Err(PerlError::runtime(
16261                            format!(
16262                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
16263                                method,
16264                                args.len()
16265                            ),
16266                            line,
16267                        )));
16268                    }
16269                }
16270            }
16271            // Built-in struct methods
16272            match method {
16273                "with" => {
16274                    // Functional update: $p->with(x => 5) returns new instance with changed field
16275                    let mut new_values = s.get_values();
16276                    let mut i = 0;
16277                    while i + 1 < args.len() {
16278                        let k = args[i].to_string();
16279                        let v = args[i + 1].clone();
16280                        if let Some(idx) = s.def.field_index(&k) {
16281                            let field = &s.def.fields[idx];
16282                            if let Err(msg) = field.ty.check_value(&v) {
16283                                return Some(Err(PerlError::type_error(
16284                                    format!(
16285                                        "struct {} field `{}`: {}",
16286                                        s.def.name, field.name, msg
16287                                    ),
16288                                    line,
16289                                )));
16290                            }
16291                            new_values[idx] = v;
16292                        } else {
16293                            return Some(Err(PerlError::runtime(
16294                                format!("struct {}: unknown field `{}`", s.def.name, k),
16295                                line,
16296                            )));
16297                        }
16298                        i += 2;
16299                    }
16300                    return Some(Ok(PerlValue::struct_inst(Arc::new(
16301                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
16302                    ))));
16303                }
16304                "to_hash" => {
16305                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
16306                    if !args.is_empty() {
16307                        return Some(Err(PerlError::runtime(
16308                            "struct to_hash takes no arguments",
16309                            line,
16310                        )));
16311                    }
16312                    let mut map = IndexMap::new();
16313                    let values = s.get_values();
16314                    for (i, field) in s.def.fields.iter().enumerate() {
16315                        map.insert(field.name.clone(), values[i].clone());
16316                    }
16317                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
16318                }
16319                "to_hash_rec" | "to_hash_deep" => {
16320                    // Like to_hash but recurse: nested struct/class/hash/
16321                    // array values become plain hashref/arrayref trees.
16322                    if !args.is_empty() {
16323                        return Some(Err(PerlError::runtime(
16324                            "struct to_hash_rec takes no arguments",
16325                            line,
16326                        )));
16327                    }
16328                    return Some(Ok(self.deep_to_hash_value(receiver)));
16329                }
16330                "fields" => {
16331                    // Field list: $p->fields returns field names
16332                    if !args.is_empty() {
16333                        return Some(Err(PerlError::runtime(
16334                            "struct fields takes no arguments",
16335                            line,
16336                        )));
16337                    }
16338                    let names: Vec<PerlValue> = s
16339                        .def
16340                        .fields
16341                        .iter()
16342                        .map(|f| PerlValue::string(f.name.clone()))
16343                        .collect();
16344                    return Some(Ok(PerlValue::array(names)));
16345                }
16346                "clone" => {
16347                    // Clone: $p->clone deep copies
16348                    if !args.is_empty() {
16349                        return Some(Err(PerlError::runtime(
16350                            "struct clone takes no arguments",
16351                            line,
16352                        )));
16353                    }
16354                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
16355                    return Some(Ok(PerlValue::struct_inst(Arc::new(
16356                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
16357                    ))));
16358                }
16359                _ => {}
16360            }
16361            // User-defined struct method
16362            if let Some(m) = s.def.method(method) {
16363                let body = m.body.clone();
16364                let params = m.params.clone();
16365                // Build args: $self is the receiver, then the passed args
16366                let mut call_args = vec![receiver.clone()];
16367                call_args.extend(args.iter().cloned());
16368                return Some(
16369                    match self.call_struct_method(&body, &params, call_args, line) {
16370                        Ok(v) => Ok(v),
16371                        Err(FlowOrError::Error(e)) => Err(e),
16372                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16373                        Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
16374                            "unexpected control flow in struct method",
16375                            line,
16376                        )),
16377                    },
16378                );
16379            }
16380            return None;
16381        }
16382        // Class instance method dispatch
16383        if let Some(c) = receiver.as_class_inst() {
16384            // Collect all fields from inheritance chain (with visibility)
16385            let all_fields_full = self.collect_class_fields_full(&c.def);
16386            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
16387                .iter()
16388                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
16389                .collect();
16390
16391            // Field access: $obj->name or $obj->name(value)
16392            if let Some(idx) = all_fields_full
16393                .iter()
16394                .position(|(name, _, _, _, _)| name == method)
16395            {
16396                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
16397
16398                // Enforce field visibility
16399                match vis {
16400                    crate::ast::Visibility::Private => {
16401                        // Only accessible from within the owning class's methods
16402                        let caller_class = self
16403                            .scope
16404                            .get_scalar("self")
16405                            .as_class_inst()
16406                            .map(|ci| ci.def.name.clone());
16407                        if caller_class.as_deref() != Some(owner_class.as_str()) {
16408                            return Some(Err(PerlError::runtime(
16409                                format!("field `{}` of class {} is private", method, owner_class),
16410                                line,
16411                            )));
16412                        }
16413                    }
16414                    crate::ast::Visibility::Protected => {
16415                        // Accessible from owning class or subclasses
16416                        let caller_class = self
16417                            .scope
16418                            .get_scalar("self")
16419                            .as_class_inst()
16420                            .map(|ci| ci.def.name.clone());
16421                        let allowed = caller_class.as_deref().is_some_and(|caller| {
16422                            caller == owner_class || self.class_inherits_from(caller, owner_class)
16423                        });
16424                        if !allowed {
16425                            return Some(Err(PerlError::runtime(
16426                                format!("field `{}` of class {} is protected", method, owner_class),
16427                                line,
16428                            )));
16429                        }
16430                    }
16431                    crate::ast::Visibility::Public => {}
16432                }
16433
16434                match args.len() {
16435                    0 => {
16436                        return Some(Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF)));
16437                    }
16438                    1 => {
16439                        let new_val = args[0].clone();
16440                        if let Err(msg) = ty.check_value(&new_val) {
16441                            return Some(Err(PerlError::type_error(
16442                                format!("class {} field `{}`: {}", c.def.name, method, msg),
16443                                line,
16444                            )));
16445                        }
16446                        c.set_field(idx, new_val.clone());
16447                        return Some(Ok(new_val));
16448                    }
16449                    _ => {
16450                        return Some(Err(PerlError::runtime(
16451                            format!(
16452                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
16453                                method,
16454                                args.len()
16455                            ),
16456                            line,
16457                        )));
16458                    }
16459                }
16460            }
16461            // Built-in class methods (use all_fields for inheritance)
16462            match method {
16463                "with" => {
16464                    let mut new_values = c.get_values();
16465                    let mut i = 0;
16466                    while i + 1 < args.len() {
16467                        let k = args[i].to_string();
16468                        let v = args[i + 1].clone();
16469                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
16470                            let (_, _, ref ty) = all_fields[idx];
16471                            if let Err(msg) = ty.check_value(&v) {
16472                                return Some(Err(PerlError::type_error(
16473                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
16474                                    line,
16475                                )));
16476                            }
16477                            new_values[idx] = v;
16478                        } else {
16479                            return Some(Err(PerlError::runtime(
16480                                format!("class {}: unknown field `{}`", c.def.name, k),
16481                                line,
16482                            )));
16483                        }
16484                        i += 2;
16485                    }
16486                    return Some(Ok(PerlValue::class_inst(Arc::new(
16487                        crate::value::ClassInstance::new_with_isa(
16488                            Arc::clone(&c.def),
16489                            new_values,
16490                            c.isa_chain.clone(),
16491                        ),
16492                    ))));
16493                }
16494                "to_hash" => {
16495                    if !args.is_empty() {
16496                        return Some(Err(PerlError::runtime(
16497                            "class to_hash takes no arguments",
16498                            line,
16499                        )));
16500                    }
16501                    let mut map = IndexMap::new();
16502                    let values = c.get_values();
16503                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
16504                        if let Some(v) = values.get(i) {
16505                            map.insert(name.clone(), v.clone());
16506                        }
16507                    }
16508                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
16509                }
16510                "to_hash_rec" | "to_hash_deep" => {
16511                    // Recursive flatten: nested class/struct/hash/array
16512                    // values become plain hashref/arrayref trees, so the
16513                    // result is JSON-serializable end-to-end without any
16514                    // surviving ClassInstance/StructInstance leaves.
16515                    if !args.is_empty() {
16516                        return Some(Err(PerlError::runtime(
16517                            "class to_hash_rec takes no arguments",
16518                            line,
16519                        )));
16520                    }
16521                    return Some(Ok(self.deep_to_hash_value(receiver)));
16522                }
16523                "fields" => {
16524                    if !args.is_empty() {
16525                        return Some(Err(PerlError::runtime(
16526                            "class fields takes no arguments",
16527                            line,
16528                        )));
16529                    }
16530                    let names: Vec<PerlValue> = all_fields
16531                        .iter()
16532                        .map(|(name, _, _)| PerlValue::string(name.clone()))
16533                        .collect();
16534                    return Some(Ok(PerlValue::array(names)));
16535                }
16536                "clone" => {
16537                    if !args.is_empty() {
16538                        return Some(Err(PerlError::runtime(
16539                            "class clone takes no arguments",
16540                            line,
16541                        )));
16542                    }
16543                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
16544                    return Some(Ok(PerlValue::class_inst(Arc::new(
16545                        crate::value::ClassInstance::new_with_isa(
16546                            Arc::clone(&c.def),
16547                            new_values,
16548                            c.isa_chain.clone(),
16549                        ),
16550                    ))));
16551                }
16552                "isa" => {
16553                    if args.len() != 1 {
16554                        return Some(Err(PerlError::runtime("isa requires one argument", line)));
16555                    }
16556                    let class_name = args[0].to_string();
16557                    let is_a = c.isa(&class_name);
16558                    return Some(Ok(if is_a {
16559                        PerlValue::integer(1)
16560                    } else {
16561                        PerlValue::string(String::new())
16562                    }));
16563                }
16564                "does" => {
16565                    if args.len() != 1 {
16566                        return Some(Err(PerlError::runtime("does requires one argument", line)));
16567                    }
16568                    let trait_name = args[0].to_string();
16569                    let implements = c.def.implements.contains(&trait_name);
16570                    return Some(Ok(if implements {
16571                        PerlValue::integer(1)
16572                    } else {
16573                        PerlValue::string(String::new())
16574                    }));
16575                }
16576                "methods" => {
16577                    if !args.is_empty() {
16578                        return Some(Err(PerlError::runtime("methods takes no arguments", line)));
16579                    }
16580                    let mut names = Vec::new();
16581                    self.collect_class_method_names(&c.def, &mut names);
16582                    let values: Vec<PerlValue> = names.into_iter().map(PerlValue::string).collect();
16583                    return Some(Ok(PerlValue::array(values)));
16584                }
16585                "superclass" => {
16586                    if !args.is_empty() {
16587                        return Some(Err(PerlError::runtime(
16588                            "superclass takes no arguments",
16589                            line,
16590                        )));
16591                    }
16592                    let parents: Vec<PerlValue> = c
16593                        .def
16594                        .extends
16595                        .iter()
16596                        .map(|s| PerlValue::string(s.clone()))
16597                        .collect();
16598                    return Some(Ok(PerlValue::array(parents)));
16599                }
16600                "destroy" => {
16601                    // Explicit destructor call — runs DESTROY chain child-first
16602                    let destroy_chain = self.collect_destroy_chain(&c.def);
16603                    for (body, params) in &destroy_chain {
16604                        let call_args = vec![receiver.clone()];
16605                        match self.call_class_method(body, params, call_args, line) {
16606                            Ok(_) => {}
16607                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
16608                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
16609                            Err(_) => {}
16610                        }
16611                    }
16612                    return Some(Ok(PerlValue::UNDEF));
16613                }
16614                _ => {}
16615            }
16616            // User-defined class method (search inheritance chain)
16617            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
16618                // Check visibility
16619                match m.visibility {
16620                    crate::ast::Visibility::Private => {
16621                        let caller_class = self
16622                            .scope
16623                            .get_scalar("self")
16624                            .as_class_inst()
16625                            .map(|ci| ci.def.name.clone());
16626                        if caller_class.as_deref() != Some(owner_class.as_str()) {
16627                            return Some(Err(PerlError::runtime(
16628                                format!("method `{}` of class {} is private", method, owner_class),
16629                                line,
16630                            )));
16631                        }
16632                    }
16633                    crate::ast::Visibility::Protected => {
16634                        let caller_class = self
16635                            .scope
16636                            .get_scalar("self")
16637                            .as_class_inst()
16638                            .map(|ci| ci.def.name.clone());
16639                        let allowed = caller_class.as_deref().is_some_and(|caller| {
16640                            caller == owner_class.as_str()
16641                                || self.class_inherits_from(caller, owner_class)
16642                        });
16643                        if !allowed {
16644                            return Some(Err(PerlError::runtime(
16645                                format!(
16646                                    "method `{}` of class {} is protected",
16647                                    method, owner_class
16648                                ),
16649                                line,
16650                            )));
16651                        }
16652                    }
16653                    crate::ast::Visibility::Public => {}
16654                }
16655                if let Some(ref body) = m.body {
16656                    let params = m.params.clone();
16657                    let mut call_args = vec![receiver.clone()];
16658                    call_args.extend(args.iter().cloned());
16659                    return Some(
16660                        match self.call_class_method(body, &params, call_args, line) {
16661                            Ok(v) => Ok(v),
16662                            Err(FlowOrError::Error(e)) => Err(e),
16663                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16664                            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
16665                                "unexpected control flow in class method",
16666                                line,
16667                            )),
16668                        },
16669                    );
16670                }
16671            }
16672            return None;
16673        }
16674        if let Some(d) = receiver.as_dataframe() {
16675            return Some(self.dataframe_method(d, method, args, line));
16676        }
16677        if let Some(s) = crate::value::set_payload(receiver) {
16678            return Some(self.set_method(s, method, args, line));
16679        }
16680        if let Some(d) = receiver.as_deque() {
16681            return Some(self.deque_method(d, method, args, line));
16682        }
16683        if let Some(h) = receiver.as_heap_pq() {
16684            return Some(self.heap_method(h, method, args, line));
16685        }
16686        if let Some(p) = receiver.as_pipeline() {
16687            return Some(self.pipeline_method(p, method, args, line));
16688        }
16689        if let Some(c) = receiver.as_capture() {
16690            return Some(self.capture_method(c, method, args, line));
16691        }
16692        if let Some(p) = receiver.as_ppool() {
16693            return Some(self.ppool_method(p, method, args, line));
16694        }
16695        if let Some(b) = receiver.as_barrier() {
16696            return Some(self.barrier_method(b, method, args, line));
16697        }
16698        if let Some(g) = receiver.as_generator() {
16699            if method == "next" {
16700                if !args.is_empty() {
16701                    return Some(Err(PerlError::runtime(
16702                        "generator->next takes no arguments",
16703                        line,
16704                    )));
16705                }
16706                return Some(self.generator_next(&g));
16707            }
16708            return None;
16709        }
16710        if let Some(arc) = receiver.as_atomic_arc() {
16711            let inner = arc.lock().clone();
16712            if let Some(d) = inner.as_deque() {
16713                return Some(self.deque_method(d, method, args, line));
16714            }
16715            if let Some(h) = inner.as_heap_pq() {
16716                return Some(self.heap_method(h, method, args, line));
16717            }
16718        }
16719        None
16720    }
16721
16722    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
16723    fn dataframe_method(
16724        &mut self,
16725        d: Arc<Mutex<PerlDataFrame>>,
16726        method: &str,
16727        args: &[PerlValue],
16728        line: usize,
16729    ) -> PerlResult<PerlValue> {
16730        match method {
16731            "nrow" | "nrows" => {
16732                if !args.is_empty() {
16733                    return Err(PerlError::runtime(
16734                        format!("dataframe {} takes no arguments", method),
16735                        line,
16736                    ));
16737                }
16738                Ok(PerlValue::integer(d.lock().nrows() as i64))
16739            }
16740            "ncol" | "ncols" => {
16741                if !args.is_empty() {
16742                    return Err(PerlError::runtime(
16743                        format!("dataframe {} takes no arguments", method),
16744                        line,
16745                    ));
16746                }
16747                Ok(PerlValue::integer(d.lock().ncols() as i64))
16748            }
16749            "filter" => {
16750                if args.len() != 1 {
16751                    return Err(PerlError::runtime(
16752                        "dataframe filter expects 1 argument (sub)",
16753                        line,
16754                    ));
16755                }
16756                let Some(sub) = args[0].as_code_ref() else {
16757                    return Err(PerlError::runtime(
16758                        "dataframe filter expects a code reference",
16759                        line,
16760                    ));
16761                };
16762                let df_guard = d.lock();
16763                let n = df_guard.nrows();
16764                let mut keep = vec![false; n];
16765                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
16766                    let row = df_guard.row_hashref(r);
16767                    self.scope_push_hook();
16768                    self.scope.set_topic(row);
16769                    if let Some(ref env) = sub.closure_env {
16770                        self.scope.restore_capture(env);
16771                    }
16772                    let pass = match self.exec_block_no_scope(&sub.body) {
16773                        Ok(v) => v.is_true(),
16774                        Err(_) => false,
16775                    };
16776                    self.scope_pop_hook();
16777                    *row_keep = pass;
16778                }
16779                let columns = df_guard.columns.clone();
16780                let cols: Vec<Vec<PerlValue>> = (0..df_guard.ncols())
16781                    .map(|i| {
16782                        let mut out = Vec::new();
16783                        for (r, pass_row) in keep.iter().enumerate().take(n) {
16784                            if *pass_row {
16785                                out.push(df_guard.cols[i][r].clone());
16786                            }
16787                        }
16788                        out
16789                    })
16790                    .collect();
16791                let group_by = df_guard.group_by.clone();
16792                drop(df_guard);
16793                let new_df = PerlDataFrame {
16794                    columns,
16795                    cols,
16796                    group_by,
16797                };
16798                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
16799            }
16800            "group_by" => {
16801                if args.len() != 1 {
16802                    return Err(PerlError::runtime(
16803                        "dataframe group_by expects 1 column name",
16804                        line,
16805                    ));
16806                }
16807                let key = args[0].to_string();
16808                let inner = d.lock();
16809                if inner.col_index(&key).is_none() {
16810                    return Err(PerlError::runtime(
16811                        format!("dataframe group_by: unknown column \"{}\"", key),
16812                        line,
16813                    ));
16814                }
16815                let new_df = PerlDataFrame {
16816                    columns: inner.columns.clone(),
16817                    cols: inner.cols.clone(),
16818                    group_by: Some(key),
16819                };
16820                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
16821            }
16822            "sum" => {
16823                if args.len() != 1 {
16824                    return Err(PerlError::runtime(
16825                        "dataframe sum expects 1 column name",
16826                        line,
16827                    ));
16828                }
16829                let col_name = args[0].to_string();
16830                let inner = d.lock();
16831                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
16832                    PerlError::runtime(
16833                        format!("dataframe sum: unknown column \"{}\"", col_name),
16834                        line,
16835                    )
16836                })?;
16837                match &inner.group_by {
16838                    Some(gcol) => {
16839                        let gi = inner.col_index(gcol).ok_or_else(|| {
16840                            PerlError::runtime(
16841                                format!("dataframe sum: unknown group column \"{}\"", gcol),
16842                                line,
16843                            )
16844                        })?;
16845                        let mut acc: IndexMap<String, f64> = IndexMap::new();
16846                        for r in 0..inner.nrows() {
16847                            let k = inner.cols[gi][r].to_string();
16848                            let v = inner.cols[val_idx][r].to_number();
16849                            *acc.entry(k).or_insert(0.0) += v;
16850                        }
16851                        let keys: Vec<String> = acc.keys().cloned().collect();
16852                        let sums: Vec<f64> = acc.values().copied().collect();
16853                        let cols = vec![
16854                            keys.into_iter().map(PerlValue::string).collect(),
16855                            sums.into_iter().map(PerlValue::float).collect(),
16856                        ];
16857                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
16858                        let out = PerlDataFrame {
16859                            columns,
16860                            cols,
16861                            group_by: None,
16862                        };
16863                        Ok(PerlValue::dataframe(Arc::new(Mutex::new(out))))
16864                    }
16865                    None => {
16866                        let total: f64 = (0..inner.nrows())
16867                            .map(|r| inner.cols[val_idx][r].to_number())
16868                            .sum();
16869                        Ok(PerlValue::float(total))
16870                    }
16871                }
16872            }
16873            _ => Err(PerlError::runtime(
16874                format!("Unknown method for dataframe: {}", method),
16875                line,
16876            )),
16877        }
16878    }
16879
16880    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
16881    fn set_method(
16882        &self,
16883        s: Arc<crate::value::PerlSet>,
16884        method: &str,
16885        args: &[PerlValue],
16886        line: usize,
16887    ) -> PerlResult<PerlValue> {
16888        match method {
16889            "has" | "contains" | "member" => {
16890                if args.len() != 1 {
16891                    return Err(PerlError::runtime(
16892                        "set->has expects one argument (element)",
16893                        line,
16894                    ));
16895                }
16896                let k = crate::value::set_member_key(&args[0]);
16897                Ok(PerlValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
16898            }
16899            "size" | "len" | "count" => {
16900                if !args.is_empty() {
16901                    return Err(PerlError::runtime("set->size takes no arguments", line));
16902                }
16903                Ok(PerlValue::integer(s.len() as i64))
16904            }
16905            "values" | "list" | "elements" => {
16906                if !args.is_empty() {
16907                    return Err(PerlError::runtime("set->values takes no arguments", line));
16908                }
16909                Ok(PerlValue::array(s.values().cloned().collect()))
16910            }
16911            _ => Err(PerlError::runtime(
16912                format!("Unknown method for set: {}", method),
16913                line,
16914            )),
16915        }
16916    }
16917
16918    fn deque_method(
16919        &mut self,
16920        d: Arc<Mutex<VecDeque<PerlValue>>>,
16921        method: &str,
16922        args: &[PerlValue],
16923        line: usize,
16924    ) -> PerlResult<PerlValue> {
16925        match method {
16926            "push_back" => {
16927                if args.len() != 1 {
16928                    return Err(PerlError::runtime("push_back expects 1 argument", line));
16929                }
16930                d.lock().push_back(args[0].clone());
16931                Ok(PerlValue::integer(d.lock().len() as i64))
16932            }
16933            "push_front" => {
16934                if args.len() != 1 {
16935                    return Err(PerlError::runtime("push_front expects 1 argument", line));
16936                }
16937                d.lock().push_front(args[0].clone());
16938                Ok(PerlValue::integer(d.lock().len() as i64))
16939            }
16940            "pop_back" => Ok(d.lock().pop_back().unwrap_or(PerlValue::UNDEF)),
16941            "pop_front" => Ok(d.lock().pop_front().unwrap_or(PerlValue::UNDEF)),
16942            "size" | "len" => Ok(PerlValue::integer(d.lock().len() as i64)),
16943            _ => Err(PerlError::runtime(
16944                format!("Unknown method for deque: {}", method),
16945                line,
16946            )),
16947        }
16948    }
16949
16950    fn heap_method(
16951        &mut self,
16952        h: Arc<Mutex<PerlHeap>>,
16953        method: &str,
16954        args: &[PerlValue],
16955        line: usize,
16956    ) -> PerlResult<PerlValue> {
16957        match method {
16958            "push" => {
16959                if args.len() != 1 {
16960                    return Err(PerlError::runtime("heap push expects 1 argument", line));
16961                }
16962                let mut g = h.lock();
16963                let n = g.items.len();
16964                g.items.push(args[0].clone());
16965                let cmp = g.cmp.clone();
16966                drop(g);
16967                let mut g = h.lock();
16968                self.heap_sift_up(&mut g.items, &cmp, n);
16969                Ok(PerlValue::integer(g.items.len() as i64))
16970            }
16971            "pop" => {
16972                let mut g = h.lock();
16973                if g.items.is_empty() {
16974                    return Ok(PerlValue::UNDEF);
16975                }
16976                let cmp = g.cmp.clone();
16977                let n = g.items.len();
16978                g.items.swap(0, n - 1);
16979                let v = g.items.pop().unwrap();
16980                if !g.items.is_empty() {
16981                    self.heap_sift_down(&mut g.items, &cmp, 0);
16982                }
16983                Ok(v)
16984            }
16985            "peek" => Ok(h.lock().items.first().cloned().unwrap_or(PerlValue::UNDEF)),
16986            _ => Err(PerlError::runtime(
16987                format!("Unknown method for heap: {}", method),
16988                line,
16989            )),
16990        }
16991    }
16992
16993    fn ppool_method(
16994        &mut self,
16995        pool: PerlPpool,
16996        method: &str,
16997        args: &[PerlValue],
16998        line: usize,
16999    ) -> PerlResult<PerlValue> {
17000        match method {
17001            "submit" => pool.submit(self, args, line),
17002            "collect" => {
17003                if !args.is_empty() {
17004                    return Err(PerlError::runtime("collect() takes no arguments", line));
17005                }
17006                pool.collect(line)
17007            }
17008            _ => Err(PerlError::runtime(
17009                format!("Unknown method for ppool: {}", method),
17010                line,
17011            )),
17012        }
17013    }
17014
17015    fn barrier_method(
17016        &self,
17017        barrier: PerlBarrier,
17018        method: &str,
17019        args: &[PerlValue],
17020        line: usize,
17021    ) -> PerlResult<PerlValue> {
17022        match method {
17023            "wait" => {
17024                if !args.is_empty() {
17025                    return Err(PerlError::runtime("wait() takes no arguments", line));
17026                }
17027                let _ = barrier.0.wait();
17028                Ok(PerlValue::integer(1))
17029            }
17030            _ => Err(PerlError::runtime(
17031                format!("Unknown method for barrier: {}", method),
17032                line,
17033            )),
17034        }
17035    }
17036
17037    fn capture_method(
17038        &self,
17039        c: Arc<CaptureResult>,
17040        method: &str,
17041        args: &[PerlValue],
17042        line: usize,
17043    ) -> PerlResult<PerlValue> {
17044        if !args.is_empty() {
17045            return Err(PerlError::runtime(
17046                format!("capture: {} takes no arguments", method),
17047                line,
17048            ));
17049        }
17050        match method {
17051            "stdout" => Ok(PerlValue::string(c.stdout.clone())),
17052            "stderr" => Ok(PerlValue::string(c.stderr.clone())),
17053            "exitcode" => Ok(PerlValue::integer(c.exitcode)),
17054            "failed" => Ok(PerlValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
17055            _ => Err(PerlError::runtime(
17056                format!("Unknown method for capture: {}", method),
17057                line,
17058            )),
17059        }
17060    }
17061
17062    pub(crate) fn builtin_par_pipeline_stream(
17063        &mut self,
17064        args: &[PerlValue],
17065        _line: usize,
17066    ) -> PerlResult<PerlValue> {
17067        let mut items = Vec::new();
17068        for v in args {
17069            if let Some(a) = v.as_array_vec() {
17070                items.extend(a);
17071            } else {
17072                items.push(v.clone());
17073            }
17074        }
17075        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
17076            source: items,
17077            ops: Vec::new(),
17078            has_scalar_terminal: false,
17079            par_stream: true,
17080            streaming: false,
17081            streaming_workers: 0,
17082            streaming_buffer: 256,
17083        }))))
17084    }
17085
17086    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
17087    /// that wires ops through bounded channels on `collect()`.
17088    pub(crate) fn builtin_par_pipeline_stream_new(
17089        &mut self,
17090        args: &[PerlValue],
17091        _line: usize,
17092    ) -> PerlResult<PerlValue> {
17093        let mut items = Vec::new();
17094        let mut workers: usize = 0;
17095        let mut buffer: usize = 256;
17096        // Separate list items from keyword args (workers => N, buffer => N).
17097        let mut i = 0;
17098        while i < args.len() {
17099            let s = args[i].to_string();
17100            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
17101                let val = args[i + 1].to_int().max(1) as usize;
17102                if s == "workers" {
17103                    workers = val;
17104                } else {
17105                    buffer = val;
17106                }
17107                i += 2;
17108            } else if let Some(a) = args[i].as_array_vec() {
17109                items.extend(a);
17110                i += 1;
17111            } else {
17112                items.push(args[i].clone());
17113                i += 1;
17114            }
17115        }
17116        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
17117            source: items,
17118            ops: Vec::new(),
17119            has_scalar_terminal: false,
17120            par_stream: false,
17121            streaming: true,
17122            streaming_workers: workers,
17123            streaming_buffer: buffer,
17124        }))))
17125    }
17126
17127    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
17128    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<PerlSub> {
17129        let line = 1usize;
17130        let body = vec![Statement {
17131            label: None,
17132            kind: StmtKind::Expression(Expr {
17133                kind: ExprKind::BinOp {
17134                    left: Box::new(Expr {
17135                        kind: ExprKind::ScalarVar("_".into()),
17136                        line,
17137                    }),
17138                    op: BinOp::Mul,
17139                    right: Box::new(Expr {
17140                        kind: ExprKind::Integer(k),
17141                        line,
17142                    }),
17143                },
17144                line,
17145            }),
17146            line,
17147        }];
17148        Arc::new(PerlSub {
17149            name: "__pipeline_int_mul__".into(),
17150            params: vec![],
17151            body,
17152            closure_env: None,
17153            prototype: None,
17154            fib_like: None,
17155        })
17156    }
17157
17158    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<PerlSub> {
17159        let captured = self.scope.capture();
17160        Arc::new(PerlSub {
17161            name: "__ANON__".into(),
17162            params: vec![],
17163            body: block.clone(),
17164            closure_env: Some(captured),
17165            prototype: None,
17166            fib_like: None,
17167        })
17168    }
17169
17170    pub(crate) fn builtin_collect_execute(
17171        &mut self,
17172        args: &[PerlValue],
17173        line: usize,
17174    ) -> PerlResult<PerlValue> {
17175        if args.is_empty() {
17176            return Err(PerlError::runtime(
17177                "collect() expects at least one argument",
17178                line,
17179            ));
17180        }
17181        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
17182        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
17183        if args.len() == 1 {
17184            if let Some(p) = args[0].as_pipeline() {
17185                return self.pipeline_collect(&p, line);
17186            }
17187            return Ok(PerlValue::array(args[0].to_list()));
17188        }
17189        Ok(PerlValue::array(args.to_vec()))
17190    }
17191
17192    pub(crate) fn pipeline_push(
17193        &self,
17194        p: &Arc<Mutex<PipelineInner>>,
17195        op: PipelineOp,
17196        line: usize,
17197    ) -> PerlResult<()> {
17198        let mut g = p.lock();
17199        if g.has_scalar_terminal {
17200            return Err(PerlError::runtime(
17201                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
17202                line,
17203            ));
17204        }
17205        if matches!(
17206            &op,
17207            PipelineOp::PReduce { .. }
17208                | PipelineOp::PReduceInit { .. }
17209                | PipelineOp::PMapReduce { .. }
17210        ) {
17211            g.has_scalar_terminal = true;
17212        }
17213        g.ops.push(op);
17214        Ok(())
17215    }
17216
17217    fn pipeline_parse_sub_progress(
17218        args: &[PerlValue],
17219        line: usize,
17220        name: &str,
17221    ) -> PerlResult<(Arc<PerlSub>, bool)> {
17222        if args.is_empty() {
17223            return Err(PerlError::runtime(
17224                format!("pipeline {}: expects at least 1 argument (code ref)", name),
17225                line,
17226            ));
17227        }
17228        let Some(sub) = args[0].as_code_ref() else {
17229            return Err(PerlError::runtime(
17230                format!("pipeline {}: first argument must be a code reference", name),
17231                line,
17232            ));
17233        };
17234        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
17235        if args.len() > 2 {
17236            return Err(PerlError::runtime(
17237                format!(
17238                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
17239                    name
17240                ),
17241                line,
17242            ));
17243        }
17244        Ok((sub, progress))
17245    }
17246
17247    pub(crate) fn pipeline_method(
17248        &mut self,
17249        p: Arc<Mutex<PipelineInner>>,
17250        method: &str,
17251        args: &[PerlValue],
17252        line: usize,
17253    ) -> PerlResult<PerlValue> {
17254        match method {
17255            "filter" | "f" | "grep" => {
17256                if args.len() != 1 {
17257                    return Err(PerlError::runtime(
17258                        "pipeline filter/grep expects 1 argument (sub)",
17259                        line,
17260                    ));
17261                }
17262                let Some(sub) = args[0].as_code_ref() else {
17263                    return Err(PerlError::runtime(
17264                        "pipeline filter/grep expects a code reference",
17265                        line,
17266                    ));
17267                };
17268                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
17269                Ok(PerlValue::pipeline(Arc::clone(&p)))
17270            }
17271            "map" => {
17272                if args.len() != 1 {
17273                    return Err(PerlError::runtime(
17274                        "pipeline map expects 1 argument (sub)",
17275                        line,
17276                    ));
17277                }
17278                let Some(sub) = args[0].as_code_ref() else {
17279                    return Err(PerlError::runtime(
17280                        "pipeline map expects a code reference",
17281                        line,
17282                    ));
17283                };
17284                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
17285                Ok(PerlValue::pipeline(Arc::clone(&p)))
17286            }
17287            "tap" | "peek" => {
17288                if args.len() != 1 {
17289                    return Err(PerlError::runtime(
17290                        "pipeline tap/peek expects 1 argument (sub)",
17291                        line,
17292                    ));
17293                }
17294                let Some(sub) = args[0].as_code_ref() else {
17295                    return Err(PerlError::runtime(
17296                        "pipeline tap/peek expects a code reference",
17297                        line,
17298                    ));
17299                };
17300                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
17301                Ok(PerlValue::pipeline(Arc::clone(&p)))
17302            }
17303            "take" => {
17304                if args.len() != 1 {
17305                    return Err(PerlError::runtime("pipeline take expects 1 argument", line));
17306                }
17307                let n = args[0].to_int();
17308                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
17309                Ok(PerlValue::pipeline(Arc::clone(&p)))
17310            }
17311            "pmap" => {
17312                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
17313                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
17314                Ok(PerlValue::pipeline(Arc::clone(&p)))
17315            }
17316            "pgrep" => {
17317                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
17318                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
17319                Ok(PerlValue::pipeline(Arc::clone(&p)))
17320            }
17321            "pfor" => {
17322                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
17323                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
17324                Ok(PerlValue::pipeline(Arc::clone(&p)))
17325            }
17326            "pmap_chunked" => {
17327                if args.len() < 2 {
17328                    return Err(PerlError::runtime(
17329                        "pipeline pmap_chunked expects chunk size and a code reference",
17330                        line,
17331                    ));
17332                }
17333                let chunk = args[0].to_int().max(1);
17334                let Some(sub) = args[1].as_code_ref() else {
17335                    return Err(PerlError::runtime(
17336                        "pipeline pmap_chunked: second argument must be a code reference",
17337                        line,
17338                    ));
17339                };
17340                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
17341                if args.len() > 3 {
17342                    return Err(PerlError::runtime(
17343                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
17344                        line,
17345                    ));
17346                }
17347                self.pipeline_push(
17348                    &p,
17349                    PipelineOp::PMapChunked {
17350                        chunk,
17351                        sub,
17352                        progress,
17353                    },
17354                    line,
17355                )?;
17356                Ok(PerlValue::pipeline(Arc::clone(&p)))
17357            }
17358            "psort" => {
17359                let (cmp, progress) = match args.len() {
17360                    0 => (None, false),
17361                    1 => {
17362                        if let Some(s) = args[0].as_code_ref() {
17363                            (Some(s), false)
17364                        } else {
17365                            (None, args[0].is_true())
17366                        }
17367                    }
17368                    2 => {
17369                        let Some(s) = args[0].as_code_ref() else {
17370                            return Err(PerlError::runtime(
17371                                "pipeline psort: with two arguments, the first must be a comparator sub",
17372                                line,
17373                            ));
17374                        };
17375                        (Some(s), args[1].is_true())
17376                    }
17377                    _ => {
17378                        return Err(PerlError::runtime(
17379                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
17380                            line,
17381                        ));
17382                    }
17383                };
17384                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
17385                Ok(PerlValue::pipeline(Arc::clone(&p)))
17386            }
17387            "pcache" => {
17388                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
17389                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
17390                Ok(PerlValue::pipeline(Arc::clone(&p)))
17391            }
17392            "preduce" => {
17393                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
17394                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
17395                Ok(PerlValue::pipeline(Arc::clone(&p)))
17396            }
17397            "preduce_init" => {
17398                if args.len() < 2 {
17399                    return Err(PerlError::runtime(
17400                        "pipeline preduce_init expects init value and a code reference",
17401                        line,
17402                    ));
17403                }
17404                let init = args[0].clone();
17405                let Some(sub) = args[1].as_code_ref() else {
17406                    return Err(PerlError::runtime(
17407                        "pipeline preduce_init: second argument must be a code reference",
17408                        line,
17409                    ));
17410                };
17411                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
17412                if args.len() > 3 {
17413                    return Err(PerlError::runtime(
17414                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
17415                        line,
17416                    ));
17417                }
17418                self.pipeline_push(
17419                    &p,
17420                    PipelineOp::PReduceInit {
17421                        init,
17422                        sub,
17423                        progress,
17424                    },
17425                    line,
17426                )?;
17427                Ok(PerlValue::pipeline(Arc::clone(&p)))
17428            }
17429            "pmap_reduce" => {
17430                if args.len() < 2 {
17431                    return Err(PerlError::runtime(
17432                        "pipeline pmap_reduce expects map sub and reduce sub",
17433                        line,
17434                    ));
17435                }
17436                let Some(map) = args[0].as_code_ref() else {
17437                    return Err(PerlError::runtime(
17438                        "pipeline pmap_reduce: first argument must be a code reference (map)",
17439                        line,
17440                    ));
17441                };
17442                let Some(reduce) = args[1].as_code_ref() else {
17443                    return Err(PerlError::runtime(
17444                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
17445                        line,
17446                    ));
17447                };
17448                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
17449                if args.len() > 3 {
17450                    return Err(PerlError::runtime(
17451                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
17452                        line,
17453                    ));
17454                }
17455                self.pipeline_push(
17456                    &p,
17457                    PipelineOp::PMapReduce {
17458                        map,
17459                        reduce,
17460                        progress,
17461                    },
17462                    line,
17463                )?;
17464                Ok(PerlValue::pipeline(Arc::clone(&p)))
17465            }
17466            "collect" => {
17467                if !args.is_empty() {
17468                    return Err(PerlError::runtime(
17469                        "pipeline collect takes no arguments",
17470                        line,
17471                    ));
17472                }
17473                self.pipeline_collect(&p, line)
17474            }
17475            _ => {
17476                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
17477                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
17478                if let Some(sub) = self.resolve_sub_by_name(method) {
17479                    if !args.is_empty() {
17480                        return Err(PerlError::runtime(
17481                            format!(
17482                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
17483                                method
17484                            ),
17485                            line,
17486                        ));
17487                    }
17488                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
17489                    Ok(PerlValue::pipeline(Arc::clone(&p)))
17490                } else {
17491                    Err(PerlError::runtime(
17492                        format!("Unknown method for pipeline: {}", method),
17493                        line,
17494                    ))
17495                }
17496            }
17497        }
17498    }
17499
17500    fn pipeline_parallel_map(
17501        &mut self,
17502        items: Vec<PerlValue>,
17503        sub: &Arc<PerlSub>,
17504        progress: bool,
17505    ) -> Vec<PerlValue> {
17506        let subs = self.subs.clone();
17507        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
17508        let pmap_progress = PmapProgress::new(progress, items.len());
17509        let results: Vec<PerlValue> = items
17510            .into_par_iter()
17511            .map(|item| {
17512                let mut local_interp = VMHelper::new();
17513                local_interp.subs = subs.clone();
17514                local_interp.scope.restore_capture(&scope_capture);
17515                local_interp
17516                    .scope
17517                    .restore_atomics(&atomic_arrays, &atomic_hashes);
17518                local_interp.enable_parallel_guard();
17519                local_interp.scope.set_topic(item);
17520                local_interp.scope_push_hook();
17521                let val = match local_interp.exec_block_no_scope(&sub.body) {
17522                    Ok(val) => val,
17523                    Err(_) => PerlValue::UNDEF,
17524                };
17525                local_interp.scope_pop_hook();
17526                pmap_progress.tick();
17527                val
17528            })
17529            .collect();
17530        pmap_progress.finish();
17531        results
17532    }
17533
17534    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
17535    fn pipeline_par_stream_filter(
17536        &mut self,
17537        items: Vec<PerlValue>,
17538        sub: &Arc<PerlSub>,
17539    ) -> Vec<PerlValue> {
17540        if items.is_empty() {
17541            return items;
17542        }
17543        let subs = self.subs.clone();
17544        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
17545        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
17546        let mut kept: Vec<(usize, PerlValue)> = indexed
17547            .into_par_iter()
17548            .filter_map(|(i, item)| {
17549                let mut local_interp = VMHelper::new();
17550                local_interp.subs = subs.clone();
17551                local_interp.scope.restore_capture(&scope_capture);
17552                local_interp
17553                    .scope
17554                    .restore_atomics(&atomic_arrays, &atomic_hashes);
17555                local_interp.enable_parallel_guard();
17556                local_interp.scope.set_topic(item.clone());
17557                local_interp.scope_push_hook();
17558                let keep = match local_interp.exec_block_no_scope(&sub.body) {
17559                    Ok(val) => val.is_true(),
17560                    Err(_) => false,
17561                };
17562                local_interp.scope_pop_hook();
17563                if keep {
17564                    Some((i, item))
17565                } else {
17566                    None
17567                }
17568            })
17569            .collect();
17570        kept.sort_by_key(|(i, _)| *i);
17571        kept.into_iter().map(|(_, x)| x).collect()
17572    }
17573
17574    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
17575    fn pipeline_par_stream_map(
17576        &mut self,
17577        items: Vec<PerlValue>,
17578        sub: &Arc<PerlSub>,
17579    ) -> Vec<PerlValue> {
17580        if items.is_empty() {
17581            return items;
17582        }
17583        let subs = self.subs.clone();
17584        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
17585        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
17586        let mut mapped: Vec<(usize, PerlValue)> = indexed
17587            .into_par_iter()
17588            .map(|(i, item)| {
17589                let mut local_interp = VMHelper::new();
17590                local_interp.subs = subs.clone();
17591                local_interp.scope.restore_capture(&scope_capture);
17592                local_interp
17593                    .scope
17594                    .restore_atomics(&atomic_arrays, &atomic_hashes);
17595                local_interp.enable_parallel_guard();
17596                local_interp.scope.set_topic(item);
17597                local_interp.scope_push_hook();
17598                let val = match local_interp.exec_block_no_scope(&sub.body) {
17599                    Ok(val) => val,
17600                    Err(_) => PerlValue::UNDEF,
17601                };
17602                local_interp.scope_pop_hook();
17603                (i, val)
17604            })
17605            .collect();
17606        mapped.sort_by_key(|(i, _)| *i);
17607        mapped.into_iter().map(|(_, x)| x).collect()
17608    }
17609
17610    fn pipeline_collect(
17611        &mut self,
17612        p: &Arc<Mutex<PipelineInner>>,
17613        line: usize,
17614    ) -> PerlResult<PerlValue> {
17615        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
17616            let g = p.lock();
17617            (
17618                g.source.clone(),
17619                g.ops.clone(),
17620                g.par_stream,
17621                g.streaming,
17622                g.streaming_workers,
17623                g.streaming_buffer,
17624            )
17625        };
17626        if streaming {
17627            return self.pipeline_collect_streaming(
17628                v,
17629                &ops,
17630                streaming_workers,
17631                streaming_buffer,
17632                line,
17633            );
17634        }
17635        for op in ops {
17636            match op {
17637                PipelineOp::Filter(sub) => {
17638                    if par_stream {
17639                        v = self.pipeline_par_stream_filter(v, &sub);
17640                    } else {
17641                        let mut out = Vec::new();
17642                        for item in v {
17643                            self.scope_push_hook();
17644                            self.scope.set_topic(item.clone());
17645                            if let Some(ref env) = sub.closure_env {
17646                                self.scope.restore_capture(env);
17647                            }
17648                            let keep = match self.exec_block_no_scope(&sub.body) {
17649                                Ok(val) => val.is_true(),
17650                                Err(_) => false,
17651                            };
17652                            self.scope_pop_hook();
17653                            if keep {
17654                                out.push(item);
17655                            }
17656                        }
17657                        v = out;
17658                    }
17659                }
17660                PipelineOp::Map(sub) => {
17661                    if par_stream {
17662                        v = self.pipeline_par_stream_map(v, &sub);
17663                    } else {
17664                        let mut out = Vec::new();
17665                        for item in v {
17666                            self.scope_push_hook();
17667                            self.scope.set_topic(item);
17668                            if let Some(ref env) = sub.closure_env {
17669                                self.scope.restore_capture(env);
17670                            }
17671                            let mapped = match self.exec_block_no_scope(&sub.body) {
17672                                Ok(val) => val,
17673                                Err(_) => PerlValue::UNDEF,
17674                            };
17675                            self.scope_pop_hook();
17676                            out.push(mapped);
17677                        }
17678                        v = out;
17679                    }
17680                }
17681                PipelineOp::Tap(sub) => {
17682                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
17683                        Ok(_) => {}
17684                        Err(FlowOrError::Error(e)) => return Err(e),
17685                        Err(FlowOrError::Flow(_)) => {
17686                            return Err(PerlError::runtime(
17687                                "tap: unsupported control flow in block",
17688                                line,
17689                            ));
17690                        }
17691                    }
17692                }
17693                PipelineOp::Take(n) => {
17694                    let n = n.max(0) as usize;
17695                    if v.len() > n {
17696                        v.truncate(n);
17697                    }
17698                }
17699                PipelineOp::PMap { sub, progress } => {
17700                    v = self.pipeline_parallel_map(v, &sub, progress);
17701                }
17702                PipelineOp::PGrep { sub, progress } => {
17703                    let subs = self.subs.clone();
17704                    let (scope_capture, atomic_arrays, atomic_hashes) =
17705                        self.scope.capture_with_atomics();
17706                    let pmap_progress = PmapProgress::new(progress, v.len());
17707                    v = v
17708                        .into_par_iter()
17709                        .filter_map(|item| {
17710                            let mut local_interp = VMHelper::new();
17711                            local_interp.subs = subs.clone();
17712                            local_interp.scope.restore_capture(&scope_capture);
17713                            local_interp
17714                                .scope
17715                                .restore_atomics(&atomic_arrays, &atomic_hashes);
17716                            local_interp.enable_parallel_guard();
17717                            local_interp.scope.set_topic(item.clone());
17718                            local_interp.scope_push_hook();
17719                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
17720                                Ok(val) => val.is_true(),
17721                                Err(_) => false,
17722                            };
17723                            local_interp.scope_pop_hook();
17724                            pmap_progress.tick();
17725                            if keep {
17726                                Some(item)
17727                            } else {
17728                                None
17729                            }
17730                        })
17731                        .collect();
17732                    pmap_progress.finish();
17733                }
17734                PipelineOp::PFor { sub, progress } => {
17735                    let subs = self.subs.clone();
17736                    let (scope_capture, atomic_arrays, atomic_hashes) =
17737                        self.scope.capture_with_atomics();
17738                    let pmap_progress = PmapProgress::new(progress, v.len());
17739                    let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
17740                    v.clone().into_par_iter().for_each(|item| {
17741                        if first_err.lock().is_some() {
17742                            return;
17743                        }
17744                        let mut local_interp = VMHelper::new();
17745                        local_interp.subs = subs.clone();
17746                        local_interp.scope.restore_capture(&scope_capture);
17747                        local_interp
17748                            .scope
17749                            .restore_atomics(&atomic_arrays, &atomic_hashes);
17750                        local_interp.enable_parallel_guard();
17751                        local_interp.scope.set_topic(item);
17752                        local_interp.scope_push_hook();
17753                        match local_interp.exec_block_no_scope(&sub.body) {
17754                            Ok(_) => {}
17755                            Err(e) => {
17756                                let stryke = match e {
17757                                    FlowOrError::Error(stryke) => stryke,
17758                                    FlowOrError::Flow(_) => PerlError::runtime(
17759                                        "return/last/next/redo not supported inside pipeline pfor block",
17760                                        line,
17761                                    ),
17762                                };
17763                                let mut g = first_err.lock();
17764                                if g.is_none() {
17765                                    *g = Some(stryke);
17766                                }
17767                            }
17768                        }
17769                        local_interp.scope_pop_hook();
17770                        pmap_progress.tick();
17771                    });
17772                    pmap_progress.finish();
17773                    let pfor_err = first_err.lock().take();
17774                    if let Some(e) = pfor_err {
17775                        return Err(e);
17776                    }
17777                }
17778                PipelineOp::PMapChunked {
17779                    chunk,
17780                    sub,
17781                    progress,
17782                } => {
17783                    let chunk_n = chunk.max(1) as usize;
17784                    let subs = self.subs.clone();
17785                    let (scope_capture, atomic_arrays, atomic_hashes) =
17786                        self.scope.capture_with_atomics();
17787                    let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = v
17788                        .chunks(chunk_n)
17789                        .enumerate()
17790                        .map(|(i, c)| (i, c.to_vec()))
17791                        .collect();
17792                    let n_chunks = indexed_chunks.len();
17793                    let pmap_progress = PmapProgress::new(progress, n_chunks);
17794                    let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
17795                        .into_par_iter()
17796                        .map(|(chunk_idx, chunk)| {
17797                            let mut local_interp = VMHelper::new();
17798                            local_interp.subs = subs.clone();
17799                            local_interp.scope.restore_capture(&scope_capture);
17800                            local_interp
17801                                .scope
17802                                .restore_atomics(&atomic_arrays, &atomic_hashes);
17803                            local_interp.enable_parallel_guard();
17804                            let mut out = Vec::with_capacity(chunk.len());
17805                            for item in chunk {
17806                                local_interp.scope.set_topic(item);
17807                                local_interp.scope_push_hook();
17808                                match local_interp.exec_block_no_scope(&sub.body) {
17809                                    Ok(val) => {
17810                                        local_interp.scope_pop_hook();
17811                                        out.push(val);
17812                                    }
17813                                    Err(_) => {
17814                                        local_interp.scope_pop_hook();
17815                                        out.push(PerlValue::UNDEF);
17816                                    }
17817                                }
17818                            }
17819                            pmap_progress.tick();
17820                            (chunk_idx, out)
17821                        })
17822                        .collect();
17823                    pmap_progress.finish();
17824                    chunk_results.sort_by_key(|(i, _)| *i);
17825                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
17826                }
17827                PipelineOp::PSort { cmp, progress } => {
17828                    let pmap_progress = PmapProgress::new(progress, 2);
17829                    pmap_progress.tick();
17830                    match cmp {
17831                        Some(cmp_block) => {
17832                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
17833                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
17834                            } else {
17835                                let subs = self.subs.clone();
17836                                let scope_capture = self.scope.capture();
17837                                v.par_sort_by(|a, b| {
17838                                    let mut local_interp = VMHelper::new();
17839                                    local_interp.subs = subs.clone();
17840                                    local_interp.scope.restore_capture(&scope_capture);
17841                                    local_interp.enable_parallel_guard();
17842                                    local_interp.scope.set_sort_pair(a.clone(), b.clone());
17843                                    local_interp.scope_push_hook();
17844                                    let ord =
17845                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
17846                                            Ok(v) => {
17847                                                let n = v.to_int();
17848                                                if n < 0 {
17849                                                    std::cmp::Ordering::Less
17850                                                } else if n > 0 {
17851                                                    std::cmp::Ordering::Greater
17852                                                } else {
17853                                                    std::cmp::Ordering::Equal
17854                                                }
17855                                            }
17856                                            Err(_) => std::cmp::Ordering::Equal,
17857                                        };
17858                                    local_interp.scope_pop_hook();
17859                                    ord
17860                                });
17861                            }
17862                        }
17863                        None => {
17864                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
17865                        }
17866                    }
17867                    pmap_progress.tick();
17868                    pmap_progress.finish();
17869                }
17870                PipelineOp::PCache { sub, progress } => {
17871                    let subs = self.subs.clone();
17872                    let scope_capture = self.scope.capture();
17873                    let cache = &*crate::pcache::GLOBAL_PCACHE;
17874                    let pmap_progress = PmapProgress::new(progress, v.len());
17875                    v = v
17876                        .into_par_iter()
17877                        .map(|item| {
17878                            let k = crate::pcache::cache_key(&item);
17879                            if let Some(cached) = cache.get(&k) {
17880                                pmap_progress.tick();
17881                                return cached.clone();
17882                            }
17883                            let mut local_interp = VMHelper::new();
17884                            local_interp.subs = subs.clone();
17885                            local_interp.scope.restore_capture(&scope_capture);
17886                            local_interp.enable_parallel_guard();
17887                            local_interp.scope.set_topic(item.clone());
17888                            local_interp.scope_push_hook();
17889                            let val = match local_interp.exec_block_no_scope(&sub.body) {
17890                                Ok(v) => v,
17891                                Err(_) => PerlValue::UNDEF,
17892                            };
17893                            local_interp.scope_pop_hook();
17894                            cache.insert(k, val.clone());
17895                            pmap_progress.tick();
17896                            val
17897                        })
17898                        .collect();
17899                    pmap_progress.finish();
17900                }
17901                PipelineOp::PReduce { sub, progress } => {
17902                    if v.is_empty() {
17903                        return Ok(PerlValue::UNDEF);
17904                    }
17905                    if v.len() == 1 {
17906                        return Ok(v.into_iter().next().unwrap());
17907                    }
17908                    let block = sub.body.clone();
17909                    let subs = self.subs.clone();
17910                    let scope_capture = self.scope.capture();
17911                    let pmap_progress = PmapProgress::new(progress, v.len());
17912                    let result = v
17913                        .into_par_iter()
17914                        .map(|x| {
17915                            pmap_progress.tick();
17916                            x
17917                        })
17918                        .reduce_with(|a, b| {
17919                            let mut local_interp = VMHelper::new();
17920                            local_interp.subs = subs.clone();
17921                            local_interp.scope.restore_capture(&scope_capture);
17922                            local_interp.enable_parallel_guard();
17923                            local_interp.scope.set_sort_pair(a, b);
17924                            match local_interp.exec_block(&block) {
17925                                Ok(val) => val,
17926                                Err(_) => PerlValue::UNDEF,
17927                            }
17928                        });
17929                    pmap_progress.finish();
17930                    return Ok(result.unwrap_or(PerlValue::UNDEF));
17931                }
17932                PipelineOp::PReduceInit {
17933                    init,
17934                    sub,
17935                    progress,
17936                } => {
17937                    if v.is_empty() {
17938                        return Ok(init);
17939                    }
17940                    let block = sub.body.clone();
17941                    let subs = self.subs.clone();
17942                    let scope_capture = self.scope.capture();
17943                    let cap: &[(String, PerlValue)] = scope_capture.as_slice();
17944                    if v.len() == 1 {
17945                        return Ok(fold_preduce_init_step(
17946                            &subs,
17947                            cap,
17948                            &block,
17949                            preduce_init_fold_identity(&init),
17950                            v.into_iter().next().unwrap(),
17951                        ));
17952                    }
17953                    let pmap_progress = PmapProgress::new(progress, v.len());
17954                    let result = v
17955                        .into_par_iter()
17956                        .fold(
17957                            || preduce_init_fold_identity(&init),
17958                            |acc, item| {
17959                                pmap_progress.tick();
17960                                fold_preduce_init_step(&subs, cap, &block, acc, item)
17961                            },
17962                        )
17963                        .reduce(
17964                            || preduce_init_fold_identity(&init),
17965                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
17966                        );
17967                    pmap_progress.finish();
17968                    return Ok(result);
17969                }
17970                PipelineOp::PMapReduce {
17971                    map,
17972                    reduce,
17973                    progress,
17974                } => {
17975                    if v.is_empty() {
17976                        return Ok(PerlValue::UNDEF);
17977                    }
17978                    let map_block = map.body.clone();
17979                    let reduce_block = reduce.body.clone();
17980                    let subs = self.subs.clone();
17981                    let scope_capture = self.scope.capture();
17982                    if v.len() == 1 {
17983                        let mut local_interp = VMHelper::new();
17984                        local_interp.subs = subs.clone();
17985                        local_interp.scope.restore_capture(&scope_capture);
17986                        local_interp.scope.set_topic(v[0].clone());
17987                        return match local_interp.exec_block_no_scope(&map_block) {
17988                            Ok(val) => Ok(val),
17989                            Err(_) => Ok(PerlValue::UNDEF),
17990                        };
17991                    }
17992                    let pmap_progress = PmapProgress::new(progress, v.len());
17993                    let result = v
17994                        .into_par_iter()
17995                        .map(|item| {
17996                            let mut local_interp = VMHelper::new();
17997                            local_interp.subs = subs.clone();
17998                            local_interp.scope.restore_capture(&scope_capture);
17999                            local_interp.scope.set_topic(item);
18000                            let val = match local_interp.exec_block_no_scope(&map_block) {
18001                                Ok(val) => val,
18002                                Err(_) => PerlValue::UNDEF,
18003                            };
18004                            pmap_progress.tick();
18005                            val
18006                        })
18007                        .reduce_with(|a, b| {
18008                            let mut local_interp = VMHelper::new();
18009                            local_interp.subs = subs.clone();
18010                            local_interp.scope.restore_capture(&scope_capture);
18011                            local_interp.scope.set_sort_pair(a, b);
18012                            match local_interp.exec_block_no_scope(&reduce_block) {
18013                                Ok(val) => val,
18014                                Err(_) => PerlValue::UNDEF,
18015                            }
18016                        });
18017                    pmap_progress.finish();
18018                    return Ok(result.unwrap_or(PerlValue::UNDEF));
18019                }
18020            }
18021        }
18022        Ok(PerlValue::array(v))
18023    }
18024
18025    /// Streaming collect: wire pipeline ops through bounded channels so items flow
18026    /// between stages concurrently.  Order is **not** preserved.
18027    fn pipeline_collect_streaming(
18028        &mut self,
18029        source: Vec<PerlValue>,
18030        ops: &[PipelineOp],
18031        workers_per_stage: usize,
18032        buffer: usize,
18033        line: usize,
18034    ) -> PerlResult<PerlValue> {
18035        use crossbeam::channel::{bounded, Receiver, Sender};
18036
18037        // Validate: reject ops that require all items (can't stream).
18038        for op in ops {
18039            match op {
18040                PipelineOp::PSort { .. }
18041                | PipelineOp::PReduce { .. }
18042                | PipelineOp::PReduceInit { .. }
18043                | PipelineOp::PMapReduce { .. }
18044                | PipelineOp::PMapChunked { .. } => {
18045                    return Err(PerlError::runtime(
18046                        format!(
18047                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
18048                            std::mem::discriminant(op)
18049                        ),
18050                        line,
18051                    ));
18052                }
18053                _ => {}
18054            }
18055        }
18056
18057        // Filter out non-streamable ops and collect streamable ones.
18058        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
18059        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
18060        if streamable_ops.is_empty() {
18061            return Ok(PerlValue::array(source));
18062        }
18063
18064        let n_stages = streamable_ops.len();
18065        let wn = if workers_per_stage > 0 {
18066            workers_per_stage
18067        } else {
18068            self.parallel_thread_count()
18069        };
18070        let subs = self.subs.clone();
18071        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18072
18073        // Build channels: one between each pair of stages, plus one for output.
18074        // channel[0]: source → stage 0
18075        // channel[i]: stage i-1 → stage i
18076        // channel[n_stages]: stage n_stages-1 → collector
18077        let mut channels: Vec<(Sender<PerlValue>, Receiver<PerlValue>)> =
18078            (0..=n_stages).map(|_| bounded(buffer)).collect();
18079
18080        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
18081        let take_done: Arc<std::sync::atomic::AtomicBool> =
18082            Arc::new(std::sync::atomic::AtomicBool::new(false));
18083
18084        // Collect senders/receivers for each stage.
18085        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
18086        let source_tx = channels[0].0.clone();
18087        let result_rx = channels[n_stages].1.clone();
18088        let results: Arc<Mutex<Vec<PerlValue>>> = Arc::new(Mutex::new(Vec::new()));
18089
18090        std::thread::scope(|scope| {
18091            // Collector thread: drain results concurrently to avoid deadlock
18092            // when bounded channels fill up.
18093            let result_rx_c = result_rx.clone();
18094            let results_c = Arc::clone(&results);
18095            scope.spawn(move || {
18096                while let Ok(item) = result_rx_c.recv() {
18097                    results_c.lock().push(item);
18098                }
18099            });
18100
18101            // Source feeder thread.
18102            let err_s = Arc::clone(&err);
18103            let take_done_s = Arc::clone(&take_done);
18104            scope.spawn(move || {
18105                for item in source {
18106                    if err_s.lock().is_some()
18107                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
18108                    {
18109                        break;
18110                    }
18111                    if source_tx.send(item).is_err() {
18112                        break;
18113                    }
18114                }
18115            });
18116
18117            // Spawn workers for each stage.
18118            for (stage_idx, op) in streamable_ops.iter().enumerate() {
18119                let rx = channels[stage_idx].1.clone();
18120                let tx = channels[stage_idx + 1].0.clone();
18121
18122                for _ in 0..wn {
18123                    let rx = rx.clone();
18124                    let tx = tx.clone();
18125                    let subs = subs.clone();
18126                    let capture = capture.clone();
18127                    let atomic_arrays = atomic_arrays.clone();
18128                    let atomic_hashes = atomic_hashes.clone();
18129                    let err_w = Arc::clone(&err);
18130                    let take_done_w = Arc::clone(&take_done);
18131
18132                    match *op {
18133                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
18134                            let sub = Arc::clone(sub);
18135                            scope.spawn(move || {
18136                                while let Ok(item) = rx.recv() {
18137                                    if err_w.lock().is_some() {
18138                                        break;
18139                                    }
18140                                    let mut interp = VMHelper::new();
18141                                    interp.subs = subs.clone();
18142                                    interp.scope.restore_capture(&capture);
18143                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
18144                                    interp.enable_parallel_guard();
18145                                    interp.scope.set_topic(item.clone());
18146                                    interp.scope_push_hook();
18147                                    let keep = match interp.exec_block_no_scope(&sub.body) {
18148                                        Ok(val) => val.is_true(),
18149                                        Err(_) => false,
18150                                    };
18151                                    interp.scope_pop_hook();
18152                                    if keep && tx.send(item).is_err() {
18153                                        break;
18154                                    }
18155                                }
18156                            });
18157                        }
18158                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
18159                            let sub = Arc::clone(sub);
18160                            scope.spawn(move || {
18161                                while let Ok(item) = rx.recv() {
18162                                    if err_w.lock().is_some() {
18163                                        break;
18164                                    }
18165                                    let mut interp = VMHelper::new();
18166                                    interp.subs = subs.clone();
18167                                    interp.scope.restore_capture(&capture);
18168                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
18169                                    interp.enable_parallel_guard();
18170                                    interp.scope.set_topic(item);
18171                                    interp.scope_push_hook();
18172                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
18173                                        Ok(val) => val,
18174                                        Err(_) => PerlValue::UNDEF,
18175                                    };
18176                                    interp.scope_pop_hook();
18177                                    if tx.send(mapped).is_err() {
18178                                        break;
18179                                    }
18180                                }
18181                            });
18182                        }
18183                        PipelineOp::Take(n) => {
18184                            let limit = (*n).max(0) as usize;
18185                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
18186                            let count_w = Arc::clone(&count);
18187                            scope.spawn(move || {
18188                                while let Ok(item) = rx.recv() {
18189                                    let prev =
18190                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
18191                                    if prev >= limit {
18192                                        take_done_w
18193                                            .store(true, std::sync::atomic::Ordering::Relaxed);
18194                                        break;
18195                                    }
18196                                    if tx.send(item).is_err() {
18197                                        break;
18198                                    }
18199                                }
18200                            });
18201                            // Take only needs 1 worker; skip remaining worker spawns.
18202                            break;
18203                        }
18204                        PipelineOp::PFor { ref sub, .. } => {
18205                            let sub = Arc::clone(sub);
18206                            scope.spawn(move || {
18207                                while let Ok(item) = rx.recv() {
18208                                    if err_w.lock().is_some() {
18209                                        break;
18210                                    }
18211                                    let mut interp = VMHelper::new();
18212                                    interp.subs = subs.clone();
18213                                    interp.scope.restore_capture(&capture);
18214                                    interp
18215                                        .scope
18216                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
18217                                    interp.enable_parallel_guard();
18218                                    interp.scope.set_topic(item.clone());
18219                                    interp.scope_push_hook();
18220                                    match interp.exec_block_no_scope(&sub.body) {
18221                                        Ok(_) => {}
18222                                        Err(e) => {
18223                                            let msg = match e {
18224                                                FlowOrError::Error(stryke) => stryke.to_string(),
18225                                                FlowOrError::Flow(_) => {
18226                                                    "unexpected control flow in par_pipeline_stream pfor".into()
18227                                                }
18228                                            };
18229                                            let mut g = err_w.lock();
18230                                            if g.is_none() {
18231                                                *g = Some(msg);
18232                                            }
18233                                            interp.scope_pop_hook();
18234                                            break;
18235                                        }
18236                                    }
18237                                    interp.scope_pop_hook();
18238                                    if tx.send(item).is_err() {
18239                                        break;
18240                                    }
18241                                }
18242                            });
18243                        }
18244                        PipelineOp::Tap(ref sub) => {
18245                            let sub = Arc::clone(sub);
18246                            scope.spawn(move || {
18247                                while let Ok(item) = rx.recv() {
18248                                    if err_w.lock().is_some() {
18249                                        break;
18250                                    }
18251                                    let mut interp = VMHelper::new();
18252                                    interp.subs = subs.clone();
18253                                    interp.scope.restore_capture(&capture);
18254                                    interp
18255                                        .scope
18256                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
18257                                    interp.enable_parallel_guard();
18258                                    match interp.call_sub(
18259                                        &sub,
18260                                        vec![item.clone()],
18261                                        WantarrayCtx::Void,
18262                                        line,
18263                                    )
18264                                    {
18265                                        Ok(_) => {}
18266                                        Err(e) => {
18267                                            let msg = match e {
18268                                                FlowOrError::Error(stryke) => stryke.to_string(),
18269                                                FlowOrError::Flow(_) => {
18270                                                    "unexpected control flow in par_pipeline_stream tap"
18271                                                        .into()
18272                                                }
18273                                            };
18274                                            let mut g = err_w.lock();
18275                                            if g.is_none() {
18276                                                *g = Some(msg);
18277                                            }
18278                                            break;
18279                                        }
18280                                    }
18281                                    if tx.send(item).is_err() {
18282                                        break;
18283                                    }
18284                                }
18285                            });
18286                        }
18287                        PipelineOp::PCache { ref sub, .. } => {
18288                            let sub = Arc::clone(sub);
18289                            scope.spawn(move || {
18290                                while let Ok(item) = rx.recv() {
18291                                    if err_w.lock().is_some() {
18292                                        break;
18293                                    }
18294                                    let k = crate::pcache::cache_key(&item);
18295                                    let val = if let Some(cached) =
18296                                        crate::pcache::GLOBAL_PCACHE.get(&k)
18297                                    {
18298                                        cached.clone()
18299                                    } else {
18300                                        let mut interp = VMHelper::new();
18301                                        interp.subs = subs.clone();
18302                                        interp.scope.restore_capture(&capture);
18303                                        interp
18304                                            .scope
18305                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
18306                                        interp.enable_parallel_guard();
18307                                        interp.scope.set_topic(item);
18308                                        interp.scope_push_hook();
18309                                        let v = match interp.exec_block_no_scope(&sub.body) {
18310                                            Ok(v) => v,
18311                                            Err(_) => PerlValue::UNDEF,
18312                                        };
18313                                        interp.scope_pop_hook();
18314                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
18315                                        v
18316                                    };
18317                                    if tx.send(val).is_err() {
18318                                        break;
18319                                    }
18320                                }
18321                            });
18322                        }
18323                        // Non-streaming ops already rejected above.
18324                        _ => unreachable!(),
18325                    }
18326                }
18327            }
18328
18329            // Drop our copies of intermediate senders/receivers so channels disconnect
18330            // when workers finish.  Also drop result_rx so the collector thread exits
18331            // once all stage workers are done.
18332            channels.clear();
18333            drop(result_rx);
18334        });
18335
18336        if let Some(msg) = err.lock().take() {
18337            return Err(PerlError::runtime(msg, line));
18338        }
18339
18340        let results = std::mem::take(&mut *results.lock());
18341        Ok(PerlValue::array(results))
18342    }
18343
18344    fn heap_compare(&mut self, cmp: &Arc<PerlSub>, a: &PerlValue, b: &PerlValue) -> Ordering {
18345        self.scope_push_hook();
18346        if let Some(ref env) = cmp.closure_env {
18347            self.scope.restore_capture(env);
18348        }
18349        self.scope.set_sort_pair(a.clone(), b.clone());
18350        let ord = match self.exec_block_no_scope(&cmp.body) {
18351            Ok(v) => {
18352                let n = v.to_int();
18353                if n < 0 {
18354                    Ordering::Less
18355                } else if n > 0 {
18356                    Ordering::Greater
18357                } else {
18358                    Ordering::Equal
18359                }
18360            }
18361            Err(_) => Ordering::Equal,
18362        };
18363        self.scope_pop_hook();
18364        ord
18365    }
18366
18367    fn heap_sift_up(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
18368        while i > 0 {
18369            let p = (i - 1) / 2;
18370            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
18371                break;
18372            }
18373            items.swap(i, p);
18374            i = p;
18375        }
18376    }
18377
18378    fn heap_sift_down(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
18379        let n = items.len();
18380        loop {
18381            let mut sm = i;
18382            let l = 2 * i + 1;
18383            let r = 2 * i + 2;
18384            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
18385                sm = l;
18386            }
18387            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
18388                sm = r;
18389            }
18390            if sm == i {
18391                break;
18392            }
18393            items.swap(i, sm);
18394            i = sm;
18395        }
18396    }
18397
18398    fn hash_for_signature_destruct(
18399        &mut self,
18400        v: &PerlValue,
18401        line: usize,
18402    ) -> PerlResult<IndexMap<String, PerlValue>> {
18403        let Some(m) = self.match_subject_as_hash(v) else {
18404            return Err(PerlError::runtime(
18405                format!(
18406                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
18407                    v.ref_type()
18408                ),
18409                line,
18410            ));
18411        };
18412        Ok(m)
18413    }
18414
18415    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
18416    pub(crate) fn apply_sub_signature(
18417        &mut self,
18418        sub: &PerlSub,
18419        argv: &[PerlValue],
18420        line: usize,
18421    ) -> PerlResult<()> {
18422        if sub.params.is_empty() {
18423            return Ok(());
18424        }
18425        let mut i = 0usize;
18426        for p in &sub.params {
18427            match p {
18428                SubSigParam::Scalar(name, ty, default) => {
18429                    let val = if i < argv.len() {
18430                        argv[i].clone()
18431                    } else if let Some(default_expr) = default {
18432                        match self.eval_expr(default_expr) {
18433                            Ok(v) => v,
18434                            Err(FlowOrError::Error(e)) => return Err(e),
18435                            Err(FlowOrError::Flow(_)) => {
18436                                return Err(PerlError::runtime(
18437                                    "unexpected control flow in parameter default",
18438                                    line,
18439                                ))
18440                            }
18441                        }
18442                    } else {
18443                        PerlValue::UNDEF
18444                    };
18445                    i += 1;
18446                    if let Some(t) = ty {
18447                        if let Err(e) = t.check_value(&val) {
18448                            return Err(PerlError::runtime(
18449                                format!("sub parameter ${}: {}", name, e),
18450                                line,
18451                            ));
18452                        }
18453                    }
18454                    let n = self.english_scalar_name(name);
18455                    self.scope.declare_scalar(n, val);
18456                }
18457                SubSigParam::Array(name, default) => {
18458                    let rest: Vec<PerlValue> = if i < argv.len() {
18459                        let r = argv[i..].to_vec();
18460                        i = argv.len();
18461                        r
18462                    } else if let Some(default_expr) = default {
18463                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
18464                            Ok(v) => v,
18465                            Err(FlowOrError::Error(e)) => return Err(e),
18466                            Err(FlowOrError::Flow(_)) => {
18467                                return Err(PerlError::runtime(
18468                                    "unexpected control flow in parameter default",
18469                                    line,
18470                                ))
18471                            }
18472                        };
18473                        val.to_list()
18474                    } else {
18475                        vec![]
18476                    };
18477                    let aname = self.stash_array_name_for_package(name);
18478                    self.scope.declare_array(&aname, rest);
18479                }
18480                SubSigParam::Hash(name, default) => {
18481                    let rest: Vec<PerlValue> = if i < argv.len() {
18482                        let r = argv[i..].to_vec();
18483                        i = argv.len();
18484                        r
18485                    } else if let Some(default_expr) = default {
18486                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
18487                            Ok(v) => v,
18488                            Err(FlowOrError::Error(e)) => return Err(e),
18489                            Err(FlowOrError::Flow(_)) => {
18490                                return Err(PerlError::runtime(
18491                                    "unexpected control flow in parameter default",
18492                                    line,
18493                                ))
18494                            }
18495                        };
18496                        val.to_list()
18497                    } else {
18498                        vec![]
18499                    };
18500                    let mut map = IndexMap::new();
18501                    let mut j = 0;
18502                    while j + 1 < rest.len() {
18503                        map.insert(rest[j].to_string(), rest[j + 1].clone());
18504                        j += 2;
18505                    }
18506                    self.scope.declare_hash(name, map);
18507                }
18508                SubSigParam::ArrayDestruct(elems) => {
18509                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
18510                    i += 1;
18511                    let Some(arr) = self.match_subject_as_array(&arg) else {
18512                        return Err(PerlError::runtime(
18513                            format!(
18514                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
18515                                arg.ref_type()
18516                            ),
18517                            line,
18518                        ));
18519                    };
18520                    let binds = self
18521                        .match_array_pattern_elems(&arr, elems, line)
18522                        .map_err(|e| match e {
18523                            FlowOrError::Error(stryke) => stryke,
18524                            FlowOrError::Flow(_) => PerlError::runtime(
18525                                "unexpected flow in sub signature array destruct",
18526                                line,
18527                            ),
18528                        })?;
18529                    let Some(binds) = binds else {
18530                        return Err(PerlError::runtime(
18531                            "sub signature array destruct: length or element mismatch",
18532                            line,
18533                        ));
18534                    };
18535                    for b in binds {
18536                        match b {
18537                            PatternBinding::Scalar(name, v) => {
18538                                let n = self.english_scalar_name(&name);
18539                                self.scope.declare_scalar(n, v);
18540                            }
18541                            PatternBinding::Array(name, elems) => {
18542                                self.scope.declare_array(&name, elems);
18543                            }
18544                        }
18545                    }
18546                }
18547                SubSigParam::HashDestruct(pairs) => {
18548                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
18549                    i += 1;
18550                    let map = self.hash_for_signature_destruct(&arg, line)?;
18551                    for (key, varname) in pairs {
18552                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
18553                        let n = self.english_scalar_name(varname);
18554                        self.scope.declare_scalar(n, v);
18555                    }
18556                }
18557            }
18558        }
18559        Ok(())
18560    }
18561
18562    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
18563    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
18564    /// These are `PerlSub`s with empty bodies and magic keys in `closure_env`.
18565    pub(crate) fn try_hof_dispatch(
18566        &mut self,
18567        sub: &PerlSub,
18568        args: &[PerlValue],
18569        want: WantarrayCtx,
18570        line: usize,
18571    ) -> Option<ExecResult> {
18572        let env = sub.closure_env.as_ref()?;
18573        fn env_get<'a>(env: &'a [(String, PerlValue)], key: &str) -> Option<&'a PerlValue> {
18574            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
18575        }
18576
18577        match sub.name.as_str() {
18578            // ── compose: right-to-left function application ──
18579            "__comp__" => {
18580                let fns = env_get(env, "__comp_fns__")?.to_list();
18581                let mut val = args.first().cloned().unwrap_or(PerlValue::UNDEF);
18582                for f in fns.iter().rev() {
18583                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
18584                        Ok(v) => val = v,
18585                        Err(e) => return Some(Err(e)),
18586                    }
18587                }
18588                Some(Ok(val))
18589            }
18590            // ── constantly: always return the captured value ──
18591            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
18592            // ── juxt: call each fn with same args, collect results ──
18593            "__juxt__" => {
18594                let fns = env_get(env, "__juxt_fns__")?.to_list();
18595                let mut results = Vec::with_capacity(fns.len());
18596                for f in &fns {
18597                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
18598                        Ok(v) => results.push(v),
18599                        Err(e) => return Some(Err(e)),
18600                    }
18601                }
18602                Some(Ok(PerlValue::array(results)))
18603            }
18604            // ── partial: prepend bound args ──
18605            "__partial__" => {
18606                let fn_val = env_get(env, "__partial_fn__")?.clone();
18607                let bound = env_get(env, "__partial_args__")?.to_list();
18608                let mut all_args = bound;
18609                all_args.extend_from_slice(args);
18610                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
18611            }
18612            // ── complement: negate the result ──
18613            "__complement__" => {
18614                let fn_val = env_get(env, "__complement_fn__")?.clone();
18615                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
18616                    Ok(v) => Some(Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }))),
18617                    Err(e) => Some(Err(e)),
18618                }
18619            }
18620            // ── fnil: replace undef args with defaults ──
18621            "__fnil__" => {
18622                let fn_val = env_get(env, "__fnil_fn__")?.clone();
18623                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
18624                let mut patched = args.to_vec();
18625                for (i, d) in defaults.iter().enumerate() {
18626                    if i < patched.len() {
18627                        if patched[i].is_undef() {
18628                            patched[i] = d.clone();
18629                        }
18630                    } else {
18631                        patched.push(d.clone());
18632                    }
18633                }
18634                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
18635            }
18636            // ── memoize: cache by stringified args ──
18637            "__memoize__" => {
18638                let fn_val = env_get(env, "__memoize_fn__")?.clone();
18639                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
18640                let key = args
18641                    .iter()
18642                    .map(|a| a.to_string())
18643                    .collect::<Vec<_>>()
18644                    .join("\x00");
18645                if let Some(href) = cache_ref.as_hash_ref() {
18646                    if let Some(cached) = href.read().get(&key) {
18647                        return Some(Ok(cached.clone()));
18648                    }
18649                }
18650                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
18651                    Ok(v) => {
18652                        if let Some(href) = cache_ref.as_hash_ref() {
18653                            href.write().insert(key, v.clone());
18654                        }
18655                        Some(Ok(v))
18656                    }
18657                    Err(e) => Some(Err(e)),
18658                }
18659            }
18660            // ── curry: accumulate args until arity reached ──
18661            "__curry__" => {
18662                let fn_val = env_get(env, "__curry_fn__")?.clone();
18663                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
18664                let bound = env_get(env, "__curry_bound__")?.to_list();
18665                let mut all = bound;
18666                all.extend_from_slice(args);
18667                if all.len() >= arity {
18668                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
18669                } else {
18670                    let curry_sub = PerlSub {
18671                        name: "__curry__".to_string(),
18672                        params: vec![],
18673                        body: vec![],
18674                        closure_env: Some(vec![
18675                            ("__curry_fn__".to_string(), fn_val),
18676                            (
18677                                "__curry_arity__".to_string(),
18678                                PerlValue::integer(arity as i64),
18679                            ),
18680                            ("__curry_bound__".to_string(), PerlValue::array(all)),
18681                        ]),
18682                        prototype: None,
18683                        fib_like: None,
18684                    };
18685                    Some(Ok(PerlValue::code_ref(Arc::new(curry_sub))))
18686                }
18687            }
18688            // ── once: call once, cache forever ──
18689            "__once__" => {
18690                let cache_ref = env_get(env, "__once_cache__")?.clone();
18691                if let Some(href) = cache_ref.as_hash_ref() {
18692                    let r = href.read();
18693                    if r.contains_key("done") {
18694                        return Some(Ok(r.get("val").cloned().unwrap_or(PerlValue::UNDEF)));
18695                    }
18696                }
18697                let fn_val = env_get(env, "__once_fn__")?.clone();
18698                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
18699                    Ok(v) => {
18700                        if let Some(href) = cache_ref.as_hash_ref() {
18701                            let mut w = href.write();
18702                            w.insert("done".to_string(), PerlValue::integer(1));
18703                            w.insert("val".to_string(), v.clone());
18704                        }
18705                        Some(Ok(v))
18706                    }
18707                    Err(e) => Some(Err(e)),
18708                }
18709            }
18710            _ => None,
18711        }
18712    }
18713
18714    pub(crate) fn call_sub(
18715        &mut self,
18716        sub: &PerlSub,
18717        args: Vec<PerlValue>,
18718        want: WantarrayCtx,
18719        line: usize,
18720    ) -> ExecResult {
18721        // Default path: derive the package from `sub.name` if it is qualified. Bare-named
18722        // subs (registered without a `Pkg::` prefix) leave `__PACKAGE__` untouched.
18723        let pkg = sub.name.rsplit_once("::").map(|(p, _)| p.to_string());
18724        self.call_sub_with_package(sub, args, want, line, pkg)
18725    }
18726
18727    /// Internal helper: like [`Self::call_sub`] but takes an explicit home-package override
18728    /// (used by [`Self::call_named_sub`], which knows the qualified registry key even when
18729    /// the cached `PerlSub.name` is bare).
18730    fn call_sub_with_package(
18731        &mut self,
18732        sub: &PerlSub,
18733        args: Vec<PerlValue>,
18734        want: WantarrayCtx,
18735        _line: usize,
18736        home_package: Option<String>,
18737    ) -> ExecResult {
18738        // Push current sub for __SUB__ access
18739        self.current_sub_stack.push(Arc::new(sub.clone()));
18740
18741        // Single frame for both @_ and the block's local variables —
18742        // avoids the double push_frame/pop_frame overhead per call.
18743        self.scope_push_hook();
18744        self.scope.declare_array("_", args.clone());
18745        if let Some(ref env) = sub.closure_env {
18746            self.scope.restore_capture(env);
18747        }
18748        // Switch `__PACKAGE__` to the sub's home package so cross-package `our`/`oursync`
18749        // qualifies correctly inside the body. Bytecode VM rewrites at compile time so it
18750        // never needed this; the tree walker (used by parallel workers) does need it.
18751        // Goes AFTER restore_capture so the closure's captured `__PACKAGE__` doesn't
18752        // overwrite our home-package switch.
18753        if let Some(pkg) = home_package {
18754            self.scope
18755                .declare_scalar("__PACKAGE__", PerlValue::string(pkg));
18756        }
18757        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
18758        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
18759        // Must be AFTER restore_capture so we don't get shadowed by captured $_
18760        self.scope.set_closure_args(&args);
18761        // Move `@_` out so `fib_like` / hof dispatch take `&[PerlValue]` without cloning.
18762        let argv = self.scope.take_sub_underscore().unwrap_or_default();
18763        self.apply_sub_signature(sub, &argv, _line)?;
18764        let saved = self.wantarray_kind;
18765        self.wantarray_kind = want;
18766        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
18767            self.wantarray_kind = saved;
18768            self.scope_pop_hook();
18769            self.current_sub_stack.pop();
18770            return match r {
18771                Ok(v) => Ok(v),
18772                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
18773                Err(e) => Err(e),
18774            };
18775        }
18776        if let Some(pat) = sub.fib_like.as_ref() {
18777            if argv.len() == 1 {
18778                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
18779                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
18780                    if let Some(p) = &mut self.profiler {
18781                        p.enter_sub(&sub.name);
18782                    }
18783                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
18784                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
18785                        p.exit_sub(t0.elapsed());
18786                    }
18787                    self.wantarray_kind = saved;
18788                    self.scope_pop_hook();
18789                    self.current_sub_stack.pop();
18790                    return Ok(PerlValue::integer(n));
18791                }
18792            }
18793        }
18794        self.scope.declare_array("_", argv.clone());
18795        // Note: set_closure_args was already called at line 15077; don't call it again
18796        // as that would incorrectly shift the outer topic stack a second time.
18797        let t0 = self.profiler.is_some().then(std::time::Instant::now);
18798        if let Some(p) = &mut self.profiler {
18799            p.enter_sub(&sub.name);
18800        }
18801        // Always evaluate the function body's last expression in List context so
18802        // `@array` returns the array contents, not the count. The caller adapts the
18803        // return value to their own wantarray context after receiving it.
18804        let result = self.exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List);
18805        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
18806            p.exit_sub(t0.elapsed());
18807        }
18808        // For goto &sub, capture @_ before popping the frame
18809        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
18810            Some(self.scope.get_array("_"))
18811        } else {
18812            None
18813        };
18814        self.wantarray_kind = saved;
18815        self.scope_pop_hook();
18816        self.current_sub_stack.pop();
18817        match result {
18818            Ok(v) => Ok(v),
18819            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
18820            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
18821                // goto &sub — tail call: look up target and call with same @_
18822                let goto_args = goto_args.unwrap_or_default();
18823                let fqn = if target_name.contains("::") {
18824                    target_name.clone()
18825                } else {
18826                    format!("{}::{}", self.current_package(), target_name)
18827                };
18828                if let Some(target_sub) = self
18829                    .subs
18830                    .get(&fqn)
18831                    .cloned()
18832                    .or_else(|| self.subs.get(&target_name).cloned())
18833                {
18834                    self.call_sub(&target_sub, goto_args, want, _line)
18835                } else {
18836                    Err(
18837                        PerlError::runtime(format!("Undefined subroutine &{}", target_name), _line)
18838                            .into(),
18839                    )
18840                }
18841            }
18842            Err(FlowOrError::Flow(Flow::Yield(_))) => {
18843                Err(PerlError::runtime("yield is only valid inside gen { }", 0).into())
18844            }
18845            Err(e) => Err(e),
18846        }
18847    }
18848
18849    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
18850    fn call_struct_method(
18851        &mut self,
18852        body: &Block,
18853        params: &[SubSigParam],
18854        args: Vec<PerlValue>,
18855        line: usize,
18856    ) -> ExecResult {
18857        self.scope_push_hook();
18858        self.scope.declare_array("_", args.clone());
18859        // Bind $self to first arg (the receiver)
18860        if let Some(self_val) = args.first() {
18861            self.scope.declare_scalar("self", self_val.clone());
18862        }
18863        // Set $_0, $_1, etc. for all args
18864        self.scope.set_closure_args(&args);
18865        // Apply signature if provided - skip the first arg ($self) for user params
18866        let user_args: Vec<PerlValue> = args.iter().skip(1).cloned().collect();
18867        self.apply_params_to_argv(params, &user_args, line)?;
18868        let result = self.exec_block_no_scope(body);
18869        self.scope_pop_hook();
18870        match result {
18871            Ok(v) => Ok(v),
18872            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
18873            Err(e) => Err(e),
18874        }
18875    }
18876
18877    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
18878    pub(crate) fn call_class_method(
18879        &mut self,
18880        body: &Block,
18881        params: &[SubSigParam],
18882        args: Vec<PerlValue>,
18883        line: usize,
18884    ) -> ExecResult {
18885        self.call_class_method_inner(body, params, args, line, false)
18886    }
18887
18888    /// Call a static class method: `Math::add(...)`.
18889    pub(crate) fn call_static_class_method(
18890        &mut self,
18891        body: &Block,
18892        params: &[SubSigParam],
18893        args: Vec<PerlValue>,
18894        line: usize,
18895    ) -> ExecResult {
18896        self.call_class_method_inner(body, params, args, line, true)
18897    }
18898
18899    fn call_class_method_inner(
18900        &mut self,
18901        body: &Block,
18902        params: &[SubSigParam],
18903        args: Vec<PerlValue>,
18904        line: usize,
18905        is_static: bool,
18906    ) -> ExecResult {
18907        self.scope_push_hook();
18908        self.scope.declare_array("_", args.clone());
18909        if !is_static {
18910            // Bind $self to first arg (the receiver) for instance methods
18911            if let Some(self_val) = args.first() {
18912                self.scope.declare_scalar("self", self_val.clone());
18913            }
18914        }
18915        // Set $_0, $_1, etc. for all args
18916        self.scope.set_closure_args(&args);
18917        // Apply signature: skip first arg ($self) only for instance methods
18918        let user_args: Vec<PerlValue> = if is_static {
18919            args.clone()
18920        } else {
18921            args.iter().skip(1).cloned().collect()
18922        };
18923        self.apply_params_to_argv(params, &user_args, line)?;
18924        let result = self.exec_block_no_scope(body);
18925        self.scope_pop_hook();
18926        match result {
18927            Ok(v) => Ok(v),
18928            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
18929            Err(e) => Err(e),
18930        }
18931    }
18932
18933    /// Apply SubSigParam bindings without the full PerlSub machinery.
18934    fn apply_params_to_argv(
18935        &mut self,
18936        params: &[SubSigParam],
18937        argv: &[PerlValue],
18938        line: usize,
18939    ) -> PerlResult<()> {
18940        let mut i = 0;
18941        for param in params {
18942            match param {
18943                SubSigParam::Scalar(name, ty_opt, default) => {
18944                    let v = if i < argv.len() {
18945                        argv[i].clone()
18946                    } else if let Some(default_expr) = default {
18947                        match self.eval_expr(default_expr) {
18948                            Ok(v) => v,
18949                            Err(FlowOrError::Error(e)) => return Err(e),
18950                            Err(FlowOrError::Flow(_)) => {
18951                                return Err(PerlError::runtime(
18952                                    "unexpected control flow in parameter default",
18953                                    line,
18954                                ))
18955                            }
18956                        }
18957                    } else {
18958                        PerlValue::UNDEF
18959                    };
18960                    i += 1;
18961                    if let Some(ty) = ty_opt {
18962                        ty.check_value(&v).map_err(|msg| {
18963                            PerlError::type_error(
18964                                format!("method parameter ${}: {}", name, msg),
18965                                line,
18966                            )
18967                        })?;
18968                    }
18969                    let n = self.english_scalar_name(name);
18970                    self.scope.declare_scalar(n, v);
18971                }
18972                SubSigParam::Array(name, default) => {
18973                    let rest: Vec<PerlValue> = if i < argv.len() {
18974                        let r = argv[i..].to_vec();
18975                        i = argv.len();
18976                        r
18977                    } else if let Some(default_expr) = default {
18978                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
18979                            Ok(v) => v,
18980                            Err(FlowOrError::Error(e)) => return Err(e),
18981                            Err(FlowOrError::Flow(_)) => {
18982                                return Err(PerlError::runtime(
18983                                    "unexpected control flow in parameter default",
18984                                    line,
18985                                ))
18986                            }
18987                        };
18988                        val.to_list()
18989                    } else {
18990                        vec![]
18991                    };
18992                    let aname = self.stash_array_name_for_package(name);
18993                    self.scope.declare_array(&aname, rest);
18994                }
18995                SubSigParam::Hash(name, default) => {
18996                    let rest: Vec<PerlValue> = if i < argv.len() {
18997                        let r = argv[i..].to_vec();
18998                        i = argv.len();
18999                        r
19000                    } else if let Some(default_expr) = default {
19001                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
19002                            Ok(v) => v,
19003                            Err(FlowOrError::Error(e)) => return Err(e),
19004                            Err(FlowOrError::Flow(_)) => {
19005                                return Err(PerlError::runtime(
19006                                    "unexpected control flow in parameter default",
19007                                    line,
19008                                ))
19009                            }
19010                        };
19011                        val.to_list()
19012                    } else {
19013                        vec![]
19014                    };
19015                    let mut map = IndexMap::new();
19016                    let mut j = 0;
19017                    while j + 1 < rest.len() {
19018                        map.insert(rest[j].to_string(), rest[j + 1].clone());
19019                        j += 2;
19020                    }
19021                    self.scope.declare_hash(name, map);
19022                }
19023                SubSigParam::ArrayDestruct(elems) => {
19024                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
19025                    i += 1;
19026                    let Some(arr) = self.match_subject_as_array(&arg) else {
19027                        return Err(PerlError::runtime(
19028                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
19029                            line,
19030                        ));
19031                    };
19032                    let binds = self
19033                        .match_array_pattern_elems(&arr, elems, line)
19034                        .map_err(|e| match e {
19035                            FlowOrError::Error(stryke) => stryke,
19036                            FlowOrError::Flow(_) => {
19037                                PerlError::runtime("unexpected flow in method array destruct", line)
19038                            }
19039                        })?;
19040                    let Some(binds) = binds else {
19041                        return Err(PerlError::runtime(
19042                            format!(
19043                                "method parameter: array destructure failed at position {}",
19044                                i
19045                            ),
19046                            line,
19047                        ));
19048                    };
19049                    for b in binds {
19050                        match b {
19051                            PatternBinding::Scalar(name, v) => {
19052                                let n = self.english_scalar_name(&name);
19053                                self.scope.declare_scalar(n, v);
19054                            }
19055                            PatternBinding::Array(name, elems) => {
19056                                self.scope.declare_array(&name, elems);
19057                            }
19058                        }
19059                    }
19060                }
19061                SubSigParam::HashDestruct(pairs) => {
19062                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
19063                    i += 1;
19064                    let map = self.hash_for_signature_destruct(&arg, line)?;
19065                    for (key, varname) in pairs {
19066                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
19067                        let n = self.english_scalar_name(varname);
19068                        self.scope.declare_scalar(n, v);
19069                    }
19070                }
19071            }
19072        }
19073        Ok(())
19074    }
19075
19076    fn builtin_new(&mut self, class: &str, args: Vec<PerlValue>, line: usize) -> ExecResult {
19077        if class == "Set" {
19078            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
19079        }
19080        if let Some(def) = self.struct_defs.get(class).cloned() {
19081            let mut provided = Vec::new();
19082            let mut i = 1;
19083            while i + 1 < args.len() {
19084                let k = args[i].to_string();
19085                let v = args[i + 1].clone();
19086                provided.push((k, v));
19087                i += 2;
19088            }
19089            let mut defaults = Vec::with_capacity(def.fields.len());
19090            for field in &def.fields {
19091                if let Some(ref expr) = field.default {
19092                    let val = self.eval_expr(expr)?;
19093                    defaults.push(Some(val));
19094                } else {
19095                    defaults.push(None);
19096                }
19097            }
19098            return Ok(crate::native_data::struct_new_with_defaults(
19099                &def, &provided, &defaults, line,
19100            )?);
19101        }
19102        // Stryke `class` declarations route through `class_construct` so the
19103        // result is a real `ClassInstance` (typed-my checks, isa walk, BUILD
19104        // hooks, etc.). Without this, `Class->new` for a registered class
19105        // fell through to the default Perl-style blessed-hashref path,
19106        // breaking `typed my $x : Class = Class->new` even though the
19107        // runtime check for `Struct(name)` was already in place. Skip
19108        // `args[0]` (the class-name receiver) since `class_construct`
19109        // expects user args only.
19110        if let Some(def) = self.class_defs.get(class).cloned() {
19111            let user_args: Vec<PerlValue> = args.into_iter().skip(1).collect();
19112            return self.class_construct(&def, user_args, line);
19113        }
19114        // Default OO constructor: Class->new(%args) → bless {%args}, class
19115        let mut map = IndexMap::new();
19116        let mut i = 1; // skip $self (first arg is class name)
19117        while i + 1 < args.len() {
19118            let k = args[i].to_string();
19119            let v = args[i + 1].clone();
19120            map.insert(k, v);
19121            i += 2;
19122        }
19123        Ok(PerlValue::blessed(Arc::new(
19124            crate::value::BlessedRef::new_blessed(class.to_string(), PerlValue::hash(map)),
19125        )))
19126    }
19127
19128    fn exec_print(
19129        &mut self,
19130        handle: Option<&str>,
19131        args: &[Expr],
19132        newline: bool,
19133        line: usize,
19134    ) -> ExecResult {
19135        if newline && (self.feature_bits & FEAT_SAY) == 0 {
19136            return Err(PerlError::runtime(
19137                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
19138                line,
19139            )
19140            .into());
19141        }
19142        let mut output = String::new();
19143        if args.is_empty() {
19144            // Perl: print with no LIST prints $_ (same for say).
19145            let topic = self.scope.get_scalar("_").clone();
19146            let s = self.stringify_value(topic, line)?;
19147            output.push_str(&s);
19148        } else {
19149            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
19150            // between those top-level expressions only (not between elements of an expanded `@arr`).
19151            for (i, a) in args.iter().enumerate() {
19152                if i > 0 {
19153                    output.push_str(&self.ofs);
19154                }
19155                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
19156                for item in val.to_list() {
19157                    let s = self.stringify_value(item, line)?;
19158                    output.push_str(&s);
19159                }
19160            }
19161        }
19162        if newline {
19163            output.push('\n');
19164        }
19165        output.push_str(&self.ors);
19166
19167        let handle_name =
19168            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
19169        self.write_formatted_print(handle_name.as_str(), &output, line)?;
19170        Ok(PerlValue::integer(1))
19171    }
19172
19173    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
19174        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
19175            // Perl: printf with no args uses $_ as the format string.
19176            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
19177            (s, &[])
19178        } else {
19179            (self.eval_expr(&args[0])?.to_string(), &args[1..])
19180        };
19181        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
19182        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
19183        // ranges to flip-flop values, so go through list-context eval and splat.
19184        let mut arg_vals = Vec::new();
19185        for a in rest {
19186            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
19187            if let Some(items) = v.as_array_vec() {
19188                arg_vals.extend(items);
19189            } else {
19190                arg_vals.push(v);
19191            }
19192        }
19193        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
19194        let handle_name =
19195            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
19196        match handle_name.as_str() {
19197            "STDOUT" => {
19198                if !self.suppress_stdout {
19199                    print!("{}", output);
19200                    if self.output_autoflush {
19201                        let _ = io::stdout().flush();
19202                    }
19203                }
19204            }
19205            "STDERR" => {
19206                eprint!("{}", output);
19207                let _ = io::stderr().flush();
19208            }
19209            name => {
19210                if let Some(writer) = self.output_handles.get_mut(name) {
19211                    let _ = writer.write_all(output.as_bytes());
19212                    if self.output_autoflush {
19213                        let _ = writer.flush();
19214                    }
19215                }
19216            }
19217        }
19218        Ok(PerlValue::integer(1))
19219    }
19220
19221    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
19222    pub(crate) fn eval_substr_expr(
19223        &mut self,
19224        string: &Expr,
19225        offset: &Expr,
19226        length: Option<&Expr>,
19227        replacement: Option<&Expr>,
19228        _line: usize,
19229    ) -> Result<PerlValue, FlowOrError> {
19230        let s = self.eval_expr(string)?.to_string();
19231        let off = self.eval_expr(offset)?.to_int();
19232        let start = if off < 0 {
19233            (s.len() as i64 + off).max(0) as usize
19234        } else {
19235            off as usize
19236        };
19237        let len = if let Some(l) = length {
19238            let len_val = self.eval_expr(l)?.to_int();
19239            if len_val < 0 {
19240                // Negative length: count from end of string
19241                let remaining = s.len().saturating_sub(start) as i64;
19242                (remaining + len_val).max(0) as usize
19243            } else {
19244                len_val as usize
19245            }
19246        } else {
19247            s.len().saturating_sub(start)
19248        };
19249        let end = start.saturating_add(len).min(s.len());
19250        let result = s.get(start..end).unwrap_or("").to_string();
19251        if let Some(rep) = replacement {
19252            let rep_s = self.eval_expr(rep)?.to_string();
19253            let mut new_s = String::new();
19254            new_s.push_str(&s[..start]);
19255            new_s.push_str(&rep_s);
19256            new_s.push_str(&s[end..]);
19257            self.assign_value(string, PerlValue::string(new_s))?;
19258        }
19259        Ok(PerlValue::string(result))
19260    }
19261
19262    pub(crate) fn eval_push_expr(
19263        &mut self,
19264        array: &Expr,
19265        values: &[Expr],
19266        line: usize,
19267    ) -> Result<PerlValue, FlowOrError> {
19268        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19269            for v in values {
19270                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19271                self.push_array_deref_value(aref.clone(), val, line)?;
19272            }
19273            let len = self.array_deref_len(aref, line)?;
19274            return Ok(PerlValue::integer(len));
19275        }
19276        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19277        if self.scope.is_array_frozen(&arr_name) {
19278            return Err(PerlError::runtime(
19279                format!("Modification of a frozen value: @{}", arr_name),
19280                line,
19281            )
19282            .into());
19283        }
19284        for v in values {
19285            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19286            if let Some(items) = val.as_array_vec() {
19287                for item in items {
19288                    self.scope
19289                        .push_to_array(&arr_name, item)
19290                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19291                }
19292            } else {
19293                self.scope
19294                    .push_to_array(&arr_name, val)
19295                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19296            }
19297        }
19298        let len = self.scope.array_len(&arr_name);
19299        Ok(PerlValue::integer(len as i64))
19300    }
19301
19302    pub(crate) fn eval_pop_expr(
19303        &mut self,
19304        array: &Expr,
19305        line: usize,
19306    ) -> Result<PerlValue, FlowOrError> {
19307        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19308            return self.pop_array_deref(aref, line);
19309        }
19310        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19311        self.scope
19312            .pop_from_array(&arr_name)
19313            .map_err(|e| FlowOrError::Error(e.at_line(line)))
19314    }
19315
19316    pub(crate) fn eval_shift_expr(
19317        &mut self,
19318        array: &Expr,
19319        line: usize,
19320    ) -> Result<PerlValue, FlowOrError> {
19321        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19322            return self.shift_array_deref(aref, line);
19323        }
19324        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19325        self.scope
19326            .shift_from_array(&arr_name)
19327            .map_err(|e| FlowOrError::Error(e.at_line(line)))
19328    }
19329
19330    pub(crate) fn eval_unshift_expr(
19331        &mut self,
19332        array: &Expr,
19333        values: &[Expr],
19334        line: usize,
19335    ) -> Result<PerlValue, FlowOrError> {
19336        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19337            let mut vals = Vec::new();
19338            for v in values {
19339                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19340                if let Some(items) = val.as_array_vec() {
19341                    vals.extend(items);
19342                } else {
19343                    vals.push(val);
19344                }
19345            }
19346            let len = self.unshift_array_deref_multi(aref, vals, line)?;
19347            return Ok(PerlValue::integer(len));
19348        }
19349        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19350        let mut vals = Vec::new();
19351        for v in values {
19352            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
19353            if let Some(items) = val.as_array_vec() {
19354                vals.extend(items);
19355            } else {
19356                vals.push(val);
19357            }
19358        }
19359        let arr = self
19360            .scope
19361            .get_array_mut(&arr_name)
19362            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19363        for (i, v) in vals.into_iter().enumerate() {
19364            arr.insert(i, v);
19365        }
19366        let len = arr.len();
19367        Ok(PerlValue::integer(len as i64))
19368    }
19369
19370    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
19371    pub(crate) fn push_array_deref_value(
19372        &mut self,
19373        arr_ref: PerlValue,
19374        val: PerlValue,
19375        line: usize,
19376    ) -> Result<(), FlowOrError> {
19377        // Resolve binding refs in the value being stored so they snapshot
19378        // the current scope data and survive scope pop.
19379        let val = self.scope.resolve_container_binding_ref(val);
19380        if let Some(r) = arr_ref.as_array_ref() {
19381            let mut w = r.write();
19382            if let Some(items) = val.as_array_vec() {
19383                w.extend(items.iter().cloned());
19384            } else {
19385                w.push(val);
19386            }
19387            return Ok(());
19388        }
19389        if let Some(name) = arr_ref.as_array_binding_name() {
19390            if let Some(items) = val.as_array_vec() {
19391                for item in items {
19392                    self.scope
19393                        .push_to_array(&name, item)
19394                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19395                }
19396            } else {
19397                self.scope
19398                    .push_to_array(&name, val)
19399                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19400            }
19401            return Ok(());
19402        }
19403        if let Some(s) = arr_ref.as_str() {
19404            if self.strict_refs {
19405                return Err(PerlError::runtime(
19406                    format!(
19407                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19408                        s
19409                    ),
19410                    line,
19411                )
19412                .into());
19413            }
19414            let name = s.to_string();
19415            if let Some(items) = val.as_array_vec() {
19416                for item in items {
19417                    self.scope
19418                        .push_to_array(&name, item)
19419                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19420                }
19421            } else {
19422                self.scope
19423                    .push_to_array(&name, val)
19424                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19425            }
19426            return Ok(());
19427        }
19428        Err(PerlError::runtime("push argument is not an ARRAY reference", line).into())
19429    }
19430
19431    pub(crate) fn array_deref_len(
19432        &self,
19433        arr_ref: PerlValue,
19434        line: usize,
19435    ) -> Result<i64, FlowOrError> {
19436        if let Some(r) = arr_ref.as_array_ref() {
19437            return Ok(r.read().len() as i64);
19438        }
19439        if let Some(name) = arr_ref.as_array_binding_name() {
19440            return Ok(self.scope.array_len(&name) as i64);
19441        }
19442        if let Some(s) = arr_ref.as_str() {
19443            if self.strict_refs {
19444                return Err(PerlError::runtime(
19445                    format!(
19446                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19447                        s
19448                    ),
19449                    line,
19450                )
19451                .into());
19452            }
19453            return Ok(self.scope.array_len(&s) as i64);
19454        }
19455        Err(PerlError::runtime("argument is not an ARRAY reference", line).into())
19456    }
19457
19458    pub(crate) fn pop_array_deref(
19459        &mut self,
19460        arr_ref: PerlValue,
19461        line: usize,
19462    ) -> Result<PerlValue, FlowOrError> {
19463        if let Some(r) = arr_ref.as_array_ref() {
19464            let mut w = r.write();
19465            return Ok(w.pop().unwrap_or(PerlValue::UNDEF));
19466        }
19467        if let Some(name) = arr_ref.as_array_binding_name() {
19468            return self
19469                .scope
19470                .pop_from_array(&name)
19471                .map_err(|e| FlowOrError::Error(e.at_line(line)));
19472        }
19473        if let Some(s) = arr_ref.as_str() {
19474            if self.strict_refs {
19475                return Err(PerlError::runtime(
19476                    format!(
19477                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19478                        s
19479                    ),
19480                    line,
19481                )
19482                .into());
19483            }
19484            return self
19485                .scope
19486                .pop_from_array(&s)
19487                .map_err(|e| FlowOrError::Error(e.at_line(line)));
19488        }
19489        Err(PerlError::runtime("pop argument is not an ARRAY reference", line).into())
19490    }
19491
19492    pub(crate) fn shift_array_deref(
19493        &mut self,
19494        arr_ref: PerlValue,
19495        line: usize,
19496    ) -> Result<PerlValue, FlowOrError> {
19497        if let Some(r) = arr_ref.as_array_ref() {
19498            let mut w = r.write();
19499            return Ok(if w.is_empty() {
19500                PerlValue::UNDEF
19501            } else {
19502                w.remove(0)
19503            });
19504        }
19505        if let Some(name) = arr_ref.as_array_binding_name() {
19506            return self
19507                .scope
19508                .shift_from_array(&name)
19509                .map_err(|e| FlowOrError::Error(e.at_line(line)));
19510        }
19511        if let Some(s) = arr_ref.as_str() {
19512            if self.strict_refs {
19513                return Err(PerlError::runtime(
19514                    format!(
19515                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19516                        s
19517                    ),
19518                    line,
19519                )
19520                .into());
19521            }
19522            return self
19523                .scope
19524                .shift_from_array(&s)
19525                .map_err(|e| FlowOrError::Error(e.at_line(line)));
19526        }
19527        Err(PerlError::runtime("shift argument is not an ARRAY reference", line).into())
19528    }
19529
19530    pub(crate) fn unshift_array_deref_multi(
19531        &mut self,
19532        arr_ref: PerlValue,
19533        vals: Vec<PerlValue>,
19534        line: usize,
19535    ) -> Result<i64, FlowOrError> {
19536        let mut flat: Vec<PerlValue> = Vec::new();
19537        for v in vals {
19538            if let Some(items) = v.as_array_vec() {
19539                flat.extend(items);
19540            } else {
19541                flat.push(v);
19542            }
19543        }
19544        if let Some(r) = arr_ref.as_array_ref() {
19545            let mut w = r.write();
19546            for (i, v) in flat.into_iter().enumerate() {
19547                w.insert(i, v);
19548            }
19549            return Ok(w.len() as i64);
19550        }
19551        if let Some(name) = arr_ref.as_array_binding_name() {
19552            let arr = self
19553                .scope
19554                .get_array_mut(&name)
19555                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19556            for (i, v) in flat.into_iter().enumerate() {
19557                arr.insert(i, v);
19558            }
19559            return Ok(arr.len() as i64);
19560        }
19561        if let Some(s) = arr_ref.as_str() {
19562            if self.strict_refs {
19563                return Err(PerlError::runtime(
19564                    format!(
19565                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19566                        s
19567                    ),
19568                    line,
19569                )
19570                .into());
19571            }
19572            let name = s.to_string();
19573            let arr = self
19574                .scope
19575                .get_array_mut(&name)
19576                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19577            for (i, v) in flat.into_iter().enumerate() {
19578                arr.insert(i, v);
19579            }
19580            return Ok(arr.len() as i64);
19581        }
19582        Err(PerlError::runtime("unshift argument is not an ARRAY reference", line).into())
19583    }
19584
19585    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
19586    /// / compiler wraps `splice` like other context-sensitive builtins).
19587    pub(crate) fn splice_array_deref(
19588        &mut self,
19589        aref: PerlValue,
19590        offset_val: PerlValue,
19591        length_val: PerlValue,
19592        rep_vals: Vec<PerlValue>,
19593        line: usize,
19594    ) -> Result<PerlValue, FlowOrError> {
19595        let ctx = self.wantarray_kind;
19596        if let Some(r) = aref.as_array_ref() {
19597            let arr_len = r.read().len();
19598            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
19599            let mut w = r.write();
19600            let removed: Vec<PerlValue> = w.drain(off..end).collect();
19601            for (i, v) in rep_vals.into_iter().enumerate() {
19602                w.insert(off + i, v);
19603            }
19604            return Ok(match ctx {
19605                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
19606                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
19607            });
19608        }
19609        if let Some(name) = aref.as_array_binding_name() {
19610            let arr_len = self.scope.array_len(&name);
19611            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
19612            let removed = self
19613                .scope
19614                .splice_in_place(&name, off, end, rep_vals)
19615                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19616            return Ok(match ctx {
19617                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
19618                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
19619            });
19620        }
19621        if let Some(s) = aref.as_str() {
19622            if self.strict_refs {
19623                return Err(PerlError::runtime(
19624                    format!(
19625                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
19626                        s
19627                    ),
19628                    line,
19629                )
19630                .into());
19631            }
19632            let arr_len = self.scope.array_len(&s);
19633            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
19634            let removed = self
19635                .scope
19636                .splice_in_place(&s, off, end, rep_vals)
19637                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19638            return Ok(match ctx {
19639                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
19640                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
19641            });
19642        }
19643        Err(PerlError::runtime("splice argument is not an ARRAY reference", line).into())
19644    }
19645
19646    pub(crate) fn eval_splice_expr(
19647        &mut self,
19648        array: &Expr,
19649        offset: Option<&Expr>,
19650        length: Option<&Expr>,
19651        replacement: &[Expr],
19652        ctx: WantarrayCtx,
19653        line: usize,
19654    ) -> Result<PerlValue, FlowOrError> {
19655        if let Some(aref) = self.try_eval_array_deref_container(array)? {
19656            let offset_val = if let Some(o) = offset {
19657                self.eval_expr(o)?
19658            } else {
19659                PerlValue::integer(0)
19660            };
19661            let length_val = if let Some(l) = length {
19662                self.eval_expr(l)?
19663            } else {
19664                PerlValue::UNDEF
19665            };
19666            let mut rep_vals = Vec::new();
19667            for r in replacement {
19668                rep_vals.push(self.eval_expr(r)?);
19669            }
19670            let saved = self.wantarray_kind;
19671            self.wantarray_kind = ctx;
19672            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
19673            self.wantarray_kind = saved;
19674            return out;
19675        }
19676        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
19677        let arr_len = self.scope.array_len(&arr_name);
19678        let offset_val = if let Some(o) = offset {
19679            self.eval_expr(o)?
19680        } else {
19681            PerlValue::integer(0)
19682        };
19683        let length_val = if let Some(l) = length {
19684            self.eval_expr(l)?
19685        } else {
19686            PerlValue::UNDEF
19687        };
19688        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
19689        let mut rep_vals = Vec::new();
19690        for r in replacement {
19691            rep_vals.push(self.eval_expr(r)?);
19692        }
19693        let removed = self
19694            .scope
19695            .splice_in_place(&arr_name, off, end, rep_vals)
19696            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
19697        Ok(match ctx {
19698            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
19699            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
19700        })
19701    }
19702
19703    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
19704    pub(crate) fn keys_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
19705        if let Some(h) = val.as_hash_map() {
19706            Ok(PerlValue::array(
19707                h.keys().map(|k| PerlValue::string(k.clone())).collect(),
19708            ))
19709        } else if let Some(r) = val.as_hash_ref() {
19710            Ok(PerlValue::array(
19711                r.read()
19712                    .keys()
19713                    .map(|k| PerlValue::string(k.clone()))
19714                    .collect(),
19715            ))
19716        } else {
19717            Err(PerlError::runtime("keys requires hash", line).into())
19718        }
19719    }
19720
19721    pub(crate) fn eval_keys_expr(
19722        &mut self,
19723        expr: &Expr,
19724        line: usize,
19725    ) -> Result<PerlValue, FlowOrError> {
19726        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
19727        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
19728        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
19729        Self::keys_from_value(val, line)
19730    }
19731
19732    /// Result of `values EXPR` after `EXPR` has been evaluated.
19733    pub(crate) fn values_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
19734        if let Some(h) = val.as_hash_map() {
19735            Ok(PerlValue::array(h.values().cloned().collect()))
19736        } else if let Some(r) = val.as_hash_ref() {
19737            Ok(PerlValue::array(r.read().values().cloned().collect()))
19738        } else {
19739            Err(PerlError::runtime("values requires hash", line).into())
19740        }
19741    }
19742
19743    pub(crate) fn eval_values_expr(
19744        &mut self,
19745        expr: &Expr,
19746        line: usize,
19747    ) -> Result<PerlValue, FlowOrError> {
19748        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
19749        Self::values_from_value(val, line)
19750    }
19751
19752    pub(crate) fn eval_delete_operand(
19753        &mut self,
19754        expr: &Expr,
19755        line: usize,
19756    ) -> Result<PerlValue, FlowOrError> {
19757        match &expr.kind {
19758            ExprKind::HashElement { hash, key } => {
19759                let k = self.eval_expr(key)?.to_string();
19760                self.touch_env_hash(hash);
19761                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
19762                    let class = obj
19763                        .as_blessed_ref()
19764                        .map(|b| b.class.clone())
19765                        .unwrap_or_default();
19766                    let full = format!("{}::DELETE", class);
19767                    if let Some(sub) = self.subs.get(&full).cloned() {
19768                        return self.call_sub(
19769                            &sub,
19770                            vec![obj, PerlValue::string(k)],
19771                            WantarrayCtx::Scalar,
19772                            line,
19773                        );
19774                    }
19775                }
19776                self.scope
19777                    .delete_hash_element(hash, &k)
19778                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
19779            }
19780            ExprKind::ArrayElement { array, index } => {
19781                self.check_strict_array_var(array, line)?;
19782                let idx = self.eval_expr(index)?.to_int();
19783                let aname = self.stash_array_name_for_package(array);
19784                self.scope
19785                    .delete_array_element(&aname, idx)
19786                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
19787            }
19788            ExprKind::ArrowDeref {
19789                expr: inner,
19790                index,
19791                kind: DerefKind::Hash,
19792            } => {
19793                let k = self.eval_expr(index)?.to_string();
19794                let container = self.eval_expr(inner)?;
19795                self.delete_arrow_hash_element(container, &k, line)
19796                    .map_err(Into::into)
19797            }
19798            ExprKind::ArrowDeref {
19799                expr: inner,
19800                index,
19801                kind: DerefKind::Array,
19802            } => {
19803                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
19804                    return Err(PerlError::runtime(
19805                        "delete on array element needs scalar subscript",
19806                        line,
19807                    )
19808                    .into());
19809                }
19810                let container = self.eval_expr(inner)?;
19811                let idx = self.eval_expr(index)?.to_int();
19812                self.delete_arrow_array_element(container, idx, line)
19813                    .map_err(Into::into)
19814            }
19815            _ => Err(PerlError::runtime("delete requires hash or array element", line).into()),
19816        }
19817    }
19818
19819    /// Evaluate a deref-chain in "exists mode" — like [`Self::eval_expr`] but
19820    /// recursively walks `ArrowDeref` chains and turns undef-intermediate
19821    /// derefs into undef (instead of erroring). Used by
19822    /// [`Self::eval_exists_operand`] so `exists $h{x}{y}{z}` returns 0 for
19823    /// any missing level. (BUG-009)
19824    fn eval_expr_exists_mode(&mut self, expr: &Expr) -> Result<PerlValue, FlowOrError> {
19825        match &expr.kind {
19826            ExprKind::ArrowDeref {
19827                expr: inner,
19828                index,
19829                kind: DerefKind::Hash,
19830            } => {
19831                let inner_val = self.eval_expr_exists_mode(inner)?;
19832                if inner_val.is_undef() {
19833                    return Ok(PerlValue::UNDEF);
19834                }
19835                if let Some(r) = inner_val.as_hash_ref() {
19836                    let k = self.eval_expr(index)?.to_string();
19837                    return Ok(r.read().get(&k).cloned().unwrap_or(PerlValue::UNDEF));
19838                }
19839                if let Some(b) = inner_val.as_blessed_ref() {
19840                    let data = b.data.read();
19841                    if let Some(r) = data.as_hash_ref() {
19842                        let k = self.eval_expr(index)?.to_string();
19843                        return Ok(r.read().get(&k).cloned().unwrap_or(PerlValue::UNDEF));
19844                    }
19845                }
19846                // Struct / class instance — look up the field by name and
19847                // return its value. Without this, `exists $struct->{f}->{k}`
19848                // soft-fails to false even when the field is a real hashref.
19849                if let Some(s) = inner_val.as_struct_inst() {
19850                    let k = self.eval_expr(index)?.to_string();
19851                    if let Some(idx) = s.def.field_index(&k) {
19852                        return Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF));
19853                    }
19854                    return Ok(PerlValue::UNDEF);
19855                }
19856                if let Some(c) = inner_val.as_class_inst() {
19857                    let k = self.eval_expr(index)?.to_string();
19858                    if let Some(idx) = c.def.field_index(&k) {
19859                        return Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF));
19860                    }
19861                    return Ok(PerlValue::UNDEF);
19862                }
19863                Ok(PerlValue::UNDEF)
19864            }
19865            ExprKind::ArrowDeref {
19866                expr: inner,
19867                index,
19868                kind: DerefKind::Array,
19869            } => {
19870                let inner_val = self.eval_expr_exists_mode(inner)?;
19871                if inner_val.is_undef() {
19872                    return Ok(PerlValue::UNDEF);
19873                }
19874                if let Some(r) = inner_val.as_array_ref() {
19875                    let idx = self.eval_expr(index)?.to_int();
19876                    let arr = r.read();
19877                    let i = if idx < 0 {
19878                        (arr.len() as i64 + idx).max(0) as usize
19879                    } else {
19880                        idx as usize
19881                    };
19882                    return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
19883                }
19884                Ok(PerlValue::UNDEF)
19885            }
19886            _ => self.eval_expr(expr),
19887        }
19888    }
19889
19890    pub(crate) fn eval_exists_operand(
19891        &mut self,
19892        expr: &Expr,
19893        line: usize,
19894    ) -> Result<PerlValue, FlowOrError> {
19895        match &expr.kind {
19896            ExprKind::HashElement { hash, key } => {
19897                let k = self.eval_expr(key)?.to_string();
19898                self.touch_env_hash(hash);
19899                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
19900                    let class = obj
19901                        .as_blessed_ref()
19902                        .map(|b| b.class.clone())
19903                        .unwrap_or_default();
19904                    let full = format!("{}::EXISTS", class);
19905                    if let Some(sub) = self.subs.get(&full).cloned() {
19906                        return self.call_sub(
19907                            &sub,
19908                            vec![obj, PerlValue::string(k)],
19909                            WantarrayCtx::Scalar,
19910                            line,
19911                        );
19912                    }
19913                }
19914                Ok(PerlValue::integer(
19915                    if self.scope.exists_hash_element(hash, &k) {
19916                        1
19917                    } else {
19918                        0
19919                    },
19920                ))
19921            }
19922            ExprKind::ArrayElement { array, index } => {
19923                self.check_strict_array_var(array, line)?;
19924                let idx = self.eval_expr(index)?.to_int();
19925                let aname = self.stash_array_name_for_package(array);
19926                Ok(PerlValue::integer(
19927                    if self.scope.exists_array_element(&aname, idx) {
19928                        1
19929                    } else {
19930                        0
19931                    },
19932                ))
19933            }
19934            ExprKind::ArrowDeref {
19935                expr: inner,
19936                index,
19937                kind: DerefKind::Hash,
19938            } => {
19939                let k = self.eval_expr(index)?.to_string();
19940                // Evaluate the chain in "exists mode" — undef intermediates
19941                // propagate as undef instead of erroring on missing-key
19942                // deref, matching Perl's `exists $h{x}{y}{z}` returning 0
19943                // for any missing level. (BUG-009)
19944                let container = match self.eval_expr_exists_mode(inner) {
19945                    Ok(v) => v,
19946                    Err(_) => return Ok(PerlValue::integer(0)),
19947                };
19948                if container.is_undef() {
19949                    return Ok(PerlValue::integer(0));
19950                }
19951                let yes = self.exists_arrow_hash_element(container, &k, line)?;
19952                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
19953            }
19954            ExprKind::ArrowDeref {
19955                expr: inner,
19956                index,
19957                kind: DerefKind::Array,
19958            } => {
19959                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
19960                    return Err(PerlError::runtime(
19961                        "exists on array element needs scalar subscript",
19962                        line,
19963                    )
19964                    .into());
19965                }
19966                let container = match self.eval_expr_exists_mode(inner) {
19967                    Ok(v) => v,
19968                    Err(_) => return Ok(PerlValue::integer(0)),
19969                };
19970                if container.is_undef() {
19971                    return Ok(PerlValue::integer(0));
19972                }
19973                let idx = self.eval_expr(index)?.to_int();
19974                let yes = self.exists_arrow_array_element(container, idx, line)?;
19975                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
19976            }
19977            _ => Err(PerlError::runtime("exists requires hash or array element", line).into()),
19978        }
19979    }
19980
19981    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
19982    ///
19983    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
19984    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
19985    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
19986    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
19987    /// the new path amortizes the handshake across the whole map.
19988    pub(crate) fn eval_pmap_remote(
19989        &mut self,
19990        cluster_pv: PerlValue,
19991        list_pv: PerlValue,
19992        show_progress: bool,
19993        block: &Block,
19994        flat_outputs: bool,
19995        line: usize,
19996    ) -> Result<PerlValue, FlowOrError> {
19997        let Some(cluster) = cluster_pv.as_remote_cluster() else {
19998            return Err(PerlError::runtime("pmap_on: expected cluster(...) value", line).into());
19999        };
20000        let items = list_pv.to_list();
20001        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20002        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
20003            return Err(PerlError::runtime(
20004                "pmap_on: mysync/atomic capture is not supported for remote workers",
20005                line,
20006            )
20007            .into());
20008        }
20009        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
20010            .map_err(|e| PerlError::runtime(e, line))?;
20011        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
20012        let block_src = crate::fmt::format_block(block);
20013        let item_jsons =
20014            crate::cluster::perl_items_to_json(&items).map_err(|e| PerlError::runtime(e, line))?;
20015
20016        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
20017        // synchronous from the caller's POV, so we drive the bar before/after the call.
20018        let pmap_progress = PmapProgress::new(show_progress, items.len());
20019        let result_values =
20020            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
20021                .map_err(|e| PerlError::runtime(format!("pmap_on remote: {e}"), line))?;
20022        for _ in 0..result_values.len() {
20023            pmap_progress.tick();
20024        }
20025        pmap_progress.finish();
20026
20027        if flat_outputs {
20028            let flattened: Vec<PerlValue> = result_values
20029                .into_iter()
20030                .flat_map(|v| v.map_flatten_outputs(true))
20031                .collect();
20032            Ok(PerlValue::array(flattened))
20033        } else {
20034            Ok(PerlValue::array(result_values))
20035        }
20036    }
20037
20038    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
20039    pub(crate) fn eval_par_lines_expr(
20040        &mut self,
20041        path: &Expr,
20042        callback: &Expr,
20043        progress: Option<&Expr>,
20044        line: usize,
20045    ) -> Result<PerlValue, FlowOrError> {
20046        let show_progress = progress
20047            .map(|p| self.eval_expr(p))
20048            .transpose()?
20049            .map(|v| v.is_true())
20050            .unwrap_or(false);
20051        let path_s = self.eval_expr(path)?.to_string();
20052        let cb_val = self.eval_expr(callback)?;
20053        let sub = if let Some(s) = cb_val.as_code_ref() {
20054            s
20055        } else {
20056            return Err(PerlError::runtime(
20057                "par_lines: second argument must be a code reference",
20058                line,
20059            )
20060            .into());
20061        };
20062        let subs = self.subs.clone();
20063        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20064        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
20065            FlowOrError::Error(PerlError::runtime(format!("par_lines: {}", e), line))
20066        })?;
20067        let mmap = unsafe {
20068            memmap2::Mmap::map(&file).map_err(|e| {
20069                FlowOrError::Error(PerlError::runtime(format!("par_lines: mmap: {}", e), line))
20070            })?
20071        };
20072        let data: &[u8] = &mmap;
20073        if data.is_empty() {
20074            return Ok(PerlValue::UNDEF);
20075        }
20076        let line_total = crate::par_lines::line_count_bytes(data);
20077        let pmap_progress = PmapProgress::new(show_progress, line_total);
20078        if self.num_threads == 0 {
20079            self.num_threads = rayon::current_num_threads();
20080        }
20081        let num_chunks = self.num_threads.saturating_mul(8).max(1);
20082        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
20083        chunks.into_par_iter().try_for_each(|(start, end)| {
20084            let slice = &data[start..end];
20085            let mut s = 0usize;
20086            while s < slice.len() {
20087                let e = slice[s..]
20088                    .iter()
20089                    .position(|&b| b == b'\n')
20090                    .map(|p| s + p)
20091                    .unwrap_or(slice.len());
20092                let line_bytes = &slice[s..e];
20093                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
20094                let mut local_interp = VMHelper::new();
20095                local_interp.subs = subs.clone();
20096                local_interp.scope.restore_capture(&scope_capture);
20097                local_interp
20098                    .scope
20099                    .restore_atomics(&atomic_arrays, &atomic_hashes);
20100                local_interp.enable_parallel_guard();
20101                local_interp.scope.set_topic(PerlValue::string(line_str));
20102                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
20103                    Ok(_) => {}
20104                    Err(e) => return Err(e),
20105                }
20106                pmap_progress.tick();
20107                if e >= slice.len() {
20108                    break;
20109                }
20110                s = e + 1;
20111            }
20112            Ok(())
20113        })?;
20114        pmap_progress.finish();
20115        Ok(PerlValue::UNDEF)
20116    }
20117
20118    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
20119    pub(crate) fn eval_par_walk_expr(
20120        &mut self,
20121        path: &Expr,
20122        callback: &Expr,
20123        progress: Option<&Expr>,
20124        line: usize,
20125    ) -> Result<PerlValue, FlowOrError> {
20126        let show_progress = progress
20127            .map(|p| self.eval_expr(p))
20128            .transpose()?
20129            .map(|v| v.is_true())
20130            .unwrap_or(false);
20131        let path_val = self.eval_expr(path)?;
20132        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
20133            arr.into_iter()
20134                .map(|v| PathBuf::from(v.to_string()))
20135                .collect()
20136        } else {
20137            vec![PathBuf::from(path_val.to_string())]
20138        };
20139        let cb_val = self.eval_expr(callback)?;
20140        let sub = if let Some(s) = cb_val.as_code_ref() {
20141            s
20142        } else {
20143            return Err(PerlError::runtime(
20144                "par_walk: second argument must be a code reference",
20145                line,
20146            )
20147            .into());
20148        };
20149        let subs = self.subs.clone();
20150        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20151
20152        if show_progress {
20153            let paths = crate::par_walk::collect_paths(&roots);
20154            let pmap_progress = PmapProgress::new(true, paths.len());
20155            paths.into_par_iter().try_for_each(|p| {
20156                let s = p.to_string_lossy().into_owned();
20157                let mut local_interp = VMHelper::new();
20158                local_interp.subs = subs.clone();
20159                local_interp.scope.restore_capture(&scope_capture);
20160                local_interp
20161                    .scope
20162                    .restore_atomics(&atomic_arrays, &atomic_hashes);
20163                local_interp.enable_parallel_guard();
20164                local_interp.scope.set_topic(PerlValue::string(s));
20165                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
20166                    Ok(_) => {}
20167                    Err(e) => return Err(e),
20168                }
20169                pmap_progress.tick();
20170                Ok(())
20171            })?;
20172            pmap_progress.finish();
20173        } else {
20174            for r in &roots {
20175                par_walk_recursive(
20176                    r.as_path(),
20177                    &sub,
20178                    &subs,
20179                    &scope_capture,
20180                    &atomic_arrays,
20181                    &atomic_hashes,
20182                    line,
20183                )?;
20184            }
20185        }
20186        Ok(PerlValue::UNDEF)
20187    }
20188
20189    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
20190    pub(crate) fn builtin_par_sed(
20191        &mut self,
20192        args: &[PerlValue],
20193        line: usize,
20194        has_progress: bool,
20195    ) -> PerlResult<PerlValue> {
20196        let show_progress = if has_progress {
20197            args.last().map(|v| v.is_true()).unwrap_or(false)
20198        } else {
20199            false
20200        };
20201        let slice = if has_progress {
20202            &args[..args.len().saturating_sub(1)]
20203        } else {
20204            args
20205        };
20206        if slice.len() < 3 {
20207            return Err(PerlError::runtime(
20208                "par_sed: need pattern, replacement, and at least one file path",
20209                line,
20210            ));
20211        }
20212        let pat_val = &slice[0];
20213        let repl = slice[1].to_string();
20214        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
20215
20216        let re = if let Some(rx) = pat_val.as_regex() {
20217            rx
20218        } else {
20219            let pattern = pat_val.to_string();
20220            match self.compile_regex(&pattern, "g", line) {
20221                Ok(r) => r,
20222                Err(FlowOrError::Error(e)) => return Err(e),
20223                Err(FlowOrError::Flow(f)) => {
20224                    return Err(PerlError::runtime(format!("par_sed: {:?}", f), line))
20225                }
20226            }
20227        };
20228
20229        let pmap = PmapProgress::new(show_progress, files.len());
20230        let touched = AtomicUsize::new(0);
20231        files.par_iter().try_for_each(|path| {
20232            let content = read_file_text_perl_compat(path)
20233                .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
20234            let new_s = re.replace_all(&content, &repl);
20235            if new_s != content {
20236                std::fs::write(path, new_s.as_bytes())
20237                    .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
20238                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
20239            }
20240            pmap.tick();
20241            Ok(())
20242        })?;
20243        pmap.finish();
20244        Ok(PerlValue::integer(
20245            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
20246        ))
20247    }
20248
20249    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
20250    pub(crate) fn eval_pwatch_expr(
20251        &mut self,
20252        path: &Expr,
20253        callback: &Expr,
20254        line: usize,
20255    ) -> Result<PerlValue, FlowOrError> {
20256        let pattern_s = self.eval_expr(path)?.to_string();
20257        let cb_val = self.eval_expr(callback)?;
20258        let sub = if let Some(s) = cb_val.as_code_ref() {
20259            s
20260        } else {
20261            return Err(PerlError::runtime(
20262                "pwatch: second argument must be a code reference",
20263                line,
20264            )
20265            .into());
20266        };
20267        let subs = self.subs.clone();
20268        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
20269        crate::pwatch::run_pwatch(
20270            &pattern_s,
20271            sub,
20272            subs,
20273            scope_capture,
20274            atomic_arrays,
20275            atomic_hashes,
20276            line,
20277        )
20278        .map_err(FlowOrError::Error)
20279    }
20280
20281    /// Interpolate `$var` in s/// replacement strings, preserving numeric backrefs ($1, $2, etc.).
20282    fn interpolate_replacement_string(&self, replacement: &str) -> String {
20283        let mut out = String::with_capacity(replacement.len());
20284        let chars: Vec<char> = replacement.chars().collect();
20285        let mut i = 0;
20286        while i < chars.len() {
20287            if chars[i] == '\\' && i + 1 < chars.len() {
20288                out.push(chars[i]);
20289                out.push(chars[i + 1]);
20290                i += 2;
20291                continue;
20292            }
20293            if chars[i] == '$' && i + 1 < chars.len() {
20294                let start = i;
20295                i += 1;
20296                if chars[i].is_ascii_digit() {
20297                    out.push('$');
20298                    while i < chars.len() && chars[i].is_ascii_digit() {
20299                        out.push(chars[i]);
20300                        i += 1;
20301                    }
20302                    continue;
20303                }
20304                if chars[i] == '&' || chars[i] == '`' || chars[i] == '\'' {
20305                    out.push('$');
20306                    out.push(chars[i]);
20307                    i += 1;
20308                    continue;
20309                }
20310                if !chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{' {
20311                    out.push('$');
20312                    continue;
20313                }
20314                let mut name = String::new();
20315                if chars[i] == '{' {
20316                    i += 1;
20317                    while i < chars.len() && chars[i] != '}' {
20318                        name.push(chars[i]);
20319                        i += 1;
20320                    }
20321                    if i < chars.len() {
20322                        i += 1;
20323                    }
20324                } else {
20325                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20326                        name.push(chars[i]);
20327                        i += 1;
20328                    }
20329                }
20330                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
20331                    let val = self.scope.get_scalar(&name);
20332                    out.push_str(&val.to_string());
20333                } else if !name.is_empty() {
20334                    out.push_str(&replacement[start..i]);
20335                } else {
20336                    out.push('$');
20337                }
20338                continue;
20339            }
20340            out.push(chars[i]);
20341            i += 1;
20342        }
20343        out
20344    }
20345
20346    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
20347    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
20348        let mut out = String::with_capacity(pattern.len());
20349        let chars: Vec<char> = pattern.chars().collect();
20350        let mut i = 0;
20351        while i < chars.len() {
20352            if chars[i] == '\\' && i + 1 < chars.len() {
20353                // Preserve escape sequences (including \$ which is literal $)
20354                out.push(chars[i]);
20355                out.push(chars[i + 1]);
20356                i += 2;
20357                continue;
20358            }
20359            if chars[i] == '$' && i + 1 < chars.len() {
20360                i += 1;
20361                // `$` at end of pattern is an anchor, not a variable
20362                if i >= chars.len()
20363                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
20364                {
20365                    out.push('$');
20366                    continue;
20367                }
20368                let mut name = String::new();
20369                if chars[i] == '{' {
20370                    i += 1;
20371                    while i < chars.len() && chars[i] != '}' {
20372                        name.push(chars[i]);
20373                        i += 1;
20374                    }
20375                    if i < chars.len() {
20376                        i += 1;
20377                    } // skip }
20378                } else {
20379                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
20380                        name.push(chars[i]);
20381                        i += 1;
20382                    }
20383                }
20384                if !name.is_empty() {
20385                    let val = self.scope.get_scalar(&name);
20386                    out.push_str(&val.to_string());
20387                } else {
20388                    out.push('$');
20389                }
20390                continue;
20391            }
20392            out.push(chars[i]);
20393            i += 1;
20394        }
20395        out
20396    }
20397
20398    pub(crate) fn compile_regex(
20399        &mut self,
20400        pattern: &str,
20401        flags: &str,
20402        line: usize,
20403    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
20404        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
20405        let pattern = if pattern.contains('$') || pattern.contains('@') {
20406            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
20407        } else {
20408            std::borrow::Cow::Borrowed(pattern)
20409        };
20410        let pattern = pattern.as_ref();
20411        // Fast path: same regex as last call (common in loops).
20412        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
20413        let multiline = self.multiline_match;
20414        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
20415            if lp == pattern && lf == flags && *lm == multiline {
20416                return Ok(lr.clone());
20417            }
20418        }
20419        // Slow path: HashMap lookup
20420        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
20421        if let Some(cached) = self.regex_cache.get(&key) {
20422            self.regex_last = Some((
20423                pattern.to_string(),
20424                flags.to_string(),
20425                multiline,
20426                cached.clone(),
20427            ));
20428            return Ok(cached.clone());
20429        }
20430        let expanded = expand_perl_regex_quotemeta(pattern);
20431        let expanded = expand_perl_regex_octal_escapes(&expanded);
20432        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
20433        let mut re_str = String::new();
20434        if flags.contains('i') {
20435            re_str.push_str("(?i)");
20436        }
20437        if flags.contains('s') {
20438            re_str.push_str("(?s)");
20439        }
20440        if flags.contains('m') {
20441            re_str.push_str("(?m)");
20442        }
20443        if flags.contains('x') {
20444            re_str.push_str("(?x)");
20445        }
20446        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
20447        if multiline {
20448            re_str.push_str("(?s)");
20449        }
20450        re_str.push_str(&expanded);
20451        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
20452            FlowOrError::Error(PerlError::runtime(
20453                format!("Invalid regex /{}/: {}", pattern, e),
20454                line,
20455            ))
20456        })?;
20457        let arc = re;
20458        self.regex_last = Some((
20459            pattern.to_string(),
20460            flags.to_string(),
20461            multiline,
20462            arc.clone(),
20463        ));
20464        self.regex_cache.insert(key, arc.clone());
20465        Ok(arc)
20466    }
20467
20468    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
20469    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
20470        if self.last_readline_handle.is_empty() {
20471            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
20472        }
20473        let n = *self
20474            .handle_line_numbers
20475            .get(&self.last_readline_handle)
20476            .unwrap_or(&0);
20477        if n <= 0 {
20478            return None;
20479        }
20480        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
20481        {
20482            return Some(("<>".to_string(), n));
20483        }
20484        if self.last_readline_handle == "STDIN" {
20485            return Some((self.last_stdin_die_bracket.clone(), n));
20486        }
20487        Some((format!("<{}>", self.last_readline_handle), n))
20488    }
20489
20490    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
20491    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
20492        let mut s = format!(" at {} line {}", self.file, source_line);
20493        if let Some((bracket, n)) = self.die_warn_io_annotation() {
20494            s.push_str(&format!(", {} line {}.", bracket, n));
20495        } else {
20496            s.push('.');
20497        }
20498        s
20499    }
20500
20501    /// Process a line in -n/-p mode via the VM.
20502    ///
20503    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
20504    /// file so `eof` with no arguments matches Perl behavior on that line.
20505    pub fn process_line(
20506        &mut self,
20507        line_str: &str,
20508        _program: &Program,
20509        is_last_input_line: bool,
20510    ) -> PerlResult<Option<String>> {
20511        let chunk = self
20512            .line_mode_chunk
20513            .as_ref()
20514            .expect("process_line called without compiled chunk — execute() must run first")
20515            .clone();
20516        crate::run_line_body(&chunk, self, line_str, is_last_input_line)
20517    }
20518}
20519
20520/// Mirrors `vm.rs::both_non_numeric_strings`. Used by the tree-walker's
20521/// `==` / `!=` to decide whether to fall back to string compare in
20522/// stryke non-compat mode.
20523fn both_non_numeric_strings_iv(a: &PerlValue, b: &PerlValue) -> bool {
20524    if !a.is_string_like() || !b.is_string_like() {
20525        return false;
20526    }
20527    let sa = a.to_string();
20528    let sb = b.to_string();
20529    let looks = |s: &str| {
20530        let t = s.trim();
20531        !t.is_empty() && t.parse::<f64>().is_ok()
20532    };
20533    !looks(&sa) && !looks(&sb)
20534}
20535
20536fn par_walk_invoke_entry(
20537    path: &Path,
20538    sub: &Arc<PerlSub>,
20539    subs: &HashMap<String, Arc<PerlSub>>,
20540    scope_capture: &[(String, PerlValue)],
20541    atomic_arrays: &[(String, crate::scope::AtomicArray)],
20542    atomic_hashes: &[(String, crate::scope::AtomicHash)],
20543    line: usize,
20544) -> Result<(), FlowOrError> {
20545    let s = path.to_string_lossy().into_owned();
20546    let mut local_interp = VMHelper::new();
20547    local_interp.subs = subs.clone();
20548    local_interp.scope.restore_capture(scope_capture);
20549    local_interp
20550        .scope
20551        .restore_atomics(atomic_arrays, atomic_hashes);
20552    local_interp.enable_parallel_guard();
20553    local_interp.scope.set_topic(PerlValue::string(s));
20554    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
20555    Ok(())
20556}
20557
20558fn par_walk_recursive(
20559    path: &Path,
20560    sub: &Arc<PerlSub>,
20561    subs: &HashMap<String, Arc<PerlSub>>,
20562    scope_capture: &[(String, PerlValue)],
20563    atomic_arrays: &[(String, crate::scope::AtomicArray)],
20564    atomic_hashes: &[(String, crate::scope::AtomicHash)],
20565    line: usize,
20566) -> Result<(), FlowOrError> {
20567    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
20568        return par_walk_invoke_entry(
20569            path,
20570            sub,
20571            subs,
20572            scope_capture,
20573            atomic_arrays,
20574            atomic_hashes,
20575            line,
20576        );
20577    }
20578    if !path.is_dir() {
20579        return Ok(());
20580    }
20581    par_walk_invoke_entry(
20582        path,
20583        sub,
20584        subs,
20585        scope_capture,
20586        atomic_arrays,
20587        atomic_hashes,
20588        line,
20589    )?;
20590    let read = match std::fs::read_dir(path) {
20591        Ok(r) => r,
20592        Err(_) => return Ok(()),
20593    };
20594    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
20595    entries.par_iter().try_for_each(|e| {
20596        par_walk_recursive(
20597            &e.path(),
20598            sub,
20599            subs,
20600            scope_capture,
20601            atomic_arrays,
20602            atomic_hashes,
20603            line,
20604        )
20605    })?;
20606    Ok(())
20607}
20608
20609/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
20610/// Reformat Rust's `{:e}` / `{:E}` exponent style (`1.234568e4`) to the
20611/// Perl/C convention (`1.234568e+04`). Adds a sign character to the
20612/// exponent and zero-pads it to at least two digits.
20613/// Perl-style magical string increment.
20614///
20615/// Returns `Some(new)` when `s` matches `^[A-Za-z]+[0-9]*$` (i.e. some
20616/// letters, optionally followed by digits, ending at the end of string)
20617/// or is the empty string (which becomes `"1"`). Returns `None` for any
20618/// other shape — pure digits, leading whitespace, mixed letters/digits,
20619/// embedded punctuation, etc. — so the caller can fall back to a plain
20620/// numeric increment.
20621///
20622/// Carry rules:
20623/// - In the digit suffix, `9 -> 0` carries left.
20624/// - In the letter prefix, `z -> a` and `Z -> A` carry left.
20625/// - When a carry exits the leftmost letter, a fresh `a` or `A` is
20626///   prepended (case-matched to the first character of the original).
20627///
20628/// Split a `PerlValue` into approximately `n_threads` chunks for the
20629/// `par { BLOCK }` runtime. Strings are partitioned on UTF-8 char-aligned
20630/// byte boundaries; arrays/lists on element boundaries. Other scalar
20631/// types (int, float, undef, ref) return a single-chunk Vec containing
20632/// the value unchanged — the caller should handle this fallback.
20633///
20634/// Returned chunks are themselves `PerlValue` so the worker can bind
20635/// each to `$_` and invoke the user's block.
20636fn par_chunk_value(v: &PerlValue, n_threads: usize) -> Vec<PerlValue> {
20637    let n = n_threads.max(1);
20638    // String input: split on char boundaries.
20639    if let Some(s) = v.as_str() {
20640        let bytes = s.as_bytes();
20641        if bytes.len() < 16_384 || n < 2 {
20642            return vec![PerlValue::string(s)];
20643        }
20644        let target = bytes.len().div_ceil(n);
20645        let mut splits = vec![0usize];
20646        let mut cursor = target;
20647        while cursor < bytes.len() {
20648            // Walk forward until we hit a UTF-8 leading byte (`0xxxxxxx` or `11xxxxxx`).
20649            while cursor < bytes.len() && (bytes[cursor] & 0xC0) == 0x80 {
20650                cursor += 1;
20651            }
20652            splits.push(cursor);
20653            cursor += target;
20654        }
20655        splits.push(bytes.len());
20656        return splits
20657            .windows(2)
20658            .map(|w| {
20659                let chunk = std::str::from_utf8(&bytes[w[0]..w[1]]).unwrap_or("");
20660                PerlValue::string(chunk.to_string())
20661            })
20662            .collect();
20663    }
20664    // Array / list input: split on element boundaries.
20665    if let Some(arr) = v.as_array_vec() {
20666        if arr.len() < 32 || n < 2 {
20667            return vec![PerlValue::array(arr)];
20668        }
20669        let target = arr.len().div_ceil(n);
20670        let mut chunks = Vec::with_capacity(n);
20671        for slice in arr.chunks(target) {
20672            chunks.push(PerlValue::array(slice.to_vec()));
20673        }
20674        return chunks;
20675    }
20676    if let Some(arr_ref) = v.as_array_ref() {
20677        let arr = arr_ref.read().clone();
20678        if arr.len() < 32 || n < 2 {
20679            return vec![PerlValue::array(arr)];
20680        }
20681        let target = arr.len().div_ceil(n);
20682        let mut chunks = Vec::with_capacity(n);
20683        for slice in arr.chunks(target) {
20684            chunks.push(PerlValue::array(slice.to_vec()));
20685        }
20686        return chunks;
20687    }
20688    // Fallback: single chunk holding the original value.
20689    vec![v.clone()]
20690}
20691
20692/// Auto-merge a list of `par_reduce` per-chunk results when no explicit
20693/// reduce block is supplied. Picks the merger by inspecting the first
20694/// chunk's value type:
20695///
20696/// - **Hash with numeric values** → key-wise add (canonical histogram merge)
20697/// - **Number** → numeric `+`
20698/// - **Array / list** → concat
20699/// - **String** → concat
20700/// - **Anything else** → return chunks as a flat array (caller can post-process)
20701fn par_reduce_auto_merge(chunks: Vec<PerlValue>) -> PerlValue {
20702    if chunks.is_empty() {
20703        return PerlValue::UNDEF;
20704    }
20705    let first = &chunks[0];
20706    // Hash<number> add-merge.
20707    if let Some(_h) = first.as_hash_ref() {
20708        let mut out: indexmap::IndexMap<String, f64> = indexmap::IndexMap::new();
20709        for chunk in &chunks {
20710            if let Some(hr) = chunk.as_hash_ref() {
20711                for (k, v) in hr.read().iter() {
20712                    *out.entry(k.clone()).or_insert(0.0) += v.to_number();
20713                }
20714            }
20715        }
20716        // Round-trip integer values back to integers so `freq`-style
20717        // hashes stay integer-typed downstream.
20718        let mut indexmap_out: indexmap::IndexMap<String, PerlValue> = indexmap::IndexMap::new();
20719        for (k, v) in out {
20720            let pv = if v == v.trunc() && v.abs() < 1e15 {
20721                PerlValue::integer(v as i64)
20722            } else {
20723                PerlValue::float(v)
20724            };
20725            indexmap_out.insert(k, pv);
20726        }
20727        return PerlValue::hash_ref(Arc::new(parking_lot::RwLock::new(indexmap_out)));
20728    }
20729    // Numeric add-merge (int or float).
20730    if first.is_integer_like() || first.is_float_like() {
20731        let s: f64 = chunks.iter().map(|v| v.to_number()).sum();
20732        if s == s.trunc() && s.abs() < 1e15 {
20733            return PerlValue::integer(s as i64);
20734        }
20735        return PerlValue::float(s);
20736    }
20737    // Array concat.
20738    if first.as_array_vec().is_some() || first.as_array_ref().is_some() {
20739        let mut out = Vec::new();
20740        for v in &chunks {
20741            out.extend(v.map_flatten_outputs(true));
20742        }
20743        return PerlValue::array(out);
20744    }
20745    // String concat.
20746    if first.is_string_like() {
20747        let mut out = String::new();
20748        for v in &chunks {
20749            out.push_str(&v.to_string());
20750        }
20751        return PerlValue::string(out);
20752    }
20753    // Fallback: flat list of chunk results.
20754    PerlValue::array(chunks)
20755}
20756
20757/// Decrement has no magic counterpart in Perl 5; this helper is for `++`
20758/// only.
20759fn perl_magic_str_inc(s: &str) -> Option<String> {
20760    if s.is_empty() {
20761        return Some("1".to_string());
20762    }
20763    let bytes = s.as_bytes();
20764    let mut i = 0;
20765    while i < bytes.len() && bytes[i].is_ascii_alphabetic() {
20766        i += 1;
20767    }
20768    let letters_end = i;
20769    while i < bytes.len() && bytes[i].is_ascii_digit() {
20770        i += 1;
20771    }
20772    if i != bytes.len() {
20773        return None;
20774    }
20775    if letters_end == 0 {
20776        // Pure digits: Perl handles these as plain numbers, so defer.
20777        return None;
20778    }
20779
20780    let mut result: Vec<u8> = bytes.to_vec();
20781    let mut carry = true;
20782    let mut idx = result.len();
20783
20784    // Phase 1: digits, right to left.
20785    while carry && idx > letters_end {
20786        idx -= 1;
20787        if result[idx] == b'9' {
20788            result[idx] = b'0';
20789            // carry stays true
20790        } else {
20791            result[idx] += 1;
20792            carry = false;
20793        }
20794    }
20795
20796    // Phase 2: letters, right to left.
20797    while carry && idx > 0 {
20798        idx -= 1;
20799        let c = result[idx];
20800        if c == b'z' {
20801            result[idx] = b'a';
20802        } else if c == b'Z' {
20803            result[idx] = b'A';
20804        } else {
20805            result[idx] += 1;
20806            carry = false;
20807        }
20808    }
20809
20810    // Phase 3: prepend a fresh letter if the carry escaped.
20811    if carry {
20812        let prepend = if bytes[0].is_ascii_uppercase() {
20813            b'A'
20814        } else {
20815            b'a'
20816        };
20817        let mut grown = Vec::with_capacity(result.len() + 1);
20818        grown.push(prepend);
20819        grown.extend_from_slice(&result);
20820        return String::from_utf8(grown).ok();
20821    }
20822
20823    String::from_utf8(result).ok()
20824}
20825
20826/// `++$x` semantics: try magic string increment first when the value is
20827/// already a string; fall back to a numeric +1 for everything else
20828/// (integers, floats, undef, plain numeric strings).
20829pub(crate) fn perl_inc(v: &PerlValue) -> PerlValue {
20830    if let Some(s) = v.as_str() {
20831        if let Some(new_s) = perl_magic_str_inc(&s) {
20832            return PerlValue::string(new_s);
20833        }
20834    }
20835    PerlValue::integer(v.to_int() + 1)
20836}
20837
20838fn perl_exponent_form(rust_repr: &str, upper: bool) -> String {
20839    let marker = if upper { 'E' } else { 'e' };
20840    if let Some(pos) = rust_repr.find(marker) {
20841        let (mantissa, after) = rust_repr.split_at(pos);
20842        let exp_part = &after[1..]; // skip the 'e' / 'E'
20843        let (sign, digits) = match exp_part.chars().next() {
20844            Some('+') => ("+", &exp_part[1..]),
20845            Some('-') => ("-", &exp_part[1..]),
20846            _ => ("+", exp_part),
20847        };
20848        let padded = if digits.len() < 2 {
20849            format!("0{}", digits)
20850        } else {
20851            digits.to_string()
20852        };
20853        return format!("{}{}{}{}", mantissa, marker, sign, padded);
20854    }
20855    rust_repr.to_string()
20856}
20857
20858/// Hex-float format (`%a` / `%A`). Produces strings like `0x1.8p+0` for
20859/// 1.5 — sign, normalized hex mantissa, then `p[+-]N` decimal exponent of
20860/// the radix-2 form. Matches C99 / POSIX `%a`.
20861fn perl_hex_float(n: f64, upper: bool) -> String {
20862    if n.is_nan() {
20863        return if upper { "NAN" } else { "nan" }.to_string();
20864    }
20865    if n.is_infinite() {
20866        let sign = if n.is_sign_negative() { "-" } else { "" };
20867        let body = if upper { "INF" } else { "inf" };
20868        return format!("{}{}", sign, body);
20869    }
20870    let prefix = if upper { "0X" } else { "0x" };
20871    let p_letter = if upper { 'P' } else { 'p' };
20872    let bits = n.to_bits();
20873    let sign_bit = bits >> 63;
20874    let exp_bits = (bits >> 52) & 0x7FF;
20875    let mant_bits = bits & 0x000F_FFFF_FFFF_FFFF;
20876    let sign_str = if sign_bit == 1 { "-" } else { "" };
20877    if exp_bits == 0 && mant_bits == 0 {
20878        return format!("{}{}{}{}{}", sign_str, prefix, "0", p_letter, "+0");
20879    }
20880    let (lead_digit, exp_unbiased): (u64, i32) = if exp_bits == 0 {
20881        // Subnormal: implicit leading 0, exponent fixed at -1022.
20882        (0, -1022)
20883    } else {
20884        (1, (exp_bits as i32) - 1023)
20885    };
20886    let exp_sign = if exp_unbiased >= 0 { "+" } else { "-" };
20887    let exp_abs = exp_unbiased.unsigned_abs();
20888    if mant_bits == 0 {
20889        return format!(
20890            "{}{}{}{}{}{}",
20891            sign_str, prefix, lead_digit, p_letter, exp_sign, exp_abs
20892        );
20893    }
20894    // 52 mantissa bits = 13 hex digits.
20895    let mant_hex = format!("{:013x}", mant_bits);
20896    let trimmed = mant_hex.trim_end_matches('0');
20897    let mant_str = if upper {
20898        trimmed.to_uppercase()
20899    } else {
20900        trimmed.to_string()
20901    };
20902    format!(
20903        "{}{}{}.{}{}{}{}",
20904        sign_str, prefix, lead_digit, mant_str, p_letter, exp_sign, exp_abs
20905    )
20906}
20907
20908/// Format a value with `%g`-style "shortest of %e or %f, strip trailing
20909/// zeros". Precision is the number of *significant* digits (default 6).
20910fn perl_g_form(n: f64, prec: usize, upper: bool) -> String {
20911    let prec = prec.max(1);
20912    if !n.is_finite() {
20913        return if upper {
20914            format!("{}", n).to_uppercase()
20915        } else {
20916            format!("{}", n)
20917        };
20918    }
20919    // Compute base-10 exponent.
20920    let abs = n.abs();
20921    let x = if abs == 0.0 {
20922        0i32
20923    } else {
20924        abs.log10().floor() as i32
20925    };
20926    // %g rule: use exponential form if x < -4 OR x >= prec.
20927    let use_e = x < -4 || x >= prec as i32;
20928    // Always work in lowercase-`e` form internally so the trim logic has a
20929    // single shape; upcase the marker letter at the end for `%G`.
20930    let formatted = if use_e {
20931        let raw = format!("{:.*e}", prec - 1, n);
20932        perl_exponent_form(&raw, false)
20933    } else {
20934        let f_prec = (prec as i32 - 1 - x).max(0) as usize;
20935        format!("{:.*}", f_prec, n)
20936    };
20937    // Strip trailing zeros from the fractional part (and a trailing '.'),
20938    // but only on the mantissa side — leave the exponent untouched.
20939    let (mant, exp) = if let Some(pos) = formatted.find('e') {
20940        (formatted[..pos].to_string(), formatted[pos..].to_string())
20941    } else {
20942        (formatted.clone(), String::new())
20943    };
20944    let trimmed = if mant.contains('.') {
20945        let t = mant.trim_end_matches('0');
20946        let t = t.trim_end_matches('.');
20947        t.to_string()
20948    } else {
20949        mant
20950    };
20951    let combined = format!("{}{}", trimmed, exp);
20952    if upper {
20953        combined.replace('e', "E")
20954    } else {
20955        combined
20956    }
20957}
20958
20959/// Public sprintf entry point. Returns the formatted string plus the list
20960/// of `%n` store-targets and counts that the caller should apply via
20961/// [`VMHelper::assign_scalar_ref_deref`]. Callers that don't use `%n`
20962/// can ignore the second tuple element.
20963pub(crate) fn perl_sprintf_format_full<F>(
20964    fmt: &str,
20965    args: &[PerlValue],
20966    string_for_s: &mut F,
20967) -> Result<(String, Vec<(PerlValue, i64)>), FlowOrError>
20968where
20969    F: FnMut(&PerlValue) -> Result<String, FlowOrError>,
20970{
20971    let mut pending_n: Vec<(PerlValue, i64)> = Vec::new();
20972    let mut result = String::new();
20973    let mut arg_idx = 0;
20974    let chars: Vec<char> = fmt.chars().collect();
20975    let mut i = 0;
20976
20977    // Helper to consume the next arg as an i64 (used for `*` width / precision).
20978    let take_arg_int = |args: &[PerlValue], idx: &mut usize| -> i64 {
20979        let v = args.get(*idx).cloned().unwrap_or(PerlValue::UNDEF);
20980        *idx += 1;
20981        v.to_int()
20982    };
20983
20984    while i < chars.len() {
20985        if chars[i] == '%' {
20986            i += 1;
20987            if i >= chars.len() {
20988                break;
20989            }
20990            if chars[i] == '%' {
20991                result.push('%');
20992                i += 1;
20993                continue;
20994            }
20995
20996            // Positional `%N$...`: take this conversion's value from args[N-1]
20997            // instead of advancing the sequential cursor. Must be the very
20998            // first thing after `%`. We peek for `digits$` and rewind if the
20999            // `$` isn't there (the digits could just be a width).
21000            let mut positional: Option<usize> = None;
21001            {
21002                let saved = i;
21003                let mut digits = String::new();
21004                let mut j = i;
21005                while j < chars.len() && chars[j].is_ascii_digit() {
21006                    digits.push(chars[j]);
21007                    j += 1;
21008                }
21009                if j < chars.len() && chars[j] == '$' && !digits.is_empty() {
21010                    if let Ok(n) = digits.parse::<usize>() {
21011                        if n >= 1 {
21012                            positional = Some(n - 1);
21013                            i = j + 1; // consume the digits and the '$'
21014                        }
21015                    }
21016                }
21017                if positional.is_none() {
21018                    i = saved;
21019                }
21020            }
21021
21022            // Parse format specifier
21023            let mut flags = String::new();
21024            while i < chars.len() && "-+ #0".contains(chars[i]) {
21025                flags.push(chars[i]);
21026                i += 1;
21027            }
21028            // Vector flag: `v` (separator = ".") or `*v` (separator = next arg).
21029            // When set, the conversion runs once per byte of the value's
21030            // string form, joining results with the separator.
21031            let mut vector_sep: Option<String> = None;
21032            if i < chars.len() && chars[i] == 'v' {
21033                vector_sep = Some(".".to_string());
21034                i += 1;
21035            } else if i + 1 < chars.len() && chars[i] == '*' && chars[i + 1] == 'v' {
21036                let sep_arg = args.get(arg_idx).cloned().unwrap_or(PerlValue::UNDEF);
21037                arg_idx += 1;
21038                vector_sep = Some(sep_arg.to_string());
21039                i += 2;
21040            }
21041            // Width: either `*` (consume an arg) or run of digits.
21042            let mut width = String::new();
21043            let mut left_align = flags.contains('-');
21044            if i < chars.len() && chars[i] == '*' {
21045                let n = take_arg_int(args, &mut arg_idx);
21046                if n < 0 {
21047                    // Negative width means left-align with |n|.
21048                    left_align = true;
21049                    width = (-n).to_string();
21050                } else {
21051                    width = n.to_string();
21052                }
21053                i += 1;
21054            } else {
21055                while i < chars.len() && chars[i].is_ascii_digit() {
21056                    width.push(chars[i]);
21057                    i += 1;
21058                }
21059            }
21060            // Precision: `.*` or `.<digits>` (or nothing).
21061            let mut precision = String::new();
21062            if i < chars.len() && chars[i] == '.' {
21063                i += 1;
21064                if i < chars.len() && chars[i] == '*' {
21065                    let n = take_arg_int(args, &mut arg_idx);
21066                    precision = n.max(0).to_string();
21067                    i += 1;
21068                } else {
21069                    while i < chars.len() && chars[i].is_ascii_digit() {
21070                        precision.push(chars[i]);
21071                        i += 1;
21072                    }
21073                    // ".<no digits>" means precision 0 (Perl/C convention).
21074                    if precision.is_empty() {
21075                        precision = "0".to_string();
21076                    }
21077                }
21078            }
21079            if i >= chars.len() {
21080                break;
21081            }
21082            let spec = chars[i];
21083            i += 1;
21084
21085            // For vector conversions the conversion's value-arg is the
21086            // string whose bytes we'll iterate; for non-vector, it's the
21087            // value we format. Either way the index resolution is the
21088            // same: positional or sequential.
21089            let arg = if let Some(idx) = positional {
21090                args.get(idx).cloned().unwrap_or(PerlValue::UNDEF)
21091            } else {
21092                let v = args.get(arg_idx).cloned().unwrap_or(PerlValue::UNDEF);
21093                arg_idx += 1;
21094                v
21095            };
21096
21097            let w: usize = width.parse().unwrap_or(0);
21098            let p: usize = precision.parse().unwrap_or(6);
21099
21100            let zero_pad = flags.contains('0') && !left_align;
21101            let plus = flags.contains('+');
21102            let space = flags.contains(' ');
21103            let hash = flags.contains('#');
21104
21105            // Apply width + alignment to a body string. Honors zero-pad for
21106            // numerics (caller passes the raw signed body so we can splice
21107            // zeros after the sign).
21108            let pad_align = |body: &str, width: usize, left: bool, zero: bool| -> String {
21109                if width == 0 || body.len() >= width {
21110                    return body.to_string();
21111                }
21112                if zero && !left {
21113                    if let Some(rest) = body.strip_prefix('-') {
21114                        return format!("-{:0>width$}", rest, width = width - 1);
21115                    }
21116                    if let Some(rest) = body.strip_prefix('+') {
21117                        return format!("+{:0>width$}", rest, width = width - 1);
21118                    }
21119                    return format!("{:0>width$}", body, width = width);
21120                }
21121                if left {
21122                    format!("{:<width$}", body, width = width)
21123                } else {
21124                    format!("{:>width$}", body, width = width)
21125                }
21126            };
21127
21128            // Format a single integer with the inner spec for `%v...`. No
21129            // width/precision is applied here — those are deferred to the
21130            // joined result. Supports the common int-shape conversions.
21131            let format_int_for_vector = |n: i64, spec: char| -> String {
21132                match spec {
21133                    'd' | 'i' => format!("{}", n),
21134                    'u' => format!("{}", n as u64),
21135                    'x' => {
21136                        if hash && n != 0 {
21137                            format!("0x{:x}", n)
21138                        } else {
21139                            format!("{:x}", n)
21140                        }
21141                    }
21142                    'X' => {
21143                        if hash && n != 0 {
21144                            format!("0X{:X}", n)
21145                        } else {
21146                            format!("{:X}", n)
21147                        }
21148                    }
21149                    'o' => {
21150                        if hash && n != 0 {
21151                            format!("0{:o}", n)
21152                        } else {
21153                            format!("{:o}", n)
21154                        }
21155                    }
21156                    'b' => {
21157                        if hash && n != 0 {
21158                            format!("0b{:b}", n)
21159                        } else {
21160                            format!("{:b}", n)
21161                        }
21162                    }
21163                    'c' => char::from_u32(n as u32)
21164                        .map(|c| c.to_string())
21165                        .unwrap_or_default(),
21166                    _ => format!("{}", n),
21167                }
21168            };
21169
21170            // `%v` short-circuit: format each byte of the arg's string form
21171            // with the inner spec, join with `vector_sep`, then pad/align
21172            // the joined string. Skips the regular per-spec match below.
21173            if let Some(ref sep) = vector_sep {
21174                let s = arg.to_string();
21175                let parts: Vec<String> = s
21176                    .bytes()
21177                    .map(|b| format_int_for_vector(b as i64, spec))
21178                    .collect();
21179                let body = parts.join(sep);
21180                let final_body = if width.is_empty() {
21181                    body
21182                } else if left_align {
21183                    format!("{:<width$}", body, width = w)
21184                } else {
21185                    format!("{:>width$}", body, width = w)
21186                };
21187                result.push_str(&final_body);
21188                continue;
21189            }
21190
21191            let formatted = match spec {
21192                'd' | 'i' => {
21193                    let v = arg.to_int();
21194                    let body = if plus && v >= 0 {
21195                        format!("+{}", v)
21196                    } else if space && v >= 0 {
21197                        format!(" {}", v)
21198                    } else {
21199                        format!("{}", v)
21200                    };
21201                    pad_align(&body, w, left_align, zero_pad)
21202                }
21203                'u' => {
21204                    let v = arg.to_int() as u64;
21205                    pad_align(&format!("{}", v), w, left_align, zero_pad)
21206                }
21207                'f' => {
21208                    let n = arg.to_number();
21209                    let body = if plus && n.is_sign_positive() {
21210                        format!("+{:.*}", p, n)
21211                    } else if space && n.is_sign_positive() {
21212                        format!(" {:.*}", p, n)
21213                    } else {
21214                        format!("{:.*}", p, n)
21215                    };
21216                    pad_align(&body, w, left_align, zero_pad)
21217                }
21218                'e' => {
21219                    let n = arg.to_number();
21220                    let raw = format!("{:.*e}", p, n);
21221                    let body0 = perl_exponent_form(&raw, false);
21222                    let body = if plus && n.is_sign_positive() {
21223                        format!("+{}", body0)
21224                    } else if space && n.is_sign_positive() {
21225                        format!(" {}", body0)
21226                    } else {
21227                        body0
21228                    };
21229                    pad_align(&body, w, left_align, zero_pad)
21230                }
21231                'E' => {
21232                    let n = arg.to_number();
21233                    let raw = format!("{:.*E}", p, n);
21234                    let body0 = perl_exponent_form(&raw, true);
21235                    let body = if plus && n.is_sign_positive() {
21236                        format!("+{}", body0)
21237                    } else if space && n.is_sign_positive() {
21238                        format!(" {}", body0)
21239                    } else {
21240                        body0
21241                    };
21242                    pad_align(&body, w, left_align, zero_pad)
21243                }
21244                'g' => {
21245                    let n = arg.to_number();
21246                    // For %g, precision means "significant digits" (default 6).
21247                    let prec_g = if precision.is_empty() { 6 } else { p };
21248                    let body0 = perl_g_form(n, prec_g, false);
21249                    let body = if plus && n.is_sign_positive() {
21250                        format!("+{}", body0)
21251                    } else if space && n.is_sign_positive() {
21252                        format!(" {}", body0)
21253                    } else {
21254                        body0
21255                    };
21256                    pad_align(&body, w, left_align, zero_pad)
21257                }
21258                'G' => {
21259                    let n = arg.to_number();
21260                    let prec_g = if precision.is_empty() { 6 } else { p };
21261                    let body0 = perl_g_form(n, prec_g, true);
21262                    let body = if plus && n.is_sign_positive() {
21263                        format!("+{}", body0)
21264                    } else if space && n.is_sign_positive() {
21265                        format!(" {}", body0)
21266                    } else {
21267                        body0
21268                    };
21269                    pad_align(&body, w, left_align, zero_pad)
21270                }
21271                's' => {
21272                    let s = string_for_s(&arg)?;
21273                    let body = if !precision.is_empty() {
21274                        s.chars().take(p).collect::<String>()
21275                    } else {
21276                        s
21277                    };
21278                    if left_align {
21279                        format!("{:<width$}", body, width = w)
21280                    } else {
21281                        format!("{:>width$}", body, width = w)
21282                    }
21283                }
21284                'x' => {
21285                    let v = arg.to_int();
21286                    let body = if hash && v != 0 {
21287                        format!("0x{:x}", v)
21288                    } else {
21289                        format!("{:x}", v)
21290                    };
21291                    pad_align(&body, w, left_align, zero_pad)
21292                }
21293                'X' => {
21294                    let v = arg.to_int();
21295                    let body = if hash && v != 0 {
21296                        format!("0X{:X}", v)
21297                    } else {
21298                        format!("{:X}", v)
21299                    };
21300                    pad_align(&body, w, left_align, zero_pad)
21301                }
21302                'o' => {
21303                    let v = arg.to_int();
21304                    let body = if hash && v != 0 {
21305                        format!("0{:o}", v)
21306                    } else {
21307                        format!("{:o}", v)
21308                    };
21309                    pad_align(&body, w, left_align, zero_pad)
21310                }
21311                'b' => {
21312                    let v = arg.to_int();
21313                    let body = if hash && v != 0 {
21314                        format!("0b{:b}", v)
21315                    } else {
21316                        format!("{:b}", v)
21317                    };
21318                    pad_align(&body, w, left_align, zero_pad)
21319                }
21320                'c' => char::from_u32(arg.to_int() as u32)
21321                    .map(|c| c.to_string())
21322                    .unwrap_or_default(),
21323                'a' | 'A' => {
21324                    let upper = spec == 'A';
21325                    let body0 = perl_hex_float(arg.to_number(), upper);
21326                    let body = if plus && !body0.starts_with('-') {
21327                        format!("+{}", body0)
21328                    } else if space && !body0.starts_with('-') {
21329                        format!(" {}", body0)
21330                    } else {
21331                        body0
21332                    };
21333                    pad_align(&body, w, left_align, zero_pad)
21334                }
21335                'p' => {
21336                    // Stryke uses placeholder addresses for refs; emit the
21337                    // same `0x...` form here so output stays deterministic
21338                    // and machine-comparable across runs.
21339                    pad_align("0x...", w, left_align, false)
21340                }
21341                'n' => {
21342                    // Write the number of bytes emitted so far into the
21343                    // referent of the arg (must be a scalar ref, e.g.
21344                    // `\$count`). `%n` does NOT consume an output slot, so
21345                    // the formatted body is empty. The store is queued and
21346                    // applied by the caller after formatting finishes —
21347                    // works for both `HeapObject::ScalarRef` and the
21348                    // `ScalarBindingRef` shape that `\$my_var` produces.
21349                    pending_n.push((arg.clone(), result.len() as i64));
21350                    String::new()
21351                }
21352                _ => arg.to_string(),
21353            };
21354
21355            result.push_str(&formatted);
21356        } else {
21357            result.push(chars[i]);
21358            i += 1;
21359        }
21360    }
21361    Ok((result, pending_n))
21362}
21363
21364#[cfg(test)]
21365mod regex_expand_tests {
21366    use super::VMHelper;
21367
21368    #[test]
21369    fn compile_regex_quotemeta_qe_matches_literal() {
21370        let mut i = VMHelper::new();
21371        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
21372        assert!(re.is_match("a.c"));
21373        assert!(!re.is_match("abc"));
21374    }
21375
21376    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
21377    /// stay literal (not rewritten to `(?:\n?\z)`).
21378    #[test]
21379    fn compile_regex_char_class_leading_close_bracket_is_literal() {
21380        let mut i = VMHelper::new();
21381        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
21382        assert!(re.is_match("$"));
21383        assert!(re.is_match("]"));
21384        assert!(!re.is_match("x"));
21385    }
21386}
21387
21388#[cfg(test)]
21389mod special_scalar_name_tests {
21390    use super::VMHelper;
21391
21392    #[test]
21393    fn special_scalar_name_for_get_matches_magic_globals() {
21394        assert!(VMHelper::is_special_scalar_name_for_get("0"));
21395        assert!(VMHelper::is_special_scalar_name_for_get("!"));
21396        assert!(VMHelper::is_special_scalar_name_for_get("^W"));
21397        assert!(VMHelper::is_special_scalar_name_for_get("^O"));
21398        assert!(VMHelper::is_special_scalar_name_for_get("^MATCH"));
21399        assert!(VMHelper::is_special_scalar_name_for_get("<"));
21400        assert!(VMHelper::is_special_scalar_name_for_get("?"));
21401        assert!(VMHelper::is_special_scalar_name_for_get("|"));
21402        assert!(VMHelper::is_special_scalar_name_for_get("^UNICODE"));
21403        assert!(VMHelper::is_special_scalar_name_for_get("\""));
21404        assert!(!VMHelper::is_special_scalar_name_for_get("foo"));
21405        assert!(!VMHelper::is_special_scalar_name_for_get("plainvar"));
21406    }
21407
21408    #[test]
21409    fn special_scalar_name_for_set_matches_set_special_var_arms() {
21410        assert!(VMHelper::is_special_scalar_name_for_set("0"));
21411        assert!(VMHelper::is_special_scalar_name_for_set("^D"));
21412        assert!(VMHelper::is_special_scalar_name_for_set("^H"));
21413        assert!(VMHelper::is_special_scalar_name_for_set("^WARNING_BITS"));
21414        assert!(VMHelper::is_special_scalar_name_for_set("ARGV"));
21415        assert!(VMHelper::is_special_scalar_name_for_set("|"));
21416        assert!(VMHelper::is_special_scalar_name_for_set("?"));
21417        assert!(VMHelper::is_special_scalar_name_for_set("^UNICODE"));
21418        assert!(VMHelper::is_special_scalar_name_for_set("."));
21419        assert!(!VMHelper::is_special_scalar_name_for_set("foo"));
21420        assert!(!VMHelper::is_special_scalar_name_for_set("__PACKAGE__"));
21421    }
21422
21423    #[test]
21424    fn caret_and_id_specials_roundtrip_get() {
21425        let i = VMHelper::new();
21426        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
21427        assert_eq!(
21428            i.get_special_var("^V").to_string(),
21429            format!("v{}", env!("CARGO_PKG_VERSION"))
21430        );
21431        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
21432        assert!(i.get_special_var("^T").to_int() >= 0);
21433        #[cfg(unix)]
21434        {
21435            assert!(i.get_special_var("<").to_int() >= 0);
21436        }
21437    }
21438
21439    #[test]
21440    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
21441        let mut i = VMHelper::new();
21442        i.last_readline_handle.clear();
21443        i.line_number = 3;
21444        i.prepare_flip_flop_vm_slots(1);
21445        assert_eq!(
21446            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
21447            1
21448        );
21449        assert!(i.flip_flop_active[0]);
21450        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
21451        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
21452        assert_eq!(
21453            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
21454            1
21455        );
21456        assert!(i.flip_flop_active[0]);
21457    }
21458
21459    #[test]
21460    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
21461        let mut i = VMHelper::new();
21462        i.last_readline_handle.clear();
21463        i.line_number = 2;
21464        i.prepare_flip_flop_vm_slots(1);
21465        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
21466        assert!(i.flip_flop_active[0]);
21467        i.line_number = 3;
21468        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
21469        assert!(!i.flip_flop_active[0]);
21470        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
21471    }
21472}