Skip to main content

stryke/
interpreter.rs

1use std::cell::Cell;
2use std::cmp::Ordering;
3use std::collections::{HashMap, HashSet, VecDeque};
4use std::fs::File;
5use std::io::{self, BufRead, BufReader, Cursor, Read, Write as IoWrite};
6#[cfg(unix)]
7use std::os::unix::process::ExitStatusExt;
8use std::path::{Path, PathBuf};
9use std::process::{Child, Command, Stdio};
10use std::sync::atomic::AtomicUsize;
11use std::sync::Arc;
12use std::sync::{Barrier, OnceLock};
13use std::time::{Duration, Instant};
14
15use indexmap::IndexMap;
16use parking_lot::{Mutex, RwLock};
17use rand::rngs::StdRng;
18use rand::{Rng, SeedableRng};
19use rayon::prelude::*;
20
21use caseless::default_case_fold_str;
22
23use crate::ast::*;
24use crate::builtins::PerlSocket;
25use crate::crypt_util::perl_crypt;
26use crate::error::{ErrorKind, PerlError, PerlResult};
27use crate::mro::linearize_c3;
28use crate::perl_decode::decode_utf8_or_latin1;
29use crate::perl_fs::read_file_text_perl_compat;
30use crate::perl_regex::{perl_quotemeta, PerlCaptures, PerlCompiledRegex};
31use crate::pmap_progress::{FanProgress, PmapProgress};
32use crate::profiler::Profiler;
33use crate::scope::Scope;
34use crate::sort_fast::{detect_sort_block_fast, sort_magic_cmp};
35use crate::value::{
36    perl_list_range_expand, CaptureResult, PerlAsyncTask, PerlBarrier, PerlDataFrame,
37    PerlGenerator, PerlHeap, PerlPpool, PerlSub, PerlValue, PipelineInner, PipelineOp,
38    RemoteCluster,
39};
40
41/// Merge two counting-hash accumulators (parallel `preduce_init` partials).
42/// Returns a hashref so arrow deref (`$acc->{k}`) stays valid after parallel merge.
43pub(crate) fn preduce_init_merge_maps(
44    mut acc: IndexMap<String, PerlValue>,
45    b: IndexMap<String, PerlValue>,
46) -> PerlValue {
47    for (k, v2) in b {
48        acc.entry(k)
49            .and_modify(|v1| *v1 = PerlValue::float(v1.to_number() + v2.to_number()))
50            .or_insert(v2);
51    }
52    PerlValue::hash_ref(Arc::new(RwLock::new(acc)))
53}
54
55/// `(off, end)` for `splice` / `arr.drain(off..end)` — Perl negative OFFSET/LENGTH; clamps offset to array length.
56#[inline]
57fn splice_compute_range(
58    arr_len: usize,
59    offset_val: &PerlValue,
60    length_val: &PerlValue,
61) -> (usize, usize) {
62    let off_i = offset_val.to_int();
63    let off = if off_i < 0 {
64        arr_len.saturating_sub((-off_i) as usize)
65    } else {
66        (off_i as usize).min(arr_len)
67    };
68    let rest = arr_len.saturating_sub(off);
69    let take = if length_val.is_undef() {
70        rest
71    } else {
72        let l = length_val.to_int();
73        if l < 0 {
74            rest.saturating_sub((-l) as usize)
75        } else {
76            (l as usize).min(rest)
77        }
78    };
79    let end = (off + take).min(arr_len);
80    (off, end)
81}
82
83/// Combine two partial results from `preduce_init`: hash/hashref maps add per-key counts; otherwise
84/// the fold block is invoked with `$a` / `$b` as the two partial accumulators (associative combine).
85pub(crate) fn merge_preduce_init_partials(
86    a: PerlValue,
87    b: PerlValue,
88    block: &Block,
89    subs: &HashMap<String, Arc<PerlSub>>,
90    scope_capture: &[(String, PerlValue)],
91) -> PerlValue {
92    if let (Some(m1), Some(m2)) = (a.as_hash_map(), b.as_hash_map()) {
93        return preduce_init_merge_maps(m1, m2);
94    }
95    if let (Some(r1), Some(r2)) = (a.as_hash_ref(), b.as_hash_ref()) {
96        let m1 = r1.read().clone();
97        let m2 = r2.read().clone();
98        return preduce_init_merge_maps(m1, m2);
99    }
100    if let Some(m1) = a.as_hash_map() {
101        if let Some(r2) = b.as_hash_ref() {
102            let m2 = r2.read().clone();
103            return preduce_init_merge_maps(m1, m2);
104        }
105    }
106    if let Some(r1) = a.as_hash_ref() {
107        if let Some(m2) = b.as_hash_map() {
108            let m1 = r1.read().clone();
109            return preduce_init_merge_maps(m1, m2);
110        }
111    }
112    let mut local_interp = Interpreter::new();
113    local_interp.subs = subs.clone();
114    local_interp.scope.restore_capture(scope_capture);
115    local_interp.enable_parallel_guard();
116    local_interp
117        .scope
118        .declare_array("_", vec![a.clone(), b.clone()]);
119    let _ = local_interp.scope.set_scalar("a", a.clone());
120    let _ = local_interp.scope.set_scalar("b", b.clone());
121    let _ = local_interp.scope.set_scalar("_0", a);
122    let _ = local_interp.scope.set_scalar("_1", b);
123    match local_interp.exec_block(block) {
124        Ok(val) => val,
125        Err(_) => PerlValue::UNDEF,
126    }
127}
128
129/// Seed each parallel chunk from `init` without sharing mutable hashref storage (plain `clone` on
130/// `HashRef` reuses the same `Arc<RwLock<…>>`).
131pub(crate) fn preduce_init_fold_identity(init: &PerlValue) -> PerlValue {
132    if let Some(m) = init.as_hash_map() {
133        return PerlValue::hash(m.clone());
134    }
135    if let Some(r) = init.as_hash_ref() {
136        return PerlValue::hash_ref(Arc::new(RwLock::new(r.read().clone())));
137    }
138    init.clone()
139}
140
141pub(crate) fn fold_preduce_init_step(
142    subs: &HashMap<String, Arc<PerlSub>>,
143    scope_capture: &[(String, PerlValue)],
144    block: &Block,
145    acc: PerlValue,
146    item: PerlValue,
147) -> PerlValue {
148    let mut local_interp = Interpreter::new();
149    local_interp.subs = subs.clone();
150    local_interp.scope.restore_capture(scope_capture);
151    local_interp.enable_parallel_guard();
152    local_interp
153        .scope
154        .declare_array("_", vec![acc.clone(), item.clone()]);
155    let _ = local_interp.scope.set_scalar("a", acc.clone());
156    let _ = local_interp.scope.set_scalar("b", item.clone());
157    let _ = local_interp.scope.set_scalar("_0", acc);
158    let _ = local_interp.scope.set_scalar("_1", item);
159    match local_interp.exec_block(block) {
160        Ok(val) => val,
161        Err(_) => PerlValue::UNDEF,
162    }
163}
164
165/// `use feature 'say'`
166pub const FEAT_SAY: u64 = 1 << 0;
167/// `use feature 'state'`
168pub const FEAT_STATE: u64 = 1 << 1;
169/// `use feature 'switch'` (given/when when fully wired)
170pub const FEAT_SWITCH: u64 = 1 << 2;
171/// `use feature 'unicode_strings'`
172pub const FEAT_UNICODE_STRINGS: u64 = 1 << 3;
173
174/// Flow control signals propagated via Result.
175#[derive(Debug)]
176pub(crate) enum Flow {
177    Return(PerlValue),
178    Last(Option<String>),
179    Next(Option<String>),
180    Redo(Option<String>),
181    Yield(PerlValue),
182    /// `goto &sub` — tail-call: replace current sub with the named one, keeping @_.
183    GotoSub(String),
184}
185
186pub(crate) type ExecResult = Result<PerlValue, FlowOrError>;
187
188#[derive(Debug)]
189pub(crate) enum FlowOrError {
190    Flow(Flow),
191    Error(PerlError),
192}
193
194impl From<PerlError> for FlowOrError {
195    fn from(e: PerlError) -> Self {
196        FlowOrError::Error(e)
197    }
198}
199
200impl From<Flow> for FlowOrError {
201    fn from(f: Flow) -> Self {
202        FlowOrError::Flow(f)
203    }
204}
205
206/// Bindings introduced by a successful algebraic [`MatchPattern`] (scalar vs array).
207enum PatternBinding {
208    Scalar(String, PerlValue),
209    Array(String, Vec<PerlValue>),
210}
211
212/// Perl `$]` — numeric language level (`5 + minor/1000 + patch/1_000_000`).
213/// Emulated Perl 5.x level (not the `stryke` crate semver).
214pub fn perl_bracket_version() -> f64 {
215    const PERL_EMUL_MINOR: u32 = 38;
216    const PERL_EMUL_PATCH: u32 = 0;
217    5.0 + (PERL_EMUL_MINOR as f64) / 1000.0 + (PERL_EMUL_PATCH as f64) / 1_000_000.0
218}
219
220/// Cheap seed for [`StdRng`] at startup (avoids `getentropy` / blocking sources).
221#[inline]
222fn fast_rng_seed() -> u64 {
223    let local: u8 = 0;
224    let addr = &local as *const u8 as u64;
225    (std::process::id() as u64).wrapping_mul(0x9E37_79B9_7F4A_7C15) ^ addr
226}
227
228/// `$^X` — cache `current_exe()` once per process (tiny win on repeated `Interpreter::new`).
229fn cached_executable_path() -> String {
230    static CACHED: OnceLock<String> = OnceLock::new();
231    CACHED
232        .get_or_init(|| {
233            std::env::current_exe()
234                .map(|p| p.to_string_lossy().into_owned())
235                .unwrap_or_else(|_| "stryke".to_string())
236        })
237        .clone()
238}
239
240fn build_term_hash() -> IndexMap<String, PerlValue> {
241    let mut m = IndexMap::new();
242    m.insert(
243        "TERM".into(),
244        PerlValue::string(std::env::var("TERM").unwrap_or_default()),
245    );
246    m.insert(
247        "COLORTERM".into(),
248        PerlValue::string(std::env::var("COLORTERM").unwrap_or_default()),
249    );
250
251    let (rows, cols) = term_size();
252    m.insert("rows".into(), PerlValue::integer(rows));
253    m.insert("cols".into(), PerlValue::integer(cols));
254
255    #[cfg(unix)]
256    let is_tty = unsafe { libc::isatty(1) != 0 };
257    #[cfg(not(unix))]
258    let is_tty = false;
259    m.insert(
260        "is_tty".into(),
261        PerlValue::integer(if is_tty { 1 } else { 0 }),
262    );
263
264    m
265}
266
267fn term_size() -> (i64, i64) {
268    #[cfg(unix)]
269    {
270        unsafe {
271            let mut ws: libc::winsize = std::mem::zeroed();
272            if libc::ioctl(1, libc::TIOCGWINSZ, &mut ws) == 0 {
273                return (ws.ws_row as i64, ws.ws_col as i64);
274            }
275        }
276    }
277    let rows = std::env::var("LINES")
278        .ok()
279        .and_then(|s| s.parse().ok())
280        .unwrap_or(24);
281    let cols = std::env::var("COLUMNS")
282        .ok()
283        .and_then(|s| s.parse().ok())
284        .unwrap_or(80);
285    (rows, cols)
286}
287
288#[cfg(unix)]
289fn build_uname_hash() -> IndexMap<String, PerlValue> {
290    fn uts_field(slice: &[libc::c_char]) -> String {
291        let n = slice.iter().take_while(|&&c| c != 0).count();
292        let bytes: Vec<u8> = slice[..n].iter().map(|&c| c as u8).collect();
293        String::from_utf8_lossy(&bytes).into_owned()
294    }
295    let mut m = IndexMap::new();
296    let mut uts: libc::utsname = unsafe { std::mem::zeroed() };
297    if unsafe { libc::uname(&mut uts) } == 0 {
298        m.insert(
299            "sysname".into(),
300            PerlValue::string(uts_field(uts.sysname.as_slice())),
301        );
302        m.insert(
303            "nodename".into(),
304            PerlValue::string(uts_field(uts.nodename.as_slice())),
305        );
306        m.insert(
307            "release".into(),
308            PerlValue::string(uts_field(uts.release.as_slice())),
309        );
310        m.insert(
311            "version".into(),
312            PerlValue::string(uts_field(uts.version.as_slice())),
313        );
314        m.insert(
315            "machine".into(),
316            PerlValue::string(uts_field(uts.machine.as_slice())),
317        );
318    }
319    m
320}
321
322#[cfg(unix)]
323fn build_limits_hash() -> IndexMap<String, PerlValue> {
324    use libc::{getrlimit, rlimit, RLIM_INFINITY};
325    fn get_limit(resource: libc::c_int) -> (i64, i64) {
326        let mut rlim = rlimit {
327            rlim_cur: 0,
328            rlim_max: 0,
329        };
330        if unsafe { getrlimit(resource, &mut rlim) } == 0 {
331            let cur = if rlim.rlim_cur == RLIM_INFINITY {
332                -1
333            } else {
334                rlim.rlim_cur as i64
335            };
336            let max = if rlim.rlim_max == RLIM_INFINITY {
337                -1
338            } else {
339                rlim.rlim_max as i64
340            };
341            (cur, max)
342        } else {
343            (-1, -1)
344        }
345    }
346    let mut m = IndexMap::new();
347    let (cur, max) = get_limit(libc::RLIMIT_NOFILE);
348    m.insert("nofile".into(), PerlValue::integer(cur));
349    m.insert("nofile_max".into(), PerlValue::integer(max));
350    let (cur, max) = get_limit(libc::RLIMIT_STACK);
351    m.insert("stack".into(), PerlValue::integer(cur));
352    m.insert("stack_max".into(), PerlValue::integer(max));
353    let (cur, max) = get_limit(libc::RLIMIT_AS);
354    m.insert("as".into(), PerlValue::integer(cur));
355    m.insert("as_max".into(), PerlValue::integer(max));
356    let (cur, max) = get_limit(libc::RLIMIT_DATA);
357    m.insert("data".into(), PerlValue::integer(cur));
358    m.insert("data_max".into(), PerlValue::integer(max));
359    let (cur, max) = get_limit(libc::RLIMIT_FSIZE);
360    m.insert("fsize".into(), PerlValue::integer(cur));
361    m.insert("fsize_max".into(), PerlValue::integer(max));
362    let (cur, max) = get_limit(libc::RLIMIT_CORE);
363    m.insert("core".into(), PerlValue::integer(cur));
364    m.insert("core_max".into(), PerlValue::integer(max));
365    let (cur, max) = get_limit(libc::RLIMIT_CPU);
366    m.insert("cpu".into(), PerlValue::integer(cur));
367    m.insert("cpu_max".into(), PerlValue::integer(max));
368    let (cur, max) = get_limit(libc::RLIMIT_NPROC);
369    m.insert("nproc".into(), PerlValue::integer(cur));
370    m.insert("nproc_max".into(), PerlValue::integer(max));
371    #[cfg(target_os = "linux")]
372    {
373        let (cur, max) = get_limit(libc::RLIMIT_MEMLOCK);
374        m.insert("memlock".into(), PerlValue::integer(cur));
375        m.insert("memlock_max".into(), PerlValue::integer(max));
376    }
377    m
378}
379
380/// Context of the **current** subroutine call (`wantarray`).
381#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
382pub(crate) enum WantarrayCtx {
383    #[default]
384    Scalar,
385    List,
386    Void,
387}
388
389impl WantarrayCtx {
390    #[inline]
391    pub(crate) fn from_byte(b: u8) -> Self {
392        match b {
393            1 => Self::List,
394            2 => Self::Void,
395            _ => Self::Scalar,
396        }
397    }
398
399    #[inline]
400    pub(crate) fn as_byte(self) -> u8 {
401        match self {
402            Self::Scalar => 0,
403            Self::List => 1,
404            Self::Void => 2,
405        }
406    }
407}
408
409/// Minimum log level filter for `log_*` / `log_json` (trace = most verbose).
410#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
411pub(crate) enum LogLevelFilter {
412    Trace,
413    Debug,
414    Info,
415    Warn,
416    Error,
417}
418
419impl LogLevelFilter {
420    pub(crate) fn parse(s: &str) -> Option<Self> {
421        match s.trim().to_ascii_lowercase().as_str() {
422            "trace" => Some(Self::Trace),
423            "debug" => Some(Self::Debug),
424            "info" => Some(Self::Info),
425            "warn" | "warning" => Some(Self::Warn),
426            "error" => Some(Self::Error),
427            _ => None,
428        }
429    }
430
431    pub(crate) fn as_str(self) -> &'static str {
432        match self {
433            Self::Trace => "trace",
434            Self::Debug => "debug",
435            Self::Info => "info",
436            Self::Warn => "warn",
437            Self::Error => "error",
438        }
439    }
440}
441
442/// True when `@$aref->[IX]` / `IX` needs **list** context on the RHS of `=` (multi-slot slice).
443fn arrow_deref_array_assign_rhs_list_ctx(index: &Expr) -> bool {
444    match &index.kind {
445        ExprKind::Range { .. } => true,
446        ExprKind::QW(ws) => ws.len() > 1,
447        ExprKind::List(el) => {
448            if el.len() > 1 {
449                true
450            } else if el.len() == 1 {
451                arrow_deref_array_assign_rhs_list_ctx(&el[0])
452            } else {
453                false
454            }
455        }
456        _ => false,
457    }
458}
459
460/// Wantarray for the RHS of a plain `=` assignment — must match [`crate::compiler::Compiler`] lowering
461/// so `<>` / `readline` list-slurp matches Perl for `@a = <>` (not only `my`/`our`/`local` initializers).
462pub(crate) fn assign_rhs_wantarray(target: &Expr) -> WantarrayCtx {
463    match &target.kind {
464        ExprKind::ArrayVar(_) | ExprKind::HashVar(_) => WantarrayCtx::List,
465        ExprKind::ScalarVar(_) | ExprKind::ArrayElement { .. } | ExprKind::HashElement { .. } => {
466            WantarrayCtx::Scalar
467        }
468        ExprKind::Deref { kind, .. } => match kind {
469            Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
470            Sigil::Array | Sigil::Hash => WantarrayCtx::List,
471        },
472        ExprKind::ArrowDeref {
473            index,
474            kind: DerefKind::Array,
475            ..
476        } => {
477            if arrow_deref_array_assign_rhs_list_ctx(index) {
478                WantarrayCtx::List
479            } else {
480                WantarrayCtx::Scalar
481            }
482        }
483        ExprKind::ArrowDeref {
484            kind: DerefKind::Hash,
485            ..
486        }
487        | ExprKind::ArrowDeref {
488            kind: DerefKind::Call,
489            ..
490        } => WantarrayCtx::Scalar,
491        ExprKind::HashSliceDeref { .. } | ExprKind::HashSlice { .. } => 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 [`Interpreter::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/// Tree-walker 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
569pub struct Interpreter {
570    pub scope: Scope,
571    pub(crate) subs: HashMap<String, Arc<PerlSub>>,
572    pub(crate) file: String,
573    /// File handles: name → writer
574    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
575    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
576    /// Output separator ($,)
577    pub ofs: String,
578    /// Output record separator ($\)
579    pub ors: String,
580    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
581    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
582    pub irs: Option<String>,
583    /// $! — last OS error
584    pub errno: String,
585    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
586    pub errno_code: i32,
587    /// $@ — last eval error (string)
588    pub eval_error: String,
589    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
590    pub eval_error_code: i32,
591    /// When `die` is called with a ref argument, the ref value is preserved here.
592    pub eval_error_value: Option<PerlValue>,
593    /// @ARGV
594    pub argv: Vec<String>,
595    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
596    pub env: IndexMap<String, PerlValue>,
597    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
598    pub env_materialized: bool,
599    /// $0
600    pub program_name: String,
601    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle)
602    pub line_number: i64,
603    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
604    pub last_readline_handle: String,
605    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
606    pub(crate) last_stdin_die_bracket: String,
607    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
608    pub handle_line_numbers: HashMap<String, i64>,
609    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
610    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
611    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
612    pub(crate) flip_flop_active: Vec<bool>,
613    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
614    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
615    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
616    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
617    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
618    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
619    pub(crate) flip_flop_sequence: Vec<i64>,
620    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
621    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
622    /// range on one line return the same sequence number).
623    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
624    /// Scalar `..` / `...` flip-flop for tree-walker (key: `Expr` address).
625    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
626    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
627    pub sigint_pending_caret: Cell<bool>,
628    /// Auto-split mode (-a)
629    pub auto_split: bool,
630    /// Field separator for -F
631    pub field_separator: Option<String>,
632    /// BEGIN blocks
633    begin_blocks: Vec<Block>,
634    /// `UNITCHECK` blocks (LIFO at run)
635    unit_check_blocks: Vec<Block>,
636    /// `CHECK` blocks (LIFO at run)
637    check_blocks: Vec<Block>,
638    /// `INIT` blocks (FIFO at run)
639    init_blocks: Vec<Block>,
640    /// END blocks
641    end_blocks: Vec<Block>,
642    /// -w warnings / `use warnings` / `$^W`
643    pub warnings: bool,
644    /// Output autoflush (`$|`).
645    pub output_autoflush: bool,
646    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
647    pub default_print_handle: String,
648    /// Suppress stdout output (fan workers with progress bars).
649    pub suppress_stdout: bool,
650    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
651    pub child_exit_status: i64,
652    /// Last successful match (`$&`, `${^MATCH}`).
653    pub last_match: String,
654    /// Before match (`` $` ``, `${^PREMATCH}`).
655    pub prematch: String,
656    /// After match (`$'`, `${^POSTMATCH}`).
657    pub postmatch: String,
658    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
659    pub last_paren_match: String,
660    /// List separator for array stringification in concatenation / interpolation (`$"`).
661    pub list_separator: String,
662    /// Script start time (`$^T`) — seconds since Unix epoch.
663    pub script_start_time: i64,
664    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
665    pub compile_hints: i64,
666    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
667    pub warning_bits: i64,
668    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
669    pub global_phase: String,
670    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
671    pub subscript_sep: String,
672    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
673    /// The `stryke` driver sets this from `-i` / `-i.ext`.
674    pub inplace_edit: String,
675    /// `$^D` — debugging flags (integer; mostly ignored).
676    pub debug_flags: i64,
677    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
678    pub perl_debug_flags: i64,
679    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
680    pub eval_nesting: u32,
681    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
682    pub argv_current_file: String,
683    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
684    pub(crate) diamond_next_idx: usize,
685    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
686    pub(crate) diamond_reader: Option<BufReader<File>>,
687    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
688    pub strict_refs: bool,
689    pub strict_subs: bool,
690    pub strict_vars: bool,
691    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
692    pub utf8_pragma: bool,
693    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
694    pub open_pragma_utf8: bool,
695    /// `use feature` — bit flags (`FEAT_*`).
696    pub feature_bits: u64,
697    /// Number of parallel threads
698    pub num_threads: usize,
699    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
700    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
701    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
702    /// Third flag: `$*` multiline (prepends `(?s)` when true).
703    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
704    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
705    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
706    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
707    /// scope population entirely on cache hit.
708    ///
709    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
710    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
711    /// capture-var side-effect replay is forced on the next hit.
712    regex_match_memo: Option<RegexMatchMemo>,
713    /// False when the user (or some non-regex code path) has written to one of the capture
714    /// variables since the last `apply_regex_captures` call. The memoized match result is still
715    /// valid, but the scope side effects need to be reapplied on the next hit.
716    regex_capture_scope_fresh: bool,
717    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
718    pub(crate) regex_pos: HashMap<String, Option<usize>>,
719    /// Persistent storage for `state` variables, keyed by "line:name".
720    pub(crate) state_vars: HashMap<String, PerlValue>,
721    /// Per-frame tracking of state variable bindings: (var_name, state_key).
722    state_bindings_stack: Vec<Vec<(String, String)>>,
723    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
724    pub(crate) rand_rng: StdRng,
725    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
726    pub(crate) dir_handles: HashMap<String, DirHandleState>,
727    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
728    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
729    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
730    pub(crate) pipe_children: HashMap<String, Child>,
731    /// Sockets from `socket` / `accept` / `connect`.
732    pub(crate) socket_handles: HashMap<String, PerlSocket>,
733    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
734    pub(crate) wantarray_kind: WantarrayCtx,
735    /// `struct Name { ... }` definitions (merged from VM chunks and tree-walker).
736    pub struct_defs: HashMap<String, Arc<StructDef>>,
737    /// `enum Name { ... }` definitions (merged from VM chunks and tree-walker).
738    pub enum_defs: HashMap<String, Arc<EnumDef>>,
739    /// `class Name extends ... impl ... { ... }` definitions.
740    pub class_defs: HashMap<String, Arc<ClassDef>>,
741    /// `trait Name { ... }` definitions.
742    pub trait_defs: HashMap<String, Arc<TraitDef>>,
743    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
744    /// call/return (JIT disabled); tree-walker fallback uses per-statement lines and subs.
745    pub profiler: Option<Profiler>,
746    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
747    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
748    /// Virtual modules: path → source (for AOT bundles). Checked before filesystem in `require`.
749    pub(crate) virtual_modules: HashMap<String, String>,
750    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
751    pub(crate) tied_hashes: HashMap<String, PerlValue>,
752    /// `tie $name` — TIESCALAR object for FETCH/STORE.
753    pub(crate) tied_scalars: HashMap<String, PerlValue>,
754    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
755    pub(crate) tied_arrays: HashMap<String, PerlValue>,
756    /// `use overload` — class → Perl overload key → short method name in that package.
757    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
758    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
759    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
760    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
761    pub(crate) special_caret_scalars: HashMap<String, PerlValue>,
762    /// `$%` — format output page number.
763    pub format_page_number: i64,
764    /// `$=` — format lines per page.
765    pub format_lines_per_page: i64,
766    /// `$-` — lines remaining on format page.
767    pub format_lines_left: i64,
768    /// `$:` — characters to break format lines (Perl default `\n`).
769    pub format_line_break_chars: String,
770    /// `$^` — top-of-form format name.
771    pub format_top_name: String,
772    /// `$^A` — format write accumulator.
773    pub accumulator_format: String,
774    /// `$^F` — max system file descriptor (Perl default 2).
775    pub max_system_fd: i64,
776    /// `$^M` — emergency memory buffer (no-op pool in stryke).
777    pub emergency_memory: String,
778    /// `$^N` — last opened named regexp capture name.
779    pub last_subpattern_name: String,
780    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
781    pub inc_hook_index: i64,
782    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
783    pub multiline_match: bool,
784    /// `$^X` — path to this executable (cached).
785    pub executable_path: String,
786    /// `$^L` — formfeed string for formats (Perl default `\f`).
787    pub formfeed_string: String,
788    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
789    pub(crate) glob_handle_alias: HashMap<String, String>,
790    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
791    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
792    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
793    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
794    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
795    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
796    pub(crate) special_var_restore_frames: Vec<Vec<(String, PerlValue)>>,
797    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
798    /// Lazy-init flag: reflection hashes (`%b`, `%stryke::builtins`, etc.)
799    /// are only built on first access to avoid startup cost.
800    pub(crate) reflection_hashes_ready: bool,
801    pub(crate) english_enabled: bool,
802    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
803    pub(crate) english_no_match_vars: bool,
804    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
805    /// available for the rest of the program — Perl exports them into the caller's namespace
806    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
807    pub(crate) english_match_vars_ever_enabled: bool,
808    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
809    english_lexical_scalars: Vec<HashSet<String>>,
810    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
811    our_lexical_scalars: Vec<HashSet<String>>,
812    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
813    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
814    pub vm_jit_enabled: bool,
815    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
816    pub disasm_bytecode: bool,
817    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from a `.pec` cache hit. When
818    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
819    /// (`.take()`) on first read so re-entry compiles normally.
820    pub pec_precompiled_chunk: Option<crate::bytecode::Chunk>,
821    /// Sideband: fingerprint to save the compiled chunk under after a cache miss (pairs with
822    /// [`crate::pec::try_save`]). `None` when the cache is disabled or the caller does not want
823    /// the compiled chunk persisted.
824    pub pec_cache_fingerprint: Option<[u8; 32]>,
825    /// Set while stepping a `gen { }` body (`yield`).
826    pub(crate) in_generator: bool,
827    /// `-n`/`-p` driver: prelude only in [`Self::execute_tree`]; body runs in [`Self::process_line`].
828    pub line_mode_skip_main: bool,
829    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
830    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
831    /// matches Perl (true on the last line of that source).
832    pub(crate) line_mode_eof_pending: bool,
833    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
834    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
835    pub line_mode_stdin_pending: VecDeque<String>,
836    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
837    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
838    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
839    pub(crate) log_level_override: Option<LogLevelFilter>,
840    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
841    /// Pushed on `call_sub` entry, popped on exit.
842    pub(crate) current_sub_stack: Vec<Arc<PerlSub>>,
843    /// Interactive debugger state (`-d` flag).
844    pub debugger: Option<crate::debugger::Debugger>,
845    /// Call stack for debugger: (sub_name, call_line).
846    pub(crate) debug_call_stack: Vec<(String, usize)>,
847}
848
849/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
850#[derive(Debug, Clone, Default)]
851pub struct ReplCompletionSnapshot {
852    pub subs: Vec<String>,
853    pub blessed_scalars: HashMap<String, String>,
854    pub isa_for_class: HashMap<String, Vec<String>>,
855}
856
857impl ReplCompletionSnapshot {
858    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
859    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
860        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
861        let mro = linearize_c3(class, &parents, 0);
862        let mut names = HashSet::new();
863        for pkg in &mro {
864            if pkg == "UNIVERSAL" {
865                continue;
866            }
867            let prefix = format!("{}::", pkg);
868            for k in &self.subs {
869                if k.starts_with(&prefix) {
870                    let rest = &k[prefix.len()..];
871                    if !rest.contains("::") {
872                        names.insert(rest.to_string());
873                    }
874                }
875            }
876        }
877        for k in &self.subs {
878            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
879                if !rest.contains("::") {
880                    names.insert(rest.to_string());
881                }
882            }
883        }
884        let mut v: Vec<String> = names.into_iter().collect();
885        v.sort();
886        v
887    }
888}
889
890fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
891    let left = left.trim_end();
892    if left.is_empty() {
893        return None;
894    }
895    if let Some(i) = left.rfind('$') {
896        let name = left[i + 1..].trim();
897        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
898            return state.blessed_scalars.get(name).cloned();
899        }
900    }
901    let tok = left.split_whitespace().last()?;
902    if tok.contains("::") {
903        return Some(tok.to_string());
904    }
905    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
906        return Some(tok.to_string());
907    }
908    None
909}
910
911/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
912pub fn repl_arrow_method_completions(
913    state: &ReplCompletionSnapshot,
914    line: &str,
915    pos: usize,
916) -> Option<(usize, Vec<String>)> {
917    let pos = pos.min(line.len());
918    let before = &line[..pos];
919    let arrow_idx = before.rfind("->")?;
920    let after_arrow = &before[arrow_idx + 2..];
921    let rest = after_arrow.trim_start();
922    let ws_len = after_arrow.len() - rest.len();
923    let method_start = arrow_idx + 2 + ws_len;
924    let method_prefix = &line[method_start..pos];
925    if !method_prefix
926        .chars()
927        .all(|c| c.is_alphanumeric() || c == '_')
928    {
929        return None;
930    }
931    let left = line[..arrow_idx].trim_end();
932    let class = repl_resolve_class_for_arrow(state, left)?;
933    let mut methods = state.methods_for_class(&class);
934    methods.retain(|m| m.starts_with(method_prefix));
935    Some((method_start, methods))
936}
937
938/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
939#[derive(Debug, Clone, Default)]
940pub(crate) struct ModuleExportLists {
941    /// Default imports for `use Module` with no list.
942    pub export: Vec<String>,
943    /// Extra symbols allowed in `use Module qw(name)`.
944    pub export_ok: Vec<String>,
945}
946
947/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
948fn piped_shell_command(cmd: &str) -> Command {
949    if cfg!(windows) {
950        let mut c = Command::new("cmd");
951        c.arg("/C").arg(cmd);
952        c
953    } else {
954        let mut c = Command::new("sh");
955        c.arg("-c").arg(cmd);
956        c
957    }
958}
959
960/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
961/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
962/// so the Rust `regex` crate can match them.
963/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
964/// so the Rust regex crate can match NUL and other octal-specified bytes.
965/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
966fn expand_perl_regex_octal_escapes(pat: &str) -> String {
967    let mut out = String::with_capacity(pat.len());
968    let mut it = pat.chars().peekable();
969    while let Some(c) = it.next() {
970        if c == '\\' {
971            if let Some(&'0') = it.peek() {
972                // Collect up to 3 octal digits starting with '0'
973                let mut oct = String::new();
974                while oct.len() < 3 {
975                    if let Some(&d) = it.peek() {
976                        if ('0'..='7').contains(&d) {
977                            oct.push(d);
978                            it.next();
979                        } else {
980                            break;
981                        }
982                    } else {
983                        break;
984                    }
985                }
986                if let Ok(val) = u8::from_str_radix(&oct, 8) {
987                    out.push_str(&format!("\\x{:02x}", val));
988                } else {
989                    out.push('\\');
990                    out.push_str(&oct);
991                }
992                continue;
993            }
994        }
995        out.push(c);
996    }
997    out
998}
999
1000fn expand_perl_regex_quotemeta(pat: &str) -> String {
1001    let mut out = String::with_capacity(pat.len().saturating_mul(2));
1002    let mut it = pat.chars().peekable();
1003    let mut in_q = false;
1004    while let Some(c) = it.next() {
1005        if in_q {
1006            if c == '\\' && it.peek() == Some(&'E') {
1007                it.next();
1008                in_q = false;
1009                continue;
1010            }
1011            out.push_str(&perl_quotemeta(&c.to_string()));
1012            continue;
1013        }
1014        if c == '\\' && it.peek() == Some(&'Q') {
1015            it.next();
1016            in_q = true;
1017            continue;
1018        }
1019        out.push(c);
1020    }
1021    out
1022}
1023
1024/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
1025///
1026/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
1027/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
1028///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
1029pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
1030    let mut out = String::with_capacity(replacement.len() + 8);
1031    let mut it = replacement.chars().peekable();
1032    while let Some(c) = it.next() {
1033        if c == '\\' {
1034            match it.peek() {
1035                Some(&d) if d.is_ascii_digit() => {
1036                    it.next();
1037                    out.push_str("${");
1038                    out.push(d);
1039                    while let Some(&d2) = it.peek() {
1040                        if !d2.is_ascii_digit() {
1041                            break;
1042                        }
1043                        it.next();
1044                        out.push(d2);
1045                    }
1046                    out.push('}');
1047                }
1048                Some(&'\\') => {
1049                    it.next();
1050                    out.push('\\');
1051                }
1052                _ => out.push('\\'),
1053            }
1054        } else if c == '$' {
1055            match it.peek() {
1056                Some(&d) if d.is_ascii_digit() => {
1057                    it.next();
1058                    out.push_str("${");
1059                    out.push(d);
1060                    while let Some(&d2) = it.peek() {
1061                        if !d2.is_ascii_digit() {
1062                            break;
1063                        }
1064                        it.next();
1065                        out.push(d2);
1066                    }
1067                    out.push('}');
1068                }
1069                Some(&'{') => {
1070                    // already braced — pass through as-is
1071                    out.push('$');
1072                }
1073                _ => out.push('$'),
1074            }
1075        } else {
1076            out.push(c);
1077        }
1078    }
1079    out
1080}
1081
1082/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
1083/// past the closing `]`.
1084fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
1085    debug_assert_eq!(chars.get(i), Some(&'['));
1086    out.push('[');
1087    i += 1;
1088    if i < chars.len() && chars[i] == '^' {
1089        out.push('^');
1090        i += 1;
1091    }
1092    if i >= chars.len() {
1093        return i;
1094    }
1095    // `]` as the first class character is literal iff another unescaped `]` closes the class
1096    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
1097    // this `]`.
1098    if chars[i] == ']' {
1099        if i + 1 < chars.len() && chars[i + 1] == ']' {
1100            // `[]]` / `[^]]`: literal `]` then the closing `]`.
1101            out.push(']');
1102            i += 1;
1103        } else {
1104            let mut scan = i + 1;
1105            let mut found_closing = false;
1106            while scan < chars.len() {
1107                if chars[scan] == '\\' && scan + 1 < chars.len() {
1108                    scan += 2;
1109                    continue;
1110                }
1111                if chars[scan] == ']' {
1112                    found_closing = true;
1113                    break;
1114                }
1115                scan += 1;
1116            }
1117            if found_closing {
1118                out.push(']');
1119                i += 1;
1120            } else {
1121                out.push(']');
1122                return i + 1;
1123            }
1124        }
1125    }
1126    while i < chars.len() && chars[i] != ']' {
1127        if chars[i] == '\\' && i + 1 < chars.len() {
1128            out.push(chars[i]);
1129            out.push(chars[i + 1]);
1130            i += 2;
1131            continue;
1132        }
1133        out.push(chars[i]);
1134        i += 1;
1135    }
1136    if i < chars.len() {
1137        out.push(']');
1138        i += 1;
1139    }
1140    i
1141}
1142
1143/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
1144/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
1145/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1146/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1147fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1148    if multiline_flag {
1149        return pat.to_string();
1150    }
1151    let chars: Vec<char> = pat.chars().collect();
1152    let mut out = String::with_capacity(pat.len().saturating_add(16));
1153    let mut i = 0usize;
1154    while i < chars.len() {
1155        let c = chars[i];
1156        if c == '\\' && i + 1 < chars.len() {
1157            out.push(c);
1158            out.push(chars[i + 1]);
1159            i += 2;
1160            continue;
1161        }
1162        if c == '[' {
1163            i = copy_regex_char_class(&chars, i, &mut out);
1164            continue;
1165        }
1166        if c == '$' {
1167            if let Some(&next) = chars.get(i + 1) {
1168                if next.is_ascii_digit() {
1169                    out.push(c);
1170                    i += 1;
1171                    continue;
1172                }
1173                if next == '{' {
1174                    out.push(c);
1175                    i += 1;
1176                    continue;
1177                }
1178                if next.is_ascii_alphanumeric() || next == '_' {
1179                    out.push(c);
1180                    i += 1;
1181                    continue;
1182                }
1183            }
1184            out.push_str("(?=\\n?\\z)");
1185            i += 1;
1186            continue;
1187        }
1188        out.push(c);
1189        i += 1;
1190    }
1191    out
1192}
1193
1194/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1195#[derive(Debug, Clone)]
1196pub(crate) struct DirHandleState {
1197    pub entries: Vec<String>,
1198    pub pos: usize,
1199}
1200
1201/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1202pub(crate) fn perl_osname() -> String {
1203    match std::env::consts::OS {
1204        "linux" => "linux".to_string(),
1205        "macos" => "darwin".to_string(),
1206        "windows" => "MSWin32".to_string(),
1207        other => other.to_string(),
1208    }
1209}
1210
1211fn perl_version_v_string() -> String {
1212    format!("v{}", env!("CARGO_PKG_VERSION"))
1213}
1214
1215fn extended_os_error_string() -> String {
1216    std::io::Error::last_os_error().to_string()
1217}
1218
1219#[cfg(unix)]
1220fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1221    unsafe {
1222        (
1223            libc::getuid() as i64,
1224            libc::geteuid() as i64,
1225            libc::getgid() as i64,
1226            libc::getegid() as i64,
1227        )
1228    }
1229}
1230
1231#[cfg(not(unix))]
1232fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1233    (0, 0, 0, 0)
1234}
1235
1236fn unix_id_for_special(name: &str) -> i64 {
1237    let (r, e, _, _) = unix_real_effective_ids();
1238    match name {
1239        "<" => r,
1240        ">" => e,
1241        _ => 0,
1242    }
1243}
1244
1245#[cfg(unix)]
1246fn unix_group_list_string(primary: libc::gid_t) -> String {
1247    let mut buf = vec![0 as libc::gid_t; 256];
1248    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1249    if n <= 0 {
1250        return format!("{}", primary);
1251    }
1252    let mut parts = vec![format!("{}", primary)];
1253    for g in buf.iter().take(n as usize) {
1254        parts.push(format!("{}", g));
1255    }
1256    parts.join(" ")
1257}
1258
1259/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1260#[cfg(unix)]
1261fn unix_group_list_for_special(name: &str) -> String {
1262    let (_, _, gid, egid) = unix_real_effective_ids();
1263    match name {
1264        "(" => unix_group_list_string(gid as libc::gid_t),
1265        ")" => unix_group_list_string(egid as libc::gid_t),
1266        _ => String::new(),
1267    }
1268}
1269
1270#[cfg(not(unix))]
1271fn unix_group_list_for_special(_name: &str) -> String {
1272    String::new()
1273}
1274
1275/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1276/// `~/.ssh/config` and keys).
1277#[cfg(unix)]
1278fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1279    use libc::{getpwuid_r, getuid};
1280    use std::ffi::CStr;
1281    use std::os::unix::ffi::OsStringExt;
1282    let uid = unsafe { getuid() };
1283    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1284    let mut result: *mut libc::passwd = std::ptr::null_mut();
1285    let mut buf = vec![0u8; 16_384];
1286    let rc = unsafe {
1287        getpwuid_r(
1288            uid,
1289            &mut pw,
1290            buf.as_mut_ptr().cast::<libc::c_char>(),
1291            buf.len(),
1292            &mut result,
1293        )
1294    };
1295    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1296        return None;
1297    }
1298    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1299    if bytes.is_empty() {
1300        return None;
1301    }
1302    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1303}
1304
1305/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1306#[cfg(unix)]
1307fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1308    use libc::getpwnam_r;
1309    use std::ffi::{CStr, CString};
1310    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1311    let bytes = login.as_bytes();
1312    if bytes.is_empty() || bytes.contains(&0) {
1313        return None;
1314    }
1315    let cname = CString::new(bytes).ok()?;
1316    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1317    let mut result: *mut libc::passwd = std::ptr::null_mut();
1318    let mut buf = vec![0u8; 16_384];
1319    let rc = unsafe {
1320        getpwnam_r(
1321            cname.as_ptr(),
1322            &mut pw,
1323            buf.as_mut_ptr().cast::<libc::c_char>(),
1324            buf.len(),
1325            &mut result,
1326        )
1327    };
1328    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1329        return None;
1330    }
1331    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1332    if dir_bytes.is_empty() {
1333        return None;
1334    }
1335    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1336}
1337
1338impl Default for Interpreter {
1339    fn default() -> Self {
1340        Self::new()
1341    }
1342}
1343
1344/// How [`Interpreter::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1345#[derive(Clone, Copy)]
1346pub(crate) enum CaptureAllMode {
1347    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1348    Empty,
1349    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1350    Append,
1351    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1352    Skip,
1353}
1354
1355impl Interpreter {
1356    pub fn new() -> Self {
1357        let mut scope = Scope::new();
1358        scope.declare_array("INC", vec![PerlValue::string(".".to_string())]);
1359        scope.declare_hash("INC", IndexMap::new());
1360        scope.declare_array("ARGV", vec![]);
1361        scope.declare_array("_", vec![]);
1362
1363        // @path / @p — $PATH split by OS path separator, frozen (immutable)
1364        let path_vec: Vec<PerlValue> = std::env::var("PATH")
1365            .unwrap_or_default()
1366            .split(if cfg!(windows) { ';' } else { ':' })
1367            .filter(|s| !s.is_empty())
1368            .map(|p| PerlValue::string(p.to_string()))
1369            .collect();
1370        scope.declare_array_frozen("path", path_vec.clone(), true);
1371        scope.declare_array_frozen("p", path_vec, true);
1372
1373        // @fpath / @f — $FPATH (zsh function path) split by ':', frozen
1374        let fpath_vec: Vec<PerlValue> = std::env::var("FPATH")
1375            .unwrap_or_default()
1376            .split(':')
1377            .filter(|s| !s.is_empty())
1378            .map(|p| PerlValue::string(p.to_string()))
1379            .collect();
1380        scope.declare_array_frozen("fpath", fpath_vec.clone(), true);
1381        scope.declare_array_frozen("f", fpath_vec, true);
1382        scope.declare_hash("ENV", IndexMap::new());
1383        scope.declare_hash("SIG", IndexMap::new());
1384
1385        // %term — terminal info (frozen)
1386        let term_map = build_term_hash();
1387        scope.declare_hash_global_frozen("term", term_map);
1388
1389        // %uname — system identification (frozen, Unix only)
1390        #[cfg(unix)]
1391        {
1392            let uname_map = build_uname_hash();
1393            scope.declare_hash_global_frozen("uname", uname_map);
1394        }
1395        #[cfg(not(unix))]
1396        {
1397            scope.declare_hash_global_frozen("uname", IndexMap::new());
1398        }
1399
1400        // %limits — resource limits (frozen, Unix only)
1401        #[cfg(unix)]
1402        {
1403            let limits_map = build_limits_hash();
1404            scope.declare_hash_global_frozen("limits", limits_map);
1405        }
1406        #[cfg(not(unix))]
1407        {
1408            scope.declare_hash_global_frozen("limits", IndexMap::new());
1409        }
1410
1411        // Reflection hashes — populated from `build.rs`-generated tables so
1412        // they track the real parser/dispatcher/LSP without hand-maintenance.
1413        // Seven hashes; all lookups are O(1). Forward maps:
1414        //   %b  / %stryke::builtins      — name → category ("parallel", "string", …)
1415        //   %pc / %stryke::perl_compats  — subset: Perl 5 core only
1416        //   %e  / %stryke::extensions    — subset: stryke-only
1417        //   %a  / %stryke::aliases       — alias → primary
1418        //   %d  / %stryke::descriptions  — name → LSP one-liner (sparse)
1419        // Inverted indexes for constant-time reverse queries:
1420        //   %c  / %stryke::categories    — category → arrayref of names
1421        //   %p  / %stryke::primaries     — primary → arrayref of aliases
1422        //
1423        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1424        // together they cover `keys %builtins`. Short aliases use the
1425        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1426        // Reflection hashes are lazily initialized on first access
1427        // (see `ensure_reflection_hashes`). Only declare the version scalar
1428        // eagerly since it's trivial.
1429        scope.declare_scalar(
1430            "stryke::VERSION",
1431            PerlValue::string(env!("CARGO_PKG_VERSION").to_string()),
1432        );
1433        scope.declare_array("-", vec![]);
1434        scope.declare_array("+", vec![]);
1435        scope.declare_array("^CAPTURE", vec![]);
1436        scope.declare_array("^CAPTURE_ALL", vec![]);
1437        scope.declare_hash("^HOOK", IndexMap::new());
1438        scope.declare_scalar("~", PerlValue::string("STDOUT".to_string()));
1439
1440        let script_start_time = std::time::SystemTime::now()
1441            .duration_since(std::time::UNIX_EPOCH)
1442            .map(|d| d.as_secs() as i64)
1443            .unwrap_or(0);
1444
1445        let executable_path = cached_executable_path();
1446
1447        let mut special_caret_scalars: HashMap<String, PerlValue> = HashMap::new();
1448        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1449            special_caret_scalars.insert(format!("^{}", name), PerlValue::UNDEF);
1450        }
1451
1452        let mut s = Self {
1453            scope,
1454            subs: HashMap::new(),
1455            struct_defs: HashMap::new(),
1456            enum_defs: HashMap::new(),
1457            class_defs: HashMap::new(),
1458            trait_defs: HashMap::new(),
1459            file: "-e".to_string(),
1460            output_handles: HashMap::new(),
1461            input_handles: HashMap::new(),
1462            ofs: String::new(),
1463            ors: String::new(),
1464            irs: Some("\n".to_string()),
1465            errno: String::new(),
1466            errno_code: 0,
1467            eval_error: String::new(),
1468            eval_error_code: 0,
1469            eval_error_value: None,
1470            argv: Vec::new(),
1471            env: IndexMap::new(),
1472            env_materialized: false,
1473            program_name: "stryke".to_string(),
1474            line_number: 0,
1475            last_readline_handle: String::new(),
1476            last_stdin_die_bracket: "<STDIN>".to_string(),
1477            handle_line_numbers: HashMap::new(),
1478            flip_flop_active: Vec::new(),
1479            flip_flop_exclusive_left_line: Vec::new(),
1480            flip_flop_sequence: Vec::new(),
1481            flip_flop_last_dot: Vec::new(),
1482            flip_flop_tree: HashMap::new(),
1483            sigint_pending_caret: Cell::new(false),
1484            auto_split: false,
1485            field_separator: None,
1486            begin_blocks: Vec::new(),
1487            unit_check_blocks: Vec::new(),
1488            check_blocks: Vec::new(),
1489            init_blocks: Vec::new(),
1490            end_blocks: Vec::new(),
1491            warnings: false,
1492            output_autoflush: false,
1493            default_print_handle: "STDOUT".to_string(),
1494            suppress_stdout: false,
1495            child_exit_status: 0,
1496            last_match: String::new(),
1497            prematch: String::new(),
1498            postmatch: String::new(),
1499            last_paren_match: String::new(),
1500            list_separator: " ".to_string(),
1501            script_start_time,
1502            compile_hints: 0,
1503            warning_bits: 0,
1504            global_phase: "RUN".to_string(),
1505            subscript_sep: "\x1c".to_string(),
1506            inplace_edit: String::new(),
1507            debug_flags: 0,
1508            perl_debug_flags: 0,
1509            eval_nesting: 0,
1510            argv_current_file: String::new(),
1511            diamond_next_idx: 0,
1512            diamond_reader: None,
1513            strict_refs: false,
1514            strict_subs: false,
1515            strict_vars: false,
1516            utf8_pragma: false,
1517            open_pragma_utf8: false,
1518            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1519            feature_bits: FEAT_SAY,
1520            num_threads: 0, // lazily read from rayon on first parallel op
1521            regex_cache: HashMap::new(),
1522            regex_last: None,
1523            regex_match_memo: None,
1524            regex_capture_scope_fresh: false,
1525            regex_pos: HashMap::new(),
1526            state_vars: HashMap::new(),
1527            state_bindings_stack: Vec::new(),
1528            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1529            dir_handles: HashMap::new(),
1530            io_file_slots: HashMap::new(),
1531            pipe_children: HashMap::new(),
1532            socket_handles: HashMap::new(),
1533            wantarray_kind: WantarrayCtx::Scalar,
1534            profiler: None,
1535            module_export_lists: HashMap::new(),
1536            virtual_modules: HashMap::new(),
1537            tied_hashes: HashMap::new(),
1538            tied_scalars: HashMap::new(),
1539            tied_arrays: HashMap::new(),
1540            overload_table: HashMap::new(),
1541            format_templates: HashMap::new(),
1542            special_caret_scalars,
1543            format_page_number: 0,
1544            format_lines_per_page: 60,
1545            format_lines_left: 0,
1546            format_line_break_chars: "\n".to_string(),
1547            format_top_name: String::new(),
1548            accumulator_format: String::new(),
1549            max_system_fd: 2,
1550            emergency_memory: String::new(),
1551            last_subpattern_name: String::new(),
1552            inc_hook_index: 0,
1553            multiline_match: false,
1554            executable_path,
1555            formfeed_string: "\x0c".to_string(),
1556            glob_handle_alias: HashMap::new(),
1557            glob_restore_frames: vec![Vec::new()],
1558            special_var_restore_frames: vec![Vec::new()],
1559            reflection_hashes_ready: false,
1560            english_enabled: false,
1561            english_no_match_vars: false,
1562            english_match_vars_ever_enabled: false,
1563            english_lexical_scalars: vec![HashSet::new()],
1564            our_lexical_scalars: vec![HashSet::new()],
1565            vm_jit_enabled: !matches!(
1566                std::env::var("STRYKE_NO_JIT"),
1567                Ok(v)
1568                    if v == "1"
1569                        || v.eq_ignore_ascii_case("true")
1570                        || v.eq_ignore_ascii_case("yes")
1571            ),
1572            disasm_bytecode: false,
1573            pec_precompiled_chunk: None,
1574            pec_cache_fingerprint: None,
1575            in_generator: false,
1576            line_mode_skip_main: false,
1577            line_mode_eof_pending: false,
1578            line_mode_stdin_pending: VecDeque::new(),
1579            rate_limit_slots: Vec::new(),
1580            log_level_override: None,
1581            current_sub_stack: Vec::new(),
1582            debugger: None,
1583            debug_call_stack: Vec::new(),
1584        };
1585        s.install_overload_pragma_stubs();
1586        crate::list_util::install_scalar_util(&mut s);
1587        crate::list_util::install_sub_util(&mut s);
1588        s.install_utf8_unicode_to_native_stub();
1589        s
1590    }
1591
1592    /// `utf8::unicode_to_native` — core XS in perl; JSON::PP calls it from BEGIN before utf8_heavy.
1593    fn install_utf8_unicode_to_native_stub(&mut self) {
1594        let empty: Block = vec![];
1595        let key = "utf8::unicode_to_native".to_string();
1596        self.subs.insert(
1597            key.clone(),
1598            Arc::new(PerlSub {
1599                name: key,
1600                params: vec![],
1601                body: empty,
1602                prototype: None,
1603                closure_env: None,
1604                fib_like: None,
1605            }),
1606        );
1607    }
1608
1609    /// Lazily populate the reflection hashes (`%b`, `%stryke::builtins`, etc.)
1610    /// on first access. This avoids building ~12k hash entries on startup for
1611    /// one-liners that never touch introspection.
1612    pub(crate) fn ensure_reflection_hashes(&mut self) {
1613        if self.reflection_hashes_ready {
1614            return;
1615        }
1616        self.reflection_hashes_ready = true;
1617        let builtins_map = crate::builtins::builtins_hash_map();
1618        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1619        let extensions_map = crate::builtins::extensions_hash_map();
1620        let aliases_map = crate::builtins::aliases_hash_map();
1621        let descriptions_map = crate::builtins::descriptions_hash_map();
1622        let categories_map = crate::builtins::categories_hash_map();
1623        let primaries_map = crate::builtins::primaries_hash_map();
1624        let all_map = crate::builtins::all_hash_map();
1625        self.scope
1626            .declare_hash_global_frozen("stryke::builtins", builtins_map.clone());
1627        self.scope
1628            .declare_hash_global_frozen("stryke::perl_compats", perl_compats_map.clone());
1629        self.scope
1630            .declare_hash_global_frozen("stryke::extensions", extensions_map.clone());
1631        self.scope
1632            .declare_hash_global_frozen("stryke::aliases", aliases_map.clone());
1633        self.scope
1634            .declare_hash_global_frozen("stryke::descriptions", descriptions_map.clone());
1635        self.scope
1636            .declare_hash_global_frozen("stryke::categories", categories_map.clone());
1637        self.scope
1638            .declare_hash_global_frozen("stryke::primaries", primaries_map.clone());
1639        self.scope
1640            .declare_hash_global_frozen("stryke::all", all_map.clone());
1641        // Short aliases: only declare if no user-declared hash with that name
1642        // exists, to avoid overwriting `my %e` etc.
1643        for (name, val) in [
1644            ("b", builtins_map),
1645            ("pc", perl_compats_map),
1646            ("e", extensions_map),
1647            ("a", aliases_map),
1648            ("d", descriptions_map),
1649            ("c", categories_map),
1650            ("p", primaries_map),
1651            ("all", all_map),
1652        ] {
1653            if !self.scope.any_frame_has_hash(name) {
1654                self.scope.declare_hash_global_frozen(name, val);
1655            }
1656        }
1657    }
1658
1659    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1660    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1661    /// strict subs and to satisfy `use overload ();` call sites.
1662    fn install_overload_pragma_stubs(&mut self) {
1663        let empty: Block = vec![];
1664        for key in ["overload::import", "overload::unimport"] {
1665            let name = key.to_string();
1666            self.subs.insert(
1667                name.clone(),
1668                Arc::new(PerlSub {
1669                    name,
1670                    params: vec![],
1671                    body: empty.clone(),
1672                    prototype: None,
1673                    closure_env: None,
1674                    fib_like: None,
1675                }),
1676            );
1677        }
1678    }
1679
1680    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1681    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1682    pub fn line_mode_worker_clone(&self) -> Interpreter {
1683        Interpreter {
1684            scope: self.scope.clone(),
1685            subs: self.subs.clone(),
1686            struct_defs: self.struct_defs.clone(),
1687            enum_defs: self.enum_defs.clone(),
1688            class_defs: self.class_defs.clone(),
1689            trait_defs: self.trait_defs.clone(),
1690            file: self.file.clone(),
1691            output_handles: HashMap::new(),
1692            input_handles: HashMap::new(),
1693            ofs: self.ofs.clone(),
1694            ors: self.ors.clone(),
1695            irs: self.irs.clone(),
1696            errno: self.errno.clone(),
1697            errno_code: self.errno_code,
1698            eval_error: self.eval_error.clone(),
1699            eval_error_code: self.eval_error_code,
1700            eval_error_value: self.eval_error_value.clone(),
1701            argv: self.argv.clone(),
1702            env: self.env.clone(),
1703            env_materialized: self.env_materialized,
1704            program_name: self.program_name.clone(),
1705            line_number: 0,
1706            last_readline_handle: String::new(),
1707            last_stdin_die_bracket: "<STDIN>".to_string(),
1708            handle_line_numbers: HashMap::new(),
1709            flip_flop_active: Vec::new(),
1710            flip_flop_exclusive_left_line: Vec::new(),
1711            flip_flop_sequence: Vec::new(),
1712            flip_flop_last_dot: Vec::new(),
1713            flip_flop_tree: HashMap::new(),
1714            sigint_pending_caret: Cell::new(false),
1715            auto_split: self.auto_split,
1716            field_separator: self.field_separator.clone(),
1717            begin_blocks: self.begin_blocks.clone(),
1718            unit_check_blocks: self.unit_check_blocks.clone(),
1719            check_blocks: self.check_blocks.clone(),
1720            init_blocks: self.init_blocks.clone(),
1721            end_blocks: self.end_blocks.clone(),
1722            warnings: self.warnings,
1723            output_autoflush: self.output_autoflush,
1724            default_print_handle: self.default_print_handle.clone(),
1725            suppress_stdout: self.suppress_stdout,
1726            child_exit_status: self.child_exit_status,
1727            last_match: self.last_match.clone(),
1728            prematch: self.prematch.clone(),
1729            postmatch: self.postmatch.clone(),
1730            last_paren_match: self.last_paren_match.clone(),
1731            list_separator: self.list_separator.clone(),
1732            script_start_time: self.script_start_time,
1733            compile_hints: self.compile_hints,
1734            warning_bits: self.warning_bits,
1735            global_phase: self.global_phase.clone(),
1736            subscript_sep: self.subscript_sep.clone(),
1737            inplace_edit: self.inplace_edit.clone(),
1738            debug_flags: self.debug_flags,
1739            perl_debug_flags: self.perl_debug_flags,
1740            eval_nesting: self.eval_nesting,
1741            argv_current_file: String::new(),
1742            diamond_next_idx: 0,
1743            diamond_reader: None,
1744            strict_refs: self.strict_refs,
1745            strict_subs: self.strict_subs,
1746            strict_vars: self.strict_vars,
1747            utf8_pragma: self.utf8_pragma,
1748            open_pragma_utf8: self.open_pragma_utf8,
1749            feature_bits: self.feature_bits,
1750            num_threads: 0,
1751            regex_cache: self.regex_cache.clone(),
1752            regex_last: self.regex_last.clone(),
1753            regex_match_memo: self.regex_match_memo.clone(),
1754            regex_capture_scope_fresh: false,
1755            regex_pos: self.regex_pos.clone(),
1756            state_vars: self.state_vars.clone(),
1757            state_bindings_stack: Vec::new(),
1758            rand_rng: self.rand_rng.clone(),
1759            dir_handles: HashMap::new(),
1760            io_file_slots: HashMap::new(),
1761            pipe_children: HashMap::new(),
1762            socket_handles: HashMap::new(),
1763            wantarray_kind: self.wantarray_kind,
1764            profiler: None,
1765            module_export_lists: self.module_export_lists.clone(),
1766            virtual_modules: self.virtual_modules.clone(),
1767            tied_hashes: self.tied_hashes.clone(),
1768            tied_scalars: self.tied_scalars.clone(),
1769            tied_arrays: self.tied_arrays.clone(),
1770            overload_table: self.overload_table.clone(),
1771            format_templates: self.format_templates.clone(),
1772            special_caret_scalars: self.special_caret_scalars.clone(),
1773            format_page_number: self.format_page_number,
1774            format_lines_per_page: self.format_lines_per_page,
1775            format_lines_left: self.format_lines_left,
1776            format_line_break_chars: self.format_line_break_chars.clone(),
1777            format_top_name: self.format_top_name.clone(),
1778            accumulator_format: self.accumulator_format.clone(),
1779            max_system_fd: self.max_system_fd,
1780            emergency_memory: self.emergency_memory.clone(),
1781            last_subpattern_name: self.last_subpattern_name.clone(),
1782            inc_hook_index: self.inc_hook_index,
1783            multiline_match: self.multiline_match,
1784            executable_path: self.executable_path.clone(),
1785            formfeed_string: self.formfeed_string.clone(),
1786            glob_handle_alias: self.glob_handle_alias.clone(),
1787            glob_restore_frames: self.glob_restore_frames.clone(),
1788            special_var_restore_frames: self.special_var_restore_frames.clone(),
1789            reflection_hashes_ready: self.reflection_hashes_ready,
1790            english_enabled: self.english_enabled,
1791            english_no_match_vars: self.english_no_match_vars,
1792            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
1793            english_lexical_scalars: self.english_lexical_scalars.clone(),
1794            our_lexical_scalars: self.our_lexical_scalars.clone(),
1795            vm_jit_enabled: self.vm_jit_enabled,
1796            disasm_bytecode: self.disasm_bytecode,
1797            // Sideband cache fields belong to the top-level driver, not line-mode workers.
1798            pec_precompiled_chunk: None,
1799            pec_cache_fingerprint: None,
1800            in_generator: false,
1801            line_mode_skip_main: false,
1802            line_mode_eof_pending: false,
1803            line_mode_stdin_pending: VecDeque::new(),
1804            rate_limit_slots: Vec::new(),
1805            log_level_override: self.log_level_override,
1806            current_sub_stack: Vec::new(),
1807            debugger: None,
1808            debug_call_stack: Vec::new(),
1809        }
1810    }
1811
1812    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
1813    pub(crate) fn parallel_thread_count(&mut self) -> usize {
1814        if self.num_threads == 0 {
1815            self.num_threads = rayon::current_num_threads();
1816        }
1817        self.num_threads
1818    }
1819
1820    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
1821    pub(crate) fn eval_par_list_call(
1822        &mut self,
1823        name: &str,
1824        args: &[PerlValue],
1825        ctx: WantarrayCtx,
1826        line: usize,
1827    ) -> PerlResult<PerlValue> {
1828        match name {
1829            "puniq" => {
1830                let (list_src, show_prog) = match args.len() {
1831                    0 => return Err(PerlError::runtime("puniq: expected LIST", line)),
1832                    1 => (&args[0], false),
1833                    2 => (&args[0], args[1].is_true()),
1834                    _ => {
1835                        return Err(PerlError::runtime(
1836                            "puniq: expected LIST [, progress => EXPR]",
1837                            line,
1838                        ));
1839                    }
1840                };
1841                let list = list_src.to_list();
1842                let n_threads = self.parallel_thread_count();
1843                let pmap_progress = PmapProgress::new(show_prog, list.len());
1844                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
1845                pmap_progress.finish();
1846                if ctx == WantarrayCtx::List {
1847                    Ok(PerlValue::array(out))
1848                } else {
1849                    Ok(PerlValue::integer(out.len() as i64))
1850                }
1851            }
1852            "pfirst" => {
1853                let (code_val, list_src, show_prog) = match args.len() {
1854                    2 => (&args[0], &args[1], false),
1855                    3 => (&args[0], &args[1], args[2].is_true()),
1856                    _ => {
1857                        return Err(PerlError::runtime(
1858                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
1859                            line,
1860                        ));
1861                    }
1862                };
1863                let Some(sub) = code_val.as_code_ref() else {
1864                    return Err(PerlError::runtime(
1865                        "pfirst: first argument must be a code reference",
1866                        line,
1867                    ));
1868                };
1869                let sub = sub.clone();
1870                let list = list_src.to_list();
1871                if list.is_empty() {
1872                    return Ok(PerlValue::UNDEF);
1873                }
1874                let pmap_progress = PmapProgress::new(show_prog, list.len());
1875                let subs = self.subs.clone();
1876                let (scope_capture, atomic_arrays, atomic_hashes) =
1877                    self.scope.capture_with_atomics();
1878                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
1879                    let mut local_interp = Interpreter::new();
1880                    local_interp.subs = subs.clone();
1881                    local_interp.scope.restore_capture(&scope_capture);
1882                    local_interp
1883                        .scope
1884                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1885                    local_interp.enable_parallel_guard();
1886                    local_interp.scope.set_topic(item);
1887                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1888                        Ok(v) => v.is_true(),
1889                        Err(_) => false,
1890                    }
1891                });
1892                pmap_progress.finish();
1893                Ok(out.unwrap_or(PerlValue::UNDEF))
1894            }
1895            "pany" => {
1896                let (code_val, list_src, show_prog) = match args.len() {
1897                    2 => (&args[0], &args[1], false),
1898                    3 => (&args[0], &args[1], args[2].is_true()),
1899                    _ => {
1900                        return Err(PerlError::runtime(
1901                            "pany: expected BLOCK, LIST [, progress => EXPR]",
1902                            line,
1903                        ));
1904                    }
1905                };
1906                let Some(sub) = code_val.as_code_ref() else {
1907                    return Err(PerlError::runtime(
1908                        "pany: first argument must be a code reference",
1909                        line,
1910                    ));
1911                };
1912                let sub = sub.clone();
1913                let list = list_src.to_list();
1914                let pmap_progress = PmapProgress::new(show_prog, list.len());
1915                let subs = self.subs.clone();
1916                let (scope_capture, atomic_arrays, atomic_hashes) =
1917                    self.scope.capture_with_atomics();
1918                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
1919                    let mut local_interp = Interpreter::new();
1920                    local_interp.subs = subs.clone();
1921                    local_interp.scope.restore_capture(&scope_capture);
1922                    local_interp
1923                        .scope
1924                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1925                    local_interp.enable_parallel_guard();
1926                    local_interp.scope.set_topic(item);
1927                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1928                        Ok(v) => v.is_true(),
1929                        Err(_) => false,
1930                    }
1931                });
1932                pmap_progress.finish();
1933                Ok(PerlValue::integer(if b { 1 } else { 0 }))
1934            }
1935            _ => Err(PerlError::runtime(
1936                format!("internal: unknown par_list builtin {name}"),
1937                line,
1938            )),
1939        }
1940    }
1941
1942    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
1943        #[cfg(unix)]
1944        if let Some(sig) = s.signal() {
1945            return sig as i64 & 0x7f;
1946        }
1947        let code = s.code().unwrap_or(0) as i64;
1948        code << 8
1949    }
1950
1951    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
1952        self.child_exit_status = self.encode_exit_status(s);
1953    }
1954
1955    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
1956    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
1957        self.errno = e.to_string();
1958        self.errno_code = e.raw_os_error().unwrap_or(0);
1959    }
1960
1961    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
1962    ///
1963    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
1964    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
1965    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
1966    ///
1967    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
1968    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
1969    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
1970    /// `~/.ssh/config` and keys apply.
1971    pub(crate) fn ssh_builtin_execute(&mut self, args: &[PerlValue]) -> PerlResult<PerlValue> {
1972        use std::process::Command;
1973        let mut cmd = Command::new("ssh");
1974        #[cfg(unix)]
1975        {
1976            use libc::geteuid;
1977            let home_for_ssh = if unsafe { geteuid() } == 0 {
1978                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
1979            } else {
1980                None
1981            };
1982            if let Some(h) = home_for_ssh {
1983                cmd.env("HOME", h);
1984            } else if std::env::var_os("HOME").is_none() {
1985                if let Some(h) = pw_home_dir_for_current_uid() {
1986                    cmd.env("HOME", h);
1987                }
1988            }
1989        }
1990        for a in args {
1991            cmd.arg(a.to_string());
1992        }
1993        match cmd.status() {
1994            Ok(s) => {
1995                self.record_child_exit_status(s);
1996                Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
1997            }
1998            Err(e) => {
1999                self.apply_io_error_to_errno(&e);
2000                Ok(PerlValue::integer(-1))
2001            }
2002        }
2003    }
2004
2005    /// Set `$@` message; numeric side is `0` if empty, else `1`.
2006    pub(crate) fn set_eval_error(&mut self, msg: String) {
2007        self.eval_error = msg;
2008        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2009        self.eval_error_value = None;
2010    }
2011
2012    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &PerlError) {
2013        self.eval_error = e.to_string();
2014        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
2015        self.eval_error_value = e.die_value.clone();
2016    }
2017
2018    pub(crate) fn clear_eval_error(&mut self) {
2019        self.eval_error = String::new();
2020        self.eval_error_code = 0;
2021        self.eval_error_value = None;
2022    }
2023
2024    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
2025    fn bump_line_for_handle(&mut self, handle_key: &str) {
2026        self.last_readline_handle = handle_key.to_string();
2027        *self
2028            .handle_line_numbers
2029            .entry(handle_key.to_string())
2030            .or_insert(0) += 1;
2031    }
2032
2033    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
2034    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
2035        if name.starts_with('^') {
2036            return name.to_string();
2037        }
2038        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
2039            let pkg = self.current_package();
2040            if !pkg.is_empty() && pkg != "main" {
2041                return format!("{}::{}", pkg, name);
2042            }
2043        }
2044        name.to_string()
2045    }
2046
2047    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
2048    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
2049        if name.contains("::") {
2050            return name.to_string();
2051        }
2052        let pkg = self.current_package();
2053        if pkg.is_empty() || pkg == "main" {
2054            format!("main::{}", name)
2055        } else {
2056            format!("{}::{}", pkg, name)
2057        }
2058    }
2059
2060    /// Tree-walker: bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
2061    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
2062        if name.contains("::") {
2063            return name.to_string();
2064        }
2065        for (lex, our) in self
2066            .english_lexical_scalars
2067            .iter()
2068            .zip(self.our_lexical_scalars.iter())
2069            .rev()
2070        {
2071            if lex.contains(name) {
2072                if our.contains(name) {
2073                    return self.stash_scalar_name_for_package(name);
2074                }
2075                return name.to_string();
2076            }
2077        }
2078        name.to_string()
2079    }
2080
2081    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
2082    pub(crate) fn tie_execute(
2083        &mut self,
2084        target_kind: u8,
2085        target_name: &str,
2086        class_and_args: Vec<PerlValue>,
2087        line: usize,
2088    ) -> PerlResult<PerlValue> {
2089        let mut it = class_and_args.into_iter();
2090        let class = it.next().unwrap_or(PerlValue::UNDEF);
2091        let pkg = class.to_string();
2092        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
2093        let tie_ctor = match target_kind {
2094            0 => "TIESCALAR",
2095            1 => "TIEARRAY",
2096            2 => "TIEHASH",
2097            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
2098        };
2099        let tie_fn = format!("{}::{}", pkg, tie_ctor);
2100        let sub = self
2101            .subs
2102            .get(&tie_fn)
2103            .cloned()
2104            .ok_or_else(|| PerlError::runtime(format!("tie: cannot find &{}", tie_fn), line))?;
2105        let mut call_args = vec![PerlValue::string(pkg.clone())];
2106        call_args.extend(it);
2107        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
2108            Ok(v) => v,
2109            Err(FlowOrError::Flow(_)) => PerlValue::UNDEF,
2110            Err(FlowOrError::Error(e)) => return Err(e),
2111        };
2112        match target_kind {
2113            0 => {
2114                self.tied_scalars.insert(target_name.to_string(), obj);
2115            }
2116            1 => {
2117                let key = self.stash_array_name_for_package(target_name);
2118                self.tied_arrays.insert(key, obj);
2119            }
2120            2 => {
2121                self.tied_hashes.insert(target_name.to_string(), obj);
2122            }
2123            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
2124        }
2125        Ok(PerlValue::UNDEF)
2126    }
2127
2128    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
2129    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
2130        let key = format!("{}::ISA", class);
2131        self.scope
2132            .get_array(&key)
2133            .into_iter()
2134            .map(|v| v.to_string())
2135            .collect()
2136    }
2137
2138    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
2139        let p = |c: &str| self.parents_of_class(c);
2140        linearize_c3(class, &p, 0)
2141    }
2142
2143    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
2144    pub(crate) fn resolve_method_full_name(
2145        &self,
2146        invocant_class: &str,
2147        method: &str,
2148        super_mode: bool,
2149    ) -> Option<String> {
2150        let mro = self.mro_linearize(invocant_class);
2151        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
2152        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
2153        // even when running `C::meth`.
2154        let start = if super_mode {
2155            mro.iter()
2156                .position(|p| p == invocant_class)
2157                .map(|i| i + 1)
2158                // If the class string does not appear in MRO (should be rare), skip the first
2159                // entry so we still search parents before giving up.
2160                .unwrap_or(1)
2161        } else {
2162            0
2163        };
2164        for pkg in mro.iter().skip(start) {
2165            if pkg == "UNIVERSAL" {
2166                continue;
2167            }
2168            let fq = format!("{}::{}", pkg, method);
2169            if self.subs.contains_key(&fq) {
2170                return Some(fq);
2171            }
2172        }
2173        mro.iter()
2174            .skip(start)
2175            .find(|p| *p != "UNIVERSAL")
2176            .map(|pkg| format!("{}::{}", pkg, method))
2177    }
2178
2179    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
2180        if let Some(alias) = self.glob_handle_alias.get(name) {
2181            return alias.clone();
2182        }
2183        // `print $fh …` stores the handle as "$varname"; resolve it by
2184        // reading the scalar variable which holds the IO handle name.
2185        if let Some(var_name) = name.strip_prefix('$') {
2186            let val = self.scope.get_scalar(var_name);
2187            let s = val.to_string();
2188            if !s.is_empty() {
2189                return self.resolve_io_handle_name(&s);
2190            }
2191        }
2192        name.to_string()
2193    }
2194
2195    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
2196    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
2197        if name.contains("::") {
2198            name.to_string()
2199        } else {
2200            self.qualify_sub_key(name)
2201        }
2202    }
2203
2204    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
2205    pub(crate) fn copy_typeglob_slots(
2206        &mut self,
2207        lhs: &str,
2208        rhs: &str,
2209        line: usize,
2210    ) -> PerlResult<()> {
2211        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
2212        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
2213        match self.subs.get(&rhs_sub).cloned() {
2214            Some(s) => {
2215                self.subs.insert(lhs_sub, s);
2216            }
2217            None => {
2218                self.subs.remove(&lhs_sub);
2219            }
2220        }
2221        let sv = self.scope.get_scalar(rhs);
2222        self.scope
2223            .set_scalar(lhs, sv.clone())
2224            .map_err(|e| e.at_line(line))?;
2225        let lhs_an = self.stash_array_name_for_package(lhs);
2226        let rhs_an = self.stash_array_name_for_package(rhs);
2227        let av = self.scope.get_array(&rhs_an);
2228        self.scope
2229            .set_array(&lhs_an, av.clone())
2230            .map_err(|e| e.at_line(line))?;
2231        let hv = self.scope.get_hash(rhs);
2232        self.scope
2233            .set_hash(lhs, hv.clone())
2234            .map_err(|e| e.at_line(line))?;
2235        match self.glob_handle_alias.get(rhs).cloned() {
2236            Some(t) => {
2237                self.glob_handle_alias.insert(lhs.to_string(), t);
2238            }
2239            None => {
2240                self.glob_handle_alias.remove(lhs);
2241            }
2242        }
2243        Ok(())
2244    }
2245
2246    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2247    pub(crate) fn install_format_decl(
2248        &mut self,
2249        basename: &str,
2250        lines: &[String],
2251        line: usize,
2252    ) -> PerlResult<()> {
2253        let pkg = self.current_package();
2254        let key = format!("{}::{}", pkg, basename);
2255        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2256        self.format_templates.insert(key, Arc::new(tmpl));
2257        Ok(())
2258    }
2259
2260    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2261    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2262        let pkg = self.current_package();
2263        let ent = self.overload_table.entry(pkg).or_default();
2264        for (k, v) in pairs {
2265            ent.insert(k.clone(), v.clone());
2266        }
2267    }
2268
2269    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2270    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2271    pub(crate) fn local_declare_typeglob(
2272        &mut self,
2273        lhs: &str,
2274        rhs: Option<&str>,
2275        line: usize,
2276    ) -> PerlResult<()> {
2277        let old = self.glob_handle_alias.remove(lhs);
2278        let Some(frame) = self.glob_restore_frames.last_mut() else {
2279            return Err(PerlError::runtime(
2280                "internal: no glob restore frame for local *GLOB",
2281                line,
2282            ));
2283        };
2284        frame.push((lhs.to_string(), old));
2285        if let Some(r) = rhs {
2286            self.glob_handle_alias
2287                .insert(lhs.to_string(), r.to_string());
2288        }
2289        Ok(())
2290    }
2291
2292    pub(crate) fn scope_push_hook(&mut self) {
2293        self.scope.push_frame();
2294        self.glob_restore_frames.push(Vec::new());
2295        self.special_var_restore_frames.push(Vec::new());
2296        self.english_lexical_scalars.push(HashSet::new());
2297        self.our_lexical_scalars.push(HashSet::new());
2298        self.state_bindings_stack.push(Vec::new());
2299    }
2300
2301    #[inline]
2302    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2303        if let Some(s) = self.english_lexical_scalars.last_mut() {
2304            s.insert(name.to_string());
2305        }
2306    }
2307
2308    #[inline]
2309    fn note_our_scalar(&mut self, bare_name: &str) {
2310        if let Some(s) = self.our_lexical_scalars.last_mut() {
2311            s.insert(bare_name.to_string());
2312        }
2313    }
2314
2315    pub(crate) fn scope_pop_hook(&mut self) {
2316        if !self.scope.can_pop_frame() {
2317            return;
2318        }
2319        // Execute deferred blocks in LIFO order before popping the frame.
2320        // Important: defer blocks run in the CURRENT scope (not a new frame),
2321        // so they can modify variables in the enclosing scope.
2322        let defers = self.scope.take_defers();
2323        for coderef in defers {
2324            if let Some(sub) = coderef.as_code_ref() {
2325                // Execute the defer block body directly in the current scope,
2326                // without creating a new frame or restoring closure captures.
2327                // This allows defer { $x = 100 } to modify the outer $x.
2328                let saved_wa = self.wantarray_kind;
2329                self.wantarray_kind = WantarrayCtx::Void;
2330                let _ = self.exec_block_no_scope(&sub.body);
2331                self.wantarray_kind = saved_wa;
2332            }
2333        }
2334        // Save state variable values back before popping the frame
2335        if let Some(bindings) = self.state_bindings_stack.pop() {
2336            for (var_name, state_key) in &bindings {
2337                let val = self.scope.get_scalar(var_name).clone();
2338                self.state_vars.insert(state_key.clone(), val);
2339            }
2340        }
2341        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2342        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2343        if let Some(entries) = self.special_var_restore_frames.pop() {
2344            for (name, old) in entries.into_iter().rev() {
2345                let _ = self.set_special_var(&name, &old);
2346            }
2347        }
2348        if let Some(entries) = self.glob_restore_frames.pop() {
2349            for (name, old) in entries.into_iter().rev() {
2350                match old {
2351                    Some(s) => {
2352                        self.glob_handle_alias.insert(name, s);
2353                    }
2354                    None => {
2355                        self.glob_handle_alias.remove(&name);
2356                    }
2357                }
2358            }
2359        }
2360        self.scope.pop_frame();
2361        let _ = self.english_lexical_scalars.pop();
2362        let _ = self.our_lexical_scalars.pop();
2363    }
2364
2365    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2366    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2367    #[inline]
2368    pub(crate) fn enable_parallel_guard(&mut self) {
2369        self.scope.set_parallel_guard(true);
2370    }
2371
2372    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues so a later tree-walker
2373    /// run does not execute them again.
2374    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2375        self.begin_blocks.clear();
2376        self.unit_check_blocks.clear();
2377        self.check_blocks.clear();
2378        self.init_blocks.clear();
2379        self.end_blocks.clear();
2380    }
2381
2382    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2383    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2384    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2385    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2386    /// there because it only calls [`Scope::pop_frame`].
2387    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2388        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2389            self.scope_pop_hook();
2390        }
2391    }
2392
2393    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2394    ///
2395    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2396    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2397    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2398    /// workers that call `perl_signal::poll`).
2399    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> PerlResult<()> {
2400        self.touch_env_hash("SIG");
2401        let v = self.scope.get_hash_element("SIG", sig);
2402        if v.is_undef() {
2403            return Self::default_sig_action(sig);
2404        }
2405        if let Some(s) = v.as_str() {
2406            if s == "IGNORE" {
2407                return Ok(());
2408            }
2409            if s == "DEFAULT" {
2410                return Self::default_sig_action(sig);
2411            }
2412        }
2413        if let Some(sub) = v.as_code_ref() {
2414            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2415                Ok(_) => Ok(()),
2416                Err(FlowOrError::Flow(_)) => Ok(()),
2417                Err(FlowOrError::Error(e)) => Err(e),
2418            }
2419        } else {
2420            Self::default_sig_action(sig)
2421        }
2422    }
2423
2424    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2425    #[inline]
2426    fn default_sig_action(sig: &str) -> PerlResult<()> {
2427        match sig {
2428            // 128 + signal number (common shell convention)
2429            "INT" => std::process::exit(130),
2430            "TERM" => std::process::exit(143),
2431            "ALRM" => std::process::exit(142),
2432            // Default for SIGCHLD is ignore
2433            "CHLD" => Ok(()),
2434            _ => Ok(()),
2435        }
2436    }
2437
2438    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2439    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2440    pub fn materialize_env_if_needed(&mut self) {
2441        if self.env_materialized {
2442            return;
2443        }
2444        self.env = std::env::vars()
2445            .map(|(k, v)| (k, PerlValue::string(v)))
2446            .collect();
2447        self.scope
2448            .set_hash("ENV", self.env.clone())
2449            .expect("set %ENV");
2450        self.env_materialized = true;
2451    }
2452
2453    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2454    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2455        self.materialize_env_if_needed();
2456        if let Some(x) = self.log_level_override {
2457            return x;
2458        }
2459        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2460        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2461    }
2462
2463    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2464    pub(crate) fn no_color_effective(&mut self) -> bool {
2465        self.materialize_env_if_needed();
2466        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2467        if v.is_undef() {
2468            return false;
2469        }
2470        !v.to_string().is_empty()
2471    }
2472
2473    #[inline]
2474    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2475        if hash_name == "ENV" {
2476            self.materialize_env_if_needed();
2477        } else if !self.reflection_hashes_ready && !self.scope.has_lexical_hash(hash_name) {
2478            match hash_name {
2479                "b"
2480                | "pc"
2481                | "e"
2482                | "a"
2483                | "d"
2484                | "c"
2485                | "p"
2486                | "all"
2487                | "stryke::builtins"
2488                | "stryke::perl_compats"
2489                | "stryke::extensions"
2490                | "stryke::aliases"
2491                | "stryke::descriptions"
2492                | "stryke::categories"
2493                | "stryke::primaries"
2494                | "stryke::all" => {
2495                    self.ensure_reflection_hashes();
2496                }
2497                _ => {}
2498            }
2499        }
2500    }
2501
2502    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2503    pub(crate) fn exists_arrow_hash_element(
2504        &self,
2505        container: PerlValue,
2506        key: &str,
2507        line: usize,
2508    ) -> PerlResult<bool> {
2509        if let Some(r) = container.as_hash_ref() {
2510            return Ok(r.read().contains_key(key));
2511        }
2512        if let Some(b) = container.as_blessed_ref() {
2513            let data = b.data.read();
2514            if let Some(r) = data.as_hash_ref() {
2515                return Ok(r.read().contains_key(key));
2516            }
2517            if let Some(hm) = data.as_hash_map() {
2518                return Ok(hm.contains_key(key));
2519            }
2520            return Err(PerlError::runtime(
2521                "exists argument is not a HASH reference",
2522                line,
2523            ));
2524        }
2525        Err(PerlError::runtime(
2526            "exists argument is not a HASH reference",
2527            line,
2528        ))
2529    }
2530
2531    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
2532    pub(crate) fn delete_arrow_hash_element(
2533        &self,
2534        container: PerlValue,
2535        key: &str,
2536        line: usize,
2537    ) -> PerlResult<PerlValue> {
2538        if let Some(r) = container.as_hash_ref() {
2539            return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2540        }
2541        if let Some(b) = container.as_blessed_ref() {
2542            let mut data = b.data.write();
2543            if let Some(r) = data.as_hash_ref() {
2544                return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2545            }
2546            if let Some(mut map) = data.as_hash_map() {
2547                let v = map.shift_remove(key).unwrap_or(PerlValue::UNDEF);
2548                *data = PerlValue::hash(map);
2549                return Ok(v);
2550            }
2551            return Err(PerlError::runtime(
2552                "delete argument is not a HASH reference",
2553                line,
2554            ));
2555        }
2556        Err(PerlError::runtime(
2557            "delete argument is not a HASH reference",
2558            line,
2559        ))
2560    }
2561
2562    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
2563    pub(crate) fn exists_arrow_array_element(
2564        &self,
2565        container: PerlValue,
2566        idx: i64,
2567        line: usize,
2568    ) -> PerlResult<bool> {
2569        if let Some(a) = container.as_array_ref() {
2570            let arr = a.read();
2571            let i = if idx < 0 {
2572                (arr.len() as i64 + idx) as usize
2573            } else {
2574                idx as usize
2575            };
2576            return Ok(i < arr.len());
2577        }
2578        Err(PerlError::runtime(
2579            "exists argument is not an ARRAY reference",
2580            line,
2581        ))
2582    }
2583
2584    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
2585    pub(crate) fn delete_arrow_array_element(
2586        &self,
2587        container: PerlValue,
2588        idx: i64,
2589        line: usize,
2590    ) -> PerlResult<PerlValue> {
2591        if let Some(a) = container.as_array_ref() {
2592            let mut arr = a.write();
2593            let i = if idx < 0 {
2594                (arr.len() as i64 + idx) as usize
2595            } else {
2596                idx as usize
2597            };
2598            if i >= arr.len() {
2599                return Ok(PerlValue::UNDEF);
2600            }
2601            let old = arr.get(i).cloned().unwrap_or(PerlValue::UNDEF);
2602            arr[i] = PerlValue::UNDEF;
2603            return Ok(old);
2604        }
2605        Err(PerlError::runtime(
2606            "delete argument is not an ARRAY reference",
2607            line,
2608        ))
2609    }
2610
2611    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
2612    pub(crate) fn inc_directories(&self) -> Vec<String> {
2613        let mut v: Vec<String> = self
2614            .scope
2615            .get_array("INC")
2616            .into_iter()
2617            .map(|x| x.to_string())
2618            .filter(|s| !s.is_empty())
2619            .collect();
2620        if v.is_empty() {
2621            v.push(".".to_string());
2622        }
2623        v
2624    }
2625
2626    #[inline]
2627    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
2628        matches!(
2629            name,
2630            "_" | "0"
2631                | "!"
2632                | "@"
2633                | "/"
2634                | "\\"
2635                | ","
2636                | "."
2637                | "__PACKAGE__"
2638                | "$$"
2639                | "|"
2640                | "?"
2641                | "\""
2642                | "&"
2643                | "`"
2644                | "'"
2645                | "+"
2646                | "<"
2647                | ">"
2648                | "("
2649                | ")"
2650                | "]"
2651                | ";"
2652                | "ARGV"
2653                | "%"
2654                | "="
2655                | "-"
2656                | ":"
2657                | "*"
2658                | "INC"
2659        ) || name.chars().all(|c| c.is_ascii_digit())
2660            || name.starts_with('^')
2661            || (name.starts_with('#') && name.len() > 1)
2662    }
2663
2664    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2665        if !self.strict_vars
2666            || Self::strict_scalar_exempt(name)
2667            || name.contains("::")
2668            || self.scope.scalar_binding_exists(name)
2669        {
2670            return Ok(());
2671        }
2672        Err(PerlError::runtime(
2673            format!(
2674                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
2675                name, name
2676            ),
2677            line,
2678        )
2679        .into())
2680    }
2681
2682    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2683        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
2684            return Ok(());
2685        }
2686        Err(PerlError::runtime(
2687            format!(
2688                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
2689                name, name
2690            ),
2691            line,
2692        )
2693        .into())
2694    }
2695
2696    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2697        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
2698        if !self.strict_vars
2699            || name.contains("::")
2700            || self.scope.hash_binding_exists(name)
2701            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
2702        {
2703            return Ok(());
2704        }
2705        Err(PerlError::runtime(
2706            format!(
2707                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
2708                name, name
2709            ),
2710            line,
2711        )
2712        .into())
2713    }
2714
2715    fn looks_like_version_only(spec: &str) -> bool {
2716        let t = spec.trim();
2717        !t.is_empty()
2718            && !t.contains('/')
2719            && !t.contains('\\')
2720            && !t.contains("::")
2721            && t.chars()
2722                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
2723            && t.chars().any(|c| c.is_ascii_digit())
2724    }
2725
2726    fn module_spec_to_relpath(spec: &str) -> String {
2727        let t = spec.trim();
2728        if t.contains("::") {
2729            format!("{}.pm", t.replace("::", "/"))
2730        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
2731            t.replace('\\', "/")
2732        } else {
2733            format!("{}.pm", t)
2734        }
2735    }
2736
2737    /// `sub name` in `package P` → stash key `P::name` (otherwise `name` in `main`).
2738    /// `sub Q::name { }` is already fully qualified — do not prepend the current package.
2739    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
2740        if name.contains("::") {
2741            return name.to_string();
2742        }
2743        let pkg = self.current_package();
2744        if pkg.is_empty() || pkg == "main" {
2745            name.to_string()
2746        } else {
2747            format!("{}::{}", pkg, name)
2748        }
2749    }
2750
2751    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
2752    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
2753        let mut msg = format!("Undefined subroutine &{}", name);
2754        if self.strict_subs {
2755            msg.push_str(
2756                " (strict subs: declare the sub or use a fully qualified name before calling)",
2757            );
2758        }
2759        msg
2760    }
2761
2762    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
2763    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
2764        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
2765        if self.strict_subs {
2766            msg.push_str(
2767                " (strict subs: declare the sub or use a fully qualified name before calling)",
2768            );
2769        }
2770        msg
2771    }
2772
2773    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
2774    fn import_alias_key(&self, short: &str) -> String {
2775        self.qualify_sub_key(short)
2776    }
2777
2778    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
2779    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
2780        if imports.len() == 1 {
2781            match &imports[0].kind {
2782                ExprKind::QW(ws) => return ws.is_empty(),
2783                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
2784                ExprKind::List(xs) => return xs.is_empty(),
2785                _ => {}
2786            }
2787        }
2788        false
2789    }
2790
2791    /// After `require`, copy `Module::export` → caller stash per `use` list.
2792    fn apply_module_import(
2793        &mut self,
2794        module: &str,
2795        imports: &[Expr],
2796        line: usize,
2797    ) -> PerlResult<()> {
2798        if imports.is_empty() {
2799            return self.import_all_from_module(module, line);
2800        }
2801        if Self::is_explicit_empty_import_list(imports) {
2802            return Ok(());
2803        }
2804        let names = Self::pragma_import_strings(imports, line)?;
2805        if names.is_empty() {
2806            return Ok(());
2807        }
2808        for name in names {
2809            self.import_one_symbol(module, &name, line)?;
2810        }
2811        Ok(())
2812    }
2813
2814    fn import_all_from_module(&mut self, module: &str, line: usize) -> PerlResult<()> {
2815        if module == "List::Util" {
2816            crate::list_util::ensure_list_util(self);
2817        }
2818        if let Some(lists) = self.module_export_lists.get(module) {
2819            let export: Vec<String> = lists.export.clone();
2820            for short in export {
2821                self.import_named_sub(module, &short, line)?;
2822            }
2823            return Ok(());
2824        }
2825        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
2826        let prefix = format!("{}::", module);
2827        let keys: Vec<String> = self
2828            .subs
2829            .keys()
2830            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
2831            .cloned()
2832            .collect();
2833        for k in keys {
2834            let short = k[prefix.len()..].to_string();
2835            if let Some(sub) = self.subs.get(&k).cloned() {
2836                let alias = self.import_alias_key(&short);
2837                self.subs.insert(alias, sub);
2838            }
2839        }
2840        Ok(())
2841    }
2842
2843    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
2844    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> PerlResult<()> {
2845        if module == "List::Util" {
2846            crate::list_util::ensure_list_util(self);
2847        }
2848        let qual = format!("{}::{}", module, short);
2849        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
2850            PerlError::runtime(
2851                format!(
2852                    "`{}` is not defined in module `{}` (expected `{}`)",
2853                    short, module, qual
2854                ),
2855                line,
2856            )
2857        })?;
2858        let alias = self.import_alias_key(short);
2859        self.subs.insert(alias, sub);
2860        Ok(())
2861    }
2862
2863    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> PerlResult<()> {
2864        if let Some(lists) = self.module_export_lists.get(module) {
2865            let allowed: HashSet<&str> = lists
2866                .export
2867                .iter()
2868                .map(|s| s.as_str())
2869                .chain(lists.export_ok.iter().map(|s| s.as_str()))
2870                .collect();
2871            if !allowed.contains(export) {
2872                return Err(PerlError::runtime(
2873                    format!(
2874                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
2875                        export, module
2876                    ),
2877                    line,
2878                ));
2879            }
2880        }
2881        self.import_named_sub(module, export, line)
2882    }
2883
2884    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
2885    fn record_exporter_our_array_name(&mut self, name: &str, items: &[PerlValue]) {
2886        if name != "EXPORT" && name != "EXPORT_OK" {
2887            return;
2888        }
2889        let pkg = self.current_package();
2890        if pkg.is_empty() || pkg == "main" {
2891            return;
2892        }
2893        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
2894        let ent = self.module_export_lists.entry(pkg).or_default();
2895        if name == "EXPORT" {
2896            ent.export = names;
2897        } else {
2898            ent.export_ok = names;
2899        }
2900    }
2901
2902    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
2903    /// Refresh [`PerlSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
2904    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
2905    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
2906        let key = self.qualify_sub_key(name);
2907        let Some(sub) = self.subs.get(&key).cloned() else {
2908            return;
2909        };
2910        let captured = self.scope.capture();
2911        let closure_env = if captured.is_empty() {
2912            None
2913        } else {
2914            Some(captured)
2915        };
2916        let mut new_sub = (*sub).clone();
2917        new_sub.closure_env = closure_env;
2918        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
2919        self.subs.insert(key, Arc::new(new_sub));
2920    }
2921
2922    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<PerlSub>> {
2923        if let Some(s) = self.subs.get(name) {
2924            return Some(s.clone());
2925        }
2926        if !name.contains("::") {
2927            let pkg = self.current_package();
2928            if !pkg.is_empty() && pkg != "main" {
2929                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
2930                q.push_str(&pkg);
2931                q.push_str("::");
2932                q.push_str(name);
2933                return self.subs.get(&q).cloned();
2934            }
2935        }
2936        None
2937    }
2938
2939    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
2940    /// before calling `import`).
2941    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
2942        if let Some(first) = imports.first() {
2943            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
2944                return &imports[1..];
2945            }
2946        }
2947        imports
2948    }
2949
2950    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
2951    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> PerlResult<Vec<String>> {
2952        let mut out = Vec::new();
2953        for e in imports {
2954            match &e.kind {
2955                ExprKind::String(s) => out.push(s.clone()),
2956                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
2957                ExprKind::Integer(n) => out.push(n.to_string()),
2958                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
2959                // a single interpolated variable.  Reconstruct the sigil+name form.
2960                ExprKind::InterpolatedString(parts) => {
2961                    let mut s = String::new();
2962                    for p in parts {
2963                        match p {
2964                            StringPart::Literal(l) => s.push_str(l),
2965                            StringPart::ScalarVar(v) => {
2966                                s.push('$');
2967                                s.push_str(v);
2968                            }
2969                            StringPart::ArrayVar(v) => {
2970                                s.push('@');
2971                                s.push_str(v);
2972                            }
2973                            _ => {
2974                                return Err(PerlError::runtime(
2975                                    "pragma import must be a compile-time string, qw(), or integer",
2976                                    e.line.max(default_line),
2977                                ));
2978                            }
2979                        }
2980                    }
2981                    out.push(s);
2982                }
2983                _ => {
2984                    return Err(PerlError::runtime(
2985                        "pragma import must be a compile-time string, qw(), or integer",
2986                        e.line.max(default_line),
2987                    ));
2988                }
2989            }
2990        }
2991        Ok(out)
2992    }
2993
2994    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2995        if imports.is_empty() {
2996            self.strict_refs = true;
2997            self.strict_subs = true;
2998            self.strict_vars = true;
2999            return Ok(());
3000        }
3001        let names = Self::pragma_import_strings(imports, line)?;
3002        for name in names {
3003            match name.as_str() {
3004                "refs" => self.strict_refs = true,
3005                "subs" => self.strict_subs = true,
3006                "vars" => self.strict_vars = true,
3007                _ => {
3008                    return Err(PerlError::runtime(
3009                        format!("Unknown strict mode `{}`", name),
3010                        line,
3011                    ));
3012                }
3013            }
3014        }
3015        Ok(())
3016    }
3017
3018    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3019        if imports.is_empty() {
3020            self.strict_refs = false;
3021            self.strict_subs = false;
3022            self.strict_vars = false;
3023            return Ok(());
3024        }
3025        let names = Self::pragma_import_strings(imports, line)?;
3026        for name in names {
3027            match name.as_str() {
3028                "refs" => self.strict_refs = false,
3029                "subs" => self.strict_subs = false,
3030                "vars" => self.strict_vars = false,
3031                _ => {
3032                    return Err(PerlError::runtime(
3033                        format!("Unknown strict mode `{}`", name),
3034                        line,
3035                    ));
3036                }
3037            }
3038        }
3039        Ok(())
3040    }
3041
3042    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3043        let items = Self::pragma_import_strings(imports, line)?;
3044        if items.is_empty() {
3045            return Err(PerlError::runtime(
3046                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
3047                line,
3048            ));
3049        }
3050        for item in items {
3051            let s = item.trim();
3052            if let Some(rest) = s.strip_prefix(':') {
3053                self.apply_feature_bundle(rest, line)?;
3054            } else {
3055                self.apply_feature_name(s, true, line)?;
3056            }
3057        }
3058        Ok(())
3059    }
3060
3061    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3062        if imports.is_empty() {
3063            self.feature_bits = 0;
3064            return Ok(());
3065        }
3066        let items = Self::pragma_import_strings(imports, line)?;
3067        for item in items {
3068            let s = item.trim();
3069            if let Some(rest) = s.strip_prefix(':') {
3070                self.clear_feature_bundle(rest);
3071            } else {
3072                self.apply_feature_name(s, false, line)?;
3073            }
3074        }
3075        Ok(())
3076    }
3077
3078    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> PerlResult<()> {
3079        let key = v.trim();
3080        match key {
3081            "5.10" | "5.010" | "5.10.0" => {
3082                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3083            }
3084            "5.12" | "5.012" | "5.12.0" => {
3085                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
3086            }
3087            _ => {
3088                return Err(PerlError::runtime(
3089                    format!("unsupported feature bundle :{}", key),
3090                    line,
3091                ));
3092            }
3093        }
3094        Ok(())
3095    }
3096
3097    fn clear_feature_bundle(&mut self, v: &str) {
3098        let key = v.trim();
3099        if matches!(
3100            key,
3101            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
3102        ) {
3103            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
3104        }
3105    }
3106
3107    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> PerlResult<()> {
3108        let bit = match name {
3109            "say" => FEAT_SAY,
3110            "state" => FEAT_STATE,
3111            "switch" => FEAT_SWITCH,
3112            "unicode_strings" => FEAT_UNICODE_STRINGS,
3113            // Features that stryke accepts as known but tracks no separate bit for —
3114            // either always-on, always-off, or syntactic sugar already enabled.
3115            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
3116            "postderef"
3117            | "postderef_qq"
3118            | "evalbytes"
3119            | "current_sub"
3120            | "fc"
3121            | "lexical_subs"
3122            | "signatures"
3123            | "refaliasing"
3124            | "bitwise"
3125            | "isa"
3126            | "indirect"
3127            | "multidimensional"
3128            | "bareword_filehandles"
3129            | "try"
3130            | "defer"
3131            | "extra_paired_delimiters"
3132            | "module_true"
3133            | "class"
3134            | "array_base" => return Ok(()),
3135            _ => {
3136                return Err(PerlError::runtime(
3137                    format!("unknown feature `{}`", name),
3138                    line,
3139                ));
3140            }
3141        };
3142        if enable {
3143            self.feature_bits |= bit;
3144        } else {
3145            self.feature_bits &= !bit;
3146        }
3147        Ok(())
3148    }
3149
3150    /// `require EXPR` — load once, record `%INC`, return `1` on success.
3151    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> PerlResult<PerlValue> {
3152        let t = spec.trim();
3153        if t.is_empty() {
3154            return Err(PerlError::runtime("require: empty argument", line));
3155        }
3156        match t {
3157            "strict" => {
3158                self.apply_use_strict(&[], line)?;
3159                return Ok(PerlValue::integer(1));
3160            }
3161            "utf8" => {
3162                self.utf8_pragma = true;
3163                return Ok(PerlValue::integer(1));
3164            }
3165            "feature" | "v5" => {
3166                return Ok(PerlValue::integer(1));
3167            }
3168            "warnings" => {
3169                self.warnings = true;
3170                return Ok(PerlValue::integer(1));
3171            }
3172            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
3173                return Ok(PerlValue::integer(1));
3174            }
3175            _ => {}
3176        }
3177        let p = Path::new(t);
3178        if p.is_absolute() {
3179            return self.require_absolute_path(p, line);
3180        }
3181        if t.starts_with("./") || t.starts_with("../") {
3182            return self.require_relative_path(p, line);
3183        }
3184        if Self::looks_like_version_only(t) {
3185            return Ok(PerlValue::integer(1));
3186        }
3187        let relpath = Self::module_spec_to_relpath(t);
3188        self.require_from_inc(&relpath, line)
3189    }
3190
3191    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
3192    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> PerlResult<()> {
3193        let v = self.scope.get_hash_element("^HOOK", key);
3194        if v.is_undef() {
3195            return Ok(());
3196        }
3197        let Some(sub) = v.as_code_ref() else {
3198            return Ok(());
3199        };
3200        let r = self.call_sub(
3201            sub.as_ref(),
3202            vec![PerlValue::string(path.to_string())],
3203            WantarrayCtx::Scalar,
3204            line,
3205        );
3206        match r {
3207            Ok(_) => Ok(()),
3208            Err(FlowOrError::Error(e)) => Err(e),
3209            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
3210            Err(FlowOrError::Flow(other)) => Err(PerlError::runtime(
3211                format!(
3212                    "require hook {:?} returned unexpected control flow: {:?}",
3213                    key, other
3214                ),
3215                line,
3216            )),
3217        }
3218    }
3219
3220    fn require_absolute_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3221        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
3222        let key = canon.to_string_lossy().into_owned();
3223        if self.scope.exists_hash_element("INC", &key) {
3224            return Ok(PerlValue::integer(1));
3225        }
3226        self.invoke_require_hook("require__before", &key, line)?;
3227        let code = read_file_text_perl_compat(&canon).map_err(|e| {
3228            PerlError::runtime(
3229                format!("Can't open {} for reading: {}", canon.display(), e),
3230                line,
3231            )
3232        })?;
3233        let code = crate::data_section::strip_perl_end_marker(&code);
3234        self.scope
3235            .set_hash_element("INC", &key, PerlValue::string(key.clone()))?;
3236        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3237        let r = crate::parse_and_run_string_in_file(code, self, &key);
3238        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3239        r?;
3240        self.invoke_require_hook("require__after", &key, line)?;
3241        Ok(PerlValue::integer(1))
3242    }
3243
3244    fn require_relative_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3245        if !path.exists() {
3246            return Err(PerlError::runtime(
3247                format!(
3248                    "Can't locate {} (relative path does not exist)",
3249                    path.display()
3250                ),
3251                line,
3252            ));
3253        }
3254        self.require_absolute_path(path, line)
3255    }
3256
3257    fn require_from_inc(&mut self, relpath: &str, line: usize) -> PerlResult<PerlValue> {
3258        if self.scope.exists_hash_element("INC", relpath) {
3259            return Ok(PerlValue::integer(1));
3260        }
3261        self.invoke_require_hook("require__before", relpath, line)?;
3262
3263        // Check virtual modules first (AOT bundles).
3264        if let Some(code) = self.virtual_modules.get(relpath).cloned() {
3265            let code = crate::data_section::strip_perl_end_marker(&code);
3266            self.scope.set_hash_element(
3267                "INC",
3268                relpath,
3269                PerlValue::string(format!("(virtual)/{}", relpath)),
3270            )?;
3271            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3272            let r = crate::parse_and_run_string_in_file(code, self, relpath);
3273            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3274            r?;
3275            self.invoke_require_hook("require__after", relpath, line)?;
3276            return Ok(PerlValue::integer(1));
3277        }
3278
3279        for dir in self.inc_directories() {
3280            let full = Path::new(&dir).join(relpath);
3281            if full.is_file() {
3282                let code = read_file_text_perl_compat(&full).map_err(|e| {
3283                    PerlError::runtime(
3284                        format!("Can't open {} for reading: {}", full.display(), e),
3285                        line,
3286                    )
3287                })?;
3288                let code = crate::data_section::strip_perl_end_marker(&code);
3289                let abs = full.canonicalize().unwrap_or(full);
3290                let abs_s = abs.to_string_lossy().into_owned();
3291                self.scope
3292                    .set_hash_element("INC", relpath, PerlValue::string(abs_s.clone()))?;
3293                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3294                let r = crate::parse_and_run_string_in_file(code, self, &abs_s);
3295                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3296                r?;
3297                self.invoke_require_hook("require__after", relpath, line)?;
3298                return Ok(PerlValue::integer(1));
3299            }
3300        }
3301        Err(PerlError::runtime(
3302            format!(
3303                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
3304                relpath
3305            ),
3306            line,
3307        ))
3308    }
3309
3310    /// Register a virtual module (for AOT bundles). Path should be relative like "lib/foo.stk".
3311    pub fn register_virtual_module(&mut self, path: String, source: String) {
3312        self.virtual_modules.insert(path, source);
3313    }
3314
3315    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
3316    pub(crate) fn exec_use_stmt(
3317        &mut self,
3318        module: &str,
3319        imports: &[Expr],
3320        line: usize,
3321    ) -> PerlResult<()> {
3322        match module {
3323            "strict" => self.apply_use_strict(imports, line),
3324            "utf8" => {
3325                if !imports.is_empty() {
3326                    return Err(PerlError::runtime("use utf8 takes no arguments", line));
3327                }
3328                self.utf8_pragma = true;
3329                Ok(())
3330            }
3331            "feature" => self.apply_use_feature(imports, line),
3332            "v5" => Ok(()),
3333            "warnings" => {
3334                self.warnings = true;
3335                Ok(())
3336            }
3337            "English" => {
3338                self.english_enabled = true;
3339                let args = Self::pragma_import_strings(imports, line)?;
3340                let no_match = args.iter().any(|a| a == "-no_match_vars");
3341                // Once match vars are exported (use English without -no_match_vars),
3342                // they stay available for the rest of the program — Perl exports them
3343                // into the caller's namespace and later pragmas cannot un-export them.
3344                if !no_match {
3345                    self.english_match_vars_ever_enabled = true;
3346                }
3347                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
3348                Ok(())
3349            }
3350            "Env" => self.apply_use_env(imports, line),
3351            "open" => self.apply_use_open(imports, line),
3352            "constant" => self.apply_use_constant(imports, line),
3353            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3354            _ => {
3355                self.require_execute(module, line)?;
3356                let imports = Self::imports_after_leading_use_version(imports);
3357                self.apply_module_import(module, imports, line)?;
3358                Ok(())
3359            }
3360        }
3361    }
3362
3363    /// `no strict 'refs'`, `no warnings`, `no feature`, …
3364    pub(crate) fn exec_no_stmt(
3365        &mut self,
3366        module: &str,
3367        imports: &[Expr],
3368        line: usize,
3369    ) -> PerlResult<()> {
3370        match module {
3371            "strict" => self.apply_no_strict(imports, line),
3372            "utf8" => {
3373                if !imports.is_empty() {
3374                    return Err(PerlError::runtime("no utf8 takes no arguments", line));
3375                }
3376                self.utf8_pragma = false;
3377                Ok(())
3378            }
3379            "feature" => self.apply_no_feature(imports, line),
3380            "v5" => Ok(()),
3381            "warnings" => {
3382                self.warnings = false;
3383                Ok(())
3384            }
3385            "English" => {
3386                self.english_enabled = false;
3387                // Don't reset no_match_vars here — if match vars were ever enabled,
3388                // they persist (Perl's export cannot be un-exported).
3389                if !self.english_match_vars_ever_enabled {
3390                    self.english_no_match_vars = false;
3391                }
3392                Ok(())
3393            }
3394            "open" => {
3395                self.open_pragma_utf8 = false;
3396                Ok(())
3397            }
3398            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3399            _ => Ok(()),
3400        }
3401    }
3402
3403    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
3404    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3405        let names = Self::pragma_import_strings(imports, line)?;
3406        for n in names {
3407            let key = n.trim_start_matches('@');
3408            if key.eq_ignore_ascii_case("PATH") {
3409                let path_env = std::env::var("PATH").unwrap_or_default();
3410                let path_vec: Vec<PerlValue> = std::env::split_paths(&path_env)
3411                    .map(|p| PerlValue::string(p.to_string_lossy().into_owned()))
3412                    .collect();
3413                let aname = self.stash_array_name_for_package("PATH");
3414                self.scope.declare_array(&aname, path_vec);
3415            }
3416        }
3417        Ok(())
3418    }
3419
3420    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
3421    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3422        let items = Self::pragma_import_strings(imports, line)?;
3423        for item in items {
3424            let s = item.trim();
3425            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
3426                self.open_pragma_utf8 = true;
3427                continue;
3428            }
3429            if let Some(rest) = s.strip_prefix(":encoding(") {
3430                if let Some(inner) = rest.strip_suffix(')') {
3431                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
3432                        self.open_pragma_utf8 = true;
3433                    }
3434                }
3435            }
3436        }
3437        Ok(())
3438    }
3439
3440    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
3441    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3442        if imports.is_empty() {
3443            return Ok(());
3444        }
3445        // `use constant 1.03;` — version check only (ignored here).
3446        if imports.len() == 1 {
3447            match &imports[0].kind {
3448                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
3449                _ => {}
3450            }
3451        }
3452        for imp in imports {
3453            match &imp.kind {
3454                ExprKind::List(items) => {
3455                    if items.len() % 2 != 0 {
3456                        return Err(PerlError::runtime(
3457                            format!(
3458                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
3459                                items.len()
3460                            ),
3461                            line,
3462                        ));
3463                    }
3464                    let mut i = 0;
3465                    while i < items.len() {
3466                        let name = match &items[i].kind {
3467                            ExprKind::String(s) => s.clone(),
3468                            _ => {
3469                                return Err(PerlError::runtime(
3470                                    "use constant: constant name must be a string literal",
3471                                    line,
3472                                ));
3473                            }
3474                        };
3475                        let val = match self.eval_expr(&items[i + 1]) {
3476                            Ok(v) => v,
3477                            Err(FlowOrError::Error(e)) => return Err(e),
3478                            Err(FlowOrError::Flow(_)) => {
3479                                return Err(PerlError::runtime(
3480                                    "use constant: unexpected control flow in initializer",
3481                                    line,
3482                                ));
3483                            }
3484                        };
3485                        self.install_constant_sub(&name, &val, line)?;
3486                        i += 2;
3487                    }
3488                }
3489                _ => {
3490                    return Err(PerlError::runtime(
3491                        "use constant: expected list of NAME => VALUE pairs",
3492                        line,
3493                    ));
3494                }
3495            }
3496        }
3497        Ok(())
3498    }
3499
3500    fn install_constant_sub(&mut self, name: &str, val: &PerlValue, line: usize) -> PerlResult<()> {
3501        let key = self.qualify_sub_key(name);
3502        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
3503        let body = vec![Statement {
3504            label: None,
3505            kind: StmtKind::Return(Some(ret_expr)),
3506            line,
3507        }];
3508        self.subs.insert(
3509            key.clone(),
3510            Arc::new(PerlSub {
3511                name: key,
3512                params: vec![],
3513                body,
3514                prototype: None,
3515                closure_env: None,
3516                fib_like: None,
3517            }),
3518        );
3519        Ok(())
3520    }
3521
3522    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
3523    fn perl_value_to_const_literal_expr(&self, v: &PerlValue, line: usize) -> PerlResult<Expr> {
3524        if v.is_undef() {
3525            return Ok(Expr {
3526                kind: ExprKind::Undef,
3527                line,
3528            });
3529        }
3530        if let Some(n) = v.as_integer() {
3531            return Ok(Expr {
3532                kind: ExprKind::Integer(n),
3533                line,
3534            });
3535        }
3536        if let Some(f) = v.as_float() {
3537            return Ok(Expr {
3538                kind: ExprKind::Float(f),
3539                line,
3540            });
3541        }
3542        if let Some(s) = v.as_str() {
3543            return Ok(Expr {
3544                kind: ExprKind::String(s),
3545                line,
3546            });
3547        }
3548        if let Some(arr) = v.as_array_vec() {
3549            let mut elems = Vec::with_capacity(arr.len());
3550            for e in &arr {
3551                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3552            }
3553            return Ok(Expr {
3554                kind: ExprKind::ArrayRef(elems),
3555                line,
3556            });
3557        }
3558        if let Some(h) = v.as_hash_map() {
3559            let mut pairs = Vec::with_capacity(h.len());
3560            for (k, vv) in h.iter() {
3561                pairs.push((
3562                    Expr {
3563                        kind: ExprKind::String(k.clone()),
3564                        line,
3565                    },
3566                    self.perl_value_to_const_literal_expr(vv, line)?,
3567                ));
3568            }
3569            return Ok(Expr {
3570                kind: ExprKind::HashRef(pairs),
3571                line,
3572            });
3573        }
3574        if let Some(aref) = v.as_array_ref() {
3575            let arr = aref.read();
3576            let mut elems = Vec::with_capacity(arr.len());
3577            for e in arr.iter() {
3578                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3579            }
3580            return Ok(Expr {
3581                kind: ExprKind::ArrayRef(elems),
3582                line,
3583            });
3584        }
3585        if let Some(href) = v.as_hash_ref() {
3586            let h = href.read();
3587            let mut pairs = Vec::with_capacity(h.len());
3588            for (k, vv) in h.iter() {
3589                pairs.push((
3590                    Expr {
3591                        kind: ExprKind::String(k.clone()),
3592                        line,
3593                    },
3594                    self.perl_value_to_const_literal_expr(vv, line)?,
3595                ));
3596            }
3597            return Ok(Expr {
3598                kind: ExprKind::HashRef(pairs),
3599                line,
3600            });
3601        }
3602        Err(PerlError::runtime(
3603            format!("use constant: unsupported value type ({v:?})"),
3604            line,
3605        ))
3606    }
3607
3608    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
3609    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> PerlResult<()> {
3610        if crate::list_util::program_needs_list_util(program) {
3611            crate::list_util::ensure_list_util(self);
3612        }
3613        for stmt in &program.statements {
3614            match &stmt.kind {
3615                StmtKind::Package { name } => {
3616                    let _ = self
3617                        .scope
3618                        .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
3619                }
3620                StmtKind::SubDecl {
3621                    name,
3622                    params,
3623                    body,
3624                    prototype,
3625                } => {
3626                    let key = self.qualify_sub_key(name);
3627                    let mut sub = PerlSub {
3628                        name: name.clone(),
3629                        params: params.clone(),
3630                        body: body.clone(),
3631                        closure_env: None,
3632                        prototype: prototype.clone(),
3633                        fib_like: None,
3634                    };
3635                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
3636                    self.subs.insert(key, Arc::new(sub));
3637                }
3638                StmtKind::UsePerlVersion { .. } => {}
3639                StmtKind::Use { module, imports } => {
3640                    self.exec_use_stmt(module, imports, stmt.line)?;
3641                }
3642                StmtKind::UseOverload { pairs } => {
3643                    self.install_use_overload_pairs(pairs);
3644                }
3645                StmtKind::FormatDecl { name, lines } => {
3646                    self.install_format_decl(name, lines, stmt.line)?;
3647                }
3648                StmtKind::No { module, imports } => {
3649                    self.exec_no_stmt(module, imports, stmt.line)?;
3650                }
3651                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
3652                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
3653                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
3654                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
3655                StmtKind::End(block) => self.end_blocks.push(block.clone()),
3656                _ => {}
3657            }
3658        }
3659        Ok(())
3660    }
3661
3662    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
3663    pub fn install_data_handle(&mut self, data: Vec<u8>) {
3664        self.input_handles.insert(
3665            "DATA".to_string(),
3666            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
3667        );
3668    }
3669
3670    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
3671    ///
3672    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
3673    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
3674    /// [`piped_shell_command`]).
3675    pub(crate) fn open_builtin_execute(
3676        &mut self,
3677        handle_name: String,
3678        mode_s: String,
3679        file_opt: Option<String>,
3680        line: usize,
3681    ) -> PerlResult<PerlValue> {
3682        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
3683        // - leading `|`  → pipe to command (write to child's stdin)
3684        // - trailing `|` → pipe from command (read child's stdout)
3685        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
3686        let (actual_mode, path) = if let Some(f) = file_opt {
3687            (mode_s, f)
3688        } else {
3689            let trimmed = mode_s.trim();
3690            if let Some(rest) = trimmed.strip_prefix('|') {
3691                ("|-".to_string(), rest.trim_start().to_string())
3692            } else if trimmed.ends_with('|') {
3693                let mut cmd = trimmed.to_string();
3694                cmd.pop(); // trailing `|` that selects pipe-from-command
3695                ("-|".to_string(), cmd.trim_end().to_string())
3696            } else if let Some(rest) = trimmed.strip_prefix(">>") {
3697                (">>".to_string(), rest.trim().to_string())
3698            } else if let Some(rest) = trimmed.strip_prefix('>') {
3699                (">".to_string(), rest.trim().to_string())
3700            } else if let Some(rest) = trimmed.strip_prefix('<') {
3701                ("<".to_string(), rest.trim().to_string())
3702            } else {
3703                ("<".to_string(), trimmed.to_string())
3704            }
3705        };
3706        let handle_return = handle_name.clone();
3707        match actual_mode.as_str() {
3708            "-|" => {
3709                let mut cmd = piped_shell_command(&path);
3710                cmd.stdout(Stdio::piped());
3711                let mut child = cmd.spawn().map_err(|e| {
3712                    self.apply_io_error_to_errno(&e);
3713                    PerlError::runtime(format!("Can't open pipe from command: {}", e), line)
3714                })?;
3715                let stdout = child
3716                    .stdout
3717                    .take()
3718                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdout", line))?;
3719                self.input_handles
3720                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
3721                self.pipe_children.insert(handle_name, child);
3722            }
3723            "|-" => {
3724                let mut cmd = piped_shell_command(&path);
3725                cmd.stdin(Stdio::piped());
3726                let mut child = cmd.spawn().map_err(|e| {
3727                    self.apply_io_error_to_errno(&e);
3728                    PerlError::runtime(format!("Can't open pipe to command: {}", e), line)
3729                })?;
3730                let stdin = child
3731                    .stdin
3732                    .take()
3733                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdin", line))?;
3734                self.output_handles
3735                    .insert(handle_name.clone(), Box::new(stdin));
3736                self.pipe_children.insert(handle_name, child);
3737            }
3738            "<" => {
3739                let file = std::fs::File::open(&path).map_err(|e| {
3740                    self.apply_io_error_to_errno(&e);
3741                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3742                })?;
3743                let shared = Arc::new(Mutex::new(file));
3744                self.io_file_slots
3745                    .insert(handle_name.clone(), Arc::clone(&shared));
3746                self.input_handles.insert(
3747                    handle_name.clone(),
3748                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
3749                );
3750            }
3751            ">" => {
3752                let file = std::fs::File::create(&path).map_err(|e| {
3753                    self.apply_io_error_to_errno(&e);
3754                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3755                })?;
3756                let shared = Arc::new(Mutex::new(file));
3757                self.io_file_slots
3758                    .insert(handle_name.clone(), Arc::clone(&shared));
3759                self.output_handles.insert(
3760                    handle_name.clone(),
3761                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3762                );
3763            }
3764            ">>" => {
3765                let file = std::fs::OpenOptions::new()
3766                    .append(true)
3767                    .create(true)
3768                    .open(&path)
3769                    .map_err(|e| {
3770                        self.apply_io_error_to_errno(&e);
3771                        PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3772                    })?;
3773                let shared = Arc::new(Mutex::new(file));
3774                self.io_file_slots
3775                    .insert(handle_name.clone(), Arc::clone(&shared));
3776                self.output_handles.insert(
3777                    handle_name.clone(),
3778                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3779                );
3780            }
3781            _ => {
3782                return Err(PerlError::runtime(
3783                    format!("Unknown open mode '{}'", actual_mode),
3784                    line,
3785                ));
3786            }
3787        }
3788        Ok(PerlValue::io_handle(handle_return))
3789    }
3790
3791    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
3792    /// matches the previous key under [`PerlValue::str_eq`]. Returns a list of arrayrefs
3793    /// (same outer shape as `chunked`).
3794    pub(crate) fn eval_chunk_by_builtin(
3795        &mut self,
3796        key_spec: &Expr,
3797        list_expr: &Expr,
3798        ctx: WantarrayCtx,
3799        line: usize,
3800    ) -> ExecResult {
3801        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
3802        let chunks = match &key_spec.kind {
3803            ExprKind::CodeRef { .. } => {
3804                let cr = self.eval_expr(key_spec)?;
3805                let Some(sub) = cr.as_code_ref() else {
3806                    return Err(PerlError::runtime(
3807                        "group_by/chunk_by: first argument must be { BLOCK }",
3808                        line,
3809                    )
3810                    .into());
3811                };
3812                let sub = sub.clone();
3813                let mut chunks: Vec<PerlValue> = Vec::new();
3814                let mut run: Vec<PerlValue> = Vec::new();
3815                let mut prev_key: Option<PerlValue> = None;
3816                for item in list {
3817                    self.scope.set_topic(item.clone());
3818                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
3819                        Ok(k) => k,
3820                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
3821                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
3822                        Err(_) => PerlValue::UNDEF,
3823                    };
3824                    match &prev_key {
3825                        None => {
3826                            run.push(item);
3827                            prev_key = Some(key);
3828                        }
3829                        Some(pk) => {
3830                            if key.str_eq(pk) {
3831                                run.push(item);
3832                            } else {
3833                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3834                                    std::mem::take(&mut run),
3835                                ))));
3836                                run.push(item);
3837                                prev_key = Some(key);
3838                            }
3839                        }
3840                    }
3841                }
3842                if !run.is_empty() {
3843                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3844                }
3845                chunks
3846            }
3847            _ => {
3848                let mut chunks: Vec<PerlValue> = Vec::new();
3849                let mut run: Vec<PerlValue> = Vec::new();
3850                let mut prev_key: Option<PerlValue> = None;
3851                for item in list {
3852                    self.scope.set_topic(item.clone());
3853                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
3854                    match &prev_key {
3855                        None => {
3856                            run.push(item);
3857                            prev_key = Some(key);
3858                        }
3859                        Some(pk) => {
3860                            if key.str_eq(pk) {
3861                                run.push(item);
3862                            } else {
3863                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3864                                    std::mem::take(&mut run),
3865                                ))));
3866                                run.push(item);
3867                                prev_key = Some(key);
3868                            }
3869                        }
3870                    }
3871                }
3872                if !run.is_empty() {
3873                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3874                }
3875                chunks
3876            }
3877        };
3878        Ok(match ctx {
3879            WantarrayCtx::List => PerlValue::array(chunks),
3880            WantarrayCtx::Scalar => PerlValue::integer(chunks.len() as i64),
3881            WantarrayCtx::Void => PerlValue::UNDEF,
3882        })
3883    }
3884
3885    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
3886    pub(crate) fn list_higher_order_block_builtin(
3887        &mut self,
3888        name: &str,
3889        args: &[PerlValue],
3890        line: usize,
3891    ) -> PerlResult<PerlValue> {
3892        match self.list_higher_order_block_builtin_exec(name, args, line) {
3893            Ok(v) => Ok(v),
3894            Err(FlowOrError::Error(e)) => Err(e),
3895            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
3896            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
3897                format!("{name}: unsupported control flow in block"),
3898                line,
3899            )),
3900        }
3901    }
3902
3903    fn list_higher_order_block_builtin_exec(
3904        &mut self,
3905        name: &str,
3906        args: &[PerlValue],
3907        line: usize,
3908    ) -> ExecResult {
3909        if args.is_empty() {
3910            return Err(
3911                PerlError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
3912            );
3913        }
3914        let Some(sub) = args[0].as_code_ref() else {
3915            return Err(PerlError::runtime(
3916                format!("{name}: first argument must be {{ BLOCK }}"),
3917                line,
3918            )
3919            .into());
3920        };
3921        let sub = sub.clone();
3922        let items: Vec<PerlValue> = args[1..].to_vec();
3923        if matches!(name, "tap" | "peek") && items.len() == 1 {
3924            if let Some(p) = items[0].as_pipeline() {
3925                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
3926                return Ok(PerlValue::pipeline(Arc::clone(&p)));
3927            }
3928            let v = &items[0];
3929            if v.is_iterator() || v.as_array_vec().is_some() {
3930                let source = crate::map_stream::into_pull_iter(v.clone());
3931                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
3932                return Ok(PerlValue::iterator(Arc::new(
3933                    crate::map_stream::TapIterator::new(
3934                        source,
3935                        sub,
3936                        self.subs.clone(),
3937                        capture,
3938                        atomic_arrays,
3939                        atomic_hashes,
3940                    ),
3941                )));
3942            }
3943        }
3944        // Streaming optimization disabled for these functions because the pre-captured
3945        // coderef from args[0] has its closure_env populated at parse time, which causes
3946        // $_ to get stale values on subsequent calls. These functions work correctly in
3947        // the non-streaming eager path below.
3948        let wa = self.wantarray_kind;
3949        match name {
3950            "take_while" => {
3951                let mut out = Vec::new();
3952                for item in items {
3953                    self.scope_push_hook();
3954                    self.scope.set_topic(item.clone());
3955                    let pred = self.exec_block(&sub.body)?;
3956                    self.scope_pop_hook();
3957                    if !pred.is_true() {
3958                        break;
3959                    }
3960                    out.push(item);
3961                }
3962                Ok(match wa {
3963                    WantarrayCtx::List => PerlValue::array(out),
3964                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
3965                    WantarrayCtx::Void => PerlValue::UNDEF,
3966                })
3967            }
3968            "drop_while" | "skip_while" => {
3969                let mut i = 0usize;
3970                while i < items.len() {
3971                    self.scope_push_hook();
3972                    self.scope.set_topic(items[i].clone());
3973                    let pred = self.exec_block(&sub.body)?;
3974                    self.scope_pop_hook();
3975                    if !pred.is_true() {
3976                        break;
3977                    }
3978                    i += 1;
3979                }
3980                let rest = items[i..].to_vec();
3981                Ok(match wa {
3982                    WantarrayCtx::List => PerlValue::array(rest),
3983                    WantarrayCtx::Scalar => PerlValue::integer(rest.len() as i64),
3984                    WantarrayCtx::Void => PerlValue::UNDEF,
3985                })
3986            }
3987            "reject" => {
3988                let mut out = Vec::new();
3989                for item in items {
3990                    self.scope_push_hook();
3991                    self.scope.set_topic(item.clone());
3992                    let pred = self.exec_block(&sub.body)?;
3993                    self.scope_pop_hook();
3994                    if !pred.is_true() {
3995                        out.push(item);
3996                    }
3997                }
3998                Ok(match wa {
3999                    WantarrayCtx::List => PerlValue::array(out),
4000                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4001                    WantarrayCtx::Void => PerlValue::UNDEF,
4002                })
4003            }
4004            "tap" | "peek" => {
4005                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
4006                Ok(match wa {
4007                    WantarrayCtx::List => PerlValue::array(items),
4008                    WantarrayCtx::Scalar => PerlValue::integer(items.len() as i64),
4009                    WantarrayCtx::Void => PerlValue::UNDEF,
4010                })
4011            }
4012            "partition" => {
4013                let mut yes = Vec::new();
4014                let mut no = Vec::new();
4015                for item in items {
4016                    self.scope.set_topic(item.clone());
4017                    let pred = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4018                    if pred.is_true() {
4019                        yes.push(item);
4020                    } else {
4021                        no.push(item);
4022                    }
4023                }
4024                let yes_ref = PerlValue::array_ref(Arc::new(RwLock::new(yes)));
4025                let no_ref = PerlValue::array_ref(Arc::new(RwLock::new(no)));
4026                Ok(match wa {
4027                    WantarrayCtx::List => PerlValue::array(vec![yes_ref, no_ref]),
4028                    WantarrayCtx::Scalar => PerlValue::integer(2),
4029                    WantarrayCtx::Void => PerlValue::UNDEF,
4030                })
4031            }
4032            "min_by" => {
4033                let mut best: Option<(PerlValue, PerlValue)> = None;
4034                for item in items {
4035                    self.scope.set_topic(item.clone());
4036                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4037                    best = Some(match best {
4038                        None => (item, key),
4039                        Some((bv, bk)) => {
4040                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
4041                                (item, key)
4042                            } else {
4043                                (bv, bk)
4044                            }
4045                        }
4046                    });
4047                }
4048                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
4049            }
4050            "max_by" => {
4051                let mut best: Option<(PerlValue, PerlValue)> = None;
4052                for item in items {
4053                    self.scope.set_topic(item.clone());
4054                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4055                    best = Some(match best {
4056                        None => (item, key),
4057                        Some((bv, bk)) => {
4058                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
4059                                (item, key)
4060                            } else {
4061                                (bv, bk)
4062                            }
4063                        }
4064                    });
4065                }
4066                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
4067            }
4068            "zip_with" => {
4069                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
4070                // Flatten items, then treat each array ref/binding as a separate list.
4071                let flat: Vec<PerlValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
4072                let refs: Vec<Vec<PerlValue>> = flat
4073                    .iter()
4074                    .map(|el| {
4075                        if let Some(ar) = el.as_array_ref() {
4076                            ar.read().clone()
4077                        } else if let Some(name) = el.as_array_binding_name() {
4078                            self.scope.get_array(&name)
4079                        } else {
4080                            vec![el.clone()]
4081                        }
4082                    })
4083                    .collect();
4084                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
4085                let mut out = Vec::with_capacity(max_len);
4086                for i in 0..max_len {
4087                    let pair: Vec<PerlValue> = refs
4088                        .iter()
4089                        .map(|l| l.get(i).cloned().unwrap_or(PerlValue::UNDEF))
4090                        .collect();
4091                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
4092                    out.push(result);
4093                }
4094                Ok(match wa {
4095                    WantarrayCtx::List => PerlValue::array(out),
4096                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
4097                    WantarrayCtx::Void => PerlValue::UNDEF,
4098                })
4099            }
4100            "count_by" => {
4101                let mut counts = indexmap::IndexMap::new();
4102                for item in items {
4103                    self.scope.set_topic(item.clone());
4104                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
4105                    let k = key.to_string();
4106                    let entry = counts.entry(k).or_insert(PerlValue::integer(0));
4107                    *entry = PerlValue::integer(entry.to_int() + 1);
4108                }
4109                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(counts))))
4110            }
4111            _ => Err(PerlError::runtime(
4112                format!("internal: unknown list block builtin `{name}`"),
4113                line,
4114            )
4115            .into()),
4116        }
4117    }
4118
4119    /// `rmdir LIST` — remove empty directories; returns count removed.
4120    pub(crate) fn builtin_rmdir_execute(
4121        &mut self,
4122        args: &[PerlValue],
4123        _line: usize,
4124    ) -> PerlResult<PerlValue> {
4125        let mut count = 0i64;
4126        for a in args {
4127            let p = a.to_string();
4128            if p.is_empty() {
4129                continue;
4130            }
4131            if std::fs::remove_dir(&p).is_ok() {
4132                count += 1;
4133            }
4134        }
4135        Ok(PerlValue::integer(count))
4136    }
4137
4138    /// `touch FILE, ...` — create if absent, update timestamps to now.
4139    pub(crate) fn builtin_touch_execute(
4140        &mut self,
4141        args: &[PerlValue],
4142        _line: usize,
4143    ) -> PerlResult<PerlValue> {
4144        let paths: Vec<String> = args.iter().map(|v| v.to_string()).collect();
4145        Ok(PerlValue::integer(crate::perl_fs::touch_paths(&paths)))
4146    }
4147
4148    /// `utime ATIME, MTIME, LIST`
4149    pub(crate) fn builtin_utime_execute(
4150        &mut self,
4151        args: &[PerlValue],
4152        line: usize,
4153    ) -> PerlResult<PerlValue> {
4154        if args.len() < 3 {
4155            return Err(PerlError::runtime(
4156                "utime requires at least three arguments (atime, mtime, files...)",
4157                line,
4158            ));
4159        }
4160        let at = args[0].to_int();
4161        let mt = args[1].to_int();
4162        let paths: Vec<String> = args.iter().skip(2).map(|v| v.to_string()).collect();
4163        let n = crate::perl_fs::utime_paths(at, mt, &paths);
4164        #[cfg(not(unix))]
4165        if !paths.is_empty() && n == 0 {
4166            return Err(PerlError::runtime(
4167                "utime is not supported on this platform",
4168                line,
4169            ));
4170        }
4171        Ok(PerlValue::integer(n))
4172    }
4173
4174    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
4175    pub(crate) fn builtin_umask_execute(
4176        &mut self,
4177        args: &[PerlValue],
4178        line: usize,
4179    ) -> PerlResult<PerlValue> {
4180        #[cfg(unix)]
4181        {
4182            let _ = line;
4183            if args.is_empty() {
4184                let cur = unsafe { libc::umask(0) };
4185                unsafe { libc::umask(cur) };
4186                return Ok(PerlValue::integer(cur as i64));
4187            }
4188            let new_m = args[0].to_int() as libc::mode_t;
4189            let old = unsafe { libc::umask(new_m) };
4190            Ok(PerlValue::integer(old as i64))
4191        }
4192        #[cfg(not(unix))]
4193        {
4194            let _ = args;
4195            Err(PerlError::runtime(
4196                "umask is not supported on this platform",
4197                line,
4198            ))
4199        }
4200    }
4201
4202    /// `getcwd` — current directory or undef on failure.
4203    pub(crate) fn builtin_getcwd_execute(
4204        &mut self,
4205        args: &[PerlValue],
4206        line: usize,
4207    ) -> PerlResult<PerlValue> {
4208        if !args.is_empty() {
4209            return Err(PerlError::runtime("getcwd takes no arguments", line));
4210        }
4211        match std::env::current_dir() {
4212            Ok(p) => Ok(PerlValue::string(p.to_string_lossy().into_owned())),
4213            Err(e) => {
4214                self.apply_io_error_to_errno(&e);
4215                Ok(PerlValue::UNDEF)
4216            }
4217        }
4218    }
4219
4220    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
4221    pub(crate) fn builtin_realpath_execute(
4222        &mut self,
4223        args: &[PerlValue],
4224        line: usize,
4225    ) -> PerlResult<PerlValue> {
4226        let path = args
4227            .first()
4228            .ok_or_else(|| PerlError::runtime("realpath: need path", line))?
4229            .to_string();
4230        if path.is_empty() {
4231            return Err(PerlError::runtime("realpath: need path", line));
4232        }
4233        match crate::perl_fs::realpath_resolved(&path) {
4234            Ok(s) => Ok(PerlValue::string(s)),
4235            Err(e) => {
4236                self.apply_io_error_to_errno(&e);
4237                Ok(PerlValue::UNDEF)
4238            }
4239        }
4240    }
4241
4242    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
4243    pub(crate) fn builtin_pipe_execute(
4244        &mut self,
4245        args: &[PerlValue],
4246        line: usize,
4247    ) -> PerlResult<PerlValue> {
4248        if args.len() != 2 {
4249            return Err(PerlError::runtime(
4250                "pipe requires exactly two arguments",
4251                line,
4252            ));
4253        }
4254        #[cfg(unix)]
4255        {
4256            use std::fs::File;
4257            use std::os::unix::io::FromRawFd;
4258
4259            let read_name = args[0].to_string();
4260            let write_name = args[1].to_string();
4261            if read_name.is_empty() || write_name.is_empty() {
4262                return Err(PerlError::runtime("pipe: invalid handle name", line));
4263            }
4264            let mut fds = [0i32; 2];
4265            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
4266                let e = std::io::Error::last_os_error();
4267                self.apply_io_error_to_errno(&e);
4268                return Ok(PerlValue::integer(0));
4269            }
4270            let read_file = unsafe { File::from_raw_fd(fds[0]) };
4271            let write_file = unsafe { File::from_raw_fd(fds[1]) };
4272
4273            let read_shared = Arc::new(Mutex::new(read_file));
4274            let write_shared = Arc::new(Mutex::new(write_file));
4275
4276            self.close_builtin_execute(read_name.clone()).ok();
4277            self.close_builtin_execute(write_name.clone()).ok();
4278
4279            self.io_file_slots
4280                .insert(read_name.clone(), Arc::clone(&read_shared));
4281            self.input_handles.insert(
4282                read_name,
4283                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
4284            );
4285
4286            self.io_file_slots
4287                .insert(write_name.clone(), Arc::clone(&write_shared));
4288            self.output_handles
4289                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
4290
4291            Ok(PerlValue::integer(1))
4292        }
4293        #[cfg(not(unix))]
4294        {
4295            let _ = args;
4296            Err(PerlError::runtime(
4297                "pipe is not supported on this platform",
4298                line,
4299            ))
4300        }
4301    }
4302
4303    pub(crate) fn close_builtin_execute(&mut self, name: String) -> PerlResult<PerlValue> {
4304        self.output_handles.remove(&name);
4305        self.input_handles.remove(&name);
4306        self.io_file_slots.remove(&name);
4307        if let Some(mut child) = self.pipe_children.remove(&name) {
4308            if let Ok(st) = child.wait() {
4309                self.record_child_exit_status(st);
4310            }
4311        }
4312        Ok(PerlValue::integer(1))
4313    }
4314
4315    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
4316        self.input_handles.contains_key(name)
4317    }
4318
4319    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
4320    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
4321    /// readline-level EOF tracking exists.
4322    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
4323        self.line_mode_eof_pending
4324    }
4325
4326    /// `eof` / `eof()` / `eof FH` — shared by the tree walker, [`crate::vm::VM`], and
4327    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
4328    /// not [`ExprKind::Eof`]).
4329    pub(crate) fn eof_builtin_execute(
4330        &self,
4331        args: &[PerlValue],
4332        line: usize,
4333    ) -> PerlResult<PerlValue> {
4334        match args.len() {
4335            0 => Ok(PerlValue::integer(if self.eof_without_arg_is_true() {
4336                1
4337            } else {
4338                0
4339            })),
4340            1 => {
4341                let name = args[0].to_string();
4342                let at_eof = !self.has_input_handle(&name);
4343                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
4344            }
4345            _ => Err(PerlError::runtime("eof: too many arguments", line)),
4346        }
4347    }
4348
4349    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
4350    /// `0`, stringifies to `""`) for `""`.
4351    pub(crate) fn study_return_value(s: &str) -> PerlValue {
4352        if s.is_empty() {
4353            PerlValue::string(String::new())
4354        } else {
4355            PerlValue::integer(1)
4356        }
4357    }
4358
4359    pub(crate) fn readline_builtin_execute(
4360        &mut self,
4361        handle: Option<&str>,
4362    ) -> PerlResult<PerlValue> {
4363        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
4364        if handle.is_none() {
4365            let argv = self.scope.get_array("ARGV");
4366            if !argv.is_empty() {
4367                loop {
4368                    if self.diamond_reader.is_none() {
4369                        while self.diamond_next_idx < argv.len() {
4370                            let path = argv[self.diamond_next_idx].to_string();
4371                            self.diamond_next_idx += 1;
4372                            match File::open(&path) {
4373                                Ok(f) => {
4374                                    self.argv_current_file = path;
4375                                    self.diamond_reader = Some(BufReader::new(f));
4376                                    break;
4377                                }
4378                                Err(e) => {
4379                                    self.apply_io_error_to_errno(&e);
4380                                }
4381                            }
4382                        }
4383                        if self.diamond_reader.is_none() {
4384                            return Ok(PerlValue::UNDEF);
4385                        }
4386                    }
4387                    let mut line_str = String::new();
4388                    let read_result: Result<usize, io::Error> =
4389                        if let Some(reader) = self.diamond_reader.as_mut() {
4390                            if self.open_pragma_utf8 {
4391                                let mut buf = Vec::new();
4392                                reader.read_until(b'\n', &mut buf).inspect(|n| {
4393                                    if *n > 0 {
4394                                        line_str = String::from_utf8_lossy(&buf).into_owned();
4395                                    }
4396                                })
4397                            } else {
4398                                let mut buf = Vec::new();
4399                                match reader.read_until(b'\n', &mut buf) {
4400                                    Ok(n) => {
4401                                        if n > 0 {
4402                                            line_str =
4403                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
4404                                                &buf,
4405                                            );
4406                                        }
4407                                        Ok(n)
4408                                    }
4409                                    Err(e) => Err(e),
4410                                }
4411                            }
4412                        } else {
4413                            unreachable!()
4414                        };
4415                    match read_result {
4416                        Ok(0) => {
4417                            self.diamond_reader = None;
4418                            continue;
4419                        }
4420                        Ok(_) => {
4421                            self.bump_line_for_handle(&self.argv_current_file.clone());
4422                            return Ok(PerlValue::string(line_str));
4423                        }
4424                        Err(e) => {
4425                            self.apply_io_error_to_errno(&e);
4426                            self.diamond_reader = None;
4427                            continue;
4428                        }
4429                    }
4430                }
4431            } else {
4432                self.argv_current_file.clear();
4433            }
4434        }
4435
4436        let handle_name = handle.unwrap_or("STDIN");
4437        let mut line_str = String::new();
4438        if handle_name == "STDIN" {
4439            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
4440                self.last_stdin_die_bracket = if handle.is_none() {
4441                    "<>".to_string()
4442                } else {
4443                    "<STDIN>".to_string()
4444                };
4445                self.bump_line_for_handle("STDIN");
4446                return Ok(PerlValue::string(queued));
4447            }
4448            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4449                let mut buf = Vec::new();
4450                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
4451                    if *n > 0 {
4452                        line_str = String::from_utf8_lossy(&buf).into_owned();
4453                    }
4454                })
4455            } else {
4456                let mut buf = Vec::new();
4457                let mut lock = io::stdin().lock();
4458                match lock.read_until(b'\n', &mut buf) {
4459                    Ok(n) => {
4460                        if n > 0 {
4461                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4462                        }
4463                        Ok(n)
4464                    }
4465                    Err(e) => Err(e),
4466                }
4467            };
4468            match r {
4469                Ok(0) => Ok(PerlValue::UNDEF),
4470                Ok(_) => {
4471                    self.last_stdin_die_bracket = if handle.is_none() {
4472                        "<>".to_string()
4473                    } else {
4474                        "<STDIN>".to_string()
4475                    };
4476                    self.bump_line_for_handle("STDIN");
4477                    Ok(PerlValue::string(line_str))
4478                }
4479                Err(e) => {
4480                    self.apply_io_error_to_errno(&e);
4481                    Ok(PerlValue::UNDEF)
4482                }
4483            }
4484        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
4485            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4486                let mut buf = Vec::new();
4487                reader.read_until(b'\n', &mut buf).inspect(|n| {
4488                    if *n > 0 {
4489                        line_str = String::from_utf8_lossy(&buf).into_owned();
4490                    }
4491                })
4492            } else {
4493                let mut buf = Vec::new();
4494                match reader.read_until(b'\n', &mut buf) {
4495                    Ok(n) => {
4496                        if n > 0 {
4497                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4498                        }
4499                        Ok(n)
4500                    }
4501                    Err(e) => Err(e),
4502                }
4503            };
4504            match r {
4505                Ok(0) => Ok(PerlValue::UNDEF),
4506                Ok(_) => {
4507                    self.bump_line_for_handle(handle_name);
4508                    Ok(PerlValue::string(line_str))
4509                }
4510                Err(e) => {
4511                    self.apply_io_error_to_errno(&e);
4512                    Ok(PerlValue::UNDEF)
4513                }
4514            }
4515        } else {
4516            Ok(PerlValue::UNDEF)
4517        }
4518    }
4519
4520    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
4521    pub(crate) fn readline_builtin_execute_list(
4522        &mut self,
4523        handle: Option<&str>,
4524    ) -> PerlResult<PerlValue> {
4525        let mut lines = Vec::new();
4526        loop {
4527            let v = self.readline_builtin_execute(handle)?;
4528            if v.is_undef() {
4529                break;
4530            }
4531            lines.push(v);
4532        }
4533        Ok(PerlValue::array(lines))
4534    }
4535
4536    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> PerlValue {
4537        match std::fs::read_dir(path) {
4538            Ok(rd) => {
4539                let entries: Vec<String> = rd
4540                    .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
4541                    .collect();
4542                self.dir_handles
4543                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
4544                PerlValue::integer(1)
4545            }
4546            Err(e) => {
4547                self.apply_io_error_to_errno(&e);
4548                PerlValue::integer(0)
4549            }
4550        }
4551    }
4552
4553    pub(crate) fn readdir_handle(&mut self, handle: &str) -> PerlValue {
4554        if let Some(dh) = self.dir_handles.get_mut(handle) {
4555            if dh.pos < dh.entries.len() {
4556                let s = dh.entries[dh.pos].clone();
4557                dh.pos += 1;
4558                PerlValue::string(s)
4559            } else {
4560                PerlValue::UNDEF
4561            }
4562        } else {
4563            PerlValue::UNDEF
4564        }
4565    }
4566
4567    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
4568    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> PerlValue {
4569        if let Some(dh) = self.dir_handles.get_mut(handle) {
4570            let rest: Vec<PerlValue> = dh.entries[dh.pos..]
4571                .iter()
4572                .cloned()
4573                .map(PerlValue::string)
4574                .collect();
4575            dh.pos = dh.entries.len();
4576            PerlValue::array(rest)
4577        } else {
4578            PerlValue::array(Vec::new())
4579        }
4580    }
4581
4582    pub(crate) fn closedir_handle(&mut self, handle: &str) -> PerlValue {
4583        PerlValue::integer(if self.dir_handles.remove(handle).is_some() {
4584            1
4585        } else {
4586            0
4587        })
4588    }
4589
4590    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> PerlValue {
4591        if let Some(dh) = self.dir_handles.get_mut(handle) {
4592            dh.pos = 0;
4593            PerlValue::integer(1)
4594        } else {
4595            PerlValue::integer(0)
4596        }
4597    }
4598
4599    pub(crate) fn telldir_handle(&mut self, handle: &str) -> PerlValue {
4600        self.dir_handles
4601            .get(handle)
4602            .map(|dh| PerlValue::integer(dh.pos as i64))
4603            .unwrap_or(PerlValue::UNDEF)
4604    }
4605
4606    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> PerlValue {
4607        if let Some(dh) = self.dir_handles.get_mut(handle) {
4608            dh.pos = pos.min(dh.entries.len());
4609            PerlValue::integer(1)
4610        } else {
4611            PerlValue::integer(0)
4612        }
4613    }
4614
4615    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
4616    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
4617    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
4618    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
4619    #[inline]
4620    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
4621        crate::special_vars::is_regex_match_scalar_name(name)
4622    }
4623
4624    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
4625    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
4626    /// `apply_regex_captures` instead of short-circuiting.
4627    #[inline]
4628    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
4629        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
4630            self.regex_capture_scope_fresh = false;
4631        }
4632    }
4633
4634    pub(crate) fn apply_regex_captures(
4635        &mut self,
4636        haystack: &str,
4637        offset: usize,
4638        re: &PerlCompiledRegex,
4639        caps: &PerlCaptures<'_>,
4640        capture_all: CaptureAllMode,
4641    ) -> Result<(), FlowOrError> {
4642        let m0 = caps.get(0).expect("regex capture 0");
4643        let s0 = offset + m0.start;
4644        let e0 = offset + m0.end;
4645        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
4646        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
4647        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
4648        let mut last_paren = String::new();
4649        for i in 1..caps.len() {
4650            if let Some(m) = caps.get(i) {
4651                last_paren = m.text.to_string();
4652            }
4653        }
4654        self.last_paren_match = last_paren;
4655        self.last_subpattern_name = String::new();
4656        for n in re.capture_names().flatten() {
4657            if caps.name(n).is_some() {
4658                self.last_subpattern_name = n.to_string();
4659            }
4660        }
4661        self.scope
4662            .set_scalar("&", PerlValue::string(self.last_match.clone()))?;
4663        self.scope
4664            .set_scalar("`", PerlValue::string(self.prematch.clone()))?;
4665        self.scope
4666            .set_scalar("'", PerlValue::string(self.postmatch.clone()))?;
4667        self.scope
4668            .set_scalar("+", PerlValue::string(self.last_paren_match.clone()))?;
4669        for i in 1..caps.len() {
4670            if let Some(m) = caps.get(i) {
4671                self.scope
4672                    .set_scalar(&i.to_string(), PerlValue::string(m.text.to_string()))?;
4673            }
4674        }
4675        let mut start_arr = vec![PerlValue::integer(s0 as i64)];
4676        let mut end_arr = vec![PerlValue::integer(e0 as i64)];
4677        for i in 1..caps.len() {
4678            if let Some(m) = caps.get(i) {
4679                start_arr.push(PerlValue::integer((offset + m.start) as i64));
4680                end_arr.push(PerlValue::integer((offset + m.end) as i64));
4681            } else {
4682                start_arr.push(PerlValue::integer(-1));
4683                end_arr.push(PerlValue::integer(-1));
4684            }
4685        }
4686        self.scope.set_array("-", start_arr)?;
4687        self.scope.set_array("+", end_arr)?;
4688        let mut named = IndexMap::new();
4689        for name in re.capture_names().flatten() {
4690            if let Some(m) = caps.name(name) {
4691                named.insert(name.to_string(), PerlValue::string(m.text.to_string()));
4692            }
4693        }
4694        self.scope.set_hash("+", named.clone())?;
4695        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
4696        let mut named_minus = IndexMap::new();
4697        for (name, val) in &named {
4698            named_minus.insert(
4699                name.clone(),
4700                PerlValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
4701            );
4702        }
4703        self.scope.set_hash("-", named_minus)?;
4704        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
4705        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
4706        match capture_all {
4707            CaptureAllMode::Empty => {
4708                self.scope.set_array("^CAPTURE_ALL", vec![])?;
4709            }
4710            CaptureAllMode::Append => {
4711                let mut rows = self.scope.get_array("^CAPTURE_ALL");
4712                rows.push(PerlValue::array(cap_flat));
4713                self.scope.set_array("^CAPTURE_ALL", rows)?;
4714            }
4715            CaptureAllMode::Skip => {}
4716        }
4717        Ok(())
4718    }
4719
4720    pub(crate) fn clear_flip_flop_state(&mut self) {
4721        self.flip_flop_active.clear();
4722        self.flip_flop_exclusive_left_line.clear();
4723        self.flip_flop_sequence.clear();
4724        self.flip_flop_last_dot.clear();
4725        self.flip_flop_tree.clear();
4726    }
4727
4728    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
4729        self.flip_flop_active.resize(slots as usize, false);
4730        self.flip_flop_active.fill(false);
4731        self.flip_flop_exclusive_left_line
4732            .resize(slots as usize, None);
4733        self.flip_flop_exclusive_left_line.fill(None);
4734        self.flip_flop_sequence.resize(slots as usize, 0);
4735        self.flip_flop_sequence.fill(0);
4736        self.flip_flop_last_dot.resize(slots as usize, None);
4737        self.flip_flop_last_dot.fill(None);
4738    }
4739
4740    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
4741    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
4742    /// [`Self::handle_line_numbers`]).
4743    #[inline]
4744    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
4745        if self.last_readline_handle.is_empty() {
4746            self.line_number
4747        } else {
4748            *self
4749                .handle_line_numbers
4750                .get(&self.last_readline_handle)
4751                .unwrap_or(&0)
4752        }
4753    }
4754
4755    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
4756    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
4757    ///
4758    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
4759    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
4760    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
4761    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
4762    /// get `0` / `N` from [`PerlValue::to_int`].
4763    pub(crate) fn scalar_flip_flop_eval(
4764        &mut self,
4765        left: i64,
4766        right: i64,
4767        slot: usize,
4768        exclusive: bool,
4769    ) -> PerlResult<PerlValue> {
4770        if self.flip_flop_active.len() <= slot {
4771            self.flip_flop_active.resize(slot + 1, false);
4772        }
4773        if self.flip_flop_exclusive_left_line.len() <= slot {
4774            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4775        }
4776        if self.flip_flop_sequence.len() <= slot {
4777            self.flip_flop_sequence.resize(slot + 1, 0);
4778        }
4779        if self.flip_flop_last_dot.len() <= slot {
4780            self.flip_flop_last_dot.resize(slot + 1, None);
4781        }
4782        let dot = self.scalar_flipflop_dot_line();
4783        let active = &mut self.flip_flop_active[slot];
4784        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4785        let seq = &mut self.flip_flop_sequence[slot];
4786        let last_dot = &mut self.flip_flop_last_dot[slot];
4787        if !*active {
4788            if dot == left {
4789                *active = true;
4790                *seq = 1;
4791                *last_dot = Some(dot);
4792                if exclusive {
4793                    *excl_left = Some(dot);
4794                } else {
4795                    *excl_left = None;
4796                    if dot == right {
4797                        *active = false;
4798                        return Ok(PerlValue::string(format!("{}E0", *seq)));
4799                    }
4800                }
4801                return Ok(PerlValue::string(seq.to_string()));
4802            }
4803            *last_dot = Some(dot);
4804            return Ok(PerlValue::string(String::new()));
4805        }
4806        // Already active: increment the sequence once per new `$.`, so a second evaluation on
4807        // the same line reads the same number (matches Perl `pp_flop`).
4808        if *last_dot != Some(dot) {
4809            *seq += 1;
4810            *last_dot = Some(dot);
4811        }
4812        let cur_seq = *seq;
4813        if let Some(ll) = *excl_left {
4814            if dot == right && dot > ll {
4815                *active = false;
4816                *excl_left = None;
4817                *seq = 0;
4818                return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4819            }
4820        } else if dot == right {
4821            *active = false;
4822            *seq = 0;
4823            return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4824        }
4825        Ok(PerlValue::string(cur_seq.to_string()))
4826    }
4827
4828    fn regex_flip_flop_transition(
4829        active: &mut bool,
4830        excl_left: &mut Option<i64>,
4831        exclusive: bool,
4832        dot: i64,
4833        left_m: bool,
4834        right_m: bool,
4835    ) -> i64 {
4836        if !*active {
4837            if left_m {
4838                *active = true;
4839                if exclusive {
4840                    *excl_left = Some(dot);
4841                } else {
4842                    *excl_left = None;
4843                    if right_m {
4844                        *active = false;
4845                    }
4846                }
4847                return 1;
4848            }
4849            return 0;
4850        }
4851        if let Some(ll) = *excl_left {
4852            if right_m && dot > ll {
4853                *active = false;
4854                *excl_left = None;
4855            }
4856        } else if right_m {
4857            *active = false;
4858        }
4859        1
4860    }
4861
4862    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
4863    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
4864    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
4865    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
4866    pub(crate) fn regex_flip_flop_eval(
4867        &mut self,
4868        left_pat: &str,
4869        left_flags: &str,
4870        right_pat: &str,
4871        right_flags: &str,
4872        slot: usize,
4873        exclusive: bool,
4874        line: usize,
4875    ) -> PerlResult<PerlValue> {
4876        let dot = self.scalar_flipflop_dot_line();
4877        let subject = self.scope.get_scalar("_").to_string();
4878        let left_re = self
4879            .compile_regex(left_pat, left_flags, line)
4880            .map_err(|e| match e {
4881                FlowOrError::Error(err) => err,
4882                FlowOrError::Flow(_) => {
4883                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4884                }
4885            })?;
4886        let right_re = self
4887            .compile_regex(right_pat, right_flags, line)
4888            .map_err(|e| match e {
4889                FlowOrError::Error(err) => err,
4890                FlowOrError::Flow(_) => {
4891                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4892                }
4893            })?;
4894        let left_m = left_re.is_match(&subject);
4895        let right_m = right_re.is_match(&subject);
4896        if self.flip_flop_active.len() <= slot {
4897            self.flip_flop_active.resize(slot + 1, false);
4898        }
4899        if self.flip_flop_exclusive_left_line.len() <= slot {
4900            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4901        }
4902        let active = &mut self.flip_flop_active[slot];
4903        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4904        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4905            active, excl_left, exclusive, dot, left_m, right_m,
4906        )))
4907    }
4908
4909    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
4910    pub(crate) fn regex_flip_flop_eval_dynamic_right(
4911        &mut self,
4912        left_pat: &str,
4913        left_flags: &str,
4914        slot: usize,
4915        exclusive: bool,
4916        line: usize,
4917        right_m: bool,
4918    ) -> PerlResult<PerlValue> {
4919        let dot = self.scalar_flipflop_dot_line();
4920        let subject = self.scope.get_scalar("_").to_string();
4921        let left_re = self
4922            .compile_regex(left_pat, left_flags, line)
4923            .map_err(|e| match e {
4924                FlowOrError::Error(err) => err,
4925                FlowOrError::Flow(_) => {
4926                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4927                }
4928            })?;
4929        let left_m = left_re.is_match(&subject);
4930        if self.flip_flop_active.len() <= slot {
4931            self.flip_flop_active.resize(slot + 1, false);
4932        }
4933        if self.flip_flop_exclusive_left_line.len() <= slot {
4934            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4935        }
4936        let active = &mut self.flip_flop_active[slot];
4937        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4938        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4939            active, excl_left, exclusive, dot, left_m, right_m,
4940        )))
4941    }
4942
4943    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
4944    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
4945        &mut self,
4946        left_pat: &str,
4947        left_flags: &str,
4948        slot: usize,
4949        exclusive: bool,
4950        line: usize,
4951        rhs_line: i64,
4952    ) -> PerlResult<PerlValue> {
4953        let dot = self.scalar_flipflop_dot_line();
4954        let subject = self.scope.get_scalar("_").to_string();
4955        let left_re = self
4956            .compile_regex(left_pat, left_flags, line)
4957            .map_err(|e| match e {
4958                FlowOrError::Error(err) => err,
4959                FlowOrError::Flow(_) => {
4960                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4961                }
4962            })?;
4963        let left_m = left_re.is_match(&subject);
4964        let right_m = dot == rhs_line;
4965        if self.flip_flop_active.len() <= slot {
4966            self.flip_flop_active.resize(slot + 1, false);
4967        }
4968        if self.flip_flop_exclusive_left_line.len() <= slot {
4969            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4970        }
4971        let active = &mut self.flip_flop_active[slot];
4972        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4973        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4974            active, excl_left, exclusive, dot, left_m, right_m,
4975        )))
4976    }
4977
4978    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
4979    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
4980    /// right test until `$.` is strictly past the line where the left regex matched (same as
4981    /// [`Self::regex_flip_flop_eval`]).
4982    pub(crate) fn regex_eof_flip_flop_eval(
4983        &mut self,
4984        left_pat: &str,
4985        left_flags: &str,
4986        slot: usize,
4987        exclusive: bool,
4988        line: usize,
4989    ) -> PerlResult<PerlValue> {
4990        let dot = self.scalar_flipflop_dot_line();
4991        let subject = self.scope.get_scalar("_").to_string();
4992        let left_re = self
4993            .compile_regex(left_pat, left_flags, line)
4994            .map_err(|e| match e {
4995                FlowOrError::Error(err) => err,
4996                FlowOrError::Flow(_) => {
4997                    PerlError::runtime("unexpected flow in regex/eof flip-flop", line)
4998                }
4999            })?;
5000        let left_m = left_re.is_match(&subject);
5001        let right_m = self.eof_without_arg_is_true();
5002        if self.flip_flop_active.len() <= slot {
5003            self.flip_flop_active.resize(slot + 1, false);
5004        }
5005        if self.flip_flop_exclusive_left_line.len() <= slot {
5006            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
5007        }
5008        let active = &mut self.flip_flop_active[slot];
5009        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
5010        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
5011            active, excl_left, exclusive, dot, left_m, right_m,
5012        )))
5013    }
5014
5015    /// Shared `chomp` for tree-walker and VM (mutates `target`).
5016    pub(crate) fn chomp_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
5017        let mut s = val.to_string();
5018        let removed = if s.ends_with('\n') {
5019            s.pop();
5020            1i64
5021        } else {
5022            0i64
5023        };
5024        self.assign_value(target, PerlValue::string(s))?;
5025        Ok(PerlValue::integer(removed))
5026    }
5027
5028    /// Shared `chop` for tree-walker and VM (mutates `target`).
5029    pub(crate) fn chop_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
5030        let mut s = val.to_string();
5031        let chopped = s
5032            .pop()
5033            .map(|c| PerlValue::string(c.to_string()))
5034            .unwrap_or(PerlValue::UNDEF);
5035        self.assign_value(target, PerlValue::string(s))?;
5036        Ok(chopped)
5037    }
5038
5039    /// Shared regex match for tree-walker and VM (`pos` is updated for scalar `/g`).
5040    pub(crate) fn regex_match_execute(
5041        &mut self,
5042        s: String,
5043        pattern: &str,
5044        flags: &str,
5045        scalar_g: bool,
5046        pos_key: &str,
5047        line: usize,
5048    ) -> ExecResult {
5049        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
5050        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
5051        // also keep per-pattern `pos()` state that the memo doesn't track.
5052        //
5053        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
5054        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
5055        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
5056        if !flags.contains('g') && !scalar_g {
5057            let memo_hit = {
5058                if let Some(ref mem) = self.regex_match_memo {
5059                    mem.pattern == pattern
5060                        && mem.flags == flags
5061                        && mem.multiline == self.multiline_match
5062                        && mem.haystack == s
5063                } else {
5064                    false
5065                }
5066            };
5067            if memo_hit {
5068                if self.regex_capture_scope_fresh {
5069                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
5070                }
5071                // Memo hit but scope side effects were invalidated. Re-apply captures
5072                // from the memoized haystack + a fresh compiled regex.
5073                let (memo_s, memo_result) = {
5074                    let mem = self.regex_match_memo.as_ref().expect("memo");
5075                    (mem.haystack.clone(), mem.result.clone())
5076                };
5077                let re = self.compile_regex(pattern, flags, line)?;
5078                if let Some(caps) = re.captures(&memo_s) {
5079                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
5080                }
5081                self.regex_capture_scope_fresh = true;
5082                return Ok(memo_result);
5083            }
5084        }
5085        let re = self.compile_regex(pattern, flags, line)?;
5086        if flags.contains('g') && scalar_g {
5087            let key = pos_key.to_string();
5088            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
5089            if start == 0 {
5090                self.scope.set_array("^CAPTURE_ALL", vec![])?;
5091            }
5092            if start > s.len() {
5093                self.regex_pos.insert(key, None);
5094                return Ok(PerlValue::integer(0));
5095            }
5096            let sub = s.get(start..).unwrap_or("");
5097            if let Some(caps) = re.captures(sub) {
5098                let overall = caps.get(0).expect("capture 0");
5099                let abs_end = start + overall.end;
5100                self.regex_pos.insert(key, Some(abs_end));
5101                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
5102                Ok(PerlValue::integer(1))
5103            } else {
5104                self.regex_pos.insert(key, None);
5105                Ok(PerlValue::integer(0))
5106            }
5107        } else if flags.contains('g') {
5108            let mut rows = Vec::new();
5109            let mut last_caps: Option<PerlCaptures<'_>> = None;
5110            for caps in re.captures_iter(&s) {
5111                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5112                    &caps,
5113                )));
5114                last_caps = Some(caps);
5115            }
5116            self.scope.set_array("^CAPTURE_ALL", rows)?;
5117            let matches: Vec<PerlValue> = match &*re {
5118                PerlCompiledRegex::Rust(r) => r
5119                    .find_iter(&s)
5120                    .map(|m| PerlValue::string(m.as_str().to_string()))
5121                    .collect(),
5122                PerlCompiledRegex::Fancy(r) => r
5123                    .find_iter(&s)
5124                    .filter_map(|m| m.ok())
5125                    .map(|m| PerlValue::string(m.as_str().to_string()))
5126                    .collect(),
5127                PerlCompiledRegex::Pcre2(r) => r
5128                    .find_iter(s.as_bytes())
5129                    .filter_map(|m| m.ok())
5130                    .map(|m| {
5131                        let t = s.get(m.start()..m.end()).unwrap_or("");
5132                        PerlValue::string(t.to_string())
5133                    })
5134                    .collect(),
5135            };
5136            if matches.is_empty() {
5137                Ok(PerlValue::integer(0))
5138            } else {
5139                if let Some(caps) = last_caps {
5140                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
5141                }
5142                Ok(PerlValue::array(matches))
5143            }
5144        } else if let Some(caps) = re.captures(&s) {
5145            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
5146            let result = PerlValue::integer(1);
5147            self.regex_match_memo = Some(RegexMatchMemo {
5148                pattern: pattern.to_string(),
5149                flags: flags.to_string(),
5150                multiline: self.multiline_match,
5151                haystack: s,
5152                result: result.clone(),
5153            });
5154            self.regex_capture_scope_fresh = true;
5155            Ok(result)
5156        } else {
5157            let result = PerlValue::integer(0);
5158            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
5159            self.regex_match_memo = Some(RegexMatchMemo {
5160                pattern: pattern.to_string(),
5161                flags: flags.to_string(),
5162                multiline: self.multiline_match,
5163                haystack: s,
5164                result: result.clone(),
5165            });
5166            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
5167            // the last successful match (if any) set them to. Don't flip the flag.
5168            Ok(result)
5169        }
5170    }
5171
5172    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
5173    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
5174    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
5175    pub(crate) fn expand_env_braces_in_subst(
5176        &mut self,
5177        raw: &str,
5178        line: usize,
5179    ) -> PerlResult<String> {
5180        self.materialize_env_if_needed();
5181        let mut out = String::new();
5182        let mut rest = raw;
5183        while let Some(idx) = rest.find("$ENV{") {
5184            out.push_str(&rest[..idx]);
5185            let after = &rest[idx + 5..];
5186            let end = after
5187                .find('}')
5188                .ok_or_else(|| PerlError::runtime("Unclosed $ENV{...} in s///", line))?;
5189            let key = &after[..end];
5190            let val = self.scope.get_hash_element("ENV", key);
5191            out.push_str(&val.to_string());
5192            rest = &after[end + 1..];
5193        }
5194        out.push_str(rest);
5195        Ok(out)
5196    }
5197
5198    /// Shared `s///` for tree-walker and VM.
5199    ///
5200    /// Perl replacement strings accept both `\1` and `$1` for back-references.
5201    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
5202    /// understand `$N`, so we normalise here.
5203    pub(crate) fn regex_subst_execute(
5204        &mut self,
5205        s: String,
5206        pattern: &str,
5207        replacement: &str,
5208        flags: &str,
5209        target: &Expr,
5210        line: usize,
5211    ) -> ExecResult {
5212        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
5213        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
5214        let re = self.compile_regex(&pattern, &re_flags, line)?;
5215        if flags.contains('e') {
5216            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
5217        }
5218        let replacement = self.expand_env_braces_in_subst(replacement, line)?;
5219        let replacement = self.interpolate_replacement_string(&replacement);
5220        let replacement = normalize_replacement_backrefs(&replacement);
5221        let last_caps = if flags.contains('g') {
5222            let mut rows = Vec::new();
5223            let mut last = None;
5224            for caps in re.captures_iter(&s) {
5225                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5226                    &caps,
5227                )));
5228                last = Some(caps);
5229            }
5230            self.scope.set_array("^CAPTURE_ALL", rows)?;
5231            last
5232        } else {
5233            re.captures(&s)
5234        };
5235        if let Some(caps) = last_caps {
5236            let mode = if flags.contains('g') {
5237                CaptureAllMode::Skip
5238            } else {
5239                CaptureAllMode::Empty
5240            };
5241            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
5242        }
5243        let (new_s, count) = if flags.contains('g') {
5244            let count = re.find_iter_count(&s);
5245            (re.replace_all(&s, replacement.as_str()), count)
5246        } else {
5247            let count = if re.is_match(&s) { 1 } else { 0 };
5248            (re.replace(&s, replacement.as_str()), count)
5249        };
5250        if flags.contains('r') {
5251            // /r — non-destructive: return the modified string, leave target unchanged
5252            Ok(PerlValue::string(new_s))
5253        } else {
5254            self.assign_value(target, PerlValue::string(new_s))?;
5255            Ok(PerlValue::integer(count as i64))
5256        }
5257    }
5258
5259    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
5260    /// and executes the string; the next round uses [`PerlValue::to_string`] of the prior value).
5261    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
5262        let prep_source = |raw: &str| -> String {
5263            let mut code = raw.trim().to_string();
5264            if !code.ends_with(';') {
5265                code.push(';');
5266            }
5267            code
5268        };
5269        let mut cur = prep_source(replacement);
5270        let mut last = PerlValue::UNDEF;
5271        for round in 0..e_count {
5272            last = crate::parse_and_run_string(&cur, self)?;
5273            if round + 1 < e_count {
5274                cur = prep_source(&last.to_string());
5275            }
5276        }
5277        Ok(last)
5278    }
5279
5280    fn regex_subst_execute_eval(
5281        &mut self,
5282        s: String,
5283        re: &PerlCompiledRegex,
5284        replacement: &str,
5285        flags: &str,
5286        target: &Expr,
5287        line: usize,
5288    ) -> ExecResult {
5289        let e_count = flags.chars().filter(|c| *c == 'e').count();
5290        if e_count == 0 {
5291            return Err(PerlError::runtime("s///e: internal error (no e flag)", line).into());
5292        }
5293
5294        if flags.contains('g') {
5295            let mut rows = Vec::new();
5296            let mut out = String::new();
5297            let mut last = 0usize;
5298            let mut count = 0usize;
5299            for caps in re.captures_iter(&s) {
5300                let m0 = caps.get(0).expect("regex capture 0");
5301                out.push_str(&s[last..m0.start]);
5302                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5303                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5304                out.push_str(&repl_val.to_string());
5305                last = m0.end;
5306                count += 1;
5307                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5308                    &caps,
5309                )));
5310            }
5311            self.scope.set_array("^CAPTURE_ALL", rows)?;
5312            out.push_str(&s[last..]);
5313            if flags.contains('r') {
5314                return Ok(PerlValue::string(out));
5315            }
5316            self.assign_value(target, PerlValue::string(out))?;
5317            return Ok(PerlValue::integer(count as i64));
5318        }
5319        if let Some(caps) = re.captures(&s) {
5320            let m0 = caps.get(0).expect("regex capture 0");
5321            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5322            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5323            let mut out = String::new();
5324            out.push_str(&s[..m0.start]);
5325            out.push_str(&repl_val.to_string());
5326            out.push_str(&s[m0.end..]);
5327            if flags.contains('r') {
5328                return Ok(PerlValue::string(out));
5329            }
5330            self.assign_value(target, PerlValue::string(out))?;
5331            return Ok(PerlValue::integer(1));
5332        }
5333        if flags.contains('r') {
5334            return Ok(PerlValue::string(s));
5335        }
5336        self.assign_value(target, PerlValue::string(s))?;
5337        Ok(PerlValue::integer(0))
5338    }
5339
5340    /// Shared `tr///` for tree-walker and VM.
5341    pub(crate) fn regex_transliterate_execute(
5342        &mut self,
5343        s: String,
5344        from: &str,
5345        to: &str,
5346        flags: &str,
5347        target: &Expr,
5348        line: usize,
5349    ) -> ExecResult {
5350        let _ = line;
5351        let from_chars = Self::tr_expand_ranges(from);
5352        let to_chars = Self::tr_expand_ranges(to);
5353        let delete_mode = flags.contains('d');
5354        let mut count = 0i64;
5355        let new_s: String = s
5356            .chars()
5357            .filter_map(|c| {
5358                if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
5359                    count += 1;
5360                    if delete_mode {
5361                        // /d — delete characters that match but have no replacement
5362                        if pos < to_chars.len() {
5363                            Some(to_chars[pos])
5364                        } else {
5365                            None // delete this character
5366                        }
5367                    } else {
5368                        // Normal mode: use last char in to_chars if pos exceeds, or keep original
5369                        Some(to_chars.get(pos).or(to_chars.last()).copied().unwrap_or(c))
5370                    }
5371                } else {
5372                    Some(c)
5373                }
5374            })
5375            .collect();
5376        if flags.contains('r') {
5377            // /r — non-destructive: return the modified string, leave target unchanged
5378            Ok(PerlValue::string(new_s))
5379        } else {
5380            self.assign_value(target, PerlValue::string(new_s))?;
5381            Ok(PerlValue::integer(count))
5382        }
5383    }
5384
5385    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
5386    /// A literal `-` at the start or end of the spec is kept as-is.
5387    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
5388        let raw: Vec<char> = spec.chars().collect();
5389        let mut out = Vec::with_capacity(raw.len());
5390        let mut i = 0;
5391        while i < raw.len() {
5392            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
5393                let start = raw[i] as u32;
5394                let end = raw[i + 2] as u32;
5395                for code in start..=end {
5396                    if let Some(c) = char::from_u32(code) {
5397                        out.push(c);
5398                    }
5399                }
5400                i += 3;
5401            } else {
5402                out.push(raw[i]);
5403                i += 1;
5404            }
5405        }
5406        out
5407    }
5408
5409    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
5410    pub(crate) fn splice_builtin_execute(
5411        &mut self,
5412        args: &[PerlValue],
5413        line: usize,
5414    ) -> PerlResult<PerlValue> {
5415        if args.is_empty() {
5416            return Err(PerlError::runtime("splice: missing array", line));
5417        }
5418        let arr_name = args[0].to_string();
5419        let arr_len = self.scope.array_len(&arr_name);
5420        let offset_val = args
5421            .get(1)
5422            .cloned()
5423            .unwrap_or_else(|| PerlValue::integer(0));
5424        let length_val = match args.get(2) {
5425            None => PerlValue::UNDEF,
5426            Some(v) => v.clone(),
5427        };
5428        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
5429        let rep_vals: Vec<PerlValue> = args.iter().skip(3).cloned().collect();
5430        let arr = self.scope.get_array_mut(&arr_name)?;
5431        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
5432        for (i, v) in rep_vals.into_iter().enumerate() {
5433            arr.insert(off + i, v);
5434        }
5435        Ok(match self.wantarray_kind {
5436            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
5437            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
5438        })
5439    }
5440
5441    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
5442    pub(crate) fn unshift_builtin_execute(
5443        &mut self,
5444        args: &[PerlValue],
5445        line: usize,
5446    ) -> PerlResult<PerlValue> {
5447        if args.is_empty() {
5448            return Err(PerlError::runtime("unshift: missing array", line));
5449        }
5450        let arr_name = args[0].to_string();
5451        let mut flat_vals: Vec<PerlValue> = Vec::new();
5452        for a in args.iter().skip(1) {
5453            if let Some(items) = a.as_array_vec() {
5454                flat_vals.extend(items);
5455            } else {
5456                flat_vals.push(a.clone());
5457            }
5458        }
5459        let arr = self.scope.get_array_mut(&arr_name)?;
5460        for (i, v) in flat_vals.into_iter().enumerate() {
5461            arr.insert(i, v);
5462        }
5463        Ok(PerlValue::integer(arr.len() as i64))
5464    }
5465
5466    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
5467    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
5468    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
5469        if upper == 0.0 {
5470            self.rand_rng.gen_range(0.0..1.0)
5471        } else if upper > 0.0 {
5472            self.rand_rng.gen_range(0.0..upper)
5473        } else {
5474            self.rand_rng.gen_range(upper..0.0)
5475        }
5476    }
5477
5478    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
5479    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
5480        let n = if let Some(s) = seed {
5481            s as i64
5482        } else {
5483            std::time::SystemTime::now()
5484                .duration_since(std::time::UNIX_EPOCH)
5485                .map(|d| d.as_secs() as i64)
5486                .unwrap_or(1)
5487        };
5488        let mag = n.unsigned_abs();
5489        self.rand_rng = StdRng::seed_from_u64(mag);
5490        n.abs()
5491    }
5492
5493    pub fn set_file(&mut self, file: &str) {
5494        self.file = file.to_string();
5495    }
5496
5497    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
5498    pub fn repl_completion_names(&self) -> Vec<String> {
5499        let mut v = self.scope.repl_binding_names();
5500        v.extend(self.subs.keys().cloned());
5501        v.sort();
5502        v.dedup();
5503        v
5504    }
5505
5506    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
5507    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
5508        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
5509        subs.sort();
5510        let mut classes: HashSet<String> = HashSet::new();
5511        for k in &subs {
5512            if let Some((pkg, rest)) = k.split_once("::") {
5513                if !rest.contains("::") {
5514                    classes.insert(pkg.to_string());
5515                }
5516            }
5517        }
5518        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
5519        for bn in self.scope.repl_binding_names() {
5520            if let Some(r) = bn.strip_prefix('$') {
5521                let v = self.scope.get_scalar(r);
5522                if let Some(b) = v.as_blessed_ref() {
5523                    blessed_scalars.insert(r.to_string(), b.class.clone());
5524                    classes.insert(b.class.clone());
5525                }
5526            }
5527        }
5528        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
5529        for c in classes {
5530            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
5531        }
5532        ReplCompletionSnapshot {
5533            subs,
5534            blessed_scalars,
5535            isa_for_class,
5536        }
5537    }
5538
5539    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
5540        if n == 0 {
5541            return Err(FlowOrError::Error(PerlError::runtime(
5542                "bench: iteration count must be positive",
5543                line,
5544            )));
5545        }
5546        let mut samples = Vec::with_capacity(n);
5547        for _ in 0..n {
5548            let start = std::time::Instant::now();
5549            self.exec_block(body)?;
5550            samples.push(start.elapsed().as_secs_f64() * 1000.0);
5551        }
5552        let mut sorted = samples.clone();
5553        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
5554        let min_ms = sorted[0];
5555        let mean = samples.iter().sum::<f64>() / n as f64;
5556        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
5557            .saturating_sub(1)
5558            .min(n - 1);
5559        let p99_ms = sorted[p99_idx];
5560        Ok(PerlValue::string(format!(
5561            "bench: n={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
5562            n, min_ms, mean, p99_ms
5563        )))
5564    }
5565
5566    pub fn execute(&mut self, program: &Program) -> PerlResult<PerlValue> {
5567        // `-n`/`-p`: main must run only inside [`Self::process_line`], not as a full-program VM/tree
5568        // run (would execute `print` once before any input, etc.).
5569        if self.line_mode_skip_main {
5570            return self.execute_tree(program);
5571        }
5572        // With `--profile`, the VM records per-opcode line times and sub enter/return (JIT off).
5573        // Try bytecode VM first — falls back to tree-walker on unsupported features
5574        if let Some(result) = crate::try_vm_execute(program, self) {
5575            return result;
5576        }
5577
5578        // Tree-walker fallback
5579        self.execute_tree(program)
5580    }
5581
5582    /// Run `END` blocks (after `-n`/`-p` line loop when prelude used [`Self::line_mode_skip_main`]).
5583    pub fn run_end_blocks(&mut self) -> PerlResult<()> {
5584        self.global_phase = "END".to_string();
5585        let ends = std::mem::take(&mut self.end_blocks);
5586        for block in &ends {
5587            self.exec_block(block).map_err(|e| match e {
5588                FlowOrError::Error(e) => e,
5589                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in END", 0),
5590            })?;
5591        }
5592        Ok(())
5593    }
5594
5595    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
5596    /// and drain remaining `DESTROY` callbacks.
5597    pub fn run_global_teardown(&mut self) -> PerlResult<()> {
5598        self.global_phase = "DESTRUCT".to_string();
5599        self.drain_pending_destroys(0)
5600    }
5601
5602    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
5603    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> PerlResult<()> {
5604        loop {
5605            let batch = crate::pending_destroy::take_queue();
5606            if batch.is_empty() {
5607                break;
5608            }
5609            for (class, payload) in batch {
5610                let fq = format!("{}::DESTROY", class);
5611                let Some(sub) = self.subs.get(&fq).cloned() else {
5612                    continue;
5613                };
5614                let inv = PerlValue::blessed(Arc::new(
5615                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
5616                ));
5617                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
5618                    Ok(_) => {}
5619                    Err(FlowOrError::Error(e)) => return Err(e),
5620                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
5621                    Err(FlowOrError::Flow(other)) => {
5622                        return Err(PerlError::runtime(
5623                            format!("DESTROY: unexpected control flow ({other:?})"),
5624                            line,
5625                        ));
5626                    }
5627                }
5628            }
5629        }
5630        Ok(())
5631    }
5632
5633    /// Tree-walking execution (fallback when bytecode compilation fails).
5634    pub fn execute_tree(&mut self, program: &Program) -> PerlResult<PerlValue> {
5635        // `${^GLOBAL_PHASE}` — each program starts in `RUN` (Perl before any `BEGIN` runs).
5636        self.global_phase = "RUN".to_string();
5637        self.clear_flip_flop_state();
5638        // First pass: subs, `use` (source order), BEGIN/END collection
5639        self.prepare_program_top_level(program)?;
5640
5641        // Execute BEGIN blocks (Perl uses phase `START` here).
5642        let begins = std::mem::take(&mut self.begin_blocks);
5643        if !begins.is_empty() {
5644            self.global_phase = "START".to_string();
5645        }
5646        for block in &begins {
5647            self.exec_block(block).map_err(|e| match e {
5648                FlowOrError::Error(e) => e,
5649                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in BEGIN", 0),
5650            })?;
5651        }
5652
5653        // UNITCHECK — reverse order of compilation (end of unit, before CHECK).
5654        // Perl keeps `${^GLOBAL_PHASE}` as **`START`** during these blocks (not `UNITCHECK`).
5655        let ucs = std::mem::take(&mut self.unit_check_blocks);
5656        for block in ucs.iter().rev() {
5657            self.exec_block(block).map_err(|e| match e {
5658                FlowOrError::Error(e) => e,
5659                FlowOrError::Flow(_) => {
5660                    PerlError::runtime("Unexpected flow control in UNITCHECK", 0)
5661                }
5662            })?;
5663        }
5664
5665        // CHECK — reverse order (end of compile phase).
5666        let checks = std::mem::take(&mut self.check_blocks);
5667        if !checks.is_empty() {
5668            self.global_phase = "CHECK".to_string();
5669        }
5670        for block in checks.iter().rev() {
5671            self.exec_block(block).map_err(|e| match e {
5672                FlowOrError::Error(e) => e,
5673                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in CHECK", 0),
5674            })?;
5675        }
5676
5677        // INIT — forward order (before main runtime).
5678        let inits = std::mem::take(&mut self.init_blocks);
5679        if !inits.is_empty() {
5680            self.global_phase = "INIT".to_string();
5681        }
5682        for block in &inits {
5683            self.exec_block(block).map_err(|e| match e {
5684                FlowOrError::Error(e) => e,
5685                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in INIT", 0),
5686            })?;
5687        }
5688
5689        self.global_phase = "RUN".to_string();
5690
5691        if self.line_mode_skip_main {
5692            // Body runs once per input line in [`Self::process_line`]; `END` runs after the loop
5693            // via [`Self::run_end_blocks`].
5694            return Ok(PerlValue::UNDEF);
5695        }
5696
5697        // Execute main program
5698        let mut last = PerlValue::UNDEF;
5699        for stmt in &program.statements {
5700            match &stmt.kind {
5701                StmtKind::Begin(_)
5702                | StmtKind::UnitCheck(_)
5703                | StmtKind::Check(_)
5704                | StmtKind::Init(_)
5705                | StmtKind::End(_)
5706                | StmtKind::UsePerlVersion { .. }
5707                | StmtKind::Use { .. }
5708                | StmtKind::No { .. }
5709                | StmtKind::FormatDecl { .. } => continue,
5710                _ => {
5711                    match self.exec_statement(stmt) {
5712                        Ok(val) => last = val,
5713                        Err(FlowOrError::Error(e)) => {
5714                            // Execute END blocks before propagating (all exit codes, including 0)
5715                            self.global_phase = "END".to_string();
5716                            let ends = std::mem::take(&mut self.end_blocks);
5717                            for block in &ends {
5718                                let _ = self.exec_block(block);
5719                            }
5720                            return Err(e);
5721                        }
5722                        Err(FlowOrError::Flow(Flow::Return(v))) => {
5723                            last = v;
5724                            break;
5725                        }
5726                        Err(FlowOrError::Flow(_)) => {}
5727                    }
5728                }
5729            }
5730        }
5731
5732        // Execute END blocks (Perl uses phase `END` here).
5733        self.global_phase = "END".to_string();
5734        let ends = std::mem::take(&mut self.end_blocks);
5735        for block in &ends {
5736            let _ = self.exec_block(block);
5737        }
5738
5739        self.drain_pending_destroys(0)?;
5740        Ok(last)
5741    }
5742
5743    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
5744        self.exec_block_with_tail(block, WantarrayCtx::Void)
5745    }
5746
5747    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
5748    /// Non-final statements stay void context.
5749    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5750        let uses_goto = block
5751            .iter()
5752            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
5753        if uses_goto {
5754            self.scope_push_hook();
5755            let r = self.exec_block_with_goto_tail(block, tail);
5756            self.scope_pop_hook();
5757            r
5758        } else {
5759            self.scope_push_hook();
5760            let result = self.exec_block_no_scope_with_tail(block, tail);
5761            self.scope_pop_hook();
5762            result
5763        }
5764    }
5765
5766    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5767        let mut map: HashMap<String, usize> = HashMap::new();
5768        for (i, s) in block.iter().enumerate() {
5769            if let Some(l) = &s.label {
5770                map.insert(l.clone(), i);
5771            }
5772        }
5773        let mut pc = 0usize;
5774        let mut last = PerlValue::UNDEF;
5775        let last_idx = block.len().saturating_sub(1);
5776        while pc < block.len() {
5777            if let StmtKind::Goto { target } = &block[pc].kind {
5778                let line = block[pc].line;
5779                let name = self.eval_expr(target)?.to_string();
5780                pc = *map.get(&name).ok_or_else(|| {
5781                    FlowOrError::Error(PerlError::runtime(
5782                        format!("goto: unknown label {}", name),
5783                        line,
5784                    ))
5785                })?;
5786                continue;
5787            }
5788            let v = if pc == last_idx {
5789                match &block[pc].kind {
5790                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
5791                    _ => self.exec_statement(&block[pc])?,
5792                }
5793            } else {
5794                self.exec_statement(&block[pc])?
5795            };
5796            last = v;
5797            pc += 1;
5798        }
5799        Ok(last)
5800    }
5801
5802    /// Execute block statements without pushing/popping a scope frame.
5803    /// Used internally by loops and the VM for sub calls.
5804    #[inline]
5805    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
5806        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
5807    }
5808
5809    pub(crate) fn exec_block_no_scope_with_tail(
5810        &mut self,
5811        block: &Block,
5812        tail: WantarrayCtx,
5813    ) -> ExecResult {
5814        if block.is_empty() {
5815            return Ok(PerlValue::UNDEF);
5816        }
5817        let last_i = block.len() - 1;
5818        for (i, stmt) in block.iter().enumerate() {
5819            if i < last_i {
5820                self.exec_statement(stmt)?;
5821            } else {
5822                return match &stmt.kind {
5823                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
5824                    _ => self.exec_statement(stmt),
5825                };
5826            }
5827        }
5828        Ok(PerlValue::UNDEF)
5829    }
5830
5831    /// Spawn `block` on a worker thread; returns an [`PerlValue::AsyncTask`] handle (`async { }` / `spawn { }`).
5832    pub(crate) fn spawn_async_block(&self, block: &Block) -> PerlValue {
5833        use parking_lot::Mutex as ParkMutex;
5834
5835        let block = block.clone();
5836        let subs = self.subs.clone();
5837        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5838        let result = Arc::new(ParkMutex::new(None));
5839        let join = Arc::new(ParkMutex::new(None));
5840        let result2 = result.clone();
5841        let h = std::thread::spawn(move || {
5842            let mut interp = Interpreter::new();
5843            interp.subs = subs;
5844            interp.scope.restore_capture(&scalars);
5845            interp.scope.restore_atomics(&aar, &ahash);
5846            interp.enable_parallel_guard();
5847            let r = match interp.exec_block(&block) {
5848                Ok(v) => Ok(v),
5849                Err(FlowOrError::Error(e)) => Err(e),
5850                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5851                    Err(PerlError::runtime("yield inside async/spawn block", 0))
5852                }
5853                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5854            };
5855            *result2.lock() = Some(r);
5856        });
5857        *join.lock() = Some(h);
5858        PerlValue::async_task(Arc::new(PerlAsyncTask { result, join }))
5859    }
5860
5861    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
5862    pub(crate) fn eval_timeout_block(
5863        &mut self,
5864        body: &Block,
5865        secs: f64,
5866        line: usize,
5867    ) -> ExecResult {
5868        use std::sync::mpsc::channel;
5869        use std::time::Duration;
5870
5871        let block = body.clone();
5872        let subs = self.subs.clone();
5873        let struct_defs = self.struct_defs.clone();
5874        let enum_defs = self.enum_defs.clone();
5875        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5876        self.materialize_env_if_needed();
5877        let env = self.env.clone();
5878        let argv = self.argv.clone();
5879        let inc = self.scope.get_array("INC");
5880        let (tx, rx) = channel::<PerlResult<PerlValue>>();
5881        let _handle = std::thread::spawn(move || {
5882            let mut interp = Interpreter::new();
5883            interp.subs = subs;
5884            interp.struct_defs = struct_defs;
5885            interp.enum_defs = enum_defs;
5886            interp.env = env.clone();
5887            interp.argv = argv.clone();
5888            interp.scope.declare_array(
5889                "ARGV",
5890                argv.iter().map(|s| PerlValue::string(s.clone())).collect(),
5891            );
5892            for (k, v) in env {
5893                interp
5894                    .scope
5895                    .set_hash_element("ENV", &k, v)
5896                    .expect("set ENV in timeout thread");
5897            }
5898            interp.scope.declare_array("INC", inc);
5899            interp.scope.restore_capture(&scalars);
5900            interp.scope.restore_atomics(&aar, &ahash);
5901            interp.enable_parallel_guard();
5902            let out: PerlResult<PerlValue> = match interp.exec_block(&block) {
5903                Ok(v) => Ok(v),
5904                Err(FlowOrError::Error(e)) => Err(e),
5905                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5906                    Err(PerlError::runtime("yield inside eval_timeout block", 0))
5907                }
5908                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5909            };
5910            let _ = tx.send(out);
5911        });
5912        let dur = Duration::from_secs_f64(secs.max(0.0));
5913        match rx.recv_timeout(dur) {
5914            Ok(Ok(v)) => Ok(v),
5915            Ok(Err(e)) => Err(FlowOrError::Error(e)),
5916            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(PerlError::runtime(
5917                format!(
5918                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
5919                    secs
5920                ),
5921                line,
5922            )
5923            .into()),
5924            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(PerlError::runtime(
5925                "eval_timeout: worker thread panicked or disconnected",
5926                line,
5927            )
5928            .into()),
5929        }
5930    }
5931
5932    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
5933        let mut last = PerlValue::UNDEF;
5934        for stmt in body {
5935            match &stmt.kind {
5936                StmtKind::When { cond, body: wb } => {
5937                    if self.when_matches(cond)? {
5938                        return self.exec_block_smart(wb);
5939                    }
5940                }
5941                StmtKind::DefaultCase { body: db } => {
5942                    return self.exec_block_smart(db);
5943                }
5944                _ => {
5945                    last = self.exec_statement(stmt)?;
5946                }
5947            }
5948        }
5949        Ok(last)
5950    }
5951
5952    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
5953    pub(crate) fn exec_given_with_topic_value(
5954        &mut self,
5955        topic: PerlValue,
5956        body: &Block,
5957    ) -> ExecResult {
5958        self.scope_push_hook();
5959        self.scope.declare_scalar("_", topic);
5960        self.english_note_lexical_scalar("_");
5961        let r = self.exec_given_body(body);
5962        self.scope_pop_hook();
5963        r
5964    }
5965
5966    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
5967        let t = self.eval_expr(topic)?;
5968        self.exec_given_with_topic_value(t, body)
5969    }
5970
5971    /// `when (COND)` — topic is `$_` (set by `given`).
5972    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
5973        let topic = self.scope.get_scalar("_");
5974        let line = cond.line;
5975        match &cond.kind {
5976            ExprKind::Regex(pattern, flags) => {
5977                let re = self.compile_regex(pattern, flags, line)?;
5978                let s = topic.to_string();
5979                Ok(re.is_match(&s))
5980            }
5981            ExprKind::String(s) => Ok(topic.to_string() == *s),
5982            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
5983            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
5984            _ => {
5985                let c = self.eval_expr(cond)?;
5986                Ok(self.smartmatch_when(&topic, &c))
5987            }
5988        }
5989    }
5990
5991    fn smartmatch_when(&self, topic: &PerlValue, c: &PerlValue) -> bool {
5992        if let Some(re) = c.as_regex() {
5993            return re.is_match(&topic.to_string());
5994        }
5995        topic.to_string() == c.to_string()
5996    }
5997
5998    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
5999    pub(crate) fn eval_boolean_rvalue_condition(
6000        &mut self,
6001        cond: &Expr,
6002    ) -> Result<bool, FlowOrError> {
6003        match &cond.kind {
6004            ExprKind::Regex(pattern, flags) => {
6005                let topic = self.scope.get_scalar("_");
6006                let line = cond.line;
6007                let s = topic.to_string();
6008                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
6009                Ok(v.is_true())
6010            }
6011            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
6012            ExprKind::ReadLine(_) => {
6013                let v = self.eval_expr(cond)?;
6014                self.scope.set_topic(v.clone());
6015                Ok(!v.is_undef())
6016            }
6017            _ => {
6018                let v = self.eval_expr(cond)?;
6019                Ok(v.is_true())
6020            }
6021        }
6022    }
6023
6024    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
6025    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
6026        self.eval_boolean_rvalue_condition(cond)
6027    }
6028
6029    pub(crate) fn eval_algebraic_match(
6030        &mut self,
6031        subject: &Expr,
6032        arms: &[MatchArm],
6033        line: usize,
6034    ) -> ExecResult {
6035        let val = self.eval_algebraic_match_subject(subject, line)?;
6036        self.eval_algebraic_match_with_subject_value(val, arms, line)
6037    }
6038
6039    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
6040    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
6041        match &subject.kind {
6042            ExprKind::ArrayVar(name) => {
6043                self.check_strict_array_var(name, line)?;
6044                let aname = self.stash_array_name_for_package(name);
6045                Ok(PerlValue::array_binding_ref(aname))
6046            }
6047            ExprKind::HashVar(name) => {
6048                self.check_strict_hash_var(name, line)?;
6049                self.touch_env_hash(name);
6050                Ok(PerlValue::hash_binding_ref(name.clone()))
6051            }
6052            _ => self.eval_expr(subject),
6053        }
6054    }
6055
6056    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
6057    pub(crate) fn eval_algebraic_match_with_subject_value(
6058        &mut self,
6059        val: PerlValue,
6060        arms: &[MatchArm],
6061        line: usize,
6062    ) -> ExecResult {
6063        // Exhaustive enum match: check variant coverage before matching
6064        if let Some(e) = val.as_enum_inst() {
6065            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
6066            if !has_catchall {
6067                let covered: Vec<String> = arms
6068                    .iter()
6069                    .filter_map(|a| {
6070                        if let MatchPattern::Value(expr) = &a.pattern {
6071                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
6072                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
6073                            }
6074                        }
6075                        None
6076                    })
6077                    .collect();
6078                let missing: Vec<&str> = e
6079                    .def
6080                    .variants
6081                    .iter()
6082                    .filter(|v| !covered.contains(&v.name))
6083                    .map(|v| v.name.as_str())
6084                    .collect();
6085                if !missing.is_empty() {
6086                    return Err(PerlError::runtime(
6087                        format!(
6088                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
6089                            e.def.name,
6090                            missing.join(", ")
6091                        ),
6092                        line,
6093                    )
6094                    .into());
6095                }
6096            }
6097        }
6098        for arm in arms {
6099            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
6100                let re = self.compile_regex(pattern, flags, line)?;
6101                let s = val.to_string();
6102                if let Some(caps) = re.captures(&s) {
6103                    self.scope_push_hook();
6104                    self.scope.declare_scalar("_", val.clone());
6105                    self.english_note_lexical_scalar("_");
6106                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
6107                    let guard_ok = if let Some(g) = &arm.guard {
6108                        self.eval_expr(g)?.is_true()
6109                    } else {
6110                        true
6111                    };
6112                    if !guard_ok {
6113                        self.scope_pop_hook();
6114                        continue;
6115                    }
6116                    let out = self.eval_expr(&arm.body);
6117                    self.scope_pop_hook();
6118                    return out;
6119                }
6120                continue;
6121            }
6122            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
6123                self.scope_push_hook();
6124                self.scope.declare_scalar("_", val.clone());
6125                self.english_note_lexical_scalar("_");
6126                for b in bindings {
6127                    match b {
6128                        PatternBinding::Scalar(name, v) => {
6129                            self.scope.declare_scalar(&name, v);
6130                            self.english_note_lexical_scalar(&name);
6131                        }
6132                        PatternBinding::Array(name, elems) => {
6133                            self.scope.declare_array(&name, elems);
6134                        }
6135                    }
6136                }
6137                let guard_ok = if let Some(g) = &arm.guard {
6138                    self.eval_expr(g)?.is_true()
6139                } else {
6140                    true
6141                };
6142                if !guard_ok {
6143                    self.scope_pop_hook();
6144                    continue;
6145                }
6146                let out = self.eval_expr(&arm.body);
6147                self.scope_pop_hook();
6148                return out;
6149            }
6150        }
6151        Err(PerlError::runtime(
6152            "match: no arm matched the value (add a `_` catch-all)",
6153            line,
6154        )
6155        .into())
6156    }
6157
6158    fn parse_duration_seconds(pv: &PerlValue) -> Option<f64> {
6159        let s = pv.to_string();
6160        let s = s.trim();
6161        if let Some(rest) = s.strip_suffix("ms") {
6162            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
6163        }
6164        if let Some(rest) = s.strip_suffix('s') {
6165            return rest.trim().parse::<f64>().ok();
6166        }
6167        if let Some(rest) = s.strip_suffix('m') {
6168            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
6169        }
6170        s.parse::<f64>().ok()
6171    }
6172
6173    fn eval_retry_block(
6174        &mut self,
6175        body: &Block,
6176        times: &Expr,
6177        backoff: RetryBackoff,
6178        _line: usize,
6179    ) -> ExecResult {
6180        let max = self.eval_expr(times)?.to_int().max(1) as usize;
6181        let base_ms: u64 = 10;
6182        let mut attempt = 0usize;
6183        loop {
6184            attempt += 1;
6185            match self.exec_block(body) {
6186                Ok(v) => return Ok(v),
6187                Err(FlowOrError::Error(e)) => {
6188                    if attempt >= max {
6189                        return Err(FlowOrError::Error(e));
6190                    }
6191                    let delay_ms = match backoff {
6192                        RetryBackoff::None => 0,
6193                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
6194                        RetryBackoff::Exponential => {
6195                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
6196                        }
6197                    };
6198                    if delay_ms > 0 {
6199                        std::thread::sleep(Duration::from_millis(delay_ms));
6200                    }
6201                }
6202                Err(e) => return Err(e),
6203            }
6204        }
6205    }
6206
6207    fn eval_rate_limit_block(
6208        &mut self,
6209        slot: u32,
6210        max: &Expr,
6211        window: &Expr,
6212        body: &Block,
6213        _line: usize,
6214    ) -> ExecResult {
6215        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
6216        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
6217            .filter(|s| *s > 0.0)
6218            .unwrap_or(1.0);
6219        let window_d = Duration::from_secs_f64(window_sec);
6220        let slot = slot as usize;
6221        while self.rate_limit_slots.len() <= slot {
6222            self.rate_limit_slots.push(VecDeque::new());
6223        }
6224        {
6225            let dq = &mut self.rate_limit_slots[slot];
6226            loop {
6227                let now = Instant::now();
6228                while let Some(t0) = dq.front().copied() {
6229                    if now.duration_since(t0) >= window_d {
6230                        dq.pop_front();
6231                    } else {
6232                        break;
6233                    }
6234                }
6235                if dq.len() < max_n || max_n == 0 {
6236                    break;
6237                }
6238                let t0 = dq.front().copied().unwrap();
6239                let wait = window_d.saturating_sub(now.duration_since(t0));
6240                if wait.is_zero() {
6241                    dq.pop_front();
6242                    continue;
6243                }
6244                std::thread::sleep(wait);
6245            }
6246            dq.push_back(Instant::now());
6247        }
6248        self.exec_block(body)
6249    }
6250
6251    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
6252        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
6253            .filter(|s| *s > 0.0)
6254            .unwrap_or(1.0);
6255        loop {
6256            match self.exec_block(body) {
6257                Ok(_) => {}
6258                Err(e) => return Err(e),
6259            }
6260            std::thread::sleep(Duration::from_secs_f64(sec));
6261        }
6262    }
6263
6264    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
6265    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> PerlResult<PerlValue> {
6266        let pair = |value: PerlValue, more: i64| {
6267            PerlValue::array_ref(Arc::new(RwLock::new(vec![value, PerlValue::integer(more)])))
6268        };
6269        let mut exhausted = gen.exhausted.lock();
6270        if *exhausted {
6271            return Ok(pair(PerlValue::UNDEF, 0));
6272        }
6273        let mut pc = gen.pc.lock();
6274        let mut scope_started = gen.scope_started.lock();
6275        if *pc >= gen.block.len() {
6276            if *scope_started {
6277                self.scope_pop_hook();
6278                *scope_started = false;
6279            }
6280            *exhausted = true;
6281            return Ok(pair(PerlValue::UNDEF, 0));
6282        }
6283        if !*scope_started {
6284            self.scope_push_hook();
6285            *scope_started = true;
6286        }
6287        self.in_generator = true;
6288        while *pc < gen.block.len() {
6289            let stmt = &gen.block[*pc];
6290            match self.exec_statement(stmt) {
6291                Ok(_) => {
6292                    *pc += 1;
6293                }
6294                Err(FlowOrError::Flow(Flow::Yield(v))) => {
6295                    *pc += 1;
6296                    self.in_generator = false;
6297                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
6298                    // binds in the caller block, not inside a frame left across yield.
6299                    if *scope_started {
6300                        self.scope_pop_hook();
6301                        *scope_started = false;
6302                    }
6303                    return Ok(pair(v, 1));
6304                }
6305                Err(e) => {
6306                    self.in_generator = false;
6307                    if *scope_started {
6308                        self.scope_pop_hook();
6309                        *scope_started = false;
6310                    }
6311                    return Err(match e {
6312                        FlowOrError::Error(ee) => ee,
6313                        FlowOrError::Flow(Flow::Yield(_)) => {
6314                            unreachable!("yield handled above")
6315                        }
6316                        FlowOrError::Flow(flow) => PerlError::runtime(
6317                            format!("unexpected control flow in generator: {:?}", flow),
6318                            0,
6319                        ),
6320                    });
6321                }
6322            }
6323        }
6324        self.in_generator = false;
6325        if *scope_started {
6326            self.scope_pop_hook();
6327            *scope_started = false;
6328        }
6329        *exhausted = true;
6330        Ok(pair(PerlValue::UNDEF, 0))
6331    }
6332
6333    fn match_pattern_try(
6334        &mut self,
6335        subject: &PerlValue,
6336        pattern: &MatchPattern,
6337        line: usize,
6338    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6339        match pattern {
6340            MatchPattern::Any => Ok(Some(vec![])),
6341            MatchPattern::Regex { .. } => {
6342                unreachable!("regex arms are handled in eval_algebraic_match")
6343            }
6344            MatchPattern::Value(expr) => {
6345                if self.match_pattern_value_alternation(subject, expr, line)? {
6346                    Ok(Some(vec![]))
6347                } else {
6348                    Ok(None)
6349                }
6350            }
6351            MatchPattern::Array(elems) => {
6352                let Some(arr) = self.match_subject_as_array(subject) else {
6353                    return Ok(None);
6354                };
6355                self.match_array_pattern_elems(&arr, elems, line)
6356            }
6357            MatchPattern::Hash(pairs) => {
6358                let Some(h) = self.match_subject_as_hash(subject) else {
6359                    return Ok(None);
6360                };
6361                self.match_hash_pattern_pairs(&h, pairs, line)
6362            }
6363            MatchPattern::OptionSome(name) => {
6364                let Some(arr) = self.match_subject_as_array(subject) else {
6365                    return Ok(None);
6366                };
6367                if arr.len() < 2 {
6368                    return Ok(None);
6369                }
6370                if !arr[1].is_true() {
6371                    return Ok(None);
6372                }
6373                Ok(Some(vec![PatternBinding::Scalar(
6374                    name.clone(),
6375                    arr[0].clone(),
6376                )]))
6377            }
6378        }
6379    }
6380
6381    /// Handle pattern alternation (e.g., `"foo" | "bar" | "baz"`) in match patterns.
6382    /// If the expression is a BitOr chain, recursively check if subject matches any alternative.
6383    fn match_pattern_value_alternation(
6384        &mut self,
6385        subject: &PerlValue,
6386        expr: &Expr,
6387        _line: usize,
6388    ) -> Result<bool, FlowOrError> {
6389        if let ExprKind::BinOp {
6390            left,
6391            op: BinOp::BitOr,
6392            right,
6393        } = &expr.kind
6394        {
6395            if self.match_pattern_value_alternation(subject, left, _line)? {
6396                return Ok(true);
6397            }
6398            return self.match_pattern_value_alternation(subject, right, _line);
6399        }
6400        let pv = self.eval_expr(expr)?;
6401        Ok(self.smartmatch_when(subject, &pv))
6402    }
6403
6404    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
6405    fn match_subject_as_array(&self, v: &PerlValue) -> Option<Vec<PerlValue>> {
6406        if let Some(a) = v.as_array_vec() {
6407            return Some(a);
6408        }
6409        if let Some(r) = v.as_array_ref() {
6410            return Some(r.read().clone());
6411        }
6412        if let Some(name) = v.as_array_binding_name() {
6413            return Some(self.scope.get_array(&name));
6414        }
6415        None
6416    }
6417
6418    fn match_subject_as_hash(&mut self, v: &PerlValue) -> Option<IndexMap<String, PerlValue>> {
6419        if let Some(h) = v.as_hash_map() {
6420            return Some(h);
6421        }
6422        if let Some(r) = v.as_hash_ref() {
6423            return Some(r.read().clone());
6424        }
6425        if let Some(name) = v.as_hash_binding_name() {
6426            self.touch_env_hash(&name);
6427            return Some(self.scope.get_hash(&name));
6428        }
6429        None
6430    }
6431
6432    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
6433    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
6434    pub(crate) fn hash_slice_deref_values(
6435        &mut self,
6436        container: &PerlValue,
6437        key_values: &[PerlValue],
6438        line: usize,
6439    ) -> Result<PerlValue, FlowOrError> {
6440        let h = if let Some(m) = self.match_subject_as_hash(container) {
6441            m
6442        } else {
6443            return Err(PerlError::runtime(
6444                "Hash slice dereference needs a hash or hash reference value",
6445                line,
6446            )
6447            .into());
6448        };
6449        let mut result = Vec::new();
6450        for kv in key_values {
6451            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6452                vv.iter().map(|x| x.to_string()).collect()
6453            } else {
6454                vec![kv.to_string()]
6455            };
6456            for k in key_strings {
6457                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6458            }
6459        }
6460        Ok(PerlValue::array(result))
6461    }
6462
6463    /// Single-key write for a hash slice container (hash ref or package hash name).
6464    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
6465    pub(crate) fn assign_hash_slice_one_key(
6466        &mut self,
6467        container: PerlValue,
6468        key: &str,
6469        val: PerlValue,
6470        line: usize,
6471    ) -> Result<PerlValue, FlowOrError> {
6472        if let Some(r) = container.as_hash_ref() {
6473            r.write().insert(key.to_string(), val);
6474            return Ok(PerlValue::UNDEF);
6475        }
6476        if let Some(name) = container.as_hash_binding_name() {
6477            self.touch_env_hash(&name);
6478            self.scope
6479                .set_hash_element(&name, key, val)
6480                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6481            return Ok(PerlValue::UNDEF);
6482        }
6483        if let Some(s) = container.as_str() {
6484            self.touch_env_hash(&s);
6485            if self.strict_refs {
6486                return Err(PerlError::runtime(
6487                    format!(
6488                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6489                        s
6490                    ),
6491                    line,
6492                )
6493                .into());
6494            }
6495            self.scope
6496                .set_hash_element(&s, key, val)
6497                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6498            return Ok(PerlValue::UNDEF);
6499        }
6500        Err(PerlError::runtime(
6501            "Hash slice assignment needs a hash or hash reference value",
6502            line,
6503        )
6504        .into())
6505    }
6506
6507    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
6508    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
6509    pub(crate) fn assign_named_hash_slice(
6510        &mut self,
6511        hash: &str,
6512        key_values: Vec<PerlValue>,
6513        val: PerlValue,
6514        line: usize,
6515    ) -> Result<PerlValue, FlowOrError> {
6516        self.touch_env_hash(hash);
6517        let mut ks: Vec<String> = Vec::new();
6518        for kv in key_values {
6519            if let Some(vv) = kv.as_array_vec() {
6520                ks.extend(vv.iter().map(|x| x.to_string()));
6521            } else {
6522                ks.push(kv.to_string());
6523            }
6524        }
6525        if ks.is_empty() {
6526            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6527        }
6528        let items = val.to_list();
6529        for (i, k) in ks.iter().enumerate() {
6530            let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6531            self.scope
6532                .set_hash_element(hash, k, v)
6533                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6534        }
6535        Ok(PerlValue::UNDEF)
6536    }
6537
6538    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
6539    pub(crate) fn assign_hash_slice_deref(
6540        &mut self,
6541        container: PerlValue,
6542        key_values: Vec<PerlValue>,
6543        val: PerlValue,
6544        line: usize,
6545    ) -> Result<PerlValue, FlowOrError> {
6546        let mut ks: Vec<String> = Vec::new();
6547        for kv in key_values {
6548            if let Some(vv) = kv.as_array_vec() {
6549                ks.extend(vv.iter().map(|x| x.to_string()));
6550            } else {
6551                ks.push(kv.to_string());
6552            }
6553        }
6554        if ks.is_empty() {
6555            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6556        }
6557        let items = val.to_list();
6558        if let Some(r) = container.as_hash_ref() {
6559            let mut h = r.write();
6560            for (i, k) in ks.iter().enumerate() {
6561                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6562                h.insert(k.clone(), v);
6563            }
6564            return Ok(PerlValue::UNDEF);
6565        }
6566        if let Some(name) = container.as_hash_binding_name() {
6567            self.touch_env_hash(&name);
6568            for (i, k) in ks.iter().enumerate() {
6569                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6570                self.scope
6571                    .set_hash_element(&name, k, v)
6572                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6573            }
6574            return Ok(PerlValue::UNDEF);
6575        }
6576        if let Some(s) = container.as_str() {
6577            if self.strict_refs {
6578                return Err(PerlError::runtime(
6579                    format!(
6580                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6581                        s
6582                    ),
6583                    line,
6584                )
6585                .into());
6586            }
6587            self.touch_env_hash(&s);
6588            for (i, k) in ks.iter().enumerate() {
6589                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6590                self.scope
6591                    .set_hash_element(&s, k, v)
6592                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6593            }
6594            return Ok(PerlValue::UNDEF);
6595        }
6596        Err(PerlError::runtime(
6597            "Hash slice assignment needs a hash or hash reference value",
6598            line,
6599        )
6600        .into())
6601    }
6602
6603    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
6604    /// Perl 5 applies the compound op only to the **last** slice element.
6605    pub(crate) fn compound_assign_hash_slice_deref(
6606        &mut self,
6607        container: PerlValue,
6608        key_values: Vec<PerlValue>,
6609        op: BinOp,
6610        rhs: PerlValue,
6611        line: usize,
6612    ) -> Result<PerlValue, FlowOrError> {
6613        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6614        let last_old = old_list
6615            .to_list()
6616            .last()
6617            .cloned()
6618            .unwrap_or(PerlValue::UNDEF);
6619        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6620        let mut ks: Vec<String> = Vec::new();
6621        for kv in &key_values {
6622            if let Some(vv) = kv.as_array_vec() {
6623                ks.extend(vv.iter().map(|x| x.to_string()));
6624            } else {
6625                ks.push(kv.to_string());
6626            }
6627        }
6628        if ks.is_empty() {
6629            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6630        }
6631        let last_key = ks.last().expect("non-empty ks");
6632        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6633        Ok(new_val)
6634    }
6635
6636    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
6637    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
6638    /// the **old** value of that last element.
6639    ///
6640    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
6641    pub(crate) fn hash_slice_deref_inc_dec(
6642        &mut self,
6643        container: PerlValue,
6644        key_values: Vec<PerlValue>,
6645        kind: u8,
6646        line: usize,
6647    ) -> Result<PerlValue, FlowOrError> {
6648        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6649        let last_old = old_list
6650            .to_list()
6651            .last()
6652            .cloned()
6653            .unwrap_or(PerlValue::UNDEF);
6654        let new_val = if kind & 1 == 0 {
6655            PerlValue::integer(last_old.to_int() + 1)
6656        } else {
6657            PerlValue::integer(last_old.to_int() - 1)
6658        };
6659        let mut ks: Vec<String> = Vec::new();
6660        for kv in &key_values {
6661            if let Some(vv) = kv.as_array_vec() {
6662                ks.extend(vv.iter().map(|x| x.to_string()));
6663            } else {
6664                ks.push(kv.to_string());
6665            }
6666        }
6667        let last_key = ks.last().ok_or_else(|| {
6668            PerlError::runtime("Hash slice increment needs at least one key", line)
6669        })?;
6670        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6671        Ok(if kind < 2 { new_val } else { last_old })
6672    }
6673
6674    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[PerlValue]) -> PerlValue {
6675        self.touch_env_hash(hash);
6676        let h = self.scope.get_hash(hash);
6677        let mut result = Vec::new();
6678        for kv in key_values {
6679            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6680                vv.iter().map(|x| x.to_string()).collect()
6681            } else {
6682                vec![kv.to_string()]
6683            };
6684            for k in key_strings {
6685                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6686            }
6687        }
6688        PerlValue::array(result)
6689    }
6690
6691    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
6692    pub(crate) fn compound_assign_named_hash_slice(
6693        &mut self,
6694        hash: &str,
6695        key_values: Vec<PerlValue>,
6696        op: BinOp,
6697        rhs: PerlValue,
6698        line: usize,
6699    ) -> Result<PerlValue, FlowOrError> {
6700        let old_list = self.hash_slice_named_values(hash, &key_values);
6701        let last_old = old_list
6702            .to_list()
6703            .last()
6704            .cloned()
6705            .unwrap_or(PerlValue::UNDEF);
6706        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6707        let mut ks: Vec<String> = Vec::new();
6708        for kv in &key_values {
6709            if let Some(vv) = kv.as_array_vec() {
6710                ks.extend(vv.iter().map(|x| x.to_string()));
6711            } else {
6712                ks.push(kv.to_string());
6713            }
6714        }
6715        if ks.is_empty() {
6716            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6717        }
6718        let last_key = ks.last().expect("non-empty ks");
6719        let container = PerlValue::string(hash.to_string());
6720        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6721        Ok(new_val)
6722    }
6723
6724    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
6725    pub(crate) fn named_hash_slice_inc_dec(
6726        &mut self,
6727        hash: &str,
6728        key_values: Vec<PerlValue>,
6729        kind: u8,
6730        line: usize,
6731    ) -> Result<PerlValue, FlowOrError> {
6732        let old_list = self.hash_slice_named_values(hash, &key_values);
6733        let last_old = old_list
6734            .to_list()
6735            .last()
6736            .cloned()
6737            .unwrap_or(PerlValue::UNDEF);
6738        let new_val = if kind & 1 == 0 {
6739            PerlValue::integer(last_old.to_int() + 1)
6740        } else {
6741            PerlValue::integer(last_old.to_int() - 1)
6742        };
6743        let mut ks: Vec<String> = Vec::new();
6744        for kv in &key_values {
6745            if let Some(vv) = kv.as_array_vec() {
6746                ks.extend(vv.iter().map(|x| x.to_string()));
6747            } else {
6748                ks.push(kv.to_string());
6749            }
6750        }
6751        let last_key = ks.last().ok_or_else(|| {
6752            PerlError::runtime("Hash slice increment needs at least one key", line)
6753        })?;
6754        let container = PerlValue::string(hash.to_string());
6755        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6756        Ok(if kind < 2 { new_val } else { last_old })
6757    }
6758
6759    fn match_array_pattern_elems(
6760        &mut self,
6761        arr: &[PerlValue],
6762        elems: &[MatchArrayElem],
6763        line: usize,
6764    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6765        let has_rest = elems
6766            .iter()
6767            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
6768        let mut binds: Vec<PatternBinding> = Vec::new();
6769        let mut idx = 0usize;
6770        for (i, elem) in elems.iter().enumerate() {
6771            match elem {
6772                MatchArrayElem::Rest => {
6773                    if i != elems.len() - 1 {
6774                        return Err(PerlError::runtime(
6775                            "internal: `*` must be last in array match pattern",
6776                            line,
6777                        )
6778                        .into());
6779                    }
6780                    return Ok(Some(binds));
6781                }
6782                MatchArrayElem::RestBind(name) => {
6783                    if i != elems.len() - 1 {
6784                        return Err(PerlError::runtime(
6785                            "internal: `@name` rest bind must be last in array match pattern",
6786                            line,
6787                        )
6788                        .into());
6789                    }
6790                    let tail = arr[idx..].to_vec();
6791                    binds.push(PatternBinding::Array(name.clone(), tail));
6792                    return Ok(Some(binds));
6793                }
6794                MatchArrayElem::CaptureScalar(name) => {
6795                    if idx >= arr.len() {
6796                        return Ok(None);
6797                    }
6798                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
6799                    idx += 1;
6800                }
6801                MatchArrayElem::Expr(e) => {
6802                    if idx >= arr.len() {
6803                        return Ok(None);
6804                    }
6805                    let expected = self.eval_expr(e)?;
6806                    if !self.smartmatch_when(&arr[idx], &expected) {
6807                        return Ok(None);
6808                    }
6809                    idx += 1;
6810                }
6811            }
6812        }
6813        if !has_rest && idx != arr.len() {
6814            return Ok(None);
6815        }
6816        Ok(Some(binds))
6817    }
6818
6819    fn match_hash_pattern_pairs(
6820        &mut self,
6821        h: &IndexMap<String, PerlValue>,
6822        pairs: &[MatchHashPair],
6823        _line: usize,
6824    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6825        let mut binds = Vec::new();
6826        for pair in pairs {
6827            match pair {
6828                MatchHashPair::KeyOnly { key } => {
6829                    let ks = self.eval_expr(key)?.to_string();
6830                    if !h.contains_key(&ks) {
6831                        return Ok(None);
6832                    }
6833                }
6834                MatchHashPair::Capture { key, name } => {
6835                    let ks = self.eval_expr(key)?.to_string();
6836                    let Some(v) = h.get(&ks) else {
6837                        return Ok(None);
6838                    };
6839                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
6840                }
6841            }
6842        }
6843        Ok(Some(binds))
6844    }
6845
6846    /// Check if a block declares variables (needs its own scope frame).
6847    #[inline]
6848    fn block_needs_scope(block: &Block) -> bool {
6849        block.iter().any(|s| match &s.kind {
6850            StmtKind::My(_)
6851            | StmtKind::Our(_)
6852            | StmtKind::Local(_)
6853            | StmtKind::State(_)
6854            | StmtKind::LocalExpr { .. } => true,
6855            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
6856            _ => false,
6857        })
6858    }
6859
6860    /// Execute block, only pushing a scope frame if needed.
6861    #[inline]
6862    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
6863        if Self::block_needs_scope(block) {
6864            self.exec_block(block)
6865        } else {
6866            self.exec_block_no_scope(block)
6867        }
6868    }
6869
6870    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
6871        let t0 = self.profiler.is_some().then(std::time::Instant::now);
6872        let r = self.exec_statement_inner(stmt);
6873        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
6874            prof.on_line(&self.file, stmt.line, t0.elapsed());
6875        }
6876        r
6877    }
6878
6879    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
6880        if let Err(e) = crate::perl_signal::poll(self) {
6881            return Err(FlowOrError::Error(e));
6882        }
6883        if let Err(e) = self.drain_pending_destroys(stmt.line) {
6884            return Err(FlowOrError::Error(e));
6885        }
6886        match &stmt.kind {
6887            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
6888            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
6889            StmtKind::If {
6890                condition,
6891                body,
6892                elsifs,
6893                else_block,
6894            } => {
6895                if self.eval_boolean_rvalue_condition(condition)? {
6896                    return self.exec_block(body);
6897                }
6898                for (c, b) in elsifs {
6899                    if self.eval_boolean_rvalue_condition(c)? {
6900                        return self.exec_block(b);
6901                    }
6902                }
6903                if let Some(eb) = else_block {
6904                    return self.exec_block(eb);
6905                }
6906                Ok(PerlValue::UNDEF)
6907            }
6908            StmtKind::Unless {
6909                condition,
6910                body,
6911                else_block,
6912            } => {
6913                if !self.eval_boolean_rvalue_condition(condition)? {
6914                    return self.exec_block(body);
6915                }
6916                if let Some(eb) = else_block {
6917                    return self.exec_block(eb);
6918                }
6919                Ok(PerlValue::UNDEF)
6920            }
6921            StmtKind::While {
6922                condition,
6923                body,
6924                label,
6925                continue_block,
6926            } => {
6927                'outer: loop {
6928                    if !self.eval_boolean_rvalue_condition(condition)? {
6929                        break;
6930                    }
6931                    'inner: loop {
6932                        match self.exec_block_smart(body) {
6933                            Ok(_) => break 'inner,
6934                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6935                                if l == label || l.is_none() =>
6936                            {
6937                                break 'outer;
6938                            }
6939                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6940                                if l == label || l.is_none() =>
6941                            {
6942                                if let Some(cb) = continue_block {
6943                                    let _ = self.exec_block_smart(cb);
6944                                }
6945                                continue 'outer;
6946                            }
6947                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6948                                if l == label || l.is_none() =>
6949                            {
6950                                continue 'inner;
6951                            }
6952                            Err(e) => return Err(e),
6953                        }
6954                    }
6955                    if let Some(cb) = continue_block {
6956                        let _ = self.exec_block_smart(cb);
6957                    }
6958                }
6959                Ok(PerlValue::UNDEF)
6960            }
6961            StmtKind::Until {
6962                condition,
6963                body,
6964                label,
6965                continue_block,
6966            } => {
6967                'outer: loop {
6968                    if self.eval_boolean_rvalue_condition(condition)? {
6969                        break;
6970                    }
6971                    'inner: loop {
6972                        match self.exec_block(body) {
6973                            Ok(_) => break 'inner,
6974                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6975                                if l == label || l.is_none() =>
6976                            {
6977                                break 'outer;
6978                            }
6979                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6980                                if l == label || l.is_none() =>
6981                            {
6982                                if let Some(cb) = continue_block {
6983                                    let _ = self.exec_block_smart(cb);
6984                                }
6985                                continue 'outer;
6986                            }
6987                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6988                                if l == label || l.is_none() =>
6989                            {
6990                                continue 'inner;
6991                            }
6992                            Err(e) => return Err(e),
6993                        }
6994                    }
6995                    if let Some(cb) = continue_block {
6996                        let _ = self.exec_block_smart(cb);
6997                    }
6998                }
6999                Ok(PerlValue::UNDEF)
7000            }
7001            StmtKind::DoWhile { body, condition } => {
7002                loop {
7003                    self.exec_block(body)?;
7004                    if !self.eval_boolean_rvalue_condition(condition)? {
7005                        break;
7006                    }
7007                }
7008                Ok(PerlValue::UNDEF)
7009            }
7010            StmtKind::For {
7011                init,
7012                condition,
7013                step,
7014                body,
7015                label,
7016                continue_block,
7017            } => {
7018                self.scope_push_hook();
7019                if let Some(init) = init {
7020                    self.exec_statement(init)?;
7021                }
7022                'outer: loop {
7023                    if let Some(cond) = condition {
7024                        if !self.eval_boolean_rvalue_condition(cond)? {
7025                            break;
7026                        }
7027                    }
7028                    'inner: loop {
7029                        match self.exec_block_smart(body) {
7030                            Ok(_) => break 'inner,
7031                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7032                                if l == label || l.is_none() =>
7033                            {
7034                                break 'outer;
7035                            }
7036                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7037                                if l == label || l.is_none() =>
7038                            {
7039                                if let Some(cb) = continue_block {
7040                                    let _ = self.exec_block_smart(cb);
7041                                }
7042                                if let Some(step) = step {
7043                                    self.eval_expr(step)?;
7044                                }
7045                                continue 'outer;
7046                            }
7047                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7048                                if l == label || l.is_none() =>
7049                            {
7050                                continue 'inner;
7051                            }
7052                            Err(e) => {
7053                                self.scope_pop_hook();
7054                                return Err(e);
7055                            }
7056                        }
7057                    }
7058                    if let Some(cb) = continue_block {
7059                        let _ = self.exec_block_smart(cb);
7060                    }
7061                    if let Some(step) = step {
7062                        self.eval_expr(step)?;
7063                    }
7064                }
7065                self.scope_pop_hook();
7066                Ok(PerlValue::UNDEF)
7067            }
7068            StmtKind::Foreach {
7069                var,
7070                list,
7071                body,
7072                label,
7073                continue_block,
7074            } => {
7075                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
7076                let items = list_val.to_list();
7077                self.scope_push_hook();
7078                self.scope.declare_scalar(var, PerlValue::UNDEF);
7079                self.english_note_lexical_scalar(var);
7080                let mut i = 0usize;
7081                'outer: while i < items.len() {
7082                    self.scope
7083                        .set_scalar(var, items[i].clone())
7084                        .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
7085                    'inner: loop {
7086                        match self.exec_block_smart(body) {
7087                            Ok(_) => break 'inner,
7088                            Err(FlowOrError::Flow(Flow::Last(ref l)))
7089                                if l == label || l.is_none() =>
7090                            {
7091                                break 'outer;
7092                            }
7093                            Err(FlowOrError::Flow(Flow::Next(ref l)))
7094                                if l == label || l.is_none() =>
7095                            {
7096                                if let Some(cb) = continue_block {
7097                                    let _ = self.exec_block_smart(cb);
7098                                }
7099                                i += 1;
7100                                continue 'outer;
7101                            }
7102                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
7103                                if l == label || l.is_none() =>
7104                            {
7105                                continue 'inner;
7106                            }
7107                            Err(e) => {
7108                                self.scope_pop_hook();
7109                                return Err(e);
7110                            }
7111                        }
7112                    }
7113                    if let Some(cb) = continue_block {
7114                        let _ = self.exec_block_smart(cb);
7115                    }
7116                    i += 1;
7117                }
7118                self.scope_pop_hook();
7119                Ok(PerlValue::UNDEF)
7120            }
7121            StmtKind::SubDecl {
7122                name,
7123                params,
7124                body,
7125                prototype,
7126            } => {
7127                let key = self.qualify_sub_key(name);
7128                let captured = self.scope.capture();
7129                let closure_env = if captured.is_empty() {
7130                    None
7131                } else {
7132                    Some(captured)
7133                };
7134                let mut sub = PerlSub {
7135                    name: name.clone(),
7136                    params: params.clone(),
7137                    body: body.clone(),
7138                    closure_env,
7139                    prototype: prototype.clone(),
7140                    fib_like: None,
7141                };
7142                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
7143                self.subs.insert(key, Arc::new(sub));
7144                Ok(PerlValue::UNDEF)
7145            }
7146            StmtKind::StructDecl { def } => {
7147                if self.struct_defs.contains_key(&def.name) {
7148                    return Err(PerlError::runtime(
7149                        format!("duplicate struct `{}`", def.name),
7150                        stmt.line,
7151                    )
7152                    .into());
7153                }
7154                self.struct_defs
7155                    .insert(def.name.clone(), Arc::new(def.clone()));
7156                Ok(PerlValue::UNDEF)
7157            }
7158            StmtKind::EnumDecl { def } => {
7159                if self.enum_defs.contains_key(&def.name) {
7160                    return Err(PerlError::runtime(
7161                        format!("duplicate enum `{}`", def.name),
7162                        stmt.line,
7163                    )
7164                    .into());
7165                }
7166                self.enum_defs
7167                    .insert(def.name.clone(), Arc::new(def.clone()));
7168                Ok(PerlValue::UNDEF)
7169            }
7170            StmtKind::ClassDecl { def } => {
7171                if self.class_defs.contains_key(&def.name) {
7172                    return Err(PerlError::runtime(
7173                        format!("duplicate class `{}`", def.name),
7174                        stmt.line,
7175                    )
7176                    .into());
7177                }
7178                // Final class enforcement: prevent subclassing
7179                for parent_name in &def.extends {
7180                    if let Some(parent_def) = self.class_defs.get(parent_name) {
7181                        if parent_def.is_final {
7182                            return Err(PerlError::runtime(
7183                                format!("cannot extend final class `{}`", parent_name),
7184                                stmt.line,
7185                            )
7186                            .into());
7187                        }
7188                        // Final method enforcement: prevent overriding
7189                        for m in &def.methods {
7190                            if let Some(parent_method) = parent_def.method(&m.name) {
7191                                if parent_method.is_final {
7192                                    return Err(PerlError::runtime(
7193                                        format!(
7194                                            "cannot override final method `{}` from class `{}`",
7195                                            m.name, parent_name
7196                                        ),
7197                                        stmt.line,
7198                                    )
7199                                    .into());
7200                                }
7201                            }
7202                        }
7203                    }
7204                }
7205                // Trait contract enforcement + default method inheritance
7206                let mut def = def.clone();
7207                for trait_name in &def.implements.clone() {
7208                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
7209                        for required in trait_def.required_methods() {
7210                            let has_method = def.methods.iter().any(|m| m.name == required.name);
7211                            if !has_method {
7212                                return Err(PerlError::runtime(
7213                                    format!(
7214                                        "class `{}` implements trait `{}` but does not define required method `{}`",
7215                                        def.name, trait_name, required.name
7216                                    ),
7217                                    stmt.line,
7218                                )
7219                                .into());
7220                            }
7221                        }
7222                        // Inherit default methods from trait (methods with bodies)
7223                        for tm in &trait_def.methods {
7224                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
7225                                def.methods.push(tm.clone());
7226                            }
7227                        }
7228                    }
7229                }
7230                // Abstract method enforcement: concrete subclasses must implement
7231                // all abstract methods (body-less methods) from abstract parents
7232                if !def.is_abstract {
7233                    for parent_name in &def.extends.clone() {
7234                        if let Some(parent_def) = self.class_defs.get(parent_name) {
7235                            if parent_def.is_abstract {
7236                                for m in &parent_def.methods {
7237                                    if m.body.is_none()
7238                                        && !def.methods.iter().any(|dm| dm.name == m.name)
7239                                    {
7240                                        return Err(PerlError::runtime(
7241                                            format!(
7242                                                "class `{}` must implement abstract method `{}` from `{}`",
7243                                                def.name, m.name, parent_name
7244                                            ),
7245                                            stmt.line,
7246                                        )
7247                                        .into());
7248                                    }
7249                                }
7250                            }
7251                        }
7252                    }
7253                }
7254                // Initialize static fields
7255                for sf in &def.static_fields {
7256                    let val = if let Some(ref expr) = sf.default {
7257                        self.eval_expr(expr)?
7258                    } else {
7259                        PerlValue::UNDEF
7260                    };
7261                    let key = format!("{}::{}", def.name, sf.name);
7262                    self.scope.declare_scalar(&key, val);
7263                }
7264                // Register class methods into self.subs so method dispatch finds them.
7265                for m in &def.methods {
7266                    if let Some(ref body) = m.body {
7267                        let fq = format!("{}::{}", def.name, m.name);
7268                        let sub = Arc::new(PerlSub {
7269                            name: fq.clone(),
7270                            params: m.params.clone(),
7271                            body: body.clone(),
7272                            closure_env: None,
7273                            prototype: None,
7274                            fib_like: None,
7275                        });
7276                        self.subs.insert(fq, sub);
7277                    }
7278                }
7279                // Set @ClassName::ISA so MRO/isa resolution works.
7280                if !def.extends.is_empty() {
7281                    let isa_key = format!("{}::ISA", def.name);
7282                    let parents: Vec<PerlValue> = def
7283                        .extends
7284                        .iter()
7285                        .map(|p| PerlValue::string(p.clone()))
7286                        .collect();
7287                    self.scope.declare_array(&isa_key, parents);
7288                }
7289                self.class_defs.insert(def.name.clone(), Arc::new(def));
7290                Ok(PerlValue::UNDEF)
7291            }
7292            StmtKind::TraitDecl { def } => {
7293                if self.trait_defs.contains_key(&def.name) {
7294                    return Err(PerlError::runtime(
7295                        format!("duplicate trait `{}`", def.name),
7296                        stmt.line,
7297                    )
7298                    .into());
7299                }
7300                self.trait_defs
7301                    .insert(def.name.clone(), Arc::new(def.clone()));
7302                Ok(PerlValue::UNDEF)
7303            }
7304            StmtKind::My(decls) | StmtKind::Our(decls) => {
7305                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
7306                // For list assignment my ($a, $b) = (10, 20), distribute elements.
7307                // All decls share the same initializer in the AST (parser clones it).
7308                if decls.len() > 1 && decls[0].initializer.is_some() {
7309                    let val = self.eval_expr_ctx(
7310                        decls[0].initializer.as_ref().unwrap(),
7311                        WantarrayCtx::List,
7312                    )?;
7313                    let items = val.to_list();
7314                    let mut idx = 0;
7315                    for decl in decls {
7316                        match decl.sigil {
7317                            Sigil::Scalar => {
7318                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7319                                let skey = if is_our {
7320                                    self.stash_scalar_name_for_package(&decl.name)
7321                                } else {
7322                                    decl.name.clone()
7323                                };
7324                                self.scope.declare_scalar_frozen(
7325                                    &skey,
7326                                    v,
7327                                    decl.frozen,
7328                                    decl.type_annotation.clone(),
7329                                )?;
7330                                self.english_note_lexical_scalar(&decl.name);
7331                                if is_our {
7332                                    self.note_our_scalar(&decl.name);
7333                                }
7334                                idx += 1;
7335                            }
7336                            Sigil::Array => {
7337                                // Array slurps remaining elements
7338                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7339                                idx = items.len();
7340                                if is_our {
7341                                    self.record_exporter_our_array_name(&decl.name, &rest);
7342                                }
7343                                let aname = self.stash_array_name_for_package(&decl.name);
7344                                self.scope.declare_array(&aname, rest);
7345                            }
7346                            Sigil::Hash => {
7347                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7348                                idx = items.len();
7349                                let mut map = IndexMap::new();
7350                                let mut i = 0;
7351                                while i + 1 < rest.len() {
7352                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7353                                    i += 2;
7354                                }
7355                                self.scope.declare_hash(&decl.name, map);
7356                            }
7357                            Sigil::Typeglob => {
7358                                return Err(PerlError::runtime(
7359                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
7360                                    stmt.line,
7361                                )
7362                                .into());
7363                            }
7364                        }
7365                    }
7366                } else {
7367                    // Single decl or no initializer
7368                    for decl in decls {
7369                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
7370                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
7371                        // compound op reads the lhs (see system Exporter.pm).
7372                        let compound_init = decl
7373                            .initializer
7374                            .as_ref()
7375                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
7376
7377                        if compound_init {
7378                            match decl.sigil {
7379                                Sigil::Typeglob => {
7380                                    return Err(PerlError::runtime(
7381                                        "compound assignment on typeglob declaration is not supported",
7382                                        stmt.line,
7383                                    )
7384                                    .into());
7385                                }
7386                                Sigil::Scalar => {
7387                                    let skey = if is_our {
7388                                        self.stash_scalar_name_for_package(&decl.name)
7389                                    } else {
7390                                        decl.name.clone()
7391                                    };
7392                                    self.scope.declare_scalar_frozen(
7393                                        &skey,
7394                                        PerlValue::UNDEF,
7395                                        decl.frozen,
7396                                        decl.type_annotation.clone(),
7397                                    )?;
7398                                    self.english_note_lexical_scalar(&decl.name);
7399                                    if is_our {
7400                                        self.note_our_scalar(&decl.name);
7401                                    }
7402                                    let init = decl.initializer.as_ref().unwrap();
7403                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7404                                }
7405                                Sigil::Array => {
7406                                    let aname = self.stash_array_name_for_package(&decl.name);
7407                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
7408                                    let init = decl.initializer.as_ref().unwrap();
7409                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7410                                    if is_our {
7411                                        let items = self.scope.get_array(&aname);
7412                                        self.record_exporter_our_array_name(&decl.name, &items);
7413                                    }
7414                                }
7415                                Sigil::Hash => {
7416                                    self.scope.declare_hash_frozen(
7417                                        &decl.name,
7418                                        IndexMap::new(),
7419                                        decl.frozen,
7420                                    );
7421                                    let init = decl.initializer.as_ref().unwrap();
7422                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7423                                }
7424                            }
7425                            continue;
7426                        }
7427
7428                        let val = if let Some(init) = &decl.initializer {
7429                            let ctx = match decl.sigil {
7430                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7431                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7432                            };
7433                            self.eval_expr_ctx(init, ctx)?
7434                        } else {
7435                            PerlValue::UNDEF
7436                        };
7437                        match decl.sigil {
7438                            Sigil::Typeglob => {
7439                                return Err(PerlError::runtime(
7440                                    "`my *FH` / typeglob declaration is not supported",
7441                                    stmt.line,
7442                                )
7443                                .into());
7444                            }
7445                            Sigil::Scalar => {
7446                                let skey = if is_our {
7447                                    self.stash_scalar_name_for_package(&decl.name)
7448                                } else {
7449                                    decl.name.clone()
7450                                };
7451                                self.scope.declare_scalar_frozen(
7452                                    &skey,
7453                                    val,
7454                                    decl.frozen,
7455                                    decl.type_annotation.clone(),
7456                                )?;
7457                                self.english_note_lexical_scalar(&decl.name);
7458                                if is_our {
7459                                    self.note_our_scalar(&decl.name);
7460                                }
7461                            }
7462                            Sigil::Array => {
7463                                let items = val.to_list();
7464                                if is_our {
7465                                    self.record_exporter_our_array_name(&decl.name, &items);
7466                                }
7467                                let aname = self.stash_array_name_for_package(&decl.name);
7468                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
7469                            }
7470                            Sigil::Hash => {
7471                                let items = val.to_list();
7472                                let mut map = IndexMap::new();
7473                                let mut i = 0;
7474                                while i + 1 < items.len() {
7475                                    let k = items[i].to_string();
7476                                    let v = items[i + 1].clone();
7477                                    map.insert(k, v);
7478                                    i += 2;
7479                                }
7480                                self.scope.declare_hash_frozen(&decl.name, map, decl.frozen);
7481                            }
7482                        }
7483                    }
7484                }
7485                Ok(PerlValue::UNDEF)
7486            }
7487            StmtKind::State(decls) => {
7488                // `state` variables persist across subroutine calls.
7489                // Key by source line + name for uniqueness.
7490                for decl in decls {
7491                    let state_key = format!("{}:{}", stmt.line, decl.name);
7492                    match decl.sigil {
7493                        Sigil::Scalar => {
7494                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
7495                                // Already initialized — declare with persisted value
7496                                self.scope.declare_scalar(&decl.name, prev);
7497                            } else {
7498                                // First encounter — evaluate initializer
7499                                let val = if let Some(init) = &decl.initializer {
7500                                    self.eval_expr(init)?
7501                                } else {
7502                                    PerlValue::UNDEF
7503                                };
7504                                self.state_vars.insert(state_key.clone(), val.clone());
7505                                self.scope.declare_scalar(&decl.name, val);
7506                            }
7507                            // Register for save-back when scope pops
7508                            if let Some(frame) = self.state_bindings_stack.last_mut() {
7509                                frame.push((decl.name.clone(), state_key));
7510                            }
7511                        }
7512                        _ => {
7513                            // For arrays/hashes, fall back to simple my-like behavior
7514                            let val = if let Some(init) = &decl.initializer {
7515                                self.eval_expr(init)?
7516                            } else {
7517                                PerlValue::UNDEF
7518                            };
7519                            match decl.sigil {
7520                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
7521                                Sigil::Hash => {
7522                                    let items = val.to_list();
7523                                    let mut map = IndexMap::new();
7524                                    let mut i = 0;
7525                                    while i + 1 < items.len() {
7526                                        map.insert(items[i].to_string(), items[i + 1].clone());
7527                                        i += 2;
7528                                    }
7529                                    self.scope.declare_hash(&decl.name, map);
7530                                }
7531                                _ => {}
7532                            }
7533                        }
7534                    }
7535                }
7536                Ok(PerlValue::UNDEF)
7537            }
7538            StmtKind::Local(decls) => {
7539                if decls.len() > 1 && decls[0].initializer.is_some() {
7540                    let val = self.eval_expr_ctx(
7541                        decls[0].initializer.as_ref().unwrap(),
7542                        WantarrayCtx::List,
7543                    )?;
7544                    let items = val.to_list();
7545                    let mut idx = 0;
7546                    for decl in decls {
7547                        match decl.sigil {
7548                            Sigil::Scalar => {
7549                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7550                                idx += 1;
7551                                self.scope.local_set_scalar(&decl.name, v)?;
7552                            }
7553                            Sigil::Array => {
7554                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7555                                idx = items.len();
7556                                self.scope.local_set_array(&decl.name, rest)?;
7557                            }
7558                            Sigil::Hash => {
7559                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7560                                idx = items.len();
7561                                if decl.name == "ENV" {
7562                                    self.materialize_env_if_needed();
7563                                }
7564                                let mut map = IndexMap::new();
7565                                let mut i = 0;
7566                                while i + 1 < rest.len() {
7567                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7568                                    i += 2;
7569                                }
7570                                self.scope.local_set_hash(&decl.name, map)?;
7571                            }
7572                            Sigil::Typeglob => {
7573                                return Err(PerlError::runtime(
7574                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
7575                                    stmt.line,
7576                                )
7577                                .into());
7578                            }
7579                        }
7580                    }
7581                    Ok(val)
7582                } else {
7583                    let mut last_val = PerlValue::UNDEF;
7584                    for decl in decls {
7585                        let val = if let Some(init) = &decl.initializer {
7586                            let ctx = match decl.sigil {
7587                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7588                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7589                            };
7590                            self.eval_expr_ctx(init, ctx)?
7591                        } else {
7592                            PerlValue::UNDEF
7593                        };
7594                        last_val = val.clone();
7595                        match decl.sigil {
7596                            Sigil::Typeglob => {
7597                                let old = self.glob_handle_alias.remove(&decl.name);
7598                                if let Some(frame) = self.glob_restore_frames.last_mut() {
7599                                    frame.push((decl.name.clone(), old));
7600                                }
7601                                if let Some(init) = &decl.initializer {
7602                                    if let ExprKind::Typeglob(rhs) = &init.kind {
7603                                        self.glob_handle_alias
7604                                            .insert(decl.name.clone(), rhs.clone());
7605                                    } else {
7606                                        return Err(PerlError::runtime(
7607                                            "local *GLOB = *OTHER — right side must be a typeglob",
7608                                            stmt.line,
7609                                        )
7610                                        .into());
7611                                    }
7612                                }
7613                            }
7614                            Sigil::Scalar => {
7615                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
7616                                // must update the interpreter's backing field too — these are
7617                                // not stored in `Scope`. Save the prior value for restoration
7618                                // on `scope_pop_hook` so the block-exit restore is visible to
7619                                // print/I/O code.
7620                                if Self::is_special_scalar_name_for_set(&decl.name) {
7621                                    let old = self.get_special_var(&decl.name);
7622                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
7623                                    {
7624                                        frame.push((decl.name.clone(), old));
7625                                    }
7626                                    self.set_special_var(&decl.name, &val)
7627                                        .map_err(|e| e.at_line(stmt.line))?;
7628                                }
7629                                self.scope.local_set_scalar(&decl.name, val)?;
7630                            }
7631                            Sigil::Array => {
7632                                self.scope.local_set_array(&decl.name, val.to_list())?;
7633                            }
7634                            Sigil::Hash => {
7635                                if decl.name == "ENV" {
7636                                    self.materialize_env_if_needed();
7637                                }
7638                                let items = val.to_list();
7639                                let mut map = IndexMap::new();
7640                                let mut i = 0;
7641                                while i + 1 < items.len() {
7642                                    let k = items[i].to_string();
7643                                    let v = items[i + 1].clone();
7644                                    map.insert(k, v);
7645                                    i += 2;
7646                                }
7647                                self.scope.local_set_hash(&decl.name, map)?;
7648                            }
7649                        }
7650                    }
7651                    Ok(last_val)
7652                }
7653            }
7654            StmtKind::LocalExpr {
7655                target,
7656                initializer,
7657            } => {
7658                let rhs_name = |init: &Expr| -> PerlResult<Option<String>> {
7659                    match &init.kind {
7660                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
7661                        _ => Err(PerlError::runtime(
7662                            "local *GLOB = *OTHER — right side must be a typeglob",
7663                            stmt.line,
7664                        )),
7665                    }
7666                };
7667                match &target.kind {
7668                    ExprKind::Typeglob(name) => {
7669                        let rhs = if let Some(init) = initializer {
7670                            rhs_name(init)?
7671                        } else {
7672                            None
7673                        };
7674                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
7675                        return Ok(PerlValue::UNDEF);
7676                    }
7677                    ExprKind::Deref {
7678                        expr,
7679                        kind: Sigil::Typeglob,
7680                    } => {
7681                        let lhs = self.eval_expr(expr)?.to_string();
7682                        let rhs = if let Some(init) = initializer {
7683                            rhs_name(init)?
7684                        } else {
7685                            None
7686                        };
7687                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7688                        return Ok(PerlValue::UNDEF);
7689                    }
7690                    ExprKind::TypeglobExpr(e) => {
7691                        let lhs = self.eval_expr(e)?.to_string();
7692                        let rhs = if let Some(init) = initializer {
7693                            rhs_name(init)?
7694                        } else {
7695                            None
7696                        };
7697                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7698                        return Ok(PerlValue::UNDEF);
7699                    }
7700                    _ => {}
7701                }
7702                let val = if let Some(init) = initializer {
7703                    let ctx = match &target.kind {
7704                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
7705                        _ => WantarrayCtx::Scalar,
7706                    };
7707                    self.eval_expr_ctx(init, ctx)?
7708                } else {
7709                    PerlValue::UNDEF
7710                };
7711                match &target.kind {
7712                    ExprKind::ScalarVar(name) => {
7713                        // `local $X = …` on a special var — see twin block in
7714                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
7715                        if Self::is_special_scalar_name_for_set(name) {
7716                            let old = self.get_special_var(name);
7717                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
7718                                frame.push((name.clone(), old));
7719                            }
7720                            self.set_special_var(name, &val)
7721                                .map_err(|e| e.at_line(stmt.line))?;
7722                        }
7723                        self.scope.local_set_scalar(name, val.clone())?;
7724                    }
7725                    ExprKind::ArrayVar(name) => {
7726                        self.scope.local_set_array(name, val.to_list())?;
7727                    }
7728                    ExprKind::HashVar(name) => {
7729                        if name == "ENV" {
7730                            self.materialize_env_if_needed();
7731                        }
7732                        let items = val.to_list();
7733                        let mut map = IndexMap::new();
7734                        let mut i = 0;
7735                        while i + 1 < items.len() {
7736                            map.insert(items[i].to_string(), items[i + 1].clone());
7737                            i += 2;
7738                        }
7739                        self.scope.local_set_hash(name, map)?;
7740                    }
7741                    ExprKind::HashElement { hash, key } => {
7742                        let ks = self.eval_expr(key)?.to_string();
7743                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
7744                    }
7745                    ExprKind::ArrayElement { array, index } => {
7746                        self.check_strict_array_var(array, stmt.line)?;
7747                        let aname = self.stash_array_name_for_package(array);
7748                        let idx = self.eval_expr(index)?.to_int();
7749                        self.scope
7750                            .local_set_array_element(&aname, idx, val.clone())?;
7751                    }
7752                    _ => {
7753                        return Err(PerlError::runtime(
7754                            format!(
7755                                "local on this lvalue is not supported yet ({:?})",
7756                                target.kind
7757                            ),
7758                            stmt.line,
7759                        )
7760                        .into());
7761                    }
7762                }
7763                Ok(val)
7764            }
7765            StmtKind::MySync(decls) => {
7766                for decl in decls {
7767                    let val = if let Some(init) = &decl.initializer {
7768                        self.eval_expr(init)?
7769                    } else {
7770                        PerlValue::UNDEF
7771                    };
7772                    match decl.sigil {
7773                        Sigil::Typeglob => {
7774                            return Err(PerlError::runtime(
7775                                "`mysync` does not support typeglob variables",
7776                                stmt.line,
7777                            )
7778                            .into());
7779                        }
7780                        Sigil::Scalar => {
7781                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
7782                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
7783                            let stored = if val.is_mysync_deque_or_heap() {
7784                                val
7785                            } else {
7786                                PerlValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(val)))
7787                            };
7788                            self.scope.declare_scalar(&decl.name, stored);
7789                        }
7790                        Sigil::Array => {
7791                            self.scope.declare_atomic_array(&decl.name, val.to_list());
7792                        }
7793                        Sigil::Hash => {
7794                            let items = val.to_list();
7795                            let mut map = IndexMap::new();
7796                            let mut i = 0;
7797                            while i + 1 < items.len() {
7798                                map.insert(items[i].to_string(), items[i + 1].clone());
7799                                i += 2;
7800                            }
7801                            self.scope.declare_atomic_hash(&decl.name, map);
7802                        }
7803                    }
7804                }
7805                Ok(PerlValue::UNDEF)
7806            }
7807            StmtKind::Package { name } => {
7808                // Minimal package support — just set a variable
7809                let _ = self
7810                    .scope
7811                    .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
7812                Ok(PerlValue::UNDEF)
7813            }
7814            StmtKind::UsePerlVersion { .. } => Ok(PerlValue::UNDEF),
7815            StmtKind::Use { .. } => {
7816                // Handled in `prepare_program_top_level` before BEGIN / main.
7817                Ok(PerlValue::UNDEF)
7818            }
7819            StmtKind::UseOverload { pairs } => {
7820                self.install_use_overload_pairs(pairs);
7821                Ok(PerlValue::UNDEF)
7822            }
7823            StmtKind::No { .. } => {
7824                // Handled in `prepare_program_top_level` (same phase as `use`).
7825                Ok(PerlValue::UNDEF)
7826            }
7827            StmtKind::Return(val) => {
7828                let v = if let Some(e) = val {
7829                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
7830                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
7831                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
7832                    self.eval_expr_ctx(e, self.wantarray_kind)?
7833                } else {
7834                    PerlValue::UNDEF
7835                };
7836                Err(Flow::Return(v).into())
7837            }
7838            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
7839            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
7840            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
7841            StmtKind::Block(block) => self.exec_block(block),
7842            StmtKind::Begin(_)
7843            | StmtKind::UnitCheck(_)
7844            | StmtKind::Check(_)
7845            | StmtKind::Init(_)
7846            | StmtKind::End(_) => Ok(PerlValue::UNDEF),
7847            StmtKind::Empty => Ok(PerlValue::UNDEF),
7848            StmtKind::Goto { target } => {
7849                // goto &sub — tail call
7850                if let ExprKind::SubroutineRef(name) = &target.kind {
7851                    return Err(Flow::GotoSub(name.clone()).into());
7852                }
7853                Err(PerlError::runtime("goto reached outside goto-aware block", stmt.line).into())
7854            }
7855            StmtKind::EvalTimeout { timeout, body } => {
7856                let secs = self.eval_expr(timeout)?.to_number();
7857                self.eval_timeout_block(body, secs, stmt.line)
7858            }
7859            StmtKind::Tie {
7860                target,
7861                class,
7862                args,
7863            } => {
7864                let kind = match &target {
7865                    TieTarget::Scalar(_) => 0u8,
7866                    TieTarget::Array(_) => 1u8,
7867                    TieTarget::Hash(_) => 2u8,
7868                };
7869                let name = match &target {
7870                    TieTarget::Scalar(s) => s.as_str(),
7871                    TieTarget::Array(a) => a.as_str(),
7872                    TieTarget::Hash(h) => h.as_str(),
7873                };
7874                let mut vals = vec![self.eval_expr(class)?];
7875                for a in args {
7876                    vals.push(self.eval_expr(a)?);
7877                }
7878                self.tie_execute(kind, name, vals, stmt.line)
7879                    .map_err(Into::into)
7880            }
7881            StmtKind::TryCatch {
7882                try_block,
7883                catch_var,
7884                catch_block,
7885                finally_block,
7886            } => match self.exec_block(try_block) {
7887                Ok(v) => {
7888                    if let Some(fb) = finally_block {
7889                        self.exec_block(fb)?;
7890                    }
7891                    Ok(v)
7892                }
7893                Err(FlowOrError::Error(e)) => {
7894                    if matches!(e.kind, ErrorKind::Exit(_)) {
7895                        return Err(FlowOrError::Error(e));
7896                    }
7897                    self.scope_push_hook();
7898                    self.scope
7899                        .declare_scalar(catch_var, PerlValue::string(e.to_string()));
7900                    self.english_note_lexical_scalar(catch_var);
7901                    let r = self.exec_block(catch_block);
7902                    self.scope_pop_hook();
7903                    if let Some(fb) = finally_block {
7904                        self.exec_block(fb)?;
7905                    }
7906                    r
7907                }
7908                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
7909            },
7910            StmtKind::Given { topic, body } => self.exec_given(topic, body),
7911            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(PerlError::runtime(
7912                "when/default may only appear inside a given block",
7913                stmt.line,
7914            )
7915            .into()),
7916            StmtKind::FormatDecl { .. } => {
7917                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
7918                Ok(PerlValue::UNDEF)
7919            }
7920            StmtKind::Continue(block) => self.exec_block_smart(block),
7921        }
7922    }
7923
7924    #[inline]
7925    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
7926        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
7927    }
7928
7929    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
7930    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
7931    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
7932    pub(crate) fn scalar_compound_assign_scalar_target(
7933        &mut self,
7934        name: &str,
7935        op: BinOp,
7936        rhs: PerlValue,
7937    ) -> Result<PerlValue, PerlError> {
7938        if op == BinOp::Concat {
7939            return self.scope.scalar_concat_inplace(name, &rhs);
7940        }
7941        Ok(self
7942            .scope
7943            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs)))
7944    }
7945
7946    fn compound_scalar_binop(old: &PerlValue, op: BinOp, rhs: &PerlValue) -> PerlValue {
7947        match op {
7948            BinOp::Add => {
7949                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7950                    PerlValue::integer(a.wrapping_add(b))
7951                } else {
7952                    PerlValue::float(old.to_number() + rhs.to_number())
7953                }
7954            }
7955            BinOp::Sub => {
7956                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7957                    PerlValue::integer(a.wrapping_sub(b))
7958                } else {
7959                    PerlValue::float(old.to_number() - rhs.to_number())
7960                }
7961            }
7962            BinOp::Mul => {
7963                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7964                    PerlValue::integer(a.wrapping_mul(b))
7965                } else {
7966                    PerlValue::float(old.to_number() * rhs.to_number())
7967                }
7968            }
7969            BinOp::BitAnd => {
7970                if let Some(s) = crate::value::set_intersection(old, rhs) {
7971                    s
7972                } else {
7973                    PerlValue::integer(old.to_int() & rhs.to_int())
7974                }
7975            }
7976            BinOp::BitOr => {
7977                if let Some(s) = crate::value::set_union(old, rhs) {
7978                    s
7979                } else {
7980                    PerlValue::integer(old.to_int() | rhs.to_int())
7981                }
7982            }
7983            BinOp::BitXor => PerlValue::integer(old.to_int() ^ rhs.to_int()),
7984            BinOp::ShiftLeft => PerlValue::integer(old.to_int() << rhs.to_int()),
7985            BinOp::ShiftRight => PerlValue::integer(old.to_int() >> rhs.to_int()),
7986            BinOp::Div => PerlValue::float(old.to_number() / rhs.to_number()),
7987            BinOp::Mod => PerlValue::float(old.to_number() % rhs.to_number()),
7988            BinOp::Pow => PerlValue::float(old.to_number().powf(rhs.to_number())),
7989            BinOp::LogOr => {
7990                if old.is_true() {
7991                    old.clone()
7992                } else {
7993                    rhs.clone()
7994                }
7995            }
7996            BinOp::DefinedOr => {
7997                if !old.is_undef() {
7998                    old.clone()
7999                } else {
8000                    rhs.clone()
8001                }
8002            }
8003            BinOp::LogAnd => {
8004                if old.is_true() {
8005                    rhs.clone()
8006                } else {
8007                    old.clone()
8008                }
8009            }
8010            _ => PerlValue::float(old.to_number() + rhs.to_number()),
8011        }
8012    }
8013
8014    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
8015    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
8016    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
8017    fn eval_hash_slice_key_components(
8018        &mut self,
8019        key_expr: &Expr,
8020    ) -> Result<Vec<String>, FlowOrError> {
8021        let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
8022            self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
8023        } else {
8024            self.eval_expr(key_expr)?
8025        };
8026        if let Some(vv) = v.as_array_vec() {
8027            Ok(vv.iter().map(|x| x.to_string()).collect())
8028        } else {
8029            Ok(vec![v.to_string()])
8030        }
8031    }
8032
8033    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
8034    pub(crate) fn symbolic_deref(
8035        &mut self,
8036        val: PerlValue,
8037        kind: Sigil,
8038        line: usize,
8039    ) -> ExecResult {
8040        match kind {
8041            Sigil::Scalar => {
8042                if let Some(name) = val.as_scalar_binding_name() {
8043                    return Ok(self.get_special_var(&name));
8044                }
8045                if let Some(r) = val.as_scalar_ref() {
8046                    return Ok(r.read().clone());
8047                }
8048                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
8049                if let Some(r) = val.as_array_ref() {
8050                    return Ok(PerlValue::array(r.read().clone()));
8051                }
8052                if let Some(name) = val.as_array_binding_name() {
8053                    return Ok(PerlValue::array(self.scope.get_array(&name)));
8054                }
8055                if let Some(r) = val.as_hash_ref() {
8056                    return Ok(PerlValue::hash(r.read().clone()));
8057                }
8058                if let Some(name) = val.as_hash_binding_name() {
8059                    self.touch_env_hash(&name);
8060                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
8061                }
8062                if let Some(s) = val.as_str() {
8063                    if self.strict_refs {
8064                        return Err(PerlError::runtime(
8065                            format!(
8066                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
8067                                s
8068                            ),
8069                            line,
8070                        )
8071                        .into());
8072                    }
8073                    return Ok(self.get_special_var(&s));
8074                }
8075                Err(PerlError::runtime("Can't dereference non-reference as scalar", line).into())
8076            }
8077            Sigil::Array => {
8078                if let Some(r) = val.as_array_ref() {
8079                    return Ok(PerlValue::array(r.read().clone()));
8080                }
8081                if let Some(name) = val.as_array_binding_name() {
8082                    return Ok(PerlValue::array(self.scope.get_array(&name)));
8083                }
8084                if let Some(s) = val.as_str() {
8085                    if self.strict_refs {
8086                        return Err(PerlError::runtime(
8087                            format!(
8088                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8089                                s
8090                            ),
8091                            line,
8092                        )
8093                        .into());
8094                    }
8095                    return Ok(PerlValue::array(self.scope.get_array(&s)));
8096                }
8097                Err(PerlError::runtime("Can't dereference non-reference as array", line).into())
8098            }
8099            Sigil::Hash => {
8100                if let Some(r) = val.as_hash_ref() {
8101                    return Ok(PerlValue::hash(r.read().clone()));
8102                }
8103                if let Some(name) = val.as_hash_binding_name() {
8104                    self.touch_env_hash(&name);
8105                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
8106                }
8107                if let Some(s) = val.as_str() {
8108                    if self.strict_refs {
8109                        return Err(PerlError::runtime(
8110                            format!(
8111                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8112                                s
8113                            ),
8114                            line,
8115                        )
8116                        .into());
8117                    }
8118                    self.touch_env_hash(&s);
8119                    return Ok(PerlValue::hash(self.scope.get_hash(&s)));
8120                }
8121                Err(PerlError::runtime("Can't dereference non-reference as hash", line).into())
8122            }
8123            Sigil::Typeglob => {
8124                if let Some(s) = val.as_str() {
8125                    return Ok(PerlValue::string(self.resolve_io_handle_name(&s)));
8126                }
8127                Err(PerlError::runtime("Can't dereference non-reference as typeglob", line).into())
8128            }
8129        }
8130    }
8131
8132    /// `qq` list join expects a plain array; if a bare [`PerlValue::array_ref`] reaches join, peel
8133    /// one level so elements stringify like Perl (`"@$r"`).
8134    #[inline]
8135    pub(crate) fn peel_array_ref_for_list_join(&self, v: PerlValue) -> PerlValue {
8136        if let Some(r) = v.as_array_ref() {
8137            return PerlValue::array(r.read().clone());
8138        }
8139        v
8140    }
8141
8142    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
8143    pub(crate) fn make_array_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
8144        if let Some(a) = val.as_array_ref() {
8145            return Ok(PerlValue::array_ref(Arc::clone(&a)));
8146        }
8147        if let Some(name) = val.as_array_binding_name() {
8148            return Ok(PerlValue::array_binding_ref(name));
8149        }
8150        if let Some(s) = val.as_str() {
8151            if self.strict_refs {
8152                return Err(PerlError::runtime(
8153                    format!(
8154                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
8155                        s
8156                    ),
8157                    line,
8158                )
8159                .into());
8160            }
8161            return Ok(PerlValue::array_binding_ref(s.to_string()));
8162        }
8163        if let Some(r) = val.as_scalar_ref() {
8164            let inner = r.read().clone();
8165            return self.make_array_ref_alias(inner, line);
8166        }
8167        Err(PerlError::runtime("Can't make array reference from value", line).into())
8168    }
8169
8170    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
8171    pub(crate) fn make_hash_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
8172        if let Some(h) = val.as_hash_ref() {
8173            return Ok(PerlValue::hash_ref(Arc::clone(&h)));
8174        }
8175        if let Some(name) = val.as_hash_binding_name() {
8176            return Ok(PerlValue::hash_binding_ref(name));
8177        }
8178        if let Some(s) = val.as_str() {
8179            if self.strict_refs {
8180                return Err(PerlError::runtime(
8181                    format!(
8182                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8183                        s
8184                    ),
8185                    line,
8186                )
8187                .into());
8188            }
8189            return Ok(PerlValue::hash_binding_ref(s.to_string()));
8190        }
8191        if let Some(r) = val.as_scalar_ref() {
8192            let inner = r.read().clone();
8193            return self.make_hash_ref_alias(inner, line);
8194        }
8195        Err(PerlError::runtime("Can't make hash reference from value", line).into())
8196    }
8197
8198    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
8199    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
8200    pub(crate) fn process_case_escapes(s: &str) -> String {
8201        // Quick check: if no backslash, nothing to do
8202        if !s.contains('\\') {
8203            return s.to_string();
8204        }
8205        let mut result = String::with_capacity(s.len());
8206        let mut chars = s.chars().peekable();
8207        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
8208        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
8209
8210        while let Some(c) = chars.next() {
8211            if c == '\\' {
8212                match chars.peek() {
8213                    Some(&'U') => {
8214                        chars.next();
8215                        mode = Some('U');
8216                        continue;
8217                    }
8218                    Some(&'L') => {
8219                        chars.next();
8220                        mode = Some('L');
8221                        continue;
8222                    }
8223                    Some(&'Q') => {
8224                        chars.next();
8225                        mode = Some('Q');
8226                        continue;
8227                    }
8228                    Some(&'E') => {
8229                        chars.next();
8230                        mode = None;
8231                        next_char_mod = None;
8232                        continue;
8233                    }
8234                    Some(&'u') => {
8235                        chars.next();
8236                        next_char_mod = Some('u');
8237                        continue;
8238                    }
8239                    Some(&'l') => {
8240                        chars.next();
8241                        next_char_mod = Some('l');
8242                        continue;
8243                    }
8244                    _ => {}
8245                }
8246            }
8247
8248            let ch = c;
8249
8250            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
8251            if let Some(m) = next_char_mod.take() {
8252                let transformed = match m {
8253                    'u' => ch.to_uppercase().next().unwrap_or(ch),
8254                    'l' => ch.to_lowercase().next().unwrap_or(ch),
8255                    _ => ch,
8256                };
8257                result.push(transformed);
8258            } else {
8259                // Apply ongoing mode
8260                match mode {
8261                    Some('U') => {
8262                        for uc in ch.to_uppercase() {
8263                            result.push(uc);
8264                        }
8265                    }
8266                    Some('L') => {
8267                        for lc in ch.to_lowercase() {
8268                            result.push(lc);
8269                        }
8270                    }
8271                    Some('Q') => {
8272                        if !ch.is_ascii_alphanumeric() && ch != '_' {
8273                            result.push('\\');
8274                        }
8275                        result.push(ch);
8276                    }
8277                    None | Some(_) => {
8278                        result.push(ch);
8279                    }
8280                }
8281            }
8282        }
8283        result
8284    }
8285
8286    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
8287        let line = expr.line;
8288        match &expr.kind {
8289            ExprKind::Integer(n) => Ok(PerlValue::integer(*n)),
8290            ExprKind::Float(f) => Ok(PerlValue::float(*f)),
8291            ExprKind::String(s) => {
8292                let processed = Self::process_case_escapes(s);
8293                Ok(PerlValue::string(processed))
8294            }
8295            ExprKind::Bareword(s) => {
8296                if s == "__PACKAGE__" {
8297                    return Ok(PerlValue::string(self.current_package()));
8298                }
8299                if let Some(sub) = self.resolve_sub_by_name(s) {
8300                    return self.call_sub(&sub, vec![], ctx, line);
8301                }
8302                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
8303                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
8304                    return r.map_err(Into::into);
8305                }
8306                Ok(PerlValue::string(s.clone()))
8307            }
8308            ExprKind::Undef => Ok(PerlValue::UNDEF),
8309            ExprKind::MagicConst(MagicConstKind::File) => Ok(PerlValue::string(self.file.clone())),
8310            ExprKind::MagicConst(MagicConstKind::Line) => Ok(PerlValue::integer(expr.line as i64)),
8311            ExprKind::MagicConst(MagicConstKind::Sub) => {
8312                if let Some(sub) = self.current_sub_stack.last().cloned() {
8313                    Ok(PerlValue::code_ref(sub))
8314                } else {
8315                    Ok(PerlValue::UNDEF)
8316                }
8317            }
8318            ExprKind::Regex(pattern, flags) => {
8319                if ctx == WantarrayCtx::Void {
8320                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
8321                    let topic = self.scope.get_scalar("_");
8322                    let s = topic.to_string();
8323                    self.regex_match_execute(s, pattern, flags, false, "_", line)
8324                } else {
8325                    let re = self.compile_regex(pattern, flags, line)?;
8326                    Ok(PerlValue::regex(re, pattern.clone(), flags.clone()))
8327                }
8328            }
8329            ExprKind::QW(words) => Ok(PerlValue::array(
8330                words.iter().map(|w| PerlValue::string(w.clone())).collect(),
8331            )),
8332
8333            // Interpolated strings
8334            ExprKind::InterpolatedString(parts) => {
8335                let mut raw_result = String::new();
8336                for part in parts {
8337                    match part {
8338                        StringPart::Literal(s) => raw_result.push_str(s),
8339                        StringPart::ScalarVar(name) => {
8340                            self.check_strict_scalar_var(name, line)?;
8341                            let val = self.get_special_var(name);
8342                            let s = self.stringify_value(val, line)?;
8343                            raw_result.push_str(&s);
8344                        }
8345                        StringPart::ArrayVar(name) => {
8346                            self.check_strict_array_var(name, line)?;
8347                            let aname = self.stash_array_name_for_package(name);
8348                            let arr = self.scope.get_array(&aname);
8349                            let mut parts = Vec::with_capacity(arr.len());
8350                            for v in &arr {
8351                                parts.push(self.stringify_value(v.clone(), line)?);
8352                            }
8353                            let sep = self.list_separator.clone();
8354                            raw_result.push_str(&parts.join(&sep));
8355                        }
8356                        StringPart::Expr(e) => {
8357                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
8358                                self.check_strict_array_var(array, line)?;
8359                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8360                                let val = self.peel_array_ref_for_list_join(val);
8361                                let list = val.to_list();
8362                                let sep = self.list_separator.clone();
8363                                let mut parts = Vec::with_capacity(list.len());
8364                                for v in list {
8365                                    parts.push(self.stringify_value(v, line)?);
8366                                }
8367                                raw_result.push_str(&parts.join(&sep));
8368                            } else if let ExprKind::Deref {
8369                                kind: Sigil::Array, ..
8370                            } = &e.kind
8371                            {
8372                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8373                                let val = self.peel_array_ref_for_list_join(val);
8374                                let list = val.to_list();
8375                                let sep = self.list_separator.clone();
8376                                let mut parts = Vec::with_capacity(list.len());
8377                                for v in list {
8378                                    parts.push(self.stringify_value(v, line)?);
8379                                }
8380                                raw_result.push_str(&parts.join(&sep));
8381                            } else {
8382                                let val = self.eval_expr(e)?;
8383                                let s = self.stringify_value(val, line)?;
8384                                raw_result.push_str(&s);
8385                            }
8386                        }
8387                    }
8388                }
8389                let result = Self::process_case_escapes(&raw_result);
8390                Ok(PerlValue::string(result))
8391            }
8392
8393            // Variables
8394            ExprKind::ScalarVar(name) => {
8395                self.check_strict_scalar_var(name, line)?;
8396                let stor = self.tree_scalar_storage_name(name);
8397                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
8398                    let class = obj
8399                        .as_blessed_ref()
8400                        .map(|b| b.class.clone())
8401                        .unwrap_or_default();
8402                    let full = format!("{}::FETCH", class);
8403                    if let Some(sub) = self.subs.get(&full).cloned() {
8404                        return self.call_sub(&sub, vec![obj], ctx, line);
8405                    }
8406                }
8407                Ok(self.get_special_var(&stor))
8408            }
8409            ExprKind::ArrayVar(name) => {
8410                self.check_strict_array_var(name, line)?;
8411                let aname = self.stash_array_name_for_package(name);
8412                let arr = self.scope.get_array(&aname);
8413                if ctx == WantarrayCtx::List {
8414                    Ok(PerlValue::array(arr))
8415                } else {
8416                    Ok(PerlValue::integer(arr.len() as i64))
8417                }
8418            }
8419            ExprKind::HashVar(name) => {
8420                self.check_strict_hash_var(name, line)?;
8421                self.touch_env_hash(name);
8422                let h = self.scope.get_hash(name);
8423                let pv = PerlValue::hash(h);
8424                if ctx == WantarrayCtx::List {
8425                    Ok(pv)
8426                } else {
8427                    Ok(pv.scalar_context())
8428                }
8429            }
8430            ExprKind::Typeglob(name) => {
8431                let n = self.resolve_io_handle_name(name);
8432                Ok(PerlValue::string(n))
8433            }
8434            ExprKind::TypeglobExpr(e) => {
8435                let name = self.eval_expr(e)?.to_string();
8436                let n = self.resolve_io_handle_name(&name);
8437                Ok(PerlValue::string(n))
8438            }
8439            ExprKind::ArrayElement { array, index } => {
8440                self.check_strict_array_var(array, line)?;
8441                let idx = self.eval_expr(index)?.to_int();
8442                let aname = self.stash_array_name_for_package(array);
8443                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
8444                    let class = obj
8445                        .as_blessed_ref()
8446                        .map(|b| b.class.clone())
8447                        .unwrap_or_default();
8448                    let full = format!("{}::FETCH", class);
8449                    if let Some(sub) = self.subs.get(&full).cloned() {
8450                        let arg_vals = vec![obj, PerlValue::integer(idx)];
8451                        return self.call_sub(&sub, arg_vals, ctx, line);
8452                    }
8453                }
8454                Ok(self.scope.get_array_element(&aname, idx))
8455            }
8456            ExprKind::HashElement { hash, key } => {
8457                self.check_strict_hash_var(hash, line)?;
8458                let k = self.eval_expr(key)?.to_string();
8459                self.touch_env_hash(hash);
8460                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
8461                    let class = obj
8462                        .as_blessed_ref()
8463                        .map(|b| b.class.clone())
8464                        .unwrap_or_default();
8465                    let full = format!("{}::FETCH", class);
8466                    if let Some(sub) = self.subs.get(&full).cloned() {
8467                        let arg_vals = vec![obj, PerlValue::string(k)];
8468                        return self.call_sub(&sub, arg_vals, ctx, line);
8469                    }
8470                }
8471                Ok(self.scope.get_hash_element(hash, &k))
8472            }
8473            ExprKind::ArraySlice { array, indices } => {
8474                self.check_strict_array_var(array, line)?;
8475                let aname = self.stash_array_name_for_package(array);
8476                let flat = self.flatten_array_slice_index_specs(indices)?;
8477                let mut result = Vec::with_capacity(flat.len());
8478                for idx in flat {
8479                    result.push(self.scope.get_array_element(&aname, idx));
8480                }
8481                Ok(PerlValue::array(result))
8482            }
8483            ExprKind::HashSlice { hash, keys } => {
8484                self.check_strict_hash_var(hash, line)?;
8485                self.touch_env_hash(hash);
8486                let mut result = Vec::new();
8487                for key_expr in keys {
8488                    for k in self.eval_hash_slice_key_components(key_expr)? {
8489                        result.push(self.scope.get_hash_element(hash, &k));
8490                    }
8491                }
8492                Ok(PerlValue::array(result))
8493            }
8494            ExprKind::HashSliceDeref { container, keys } => {
8495                let hv = self.eval_expr(container)?;
8496                let mut key_vals = Vec::with_capacity(keys.len());
8497                for key_expr in keys {
8498                    let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
8499                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
8500                    } else {
8501                        self.eval_expr(key_expr)?
8502                    };
8503                    key_vals.push(v);
8504                }
8505                self.hash_slice_deref_values(&hv, &key_vals, line)
8506            }
8507            ExprKind::AnonymousListSlice { source, indices } => {
8508                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
8509                let items = list_val.to_list();
8510                let flat = self.flatten_array_slice_index_specs(indices)?;
8511                let mut out = Vec::with_capacity(flat.len());
8512                for idx in flat {
8513                    let i = if idx < 0 {
8514                        (items.len() as i64 + idx) as usize
8515                    } else {
8516                        idx as usize
8517                    };
8518                    out.push(items.get(i).cloned().unwrap_or(PerlValue::UNDEF));
8519                }
8520                let arr = PerlValue::array(out);
8521                if ctx != WantarrayCtx::List {
8522                    let v = arr.to_list();
8523                    Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF))
8524                } else {
8525                    Ok(arr)
8526                }
8527            }
8528
8529            // References
8530            ExprKind::ScalarRef(inner) => match &inner.kind {
8531                ExprKind::ScalarVar(name) => Ok(PerlValue::scalar_binding_ref(name.clone())),
8532                ExprKind::ArrayVar(name) => {
8533                    self.check_strict_array_var(name, line)?;
8534                    let aname = self.stash_array_name_for_package(name);
8535                    Ok(PerlValue::array_binding_ref(aname))
8536                }
8537                ExprKind::HashVar(name) => {
8538                    self.check_strict_hash_var(name, line)?;
8539                    Ok(PerlValue::hash_binding_ref(name.clone()))
8540                }
8541                ExprKind::Deref {
8542                    expr: e,
8543                    kind: Sigil::Array,
8544                } => {
8545                    let v = self.eval_expr(e)?;
8546                    self.make_array_ref_alias(v, line)
8547                }
8548                ExprKind::Deref {
8549                    expr: e,
8550                    kind: Sigil::Hash,
8551                } => {
8552                    let v = self.eval_expr(e)?;
8553                    self.make_hash_ref_alias(v, line)
8554                }
8555                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
8556                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8557                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8558                }
8559                ExprKind::HashSliceDeref { .. } => {
8560                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8561                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8562                }
8563                _ => {
8564                    let val = self.eval_expr(inner)?;
8565                    Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8566                }
8567            },
8568            ExprKind::ArrayRef(elems) => {
8569                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
8570                // variables flatten into the ref rather than collapsing to a scalar count /
8571                // flip-flop value.
8572                let mut arr = Vec::with_capacity(elems.len());
8573                for e in elems {
8574                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8575                    if let Some(vec) = v.as_array_vec() {
8576                        arr.extend(vec);
8577                    } else {
8578                        arr.push(v);
8579                    }
8580                }
8581                Ok(PerlValue::array_ref(Arc::new(RwLock::new(arr))))
8582            }
8583            ExprKind::HashRef(pairs) => {
8584                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
8585                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
8586                let mut map = IndexMap::new();
8587                for (k, v) in pairs {
8588                    let key_str = self.eval_expr(k)?.to_string();
8589                    if key_str == "__HASH_SPREAD__" {
8590                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
8591                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8592                        let items = spread.to_list();
8593                        let mut i = 0;
8594                        while i + 1 < items.len() {
8595                            map.insert(items[i].to_string(), items[i + 1].clone());
8596                            i += 2;
8597                        }
8598                    } else {
8599                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8600                        map.insert(key_str, val);
8601                    }
8602                }
8603                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map))))
8604            }
8605            ExprKind::CodeRef { params, body } => {
8606                let captured = self.scope.capture();
8607                Ok(PerlValue::code_ref(Arc::new(PerlSub {
8608                    name: "__ANON__".to_string(),
8609                    params: params.clone(),
8610                    body: body.clone(),
8611                    closure_env: Some(captured),
8612                    prototype: None,
8613                    fib_like: None,
8614                })))
8615            }
8616            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
8617            ExprKind::SubroutineCodeRef(name) => {
8618                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
8619                    PerlError::runtime(self.undefined_subroutine_resolve_message(name), line)
8620                })?;
8621                Ok(PerlValue::code_ref(sub))
8622            }
8623            ExprKind::DynamicSubCodeRef(expr) => {
8624                let name = self.eval_expr(expr)?.to_string();
8625                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
8626                    PerlError::runtime(self.undefined_subroutine_resolve_message(&name), line)
8627                })?;
8628                Ok(PerlValue::code_ref(sub))
8629            }
8630            ExprKind::Deref { expr, kind } => {
8631                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
8632                    let val = self.eval_expr(expr)?;
8633                    let n = self.array_deref_len(val, line)?;
8634                    return Ok(PerlValue::integer(n));
8635                }
8636                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
8637                    let val = self.eval_expr(expr)?;
8638                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
8639                    return Ok(h.scalar_context());
8640                }
8641                let val = self.eval_expr(expr)?;
8642                self.symbolic_deref(val, *kind, line)
8643            }
8644            ExprKind::ArrowDeref { expr, index, kind } => {
8645                match kind {
8646                    DerefKind::Array => {
8647                        let container = self.eval_arrow_array_base(expr, line)?;
8648                        if let ExprKind::List(indices) = &index.kind {
8649                            let mut out = Vec::with_capacity(indices.len());
8650                            for ix in indices {
8651                                let idx = self.eval_expr(ix)?.to_int();
8652                                out.push(self.read_arrow_array_element(
8653                                    container.clone(),
8654                                    idx,
8655                                    line,
8656                                )?);
8657                            }
8658                            let arr = PerlValue::array(out);
8659                            if ctx != WantarrayCtx::List {
8660                                let v = arr.to_list();
8661                                return Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF));
8662                            }
8663                            return Ok(arr);
8664                        }
8665                        let idx = self.eval_expr(index)?.to_int();
8666                        self.read_arrow_array_element(container, idx, line)
8667                    }
8668                    DerefKind::Hash => {
8669                        let val = self.eval_arrow_hash_base(expr, line)?;
8670                        let key = self.eval_expr(index)?.to_string();
8671                        self.read_arrow_hash_element(val, key.as_str(), line)
8672                    }
8673                    DerefKind::Call => {
8674                        // $coderef->(args)
8675                        let val = self.eval_expr(expr)?;
8676                        if let ExprKind::List(ref arg_exprs) = index.kind {
8677                            let mut args = Vec::new();
8678                            for a in arg_exprs {
8679                                args.push(self.eval_expr(a)?);
8680                            }
8681                            if let Some(sub) = val.as_code_ref() {
8682                                return self.call_sub(&sub, args, ctx, line);
8683                            }
8684                            Err(PerlError::runtime("Not a code reference", line).into())
8685                        } else {
8686                            Err(PerlError::runtime("Invalid call deref", line).into())
8687                        }
8688                    }
8689                }
8690            }
8691
8692            // Binary operators
8693            ExprKind::BinOp { left, op, right } => {
8694                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
8695                match op {
8696                    BinOp::BindMatch => {
8697                        let lv = self.eval_expr(left)?;
8698                        let rv = self.eval_expr(right)?;
8699                        let s = lv.to_string();
8700                        let pat = rv.to_string();
8701                        return self.regex_match_execute(s, &pat, "", false, "_", line);
8702                    }
8703                    BinOp::BindNotMatch => {
8704                        let lv = self.eval_expr(left)?;
8705                        let rv = self.eval_expr(right)?;
8706                        let s = lv.to_string();
8707                        let pat = rv.to_string();
8708                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
8709                        return Ok(PerlValue::integer(if m.is_true() { 0 } else { 1 }));
8710                    }
8711                    BinOp::LogAnd | BinOp::LogAndWord => {
8712                        match &left.kind {
8713                            ExprKind::Regex(_, _) => {
8714                                if !self.eval_boolean_rvalue_condition(left)? {
8715                                    return Ok(PerlValue::string(String::new()));
8716                                }
8717                            }
8718                            _ => {
8719                                let lv = self.eval_expr(left)?;
8720                                if !lv.is_true() {
8721                                    return Ok(lv);
8722                                }
8723                            }
8724                        }
8725                        return match &right.kind {
8726                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8727                                if self.eval_boolean_rvalue_condition(right)? {
8728                                    1
8729                                } else {
8730                                    0
8731                                },
8732                            )),
8733                            _ => self.eval_expr(right),
8734                        };
8735                    }
8736                    BinOp::LogOr | BinOp::LogOrWord => {
8737                        match &left.kind {
8738                            ExprKind::Regex(_, _) => {
8739                                if self.eval_boolean_rvalue_condition(left)? {
8740                                    return Ok(PerlValue::integer(1));
8741                                }
8742                            }
8743                            _ => {
8744                                let lv = self.eval_expr(left)?;
8745                                if lv.is_true() {
8746                                    return Ok(lv);
8747                                }
8748                            }
8749                        }
8750                        return match &right.kind {
8751                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8752                                if self.eval_boolean_rvalue_condition(right)? {
8753                                    1
8754                                } else {
8755                                    0
8756                                },
8757                            )),
8758                            _ => self.eval_expr(right),
8759                        };
8760                    }
8761                    BinOp::DefinedOr => {
8762                        let lv = self.eval_expr(left)?;
8763                        if !lv.is_undef() {
8764                            return Ok(lv);
8765                        }
8766                        return self.eval_expr(right);
8767                    }
8768                    _ => {}
8769                }
8770                let lv = self.eval_expr(left)?;
8771                let rv = self.eval_expr(right)?;
8772                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
8773                    return r;
8774                }
8775                self.eval_binop(*op, &lv, &rv, line)
8776            }
8777
8778            // Unary
8779            ExprKind::UnaryOp { op, expr } => match op {
8780                UnaryOp::PreIncrement => {
8781                    if let ExprKind::ScalarVar(name) = &expr.kind {
8782                        self.check_strict_scalar_var(name, line)?;
8783                        let n = self.english_scalar_name(name);
8784                        return Ok(self
8785                            .scope
8786                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() + 1)));
8787                    }
8788                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8789                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8790                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8791                                *kind, true, true, line,
8792                            ));
8793                        }
8794                    }
8795                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8796                        let href = self.eval_expr(container)?;
8797                        let mut key_vals = Vec::with_capacity(keys.len());
8798                        for key_expr in keys {
8799                            key_vals.push(self.eval_expr(key_expr)?);
8800                        }
8801                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
8802                    }
8803                    if let ExprKind::ArrowDeref {
8804                        expr: arr_expr,
8805                        index,
8806                        kind: DerefKind::Array,
8807                    } = &expr.kind
8808                    {
8809                        if let ExprKind::List(indices) = &index.kind {
8810                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8811                            let mut idxs = Vec::with_capacity(indices.len());
8812                            for ix in indices {
8813                                idxs.push(self.eval_expr(ix)?.to_int());
8814                            }
8815                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
8816                        }
8817                    }
8818                    let val = self.eval_expr(expr)?;
8819                    let new_val = PerlValue::integer(val.to_int() + 1);
8820                    self.assign_value(expr, new_val.clone())?;
8821                    Ok(new_val)
8822                }
8823                UnaryOp::PreDecrement => {
8824                    if let ExprKind::ScalarVar(name) = &expr.kind {
8825                        self.check_strict_scalar_var(name, line)?;
8826                        let n = self.english_scalar_name(name);
8827                        return Ok(self
8828                            .scope
8829                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() - 1)));
8830                    }
8831                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8832                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8833                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8834                                *kind, true, false, line,
8835                            ));
8836                        }
8837                    }
8838                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8839                        let href = self.eval_expr(container)?;
8840                        let mut key_vals = Vec::with_capacity(keys.len());
8841                        for key_expr in keys {
8842                            key_vals.push(self.eval_expr(key_expr)?);
8843                        }
8844                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
8845                    }
8846                    if let ExprKind::ArrowDeref {
8847                        expr: arr_expr,
8848                        index,
8849                        kind: DerefKind::Array,
8850                    } = &expr.kind
8851                    {
8852                        if let ExprKind::List(indices) = &index.kind {
8853                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8854                            let mut idxs = Vec::with_capacity(indices.len());
8855                            for ix in indices {
8856                                idxs.push(self.eval_expr(ix)?.to_int());
8857                            }
8858                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
8859                        }
8860                    }
8861                    let val = self.eval_expr(expr)?;
8862                    let new_val = PerlValue::integer(val.to_int() - 1);
8863                    self.assign_value(expr, new_val.clone())?;
8864                    Ok(new_val)
8865                }
8866                _ => {
8867                    match op {
8868                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
8869                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
8870                                let topic = self.scope.get_scalar("_");
8871                                let rl = expr.line;
8872                                let s = topic.to_string();
8873                                let v =
8874                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
8875                                return Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }));
8876                            }
8877                        }
8878                        _ => {}
8879                    }
8880                    let val = self.eval_expr(expr)?;
8881                    match op {
8882                        UnaryOp::Negate => {
8883                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
8884                                return r;
8885                            }
8886                            if let Some(n) = val.as_integer() {
8887                                Ok(PerlValue::integer(-n))
8888                            } else {
8889                                Ok(PerlValue::float(-val.to_number()))
8890                            }
8891                        }
8892                        UnaryOp::LogNot => {
8893                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8894                                let pv = r?;
8895                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8896                            }
8897                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8898                        }
8899                        UnaryOp::BitNot => Ok(PerlValue::integer(!val.to_int())),
8900                        UnaryOp::LogNotWord => {
8901                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8902                                let pv = r?;
8903                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8904                            }
8905                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8906                        }
8907                        UnaryOp::Ref => {
8908                            if let ExprKind::ScalarVar(name) = &expr.kind {
8909                                return Ok(PerlValue::scalar_binding_ref(name.clone()));
8910                            }
8911                            Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8912                        }
8913                        _ => unreachable!(),
8914                    }
8915                }
8916            },
8917
8918            ExprKind::PostfixOp { expr, op } => {
8919                // For scalar variables, use atomic_mutate_post to hold the lock
8920                // for the entire read-modify-write (critical for mysync).
8921                if let ExprKind::ScalarVar(name) = &expr.kind {
8922                    self.check_strict_scalar_var(name, line)?;
8923                    let n = self.english_scalar_name(name);
8924                    let f: fn(&PerlValue) -> PerlValue = match op {
8925                        PostfixOp::Increment => |v| PerlValue::integer(v.to_int() + 1),
8926                        PostfixOp::Decrement => |v| PerlValue::integer(v.to_int() - 1),
8927                    };
8928                    return Ok(self.scope.atomic_mutate_post(n, f));
8929                }
8930                if let ExprKind::Deref { kind, .. } = &expr.kind {
8931                    if matches!(kind, Sigil::Array | Sigil::Hash) {
8932                        let is_inc = matches!(op, PostfixOp::Increment);
8933                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8934                            *kind, false, is_inc, line,
8935                        ));
8936                    }
8937                }
8938                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8939                    let href = self.eval_expr(container)?;
8940                    let mut key_vals = Vec::with_capacity(keys.len());
8941                    for key_expr in keys {
8942                        key_vals.push(self.eval_expr(key_expr)?);
8943                    }
8944                    let kind_byte = match op {
8945                        PostfixOp::Increment => 2u8,
8946                        PostfixOp::Decrement => 3u8,
8947                    };
8948                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
8949                }
8950                if let ExprKind::ArrowDeref {
8951                    expr: arr_expr,
8952                    index,
8953                    kind: DerefKind::Array,
8954                } = &expr.kind
8955                {
8956                    if let ExprKind::List(indices) = &index.kind {
8957                        let container = self.eval_arrow_array_base(arr_expr, line)?;
8958                        let mut idxs = Vec::with_capacity(indices.len());
8959                        for ix in indices {
8960                            idxs.push(self.eval_expr(ix)?.to_int());
8961                        }
8962                        let kind_byte = match op {
8963                            PostfixOp::Increment => 2u8,
8964                            PostfixOp::Decrement => 3u8,
8965                        };
8966                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
8967                    }
8968                }
8969                let val = self.eval_expr(expr)?;
8970                let old = val.clone();
8971                let new_val = match op {
8972                    PostfixOp::Increment => PerlValue::integer(val.to_int() + 1),
8973                    PostfixOp::Decrement => PerlValue::integer(val.to_int() - 1),
8974                };
8975                self.assign_value(expr, new_val)?;
8976                Ok(old)
8977            }
8978
8979            // Assignment
8980            ExprKind::Assign { target, value } => {
8981                if let ExprKind::Typeglob(lhs) = &target.kind {
8982                    if let ExprKind::Typeglob(rhs) = &value.kind {
8983                        self.copy_typeglob_slots(lhs, rhs, line)?;
8984                        return self.eval_expr(value);
8985                    }
8986                }
8987                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
8988                self.assign_value(target, val.clone())?;
8989                Ok(val)
8990            }
8991            ExprKind::CompoundAssign { target, op, value } => {
8992                // For scalar targets, use atomic_mutate to hold the lock.
8993                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
8994                if let ExprKind::ScalarVar(name) = &target.kind {
8995                    self.check_strict_scalar_var(name, line)?;
8996                    let n = self.english_scalar_name(name);
8997                    let op = *op;
8998                    let rhs = match op {
8999                        BinOp::LogOr => {
9000                            let old = self.scope.get_scalar(n);
9001                            if old.is_true() {
9002                                return Ok(old);
9003                            }
9004                            self.eval_expr(value)?
9005                        }
9006                        BinOp::DefinedOr => {
9007                            let old = self.scope.get_scalar(n);
9008                            if !old.is_undef() {
9009                                return Ok(old);
9010                            }
9011                            self.eval_expr(value)?
9012                        }
9013                        BinOp::LogAnd => {
9014                            let old = self.scope.get_scalar(n);
9015                            if !old.is_true() {
9016                                return Ok(old);
9017                            }
9018                            self.eval_expr(value)?
9019                        }
9020                        _ => self.eval_expr(value)?,
9021                    };
9022                    return Ok(self.scalar_compound_assign_scalar_target(n, op, rhs)?);
9023                }
9024                let rhs = self.eval_expr(value)?;
9025                // For hash element targets: $h{key} += 1
9026                if let ExprKind::HashElement { hash, key } = &target.kind {
9027                    self.check_strict_hash_var(hash, line)?;
9028                    let k = self.eval_expr(key)?.to_string();
9029                    let op = *op;
9030                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
9031                        BinOp::Add => {
9032                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9033                                PerlValue::integer(a.wrapping_add(b))
9034                            } else {
9035                                PerlValue::float(old.to_number() + rhs.to_number())
9036                            }
9037                        }
9038                        BinOp::Sub => {
9039                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9040                                PerlValue::integer(a.wrapping_sub(b))
9041                            } else {
9042                                PerlValue::float(old.to_number() - rhs.to_number())
9043                            }
9044                        }
9045                        BinOp::Concat => {
9046                            let mut s = old.to_string();
9047                            rhs.append_to(&mut s);
9048                            PerlValue::string(s)
9049                        }
9050                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
9051                    })?);
9052                }
9053                // For array element targets: $a[i] += 1
9054                if let ExprKind::ArrayElement { array, index } = &target.kind {
9055                    self.check_strict_array_var(array, line)?;
9056                    let idx = self.eval_expr(index)?.to_int();
9057                    let op = *op;
9058                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
9059                        BinOp::Add => {
9060                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
9061                                PerlValue::integer(a.wrapping_add(b))
9062                            } else {
9063                                PerlValue::float(old.to_number() + rhs.to_number())
9064                            }
9065                        }
9066                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
9067                    })?);
9068                }
9069                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
9070                    let href = self.eval_expr(container)?;
9071                    let mut key_vals = Vec::with_capacity(keys.len());
9072                    for key_expr in keys {
9073                        key_vals.push(self.eval_expr(key_expr)?);
9074                    }
9075                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
9076                }
9077                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
9078                    if let ExprKind::Deref {
9079                        expr: inner,
9080                        kind: Sigil::Array,
9081                    } = &source.kind
9082                    {
9083                        let container = self.eval_arrow_array_base(inner, line)?;
9084                        let idxs = self.flatten_array_slice_index_specs(indices)?;
9085                        return self
9086                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
9087                    }
9088                }
9089                if let ExprKind::ArrowDeref {
9090                    expr: arr_expr,
9091                    index,
9092                    kind: DerefKind::Array,
9093                } = &target.kind
9094                {
9095                    if let ExprKind::List(indices) = &index.kind {
9096                        let container = self.eval_arrow_array_base(arr_expr, line)?;
9097                        let mut idxs = Vec::with_capacity(indices.len());
9098                        for ix in indices {
9099                            idxs.push(self.eval_expr(ix)?.to_int());
9100                        }
9101                        return self
9102                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
9103                    }
9104                }
9105                let old = self.eval_expr(target)?;
9106                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
9107                self.assign_value(target, new_val.clone())?;
9108                Ok(new_val)
9109            }
9110
9111            // Ternary
9112            ExprKind::Ternary {
9113                condition,
9114                then_expr,
9115                else_expr,
9116            } => {
9117                if self.eval_boolean_rvalue_condition(condition)? {
9118                    self.eval_expr(then_expr)
9119                } else {
9120                    self.eval_expr(else_expr)
9121                }
9122            }
9123
9124            // Range
9125            ExprKind::Range {
9126                from,
9127                to,
9128                exclusive,
9129                step,
9130            } => {
9131                if ctx == WantarrayCtx::List {
9132                    let f = self.eval_expr(from)?;
9133                    let t = self.eval_expr(to)?;
9134                    if let Some(s) = step {
9135                        let step_val = self.eval_expr(s)?.to_int();
9136                        let from_i = f.to_int();
9137                        let to_i = t.to_int();
9138                        let list = if step_val == 0 {
9139                            vec![]
9140                        } else if step_val > 0 {
9141                            (from_i..=to_i)
9142                                .step_by(step_val as usize)
9143                                .map(PerlValue::integer)
9144                                .collect()
9145                        } else {
9146                            std::iter::successors(Some(from_i), |&x| {
9147                                let next = x - step_val.abs();
9148                                if next >= to_i {
9149                                    Some(next)
9150                                } else {
9151                                    None
9152                                }
9153                            })
9154                            .map(PerlValue::integer)
9155                            .collect()
9156                        };
9157                        Ok(PerlValue::array(list))
9158                    } else {
9159                        let list = perl_list_range_expand(f, t);
9160                        Ok(PerlValue::array(list))
9161                    }
9162                } else {
9163                    let key = std::ptr::from_ref(expr) as usize;
9164                    match (&from.kind, &to.kind) {
9165                        (
9166                            ExprKind::Regex(left_pat, left_flags),
9167                            ExprKind::Regex(right_pat, right_flags),
9168                        ) => {
9169                            let dot = self.scalar_flipflop_dot_line();
9170                            let subject = self.scope.get_scalar("_").to_string();
9171                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9172                                |e| match e {
9173                                    FlowOrError::Error(err) => err,
9174                                    FlowOrError::Flow(_) => PerlError::runtime(
9175                                        "unexpected flow in regex flip-flop",
9176                                        line,
9177                                    ),
9178                                },
9179                            )?;
9180                            let right_re = self
9181                                .compile_regex(right_pat, right_flags, line)
9182                                .map_err(|e| match e {
9183                                    FlowOrError::Error(err) => err,
9184                                    FlowOrError::Flow(_) => PerlError::runtime(
9185                                        "unexpected flow in regex flip-flop",
9186                                        line,
9187                                    ),
9188                                })?;
9189                            let left_m = left_re.is_match(&subject);
9190                            let right_m = right_re.is_match(&subject);
9191                            let st = self.flip_flop_tree.entry(key).or_default();
9192                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9193                                &mut st.active,
9194                                &mut st.exclusive_left_line,
9195                                *exclusive,
9196                                dot,
9197                                left_m,
9198                                right_m,
9199                            )))
9200                        }
9201                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
9202                            let dot = self.scalar_flipflop_dot_line();
9203                            let subject = self.scope.get_scalar("_").to_string();
9204                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9205                                |e| match e {
9206                                    FlowOrError::Error(err) => err,
9207                                    FlowOrError::Flow(_) => PerlError::runtime(
9208                                        "unexpected flow in regex/eof flip-flop",
9209                                        line,
9210                                    ),
9211                                },
9212                            )?;
9213                            let left_m = left_re.is_match(&subject);
9214                            let right_m = self.eof_without_arg_is_true();
9215                            let st = self.flip_flop_tree.entry(key).or_default();
9216                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9217                                &mut st.active,
9218                                &mut st.exclusive_left_line,
9219                                *exclusive,
9220                                dot,
9221                                left_m,
9222                                right_m,
9223                            )))
9224                        }
9225                        (
9226                            ExprKind::Regex(left_pat, left_flags),
9227                            ExprKind::Integer(_) | ExprKind::Float(_),
9228                        ) => {
9229                            let dot = self.scalar_flipflop_dot_line();
9230                            let right = self.eval_expr(to)?.to_int();
9231                            let subject = self.scope.get_scalar("_").to_string();
9232                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9233                                |e| match e {
9234                                    FlowOrError::Error(err) => err,
9235                                    FlowOrError::Flow(_) => PerlError::runtime(
9236                                        "unexpected flow in regex flip-flop",
9237                                        line,
9238                                    ),
9239                                },
9240                            )?;
9241                            let left_m = left_re.is_match(&subject);
9242                            let right_m = dot == right;
9243                            let st = self.flip_flop_tree.entry(key).or_default();
9244                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9245                                &mut st.active,
9246                                &mut st.exclusive_left_line,
9247                                *exclusive,
9248                                dot,
9249                                left_m,
9250                                right_m,
9251                            )))
9252                        }
9253                        (ExprKind::Regex(left_pat, left_flags), _) => {
9254                            if let ExprKind::Eof(Some(_)) = &to.kind {
9255                                return Err(FlowOrError::Error(PerlError::runtime(
9256                                    "regex flip-flop with eof(HANDLE) is not supported",
9257                                    line,
9258                                )));
9259                            }
9260                            let dot = self.scalar_flipflop_dot_line();
9261                            let subject = self.scope.get_scalar("_").to_string();
9262                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9263                                |e| match e {
9264                                    FlowOrError::Error(err) => err,
9265                                    FlowOrError::Flow(_) => PerlError::runtime(
9266                                        "unexpected flow in regex flip-flop",
9267                                        line,
9268                                    ),
9269                                },
9270                            )?;
9271                            let left_m = left_re.is_match(&subject);
9272                            let right_m = self.eval_boolean_rvalue_condition(to)?;
9273                            let st = self.flip_flop_tree.entry(key).or_default();
9274                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9275                                &mut st.active,
9276                                &mut st.exclusive_left_line,
9277                                *exclusive,
9278                                dot,
9279                                left_m,
9280                                right_m,
9281                            )))
9282                        }
9283                        _ => {
9284                            let left = self.eval_expr(from)?.to_int();
9285                            let right = self.eval_expr(to)?.to_int();
9286                            let dot = self.scalar_flipflop_dot_line();
9287                            let st = self.flip_flop_tree.entry(key).or_default();
9288                            if !st.active {
9289                                if dot == left {
9290                                    st.active = true;
9291                                    if *exclusive {
9292                                        st.exclusive_left_line = Some(dot);
9293                                    } else {
9294                                        st.exclusive_left_line = None;
9295                                        if dot == right {
9296                                            st.active = false;
9297                                        }
9298                                    }
9299                                    return Ok(PerlValue::integer(1));
9300                                }
9301                                return Ok(PerlValue::integer(0));
9302                            }
9303                            if let Some(ll) = st.exclusive_left_line {
9304                                if dot == right && dot > ll {
9305                                    st.active = false;
9306                                    st.exclusive_left_line = None;
9307                                }
9308                            } else if dot == right {
9309                                st.active = false;
9310                            }
9311                            Ok(PerlValue::integer(1))
9312                        }
9313                    }
9314                }
9315            }
9316
9317            // Repeat
9318            ExprKind::Repeat { expr, count } => {
9319                let val = self.eval_expr(expr)?;
9320                let n = self.eval_expr(count)?.to_int().max(0) as usize;
9321                if let Some(s) = val.as_str() {
9322                    Ok(PerlValue::string(s.repeat(n)))
9323                } else if let Some(a) = val.as_array_vec() {
9324                    let mut result = Vec::with_capacity(a.len() * n);
9325                    for _ in 0..n {
9326                        result.extend(a.iter().cloned());
9327                    }
9328                    Ok(PerlValue::array(result))
9329                } else {
9330                    Ok(PerlValue::string(val.to_string().repeat(n)))
9331                }
9332            }
9333
9334            // `my $x = …` / `our` / `state` / `local` used as an expression
9335            // (e.g. `if (my $line = readline)`).  Declare each variable in the
9336            // current scope, evaluate the initializer (if any), and return the
9337            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
9338            ExprKind::MyExpr { keyword, decls } => {
9339                // Build a temporary statement and dispatch to the canonical
9340                // statement handler so behavior matches `my $x = …;` exactly.
9341                let stmt_kind = match keyword.as_str() {
9342                    "my" => StmtKind::My(decls.clone()),
9343                    "our" => StmtKind::Our(decls.clone()),
9344                    "state" => StmtKind::State(decls.clone()),
9345                    "local" => StmtKind::Local(decls.clone()),
9346                    _ => StmtKind::My(decls.clone()),
9347                };
9348                let stmt = Statement {
9349                    label: None,
9350                    kind: stmt_kind,
9351                    line,
9352                };
9353                self.exec_statement(&stmt)?;
9354                // Return the value of the (first) declared variable so the
9355                // surrounding expression sees the assigned value, matching
9356                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
9357                let first = decls.first().ok_or_else(|| {
9358                    FlowOrError::Error(PerlError::runtime("MyExpr: empty decl list", line))
9359                })?;
9360                Ok(match first.sigil {
9361                    Sigil::Scalar => self.scope.get_scalar(&first.name),
9362                    Sigil::Array => PerlValue::array(self.scope.get_array(&first.name)),
9363                    Sigil::Hash => {
9364                        let h = self.scope.get_hash(&first.name);
9365                        let mut flat: Vec<PerlValue> = Vec::with_capacity(h.len() * 2);
9366                        for (k, v) in h {
9367                            flat.push(PerlValue::string(k));
9368                            flat.push(v);
9369                        }
9370                        PerlValue::array(flat)
9371                    }
9372                    Sigil::Typeglob => PerlValue::UNDEF,
9373                })
9374            }
9375
9376            // Function calls
9377            ExprKind::FuncCall { name, args } => {
9378                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
9379                if matches!(name.as_str(), "read" | "CORE::read") && args.len() >= 3 {
9380                    let fh_val = self.eval_expr(&args[0])?;
9381                    let fh = fh_val
9382                        .as_io_handle_name()
9383                        .unwrap_or_else(|| fh_val.to_string());
9384                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
9385                    let offset = if args.len() > 3 {
9386                        self.eval_expr(&args[3])?.to_int().max(0) as usize
9387                    } else {
9388                        0
9389                    };
9390                    // Extract the variable name from the AST
9391                    let var_name = match &args[1].kind {
9392                        ExprKind::ScalarVar(n) => n.clone(),
9393                        _ => self.eval_expr(&args[1])?.to_string(),
9394                    };
9395                    let mut buf = vec![0u8; len];
9396                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
9397                        slot.lock().read(&mut buf).unwrap_or(0)
9398                    } else if fh == "STDIN" {
9399                        std::io::stdin().read(&mut buf).unwrap_or(0)
9400                    } else {
9401                        return Err(PerlError::runtime(
9402                            format!("read: unopened handle {}", fh),
9403                            line,
9404                        )
9405                        .into());
9406                    };
9407                    buf.truncate(n);
9408                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
9409                    if offset > 0 {
9410                        let mut existing = self.scope.get_scalar(&var_name).to_string();
9411                        while existing.len() < offset {
9412                            existing.push('\0');
9413                        }
9414                        existing.push_str(&read_str);
9415                        let _ = self
9416                            .scope
9417                            .set_scalar(&var_name, PerlValue::string(existing));
9418                    } else {
9419                        let _ = self
9420                            .scope
9421                            .set_scalar(&var_name, PerlValue::string(read_str));
9422                    }
9423                    return Ok(PerlValue::integer(n as i64));
9424                }
9425                if matches!(name.as_str(), "group_by" | "chunk_by") {
9426                    if args.len() != 2 {
9427                        return Err(PerlError::runtime(
9428                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
9429                            line,
9430                        )
9431                        .into());
9432                    }
9433                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
9434                }
9435                if matches!(name.as_str(), "puniq" | "pfirst" | "pany") {
9436                    let mut arg_vals = Vec::with_capacity(args.len());
9437                    for a in args {
9438                        arg_vals.push(self.eval_expr(a)?);
9439                    }
9440                    let saved_wa = self.wantarray_kind;
9441                    self.wantarray_kind = ctx;
9442                    let r = self.eval_par_list_call(name.as_str(), &arg_vals, ctx, line);
9443                    self.wantarray_kind = saved_wa;
9444                    return r.map_err(Into::into);
9445                }
9446                let arg_vals = if matches!(name.as_str(), "any" | "all" | "none" | "first")
9447                    || matches!(
9448                        name.as_str(),
9449                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9450                    )
9451                    || matches!(
9452                        name.as_str(),
9453                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
9454                    ) {
9455                    if args.len() != 2 {
9456                        return Err(PerlError::runtime(
9457                            format!("{}: expected BLOCK, LIST", name),
9458                            line,
9459                        )
9460                        .into());
9461                    }
9462                    let cr = self.eval_expr(&args[0])?;
9463                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
9464                    let mut v = vec![cr];
9465                    v.extend(list_src.to_list());
9466                    v
9467                } else if matches!(
9468                    name.as_str(),
9469                    "zip" | "List::Util::zip" | "List::Util::zip_longest"
9470                ) {
9471                    let mut v = Vec::with_capacity(args.len());
9472                    for a in args {
9473                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9474                    }
9475                    v
9476                } else if matches!(
9477                    name.as_str(),
9478                    "uniq"
9479                        | "distinct"
9480                        | "uniqstr"
9481                        | "uniqint"
9482                        | "uniqnum"
9483                        | "flatten"
9484                        | "set"
9485                        | "list_count"
9486                        | "list_size"
9487                        | "count"
9488                        | "size"
9489                        | "cnt"
9490                        | "with_index"
9491                        | "List::Util::uniq"
9492                        | "List::Util::uniqstr"
9493                        | "List::Util::uniqint"
9494                        | "List::Util::uniqnum"
9495                        | "shuffle"
9496                        | "List::Util::shuffle"
9497                        | "sum"
9498                        | "sum0"
9499                        | "product"
9500                        | "min"
9501                        | "max"
9502                        | "minstr"
9503                        | "maxstr"
9504                        | "mean"
9505                        | "median"
9506                        | "mode"
9507                        | "stddev"
9508                        | "variance"
9509                        | "List::Util::sum"
9510                        | "List::Util::sum0"
9511                        | "List::Util::product"
9512                        | "List::Util::min"
9513                        | "List::Util::max"
9514                        | "List::Util::minstr"
9515                        | "List::Util::maxstr"
9516                        | "List::Util::mean"
9517                        | "List::Util::median"
9518                        | "List::Util::mode"
9519                        | "List::Util::stddev"
9520                        | "List::Util::variance"
9521                        | "pairs"
9522                        | "unpairs"
9523                        | "pairkeys"
9524                        | "pairvalues"
9525                        | "List::Util::pairs"
9526                        | "List::Util::unpairs"
9527                        | "List::Util::pairkeys"
9528                        | "List::Util::pairvalues"
9529                ) {
9530                    // Perl prototype `(@)`: one slurpy list — either one list expr (`uniq @x`) or
9531                    // multiple actuals (`List::Util::uniq(1, 1, 2)`). Each actual is evaluated in
9532                    // list context so `@a, @b` flattens like Perl.
9533                    let mut list_out = Vec::new();
9534                    if args.len() == 1 {
9535                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
9536                    } else {
9537                        for a in args {
9538                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
9539                        }
9540                    }
9541                    list_out
9542                } else if matches!(
9543                    name.as_str(),
9544                    "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail"
9545                ) {
9546                    if args.is_empty() {
9547                        return Err(PerlError::runtime(
9548                            "take/head/tail/drop/List::Util::head|tail: need LIST..., N or unary N",
9549                            line,
9550                        )
9551                        .into());
9552                    }
9553                    let mut arg_vals = Vec::with_capacity(args.len());
9554                    if args.len() == 1 {
9555                        // head @l == head @l, 1 — evaluate in list context
9556                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9557                    } else {
9558                        for a in &args[..args.len() - 1] {
9559                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9560                        }
9561                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
9562                    }
9563                    arg_vals
9564                } else if matches!(
9565                    name.as_str(),
9566                    "chunked" | "List::Util::chunked" | "windowed" | "List::Util::windowed"
9567                ) {
9568                    let mut list_out = Vec::new();
9569                    match args.len() {
9570                        0 => {
9571                            return Err(PerlError::runtime(
9572                                format!("{name}: expected (LIST, N) or unary N after |>"),
9573                                line,
9574                            )
9575                            .into());
9576                        }
9577                        1 => {
9578                            // chunked @l / windowed @l — evaluate in list context, default size
9579                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9580                        }
9581                        2 => {
9582                            list_out.extend(
9583                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
9584                            );
9585                            list_out.push(self.eval_expr(&args[1])?);
9586                        }
9587                        _ => {
9588                            return Err(PerlError::runtime(
9589                                format!(
9590                                    "{name}: expected exactly (LIST, N); use one list expression then size"
9591                                ),
9592                                line,
9593                            )
9594                            .into());
9595                        }
9596                    }
9597                    list_out
9598                } else {
9599                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
9600                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
9601                    let mut arg_vals = Vec::with_capacity(args.len());
9602                    for a in args {
9603                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
9604                        if let Some(items) = v.as_array_vec() {
9605                            arg_vals.extend(items);
9606                        } else {
9607                            arg_vals.push(v);
9608                        }
9609                    }
9610                    arg_vals
9611                };
9612                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
9613                let saved_wa = self.wantarray_kind;
9614                self.wantarray_kind = ctx;
9615                // Builtins first — immune to monkey-patching (matches VM dispatch order).
9616                // In compat mode, user subs shadow builtins (Perl 5 semantics).
9617                if !crate::compat_mode() {
9618                    if matches!(
9619                        name.as_str(),
9620                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9621                    ) {
9622                        let r =
9623                            self.list_higher_order_block_builtin(name.as_str(), &arg_vals, line);
9624                        self.wantarray_kind = saved_wa;
9625                        return r.map_err(Into::into);
9626                    }
9627                    if let Some(r) =
9628                        crate::builtins::try_builtin(self, name.as_str(), &arg_vals, line)
9629                    {
9630                        self.wantarray_kind = saved_wa;
9631                        return r.map_err(Into::into);
9632                    }
9633                }
9634                if let Some(sub) = self.resolve_sub_by_name(name) {
9635                    self.wantarray_kind = saved_wa;
9636                    let args = self.with_topic_default_args(arg_vals);
9637                    return self.call_sub(&sub, args, ctx, line);
9638                }
9639                // Compat mode: check builtins after user subs (Perl 5 semantics).
9640                if crate::compat_mode() {
9641                    if matches!(
9642                        name.as_str(),
9643                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9644                    ) {
9645                        let r =
9646                            self.list_higher_order_block_builtin(name.as_str(), &arg_vals, line);
9647                        self.wantarray_kind = saved_wa;
9648                        return r.map_err(Into::into);
9649                    }
9650                    if let Some(r) =
9651                        crate::builtins::try_builtin(self, name.as_str(), &arg_vals, line)
9652                    {
9653                        self.wantarray_kind = saved_wa;
9654                        return r.map_err(Into::into);
9655                    }
9656                }
9657                self.wantarray_kind = saved_wa;
9658                self.call_named_sub(name, arg_vals, line, ctx)
9659            }
9660            ExprKind::IndirectCall {
9661                target,
9662                args,
9663                ampersand: _,
9664                pass_caller_arglist,
9665            } => {
9666                let tval = self.eval_expr(target)?;
9667                let arg_vals = if *pass_caller_arglist {
9668                    self.scope.get_array("_")
9669                } else {
9670                    let mut v = Vec::with_capacity(args.len());
9671                    for a in args {
9672                        v.push(self.eval_expr(a)?);
9673                    }
9674                    v
9675                };
9676                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
9677            }
9678            ExprKind::MethodCall {
9679                object,
9680                method,
9681                args,
9682                super_call,
9683            } => {
9684                let obj = self.eval_expr(object)?;
9685                let mut arg_vals = vec![obj.clone()];
9686                for a in args {
9687                    arg_vals.push(self.eval_expr(a)?);
9688                }
9689                if let Some(r) =
9690                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
9691                {
9692                    return r.map_err(Into::into);
9693                }
9694                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
9695                    return r.map_err(Into::into);
9696                }
9697                // Get class name
9698                let class = if let Some(b) = obj.as_blessed_ref() {
9699                    b.class.clone()
9700                } else if let Some(s) = obj.as_str() {
9701                    s // Class->method()
9702                } else {
9703                    return Err(PerlError::runtime("Can't call method on non-object", line).into());
9704                };
9705                if method == "VERSION" && !*super_call {
9706                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
9707                        return Ok(ver);
9708                    }
9709                }
9710                // UNIVERSAL methods: isa, can, DOES
9711                if !*super_call {
9712                    match method.as_str() {
9713                        "isa" => {
9714                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9715                            let mro = self.mro_linearize(&class);
9716                            let result = mro.iter().any(|c| c == &target);
9717                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9718                        }
9719                        "can" => {
9720                            let target_method =
9721                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9722                            let found = self
9723                                .resolve_method_full_name(&class, &target_method, false)
9724                                .and_then(|fq| self.subs.get(&fq))
9725                                .is_some();
9726                            if found {
9727                                return Ok(PerlValue::code_ref(Arc::new(PerlSub {
9728                                    name: target_method,
9729                                    params: vec![],
9730                                    body: vec![],
9731                                    closure_env: None,
9732                                    prototype: None,
9733                                    fib_like: None,
9734                                })));
9735                            } else {
9736                                return Ok(PerlValue::UNDEF);
9737                            }
9738                        }
9739                        "DOES" => {
9740                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9741                            let mro = self.mro_linearize(&class);
9742                            let result = mro.iter().any(|c| c == &target);
9743                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9744                        }
9745                        _ => {}
9746                    }
9747                }
9748                let full_name = self
9749                    .resolve_method_full_name(&class, method, *super_call)
9750                    .ok_or_else(|| {
9751                        PerlError::runtime(
9752                            format!(
9753                                "Can't locate method \"{}\" for invocant \"{}\"",
9754                                method, class
9755                            ),
9756                            line,
9757                        )
9758                    })?;
9759                if let Some(sub) = self.subs.get(&full_name).cloned() {
9760                    self.call_sub(&sub, arg_vals, ctx, line)
9761                } else if method == "new" && !*super_call {
9762                    // Default constructor
9763                    self.builtin_new(&class, arg_vals, line)
9764                } else if let Some(r) =
9765                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
9766                {
9767                    r
9768                } else {
9769                    Err(PerlError::runtime(
9770                        format!(
9771                            "Can't locate method \"{}\" in package \"{}\"",
9772                            method, class
9773                        ),
9774                        line,
9775                    )
9776                    .into())
9777                }
9778            }
9779
9780            // Print/Say/Printf
9781            ExprKind::Print { handle, args } => {
9782                self.exec_print(handle.as_deref(), args, false, line)
9783            }
9784            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
9785            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
9786            ExprKind::Die(args) => {
9787                if args.is_empty() {
9788                    // `die` with no args: re-die with current $@ or "Died"
9789                    let current = self.scope.get_scalar("@");
9790                    let msg = if current.is_undef() || current.to_string().is_empty() {
9791                        let mut m = "Died".to_string();
9792                        m.push_str(&self.die_warn_at_suffix(line));
9793                        m.push('\n');
9794                        m
9795                    } else {
9796                        current.to_string()
9797                    };
9798                    return Err(PerlError::die(msg, line).into());
9799                }
9800                // Single ref argument: store the ref value in $@
9801                if args.len() == 1 {
9802                    let v = self.eval_expr(&args[0])?;
9803                    if v.as_hash_ref().is_some()
9804                        || v.as_blessed_ref().is_some()
9805                        || v.as_array_ref().is_some()
9806                        || v.as_code_ref().is_some()
9807                    {
9808                        let msg = v.to_string();
9809                        return Err(PerlError::die_with_value(v, msg, line).into());
9810                    }
9811                }
9812                let mut msg = String::new();
9813                for a in args {
9814                    let v = self.eval_expr(a)?;
9815                    msg.push_str(&v.to_string());
9816                }
9817                if msg.is_empty() {
9818                    msg = "Died".to_string();
9819                }
9820                if !msg.ends_with('\n') {
9821                    msg.push_str(&self.die_warn_at_suffix(line));
9822                    msg.push('\n');
9823                }
9824                Err(PerlError::die(msg, line).into())
9825            }
9826            ExprKind::Warn(args) => {
9827                let mut msg = String::new();
9828                for a in args {
9829                    let v = self.eval_expr(a)?;
9830                    msg.push_str(&v.to_string());
9831                }
9832                if msg.is_empty() {
9833                    msg = "Warning: something's wrong".to_string();
9834                }
9835                if !msg.ends_with('\n') {
9836                    msg.push_str(&self.die_warn_at_suffix(line));
9837                    msg.push('\n');
9838                }
9839                eprint!("{}", msg);
9840                Ok(PerlValue::integer(1))
9841            }
9842
9843            // Regex
9844            ExprKind::Match {
9845                expr,
9846                pattern,
9847                flags,
9848                scalar_g,
9849                delim: _,
9850            } => {
9851                let val = self.eval_expr(expr)?;
9852                if val.is_iterator() {
9853                    let source = crate::map_stream::into_pull_iter(val);
9854                    let re = self.compile_regex(pattern, flags, line)?;
9855                    let global = flags.contains('g');
9856                    if global {
9857                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9858                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
9859                        )));
9860                    } else {
9861                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9862                            crate::map_stream::MatchStreamIterator::new(source, re),
9863                        )));
9864                    }
9865                }
9866                let s = val.to_string();
9867                let pos_key = match &expr.kind {
9868                    ExprKind::ScalarVar(n) => n.as_str(),
9869                    _ => "_",
9870                };
9871                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
9872            }
9873            ExprKind::Substitution {
9874                expr,
9875                pattern,
9876                replacement,
9877                flags,
9878                delim: _,
9879            } => {
9880                let val = self.eval_expr(expr)?;
9881                if val.is_iterator() {
9882                    let source = crate::map_stream::into_pull_iter(val);
9883                    let re = self.compile_regex(pattern, flags, line)?;
9884                    let global = flags.contains('g');
9885                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9886                        crate::map_stream::SubstStreamIterator::new(
9887                            source,
9888                            re,
9889                            normalize_replacement_backrefs(replacement),
9890                            global,
9891                        ),
9892                    )));
9893                }
9894                let s = val.to_string();
9895                self.regex_subst_execute(
9896                    s,
9897                    pattern,
9898                    replacement.as_str(),
9899                    flags.as_str(),
9900                    expr,
9901                    line,
9902                )
9903            }
9904            ExprKind::Transliterate {
9905                expr,
9906                from,
9907                to,
9908                flags,
9909                delim: _,
9910            } => {
9911                let val = self.eval_expr(expr)?;
9912                if val.is_iterator() {
9913                    let source = crate::map_stream::into_pull_iter(val);
9914                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9915                        crate::map_stream::TransliterateStreamIterator::new(
9916                            source, from, to, flags,
9917                        ),
9918                    )));
9919                }
9920                let s = val.to_string();
9921                self.regex_transliterate_execute(
9922                    s,
9923                    from.as_str(),
9924                    to.as_str(),
9925                    flags.as_str(),
9926                    expr,
9927                    line,
9928                )
9929            }
9930
9931            // List operations
9932            ExprKind::MapExpr {
9933                block,
9934                list,
9935                flatten_array_refs,
9936                stream,
9937            } => {
9938                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9939                if *stream {
9940                    let out =
9941                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
9942                    if ctx == WantarrayCtx::List {
9943                        return Ok(out);
9944                    }
9945                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9946                }
9947                let items = list_val.to_list();
9948                if items.len() == 1 {
9949                    if let Some(p) = items[0].as_pipeline() {
9950                        if *flatten_array_refs {
9951                            return Err(PerlError::runtime(
9952                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
9953                                line,
9954                            )
9955                            .into());
9956                        }
9957                        let sub = self.anon_coderef_from_block(block);
9958                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
9959                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
9960                    }
9961                }
9962                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
9963                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
9964                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
9965                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
9966                let mut result = Vec::new();
9967                for item in items {
9968                    self.scope.set_topic(item);
9969                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
9970                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
9971                }
9972                if ctx == WantarrayCtx::List {
9973                    Ok(PerlValue::array(result))
9974                } else {
9975                    Ok(PerlValue::integer(result.len() as i64))
9976                }
9977            }
9978            ExprKind::ForEachExpr { block, list } => {
9979                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9980                // Lazy: consume iterator one-at-a-time without materializing.
9981                if list_val.is_iterator() {
9982                    let iter = list_val.into_iterator();
9983                    let mut count = 0i64;
9984                    while let Some(item) = iter.next_item() {
9985                        count += 1;
9986                        self.scope.set_topic(item);
9987                        self.exec_block(block)?;
9988                    }
9989                    return Ok(PerlValue::integer(count));
9990                }
9991                let items = list_val.to_list();
9992                let count = items.len();
9993                for item in items {
9994                    self.scope.set_topic(item);
9995                    self.exec_block(block)?;
9996                }
9997                Ok(PerlValue::integer(count as i64))
9998            }
9999            ExprKind::MapExprComma {
10000                expr,
10001                list,
10002                flatten_array_refs,
10003                stream,
10004            } => {
10005                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10006                if *stream {
10007                    let out =
10008                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
10009                    if ctx == WantarrayCtx::List {
10010                        return Ok(out);
10011                    }
10012                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10013                }
10014                let items = list_val.to_list();
10015                let mut result = Vec::new();
10016                for item in items {
10017                    self.scope.set_topic(item.clone());
10018                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10019                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
10020                }
10021                if ctx == WantarrayCtx::List {
10022                    Ok(PerlValue::array(result))
10023                } else {
10024                    Ok(PerlValue::integer(result.len() as i64))
10025                }
10026            }
10027            ExprKind::GrepExpr {
10028                block,
10029                list,
10030                keyword,
10031            } => {
10032                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10033                if keyword.is_stream() {
10034                    let out = self.filter_stream_block_output(list_val, block, line)?;
10035                    if ctx == WantarrayCtx::List {
10036                        return Ok(out);
10037                    }
10038                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10039                }
10040                let items = list_val.to_list();
10041                if items.len() == 1 {
10042                    if let Some(p) = items[0].as_pipeline() {
10043                        let sub = self.anon_coderef_from_block(block);
10044                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
10045                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
10046                    }
10047                }
10048                let mut result = Vec::new();
10049                for item in items {
10050                    self.scope.set_topic(item.clone());
10051                    let val = self.exec_block(block)?;
10052                    // Bare regex in block → match against $_ (Perl: /pat/ in
10053                    // grep is `$_ =~ /pat/`, not a truthy regex object).
10054                    let keep = if let Some(re) = val.as_regex() {
10055                        re.is_match(&item.to_string())
10056                    } else {
10057                        val.is_true()
10058                    };
10059                    if keep {
10060                        result.push(item);
10061                    }
10062                }
10063                if ctx == WantarrayCtx::List {
10064                    Ok(PerlValue::array(result))
10065                } else {
10066                    Ok(PerlValue::integer(result.len() as i64))
10067                }
10068            }
10069            ExprKind::GrepExprComma {
10070                expr,
10071                list,
10072                keyword,
10073            } => {
10074                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10075                if keyword.is_stream() {
10076                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
10077                    if ctx == WantarrayCtx::List {
10078                        return Ok(out);
10079                    }
10080                    return Ok(PerlValue::integer(out.to_list().len() as i64));
10081                }
10082                let items = list_val.to_list();
10083                let mut result = Vec::new();
10084                for item in items {
10085                    self.scope.set_topic(item.clone());
10086                    let val = self.eval_expr(expr)?;
10087                    let keep = if let Some(re) = val.as_regex() {
10088                        re.is_match(&item.to_string())
10089                    } else {
10090                        val.is_true()
10091                    };
10092                    if keep {
10093                        result.push(item);
10094                    }
10095                }
10096                if ctx == WantarrayCtx::List {
10097                    Ok(PerlValue::array(result))
10098                } else {
10099                    Ok(PerlValue::integer(result.len() as i64))
10100                }
10101            }
10102            ExprKind::SortExpr { cmp, list } => {
10103                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10104                let mut items = list_val.to_list();
10105                match cmp {
10106                    Some(SortComparator::Code(code_expr)) => {
10107                        let sub = self.eval_expr(code_expr)?;
10108                        let Some(sub) = sub.as_code_ref() else {
10109                            return Err(PerlError::runtime(
10110                                "sort: comparator must be a code reference",
10111                                line,
10112                            )
10113                            .into());
10114                        };
10115                        let sub = sub.clone();
10116                        items.sort_by(|a, b| {
10117                            let _ = self.scope.set_scalar("a", a.clone());
10118                            let _ = self.scope.set_scalar("b", b.clone());
10119                            let _ = self.scope.set_scalar("_0", a.clone());
10120                            let _ = self.scope.set_scalar("_1", b.clone());
10121                            match self.call_sub(&sub, vec![], ctx, line) {
10122                                Ok(v) => {
10123                                    let n = v.to_int();
10124                                    if n < 0 {
10125                                        Ordering::Less
10126                                    } else if n > 0 {
10127                                        Ordering::Greater
10128                                    } else {
10129                                        Ordering::Equal
10130                                    }
10131                                }
10132                                Err(_) => Ordering::Equal,
10133                            }
10134                        });
10135                    }
10136                    Some(SortComparator::Block(cmp_block)) => {
10137                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
10138                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
10139                        } else {
10140                            let cmp_block = cmp_block.clone();
10141                            items.sort_by(|a, b| {
10142                                let _ = self.scope.set_scalar("a", a.clone());
10143                                let _ = self.scope.set_scalar("b", b.clone());
10144                                let _ = self.scope.set_scalar("_0", a.clone());
10145                                let _ = self.scope.set_scalar("_1", b.clone());
10146                                match self.exec_block(&cmp_block) {
10147                                    Ok(v) => {
10148                                        let n = v.to_int();
10149                                        if n < 0 {
10150                                            Ordering::Less
10151                                        } else if n > 0 {
10152                                            Ordering::Greater
10153                                        } else {
10154                                            Ordering::Equal
10155                                        }
10156                                    }
10157                                    Err(_) => Ordering::Equal,
10158                                }
10159                            });
10160                        }
10161                    }
10162                    None => {
10163                        items.sort_by_key(|a| a.to_string());
10164                    }
10165                }
10166                Ok(PerlValue::array(items))
10167            }
10168            ExprKind::Rev(expr) => {
10169                // Eval in scalar context first to preserve set/hash/array ref types
10170                let val = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
10171                if val.is_iterator() {
10172                    return Ok(PerlValue::iterator(Arc::new(
10173                        crate::value::RevIterator::new(val.into_iterator()),
10174                    )));
10175                }
10176                if let Some(s) = crate::value::set_payload(&val) {
10177                    let mut out = crate::value::PerlSet::new();
10178                    for (k, v) in s.iter().rev() {
10179                        out.insert(k.clone(), v.clone());
10180                    }
10181                    return Ok(PerlValue::set(Arc::new(out)));
10182                }
10183                if let Some(ar) = val.as_array_ref() {
10184                    let items: Vec<_> = ar.read().iter().rev().cloned().collect();
10185                    return Ok(PerlValue::array_ref(Arc::new(parking_lot::RwLock::new(
10186                        items,
10187                    ))));
10188                }
10189                if let Some(hr) = val.as_hash_ref() {
10190                    let mut out: indexmap::IndexMap<String, PerlValue> = indexmap::IndexMap::new();
10191                    for (k, v) in hr.read().iter() {
10192                        out.insert(v.to_string(), PerlValue::string(k.clone()));
10193                    }
10194                    return Ok(PerlValue::hash_ref(Arc::new(parking_lot::RwLock::new(out))));
10195                }
10196                // Re-eval in list context for bare arrays/hashes
10197                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10198                if let Some(hm) = val.as_hash_map() {
10199                    let mut out: indexmap::IndexMap<String, PerlValue> = indexmap::IndexMap::new();
10200                    for (k, v) in hm.iter() {
10201                        out.insert(v.to_string(), PerlValue::string(k.clone()));
10202                    }
10203                    return Ok(PerlValue::hash(out));
10204                }
10205                if val.as_array_vec().is_some() {
10206                    let mut items = val.to_list();
10207                    items.reverse();
10208                    Ok(PerlValue::array(items))
10209                } else {
10210                    let items = val.to_list();
10211                    if items.len() > 1 {
10212                        let mut items = items;
10213                        items.reverse();
10214                        Ok(PerlValue::array(items))
10215                    } else {
10216                        let s = val.to_string();
10217                        Ok(PerlValue::string(s.chars().rev().collect()))
10218                    }
10219                }
10220            }
10221            ExprKind::ReverseExpr(list) => {
10222                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10223                match ctx {
10224                    WantarrayCtx::List => {
10225                        let mut items = val.to_list();
10226                        items.reverse();
10227                        Ok(PerlValue::array(items))
10228                    }
10229                    _ => {
10230                        let items = val.to_list();
10231                        let s: String = items.iter().map(|v| v.to_string()).collect();
10232                        Ok(PerlValue::string(s.chars().rev().collect()))
10233                    }
10234                }
10235            }
10236
10237            // ── Parallel operations (rayon-powered) ──
10238            ExprKind::ParLinesExpr {
10239                path,
10240                callback,
10241                progress,
10242            } => self.eval_par_lines_expr(
10243                path.as_ref(),
10244                callback.as_ref(),
10245                progress.as_deref(),
10246                line,
10247            ),
10248            ExprKind::ParWalkExpr {
10249                path,
10250                callback,
10251                progress,
10252            } => {
10253                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
10254            }
10255            ExprKind::PwatchExpr { path, callback } => {
10256                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
10257            }
10258            ExprKind::PMapExpr {
10259                block,
10260                list,
10261                progress,
10262                flat_outputs,
10263                on_cluster,
10264                stream,
10265            } => {
10266                let show_progress = progress
10267                    .as_ref()
10268                    .map(|p| self.eval_expr(p))
10269                    .transpose()?
10270                    .map(|v| v.is_true())
10271                    .unwrap_or(false);
10272                let list_val = self.eval_expr(list)?;
10273                if let Some(cluster_e) = on_cluster {
10274                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
10275                    return self.eval_pmap_remote(
10276                        cluster_val,
10277                        list_val,
10278                        show_progress,
10279                        block,
10280                        *flat_outputs,
10281                        line,
10282                    );
10283                }
10284                if *stream {
10285                    let source = crate::map_stream::into_pull_iter(list_val);
10286                    let sub = self.anon_coderef_from_block(block);
10287                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
10288                    return Ok(PerlValue::iterator(Arc::new(
10289                        crate::map_stream::PMapStreamIterator::new(
10290                            source,
10291                            sub,
10292                            self.subs.clone(),
10293                            capture,
10294                            atomic_arrays,
10295                            atomic_hashes,
10296                            *flat_outputs,
10297                        ),
10298                    )));
10299                }
10300                let items = list_val.to_list();
10301                let block = block.clone();
10302                let subs = self.subs.clone();
10303                let (scope_capture, atomic_arrays, atomic_hashes) =
10304                    self.scope.capture_with_atomics();
10305                let pmap_progress = PmapProgress::new(show_progress, items.len());
10306
10307                if *flat_outputs {
10308                    let mut indexed: Vec<(usize, Vec<PerlValue>)> = items
10309                        .into_par_iter()
10310                        .enumerate()
10311                        .map(|(i, item)| {
10312                            let mut local_interp = Interpreter::new();
10313                            local_interp.subs = subs.clone();
10314                            local_interp.scope.restore_capture(&scope_capture);
10315                            local_interp
10316                                .scope
10317                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10318                            local_interp.enable_parallel_guard();
10319                            local_interp.scope.set_topic(item);
10320                            let val = match local_interp.exec_block(&block) {
10321                                Ok(val) => val,
10322                                Err(_) => PerlValue::UNDEF,
10323                            };
10324                            let chunk = val.map_flatten_outputs(true);
10325                            pmap_progress.tick();
10326                            (i, chunk)
10327                        })
10328                        .collect();
10329                    pmap_progress.finish();
10330                    indexed.sort_by_key(|(i, _)| *i);
10331                    let results: Vec<PerlValue> =
10332                        indexed.into_iter().flat_map(|(_, v)| v).collect();
10333                    Ok(PerlValue::array(results))
10334                } else {
10335                    let results: Vec<PerlValue> = items
10336                        .into_par_iter()
10337                        .map(|item| {
10338                            let mut local_interp = Interpreter::new();
10339                            local_interp.subs = subs.clone();
10340                            local_interp.scope.restore_capture(&scope_capture);
10341                            local_interp
10342                                .scope
10343                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10344                            local_interp.enable_parallel_guard();
10345                            local_interp.scope.set_topic(item);
10346                            let val = match local_interp.exec_block(&block) {
10347                                Ok(val) => val,
10348                                Err(_) => PerlValue::UNDEF,
10349                            };
10350                            pmap_progress.tick();
10351                            val
10352                        })
10353                        .collect();
10354                    pmap_progress.finish();
10355                    Ok(PerlValue::array(results))
10356                }
10357            }
10358            ExprKind::PMapChunkedExpr {
10359                chunk_size,
10360                block,
10361                list,
10362                progress,
10363            } => {
10364                let show_progress = progress
10365                    .as_ref()
10366                    .map(|p| self.eval_expr(p))
10367                    .transpose()?
10368                    .map(|v| v.is_true())
10369                    .unwrap_or(false);
10370                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
10371                let list_val = self.eval_expr(list)?;
10372                let items = list_val.to_list();
10373                let block = block.clone();
10374                let subs = self.subs.clone();
10375                let (scope_capture, atomic_arrays, atomic_hashes) =
10376                    self.scope.capture_with_atomics();
10377
10378                let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = items
10379                    .chunks(chunk_n)
10380                    .enumerate()
10381                    .map(|(i, c)| (i, c.to_vec()))
10382                    .collect();
10383
10384                let n_chunks = indexed_chunks.len();
10385                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
10386
10387                let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
10388                    .into_par_iter()
10389                    .map(|(chunk_idx, chunk)| {
10390                        let mut local_interp = Interpreter::new();
10391                        local_interp.subs = subs.clone();
10392                        local_interp.scope.restore_capture(&scope_capture);
10393                        local_interp
10394                            .scope
10395                            .restore_atomics(&atomic_arrays, &atomic_hashes);
10396                        local_interp.enable_parallel_guard();
10397                        let mut out = Vec::with_capacity(chunk.len());
10398                        for item in chunk {
10399                            local_interp.scope.set_topic(item);
10400                            match local_interp.exec_block(&block) {
10401                                Ok(val) => out.push(val),
10402                                Err(_) => out.push(PerlValue::UNDEF),
10403                            }
10404                        }
10405                        pmap_progress.tick();
10406                        (chunk_idx, out)
10407                    })
10408                    .collect();
10409
10410                pmap_progress.finish();
10411                chunk_results.sort_by_key(|(i, _)| *i);
10412                let results: Vec<PerlValue> =
10413                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
10414                Ok(PerlValue::array(results))
10415            }
10416            ExprKind::PGrepExpr {
10417                block,
10418                list,
10419                progress,
10420                stream,
10421            } => {
10422                let show_progress = progress
10423                    .as_ref()
10424                    .map(|p| self.eval_expr(p))
10425                    .transpose()?
10426                    .map(|v| v.is_true())
10427                    .unwrap_or(false);
10428                let list_val = self.eval_expr(list)?;
10429                if *stream {
10430                    let source = crate::map_stream::into_pull_iter(list_val);
10431                    let sub = self.anon_coderef_from_block(block);
10432                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
10433                    return Ok(PerlValue::iterator(Arc::new(
10434                        crate::map_stream::PGrepStreamIterator::new(
10435                            source,
10436                            sub,
10437                            self.subs.clone(),
10438                            capture,
10439                            atomic_arrays,
10440                            atomic_hashes,
10441                        ),
10442                    )));
10443                }
10444                let items = list_val.to_list();
10445                let block = block.clone();
10446                let subs = self.subs.clone();
10447                let (scope_capture, atomic_arrays, atomic_hashes) =
10448                    self.scope.capture_with_atomics();
10449                let pmap_progress = PmapProgress::new(show_progress, items.len());
10450
10451                let results: Vec<PerlValue> = items
10452                    .into_par_iter()
10453                    .filter_map(|item| {
10454                        let mut local_interp = Interpreter::new();
10455                        local_interp.subs = subs.clone();
10456                        local_interp.scope.restore_capture(&scope_capture);
10457                        local_interp
10458                            .scope
10459                            .restore_atomics(&atomic_arrays, &atomic_hashes);
10460                        local_interp.enable_parallel_guard();
10461                        local_interp.scope.set_topic(item.clone());
10462                        let keep = match local_interp.exec_block(&block) {
10463                            Ok(val) => val.is_true(),
10464                            Err(_) => false,
10465                        };
10466                        pmap_progress.tick();
10467                        if keep {
10468                            Some(item)
10469                        } else {
10470                            None
10471                        }
10472                    })
10473                    .collect();
10474                pmap_progress.finish();
10475                Ok(PerlValue::array(results))
10476            }
10477            ExprKind::PForExpr {
10478                block,
10479                list,
10480                progress,
10481            } => {
10482                let show_progress = progress
10483                    .as_ref()
10484                    .map(|p| self.eval_expr(p))
10485                    .transpose()?
10486                    .map(|v| v.is_true())
10487                    .unwrap_or(false);
10488                let list_val = self.eval_expr(list)?;
10489                let items = list_val.to_list();
10490                let block = block.clone();
10491                let subs = self.subs.clone();
10492                let (scope_capture, atomic_arrays, atomic_hashes) =
10493                    self.scope.capture_with_atomics();
10494
10495                let pmap_progress = PmapProgress::new(show_progress, items.len());
10496                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10497                items.into_par_iter().for_each(|item| {
10498                    if first_err.lock().is_some() {
10499                        return;
10500                    }
10501                    let mut local_interp = Interpreter::new();
10502                    local_interp.subs = subs.clone();
10503                    local_interp.scope.restore_capture(&scope_capture);
10504                    local_interp
10505                        .scope
10506                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10507                    local_interp.enable_parallel_guard();
10508                    local_interp.scope.set_topic(item);
10509                    match local_interp.exec_block(&block) {
10510                        Ok(_) => {}
10511                        Err(e) => {
10512                            let stryke = match e {
10513                                FlowOrError::Error(stryke) => stryke,
10514                                FlowOrError::Flow(_) => PerlError::runtime(
10515                                    "return/last/next/redo not supported inside pfor block",
10516                                    line,
10517                                ),
10518                            };
10519                            let mut g = first_err.lock();
10520                            if g.is_none() {
10521                                *g = Some(stryke);
10522                            }
10523                        }
10524                    }
10525                    pmap_progress.tick();
10526                });
10527                pmap_progress.finish();
10528                if let Some(e) = first_err.lock().take() {
10529                    return Err(FlowOrError::Error(e));
10530                }
10531                Ok(PerlValue::UNDEF)
10532            }
10533            ExprKind::FanExpr {
10534                count,
10535                block,
10536                progress,
10537                capture,
10538            } => {
10539                let show_progress = progress
10540                    .as_ref()
10541                    .map(|p| self.eval_expr(p))
10542                    .transpose()?
10543                    .map(|v| v.is_true())
10544                    .unwrap_or(false);
10545                let n = match count {
10546                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
10547                    None => self.parallel_thread_count(),
10548                };
10549                let block = block.clone();
10550                let subs = self.subs.clone();
10551                let (scope_capture, atomic_arrays, atomic_hashes) =
10552                    self.scope.capture_with_atomics();
10553
10554                let fan_progress = FanProgress::new(show_progress, n);
10555                if *capture {
10556                    if n == 0 {
10557                        return Ok(PerlValue::array(Vec::new()));
10558                    }
10559                    let pairs: Vec<(usize, ExecResult)> = (0..n)
10560                        .into_par_iter()
10561                        .map(|i| {
10562                            fan_progress.start_worker(i);
10563                            let mut local_interp = Interpreter::new();
10564                            local_interp.subs = subs.clone();
10565                            local_interp.suppress_stdout = show_progress;
10566                            local_interp.scope.restore_capture(&scope_capture);
10567                            local_interp
10568                                .scope
10569                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10570                            local_interp.enable_parallel_guard();
10571                            local_interp.scope.set_topic(PerlValue::integer(i as i64));
10572                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10573                            let res = local_interp.exec_block(&block);
10574                            crate::parallel_trace::fan_worker_set_index(None);
10575                            fan_progress.finish_worker(i);
10576                            (i, res)
10577                        })
10578                        .collect();
10579                    fan_progress.finish();
10580                    let mut pairs = pairs;
10581                    pairs.sort_by_key(|(i, _)| *i);
10582                    let mut out = Vec::with_capacity(n);
10583                    for (_, r) in pairs {
10584                        match r {
10585                            Ok(v) => out.push(v),
10586                            Err(e) => return Err(e),
10587                        }
10588                    }
10589                    return Ok(PerlValue::array(out));
10590                }
10591                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10592                (0..n).into_par_iter().for_each(|i| {
10593                    if first_err.lock().is_some() {
10594                        return;
10595                    }
10596                    fan_progress.start_worker(i);
10597                    let mut local_interp = Interpreter::new();
10598                    local_interp.subs = subs.clone();
10599                    local_interp.suppress_stdout = show_progress;
10600                    local_interp.scope.restore_capture(&scope_capture);
10601                    local_interp
10602                        .scope
10603                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10604                    local_interp.enable_parallel_guard();
10605                    local_interp.scope.set_topic(PerlValue::integer(i as i64));
10606                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10607                    match local_interp.exec_block(&block) {
10608                        Ok(_) => {}
10609                        Err(e) => {
10610                            let stryke = match e {
10611                                FlowOrError::Error(stryke) => stryke,
10612                                FlowOrError::Flow(_) => PerlError::runtime(
10613                                    "return/last/next/redo not supported inside fan block",
10614                                    line,
10615                                ),
10616                            };
10617                            let mut g = first_err.lock();
10618                            if g.is_none() {
10619                                *g = Some(stryke);
10620                            }
10621                        }
10622                    }
10623                    crate::parallel_trace::fan_worker_set_index(None);
10624                    fan_progress.finish_worker(i);
10625                });
10626                fan_progress.finish();
10627                if let Some(e) = first_err.lock().take() {
10628                    return Err(FlowOrError::Error(e));
10629                }
10630                Ok(PerlValue::UNDEF)
10631            }
10632            ExprKind::RetryBlock {
10633                body,
10634                times,
10635                backoff,
10636            } => self.eval_retry_block(body, times, *backoff, line),
10637            ExprKind::RateLimitBlock {
10638                slot,
10639                max,
10640                window,
10641                body,
10642            } => self.eval_rate_limit_block(*slot, max, window, body, line),
10643            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
10644            ExprKind::GenBlock { body } => {
10645                let g = Arc::new(PerlGenerator {
10646                    block: body.clone(),
10647                    pc: Mutex::new(0),
10648                    scope_started: Mutex::new(false),
10649                    exhausted: Mutex::new(false),
10650                });
10651                Ok(PerlValue::generator(g))
10652            }
10653            ExprKind::Yield(e) => {
10654                if !self.in_generator {
10655                    return Err(PerlError::runtime("yield outside gen block", line).into());
10656                }
10657                let v = self.eval_expr(e)?;
10658                Err(FlowOrError::Flow(Flow::Yield(v)))
10659            }
10660            ExprKind::AlgebraicMatch { subject, arms } => {
10661                self.eval_algebraic_match(subject, arms, line)
10662            }
10663            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
10664                Ok(self.spawn_async_block(body))
10665            }
10666            ExprKind::Trace { body } => {
10667                crate::parallel_trace::trace_enter();
10668                let out = self.exec_block(body);
10669                crate::parallel_trace::trace_leave();
10670                out
10671            }
10672            ExprKind::Spinner { message, body } => {
10673                use std::io::Write as _;
10674                let msg = self.eval_expr(message)?.to_string();
10675                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
10676                let done2 = done.clone();
10677                let handle = std::thread::spawn(move || {
10678                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
10679                    let mut i = 0;
10680                    let stderr = std::io::stderr();
10681                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
10682                        {
10683                            let stdout = std::io::stdout();
10684                            let _stdout_lock = stdout.lock();
10685                            let mut err = stderr.lock();
10686                            let _ = write!(
10687                                err,
10688                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
10689                                frames[i % frames.len()],
10690                                msg
10691                            );
10692                            let _ = err.flush();
10693                        }
10694                        std::thread::sleep(std::time::Duration::from_millis(80));
10695                        i += 1;
10696                    }
10697                    let mut err = stderr.lock();
10698                    let _ = write!(err, "\r\x1b[2K");
10699                    let _ = err.flush();
10700                });
10701                let result = self.exec_block(body);
10702                done.store(true, std::sync::atomic::Ordering::Relaxed);
10703                let _ = handle.join();
10704                result
10705            }
10706            ExprKind::Timer { body } => {
10707                let start = std::time::Instant::now();
10708                self.exec_block(body)?;
10709                let ms = start.elapsed().as_secs_f64() * 1000.0;
10710                Ok(PerlValue::float(ms))
10711            }
10712            ExprKind::Bench { body, times } => {
10713                let n = self.eval_expr(times)?.to_int();
10714                if n < 0 {
10715                    return Err(PerlError::runtime(
10716                        "bench: iteration count must be non-negative",
10717                        line,
10718                    )
10719                    .into());
10720                }
10721                self.run_bench_block(body, n as usize, line)
10722            }
10723            ExprKind::Await(expr) => {
10724                let v = self.eval_expr(expr)?;
10725                if let Some(t) = v.as_async_task() {
10726                    t.await_result().map_err(FlowOrError::from)
10727                } else {
10728                    Ok(v)
10729                }
10730            }
10731            ExprKind::Slurp(e) => {
10732                let path = self.eval_expr(e)?.to_string();
10733                read_file_text_perl_compat(&path)
10734                    .map(PerlValue::string)
10735                    .map_err(|e| {
10736                        FlowOrError::Error(PerlError::runtime(format!("slurp: {}", e), line))
10737                    })
10738            }
10739            ExprKind::Capture(e) => {
10740                let cmd = self.eval_expr(e)?.to_string();
10741                let output = Command::new("sh")
10742                    .arg("-c")
10743                    .arg(&cmd)
10744                    .output()
10745                    .map_err(|e| {
10746                        FlowOrError::Error(PerlError::runtime(format!("capture: {}", e), line))
10747                    })?;
10748                self.record_child_exit_status(output.status);
10749                let exitcode = output.status.code().unwrap_or(-1) as i64;
10750                let stdout = decode_utf8_or_latin1(&output.stdout);
10751                let stderr = decode_utf8_or_latin1(&output.stderr);
10752                Ok(PerlValue::capture(Arc::new(CaptureResult {
10753                    stdout,
10754                    stderr,
10755                    exitcode,
10756                })))
10757            }
10758            ExprKind::Qx(e) => {
10759                let cmd = self.eval_expr(e)?.to_string();
10760                crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)
10761            }
10762            ExprKind::FetchUrl(e) => {
10763                let url = self.eval_expr(e)?.to_string();
10764                ureq::get(&url)
10765                    .call()
10766                    .map_err(|e| {
10767                        FlowOrError::Error(PerlError::runtime(format!("fetch_url: {}", e), line))
10768                    })
10769                    .and_then(|r| {
10770                        r.into_string().map(PerlValue::string).map_err(|e| {
10771                            FlowOrError::Error(PerlError::runtime(
10772                                format!("fetch_url: {}", e),
10773                                line,
10774                            ))
10775                        })
10776                    })
10777            }
10778            ExprKind::Pchannel { capacity } => {
10779                if let Some(c) = capacity {
10780                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
10781                    Ok(crate::pchannel::create_bounded_pair(n))
10782                } else {
10783                    Ok(crate::pchannel::create_pair())
10784                }
10785            }
10786            ExprKind::PSortExpr {
10787                cmp,
10788                list,
10789                progress,
10790            } => {
10791                let show_progress = progress
10792                    .as_ref()
10793                    .map(|p| self.eval_expr(p))
10794                    .transpose()?
10795                    .map(|v| v.is_true())
10796                    .unwrap_or(false);
10797                let list_val = self.eval_expr(list)?;
10798                let mut items = list_val.to_list();
10799                let pmap_progress = PmapProgress::new(show_progress, 2);
10800                pmap_progress.tick();
10801                if let Some(cmp_block) = cmp {
10802                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
10803                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
10804                    } else {
10805                        let cmp_block = cmp_block.clone();
10806                        let subs = self.subs.clone();
10807                        let scope_capture = self.scope.capture();
10808                        items.par_sort_by(|a, b| {
10809                            let mut local_interp = Interpreter::new();
10810                            local_interp.subs = subs.clone();
10811                            local_interp.scope.restore_capture(&scope_capture);
10812                            let _ = local_interp.scope.set_scalar("a", a.clone());
10813                            let _ = local_interp.scope.set_scalar("b", b.clone());
10814                            let _ = local_interp.scope.set_scalar("_0", a.clone());
10815                            let _ = local_interp.scope.set_scalar("_1", b.clone());
10816                            match local_interp.exec_block(&cmp_block) {
10817                                Ok(v) => {
10818                                    let n = v.to_int();
10819                                    if n < 0 {
10820                                        std::cmp::Ordering::Less
10821                                    } else if n > 0 {
10822                                        std::cmp::Ordering::Greater
10823                                    } else {
10824                                        std::cmp::Ordering::Equal
10825                                    }
10826                                }
10827                                Err(_) => std::cmp::Ordering::Equal,
10828                            }
10829                        });
10830                    }
10831                } else {
10832                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
10833                }
10834                pmap_progress.tick();
10835                pmap_progress.finish();
10836                Ok(PerlValue::array(items))
10837            }
10838
10839            ExprKind::ReduceExpr { block, list } => {
10840                let list_val = self.eval_expr(list)?;
10841                let items = list_val.to_list();
10842                if items.is_empty() {
10843                    return Ok(PerlValue::UNDEF);
10844                }
10845                if items.len() == 1 {
10846                    return Ok(items.into_iter().next().unwrap());
10847                }
10848                let block = block.clone();
10849                let subs = self.subs.clone();
10850                let scope_capture = self.scope.capture();
10851                let mut acc = items[0].clone();
10852                for b in items.into_iter().skip(1) {
10853                    let mut local_interp = Interpreter::new();
10854                    local_interp.subs = subs.clone();
10855                    local_interp.scope.restore_capture(&scope_capture);
10856                    let _ = local_interp.scope.set_scalar("a", acc.clone());
10857                    let _ = local_interp.scope.set_scalar("b", b.clone());
10858                    let _ = local_interp.scope.set_scalar("_0", acc);
10859                    let _ = local_interp.scope.set_scalar("_1", b);
10860                    acc = match local_interp.exec_block(&block) {
10861                        Ok(val) => val,
10862                        Err(_) => PerlValue::UNDEF,
10863                    };
10864                }
10865                Ok(acc)
10866            }
10867
10868            ExprKind::PReduceExpr {
10869                block,
10870                list,
10871                progress,
10872            } => {
10873                let show_progress = progress
10874                    .as_ref()
10875                    .map(|p| self.eval_expr(p))
10876                    .transpose()?
10877                    .map(|v| v.is_true())
10878                    .unwrap_or(false);
10879                let list_val = self.eval_expr(list)?;
10880                let items = list_val.to_list();
10881                if items.is_empty() {
10882                    return Ok(PerlValue::UNDEF);
10883                }
10884                if items.len() == 1 {
10885                    return Ok(items.into_iter().next().unwrap());
10886                }
10887                let block = block.clone();
10888                let subs = self.subs.clone();
10889                let scope_capture = self.scope.capture();
10890                let pmap_progress = PmapProgress::new(show_progress, items.len());
10891
10892                let result = items
10893                    .into_par_iter()
10894                    .map(|x| {
10895                        pmap_progress.tick();
10896                        x
10897                    })
10898                    .reduce_with(|a, b| {
10899                        let mut local_interp = Interpreter::new();
10900                        local_interp.subs = subs.clone();
10901                        local_interp.scope.restore_capture(&scope_capture);
10902                        let _ = local_interp.scope.set_scalar("a", a.clone());
10903                        let _ = local_interp.scope.set_scalar("b", b.clone());
10904                        let _ = local_interp.scope.set_scalar("_0", a);
10905                        let _ = local_interp.scope.set_scalar("_1", b);
10906                        match local_interp.exec_block(&block) {
10907                            Ok(val) => val,
10908                            Err(_) => PerlValue::UNDEF,
10909                        }
10910                    });
10911                pmap_progress.finish();
10912                Ok(result.unwrap_or(PerlValue::UNDEF))
10913            }
10914
10915            ExprKind::PReduceInitExpr {
10916                init,
10917                block,
10918                list,
10919                progress,
10920            } => {
10921                let show_progress = progress
10922                    .as_ref()
10923                    .map(|p| self.eval_expr(p))
10924                    .transpose()?
10925                    .map(|v| v.is_true())
10926                    .unwrap_or(false);
10927                let init_val = self.eval_expr(init)?;
10928                let list_val = self.eval_expr(list)?;
10929                let items = list_val.to_list();
10930                if items.is_empty() {
10931                    return Ok(init_val);
10932                }
10933                let block = block.clone();
10934                let subs = self.subs.clone();
10935                let scope_capture = self.scope.capture();
10936                let cap: &[(String, PerlValue)] = scope_capture.as_slice();
10937                if items.len() == 1 {
10938                    return Ok(fold_preduce_init_step(
10939                        &subs,
10940                        cap,
10941                        &block,
10942                        preduce_init_fold_identity(&init_val),
10943                        items.into_iter().next().unwrap(),
10944                    ));
10945                }
10946                let pmap_progress = PmapProgress::new(show_progress, items.len());
10947                let result = items
10948                    .into_par_iter()
10949                    .fold(
10950                        || preduce_init_fold_identity(&init_val),
10951                        |acc, item| {
10952                            pmap_progress.tick();
10953                            fold_preduce_init_step(&subs, cap, &block, acc, item)
10954                        },
10955                    )
10956                    .reduce(
10957                        || preduce_init_fold_identity(&init_val),
10958                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
10959                    );
10960                pmap_progress.finish();
10961                Ok(result)
10962            }
10963
10964            ExprKind::PMapReduceExpr {
10965                map_block,
10966                reduce_block,
10967                list,
10968                progress,
10969            } => {
10970                let show_progress = progress
10971                    .as_ref()
10972                    .map(|p| self.eval_expr(p))
10973                    .transpose()?
10974                    .map(|v| v.is_true())
10975                    .unwrap_or(false);
10976                let list_val = self.eval_expr(list)?;
10977                let items = list_val.to_list();
10978                if items.is_empty() {
10979                    return Ok(PerlValue::UNDEF);
10980                }
10981                let map_block = map_block.clone();
10982                let reduce_block = reduce_block.clone();
10983                let subs = self.subs.clone();
10984                let scope_capture = self.scope.capture();
10985                if items.len() == 1 {
10986                    let mut local_interp = Interpreter::new();
10987                    local_interp.subs = subs.clone();
10988                    local_interp.scope.restore_capture(&scope_capture);
10989                    local_interp.scope.set_topic(items[0].clone());
10990                    return match local_interp.exec_block_no_scope(&map_block) {
10991                        Ok(v) => Ok(v),
10992                        Err(_) => Ok(PerlValue::UNDEF),
10993                    };
10994                }
10995                let pmap_progress = PmapProgress::new(show_progress, items.len());
10996                let result = items
10997                    .into_par_iter()
10998                    .map(|item| {
10999                        let mut local_interp = Interpreter::new();
11000                        local_interp.subs = subs.clone();
11001                        local_interp.scope.restore_capture(&scope_capture);
11002                        local_interp.scope.set_topic(item);
11003                        let val = match local_interp.exec_block_no_scope(&map_block) {
11004                            Ok(val) => val,
11005                            Err(_) => PerlValue::UNDEF,
11006                        };
11007                        pmap_progress.tick();
11008                        val
11009                    })
11010                    .reduce_with(|a, b| {
11011                        let mut local_interp = Interpreter::new();
11012                        local_interp.subs = subs.clone();
11013                        local_interp.scope.restore_capture(&scope_capture);
11014                        let _ = local_interp.scope.set_scalar("a", a.clone());
11015                        let _ = local_interp.scope.set_scalar("b", b.clone());
11016                        let _ = local_interp.scope.set_scalar("_0", a);
11017                        let _ = local_interp.scope.set_scalar("_1", b);
11018                        match local_interp.exec_block_no_scope(&reduce_block) {
11019                            Ok(val) => val,
11020                            Err(_) => PerlValue::UNDEF,
11021                        }
11022                    });
11023                pmap_progress.finish();
11024                Ok(result.unwrap_or(PerlValue::UNDEF))
11025            }
11026
11027            ExprKind::PcacheExpr {
11028                block,
11029                list,
11030                progress,
11031            } => {
11032                let show_progress = progress
11033                    .as_ref()
11034                    .map(|p| self.eval_expr(p))
11035                    .transpose()?
11036                    .map(|v| v.is_true())
11037                    .unwrap_or(false);
11038                let list_val = self.eval_expr(list)?;
11039                let items = list_val.to_list();
11040                let block = block.clone();
11041                let subs = self.subs.clone();
11042                let scope_capture = self.scope.capture();
11043                let cache = &*crate::pcache::GLOBAL_PCACHE;
11044                let pmap_progress = PmapProgress::new(show_progress, items.len());
11045                let results: Vec<PerlValue> = items
11046                    .into_par_iter()
11047                    .map(|item| {
11048                        let k = crate::pcache::cache_key(&item);
11049                        if let Some(v) = cache.get(&k) {
11050                            pmap_progress.tick();
11051                            return v.clone();
11052                        }
11053                        let mut local_interp = Interpreter::new();
11054                        local_interp.subs = subs.clone();
11055                        local_interp.scope.restore_capture(&scope_capture);
11056                        local_interp.scope.set_topic(item.clone());
11057                        let val = match local_interp.exec_block_no_scope(&block) {
11058                            Ok(v) => v,
11059                            Err(_) => PerlValue::UNDEF,
11060                        };
11061                        cache.insert(k, val.clone());
11062                        pmap_progress.tick();
11063                        val
11064                    })
11065                    .collect();
11066                pmap_progress.finish();
11067                Ok(PerlValue::array(results))
11068            }
11069
11070            ExprKind::PselectExpr { receivers, timeout } => {
11071                let mut rx_vals = Vec::with_capacity(receivers.len());
11072                for r in receivers {
11073                    rx_vals.push(self.eval_expr(r)?);
11074                }
11075                let dur = if let Some(t) = timeout.as_ref() {
11076                    Some(std::time::Duration::from_secs_f64(
11077                        self.eval_expr(t)?.to_number().max(0.0),
11078                    ))
11079                } else {
11080                    None
11081                };
11082                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
11083                    &rx_vals, dur, line,
11084                )?)
11085            }
11086
11087            // Array ops
11088            ExprKind::Push { array, values } => {
11089                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
11090            }
11091            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
11092            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
11093            ExprKind::Unshift { array, values } => {
11094                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
11095            }
11096            ExprKind::Splice {
11097                array,
11098                offset,
11099                length,
11100                replacement,
11101            } => self.eval_splice_expr(
11102                array.as_ref(),
11103                offset.as_deref(),
11104                length.as_deref(),
11105                replacement.as_slice(),
11106                ctx,
11107                line,
11108            ),
11109            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
11110            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
11111            ExprKind::Keys(expr) => {
11112                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11113                let keys = Self::keys_from_value(val, line)?;
11114                if ctx == WantarrayCtx::List {
11115                    Ok(keys)
11116                } else {
11117                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
11118                    Ok(PerlValue::integer(n as i64))
11119                }
11120            }
11121            ExprKind::Values(expr) => {
11122                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
11123                let vals = Self::values_from_value(val, line)?;
11124                if ctx == WantarrayCtx::List {
11125                    Ok(vals)
11126                } else {
11127                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
11128                    Ok(PerlValue::integer(n as i64))
11129                }
11130            }
11131            ExprKind::Each(_) => {
11132                // Simplified: returns empty list (full iterator state would need more work)
11133                Ok(PerlValue::array(vec![]))
11134            }
11135
11136            // String ops
11137            ExprKind::Chomp(expr) => {
11138                let val = self.eval_expr(expr)?;
11139                self.chomp_inplace_execute(val, expr)
11140            }
11141            ExprKind::Chop(expr) => {
11142                let val = self.eval_expr(expr)?;
11143                self.chop_inplace_execute(val, expr)
11144            }
11145            ExprKind::Length(expr) => {
11146                let val = self.eval_expr(expr)?;
11147                Ok(if let Some(a) = val.as_array_vec() {
11148                    PerlValue::integer(a.len() as i64)
11149                } else if let Some(h) = val.as_hash_map() {
11150                    PerlValue::integer(h.len() as i64)
11151                } else if let Some(b) = val.as_bytes_arc() {
11152                    PerlValue::integer(b.len() as i64)
11153                } else {
11154                    PerlValue::integer(val.to_string().len() as i64)
11155                })
11156            }
11157            ExprKind::Substr {
11158                string,
11159                offset,
11160                length,
11161                replacement,
11162            } => self.eval_substr_expr(
11163                string.as_ref(),
11164                offset.as_ref(),
11165                length.as_deref(),
11166                replacement.as_deref(),
11167                line,
11168            ),
11169            ExprKind::Index {
11170                string,
11171                substr,
11172                position,
11173            } => {
11174                let s = self.eval_expr(string)?.to_string();
11175                let sub = self.eval_expr(substr)?.to_string();
11176                let pos = if let Some(p) = position {
11177                    self.eval_expr(p)?.to_int() as usize
11178                } else {
11179                    0
11180                };
11181                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
11182                Ok(PerlValue::integer(result))
11183            }
11184            ExprKind::Rindex {
11185                string,
11186                substr,
11187                position,
11188            } => {
11189                let s = self.eval_expr(string)?.to_string();
11190                let sub = self.eval_expr(substr)?.to_string();
11191                let end = if let Some(p) = position {
11192                    self.eval_expr(p)?.to_int() as usize + sub.len()
11193                } else {
11194                    s.len()
11195                };
11196                let search = &s[..end.min(s.len())];
11197                let result = search.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
11198                Ok(PerlValue::integer(result))
11199            }
11200            ExprKind::Sprintf { format, args } => {
11201                let fmt = self.eval_expr(format)?.to_string();
11202                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
11203                // builtins into individual format arguments.
11204                let mut arg_vals = Vec::new();
11205                for a in args {
11206                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
11207                    if let Some(items) = v.as_array_vec() {
11208                        arg_vals.extend(items);
11209                    } else {
11210                        arg_vals.push(v);
11211                    }
11212                }
11213                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
11214                Ok(PerlValue::string(s))
11215            }
11216            ExprKind::JoinExpr { separator, list } => {
11217                let sep = self.eval_expr(separator)?.to_string();
11218                // Like Perl 5, arguments after the separator are evaluated in list context so
11219                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
11220                // expands `localtime` to nine fields.
11221                let items = if let ExprKind::List(exprs) = &list.kind {
11222                    let saved = self.wantarray_kind;
11223                    self.wantarray_kind = WantarrayCtx::List;
11224                    let mut vals = Vec::new();
11225                    for e in exprs {
11226                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
11227                        if let Some(items) = v.as_array_vec() {
11228                            vals.extend(items);
11229                        } else {
11230                            vals.push(v);
11231                        }
11232                    }
11233                    self.wantarray_kind = saved;
11234                    vals
11235                } else {
11236                    let saved = self.wantarray_kind;
11237                    self.wantarray_kind = WantarrayCtx::List;
11238                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
11239                    self.wantarray_kind = saved;
11240                    if let Some(items) = v.as_array_vec() {
11241                        items
11242                    } else {
11243                        vec![v]
11244                    }
11245                };
11246                let mut strs = Vec::with_capacity(items.len());
11247                for v in &items {
11248                    strs.push(self.stringify_value(v.clone(), line)?);
11249                }
11250                Ok(PerlValue::string(strs.join(&sep)))
11251            }
11252            ExprKind::SplitExpr {
11253                pattern,
11254                string,
11255                limit,
11256            } => {
11257                let pat = self.eval_expr(pattern)?.to_string();
11258                let s = self.eval_expr(string)?.to_string();
11259                let lim = if let Some(l) = limit {
11260                    self.eval_expr(l)?.to_int() as usize
11261                } else {
11262                    0
11263                };
11264                let re = self.compile_regex(&pat, "", line)?;
11265                let parts: Vec<PerlValue> = if lim > 0 {
11266                    re.splitn_strings(&s, lim)
11267                        .into_iter()
11268                        .map(PerlValue::string)
11269                        .collect()
11270                } else {
11271                    re.split_strings(&s)
11272                        .into_iter()
11273                        .map(PerlValue::string)
11274                        .collect()
11275                };
11276                Ok(PerlValue::array(parts))
11277            }
11278
11279            // Numeric
11280            ExprKind::Abs(expr) => {
11281                let val = self.eval_expr(expr)?;
11282                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
11283                    return r;
11284                }
11285                Ok(PerlValue::float(val.to_number().abs()))
11286            }
11287            ExprKind::Int(expr) => {
11288                let val = self.eval_expr(expr)?;
11289                Ok(PerlValue::integer(val.to_number() as i64))
11290            }
11291            ExprKind::Sqrt(expr) => {
11292                let val = self.eval_expr(expr)?;
11293                Ok(PerlValue::float(val.to_number().sqrt()))
11294            }
11295            ExprKind::Sin(expr) => {
11296                let val = self.eval_expr(expr)?;
11297                Ok(PerlValue::float(val.to_number().sin()))
11298            }
11299            ExprKind::Cos(expr) => {
11300                let val = self.eval_expr(expr)?;
11301                Ok(PerlValue::float(val.to_number().cos()))
11302            }
11303            ExprKind::Atan2 { y, x } => {
11304                let yv = self.eval_expr(y)?.to_number();
11305                let xv = self.eval_expr(x)?.to_number();
11306                Ok(PerlValue::float(yv.atan2(xv)))
11307            }
11308            ExprKind::Exp(expr) => {
11309                let val = self.eval_expr(expr)?;
11310                Ok(PerlValue::float(val.to_number().exp()))
11311            }
11312            ExprKind::Log(expr) => {
11313                let val = self.eval_expr(expr)?;
11314                Ok(PerlValue::float(val.to_number().ln()))
11315            }
11316            ExprKind::Rand(upper) => {
11317                let u = match upper {
11318                    Some(e) => self.eval_expr(e)?.to_number(),
11319                    None => 1.0,
11320                };
11321                Ok(PerlValue::float(self.perl_rand(u)))
11322            }
11323            ExprKind::Srand(seed) => {
11324                let s = match seed {
11325                    Some(e) => Some(self.eval_expr(e)?.to_number()),
11326                    None => None,
11327                };
11328                Ok(PerlValue::integer(self.perl_srand(s)))
11329            }
11330            ExprKind::Hex(expr) => {
11331                let val = self.eval_expr(expr)?.to_string();
11332                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
11333                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
11334                Ok(PerlValue::integer(n))
11335            }
11336            ExprKind::Oct(expr) => {
11337                let val = self.eval_expr(expr)?.to_string();
11338                let s = val.trim();
11339                let n = if s.starts_with("0x") || s.starts_with("0X") {
11340                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
11341                } else if s.starts_with("0b") || s.starts_with("0B") {
11342                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
11343                } else if s.starts_with("0o") || s.starts_with("0O") {
11344                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
11345                } else {
11346                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
11347                };
11348                Ok(PerlValue::integer(n))
11349            }
11350
11351            // Case
11352            ExprKind::Lc(expr) => Ok(PerlValue::string(
11353                self.eval_expr(expr)?.to_string().to_lowercase(),
11354            )),
11355            ExprKind::Uc(expr) => Ok(PerlValue::string(
11356                self.eval_expr(expr)?.to_string().to_uppercase(),
11357            )),
11358            ExprKind::Lcfirst(expr) => {
11359                let s = self.eval_expr(expr)?.to_string();
11360                let mut chars = s.chars();
11361                let result = match chars.next() {
11362                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
11363                    None => String::new(),
11364                };
11365                Ok(PerlValue::string(result))
11366            }
11367            ExprKind::Ucfirst(expr) => {
11368                let s = self.eval_expr(expr)?.to_string();
11369                let mut chars = s.chars();
11370                let result = match chars.next() {
11371                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
11372                    None => String::new(),
11373                };
11374                Ok(PerlValue::string(result))
11375            }
11376            ExprKind::Fc(expr) => Ok(PerlValue::string(default_case_fold_str(
11377                &self.eval_expr(expr)?.to_string(),
11378            ))),
11379            ExprKind::Crypt { plaintext, salt } => {
11380                let p = self.eval_expr(plaintext)?.to_string();
11381                let sl = self.eval_expr(salt)?.to_string();
11382                Ok(PerlValue::string(perl_crypt(&p, &sl)))
11383            }
11384            ExprKind::Pos(e) => {
11385                let key = match e {
11386                    None => "_".to_string(),
11387                    Some(expr) => match &expr.kind {
11388                        ExprKind::ScalarVar(n) => n.clone(),
11389                        _ => self.eval_expr(expr)?.to_string(),
11390                    },
11391                };
11392                Ok(self
11393                    .regex_pos
11394                    .get(&key)
11395                    .copied()
11396                    .flatten()
11397                    .map(|p| PerlValue::integer(p as i64))
11398                    .unwrap_or(PerlValue::UNDEF))
11399            }
11400            ExprKind::Study(expr) => {
11401                let s = self.eval_expr(expr)?.to_string();
11402                Ok(Self::study_return_value(&s))
11403            }
11404
11405            // Type
11406            ExprKind::Defined(expr) => {
11407                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
11408                if let ExprKind::SubroutineRef(name) = &expr.kind {
11409                    let exists = self.resolve_sub_by_name(name).is_some();
11410                    return Ok(PerlValue::integer(if exists { 1 } else { 0 }));
11411                }
11412                let val = self.eval_expr(expr)?;
11413                Ok(PerlValue::integer(if val.is_undef() { 0 } else { 1 }))
11414            }
11415            ExprKind::Ref(expr) => {
11416                let val = self.eval_expr(expr)?;
11417                Ok(val.ref_type())
11418            }
11419            ExprKind::ScalarContext(expr) => {
11420                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
11421                Ok(v.scalar_context())
11422            }
11423
11424            // Char
11425            ExprKind::Chr(expr) => {
11426                let n = self.eval_expr(expr)?.to_int() as u32;
11427                Ok(PerlValue::string(
11428                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
11429                ))
11430            }
11431            ExprKind::Ord(expr) => {
11432                let s = self.eval_expr(expr)?.to_string();
11433                Ok(PerlValue::integer(
11434                    s.chars().next().map(|c| c as i64).unwrap_or(0),
11435                ))
11436            }
11437
11438            // I/O
11439            ExprKind::OpenMyHandle { .. } => Err(PerlError::runtime(
11440                "internal: `open my $fh` handle used outside open()",
11441                line,
11442            )
11443            .into()),
11444            ExprKind::Open { handle, mode, file } => {
11445                if let ExprKind::OpenMyHandle { name } = &handle.kind {
11446                    self.scope
11447                        .declare_scalar_frozen(name, PerlValue::UNDEF, false, None)?;
11448                    self.english_note_lexical_scalar(name);
11449                    let mode_s = self.eval_expr(mode)?.to_string();
11450                    let file_opt = if let Some(f) = file {
11451                        Some(self.eval_expr(f)?.to_string())
11452                    } else {
11453                        None
11454                    };
11455                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
11456                    self.scope.set_scalar(name, ret.clone())?;
11457                    return Ok(ret);
11458                }
11459                let handle_s = self.eval_expr(handle)?.to_string();
11460                let handle_name = self.resolve_io_handle_name(&handle_s);
11461                let mode_s = self.eval_expr(mode)?.to_string();
11462                let file_opt = if let Some(f) = file {
11463                    Some(self.eval_expr(f)?.to_string())
11464                } else {
11465                    None
11466                };
11467                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
11468                    .map_err(Into::into)
11469            }
11470            ExprKind::Close(expr) => {
11471                let s = self.eval_expr(expr)?.to_string();
11472                let name = self.resolve_io_handle_name(&s);
11473                self.close_builtin_execute(name).map_err(Into::into)
11474            }
11475            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
11476                self.readline_builtin_execute_list(handle.as_deref())
11477            } else {
11478                self.readline_builtin_execute(handle.as_deref())
11479            }
11480            .map_err(Into::into),
11481            ExprKind::Eof(expr) => match expr {
11482                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
11483                Some(e) => {
11484                    let name = self.eval_expr(e)?;
11485                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
11486                }
11487            },
11488
11489            ExprKind::Opendir { handle, path } => {
11490                let h = self.eval_expr(handle)?.to_string();
11491                let p = self.eval_expr(path)?.to_string();
11492                Ok(self.opendir_handle(&h, &p))
11493            }
11494            ExprKind::Readdir(e) => {
11495                let h = self.eval_expr(e)?.to_string();
11496                Ok(if ctx == WantarrayCtx::List {
11497                    self.readdir_handle_list(&h)
11498                } else {
11499                    self.readdir_handle(&h)
11500                })
11501            }
11502            ExprKind::Closedir(e) => {
11503                let h = self.eval_expr(e)?.to_string();
11504                Ok(self.closedir_handle(&h))
11505            }
11506            ExprKind::Rewinddir(e) => {
11507                let h = self.eval_expr(e)?.to_string();
11508                Ok(self.rewinddir_handle(&h))
11509            }
11510            ExprKind::Telldir(e) => {
11511                let h = self.eval_expr(e)?.to_string();
11512                Ok(self.telldir_handle(&h))
11513            }
11514            ExprKind::Seekdir { handle, position } => {
11515                let h = self.eval_expr(handle)?.to_string();
11516                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
11517                Ok(self.seekdir_handle(&h, pos))
11518            }
11519
11520            // File tests
11521            ExprKind::FileTest { op, expr } => {
11522                let path = self.eval_expr(expr)?.to_string();
11523                // -M, -A, -C return fractional days (float), not boolean
11524                if matches!(op, 'M' | 'A' | 'C') {
11525                    #[cfg(unix)]
11526                    {
11527                        return match crate::perl_fs::filetest_age_days(&path, *op) {
11528                            Some(days) => Ok(PerlValue::float(days)),
11529                            None => Ok(PerlValue::UNDEF),
11530                        };
11531                    }
11532                    #[cfg(not(unix))]
11533                    return Ok(PerlValue::UNDEF);
11534                }
11535                // -s returns file size (or undef on error)
11536                if *op == 's' {
11537                    return match std::fs::metadata(&path) {
11538                        Ok(m) => Ok(PerlValue::integer(m.len() as i64)),
11539                        Err(_) => Ok(PerlValue::UNDEF),
11540                    };
11541                }
11542                let result = match op {
11543                    'e' => std::path::Path::new(&path).exists(),
11544                    'f' => std::path::Path::new(&path).is_file(),
11545                    'd' => std::path::Path::new(&path).is_dir(),
11546                    'l' => std::path::Path::new(&path).is_symlink(),
11547                    #[cfg(unix)]
11548                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
11549                    #[cfg(not(unix))]
11550                    'r' => std::fs::metadata(&path).is_ok(),
11551                    #[cfg(unix)]
11552                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
11553                    #[cfg(not(unix))]
11554                    'w' => std::fs::metadata(&path).is_ok(),
11555                    #[cfg(unix)]
11556                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
11557                    #[cfg(not(unix))]
11558                    'x' => false,
11559                    #[cfg(unix)]
11560                    'o' => crate::perl_fs::filetest_owned_effective(&path),
11561                    #[cfg(not(unix))]
11562                    'o' => false,
11563                    #[cfg(unix)]
11564                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
11565                    #[cfg(not(unix))]
11566                    'R' => false,
11567                    #[cfg(unix)]
11568                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
11569                    #[cfg(not(unix))]
11570                    'W' => false,
11571                    #[cfg(unix)]
11572                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
11573                    #[cfg(not(unix))]
11574                    'X' => false,
11575                    #[cfg(unix)]
11576                    'O' => crate::perl_fs::filetest_owned_real(&path),
11577                    #[cfg(not(unix))]
11578                    'O' => false,
11579                    'z' => std::fs::metadata(&path)
11580                        .map(|m| m.len() == 0)
11581                        .unwrap_or(true),
11582                    't' => crate::perl_fs::filetest_is_tty(&path),
11583                    #[cfg(unix)]
11584                    'p' => crate::perl_fs::filetest_is_pipe(&path),
11585                    #[cfg(not(unix))]
11586                    'p' => false,
11587                    #[cfg(unix)]
11588                    'S' => crate::perl_fs::filetest_is_socket(&path),
11589                    #[cfg(not(unix))]
11590                    'S' => false,
11591                    #[cfg(unix)]
11592                    'b' => crate::perl_fs::filetest_is_block_device(&path),
11593                    #[cfg(not(unix))]
11594                    'b' => false,
11595                    #[cfg(unix)]
11596                    'c' => crate::perl_fs::filetest_is_char_device(&path),
11597                    #[cfg(not(unix))]
11598                    'c' => false,
11599                    #[cfg(unix)]
11600                    'u' => crate::perl_fs::filetest_is_setuid(&path),
11601                    #[cfg(not(unix))]
11602                    'u' => false,
11603                    #[cfg(unix)]
11604                    'g' => crate::perl_fs::filetest_is_setgid(&path),
11605                    #[cfg(not(unix))]
11606                    'g' => false,
11607                    #[cfg(unix)]
11608                    'k' => crate::perl_fs::filetest_is_sticky(&path),
11609                    #[cfg(not(unix))]
11610                    'k' => false,
11611                    'T' => crate::perl_fs::filetest_is_text(&path),
11612                    'B' => crate::perl_fs::filetest_is_binary(&path),
11613                    _ => false,
11614                };
11615                Ok(PerlValue::integer(if result { 1 } else { 0 }))
11616            }
11617
11618            // System
11619            ExprKind::System(args) => {
11620                let mut cmd_args = Vec::new();
11621                for a in args {
11622                    cmd_args.push(self.eval_expr(a)?.to_string());
11623                }
11624                if cmd_args.is_empty() {
11625                    return Ok(PerlValue::integer(-1));
11626                }
11627                let status = Command::new("sh")
11628                    .arg("-c")
11629                    .arg(cmd_args.join(" "))
11630                    .status();
11631                match status {
11632                    Ok(s) => {
11633                        self.record_child_exit_status(s);
11634                        Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
11635                    }
11636                    Err(e) => {
11637                        self.apply_io_error_to_errno(&e);
11638                        Ok(PerlValue::integer(-1))
11639                    }
11640                }
11641            }
11642            ExprKind::Exec(args) => {
11643                let mut cmd_args = Vec::new();
11644                for a in args {
11645                    cmd_args.push(self.eval_expr(a)?.to_string());
11646                }
11647                if cmd_args.is_empty() {
11648                    return Ok(PerlValue::integer(-1));
11649                }
11650                let status = Command::new("sh")
11651                    .arg("-c")
11652                    .arg(cmd_args.join(" "))
11653                    .status();
11654                match status {
11655                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
11656                    Err(e) => {
11657                        self.apply_io_error_to_errno(&e);
11658                        Ok(PerlValue::integer(-1))
11659                    }
11660                }
11661            }
11662            ExprKind::Eval(expr) => {
11663                self.eval_nesting += 1;
11664                let out = match &expr.kind {
11665                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
11666                        Ok(v) => {
11667                            self.clear_eval_error();
11668                            Ok(v)
11669                        }
11670                        Err(FlowOrError::Error(e)) => {
11671                            self.set_eval_error_from_perl_error(&e);
11672                            Ok(PerlValue::UNDEF)
11673                        }
11674                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
11675                    },
11676                    _ => {
11677                        let code = self.eval_expr(expr)?.to_string();
11678                        // Parse and execute the string as Perl code
11679                        match crate::parse_and_run_string(&code, self) {
11680                            Ok(v) => {
11681                                self.clear_eval_error();
11682                                Ok(v)
11683                            }
11684                            Err(e) => {
11685                                self.set_eval_error(e.to_string());
11686                                Ok(PerlValue::UNDEF)
11687                            }
11688                        }
11689                    }
11690                };
11691                self.eval_nesting -= 1;
11692                out
11693            }
11694            ExprKind::Do(expr) => match &expr.kind {
11695                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
11696                _ => {
11697                    let val = self.eval_expr(expr)?;
11698                    let filename = val.to_string();
11699                    match read_file_text_perl_compat(&filename) {
11700                        Ok(code) => {
11701                            let code = crate::data_section::strip_perl_end_marker(&code);
11702                            match crate::parse_and_run_string_in_file(code, self, &filename) {
11703                                Ok(v) => Ok(v),
11704                                Err(e) => {
11705                                    self.set_eval_error(e.to_string());
11706                                    Ok(PerlValue::UNDEF)
11707                                }
11708                            }
11709                        }
11710                        Err(e) => {
11711                            self.apply_io_error_to_errno(&e);
11712                            Ok(PerlValue::UNDEF)
11713                        }
11714                    }
11715                }
11716            },
11717            ExprKind::Require(expr) => {
11718                let spec = self.eval_expr(expr)?.to_string();
11719                self.require_execute(&spec, line)
11720                    .map_err(FlowOrError::Error)
11721            }
11722            ExprKind::Exit(code) => {
11723                let c = if let Some(e) = code {
11724                    self.eval_expr(e)?.to_int() as i32
11725                } else {
11726                    0
11727                };
11728                Err(PerlError::new(ErrorKind::Exit(c), "", line, &self.file).into())
11729            }
11730            ExprKind::Chdir(expr) => {
11731                let path = self.eval_expr(expr)?.to_string();
11732                match std::env::set_current_dir(&path) {
11733                    Ok(_) => Ok(PerlValue::integer(1)),
11734                    Err(e) => {
11735                        self.apply_io_error_to_errno(&e);
11736                        Ok(PerlValue::integer(0))
11737                    }
11738                }
11739            }
11740            ExprKind::Mkdir { path, mode: _ } => {
11741                let p = self.eval_expr(path)?.to_string();
11742                match std::fs::create_dir(&p) {
11743                    Ok(_) => Ok(PerlValue::integer(1)),
11744                    Err(e) => {
11745                        self.apply_io_error_to_errno(&e);
11746                        Ok(PerlValue::integer(0))
11747                    }
11748                }
11749            }
11750            ExprKind::Unlink(args) => {
11751                let mut count = 0i64;
11752                for a in args {
11753                    let path = self.eval_expr(a)?.to_string();
11754                    if std::fs::remove_file(&path).is_ok() {
11755                        count += 1;
11756                    }
11757                }
11758                Ok(PerlValue::integer(count))
11759            }
11760            ExprKind::Rename { old, new } => {
11761                let o = self.eval_expr(old)?.to_string();
11762                let n = self.eval_expr(new)?.to_string();
11763                Ok(crate::perl_fs::rename_paths(&o, &n))
11764            }
11765            ExprKind::Chmod(args) => {
11766                let mode = self.eval_expr(&args[0])?.to_int();
11767                let mut paths = Vec::new();
11768                for a in &args[1..] {
11769                    paths.push(self.eval_expr(a)?.to_string());
11770                }
11771                Ok(PerlValue::integer(crate::perl_fs::chmod_paths(
11772                    &paths, mode,
11773                )))
11774            }
11775            ExprKind::Chown(args) => {
11776                let uid = self.eval_expr(&args[0])?.to_int();
11777                let gid = self.eval_expr(&args[1])?.to_int();
11778                let mut paths = Vec::new();
11779                for a in &args[2..] {
11780                    paths.push(self.eval_expr(a)?.to_string());
11781                }
11782                Ok(PerlValue::integer(crate::perl_fs::chown_paths(
11783                    &paths, uid, gid,
11784                )))
11785            }
11786            ExprKind::Stat(e) => {
11787                let path = self.eval_expr(e)?.to_string();
11788                Ok(crate::perl_fs::stat_path(&path, false))
11789            }
11790            ExprKind::Lstat(e) => {
11791                let path = self.eval_expr(e)?.to_string();
11792                Ok(crate::perl_fs::stat_path(&path, true))
11793            }
11794            ExprKind::Link { old, new } => {
11795                let o = self.eval_expr(old)?.to_string();
11796                let n = self.eval_expr(new)?.to_string();
11797                Ok(crate::perl_fs::link_hard(&o, &n))
11798            }
11799            ExprKind::Symlink { old, new } => {
11800                let o = self.eval_expr(old)?.to_string();
11801                let n = self.eval_expr(new)?.to_string();
11802                Ok(crate::perl_fs::link_sym(&o, &n))
11803            }
11804            ExprKind::Readlink(e) => {
11805                let path = self.eval_expr(e)?.to_string();
11806                Ok(crate::perl_fs::read_link(&path))
11807            }
11808            ExprKind::Files(args) => {
11809                let dir = if args.is_empty() {
11810                    ".".to_string()
11811                } else {
11812                    self.eval_expr(&args[0])?.to_string()
11813                };
11814                Ok(crate::perl_fs::list_files(&dir))
11815            }
11816            ExprKind::Filesf(args) => {
11817                let dir = if args.is_empty() {
11818                    ".".to_string()
11819                } else {
11820                    self.eval_expr(&args[0])?.to_string()
11821                };
11822                Ok(crate::perl_fs::list_filesf(&dir))
11823            }
11824            ExprKind::FilesfRecursive(args) => {
11825                let dir = if args.is_empty() {
11826                    ".".to_string()
11827                } else {
11828                    self.eval_expr(&args[0])?.to_string()
11829                };
11830                Ok(PerlValue::iterator(Arc::new(
11831                    crate::value::FsWalkIterator::new(&dir, true),
11832                )))
11833            }
11834            ExprKind::Dirs(args) => {
11835                let dir = if args.is_empty() {
11836                    ".".to_string()
11837                } else {
11838                    self.eval_expr(&args[0])?.to_string()
11839                };
11840                Ok(crate::perl_fs::list_dirs(&dir))
11841            }
11842            ExprKind::DirsRecursive(args) => {
11843                let dir = if args.is_empty() {
11844                    ".".to_string()
11845                } else {
11846                    self.eval_expr(&args[0])?.to_string()
11847                };
11848                Ok(PerlValue::iterator(Arc::new(
11849                    crate::value::FsWalkIterator::new(&dir, false),
11850                )))
11851            }
11852            ExprKind::SymLinks(args) => {
11853                let dir = if args.is_empty() {
11854                    ".".to_string()
11855                } else {
11856                    self.eval_expr(&args[0])?.to_string()
11857                };
11858                Ok(crate::perl_fs::list_sym_links(&dir))
11859            }
11860            ExprKind::Sockets(args) => {
11861                let dir = if args.is_empty() {
11862                    ".".to_string()
11863                } else {
11864                    self.eval_expr(&args[0])?.to_string()
11865                };
11866                Ok(crate::perl_fs::list_sockets(&dir))
11867            }
11868            ExprKind::Pipes(args) => {
11869                let dir = if args.is_empty() {
11870                    ".".to_string()
11871                } else {
11872                    self.eval_expr(&args[0])?.to_string()
11873                };
11874                Ok(crate::perl_fs::list_pipes(&dir))
11875            }
11876            ExprKind::BlockDevices(args) => {
11877                let dir = if args.is_empty() {
11878                    ".".to_string()
11879                } else {
11880                    self.eval_expr(&args[0])?.to_string()
11881                };
11882                Ok(crate::perl_fs::list_block_devices(&dir))
11883            }
11884            ExprKind::CharDevices(args) => {
11885                let dir = if args.is_empty() {
11886                    ".".to_string()
11887                } else {
11888                    self.eval_expr(&args[0])?.to_string()
11889                };
11890                Ok(crate::perl_fs::list_char_devices(&dir))
11891            }
11892            ExprKind::Executables(args) => {
11893                let dir = if args.is_empty() {
11894                    ".".to_string()
11895                } else {
11896                    self.eval_expr(&args[0])?.to_string()
11897                };
11898                Ok(crate::perl_fs::list_executables(&dir))
11899            }
11900            ExprKind::Glob(args) => {
11901                let mut pats = Vec::new();
11902                for a in args {
11903                    pats.push(self.eval_expr(a)?.to_string());
11904                }
11905                Ok(crate::perl_fs::glob_patterns(&pats))
11906            }
11907            ExprKind::GlobPar { args, progress } => {
11908                let mut pats = Vec::new();
11909                for a in args {
11910                    pats.push(self.eval_expr(a)?.to_string());
11911                }
11912                let show_progress = progress
11913                    .as_ref()
11914                    .map(|p| self.eval_expr(p))
11915                    .transpose()?
11916                    .map(|v| v.is_true())
11917                    .unwrap_or(false);
11918                if show_progress {
11919                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
11920                } else {
11921                    Ok(crate::perl_fs::glob_par_patterns(&pats))
11922                }
11923            }
11924            ExprKind::ParSed { args, progress } => {
11925                let has_progress = progress.is_some();
11926                let mut vals: Vec<PerlValue> = Vec::new();
11927                for a in args {
11928                    vals.push(self.eval_expr(a)?);
11929                }
11930                if let Some(p) = progress {
11931                    vals.push(self.eval_expr(p.as_ref())?);
11932                }
11933                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
11934            }
11935            ExprKind::Bless { ref_expr, class } => {
11936                let val = self.eval_expr(ref_expr)?;
11937                let class_name = if let Some(c) = class {
11938                    self.eval_expr(c)?.to_string()
11939                } else {
11940                    self.scope.get_scalar("__PACKAGE__").to_string()
11941                };
11942                Ok(PerlValue::blessed(Arc::new(
11943                    crate::value::BlessedRef::new_blessed(class_name, val),
11944                )))
11945            }
11946            ExprKind::Caller(_) => {
11947                // Simplified: return package, file, line
11948                Ok(PerlValue::array(vec![
11949                    PerlValue::string("main".into()),
11950                    PerlValue::string(self.file.clone()),
11951                    PerlValue::integer(line as i64),
11952                ]))
11953            }
11954            ExprKind::Wantarray => Ok(match self.wantarray_kind {
11955                WantarrayCtx::Void => PerlValue::UNDEF,
11956                WantarrayCtx::Scalar => PerlValue::integer(0),
11957                WantarrayCtx::List => PerlValue::integer(1),
11958            }),
11959
11960            ExprKind::List(exprs) => {
11961                // In scalar context, the comma operator evaluates to the last element.
11962                if ctx == WantarrayCtx::Scalar {
11963                    if let Some(last) = exprs.last() {
11964                        // Evaluate earlier expressions for side effects
11965                        for e in &exprs[..exprs.len() - 1] {
11966                            self.eval_expr(e)?;
11967                        }
11968                        return self.eval_expr(last);
11969                    } else {
11970                        return Ok(PerlValue::UNDEF);
11971                    }
11972                }
11973                let mut vals = Vec::new();
11974                for e in exprs {
11975                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
11976                    if let Some(items) = v.as_array_vec() {
11977                        vals.extend(items);
11978                    } else {
11979                        vals.push(v);
11980                    }
11981                }
11982                if vals.len() == 1 {
11983                    Ok(vals.pop().unwrap())
11984                } else {
11985                    Ok(PerlValue::array(vals))
11986                }
11987            }
11988
11989            // Postfix modifiers
11990            ExprKind::PostfixIf { expr, condition } => {
11991                if self.eval_postfix_condition(condition)? {
11992                    self.eval_expr(expr)
11993                } else {
11994                    Ok(PerlValue::UNDEF)
11995                }
11996            }
11997            ExprKind::PostfixUnless { expr, condition } => {
11998                if !self.eval_postfix_condition(condition)? {
11999                    self.eval_expr(expr)
12000                } else {
12001                    Ok(PerlValue::UNDEF)
12002                }
12003            }
12004            ExprKind::PostfixWhile { expr, condition } => {
12005                // `do { ... } while (COND)` — body runs before the first condition check.
12006                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
12007                let is_do_block = matches!(
12008                    &expr.kind,
12009                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
12010                );
12011                let mut last = PerlValue::UNDEF;
12012                if is_do_block {
12013                    loop {
12014                        last = self.eval_expr(expr)?;
12015                        if !self.eval_postfix_condition(condition)? {
12016                            break;
12017                        }
12018                    }
12019                } else {
12020                    loop {
12021                        if !self.eval_postfix_condition(condition)? {
12022                            break;
12023                        }
12024                        last = self.eval_expr(expr)?;
12025                    }
12026                }
12027                Ok(last)
12028            }
12029            ExprKind::PostfixUntil { expr, condition } => {
12030                let is_do_block = matches!(
12031                    &expr.kind,
12032                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
12033                );
12034                let mut last = PerlValue::UNDEF;
12035                if is_do_block {
12036                    loop {
12037                        last = self.eval_expr(expr)?;
12038                        if self.eval_postfix_condition(condition)? {
12039                            break;
12040                        }
12041                    }
12042                } else {
12043                    loop {
12044                        if self.eval_postfix_condition(condition)? {
12045                            break;
12046                        }
12047                        last = self.eval_expr(expr)?;
12048                    }
12049                }
12050                Ok(last)
12051            }
12052            ExprKind::PostfixForeach { expr, list } => {
12053                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
12054                let mut last = PerlValue::UNDEF;
12055                for item in items {
12056                    self.scope.set_topic(item);
12057                    last = self.eval_expr(expr)?;
12058                }
12059                Ok(last)
12060            }
12061        }
12062    }
12063
12064    // ── Helpers ──
12065
12066    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
12067        match op {
12068            BinOp::Add => Some("+"),
12069            BinOp::Sub => Some("-"),
12070            BinOp::Mul => Some("*"),
12071            BinOp::Div => Some("/"),
12072            BinOp::Mod => Some("%"),
12073            BinOp::Pow => Some("**"),
12074            BinOp::Concat => Some("."),
12075            BinOp::StrEq => Some("eq"),
12076            BinOp::NumEq => Some("=="),
12077            BinOp::StrNe => Some("ne"),
12078            BinOp::NumNe => Some("!="),
12079            BinOp::StrLt => Some("lt"),
12080            BinOp::StrGt => Some("gt"),
12081            BinOp::StrLe => Some("le"),
12082            BinOp::StrGe => Some("ge"),
12083            BinOp::NumLt => Some("<"),
12084            BinOp::NumGt => Some(">"),
12085            BinOp::NumLe => Some("<="),
12086            BinOp::NumGe => Some(">="),
12087            BinOp::Spaceship => Some("<=>"),
12088            BinOp::StrCmp => Some("cmp"),
12089            _ => None,
12090        }
12091    }
12092
12093    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
12094    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
12095        map.get("").or_else(|| map.get("\"\""))
12096    }
12097
12098    /// String context for blessed objects with `overload '""'`.
12099    pub(crate) fn stringify_value(
12100        &mut self,
12101        v: PerlValue,
12102        line: usize,
12103    ) -> Result<String, FlowOrError> {
12104        if let Some(r) = self.try_overload_stringify(&v, line) {
12105            let pv = r?;
12106            return Ok(pv.to_string());
12107        }
12108        Ok(v.to_string())
12109    }
12110
12111    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
12112    pub(crate) fn perl_sprintf_stringify(
12113        &mut self,
12114        fmt: &str,
12115        args: &[PerlValue],
12116        line: usize,
12117    ) -> Result<String, FlowOrError> {
12118        perl_sprintf_format_with(fmt, args, |v| self.stringify_value(v.clone(), line))
12119    }
12120
12121    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
12122    pub(crate) fn render_format_template(
12123        &mut self,
12124        tmpl: &crate::format::FormatTemplate,
12125        line: usize,
12126    ) -> Result<String, FlowOrError> {
12127        use crate::format::{FormatRecord, PictureSegment};
12128        let mut buf = String::new();
12129        for rec in &tmpl.records {
12130            match rec {
12131                FormatRecord::Literal(s) => {
12132                    buf.push_str(s);
12133                    buf.push('\n');
12134                }
12135                FormatRecord::Picture { segments, exprs } => {
12136                    let mut vals: Vec<String> = Vec::new();
12137                    for e in exprs {
12138                        let v = self.eval_expr(e)?;
12139                        vals.push(self.stringify_value(v, line)?);
12140                    }
12141                    let mut vi = 0usize;
12142                    let mut line_out = String::new();
12143                    for seg in segments {
12144                        match seg {
12145                            PictureSegment::Literal(t) => line_out.push_str(t),
12146                            PictureSegment::Field {
12147                                width,
12148                                align,
12149                                kind: _,
12150                            } => {
12151                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
12152                                vi += 1;
12153                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
12154                            }
12155                        }
12156                    }
12157                    buf.push_str(line_out.trim_end());
12158                    buf.push('\n');
12159                }
12160            }
12161        }
12162        Ok(buf)
12163    }
12164
12165    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
12166    pub(crate) fn resolve_write_output_handle(
12167        &self,
12168        v: &PerlValue,
12169        line: usize,
12170    ) -> PerlResult<String> {
12171        if let Some(n) = v.as_io_handle_name() {
12172            let n = self.resolve_io_handle_name(&n);
12173            if self.is_bound_handle(&n) {
12174                return Ok(n);
12175            }
12176        }
12177        if let Some(s) = v.as_str() {
12178            if self.is_bound_handle(&s) {
12179                return Ok(self.resolve_io_handle_name(&s));
12180            }
12181        }
12182        let s = v.to_string();
12183        if self.is_bound_handle(&s) {
12184            return Ok(self.resolve_io_handle_name(&s));
12185        }
12186        Err(PerlError::runtime(
12187            format!("write: invalid or unopened filehandle {}", s),
12188            line,
12189        ))
12190    }
12191
12192    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
12193    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
12194    /// that handle like `write FH`.
12195    pub(crate) fn write_format_execute(
12196        &mut self,
12197        args: &[PerlValue],
12198        line: usize,
12199    ) -> PerlResult<PerlValue> {
12200        let handle_name = match args.len() {
12201            0 => self.default_print_handle.clone(),
12202            1 => self.resolve_write_output_handle(&args[0], line)?,
12203            _ => {
12204                return Err(PerlError::runtime("write: too many arguments", line));
12205            }
12206        };
12207        let pkg = self.current_package();
12208        let mut fmt_name = self.scope.get_scalar("~").to_string();
12209        if fmt_name.is_empty() {
12210            fmt_name = "STDOUT".to_string();
12211        }
12212        let key = format!("{}::{}", pkg, fmt_name);
12213        let tmpl = self
12214            .format_templates
12215            .get(&key)
12216            .map(Arc::clone)
12217            .ok_or_else(|| {
12218                PerlError::runtime(
12219                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
12220                    line,
12221                )
12222            })?;
12223        let out = self
12224            .render_format_template(&tmpl, line)
12225            .map_err(|e| match e {
12226                FlowOrError::Error(e) => e,
12227                FlowOrError::Flow(_) => PerlError::runtime("write: unexpected control flow", line),
12228            })?;
12229        self.write_formatted_print(handle_name.as_str(), &out, line)?;
12230        Ok(PerlValue::integer(1))
12231    }
12232
12233    pub(crate) fn try_overload_stringify(
12234        &mut self,
12235        v: &PerlValue,
12236        line: usize,
12237    ) -> Option<ExecResult> {
12238        // Native class instance: look for method named '""' or 'stringify'
12239        if let Some(c) = v.as_class_inst() {
12240            let method_name = c
12241                .def
12242                .method("stringify")
12243                .or_else(|| c.def.method("\"\""))
12244                .filter(|m| m.body.is_some())?;
12245            let body = method_name.body.clone().unwrap();
12246            let params = method_name.params.clone();
12247            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
12248        }
12249        let br = v.as_blessed_ref()?;
12250        let class = br.class.clone();
12251        let map = self.overload_table.get(&class)?;
12252        let sub_short = Self::overload_stringify_method(map)?;
12253        let fq = format!("{}::{}", class, sub_short);
12254        let sub = self.subs.get(&fq)?.clone();
12255        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
12256    }
12257
12258    /// Map overload operator key to native class method name.
12259    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
12260        match key {
12261            "+" => Some("op_add"),
12262            "-" => Some("op_sub"),
12263            "*" => Some("op_mul"),
12264            "/" => Some("op_div"),
12265            "%" => Some("op_mod"),
12266            "**" => Some("op_pow"),
12267            "." => Some("op_concat"),
12268            "==" => Some("op_eq"),
12269            "!=" => Some("op_ne"),
12270            "<" => Some("op_lt"),
12271            ">" => Some("op_gt"),
12272            "<=" => Some("op_le"),
12273            ">=" => Some("op_ge"),
12274            "<=>" => Some("op_spaceship"),
12275            "eq" => Some("op_str_eq"),
12276            "ne" => Some("op_str_ne"),
12277            "lt" => Some("op_str_lt"),
12278            "gt" => Some("op_str_gt"),
12279            "le" => Some("op_str_le"),
12280            "ge" => Some("op_str_ge"),
12281            "cmp" => Some("op_cmp"),
12282            _ => None,
12283        }
12284    }
12285
12286    pub(crate) fn try_overload_binop(
12287        &mut self,
12288        op: BinOp,
12289        lv: &PerlValue,
12290        rv: &PerlValue,
12291        line: usize,
12292    ) -> Option<ExecResult> {
12293        let key = Self::overload_key_for_binop(op)?;
12294        // Native class instance overloading
12295        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
12296            (Some(c.def.clone()), lv.clone(), rv.clone())
12297        } else if let Some(c) = rv.as_class_inst() {
12298            (Some(c.def.clone()), rv.clone(), lv.clone())
12299        } else {
12300            (None, lv.clone(), rv.clone())
12301        };
12302        if let Some(ref def) = ci_def {
12303            if let Some(method_name) = Self::overload_method_name_for_key(key) {
12304                if let Some((m, _)) = self.find_class_method(def, method_name) {
12305                    if let Some(ref body) = m.body {
12306                        let params = m.params.clone();
12307                        return Some(self.call_class_method(
12308                            body,
12309                            &params,
12310                            vec![invocant, other],
12311                            line,
12312                        ));
12313                    }
12314                }
12315            }
12316        }
12317        // Blessed ref overloading (existing path)
12318        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
12319            (br.class.clone(), lv.clone(), rv.clone())
12320        } else if let Some(br) = rv.as_blessed_ref() {
12321            (br.class.clone(), rv.clone(), lv.clone())
12322        } else {
12323            return None;
12324        };
12325        let map = self.overload_table.get(&class)?;
12326        let sub_short = if let Some(s) = map.get(key) {
12327            s.clone()
12328        } else if let Some(nm) = map.get("nomethod") {
12329            let fq = format!("{}::{}", class, nm);
12330            let sub = self.subs.get(&fq)?.clone();
12331            return Some(self.call_sub(
12332                &sub,
12333                vec![invocant, other, PerlValue::string(key.to_string())],
12334                WantarrayCtx::Scalar,
12335                line,
12336            ));
12337        } else {
12338            return None;
12339        };
12340        let fq = format!("{}::{}", class, sub_short);
12341        let sub = self.subs.get(&fq)?.clone();
12342        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
12343    }
12344
12345    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
12346    pub(crate) fn try_overload_unary_dispatch(
12347        &mut self,
12348        op_key: &str,
12349        val: &PerlValue,
12350        line: usize,
12351    ) -> Option<ExecResult> {
12352        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
12353        if let Some(c) = val.as_class_inst() {
12354            let method_name = match op_key {
12355                "neg" => "op_neg",
12356                "bool" => "op_bool",
12357                "abs" => "op_abs",
12358                "0+" => "op_numify",
12359                _ => return None,
12360            };
12361            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
12362                if let Some(ref body) = m.body {
12363                    let params = m.params.clone();
12364                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
12365                }
12366            }
12367            return None;
12368        }
12369        // Blessed ref path
12370        let br = val.as_blessed_ref()?;
12371        let class = br.class.clone();
12372        let map = self.overload_table.get(&class)?;
12373        if let Some(s) = map.get(op_key) {
12374            let fq = format!("{}::{}", class, s);
12375            let sub = self.subs.get(&fq)?.clone();
12376            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
12377        }
12378        if let Some(nm) = map.get("nomethod") {
12379            let fq = format!("{}::{}", class, nm);
12380            let sub = self.subs.get(&fq)?.clone();
12381            return Some(self.call_sub(
12382                &sub,
12383                vec![val.clone(), PerlValue::string(op_key.to_string())],
12384                WantarrayCtx::Scalar,
12385                line,
12386            ));
12387        }
12388        None
12389    }
12390
12391    #[inline]
12392    fn eval_binop(
12393        &mut self,
12394        op: BinOp,
12395        lv: &PerlValue,
12396        rv: &PerlValue,
12397        _line: usize,
12398    ) -> ExecResult {
12399        Ok(match op {
12400            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
12401            // Perl `+` is numeric addition only; string concatenation is `.`.
12402            BinOp::Add => {
12403                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12404                    PerlValue::integer(a.wrapping_add(b))
12405                } else {
12406                    PerlValue::float(lv.to_number() + rv.to_number())
12407                }
12408            }
12409            BinOp::Sub => {
12410                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12411                    PerlValue::integer(a.wrapping_sub(b))
12412                } else {
12413                    PerlValue::float(lv.to_number() - rv.to_number())
12414                }
12415            }
12416            BinOp::Mul => {
12417                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12418                    PerlValue::integer(a.wrapping_mul(b))
12419                } else {
12420                    PerlValue::float(lv.to_number() * rv.to_number())
12421                }
12422            }
12423            BinOp::Div => {
12424                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12425                    if b == 0 {
12426                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
12427                    }
12428                    if a % b == 0 {
12429                        PerlValue::integer(a / b)
12430                    } else {
12431                        PerlValue::float(a as f64 / b as f64)
12432                    }
12433                } else {
12434                    let d = rv.to_number();
12435                    if d == 0.0 {
12436                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
12437                    }
12438                    PerlValue::float(lv.to_number() / d)
12439                }
12440            }
12441            BinOp::Mod => {
12442                let d = rv.to_int();
12443                if d == 0 {
12444                    return Err(PerlError::runtime("Illegal modulus zero", _line).into());
12445                }
12446                PerlValue::integer(lv.to_int() % d)
12447            }
12448            BinOp::Pow => {
12449                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12450                    let int_pow = (b >= 0)
12451                        .then(|| u32::try_from(b).ok())
12452                        .flatten()
12453                        .and_then(|bu| a.checked_pow(bu))
12454                        .map(PerlValue::integer);
12455                    int_pow.unwrap_or_else(|| PerlValue::float(lv.to_number().powf(rv.to_number())))
12456                } else {
12457                    PerlValue::float(lv.to_number().powf(rv.to_number()))
12458                }
12459            }
12460            BinOp::Concat => {
12461                let mut s = String::new();
12462                lv.append_to(&mut s);
12463                rv.append_to(&mut s);
12464                PerlValue::string(s)
12465            }
12466            BinOp::NumEq => {
12467                // Struct equality: compare all fields
12468                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
12469                    if a.def.name != b.def.name {
12470                        PerlValue::integer(0)
12471                    } else {
12472                        let av = a.get_values();
12473                        let bv = b.get_values();
12474                        let eq = av.len() == bv.len()
12475                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
12476                        PerlValue::integer(if eq { 1 } else { 0 })
12477                    }
12478                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12479                    PerlValue::integer(if a == b { 1 } else { 0 })
12480                } else {
12481                    PerlValue::integer(if lv.to_number() == rv.to_number() {
12482                        1
12483                    } else {
12484                        0
12485                    })
12486                }
12487            }
12488            BinOp::NumNe => {
12489                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12490                    PerlValue::integer(if a != b { 1 } else { 0 })
12491                } else {
12492                    PerlValue::integer(if lv.to_number() != rv.to_number() {
12493                        1
12494                    } else {
12495                        0
12496                    })
12497                }
12498            }
12499            BinOp::NumLt => {
12500                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12501                    PerlValue::integer(if a < b { 1 } else { 0 })
12502                } else {
12503                    PerlValue::integer(if lv.to_number() < rv.to_number() {
12504                        1
12505                    } else {
12506                        0
12507                    })
12508                }
12509            }
12510            BinOp::NumGt => {
12511                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12512                    PerlValue::integer(if a > b { 1 } else { 0 })
12513                } else {
12514                    PerlValue::integer(if lv.to_number() > rv.to_number() {
12515                        1
12516                    } else {
12517                        0
12518                    })
12519                }
12520            }
12521            BinOp::NumLe => {
12522                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12523                    PerlValue::integer(if a <= b { 1 } else { 0 })
12524                } else {
12525                    PerlValue::integer(if lv.to_number() <= rv.to_number() {
12526                        1
12527                    } else {
12528                        0
12529                    })
12530                }
12531            }
12532            BinOp::NumGe => {
12533                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12534                    PerlValue::integer(if a >= b { 1 } else { 0 })
12535                } else {
12536                    PerlValue::integer(if lv.to_number() >= rv.to_number() {
12537                        1
12538                    } else {
12539                        0
12540                    })
12541                }
12542            }
12543            BinOp::Spaceship => {
12544                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12545                    PerlValue::integer(if a < b {
12546                        -1
12547                    } else if a > b {
12548                        1
12549                    } else {
12550                        0
12551                    })
12552                } else {
12553                    let a = lv.to_number();
12554                    let b = rv.to_number();
12555                    PerlValue::integer(if a < b {
12556                        -1
12557                    } else if a > b {
12558                        1
12559                    } else {
12560                        0
12561                    })
12562                }
12563            }
12564            BinOp::StrEq => PerlValue::integer(if lv.to_string() == rv.to_string() {
12565                1
12566            } else {
12567                0
12568            }),
12569            BinOp::StrNe => PerlValue::integer(if lv.to_string() != rv.to_string() {
12570                1
12571            } else {
12572                0
12573            }),
12574            BinOp::StrLt => PerlValue::integer(if lv.to_string() < rv.to_string() {
12575                1
12576            } else {
12577                0
12578            }),
12579            BinOp::StrGt => PerlValue::integer(if lv.to_string() > rv.to_string() {
12580                1
12581            } else {
12582                0
12583            }),
12584            BinOp::StrLe => PerlValue::integer(if lv.to_string() <= rv.to_string() {
12585                1
12586            } else {
12587                0
12588            }),
12589            BinOp::StrGe => PerlValue::integer(if lv.to_string() >= rv.to_string() {
12590                1
12591            } else {
12592                0
12593            }),
12594            BinOp::StrCmp => {
12595                let cmp = lv.to_string().cmp(&rv.to_string());
12596                PerlValue::integer(match cmp {
12597                    std::cmp::Ordering::Less => -1,
12598                    std::cmp::Ordering::Greater => 1,
12599                    std::cmp::Ordering::Equal => 0,
12600                })
12601            }
12602            BinOp::BitAnd => {
12603                if let Some(s) = crate::value::set_intersection(lv, rv) {
12604                    s
12605                } else {
12606                    PerlValue::integer(lv.to_int() & rv.to_int())
12607                }
12608            }
12609            BinOp::BitOr => {
12610                if let Some(s) = crate::value::set_union(lv, rv) {
12611                    s
12612                } else {
12613                    PerlValue::integer(lv.to_int() | rv.to_int())
12614                }
12615            }
12616            BinOp::BitXor => PerlValue::integer(lv.to_int() ^ rv.to_int()),
12617            BinOp::ShiftLeft => PerlValue::integer(lv.to_int() << rv.to_int()),
12618            BinOp::ShiftRight => PerlValue::integer(lv.to_int() >> rv.to_int()),
12619            // These should have been handled by short-circuit above
12620            BinOp::LogAnd
12621            | BinOp::LogOr
12622            | BinOp::DefinedOr
12623            | BinOp::LogAndWord
12624            | BinOp::LogOrWord => unreachable!(),
12625            BinOp::BindMatch | BinOp::BindNotMatch => {
12626                unreachable!("regex bind handled in eval_expr BinOp arm")
12627            }
12628        })
12629    }
12630
12631    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
12632    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
12633    /// length — that was silently wrong vs `perl`.
12634    fn err_modify_symbolic_aggregate_deref_inc_dec(
12635        kind: Sigil,
12636        is_pre: bool,
12637        is_inc: bool,
12638        line: usize,
12639    ) -> FlowOrError {
12640        let agg = match kind {
12641            Sigil::Array => "array",
12642            Sigil::Hash => "hash",
12643            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
12644        };
12645        let op = match (is_pre, is_inc) {
12646            (true, true) => "preincrement (++)",
12647            (true, false) => "predecrement (--)",
12648            (false, true) => "postincrement (++)",
12649            (false, false) => "postdecrement (--)",
12650        };
12651        FlowOrError::Error(PerlError::runtime(
12652            format!("Can't modify {agg} dereference in {op}"),
12653            line,
12654        ))
12655    }
12656
12657    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
12658    pub(crate) fn symbolic_scalar_ref_postfix(
12659        &mut self,
12660        ref_val: PerlValue,
12661        decrement: bool,
12662        line: usize,
12663    ) -> Result<PerlValue, FlowOrError> {
12664        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
12665        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12666        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
12667        Ok(old)
12668    }
12669
12670    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
12671    /// [`Self::assign_value`] and the VM.
12672    pub(crate) fn assign_scalar_ref_deref(
12673        &mut self,
12674        ref_val: PerlValue,
12675        val: PerlValue,
12676        line: usize,
12677    ) -> ExecResult {
12678        if let Some(name) = ref_val.as_scalar_binding_name() {
12679            self.set_special_var(&name, &val)
12680                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12681            return Ok(PerlValue::UNDEF);
12682        }
12683        if let Some(r) = ref_val.as_scalar_ref() {
12684            *r.write() = val;
12685            return Ok(PerlValue::UNDEF);
12686        }
12687        Err(PerlError::runtime("Can't assign to non-scalar reference", line).into())
12688    }
12689
12690    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
12691    pub(crate) fn assign_symbolic_array_ref_deref(
12692        &mut self,
12693        ref_val: PerlValue,
12694        val: PerlValue,
12695        line: usize,
12696    ) -> ExecResult {
12697        if let Some(a) = ref_val.as_array_ref() {
12698            *a.write() = val.to_list();
12699            return Ok(PerlValue::UNDEF);
12700        }
12701        if let Some(name) = ref_val.as_array_binding_name() {
12702            self.scope
12703                .set_array(&name, val.to_list())
12704                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12705            return Ok(PerlValue::UNDEF);
12706        }
12707        if let Some(s) = ref_val.as_str() {
12708            if self.strict_refs {
12709                return Err(PerlError::runtime(
12710                    format!(
12711                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
12712                        s
12713                    ),
12714                    line,
12715                )
12716                .into());
12717            }
12718            self.scope
12719                .set_array(&s, val.to_list())
12720                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12721            return Ok(PerlValue::UNDEF);
12722        }
12723        Err(PerlError::runtime("Can't assign to non-array reference", line).into())
12724    }
12725
12726    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
12727    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
12728    pub(crate) fn assign_symbolic_typeglob_ref_deref(
12729        &mut self,
12730        ref_val: PerlValue,
12731        val: PerlValue,
12732        line: usize,
12733    ) -> ExecResult {
12734        let lhs_name = if let Some(s) = ref_val.as_str() {
12735            if self.strict_refs {
12736                return Err(PerlError::runtime(
12737                    format!(
12738                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
12739                        s
12740                    ),
12741                    line,
12742                )
12743                .into());
12744            }
12745            s.to_string()
12746        } else {
12747            return Err(
12748                PerlError::runtime("Can't assign to non-glob symbolic reference", line).into(),
12749            );
12750        };
12751        let is_coderef = val.as_code_ref().is_some()
12752            || val
12753                .as_scalar_ref()
12754                .map(|r| r.read().as_code_ref().is_some())
12755                .unwrap_or(false);
12756        if is_coderef {
12757            return self.assign_typeglob_value(&lhs_name, val, line);
12758        }
12759        let rhs_key = val.to_string();
12760        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
12761            .map_err(FlowOrError::Error)?;
12762        Ok(PerlValue::UNDEF)
12763    }
12764
12765    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
12766    pub(crate) fn assign_symbolic_hash_ref_deref(
12767        &mut self,
12768        ref_val: PerlValue,
12769        val: PerlValue,
12770        line: usize,
12771    ) -> ExecResult {
12772        let items = val.to_list();
12773        let mut map = IndexMap::new();
12774        let mut i = 0;
12775        while i + 1 < items.len() {
12776            map.insert(items[i].to_string(), items[i + 1].clone());
12777            i += 2;
12778        }
12779        if let Some(h) = ref_val.as_hash_ref() {
12780            *h.write() = map;
12781            return Ok(PerlValue::UNDEF);
12782        }
12783        if let Some(name) = ref_val.as_hash_binding_name() {
12784            self.touch_env_hash(&name);
12785            self.scope
12786                .set_hash(&name, map)
12787                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12788            return Ok(PerlValue::UNDEF);
12789        }
12790        if let Some(s) = ref_val.as_str() {
12791            if self.strict_refs {
12792                return Err(PerlError::runtime(
12793                    format!(
12794                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
12795                        s
12796                    ),
12797                    line,
12798                )
12799                .into());
12800            }
12801            self.touch_env_hash(&s);
12802            self.scope
12803                .set_hash(&s, map)
12804                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12805            return Ok(PerlValue::UNDEF);
12806        }
12807        Err(PerlError::runtime("Can't assign to non-hash reference", line).into())
12808    }
12809
12810    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
12811    pub(crate) fn assign_arrow_hash_deref(
12812        &mut self,
12813        container: PerlValue,
12814        key: String,
12815        val: PerlValue,
12816        line: usize,
12817    ) -> ExecResult {
12818        if let Some(b) = container.as_blessed_ref() {
12819            let mut data = b.data.write();
12820            if let Some(r) = data.as_hash_ref() {
12821                r.write().insert(key, val);
12822                return Ok(PerlValue::UNDEF);
12823            }
12824            if let Some(mut map) = data.as_hash_map() {
12825                map.insert(key, val);
12826                *data = PerlValue::hash(map);
12827                return Ok(PerlValue::UNDEF);
12828            }
12829            return Err(PerlError::runtime("Can't assign into non-hash blessed ref", line).into());
12830        }
12831        if let Some(r) = container.as_hash_ref() {
12832            r.write().insert(key, val);
12833            return Ok(PerlValue::UNDEF);
12834        }
12835        if let Some(name) = container.as_hash_binding_name() {
12836            self.touch_env_hash(&name);
12837            self.scope
12838                .set_hash_element(&name, &key, val)
12839                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12840            return Ok(PerlValue::UNDEF);
12841        }
12842        Err(PerlError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
12843    }
12844
12845    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
12846    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
12847    pub(crate) fn eval_arrow_array_base(
12848        &mut self,
12849        expr: &Expr,
12850        _line: usize,
12851    ) -> Result<PerlValue, FlowOrError> {
12852        match &expr.kind {
12853            ExprKind::Deref {
12854                expr: inner,
12855                kind: Sigil::Array | Sigil::Scalar,
12856            } => self.eval_expr(inner),
12857            _ => self.eval_expr(expr),
12858        }
12859    }
12860
12861    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
12862    pub(crate) fn eval_arrow_hash_base(
12863        &mut self,
12864        expr: &Expr,
12865        _line: usize,
12866    ) -> Result<PerlValue, FlowOrError> {
12867        match &expr.kind {
12868            ExprKind::Deref {
12869                expr: inner,
12870                kind: Sigil::Scalar,
12871            } => self.eval_expr(inner),
12872            _ => self.eval_expr(expr),
12873        }
12874    }
12875
12876    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
12877    pub(crate) fn read_arrow_array_element(
12878        &self,
12879        container: PerlValue,
12880        idx: i64,
12881        line: usize,
12882    ) -> Result<PerlValue, FlowOrError> {
12883        if let Some(a) = container.as_array_ref() {
12884            let arr = a.read();
12885            let i = if idx < 0 {
12886                (arr.len() as i64 + idx) as usize
12887            } else {
12888                idx as usize
12889            };
12890            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12891        }
12892        if let Some(name) = container.as_array_binding_name() {
12893            return Ok(self.scope.get_array_element(&name, idx));
12894        }
12895        if let Some(arr) = container.as_array_vec() {
12896            let i = if idx < 0 {
12897                (arr.len() as i64 + idx) as usize
12898            } else {
12899                idx as usize
12900            };
12901            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12902        }
12903        // Blessed arrayref (e.g. `List::Util::_Pair`) — Perl allows `->[N]` on
12904        // blessed arrayrefs; `pairs` returns blessed `_Pair` objects that the
12905        // doc shows being indexed via `$_->[0]` / `$_->[1]`.
12906        if let Some(b) = container.as_blessed_ref() {
12907            let inner = b.data.read().clone();
12908            if let Some(a) = inner.as_array_ref() {
12909                let arr = a.read();
12910                let i = if idx < 0 {
12911                    (arr.len() as i64 + idx) as usize
12912                } else {
12913                    idx as usize
12914                };
12915                return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12916            }
12917        }
12918        Err(PerlError::runtime("Can't use arrow deref on non-array-ref", line).into())
12919    }
12920
12921    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
12922    pub(crate) fn read_arrow_hash_element(
12923        &mut self,
12924        container: PerlValue,
12925        key: &str,
12926        line: usize,
12927    ) -> Result<PerlValue, FlowOrError> {
12928        if let Some(r) = container.as_hash_ref() {
12929            let h = r.read();
12930            return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12931        }
12932        if let Some(name) = container.as_hash_binding_name() {
12933            self.touch_env_hash(&name);
12934            return Ok(self.scope.get_hash_element(&name, key));
12935        }
12936        if let Some(b) = container.as_blessed_ref() {
12937            let data = b.data.read();
12938            if let Some(v) = data.hash_get(key) {
12939                return Ok(v);
12940            }
12941            if let Some(r) = data.as_hash_ref() {
12942                let h = r.read();
12943                return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12944            }
12945            return Err(PerlError::runtime(
12946                "Can't access hash field on non-hash blessed ref",
12947                line,
12948            )
12949            .into());
12950        }
12951        // Struct field access via hash deref syntax: $struct->{field}
12952        if let Some(s) = container.as_struct_inst() {
12953            if let Some(idx) = s.def.field_index(key) {
12954                return Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF));
12955            }
12956            return Err(PerlError::runtime(
12957                format!("struct {} has no field `{}`", s.def.name, key),
12958                line,
12959            )
12960            .into());
12961        }
12962        // Class instance field access via hash deref: $obj->{field}
12963        if let Some(c) = container.as_class_inst() {
12964            if let Some(idx) = c.def.field_index(key) {
12965                return Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF));
12966            }
12967            return Err(PerlError::runtime(
12968                format!("class {} has no field `{}`", c.def.name, key),
12969                line,
12970            )
12971            .into());
12972        }
12973        Err(PerlError::runtime("Can't use arrow deref on non-hash-ref", line).into())
12974    }
12975
12976    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
12977    pub(crate) fn arrow_array_postfix(
12978        &mut self,
12979        container: PerlValue,
12980        idx: i64,
12981        decrement: bool,
12982        line: usize,
12983    ) -> Result<PerlValue, FlowOrError> {
12984        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
12985        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12986        self.assign_arrow_array_deref(container, idx, new_val, line)?;
12987        Ok(old)
12988    }
12989
12990    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
12991    pub(crate) fn arrow_hash_postfix(
12992        &mut self,
12993        container: PerlValue,
12994        key: String,
12995        decrement: bool,
12996        line: usize,
12997    ) -> Result<PerlValue, FlowOrError> {
12998        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
12999        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
13000        self.assign_arrow_hash_deref(container, key, new_val, line)?;
13001        Ok(old)
13002    }
13003
13004    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` in the tree walker. If a nullary
13005    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
13006    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
13007    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
13008    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
13009    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
13010    /// subs" in use`).
13011    pub(crate) fn resolve_bareword_rvalue(
13012        &mut self,
13013        name: &str,
13014        want: WantarrayCtx,
13015        line: usize,
13016    ) -> Result<PerlValue, FlowOrError> {
13017        if name == "__PACKAGE__" {
13018            return Ok(PerlValue::string(self.current_package()));
13019        }
13020        if let Some(sub) = self.resolve_sub_by_name(name) {
13021            return self.call_sub(&sub, vec![], want, line);
13022        }
13023        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
13024        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
13025            return r.map_err(Into::into);
13026        }
13027        Ok(PerlValue::string(name.to_string()))
13028    }
13029
13030    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
13031    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
13032    /// compound / inc-dec / assign helpers below.
13033    pub(crate) fn arrow_array_slice_values(
13034        &mut self,
13035        container: PerlValue,
13036        indices: &[i64],
13037        line: usize,
13038    ) -> Result<PerlValue, FlowOrError> {
13039        let mut out = Vec::with_capacity(indices.len());
13040        for &idx in indices {
13041            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
13042            out.push(v);
13043        }
13044        Ok(PerlValue::array(out))
13045    }
13046
13047    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment matching the tree-walker
13048    /// `assign_value` path for multi-index `ArrowDeref { Array, List }`. Shared by the VM
13049    /// [`crate::bytecode::Op::SetArrowArraySlice`].
13050    pub(crate) fn assign_arrow_array_slice(
13051        &mut self,
13052        container: PerlValue,
13053        indices: Vec<i64>,
13054        val: PerlValue,
13055        line: usize,
13056    ) -> Result<PerlValue, FlowOrError> {
13057        if indices.is_empty() {
13058            return Err(PerlError::runtime("assign to empty array slice", line).into());
13059        }
13060        let vals = val.to_list();
13061        for (i, idx) in indices.iter().enumerate() {
13062            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
13063            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
13064        }
13065        Ok(PerlValue::UNDEF)
13066    }
13067
13068    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
13069    pub(crate) fn flatten_array_slice_index_specs(
13070        &mut self,
13071        indices: &[Expr],
13072    ) -> Result<Vec<i64>, FlowOrError> {
13073        let mut out = Vec::new();
13074        for idx_expr in indices {
13075            let v = if matches!(idx_expr.kind, ExprKind::Range { .. }) {
13076                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
13077            } else {
13078                self.eval_expr(idx_expr)?
13079            };
13080            if let Some(list) = v.as_array_vec() {
13081                for idx in list {
13082                    out.push(idx.to_int());
13083                }
13084            } else {
13085                out.push(v.to_int());
13086            }
13087        }
13088        Ok(out)
13089    }
13090
13091    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
13092    pub(crate) fn assign_named_array_slice(
13093        &mut self,
13094        stash_array_name: &str,
13095        indices: Vec<i64>,
13096        val: PerlValue,
13097        line: usize,
13098    ) -> Result<PerlValue, FlowOrError> {
13099        if indices.is_empty() {
13100            return Err(PerlError::runtime("assign to empty array slice", line).into());
13101        }
13102        let vals = val.to_list();
13103        for (i, idx) in indices.iter().enumerate() {
13104            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
13105            self.scope
13106                .set_array_element(stash_array_name, *idx, v)
13107                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13108        }
13109        Ok(PerlValue::UNDEF)
13110    }
13111
13112    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
13113    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
13114    pub(crate) fn compound_assign_arrow_array_slice(
13115        &mut self,
13116        container: PerlValue,
13117        indices: Vec<i64>,
13118        op: BinOp,
13119        rhs: PerlValue,
13120        line: usize,
13121    ) -> Result<PerlValue, FlowOrError> {
13122        if indices.is_empty() {
13123            return Err(PerlError::runtime("assign to empty array slice", line).into());
13124        }
13125        let last_idx = *indices.last().expect("non-empty indices");
13126        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
13127        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
13128        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
13129        Ok(new_val)
13130    }
13131
13132    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
13133    /// pre forms return the new value, post forms return the old **last** element.
13134    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
13135    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
13136    pub(crate) fn arrow_array_slice_inc_dec(
13137        &mut self,
13138        container: PerlValue,
13139        indices: Vec<i64>,
13140        kind: u8,
13141        line: usize,
13142    ) -> Result<PerlValue, FlowOrError> {
13143        if indices.is_empty() {
13144            return Err(
13145                PerlError::runtime("array slice increment needs at least one index", line).into(),
13146            );
13147        }
13148        let last_idx = *indices.last().expect("non-empty indices");
13149        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
13150        let new_val = if kind & 1 == 0 {
13151            PerlValue::integer(last_old.to_int() + 1)
13152        } else {
13153            PerlValue::integer(last_old.to_int() - 1)
13154        };
13155        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
13156        Ok(if kind < 2 { new_val } else { last_old })
13157    }
13158
13159    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
13160    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
13161    pub(crate) fn named_array_slice_inc_dec(
13162        &mut self,
13163        stash_array_name: &str,
13164        indices: Vec<i64>,
13165        kind: u8,
13166        line: usize,
13167    ) -> Result<PerlValue, FlowOrError> {
13168        let last_idx = *indices.last().ok_or_else(|| {
13169            PerlError::runtime("array slice increment needs at least one index", line)
13170        })?;
13171        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
13172        let new_val = if kind & 1 == 0 {
13173            PerlValue::integer(last_old.to_int() + 1)
13174        } else {
13175            PerlValue::integer(last_old.to_int() - 1)
13176        };
13177        self.scope
13178            .set_array_element(stash_array_name, last_idx, new_val.clone())
13179            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13180        Ok(if kind < 2 { new_val } else { last_old })
13181    }
13182
13183    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
13184    pub(crate) fn compound_assign_named_array_slice(
13185        &mut self,
13186        stash_array_name: &str,
13187        indices: Vec<i64>,
13188        op: BinOp,
13189        rhs: PerlValue,
13190        line: usize,
13191    ) -> Result<PerlValue, FlowOrError> {
13192        if indices.is_empty() {
13193            return Err(PerlError::runtime("assign to empty array slice", line).into());
13194        }
13195        let last_idx = *indices.last().expect("non-empty indices");
13196        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
13197        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
13198        self.scope
13199            .set_array_element(stash_array_name, last_idx, new_val.clone())
13200            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13201        Ok(new_val)
13202    }
13203
13204    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
13205    pub(crate) fn assign_arrow_array_deref(
13206        &mut self,
13207        container: PerlValue,
13208        idx: i64,
13209        val: PerlValue,
13210        line: usize,
13211    ) -> ExecResult {
13212        if let Some(a) = container.as_array_ref() {
13213            let mut arr = a.write();
13214            let i = if idx < 0 {
13215                (arr.len() as i64 + idx) as usize
13216            } else {
13217                idx as usize
13218            };
13219            if i >= arr.len() {
13220                arr.resize(i + 1, PerlValue::UNDEF);
13221            }
13222            arr[i] = val;
13223            return Ok(PerlValue::UNDEF);
13224        }
13225        if let Some(name) = container.as_array_binding_name() {
13226            self.scope
13227                .set_array_element(&name, idx, val)
13228                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
13229            return Ok(PerlValue::UNDEF);
13230        }
13231        Err(PerlError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
13232    }
13233
13234    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
13235    pub(crate) fn assign_typeglob_value(
13236        &mut self,
13237        name: &str,
13238        val: PerlValue,
13239        line: usize,
13240    ) -> ExecResult {
13241        let sub = if let Some(c) = val.as_code_ref() {
13242            Some(c)
13243        } else if let Some(r) = val.as_scalar_ref() {
13244            r.read().as_code_ref().map(|c| Arc::clone(&c))
13245        } else {
13246            None
13247        };
13248        if let Some(sub) = sub {
13249            let lhs_sub = self.qualify_typeglob_sub_key(name);
13250            self.subs.insert(lhs_sub, sub);
13251            return Ok(PerlValue::UNDEF);
13252        }
13253        Err(PerlError::runtime(
13254            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
13255            line,
13256        )
13257        .into())
13258    }
13259
13260    fn assign_value(&mut self, target: &Expr, val: PerlValue) -> ExecResult {
13261        match &target.kind {
13262            ExprKind::ScalarVar(name) => {
13263                let stor = self.tree_scalar_storage_name(name);
13264                if self.scope.is_scalar_frozen(&stor) {
13265                    return Err(FlowOrError::Error(PerlError::runtime(
13266                        format!("Modification of a frozen value: ${}", name),
13267                        target.line,
13268                    )));
13269                }
13270                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
13271                    let class = obj
13272                        .as_blessed_ref()
13273                        .map(|b| b.class.clone())
13274                        .unwrap_or_default();
13275                    let full = format!("{}::STORE", class);
13276                    if let Some(sub) = self.subs.get(&full).cloned() {
13277                        let arg_vals = vec![obj, val];
13278                        return match self.call_sub(
13279                            &sub,
13280                            arg_vals,
13281                            WantarrayCtx::Scalar,
13282                            target.line,
13283                        ) {
13284                            Ok(_) => Ok(PerlValue::UNDEF),
13285                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13286                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13287                        };
13288                    }
13289                }
13290                self.set_special_var(&stor, &val)
13291                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
13292                Ok(PerlValue::UNDEF)
13293            }
13294            ExprKind::ArrayVar(name) => {
13295                if self.scope.is_array_frozen(name) {
13296                    return Err(PerlError::runtime(
13297                        format!("Modification of a frozen value: @{}", name),
13298                        target.line,
13299                    )
13300                    .into());
13301                }
13302                if self.strict_vars
13303                    && !name.contains("::")
13304                    && !self.scope.array_binding_exists(name)
13305                {
13306                    return Err(PerlError::runtime(
13307                        format!(
13308                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
13309                            name, name
13310                        ),
13311                        target.line,
13312                    )
13313                    .into());
13314                }
13315                self.scope.set_array(name, val.to_list())?;
13316                Ok(PerlValue::UNDEF)
13317            }
13318            ExprKind::HashVar(name) => {
13319                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
13320                {
13321                    return Err(PerlError::runtime(
13322                        format!(
13323                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13324                            name, name
13325                        ),
13326                        target.line,
13327                    )
13328                    .into());
13329                }
13330                let items = val.to_list();
13331                let mut map = IndexMap::new();
13332                let mut i = 0;
13333                while i + 1 < items.len() {
13334                    map.insert(items[i].to_string(), items[i + 1].clone());
13335                    i += 2;
13336                }
13337                self.scope.set_hash(name, map)?;
13338                Ok(PerlValue::UNDEF)
13339            }
13340            ExprKind::ArrayElement { array, index } => {
13341                if self.strict_vars
13342                    && !array.contains("::")
13343                    && !self.scope.array_binding_exists(array)
13344                {
13345                    return Err(PerlError::runtime(
13346                        format!(
13347                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
13348                            array, array
13349                        ),
13350                        target.line,
13351                    )
13352                    .into());
13353                }
13354                if self.scope.is_array_frozen(array) {
13355                    return Err(PerlError::runtime(
13356                        format!("Modification of a frozen value: @{}", array),
13357                        target.line,
13358                    )
13359                    .into());
13360                }
13361                let idx = self.eval_expr(index)?.to_int();
13362                let aname = self.stash_array_name_for_package(array);
13363                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
13364                    let class = obj
13365                        .as_blessed_ref()
13366                        .map(|b| b.class.clone())
13367                        .unwrap_or_default();
13368                    let full = format!("{}::STORE", class);
13369                    if let Some(sub) = self.subs.get(&full).cloned() {
13370                        let arg_vals = vec![obj, PerlValue::integer(idx), val];
13371                        return match self.call_sub(
13372                            &sub,
13373                            arg_vals,
13374                            WantarrayCtx::Scalar,
13375                            target.line,
13376                        ) {
13377                            Ok(_) => Ok(PerlValue::UNDEF),
13378                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13379                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13380                        };
13381                    }
13382                }
13383                self.scope.set_array_element(&aname, idx, val)?;
13384                Ok(PerlValue::UNDEF)
13385            }
13386            ExprKind::ArraySlice { array, indices } => {
13387                if indices.is_empty() {
13388                    return Err(
13389                        PerlError::runtime("assign to empty array slice", target.line).into(),
13390                    );
13391                }
13392                self.check_strict_array_var(array, target.line)?;
13393                if self.scope.is_array_frozen(array) {
13394                    return Err(PerlError::runtime(
13395                        format!("Modification of a frozen value: @{}", array),
13396                        target.line,
13397                    )
13398                    .into());
13399                }
13400                let aname = self.stash_array_name_for_package(array);
13401                let flat = self.flatten_array_slice_index_specs(indices)?;
13402                self.assign_named_array_slice(&aname, flat, val, target.line)
13403            }
13404            ExprKind::HashElement { hash, key } => {
13405                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
13406                {
13407                    return Err(PerlError::runtime(
13408                        format!(
13409                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13410                            hash, hash
13411                        ),
13412                        target.line,
13413                    )
13414                    .into());
13415                }
13416                if self.scope.is_hash_frozen(hash) {
13417                    return Err(PerlError::runtime(
13418                        format!("Modification of a frozen value: %%{}", hash),
13419                        target.line,
13420                    )
13421                    .into());
13422                }
13423                let k = self.eval_expr(key)?.to_string();
13424                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
13425                    let class = obj
13426                        .as_blessed_ref()
13427                        .map(|b| b.class.clone())
13428                        .unwrap_or_default();
13429                    let full = format!("{}::STORE", class);
13430                    if let Some(sub) = self.subs.get(&full).cloned() {
13431                        let arg_vals = vec![obj, PerlValue::string(k), val];
13432                        return match self.call_sub(
13433                            &sub,
13434                            arg_vals,
13435                            WantarrayCtx::Scalar,
13436                            target.line,
13437                        ) {
13438                            Ok(_) => Ok(PerlValue::UNDEF),
13439                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13440                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13441                        };
13442                    }
13443                }
13444                self.scope.set_hash_element(hash, &k, val)?;
13445                Ok(PerlValue::UNDEF)
13446            }
13447            ExprKind::HashSlice { hash, keys } => {
13448                if keys.is_empty() {
13449                    return Err(
13450                        PerlError::runtime("assign to empty hash slice", target.line).into(),
13451                    );
13452                }
13453                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
13454                {
13455                    return Err(PerlError::runtime(
13456                        format!(
13457                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13458                            hash, hash
13459                        ),
13460                        target.line,
13461                    )
13462                    .into());
13463                }
13464                if self.scope.is_hash_frozen(hash) {
13465                    return Err(PerlError::runtime(
13466                        format!("Modification of a frozen value: %%{}", hash),
13467                        target.line,
13468                    )
13469                    .into());
13470                }
13471                let mut key_vals = Vec::with_capacity(keys.len());
13472                for key_expr in keys {
13473                    let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
13474                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
13475                    } else {
13476                        self.eval_expr(key_expr)?
13477                    };
13478                    key_vals.push(v);
13479                }
13480                self.assign_named_hash_slice(hash, key_vals, val, target.line)
13481            }
13482            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
13483            ExprKind::TypeglobExpr(e) => {
13484                let name = self.eval_expr(e)?.to_string();
13485                let synthetic = Expr {
13486                    kind: ExprKind::Typeglob(name),
13487                    line: target.line,
13488                };
13489                self.assign_value(&synthetic, val)
13490            }
13491            ExprKind::AnonymousListSlice { source, indices } => {
13492                if let ExprKind::Deref {
13493                    expr: inner,
13494                    kind: Sigil::Array,
13495                } = &source.kind
13496                {
13497                    let container = self.eval_arrow_array_base(inner, target.line)?;
13498                    let vals = val.to_list();
13499                    let n = indices.len().min(vals.len());
13500                    for i in 0..n {
13501                        let idx = self.eval_expr(&indices[i])?.to_int();
13502                        self.assign_arrow_array_deref(
13503                            container.clone(),
13504                            idx,
13505                            vals[i].clone(),
13506                            target.line,
13507                        )?;
13508                    }
13509                    return Ok(PerlValue::UNDEF);
13510                }
13511                Err(
13512                    PerlError::runtime("assign to list slice: unsupported base", target.line)
13513                        .into(),
13514                )
13515            }
13516            ExprKind::ArrowDeref {
13517                expr,
13518                index,
13519                kind: DerefKind::Hash,
13520            } => {
13521                let key = self.eval_expr(index)?.to_string();
13522                let container = self.eval_expr(expr)?;
13523                self.assign_arrow_hash_deref(container, key, val, target.line)
13524            }
13525            ExprKind::ArrowDeref {
13526                expr,
13527                index,
13528                kind: DerefKind::Array,
13529            } => {
13530                let container = self.eval_arrow_array_base(expr, target.line)?;
13531                if let ExprKind::List(indices) = &index.kind {
13532                    let vals = val.to_list();
13533                    let n = indices.len().min(vals.len());
13534                    for i in 0..n {
13535                        let idx = self.eval_expr(&indices[i])?.to_int();
13536                        self.assign_arrow_array_deref(
13537                            container.clone(),
13538                            idx,
13539                            vals[i].clone(),
13540                            target.line,
13541                        )?;
13542                    }
13543                    return Ok(PerlValue::UNDEF);
13544                }
13545                let idx = self.eval_expr(index)?.to_int();
13546                self.assign_arrow_array_deref(container, idx, val, target.line)
13547            }
13548            ExprKind::HashSliceDeref { container, keys } => {
13549                let href = self.eval_expr(container)?;
13550                let mut key_vals = Vec::with_capacity(keys.len());
13551                for key_expr in keys {
13552                    key_vals.push(self.eval_expr(key_expr)?);
13553                }
13554                self.assign_hash_slice_deref(href, key_vals, val, target.line)
13555            }
13556            ExprKind::Deref {
13557                expr,
13558                kind: Sigil::Scalar,
13559            } => {
13560                let ref_val = self.eval_expr(expr)?;
13561                self.assign_scalar_ref_deref(ref_val, val, target.line)
13562            }
13563            ExprKind::Deref {
13564                expr,
13565                kind: Sigil::Array,
13566            } => {
13567                let ref_val = self.eval_expr(expr)?;
13568                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
13569            }
13570            ExprKind::Deref {
13571                expr,
13572                kind: Sigil::Hash,
13573            } => {
13574                let ref_val = self.eval_expr(expr)?;
13575                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
13576            }
13577            ExprKind::Deref {
13578                expr,
13579                kind: Sigil::Typeglob,
13580            } => {
13581                let ref_val = self.eval_expr(expr)?;
13582                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
13583            }
13584            ExprKind::Pos(inner) => {
13585                let key = match inner {
13586                    None => "_".to_string(),
13587                    Some(expr) => match &expr.kind {
13588                        ExprKind::ScalarVar(n) => n.clone(),
13589                        _ => self.eval_expr(expr)?.to_string(),
13590                    },
13591                };
13592                if val.is_undef() {
13593                    self.regex_pos.insert(key, None);
13594                } else {
13595                    let u = val.to_int().max(0) as usize;
13596                    self.regex_pos.insert(key, Some(u));
13597                }
13598                Ok(PerlValue::UNDEF)
13599            }
13600            // List assignment: `($a, $b, ...) = (val1, val2, ...)`
13601            // RHS is already fully evaluated — distribute elements to targets.
13602            ExprKind::List(targets) => {
13603                let items = val.to_list();
13604                for (i, t) in targets.iter().enumerate() {
13605                    let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
13606                    self.assign_value(t, v)?;
13607                }
13608                Ok(PerlValue::UNDEF)
13609            }
13610            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
13611            // write the substitution result back to the assignment target.
13612            ExprKind::Assign { target, .. } => self.assign_value(target, val),
13613            _ => Ok(PerlValue::UNDEF),
13614        }
13615    }
13616
13617    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
13618    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
13619        (name.starts_with('#') && name.len() > 1)
13620            || name.starts_with('^')
13621            || matches!(
13622                name,
13623                "$$" | "0"
13624                    | "!"
13625                    | "@"
13626                    | "/"
13627                    | "\\"
13628                    | ","
13629                    | "."
13630                    | "]"
13631                    | ";"
13632                    | "ARGV"
13633                    | "^I"
13634                    | "^D"
13635                    | "^P"
13636                    | "^S"
13637                    | "^W"
13638                    | "^O"
13639                    | "^T"
13640                    | "^V"
13641                    | "^E"
13642                    | "^H"
13643                    | "^WARNING_BITS"
13644                    | "^GLOBAL_PHASE"
13645                    | "^MATCH"
13646                    | "^PREMATCH"
13647                    | "^POSTMATCH"
13648                    | "^LAST_SUBMATCH_RESULT"
13649                    | "<"
13650                    | ">"
13651                    | "("
13652                    | ")"
13653                    | "?"
13654                    | "|"
13655                    | "\""
13656                    | "+"
13657                    | "%"
13658                    | "="
13659                    | "-"
13660                    | ":"
13661                    | "*"
13662                    | "INC"
13663            )
13664            || crate::english::is_known_alias(name)
13665    }
13666
13667    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
13668    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
13669    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
13670    /// [`Self::english_no_match_vars`] is set.
13671    #[inline]
13672    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
13673        if !self.english_enabled {
13674            return name;
13675        }
13676        if self
13677            .english_lexical_scalars
13678            .iter()
13679            .any(|s| s.contains(name))
13680        {
13681            return name;
13682        }
13683        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
13684            return short;
13685        }
13686        name
13687    }
13688
13689    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
13690    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
13691        name.starts_with('^')
13692            || matches!(
13693                name,
13694                "0" | "/"
13695                    | "\\"
13696                    | ","
13697                    | ";"
13698                    | "\""
13699                    | "%"
13700                    | "="
13701                    | "-"
13702                    | ":"
13703                    | "*"
13704                    | "INC"
13705                    | "^I"
13706                    | "^D"
13707                    | "^P"
13708                    | "^W"
13709                    | "^H"
13710                    | "^WARNING_BITS"
13711                    | "$$"
13712                    | "]"
13713                    | "^S"
13714                    | "ARGV"
13715                    | "|"
13716                    | "+"
13717                    | "?"
13718                    | "!"
13719                    | "@"
13720                    | "."
13721            )
13722            || crate::english::is_known_alias(name)
13723    }
13724
13725    pub(crate) fn get_special_var(&self, name: &str) -> PerlValue {
13726        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
13727        let name = if !crate::compat_mode() {
13728            match name {
13729                "NR" => ".",
13730                "RS" => "/",
13731                "OFS" => ",",
13732                "ORS" => "\\",
13733                "NF" => {
13734                    let len = self.scope.array_len("F");
13735                    return PerlValue::integer(len as i64);
13736                }
13737                _ => self.english_scalar_name(name),
13738            }
13739        } else {
13740            self.english_scalar_name(name)
13741        };
13742        match name {
13743            "$$" => PerlValue::integer(std::process::id() as i64),
13744            "_" => self.scope.get_scalar("_"),
13745            "^MATCH" => PerlValue::string(self.last_match.clone()),
13746            "^PREMATCH" => PerlValue::string(self.prematch.clone()),
13747            "^POSTMATCH" => PerlValue::string(self.postmatch.clone()),
13748            "^LAST_SUBMATCH_RESULT" => PerlValue::string(self.last_paren_match.clone()),
13749            "0" => PerlValue::string(self.program_name.clone()),
13750            "!" => PerlValue::errno_dual(self.errno_code, self.errno.clone()),
13751            "@" => {
13752                if let Some(ref v) = self.eval_error_value {
13753                    v.clone()
13754                } else {
13755                    PerlValue::errno_dual(self.eval_error_code, self.eval_error.clone())
13756                }
13757            }
13758            "/" => match &self.irs {
13759                Some(s) => PerlValue::string(s.clone()),
13760                None => PerlValue::UNDEF,
13761            },
13762            "\\" => PerlValue::string(self.ors.clone()),
13763            "," => PerlValue::string(self.ofs.clone()),
13764            "." => {
13765                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
13766                if self.last_readline_handle.is_empty() {
13767                    if self.line_number == 0 {
13768                        PerlValue::UNDEF
13769                    } else {
13770                        PerlValue::integer(self.line_number)
13771                    }
13772                } else {
13773                    PerlValue::integer(
13774                        *self
13775                            .handle_line_numbers
13776                            .get(&self.last_readline_handle)
13777                            .unwrap_or(&0),
13778                    )
13779                }
13780            }
13781            "]" => PerlValue::float(perl_bracket_version()),
13782            ";" => PerlValue::string(self.subscript_sep.clone()),
13783            "ARGV" => PerlValue::string(self.argv_current_file.clone()),
13784            "^I" => PerlValue::string(self.inplace_edit.clone()),
13785            "^D" => PerlValue::integer(self.debug_flags),
13786            "^P" => PerlValue::integer(self.perl_debug_flags),
13787            "^S" => PerlValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
13788            "^W" => PerlValue::integer(if self.warnings { 1 } else { 0 }),
13789            "^O" => PerlValue::string(perl_osname()),
13790            "^T" => PerlValue::integer(self.script_start_time),
13791            "^V" => PerlValue::string(perl_version_v_string()),
13792            "^E" => PerlValue::string(extended_os_error_string()),
13793            "^H" => PerlValue::integer(self.compile_hints),
13794            "^WARNING_BITS" => PerlValue::integer(self.warning_bits),
13795            "^GLOBAL_PHASE" => PerlValue::string(self.global_phase.clone()),
13796            "<" | ">" => PerlValue::integer(unix_id_for_special(name)),
13797            "(" | ")" => PerlValue::string(unix_group_list_for_special(name)),
13798            "?" => PerlValue::integer(self.child_exit_status),
13799            "|" => PerlValue::integer(if self.output_autoflush { 1 } else { 0 }),
13800            "\"" => PerlValue::string(self.list_separator.clone()),
13801            "+" => PerlValue::string(self.last_paren_match.clone()),
13802            "%" => PerlValue::integer(self.format_page_number),
13803            "=" => PerlValue::integer(self.format_lines_per_page),
13804            "-" => PerlValue::integer(self.format_lines_left),
13805            ":" => PerlValue::string(self.format_line_break_chars.clone()),
13806            "*" => PerlValue::integer(if self.multiline_match { 1 } else { 0 }),
13807            "^" => PerlValue::string(self.format_top_name.clone()),
13808            "INC" => PerlValue::integer(self.inc_hook_index),
13809            "^A" => PerlValue::string(self.accumulator_format.clone()),
13810            "^C" => PerlValue::integer(if self.sigint_pending_caret.replace(false) {
13811                1
13812            } else {
13813                0
13814            }),
13815            "^F" => PerlValue::integer(self.max_system_fd),
13816            "^L" => PerlValue::string(self.formfeed_string.clone()),
13817            "^M" => PerlValue::string(self.emergency_memory.clone()),
13818            "^N" => PerlValue::string(self.last_subpattern_name.clone()),
13819            "^X" => PerlValue::string(self.executable_path.clone()),
13820            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
13821            "^TAINT" | "^TAINTED" => PerlValue::integer(0),
13822            "^UNICODE" => PerlValue::integer(if self.utf8_pragma { 1 } else { 0 }),
13823            "^OPEN" => PerlValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
13824            "^UTF8LOCALE" => PerlValue::integer(0),
13825            "^UTF8CACHE" => PerlValue::integer(-1),
13826            _ if name.starts_with('^') && name.len() > 1 => self
13827                .special_caret_scalars
13828                .get(name)
13829                .cloned()
13830                .unwrap_or(PerlValue::UNDEF),
13831            _ if name.starts_with('#') && name.len() > 1 => {
13832                let arr = &name[1..];
13833                let aname = self.stash_array_name_for_package(arr);
13834                let len = self.scope.array_len(&aname);
13835                PerlValue::integer(len as i64 - 1)
13836            }
13837            _ => self.scope.get_scalar(name),
13838        }
13839    }
13840
13841    pub(crate) fn set_special_var(&mut self, name: &str, val: &PerlValue) -> Result<(), PerlError> {
13842        let name = self.english_scalar_name(name);
13843        match name {
13844            "!" => {
13845                let code = val.to_int() as i32;
13846                self.errno_code = code;
13847                self.errno = if code == 0 {
13848                    String::new()
13849                } else {
13850                    std::io::Error::from_raw_os_error(code).to_string()
13851                };
13852            }
13853            "@" => {
13854                if let Some((code, msg)) = val.errno_dual_parts() {
13855                    self.eval_error_code = code;
13856                    self.eval_error = msg;
13857                } else {
13858                    self.eval_error = val.to_string();
13859                    let mut code = val.to_int() as i32;
13860                    if code == 0 && !self.eval_error.is_empty() {
13861                        code = 1;
13862                    }
13863                    self.eval_error_code = code;
13864                }
13865            }
13866            "." => {
13867                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
13868                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
13869                let n = val.to_int();
13870                if self.last_readline_handle.is_empty() {
13871                    self.line_number = n;
13872                } else {
13873                    self.handle_line_numbers
13874                        .insert(self.last_readline_handle.clone(), n);
13875                }
13876            }
13877            "0" => self.program_name = val.to_string(),
13878            "/" => {
13879                self.irs = if val.is_undef() {
13880                    None
13881                } else {
13882                    Some(val.to_string())
13883                }
13884            }
13885            "\\" => self.ors = val.to_string(),
13886            "," => self.ofs = val.to_string(),
13887            ";" => self.subscript_sep = val.to_string(),
13888            "\"" => self.list_separator = val.to_string(),
13889            "%" => self.format_page_number = val.to_int(),
13890            "=" => self.format_lines_per_page = val.to_int(),
13891            "-" => self.format_lines_left = val.to_int(),
13892            ":" => self.format_line_break_chars = val.to_string(),
13893            "*" => self.multiline_match = val.to_int() != 0,
13894            "^" => self.format_top_name = val.to_string(),
13895            "INC" => self.inc_hook_index = val.to_int(),
13896            "^A" => self.accumulator_format = val.to_string(),
13897            "^F" => self.max_system_fd = val.to_int(),
13898            "^L" => self.formfeed_string = val.to_string(),
13899            "^M" => self.emergency_memory = val.to_string(),
13900            "^I" => self.inplace_edit = val.to_string(),
13901            "^D" => self.debug_flags = val.to_int(),
13902            "^P" => self.perl_debug_flags = val.to_int(),
13903            "^W" => self.warnings = val.to_int() != 0,
13904            "^H" => self.compile_hints = val.to_int(),
13905            "^WARNING_BITS" => self.warning_bits = val.to_int(),
13906            "|" => {
13907                self.output_autoflush = val.to_int() != 0;
13908                if self.output_autoflush {
13909                    let _ = io::stdout().flush();
13910                }
13911            }
13912            // Read-only or pid-backed
13913            "$$"
13914            | "]"
13915            | "^S"
13916            | "ARGV"
13917            | "?"
13918            | "^O"
13919            | "^T"
13920            | "^V"
13921            | "^E"
13922            | "^GLOBAL_PHASE"
13923            | "^MATCH"
13924            | "^PREMATCH"
13925            | "^POSTMATCH"
13926            | "^LAST_SUBMATCH_RESULT"
13927            | "^C"
13928            | "^N"
13929            | "^X"
13930            | "^TAINT"
13931            | "^TAINTED"
13932            | "^UNICODE"
13933            | "^UTF8LOCALE"
13934            | "^UTF8CACHE"
13935            | "+"
13936            | "<"
13937            | ">"
13938            | "("
13939            | ")" => {}
13940            _ if name.starts_with('^') && name.len() > 1 => {
13941                self.special_caret_scalars
13942                    .insert(name.to_string(), val.clone());
13943            }
13944            _ => self.scope.set_scalar(name, val.clone())?,
13945        }
13946        Ok(())
13947    }
13948
13949    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
13950        match &expr.kind {
13951            ExprKind::ArrayVar(name) => Ok(name.clone()),
13952            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
13953            _ => Err(PerlError::runtime("Expected array", expr.line).into()),
13954        }
13955    }
13956
13957    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
13958    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
13959        match &expr.kind {
13960            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
13961            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
13962            _ => expr,
13963        }
13964    }
13965
13966    /// `@$aref` / `@{...}` after optional peeling — for tree `SpliceExpr` / `pop` fallbacks.
13967    fn try_eval_array_deref_container(
13968        &mut self,
13969        expr: &Expr,
13970    ) -> Result<Option<PerlValue>, FlowOrError> {
13971        let e = Self::peel_array_builtin_operand(expr);
13972        if let ExprKind::Deref {
13973            expr: inner,
13974            kind: Sigil::Array,
13975        } = &e.kind
13976        {
13977            return Ok(Some(self.eval_expr(inner)?));
13978        }
13979        Ok(None)
13980    }
13981
13982    /// Current package (`main` when `__PACKAGE__` is unset or empty).
13983    fn current_package(&self) -> String {
13984        let s = self.scope.get_scalar("__PACKAGE__").to_string();
13985        if s.is_empty() {
13986            "main".to_string()
13987        } else {
13988            s
13989        }
13990    }
13991
13992    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
13993    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
13994    pub(crate) fn package_version_scalar(
13995        &mut self,
13996        package: &str,
13997    ) -> PerlResult<Option<PerlValue>> {
13998        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
13999        let _ = self
14000            .scope
14001            .set_scalar("__PACKAGE__", PerlValue::string(package.to_string()));
14002        let ver = self.get_special_var("VERSION");
14003        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
14004        Ok(if ver.is_undef() { None } else { Some(ver) })
14005    }
14006
14007    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
14008    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<PerlSub>> {
14009        let root = if start_package.is_empty() {
14010            "main"
14011        } else {
14012            start_package
14013        };
14014        for pkg in self.mro_linearize(root) {
14015            let key = if pkg == "main" {
14016                "AUTOLOAD".to_string()
14017            } else {
14018                format!("{}::AUTOLOAD", pkg)
14019            };
14020            if let Some(s) = self.subs.get(&key) {
14021                return Some(s.clone());
14022            }
14023        }
14024        None
14025    }
14026
14027    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
14028    /// qualified missing sub or method name and invoke the handler (same argument list as the
14029    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
14030    /// the package prefix of the missing name (or current package).
14031    pub(crate) fn try_autoload_call(
14032        &mut self,
14033        missing_name: &str,
14034        args: Vec<PerlValue>,
14035        line: usize,
14036        want: WantarrayCtx,
14037        method_invocant_class: Option<&str>,
14038    ) -> Option<ExecResult> {
14039        let pkg = self.current_package();
14040        let full = if missing_name.contains("::") {
14041            missing_name.to_string()
14042        } else {
14043            format!("{}::{}", pkg, missing_name)
14044        };
14045        let start_pkg = method_invocant_class.unwrap_or_else(|| {
14046            full.rsplit_once("::")
14047                .map(|(p, _)| p)
14048                .filter(|p| !p.is_empty())
14049                .unwrap_or("main")
14050        });
14051        let sub = self.resolve_autoload_sub(start_pkg)?;
14052        if let Err(e) = self
14053            .scope
14054            .set_scalar("AUTOLOAD", PerlValue::string(full.clone()))
14055        {
14056            return Some(Err(e.into()));
14057        }
14058        Some(self.call_sub(&sub, args, want, line))
14059    }
14060
14061    pub(crate) fn with_topic_default_args(&self, args: Vec<PerlValue>) -> Vec<PerlValue> {
14062        if args.is_empty() {
14063            vec![self.scope.get_scalar("_").clone()]
14064        } else {
14065            args
14066        }
14067    }
14068
14069    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
14070    /// and [`crate::bytecode::Op::IndirectCall`].
14071    pub(crate) fn dispatch_indirect_call(
14072        &mut self,
14073        target: PerlValue,
14074        arg_vals: Vec<PerlValue>,
14075        want: WantarrayCtx,
14076        line: usize,
14077    ) -> ExecResult {
14078        if let Some(sub) = target.as_code_ref() {
14079            return self.call_sub(&sub, arg_vals, want, line);
14080        }
14081        if let Some(name) = target.as_str() {
14082            return self.call_named_sub(&name, arg_vals, line, want);
14083        }
14084        Err(PerlError::runtime("Can't use non-code reference as a subroutine", line).into())
14085    }
14086
14087    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
14088    /// `sum` / `sum0` /
14089    /// `product` / `min` / `max` / `mean` / `median` / `mode` / `stddev` / `variance` /
14090    /// `any` / `all` / `none` / `first` (Ruby `detect` / `find` parse to `first`; same as `List::Util` after
14091    /// [`crate::list_util::ensure_list_util`]).
14092    pub(crate) fn call_bare_list_util(
14093        &mut self,
14094        name: &str,
14095        args: Vec<PerlValue>,
14096        line: usize,
14097        want: WantarrayCtx,
14098    ) -> ExecResult {
14099        crate::list_util::ensure_list_util(self);
14100        let fq = match name {
14101            "uniq" | "distinct" | "uq" => "List::Util::uniq",
14102            "uniqstr" => "List::Util::uniqstr",
14103            "uniqint" => "List::Util::uniqint",
14104            "uniqnum" => "List::Util::uniqnum",
14105            "shuffle" | "shuf" => "List::Util::shuffle",
14106            "sample" => "List::Util::sample",
14107            "chunked" | "chk" => "List::Util::chunked",
14108            "windowed" | "win" => "List::Util::windowed",
14109            "zip" | "zp" => "List::Util::zip",
14110            "zip_longest" => "List::Util::zip_longest",
14111            "zip_shortest" => "List::Util::zip_shortest",
14112            "mesh" => "List::Util::mesh",
14113            "mesh_longest" => "List::Util::mesh_longest",
14114            "mesh_shortest" => "List::Util::mesh_shortest",
14115            "any" => "List::Util::any",
14116            "all" => "List::Util::all",
14117            "none" => "List::Util::none",
14118            "notall" => "List::Util::notall",
14119            "first" | "fst" => "List::Util::first",
14120            "reduce" | "rd" => "List::Util::reduce",
14121            "reductions" => "List::Util::reductions",
14122            "sum" => "List::Util::sum",
14123            "sum0" => "List::Util::sum0",
14124            "product" => "List::Util::product",
14125            "min" => "List::Util::min",
14126            "max" => "List::Util::max",
14127            "minstr" => "List::Util::minstr",
14128            "maxstr" => "List::Util::maxstr",
14129            "mean" => "List::Util::mean",
14130            "median" | "med" => "List::Util::median",
14131            "mode" => "List::Util::mode",
14132            "stddev" | "std" => "List::Util::stddev",
14133            "variance" | "var" => "List::Util::variance",
14134            "pairs" => "List::Util::pairs",
14135            "unpairs" => "List::Util::unpairs",
14136            "pairkeys" => "List::Util::pairkeys",
14137            "pairvalues" => "List::Util::pairvalues",
14138            "pairgrep" => "List::Util::pairgrep",
14139            "pairmap" => "List::Util::pairmap",
14140            "pairfirst" => "List::Util::pairfirst",
14141            _ => {
14142                return Err(PerlError::runtime(
14143                    format!("internal: not a bare list-util alias: {name}"),
14144                    line,
14145                )
14146                .into());
14147            }
14148        };
14149        let Some(sub) = self.subs.get(fq).cloned() else {
14150            return Err(PerlError::runtime(
14151                format!("internal: missing native stub for {fq}"),
14152                line,
14153            )
14154            .into());
14155        };
14156        let args = self.with_topic_default_args(args);
14157        self.call_sub(&sub, args, want, line)
14158    }
14159
14160    fn call_named_sub(
14161        &mut self,
14162        name: &str,
14163        args: Vec<PerlValue>,
14164        line: usize,
14165        want: WantarrayCtx,
14166    ) -> ExecResult {
14167        if let Some(sub) = self.resolve_sub_by_name(name) {
14168            let args = self.with_topic_default_args(args);
14169            return self.call_sub(&sub, args, want, line);
14170        }
14171        match name {
14172            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
14173            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
14174            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
14175            | "none" | "notall" | "first" | "fst" | "reduce" | "rd" | "reductions" | "sum"
14176            | "sum0" | "product" | "min" | "max" | "minstr" | "maxstr" | "mean" | "median"
14177            | "med" | "mode" | "stddev" | "std" | "variance" | "var" | "pairs" | "unpairs"
14178            | "pairkeys" | "pairvalues" | "pairgrep" | "pairmap" | "pairfirst" => {
14179                self.call_bare_list_util(name, args, line, want)
14180            }
14181            "deque" => {
14182                if !args.is_empty() {
14183                    return Err(PerlError::runtime("deque() takes no arguments", line).into());
14184                }
14185                Ok(PerlValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
14186            }
14187            "defer__internal" => {
14188                if args.len() != 1 {
14189                    return Err(PerlError::runtime(
14190                        "defer__internal expects one coderef argument",
14191                        line,
14192                    )
14193                    .into());
14194                }
14195                self.scope.push_defer(args[0].clone());
14196                Ok(PerlValue::UNDEF)
14197            }
14198            "heap" => {
14199                if args.len() != 1 {
14200                    return Err(
14201                        PerlError::runtime("heap() expects one comparator sub", line).into(),
14202                    );
14203                }
14204                if let Some(sub) = args[0].as_code_ref() {
14205                    Ok(PerlValue::heap(Arc::new(Mutex::new(PerlHeap {
14206                        items: Vec::new(),
14207                        cmp: Arc::clone(&sub),
14208                    }))))
14209                } else {
14210                    Err(PerlError::runtime("heap() requires a code reference", line).into())
14211                }
14212            }
14213            "pipeline" => {
14214                let mut items = Vec::new();
14215                for v in args {
14216                    if let Some(a) = v.as_array_vec() {
14217                        items.extend(a);
14218                    } else {
14219                        items.push(v);
14220                    }
14221                }
14222                Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
14223                    source: items,
14224                    ops: Vec::new(),
14225                    has_scalar_terminal: false,
14226                    par_stream: false,
14227                    streaming: false,
14228                    streaming_workers: 0,
14229                    streaming_buffer: 256,
14230                }))))
14231            }
14232            "par_pipeline" => {
14233                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
14234                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
14235                        .map_err(Into::into);
14236                }
14237                Ok(self.builtin_par_pipeline_stream(&args, line)?)
14238            }
14239            "par_pipeline_stream" => {
14240                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
14241                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
14242                        .map_err(Into::into);
14243                }
14244                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
14245            }
14246            "ppool" => {
14247                if args.len() != 1 {
14248                    return Err(PerlError::runtime(
14249                        "ppool() expects one argument (worker count)",
14250                        line,
14251                    )
14252                    .into());
14253                }
14254                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
14255            }
14256            "barrier" => {
14257                if args.len() != 1 {
14258                    return Err(PerlError::runtime(
14259                        "barrier() expects one argument (party count)",
14260                        line,
14261                    )
14262                    .into());
14263                }
14264                let n = args[0].to_int().max(1) as usize;
14265                Ok(PerlValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
14266            }
14267            "cluster" => {
14268                let items = if args.len() == 1 {
14269                    args[0].to_list()
14270                } else {
14271                    args.to_vec()
14272                };
14273                let c = RemoteCluster::from_list_args(&items)
14274                    .map_err(|msg| PerlError::runtime(msg, line))?;
14275                Ok(PerlValue::remote_cluster(Arc::new(c)))
14276            }
14277            _ => {
14278                // Late static binding: static::method() resolves to runtime class of $self
14279                if let Some(method_name) = name.strip_prefix("static::") {
14280                    let self_val = self.scope.get_scalar("self");
14281                    if let Some(c) = self_val.as_class_inst() {
14282                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
14283                            if let Some(ref body) = m.body {
14284                                let params = m.params.clone();
14285                                let mut call_args = vec![self_val.clone()];
14286                                call_args.extend(args);
14287                                return match self.call_class_method(body, &params, call_args, line)
14288                                {
14289                                    Ok(v) => Ok(v),
14290                                    Err(FlowOrError::Error(e)) => Err(e.into()),
14291                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14292                                    Err(e) => Err(e),
14293                                };
14294                            }
14295                        }
14296                        return Err(PerlError::runtime(
14297                            format!(
14298                                "static::{} — method not found on class {}",
14299                                method_name, c.def.name
14300                            ),
14301                            line,
14302                        )
14303                        .into());
14304                    }
14305                    return Err(PerlError::runtime(
14306                        "static:: can only be used inside a class method",
14307                        line,
14308                    )
14309                    .into());
14310                }
14311                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
14312                if let Some(def) = self.struct_defs.get(name).cloned() {
14313                    return self.struct_construct(&def, args, line);
14314                }
14315                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
14316                if let Some(def) = self.class_defs.get(name).cloned() {
14317                    return self.class_construct(&def, args, line);
14318                }
14319                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
14320                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
14321                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
14322                        return self.enum_construct(&def, variant_name, args, line);
14323                    }
14324                }
14325                // Check for static class method or static field: Math::add(...) / Counter::count()
14326                if let Some((class_name, member_name)) = name.rsplit_once("::") {
14327                    if let Some(def) = self.class_defs.get(class_name).cloned() {
14328                        // Static method
14329                        if let Some(m) = def.method(member_name) {
14330                            if m.is_static {
14331                                if let Some(ref body) = m.body {
14332                                    let params = m.params.clone();
14333                                    return match self.call_static_class_method(
14334                                        body,
14335                                        &params,
14336                                        args.clone(),
14337                                        line,
14338                                    ) {
14339                                        Ok(v) => Ok(v),
14340                                        Err(FlowOrError::Error(e)) => Err(e.into()),
14341                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14342                                        Err(e) => Err(e),
14343                                    };
14344                                }
14345                            }
14346                        }
14347                        // Static field access: getter (0 args) or setter (1 arg)
14348                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
14349                            let key = format!("{}::{}", class_name, member_name);
14350                            match args.len() {
14351                                0 => {
14352                                    let val = self.scope.get_scalar(&key);
14353                                    return Ok(val);
14354                                }
14355                                1 => {
14356                                    let _ = self.scope.set_scalar(&key, args[0].clone());
14357                                    return Ok(args[0].clone());
14358                                }
14359                                _ => {
14360                                    return Err(PerlError::runtime(
14361                                        format!(
14362                                            "static field `{}::{}` takes 0 or 1 arguments",
14363                                            class_name, member_name
14364                                        ),
14365                                        line,
14366                                    )
14367                                    .into());
14368                                }
14369                            }
14370                        }
14371                    }
14372                }
14373                let args = self.with_topic_default_args(args);
14374                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
14375                    return r;
14376                }
14377                Err(PerlError::runtime(self.undefined_subroutine_call_message(name), line).into())
14378            }
14379        }
14380    }
14381
14382    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
14383    pub(crate) fn struct_construct(
14384        &mut self,
14385        def: &Arc<StructDef>,
14386        args: Vec<PerlValue>,
14387        line: usize,
14388    ) -> ExecResult {
14389        // Detect if args are named (key => value pairs) or positional
14390        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
14391        let is_named = args.len() >= 2
14392            && args.len().is_multiple_of(2)
14393            && args.iter().step_by(2).all(|v| {
14394                let s = v.to_string();
14395                def.field_index(&s).is_some()
14396            });
14397
14398        let provided = if is_named {
14399            // Named construction: Point(x => 1, y => 2)
14400            let mut pairs = Vec::new();
14401            let mut i = 0;
14402            while i + 1 < args.len() {
14403                let k = args[i].to_string();
14404                let v = args[i + 1].clone();
14405                pairs.push((k, v));
14406                i += 2;
14407            }
14408            pairs
14409        } else {
14410            // Positional construction: Point(1, 2) fills fields in declaration order
14411            def.fields
14412                .iter()
14413                .zip(args.iter())
14414                .map(|(f, v)| (f.name.clone(), v.clone()))
14415                .collect()
14416        };
14417
14418        // Evaluate default expressions
14419        let mut defaults = Vec::with_capacity(def.fields.len());
14420        for field in &def.fields {
14421            if let Some(ref expr) = field.default {
14422                let val = self.eval_expr(expr)?;
14423                defaults.push(Some(val));
14424            } else {
14425                defaults.push(None);
14426            }
14427        }
14428
14429        Ok(crate::native_data::struct_new_with_defaults(
14430            def, &provided, &defaults, line,
14431        )?)
14432    }
14433
14434    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
14435    pub(crate) fn class_construct(
14436        &mut self,
14437        def: &Arc<ClassDef>,
14438        args: Vec<PerlValue>,
14439        _line: usize,
14440    ) -> ExecResult {
14441        use crate::value::ClassInstance;
14442
14443        // Prevent instantiation of abstract classes
14444        if def.is_abstract {
14445            return Err(PerlError::runtime(
14446                format!("cannot instantiate abstract class `{}`", def.name),
14447                _line,
14448            )
14449            .into());
14450        }
14451
14452        // Collect all fields from inheritance chain (parent fields first)
14453        let all_fields = self.collect_class_fields(def);
14454
14455        // Check if args are named
14456        let is_named = args.len() >= 2
14457            && args.len().is_multiple_of(2)
14458            && args.iter().step_by(2).all(|v| {
14459                let s = v.to_string();
14460                all_fields.iter().any(|(name, _, _)| name == &s)
14461            });
14462
14463        let provided: Vec<(String, PerlValue)> = if is_named {
14464            let mut pairs = Vec::new();
14465            let mut i = 0;
14466            while i + 1 < args.len() {
14467                let k = args[i].to_string();
14468                let v = args[i + 1].clone();
14469                pairs.push((k, v));
14470                i += 2;
14471            }
14472            pairs
14473        } else {
14474            all_fields
14475                .iter()
14476                .zip(args.iter())
14477                .map(|((name, _, _), v)| (name.clone(), v.clone()))
14478                .collect()
14479        };
14480
14481        // Build values array for all fields (inherited + own) with type checking
14482        let mut values = Vec::with_capacity(all_fields.len());
14483        for (name, default, ty) in &all_fields {
14484            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
14485                val.clone()
14486            } else if let Some(ref expr) = default {
14487                self.eval_expr(expr)?
14488            } else {
14489                PerlValue::UNDEF
14490            };
14491            ty.check_value(&val).map_err(|msg| {
14492                PerlError::type_error(
14493                    format!("class {} field `{}`: {}", def.name, name, msg),
14494                    _line,
14495                )
14496            })?;
14497            values.push(val);
14498        }
14499
14500        // Compute full ISA chain for type checking
14501        let isa_chain = self.mro_linearize(&def.name);
14502        let instance = PerlValue::class_inst(Arc::new(ClassInstance::new_with_isa(
14503            Arc::clone(def),
14504            values,
14505            isa_chain,
14506        )));
14507
14508        // Call BUILD hooks: parent BUILD first, then child BUILD
14509        let build_chain = self.collect_build_chain(def);
14510        if !build_chain.is_empty() {
14511            for (body, params) in &build_chain {
14512                let call_args = vec![instance.clone()];
14513                match self.call_class_method(body, params, call_args, _line) {
14514                    Ok(_) => {}
14515                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
14516                    Err(e) => return Err(e),
14517                }
14518            }
14519        }
14520
14521        Ok(instance)
14522    }
14523
14524    /// Collect BUILD methods from parent to child order.
14525    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14526        let mut chain = Vec::new();
14527        // Parent BUILD first
14528        for parent_name in &def.extends {
14529            if let Some(parent_def) = self.class_defs.get(parent_name) {
14530                chain.extend(self.collect_build_chain(parent_def));
14531            }
14532        }
14533        // Own BUILD
14534        if let Some(m) = def.method("BUILD") {
14535            if let Some(ref body) = m.body {
14536                chain.push((body.clone(), m.params.clone()));
14537            }
14538        }
14539        chain
14540    }
14541
14542    /// Collect all fields from a class and its parent hierarchy (parent fields first).
14543    /// Returns (name, default, type, visibility, owning_class_name).
14544    fn collect_class_fields(
14545        &self,
14546        def: &ClassDef,
14547    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
14548        self.collect_class_fields_full(def)
14549            .into_iter()
14550            .map(|(name, default, ty, _, _)| (name, default, ty))
14551            .collect()
14552    }
14553
14554    /// Like collect_class_fields but includes visibility and owning class name.
14555    fn collect_class_fields_full(
14556        &self,
14557        def: &ClassDef,
14558    ) -> Vec<(
14559        String,
14560        Option<Expr>,
14561        crate::ast::PerlTypeName,
14562        crate::ast::Visibility,
14563        String,
14564    )> {
14565        let mut all_fields = Vec::new();
14566
14567        for parent_name in &def.extends {
14568            if let Some(parent_def) = self.class_defs.get(parent_name) {
14569                let parent_fields = self.collect_class_fields_full(parent_def);
14570                all_fields.extend(parent_fields);
14571            }
14572        }
14573
14574        for field in &def.fields {
14575            all_fields.push((
14576                field.name.clone(),
14577                field.default.clone(),
14578                field.ty.clone(),
14579                field.visibility,
14580                def.name.clone(),
14581            ));
14582        }
14583
14584        all_fields
14585    }
14586
14587    /// Collect all method names from class and parents (deduplicates, child overrides parent).
14588    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
14589        // Parent methods first
14590        for parent_name in &def.extends {
14591            if let Some(parent_def) = self.class_defs.get(parent_name) {
14592                self.collect_class_method_names(parent_def, names);
14593            }
14594        }
14595        // Own methods (add if not already present — child overrides parent name)
14596        for m in &def.methods {
14597            if !m.is_static && !names.contains(&m.name) {
14598                names.push(m.name.clone());
14599            }
14600        }
14601    }
14602
14603    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
14604    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14605        let mut chain = Vec::new();
14606        // Own DESTROY first
14607        if let Some(m) = def.method("DESTROY") {
14608            if let Some(ref body) = m.body {
14609                chain.push((body.clone(), m.params.clone()));
14610            }
14611        }
14612        // Then parent DESTROY
14613        for parent_name in &def.extends {
14614            if let Some(parent_def) = self.class_defs.get(parent_name) {
14615                chain.extend(self.collect_destroy_chain(parent_def));
14616            }
14617        }
14618        chain
14619    }
14620
14621    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
14622    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
14623        if let Some(def) = self.class_defs.get(child) {
14624            for parent in &def.extends {
14625                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
14626                    return true;
14627                }
14628            }
14629        }
14630        false
14631    }
14632
14633    /// Find a method in a class or its parent hierarchy (child methods override parent).
14634    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
14635        // First check the current class
14636        if let Some(m) = def.method(method) {
14637            return Some((m.clone(), def.name.clone()));
14638        }
14639        // Then check parent classes
14640        for parent_name in &def.extends {
14641            if let Some(parent_def) = self.class_defs.get(parent_name) {
14642                if let Some(result) = self.find_class_method(parent_def, method) {
14643                    return Some(result);
14644                }
14645            }
14646        }
14647        None
14648    }
14649
14650    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
14651    pub(crate) fn enum_construct(
14652        &mut self,
14653        def: &Arc<EnumDef>,
14654        variant_name: &str,
14655        args: Vec<PerlValue>,
14656        line: usize,
14657    ) -> ExecResult {
14658        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
14659            FlowOrError::Error(PerlError::runtime(
14660                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
14661                line,
14662            ))
14663        })?;
14664        let variant = &def.variants[variant_idx];
14665        let data = if variant.ty.is_some() {
14666            if args.is_empty() {
14667                return Err(PerlError::runtime(
14668                    format!(
14669                        "enum variant `{}::{}` requires data",
14670                        def.name, variant_name
14671                    ),
14672                    line,
14673                )
14674                .into());
14675            }
14676            if args.len() == 1 {
14677                args.into_iter().next().unwrap()
14678            } else {
14679                PerlValue::array(args)
14680            }
14681        } else {
14682            if !args.is_empty() {
14683                return Err(PerlError::runtime(
14684                    format!(
14685                        "enum variant `{}::{}` does not take data",
14686                        def.name, variant_name
14687                    ),
14688                    line,
14689                )
14690                .into());
14691            }
14692            PerlValue::UNDEF
14693        };
14694        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
14695        Ok(PerlValue::enum_inst(Arc::new(inst)))
14696    }
14697
14698    /// True if `name` is a registered or standard process-global handle.
14699    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
14700        matches!(name, "STDIN" | "STDOUT" | "STDERR")
14701            || self.input_handles.contains_key(name)
14702            || self.output_handles.contains_key(name)
14703            || self.io_file_slots.contains_key(name)
14704            || self.pipe_children.contains_key(name)
14705    }
14706
14707    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
14708    pub(crate) fn io_handle_method(
14709        &mut self,
14710        name: &str,
14711        method: &str,
14712        args: &[PerlValue],
14713        line: usize,
14714    ) -> PerlResult<PerlValue> {
14715        match method {
14716            "print" => self.io_handle_print(name, args, false, line),
14717            "say" => self.io_handle_print(name, args, true, line),
14718            "printf" => self.io_handle_printf(name, args, line),
14719            "getline" | "readline" => {
14720                if !args.is_empty() {
14721                    return Err(PerlError::runtime(
14722                        format!("{}: too many arguments", method),
14723                        line,
14724                    ));
14725                }
14726                self.readline_builtin_execute(Some(name))
14727            }
14728            "close" => {
14729                if !args.is_empty() {
14730                    return Err(PerlError::runtime("close: too many arguments", line));
14731                }
14732                self.close_builtin_execute(name.to_string())
14733            }
14734            "eof" => {
14735                if !args.is_empty() {
14736                    return Err(PerlError::runtime("eof: too many arguments", line));
14737                }
14738                let at_eof = !self.has_input_handle(name);
14739                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
14740            }
14741            "getc" => {
14742                if !args.is_empty() {
14743                    return Err(PerlError::runtime("getc: too many arguments", line));
14744                }
14745                match crate::builtins::try_builtin(
14746                    self,
14747                    "getc",
14748                    &[PerlValue::string(name.to_string())],
14749                    line,
14750                ) {
14751                    Some(r) => r,
14752                    None => Err(PerlError::runtime("getc: not available", line)),
14753                }
14754            }
14755            "binmode" => match crate::builtins::try_builtin(
14756                self,
14757                "binmode",
14758                &[PerlValue::string(name.to_string())],
14759                line,
14760            ) {
14761                Some(r) => r,
14762                None => Err(PerlError::runtime("binmode: not available", line)),
14763            },
14764            "fileno" => match crate::builtins::try_builtin(
14765                self,
14766                "fileno",
14767                &[PerlValue::string(name.to_string())],
14768                line,
14769            ) {
14770                Some(r) => r,
14771                None => Err(PerlError::runtime("fileno: not available", line)),
14772            },
14773            "flush" => {
14774                if !args.is_empty() {
14775                    return Err(PerlError::runtime("flush: too many arguments", line));
14776                }
14777                self.io_handle_flush(name, line)
14778            }
14779            _ => Err(PerlError::runtime(
14780                format!("Unknown method for filehandle: {}", method),
14781                line,
14782            )),
14783        }
14784    }
14785
14786    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> PerlResult<PerlValue> {
14787        match handle_name {
14788            "STDOUT" => {
14789                let _ = IoWrite::flush(&mut io::stdout());
14790            }
14791            "STDERR" => {
14792                let _ = IoWrite::flush(&mut io::stderr());
14793            }
14794            name => {
14795                if let Some(writer) = self.output_handles.get_mut(name) {
14796                    let _ = IoWrite::flush(&mut *writer);
14797                } else {
14798                    return Err(PerlError::runtime(
14799                        format!("flush on unopened filehandle {}", name),
14800                        line,
14801                    ));
14802                }
14803            }
14804        }
14805        Ok(PerlValue::integer(1))
14806    }
14807
14808    fn io_handle_print(
14809        &mut self,
14810        handle_name: &str,
14811        args: &[PerlValue],
14812        newline: bool,
14813        line: usize,
14814    ) -> PerlResult<PerlValue> {
14815        if newline && (self.feature_bits & FEAT_SAY) == 0 {
14816            return Err(PerlError::runtime(
14817                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
14818                line,
14819            ));
14820        }
14821        let mut output = String::new();
14822        if args.is_empty() {
14823            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
14824            output.push_str(&self.scope.get_scalar("_").to_string());
14825        } else {
14826            for (i, val) in args.iter().enumerate() {
14827                if i > 0 && !self.ofs.is_empty() {
14828                    output.push_str(&self.ofs);
14829                }
14830                output.push_str(&val.to_string());
14831            }
14832        }
14833        if newline {
14834            output.push('\n');
14835        }
14836        output.push_str(&self.ors);
14837
14838        self.write_formatted_print(handle_name, &output, line)?;
14839        Ok(PerlValue::integer(1))
14840    }
14841
14842    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
14843    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
14844    pub(crate) fn write_formatted_print(
14845        &mut self,
14846        handle_name: &str,
14847        output: &str,
14848        line: usize,
14849    ) -> PerlResult<()> {
14850        match handle_name {
14851            "STDOUT" => {
14852                if !self.suppress_stdout {
14853                    print!("{}", output);
14854                    if self.output_autoflush {
14855                        let _ = io::stdout().flush();
14856                    }
14857                }
14858            }
14859            "STDERR" => {
14860                eprint!("{}", output);
14861                let _ = io::stderr().flush();
14862            }
14863            name => {
14864                if let Some(writer) = self.output_handles.get_mut(name) {
14865                    let _ = writer.write_all(output.as_bytes());
14866                    if self.output_autoflush {
14867                        let _ = writer.flush();
14868                    }
14869                } else {
14870                    return Err(PerlError::runtime(
14871                        format!("print on unopened filehandle {}", name),
14872                        line,
14873                    ));
14874                }
14875            }
14876        }
14877        Ok(())
14878    }
14879
14880    fn io_handle_printf(
14881        &mut self,
14882        handle_name: &str,
14883        args: &[PerlValue],
14884        line: usize,
14885    ) -> PerlResult<PerlValue> {
14886        let (fmt, rest): (String, &[PerlValue]) = if args.is_empty() {
14887            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
14888                Ok(s) => s,
14889                Err(FlowOrError::Error(e)) => return Err(e),
14890                Err(FlowOrError::Flow(_)) => {
14891                    return Err(PerlError::runtime(
14892                        "printf: unexpected control flow in sprintf",
14893                        line,
14894                    ));
14895                }
14896            };
14897            (s, &[])
14898        } else {
14899            (args[0].to_string(), &args[1..])
14900        };
14901        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
14902            Ok(s) => s,
14903            Err(FlowOrError::Error(e)) => return Err(e),
14904            Err(FlowOrError::Flow(_)) => {
14905                return Err(PerlError::runtime(
14906                    "printf: unexpected control flow in sprintf",
14907                    line,
14908                ));
14909            }
14910        };
14911        match handle_name {
14912            "STDOUT" => {
14913                if !self.suppress_stdout {
14914                    print!("{}", output);
14915                    if self.output_autoflush {
14916                        let _ = IoWrite::flush(&mut io::stdout());
14917                    }
14918                }
14919            }
14920            "STDERR" => {
14921                eprint!("{}", output);
14922                let _ = IoWrite::flush(&mut io::stderr());
14923            }
14924            name => {
14925                if let Some(writer) = self.output_handles.get_mut(name) {
14926                    let _ = writer.write_all(output.as_bytes());
14927                    if self.output_autoflush {
14928                        let _ = writer.flush();
14929                    }
14930                } else {
14931                    return Err(PerlError::runtime(
14932                        format!("printf on unopened filehandle {}", name),
14933                        line,
14934                    ));
14935                }
14936            }
14937        }
14938        Ok(PerlValue::integer(1))
14939    }
14940
14941    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
14942    pub(crate) fn try_native_method(
14943        &mut self,
14944        receiver: &PerlValue,
14945        method: &str,
14946        args: &[PerlValue],
14947        line: usize,
14948    ) -> Option<PerlResult<PerlValue>> {
14949        if let Some(name) = receiver.as_io_handle_name() {
14950            return Some(self.io_handle_method(&name, method, args, line));
14951        }
14952        if let Some(ref s) = receiver.as_str() {
14953            if self.is_bound_handle(s) {
14954                return Some(self.io_handle_method(s, method, args, line));
14955            }
14956        }
14957        if let Some(c) = receiver.as_sqlite_conn() {
14958            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
14959        }
14960        if let Some(s) = receiver.as_struct_inst() {
14961            // Field access: $p->x or $p->x(value)
14962            if let Some(idx) = s.def.field_index(method) {
14963                match args.len() {
14964                    0 => {
14965                        return Some(Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF)));
14966                    }
14967                    1 => {
14968                        let field = &s.def.fields[idx];
14969                        let new_val = args[0].clone();
14970                        if let Err(msg) = field.ty.check_value(&new_val) {
14971                            return Some(Err(PerlError::type_error(
14972                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
14973                                line,
14974                            )));
14975                        }
14976                        s.set_field(idx, new_val.clone());
14977                        return Some(Ok(new_val));
14978                    }
14979                    _ => {
14980                        return Some(Err(PerlError::runtime(
14981                            format!(
14982                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
14983                                method,
14984                                args.len()
14985                            ),
14986                            line,
14987                        )));
14988                    }
14989                }
14990            }
14991            // Built-in struct methods
14992            match method {
14993                "with" => {
14994                    // Functional update: $p->with(x => 5) returns new instance with changed field
14995                    let mut new_values = s.get_values();
14996                    let mut i = 0;
14997                    while i + 1 < args.len() {
14998                        let k = args[i].to_string();
14999                        let v = args[i + 1].clone();
15000                        if let Some(idx) = s.def.field_index(&k) {
15001                            let field = &s.def.fields[idx];
15002                            if let Err(msg) = field.ty.check_value(&v) {
15003                                return Some(Err(PerlError::type_error(
15004                                    format!(
15005                                        "struct {} field `{}`: {}",
15006                                        s.def.name, field.name, msg
15007                                    ),
15008                                    line,
15009                                )));
15010                            }
15011                            new_values[idx] = v;
15012                        } else {
15013                            return Some(Err(PerlError::runtime(
15014                                format!("struct {}: unknown field `{}`", s.def.name, k),
15015                                line,
15016                            )));
15017                        }
15018                        i += 2;
15019                    }
15020                    return Some(Ok(PerlValue::struct_inst(Arc::new(
15021                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
15022                    ))));
15023                }
15024                "to_hash" => {
15025                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
15026                    if !args.is_empty() {
15027                        return Some(Err(PerlError::runtime(
15028                            "struct to_hash takes no arguments",
15029                            line,
15030                        )));
15031                    }
15032                    let mut map = IndexMap::new();
15033                    let values = s.get_values();
15034                    for (i, field) in s.def.fields.iter().enumerate() {
15035                        map.insert(field.name.clone(), values[i].clone());
15036                    }
15037                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
15038                }
15039                "fields" => {
15040                    // Field list: $p->fields returns field names
15041                    if !args.is_empty() {
15042                        return Some(Err(PerlError::runtime(
15043                            "struct fields takes no arguments",
15044                            line,
15045                        )));
15046                    }
15047                    let names: Vec<PerlValue> = s
15048                        .def
15049                        .fields
15050                        .iter()
15051                        .map(|f| PerlValue::string(f.name.clone()))
15052                        .collect();
15053                    return Some(Ok(PerlValue::array(names)));
15054                }
15055                "clone" => {
15056                    // Clone: $p->clone deep copies
15057                    if !args.is_empty() {
15058                        return Some(Err(PerlError::runtime(
15059                            "struct clone takes no arguments",
15060                            line,
15061                        )));
15062                    }
15063                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
15064                    return Some(Ok(PerlValue::struct_inst(Arc::new(
15065                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
15066                    ))));
15067                }
15068                _ => {}
15069            }
15070            // User-defined struct method
15071            if let Some(m) = s.def.method(method) {
15072                let body = m.body.clone();
15073                let params = m.params.clone();
15074                // Build args: $self is the receiver, then the passed args
15075                let mut call_args = vec![receiver.clone()];
15076                call_args.extend(args.iter().cloned());
15077                return Some(
15078                    match self.call_struct_method(&body, &params, call_args, line) {
15079                        Ok(v) => Ok(v),
15080                        Err(FlowOrError::Error(e)) => Err(e),
15081                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15082                        Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
15083                            "unexpected control flow in struct method",
15084                            line,
15085                        )),
15086                    },
15087                );
15088            }
15089            return None;
15090        }
15091        // Class instance method dispatch
15092        if let Some(c) = receiver.as_class_inst() {
15093            // Collect all fields from inheritance chain (with visibility)
15094            let all_fields_full = self.collect_class_fields_full(&c.def);
15095            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
15096                .iter()
15097                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
15098                .collect();
15099
15100            // Field access: $obj->name or $obj->name(value)
15101            if let Some(idx) = all_fields_full
15102                .iter()
15103                .position(|(name, _, _, _, _)| name == method)
15104            {
15105                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
15106
15107                // Enforce field visibility
15108                match vis {
15109                    crate::ast::Visibility::Private => {
15110                        // Only accessible from within the owning class's methods
15111                        let caller_class = self
15112                            .scope
15113                            .get_scalar("self")
15114                            .as_class_inst()
15115                            .map(|ci| ci.def.name.clone());
15116                        if caller_class.as_deref() != Some(owner_class.as_str()) {
15117                            return Some(Err(PerlError::runtime(
15118                                format!("field `{}` of class {} is private", method, owner_class),
15119                                line,
15120                            )));
15121                        }
15122                    }
15123                    crate::ast::Visibility::Protected => {
15124                        // Accessible from owning class or subclasses
15125                        let caller_class = self
15126                            .scope
15127                            .get_scalar("self")
15128                            .as_class_inst()
15129                            .map(|ci| ci.def.name.clone());
15130                        let allowed = caller_class.as_deref().is_some_and(|caller| {
15131                            caller == owner_class || self.class_inherits_from(caller, owner_class)
15132                        });
15133                        if !allowed {
15134                            return Some(Err(PerlError::runtime(
15135                                format!("field `{}` of class {} is protected", method, owner_class),
15136                                line,
15137                            )));
15138                        }
15139                    }
15140                    crate::ast::Visibility::Public => {}
15141                }
15142
15143                match args.len() {
15144                    0 => {
15145                        return Some(Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF)));
15146                    }
15147                    1 => {
15148                        let new_val = args[0].clone();
15149                        if let Err(msg) = ty.check_value(&new_val) {
15150                            return Some(Err(PerlError::type_error(
15151                                format!("class {} field `{}`: {}", c.def.name, method, msg),
15152                                line,
15153                            )));
15154                        }
15155                        c.set_field(idx, new_val.clone());
15156                        return Some(Ok(new_val));
15157                    }
15158                    _ => {
15159                        return Some(Err(PerlError::runtime(
15160                            format!(
15161                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
15162                                method,
15163                                args.len()
15164                            ),
15165                            line,
15166                        )));
15167                    }
15168                }
15169            }
15170            // Built-in class methods (use all_fields for inheritance)
15171            match method {
15172                "with" => {
15173                    let mut new_values = c.get_values();
15174                    let mut i = 0;
15175                    while i + 1 < args.len() {
15176                        let k = args[i].to_string();
15177                        let v = args[i + 1].clone();
15178                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
15179                            let (_, _, ref ty) = all_fields[idx];
15180                            if let Err(msg) = ty.check_value(&v) {
15181                                return Some(Err(PerlError::type_error(
15182                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
15183                                    line,
15184                                )));
15185                            }
15186                            new_values[idx] = v;
15187                        } else {
15188                            return Some(Err(PerlError::runtime(
15189                                format!("class {}: unknown field `{}`", c.def.name, k),
15190                                line,
15191                            )));
15192                        }
15193                        i += 2;
15194                    }
15195                    return Some(Ok(PerlValue::class_inst(Arc::new(
15196                        crate::value::ClassInstance::new_with_isa(
15197                            Arc::clone(&c.def),
15198                            new_values,
15199                            c.isa_chain.clone(),
15200                        ),
15201                    ))));
15202                }
15203                "to_hash" => {
15204                    if !args.is_empty() {
15205                        return Some(Err(PerlError::runtime(
15206                            "class to_hash takes no arguments",
15207                            line,
15208                        )));
15209                    }
15210                    let mut map = IndexMap::new();
15211                    let values = c.get_values();
15212                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
15213                        if let Some(v) = values.get(i) {
15214                            map.insert(name.clone(), v.clone());
15215                        }
15216                    }
15217                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
15218                }
15219                "fields" => {
15220                    if !args.is_empty() {
15221                        return Some(Err(PerlError::runtime(
15222                            "class fields takes no arguments",
15223                            line,
15224                        )));
15225                    }
15226                    let names: Vec<PerlValue> = all_fields
15227                        .iter()
15228                        .map(|(name, _, _)| PerlValue::string(name.clone()))
15229                        .collect();
15230                    return Some(Ok(PerlValue::array(names)));
15231                }
15232                "clone" => {
15233                    if !args.is_empty() {
15234                        return Some(Err(PerlError::runtime(
15235                            "class clone takes no arguments",
15236                            line,
15237                        )));
15238                    }
15239                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
15240                    return Some(Ok(PerlValue::class_inst(Arc::new(
15241                        crate::value::ClassInstance::new_with_isa(
15242                            Arc::clone(&c.def),
15243                            new_values,
15244                            c.isa_chain.clone(),
15245                        ),
15246                    ))));
15247                }
15248                "isa" => {
15249                    if args.len() != 1 {
15250                        return Some(Err(PerlError::runtime("isa requires one argument", line)));
15251                    }
15252                    let class_name = args[0].to_string();
15253                    let is_a = c.isa(&class_name);
15254                    return Some(Ok(if is_a {
15255                        PerlValue::integer(1)
15256                    } else {
15257                        PerlValue::string(String::new())
15258                    }));
15259                }
15260                "does" => {
15261                    if args.len() != 1 {
15262                        return Some(Err(PerlError::runtime("does requires one argument", line)));
15263                    }
15264                    let trait_name = args[0].to_string();
15265                    let implements = c.def.implements.contains(&trait_name);
15266                    return Some(Ok(if implements {
15267                        PerlValue::integer(1)
15268                    } else {
15269                        PerlValue::string(String::new())
15270                    }));
15271                }
15272                "methods" => {
15273                    if !args.is_empty() {
15274                        return Some(Err(PerlError::runtime("methods takes no arguments", line)));
15275                    }
15276                    let mut names = Vec::new();
15277                    self.collect_class_method_names(&c.def, &mut names);
15278                    let values: Vec<PerlValue> = names.into_iter().map(PerlValue::string).collect();
15279                    return Some(Ok(PerlValue::array(values)));
15280                }
15281                "superclass" => {
15282                    if !args.is_empty() {
15283                        return Some(Err(PerlError::runtime(
15284                            "superclass takes no arguments",
15285                            line,
15286                        )));
15287                    }
15288                    let parents: Vec<PerlValue> = c
15289                        .def
15290                        .extends
15291                        .iter()
15292                        .map(|s| PerlValue::string(s.clone()))
15293                        .collect();
15294                    return Some(Ok(PerlValue::array(parents)));
15295                }
15296                "destroy" => {
15297                    // Explicit destructor call — runs DESTROY chain child-first
15298                    let destroy_chain = self.collect_destroy_chain(&c.def);
15299                    for (body, params) in &destroy_chain {
15300                        let call_args = vec![receiver.clone()];
15301                        match self.call_class_method(body, params, call_args, line) {
15302                            Ok(_) => {}
15303                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
15304                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
15305                            Err(_) => {}
15306                        }
15307                    }
15308                    return Some(Ok(PerlValue::UNDEF));
15309                }
15310                _ => {}
15311            }
15312            // User-defined class method (search inheritance chain)
15313            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
15314                // Check visibility
15315                match m.visibility {
15316                    crate::ast::Visibility::Private => {
15317                        let caller_class = self
15318                            .scope
15319                            .get_scalar("self")
15320                            .as_class_inst()
15321                            .map(|ci| ci.def.name.clone());
15322                        if caller_class.as_deref() != Some(owner_class.as_str()) {
15323                            return Some(Err(PerlError::runtime(
15324                                format!("method `{}` of class {} is private", method, owner_class),
15325                                line,
15326                            )));
15327                        }
15328                    }
15329                    crate::ast::Visibility::Protected => {
15330                        let caller_class = self
15331                            .scope
15332                            .get_scalar("self")
15333                            .as_class_inst()
15334                            .map(|ci| ci.def.name.clone());
15335                        let allowed = caller_class.as_deref().is_some_and(|caller| {
15336                            caller == owner_class.as_str()
15337                                || self.class_inherits_from(caller, owner_class)
15338                        });
15339                        if !allowed {
15340                            return Some(Err(PerlError::runtime(
15341                                format!(
15342                                    "method `{}` of class {} is protected",
15343                                    method, owner_class
15344                                ),
15345                                line,
15346                            )));
15347                        }
15348                    }
15349                    crate::ast::Visibility::Public => {}
15350                }
15351                if let Some(ref body) = m.body {
15352                    let params = m.params.clone();
15353                    let mut call_args = vec![receiver.clone()];
15354                    call_args.extend(args.iter().cloned());
15355                    return Some(
15356                        match self.call_class_method(body, &params, call_args, line) {
15357                            Ok(v) => Ok(v),
15358                            Err(FlowOrError::Error(e)) => Err(e),
15359                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15360                            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
15361                                "unexpected control flow in class method",
15362                                line,
15363                            )),
15364                        },
15365                    );
15366                }
15367            }
15368            return None;
15369        }
15370        if let Some(d) = receiver.as_dataframe() {
15371            return Some(self.dataframe_method(d, method, args, line));
15372        }
15373        if let Some(s) = crate::value::set_payload(receiver) {
15374            return Some(self.set_method(s, method, args, line));
15375        }
15376        if let Some(d) = receiver.as_deque() {
15377            return Some(self.deque_method(d, method, args, line));
15378        }
15379        if let Some(h) = receiver.as_heap_pq() {
15380            return Some(self.heap_method(h, method, args, line));
15381        }
15382        if let Some(p) = receiver.as_pipeline() {
15383            return Some(self.pipeline_method(p, method, args, line));
15384        }
15385        if let Some(c) = receiver.as_capture() {
15386            return Some(self.capture_method(c, method, args, line));
15387        }
15388        if let Some(p) = receiver.as_ppool() {
15389            return Some(self.ppool_method(p, method, args, line));
15390        }
15391        if let Some(b) = receiver.as_barrier() {
15392            return Some(self.barrier_method(b, method, args, line));
15393        }
15394        if let Some(g) = receiver.as_generator() {
15395            if method == "next" {
15396                if !args.is_empty() {
15397                    return Some(Err(PerlError::runtime(
15398                        "generator->next takes no arguments",
15399                        line,
15400                    )));
15401                }
15402                return Some(self.generator_next(&g));
15403            }
15404            return None;
15405        }
15406        if let Some(arc) = receiver.as_atomic_arc() {
15407            let inner = arc.lock().clone();
15408            if let Some(d) = inner.as_deque() {
15409                return Some(self.deque_method(d, method, args, line));
15410            }
15411            if let Some(h) = inner.as_heap_pq() {
15412                return Some(self.heap_method(h, method, args, line));
15413            }
15414        }
15415        None
15416    }
15417
15418    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
15419    fn dataframe_method(
15420        &mut self,
15421        d: Arc<Mutex<PerlDataFrame>>,
15422        method: &str,
15423        args: &[PerlValue],
15424        line: usize,
15425    ) -> PerlResult<PerlValue> {
15426        match method {
15427            "nrow" | "nrows" => {
15428                if !args.is_empty() {
15429                    return Err(PerlError::runtime(
15430                        format!("dataframe {} takes no arguments", method),
15431                        line,
15432                    ));
15433                }
15434                Ok(PerlValue::integer(d.lock().nrows() as i64))
15435            }
15436            "ncol" | "ncols" => {
15437                if !args.is_empty() {
15438                    return Err(PerlError::runtime(
15439                        format!("dataframe {} takes no arguments", method),
15440                        line,
15441                    ));
15442                }
15443                Ok(PerlValue::integer(d.lock().ncols() as i64))
15444            }
15445            "filter" => {
15446                if args.len() != 1 {
15447                    return Err(PerlError::runtime(
15448                        "dataframe filter expects 1 argument (sub)",
15449                        line,
15450                    ));
15451                }
15452                let Some(sub) = args[0].as_code_ref() else {
15453                    return Err(PerlError::runtime(
15454                        "dataframe filter expects a code reference",
15455                        line,
15456                    ));
15457                };
15458                let df_guard = d.lock();
15459                let n = df_guard.nrows();
15460                let mut keep = vec![false; n];
15461                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
15462                    let row = df_guard.row_hashref(r);
15463                    self.scope_push_hook();
15464                    self.scope.set_topic(row);
15465                    if let Some(ref env) = sub.closure_env {
15466                        self.scope.restore_capture(env);
15467                    }
15468                    let pass = match self.exec_block_no_scope(&sub.body) {
15469                        Ok(v) => v.is_true(),
15470                        Err(_) => false,
15471                    };
15472                    self.scope_pop_hook();
15473                    *row_keep = pass;
15474                }
15475                let columns = df_guard.columns.clone();
15476                let cols: Vec<Vec<PerlValue>> = (0..df_guard.ncols())
15477                    .map(|i| {
15478                        let mut out = Vec::new();
15479                        for (r, pass_row) in keep.iter().enumerate().take(n) {
15480                            if *pass_row {
15481                                out.push(df_guard.cols[i][r].clone());
15482                            }
15483                        }
15484                        out
15485                    })
15486                    .collect();
15487                let group_by = df_guard.group_by.clone();
15488                drop(df_guard);
15489                let new_df = PerlDataFrame {
15490                    columns,
15491                    cols,
15492                    group_by,
15493                };
15494                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
15495            }
15496            "group_by" => {
15497                if args.len() != 1 {
15498                    return Err(PerlError::runtime(
15499                        "dataframe group_by expects 1 column name",
15500                        line,
15501                    ));
15502                }
15503                let key = args[0].to_string();
15504                let inner = d.lock();
15505                if inner.col_index(&key).is_none() {
15506                    return Err(PerlError::runtime(
15507                        format!("dataframe group_by: unknown column \"{}\"", key),
15508                        line,
15509                    ));
15510                }
15511                let new_df = PerlDataFrame {
15512                    columns: inner.columns.clone(),
15513                    cols: inner.cols.clone(),
15514                    group_by: Some(key),
15515                };
15516                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
15517            }
15518            "sum" => {
15519                if args.len() != 1 {
15520                    return Err(PerlError::runtime(
15521                        "dataframe sum expects 1 column name",
15522                        line,
15523                    ));
15524                }
15525                let col_name = args[0].to_string();
15526                let inner = d.lock();
15527                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
15528                    PerlError::runtime(
15529                        format!("dataframe sum: unknown column \"{}\"", col_name),
15530                        line,
15531                    )
15532                })?;
15533                match &inner.group_by {
15534                    Some(gcol) => {
15535                        let gi = inner.col_index(gcol).ok_or_else(|| {
15536                            PerlError::runtime(
15537                                format!("dataframe sum: unknown group column \"{}\"", gcol),
15538                                line,
15539                            )
15540                        })?;
15541                        let mut acc: IndexMap<String, f64> = IndexMap::new();
15542                        for r in 0..inner.nrows() {
15543                            let k = inner.cols[gi][r].to_string();
15544                            let v = inner.cols[val_idx][r].to_number();
15545                            *acc.entry(k).or_insert(0.0) += v;
15546                        }
15547                        let keys: Vec<String> = acc.keys().cloned().collect();
15548                        let sums: Vec<f64> = acc.values().copied().collect();
15549                        let cols = vec![
15550                            keys.into_iter().map(PerlValue::string).collect(),
15551                            sums.into_iter().map(PerlValue::float).collect(),
15552                        ];
15553                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
15554                        let out = PerlDataFrame {
15555                            columns,
15556                            cols,
15557                            group_by: None,
15558                        };
15559                        Ok(PerlValue::dataframe(Arc::new(Mutex::new(out))))
15560                    }
15561                    None => {
15562                        let total: f64 = (0..inner.nrows())
15563                            .map(|r| inner.cols[val_idx][r].to_number())
15564                            .sum();
15565                        Ok(PerlValue::float(total))
15566                    }
15567                }
15568            }
15569            _ => Err(PerlError::runtime(
15570                format!("Unknown method for dataframe: {}", method),
15571                line,
15572            )),
15573        }
15574    }
15575
15576    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
15577    fn set_method(
15578        &self,
15579        s: Arc<crate::value::PerlSet>,
15580        method: &str,
15581        args: &[PerlValue],
15582        line: usize,
15583    ) -> PerlResult<PerlValue> {
15584        match method {
15585            "has" | "contains" | "member" => {
15586                if args.len() != 1 {
15587                    return Err(PerlError::runtime(
15588                        "set->has expects one argument (element)",
15589                        line,
15590                    ));
15591                }
15592                let k = crate::value::set_member_key(&args[0]);
15593                Ok(PerlValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
15594            }
15595            "size" | "len" | "count" => {
15596                if !args.is_empty() {
15597                    return Err(PerlError::runtime("set->size takes no arguments", line));
15598                }
15599                Ok(PerlValue::integer(s.len() as i64))
15600            }
15601            "values" | "list" | "elements" => {
15602                if !args.is_empty() {
15603                    return Err(PerlError::runtime("set->values takes no arguments", line));
15604                }
15605                Ok(PerlValue::array(s.values().cloned().collect()))
15606            }
15607            _ => Err(PerlError::runtime(
15608                format!("Unknown method for set: {}", method),
15609                line,
15610            )),
15611        }
15612    }
15613
15614    fn deque_method(
15615        &mut self,
15616        d: Arc<Mutex<VecDeque<PerlValue>>>,
15617        method: &str,
15618        args: &[PerlValue],
15619        line: usize,
15620    ) -> PerlResult<PerlValue> {
15621        match method {
15622            "push_back" => {
15623                if args.len() != 1 {
15624                    return Err(PerlError::runtime("push_back expects 1 argument", line));
15625                }
15626                d.lock().push_back(args[0].clone());
15627                Ok(PerlValue::integer(d.lock().len() as i64))
15628            }
15629            "push_front" => {
15630                if args.len() != 1 {
15631                    return Err(PerlError::runtime("push_front expects 1 argument", line));
15632                }
15633                d.lock().push_front(args[0].clone());
15634                Ok(PerlValue::integer(d.lock().len() as i64))
15635            }
15636            "pop_back" => Ok(d.lock().pop_back().unwrap_or(PerlValue::UNDEF)),
15637            "pop_front" => Ok(d.lock().pop_front().unwrap_or(PerlValue::UNDEF)),
15638            "size" | "len" => Ok(PerlValue::integer(d.lock().len() as i64)),
15639            _ => Err(PerlError::runtime(
15640                format!("Unknown method for deque: {}", method),
15641                line,
15642            )),
15643        }
15644    }
15645
15646    fn heap_method(
15647        &mut self,
15648        h: Arc<Mutex<PerlHeap>>,
15649        method: &str,
15650        args: &[PerlValue],
15651        line: usize,
15652    ) -> PerlResult<PerlValue> {
15653        match method {
15654            "push" => {
15655                if args.len() != 1 {
15656                    return Err(PerlError::runtime("heap push expects 1 argument", line));
15657                }
15658                let mut g = h.lock();
15659                let n = g.items.len();
15660                g.items.push(args[0].clone());
15661                let cmp = g.cmp.clone();
15662                drop(g);
15663                let mut g = h.lock();
15664                self.heap_sift_up(&mut g.items, &cmp, n);
15665                Ok(PerlValue::integer(g.items.len() as i64))
15666            }
15667            "pop" => {
15668                let mut g = h.lock();
15669                if g.items.is_empty() {
15670                    return Ok(PerlValue::UNDEF);
15671                }
15672                let cmp = g.cmp.clone();
15673                let n = g.items.len();
15674                g.items.swap(0, n - 1);
15675                let v = g.items.pop().unwrap();
15676                if !g.items.is_empty() {
15677                    self.heap_sift_down(&mut g.items, &cmp, 0);
15678                }
15679                Ok(v)
15680            }
15681            "peek" => Ok(h.lock().items.first().cloned().unwrap_or(PerlValue::UNDEF)),
15682            _ => Err(PerlError::runtime(
15683                format!("Unknown method for heap: {}", method),
15684                line,
15685            )),
15686        }
15687    }
15688
15689    fn ppool_method(
15690        &mut self,
15691        pool: PerlPpool,
15692        method: &str,
15693        args: &[PerlValue],
15694        line: usize,
15695    ) -> PerlResult<PerlValue> {
15696        match method {
15697            "submit" => pool.submit(self, args, line),
15698            "collect" => {
15699                if !args.is_empty() {
15700                    return Err(PerlError::runtime("collect() takes no arguments", line));
15701                }
15702                pool.collect(line)
15703            }
15704            _ => Err(PerlError::runtime(
15705                format!("Unknown method for ppool: {}", method),
15706                line,
15707            )),
15708        }
15709    }
15710
15711    fn barrier_method(
15712        &self,
15713        barrier: PerlBarrier,
15714        method: &str,
15715        args: &[PerlValue],
15716        line: usize,
15717    ) -> PerlResult<PerlValue> {
15718        match method {
15719            "wait" => {
15720                if !args.is_empty() {
15721                    return Err(PerlError::runtime("wait() takes no arguments", line));
15722                }
15723                let _ = barrier.0.wait();
15724                Ok(PerlValue::integer(1))
15725            }
15726            _ => Err(PerlError::runtime(
15727                format!("Unknown method for barrier: {}", method),
15728                line,
15729            )),
15730        }
15731    }
15732
15733    fn capture_method(
15734        &self,
15735        c: Arc<CaptureResult>,
15736        method: &str,
15737        args: &[PerlValue],
15738        line: usize,
15739    ) -> PerlResult<PerlValue> {
15740        if !args.is_empty() {
15741            return Err(PerlError::runtime(
15742                format!("capture: {} takes no arguments", method),
15743                line,
15744            ));
15745        }
15746        match method {
15747            "stdout" => Ok(PerlValue::string(c.stdout.clone())),
15748            "stderr" => Ok(PerlValue::string(c.stderr.clone())),
15749            "exitcode" => Ok(PerlValue::integer(c.exitcode)),
15750            "failed" => Ok(PerlValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
15751            _ => Err(PerlError::runtime(
15752                format!("Unknown method for capture: {}", method),
15753                line,
15754            )),
15755        }
15756    }
15757
15758    pub(crate) fn builtin_par_pipeline_stream(
15759        &mut self,
15760        args: &[PerlValue],
15761        _line: usize,
15762    ) -> PerlResult<PerlValue> {
15763        let mut items = Vec::new();
15764        for v in args {
15765            if let Some(a) = v.as_array_vec() {
15766                items.extend(a);
15767            } else {
15768                items.push(v.clone());
15769            }
15770        }
15771        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15772            source: items,
15773            ops: Vec::new(),
15774            has_scalar_terminal: false,
15775            par_stream: true,
15776            streaming: false,
15777            streaming_workers: 0,
15778            streaming_buffer: 256,
15779        }))))
15780    }
15781
15782    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
15783    /// that wires ops through bounded channels on `collect()`.
15784    pub(crate) fn builtin_par_pipeline_stream_new(
15785        &mut self,
15786        args: &[PerlValue],
15787        _line: usize,
15788    ) -> PerlResult<PerlValue> {
15789        let mut items = Vec::new();
15790        let mut workers: usize = 0;
15791        let mut buffer: usize = 256;
15792        // Separate list items from keyword args (workers => N, buffer => N).
15793        let mut i = 0;
15794        while i < args.len() {
15795            let s = args[i].to_string();
15796            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
15797                let val = args[i + 1].to_int().max(1) as usize;
15798                if s == "workers" {
15799                    workers = val;
15800                } else {
15801                    buffer = val;
15802                }
15803                i += 2;
15804            } else if let Some(a) = args[i].as_array_vec() {
15805                items.extend(a);
15806                i += 1;
15807            } else {
15808                items.push(args[i].clone());
15809                i += 1;
15810            }
15811        }
15812        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15813            source: items,
15814            ops: Vec::new(),
15815            has_scalar_terminal: false,
15816            par_stream: false,
15817            streaming: true,
15818            streaming_workers: workers,
15819            streaming_buffer: buffer,
15820        }))))
15821    }
15822
15823    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
15824    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<PerlSub> {
15825        let line = 1usize;
15826        let body = vec![Statement {
15827            label: None,
15828            kind: StmtKind::Expression(Expr {
15829                kind: ExprKind::BinOp {
15830                    left: Box::new(Expr {
15831                        kind: ExprKind::ScalarVar("_".into()),
15832                        line,
15833                    }),
15834                    op: BinOp::Mul,
15835                    right: Box::new(Expr {
15836                        kind: ExprKind::Integer(k),
15837                        line,
15838                    }),
15839                },
15840                line,
15841            }),
15842            line,
15843        }];
15844        Arc::new(PerlSub {
15845            name: "__pipeline_int_mul__".into(),
15846            params: vec![],
15847            body,
15848            closure_env: None,
15849            prototype: None,
15850            fib_like: None,
15851        })
15852    }
15853
15854    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<PerlSub> {
15855        let captured = self.scope.capture();
15856        Arc::new(PerlSub {
15857            name: "__ANON__".into(),
15858            params: vec![],
15859            body: block.clone(),
15860            closure_env: Some(captured),
15861            prototype: None,
15862            fib_like: None,
15863        })
15864    }
15865
15866    pub(crate) fn builtin_collect_execute(
15867        &mut self,
15868        args: &[PerlValue],
15869        line: usize,
15870    ) -> PerlResult<PerlValue> {
15871        if args.is_empty() {
15872            return Err(PerlError::runtime(
15873                "collect() expects at least one argument",
15874                line,
15875            ));
15876        }
15877        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
15878        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
15879        if args.len() == 1 {
15880            if let Some(p) = args[0].as_pipeline() {
15881                return self.pipeline_collect(&p, line);
15882            }
15883            return Ok(PerlValue::array(args[0].to_list()));
15884        }
15885        Ok(PerlValue::array(args.to_vec()))
15886    }
15887
15888    pub(crate) fn pipeline_push(
15889        &self,
15890        p: &Arc<Mutex<PipelineInner>>,
15891        op: PipelineOp,
15892        line: usize,
15893    ) -> PerlResult<()> {
15894        let mut g = p.lock();
15895        if g.has_scalar_terminal {
15896            return Err(PerlError::runtime(
15897                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
15898                line,
15899            ));
15900        }
15901        if matches!(
15902            &op,
15903            PipelineOp::PReduce { .. }
15904                | PipelineOp::PReduceInit { .. }
15905                | PipelineOp::PMapReduce { .. }
15906        ) {
15907            g.has_scalar_terminal = true;
15908        }
15909        g.ops.push(op);
15910        Ok(())
15911    }
15912
15913    fn pipeline_parse_sub_progress(
15914        args: &[PerlValue],
15915        line: usize,
15916        name: &str,
15917    ) -> PerlResult<(Arc<PerlSub>, bool)> {
15918        if args.is_empty() {
15919            return Err(PerlError::runtime(
15920                format!("pipeline {}: expects at least 1 argument (code ref)", name),
15921                line,
15922            ));
15923        }
15924        let Some(sub) = args[0].as_code_ref() else {
15925            return Err(PerlError::runtime(
15926                format!("pipeline {}: first argument must be a code reference", name),
15927                line,
15928            ));
15929        };
15930        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
15931        if args.len() > 2 {
15932            return Err(PerlError::runtime(
15933                format!(
15934                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
15935                    name
15936                ),
15937                line,
15938            ));
15939        }
15940        Ok((sub, progress))
15941    }
15942
15943    pub(crate) fn pipeline_method(
15944        &mut self,
15945        p: Arc<Mutex<PipelineInner>>,
15946        method: &str,
15947        args: &[PerlValue],
15948        line: usize,
15949    ) -> PerlResult<PerlValue> {
15950        match method {
15951            "filter" | "f" | "grep" => {
15952                if args.len() != 1 {
15953                    return Err(PerlError::runtime(
15954                        "pipeline filter/grep expects 1 argument (sub)",
15955                        line,
15956                    ));
15957                }
15958                let Some(sub) = args[0].as_code_ref() else {
15959                    return Err(PerlError::runtime(
15960                        "pipeline filter/grep expects a code reference",
15961                        line,
15962                    ));
15963                };
15964                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
15965                Ok(PerlValue::pipeline(Arc::clone(&p)))
15966            }
15967            "map" => {
15968                if args.len() != 1 {
15969                    return Err(PerlError::runtime(
15970                        "pipeline map expects 1 argument (sub)",
15971                        line,
15972                    ));
15973                }
15974                let Some(sub) = args[0].as_code_ref() else {
15975                    return Err(PerlError::runtime(
15976                        "pipeline map expects a code reference",
15977                        line,
15978                    ));
15979                };
15980                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
15981                Ok(PerlValue::pipeline(Arc::clone(&p)))
15982            }
15983            "tap" | "peek" => {
15984                if args.len() != 1 {
15985                    return Err(PerlError::runtime(
15986                        "pipeline tap/peek expects 1 argument (sub)",
15987                        line,
15988                    ));
15989                }
15990                let Some(sub) = args[0].as_code_ref() else {
15991                    return Err(PerlError::runtime(
15992                        "pipeline tap/peek expects a code reference",
15993                        line,
15994                    ));
15995                };
15996                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
15997                Ok(PerlValue::pipeline(Arc::clone(&p)))
15998            }
15999            "take" => {
16000                if args.len() != 1 {
16001                    return Err(PerlError::runtime("pipeline take expects 1 argument", line));
16002                }
16003                let n = args[0].to_int();
16004                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
16005                Ok(PerlValue::pipeline(Arc::clone(&p)))
16006            }
16007            "pmap" => {
16008                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
16009                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
16010                Ok(PerlValue::pipeline(Arc::clone(&p)))
16011            }
16012            "pgrep" => {
16013                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
16014                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
16015                Ok(PerlValue::pipeline(Arc::clone(&p)))
16016            }
16017            "pfor" => {
16018                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
16019                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
16020                Ok(PerlValue::pipeline(Arc::clone(&p)))
16021            }
16022            "pmap_chunked" => {
16023                if args.len() < 2 {
16024                    return Err(PerlError::runtime(
16025                        "pipeline pmap_chunked expects chunk size and a code reference",
16026                        line,
16027                    ));
16028                }
16029                let chunk = args[0].to_int().max(1);
16030                let Some(sub) = args[1].as_code_ref() else {
16031                    return Err(PerlError::runtime(
16032                        "pipeline pmap_chunked: second argument must be a code reference",
16033                        line,
16034                    ));
16035                };
16036                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
16037                if args.len() > 3 {
16038                    return Err(PerlError::runtime(
16039                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
16040                        line,
16041                    ));
16042                }
16043                self.pipeline_push(
16044                    &p,
16045                    PipelineOp::PMapChunked {
16046                        chunk,
16047                        sub,
16048                        progress,
16049                    },
16050                    line,
16051                )?;
16052                Ok(PerlValue::pipeline(Arc::clone(&p)))
16053            }
16054            "psort" => {
16055                let (cmp, progress) = match args.len() {
16056                    0 => (None, false),
16057                    1 => {
16058                        if let Some(s) = args[0].as_code_ref() {
16059                            (Some(s), false)
16060                        } else {
16061                            (None, args[0].is_true())
16062                        }
16063                    }
16064                    2 => {
16065                        let Some(s) = args[0].as_code_ref() else {
16066                            return Err(PerlError::runtime(
16067                                "pipeline psort: with two arguments, the first must be a comparator sub",
16068                                line,
16069                            ));
16070                        };
16071                        (Some(s), args[1].is_true())
16072                    }
16073                    _ => {
16074                        return Err(PerlError::runtime(
16075                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
16076                            line,
16077                        ));
16078                    }
16079                };
16080                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
16081                Ok(PerlValue::pipeline(Arc::clone(&p)))
16082            }
16083            "pcache" => {
16084                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
16085                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
16086                Ok(PerlValue::pipeline(Arc::clone(&p)))
16087            }
16088            "preduce" => {
16089                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
16090                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
16091                Ok(PerlValue::pipeline(Arc::clone(&p)))
16092            }
16093            "preduce_init" => {
16094                if args.len() < 2 {
16095                    return Err(PerlError::runtime(
16096                        "pipeline preduce_init expects init value and a code reference",
16097                        line,
16098                    ));
16099                }
16100                let init = args[0].clone();
16101                let Some(sub) = args[1].as_code_ref() else {
16102                    return Err(PerlError::runtime(
16103                        "pipeline preduce_init: second argument must be a code reference",
16104                        line,
16105                    ));
16106                };
16107                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
16108                if args.len() > 3 {
16109                    return Err(PerlError::runtime(
16110                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
16111                        line,
16112                    ));
16113                }
16114                self.pipeline_push(
16115                    &p,
16116                    PipelineOp::PReduceInit {
16117                        init,
16118                        sub,
16119                        progress,
16120                    },
16121                    line,
16122                )?;
16123                Ok(PerlValue::pipeline(Arc::clone(&p)))
16124            }
16125            "pmap_reduce" => {
16126                if args.len() < 2 {
16127                    return Err(PerlError::runtime(
16128                        "pipeline pmap_reduce expects map sub and reduce sub",
16129                        line,
16130                    ));
16131                }
16132                let Some(map) = args[0].as_code_ref() else {
16133                    return Err(PerlError::runtime(
16134                        "pipeline pmap_reduce: first argument must be a code reference (map)",
16135                        line,
16136                    ));
16137                };
16138                let Some(reduce) = args[1].as_code_ref() else {
16139                    return Err(PerlError::runtime(
16140                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
16141                        line,
16142                    ));
16143                };
16144                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
16145                if args.len() > 3 {
16146                    return Err(PerlError::runtime(
16147                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
16148                        line,
16149                    ));
16150                }
16151                self.pipeline_push(
16152                    &p,
16153                    PipelineOp::PMapReduce {
16154                        map,
16155                        reduce,
16156                        progress,
16157                    },
16158                    line,
16159                )?;
16160                Ok(PerlValue::pipeline(Arc::clone(&p)))
16161            }
16162            "collect" => {
16163                if !args.is_empty() {
16164                    return Err(PerlError::runtime(
16165                        "pipeline collect takes no arguments",
16166                        line,
16167                    ));
16168                }
16169                self.pipeline_collect(&p, line)
16170            }
16171            _ => {
16172                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
16173                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
16174                if let Some(sub) = self.resolve_sub_by_name(method) {
16175                    if !args.is_empty() {
16176                        return Err(PerlError::runtime(
16177                            format!(
16178                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
16179                                method
16180                            ),
16181                            line,
16182                        ));
16183                    }
16184                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
16185                    Ok(PerlValue::pipeline(Arc::clone(&p)))
16186                } else {
16187                    Err(PerlError::runtime(
16188                        format!("Unknown method for pipeline: {}", method),
16189                        line,
16190                    ))
16191                }
16192            }
16193        }
16194    }
16195
16196    fn pipeline_parallel_map(
16197        &mut self,
16198        items: Vec<PerlValue>,
16199        sub: &Arc<PerlSub>,
16200        progress: bool,
16201    ) -> Vec<PerlValue> {
16202        let subs = self.subs.clone();
16203        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16204        let pmap_progress = PmapProgress::new(progress, items.len());
16205        let results: Vec<PerlValue> = items
16206            .into_par_iter()
16207            .map(|item| {
16208                let mut local_interp = Interpreter::new();
16209                local_interp.subs = subs.clone();
16210                local_interp.scope.restore_capture(&scope_capture);
16211                local_interp
16212                    .scope
16213                    .restore_atomics(&atomic_arrays, &atomic_hashes);
16214                local_interp.enable_parallel_guard();
16215                local_interp.scope.set_topic(item);
16216                local_interp.scope_push_hook();
16217                let val = match local_interp.exec_block_no_scope(&sub.body) {
16218                    Ok(val) => val,
16219                    Err(_) => PerlValue::UNDEF,
16220                };
16221                local_interp.scope_pop_hook();
16222                pmap_progress.tick();
16223                val
16224            })
16225            .collect();
16226        pmap_progress.finish();
16227        results
16228    }
16229
16230    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
16231    fn pipeline_par_stream_filter(
16232        &mut self,
16233        items: Vec<PerlValue>,
16234        sub: &Arc<PerlSub>,
16235    ) -> Vec<PerlValue> {
16236        if items.is_empty() {
16237            return items;
16238        }
16239        let subs = self.subs.clone();
16240        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16241        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
16242        let mut kept: Vec<(usize, PerlValue)> = indexed
16243            .into_par_iter()
16244            .filter_map(|(i, item)| {
16245                let mut local_interp = Interpreter::new();
16246                local_interp.subs = subs.clone();
16247                local_interp.scope.restore_capture(&scope_capture);
16248                local_interp
16249                    .scope
16250                    .restore_atomics(&atomic_arrays, &atomic_hashes);
16251                local_interp.enable_parallel_guard();
16252                local_interp.scope.set_topic(item.clone());
16253                local_interp.scope_push_hook();
16254                let keep = match local_interp.exec_block_no_scope(&sub.body) {
16255                    Ok(val) => val.is_true(),
16256                    Err(_) => false,
16257                };
16258                local_interp.scope_pop_hook();
16259                if keep {
16260                    Some((i, item))
16261                } else {
16262                    None
16263                }
16264            })
16265            .collect();
16266        kept.sort_by_key(|(i, _)| *i);
16267        kept.into_iter().map(|(_, x)| x).collect()
16268    }
16269
16270    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
16271    fn pipeline_par_stream_map(
16272        &mut self,
16273        items: Vec<PerlValue>,
16274        sub: &Arc<PerlSub>,
16275    ) -> Vec<PerlValue> {
16276        if items.is_empty() {
16277            return items;
16278        }
16279        let subs = self.subs.clone();
16280        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16281        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
16282        let mut mapped: Vec<(usize, PerlValue)> = indexed
16283            .into_par_iter()
16284            .map(|(i, item)| {
16285                let mut local_interp = Interpreter::new();
16286                local_interp.subs = subs.clone();
16287                local_interp.scope.restore_capture(&scope_capture);
16288                local_interp
16289                    .scope
16290                    .restore_atomics(&atomic_arrays, &atomic_hashes);
16291                local_interp.enable_parallel_guard();
16292                local_interp.scope.set_topic(item);
16293                local_interp.scope_push_hook();
16294                let val = match local_interp.exec_block_no_scope(&sub.body) {
16295                    Ok(val) => val,
16296                    Err(_) => PerlValue::UNDEF,
16297                };
16298                local_interp.scope_pop_hook();
16299                (i, val)
16300            })
16301            .collect();
16302        mapped.sort_by_key(|(i, _)| *i);
16303        mapped.into_iter().map(|(_, x)| x).collect()
16304    }
16305
16306    fn pipeline_collect(
16307        &mut self,
16308        p: &Arc<Mutex<PipelineInner>>,
16309        line: usize,
16310    ) -> PerlResult<PerlValue> {
16311        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
16312            let g = p.lock();
16313            (
16314                g.source.clone(),
16315                g.ops.clone(),
16316                g.par_stream,
16317                g.streaming,
16318                g.streaming_workers,
16319                g.streaming_buffer,
16320            )
16321        };
16322        if streaming {
16323            return self.pipeline_collect_streaming(
16324                v,
16325                &ops,
16326                streaming_workers,
16327                streaming_buffer,
16328                line,
16329            );
16330        }
16331        for op in ops {
16332            match op {
16333                PipelineOp::Filter(sub) => {
16334                    if par_stream {
16335                        v = self.pipeline_par_stream_filter(v, &sub);
16336                    } else {
16337                        let mut out = Vec::new();
16338                        for item in v {
16339                            self.scope_push_hook();
16340                            self.scope.set_topic(item.clone());
16341                            if let Some(ref env) = sub.closure_env {
16342                                self.scope.restore_capture(env);
16343                            }
16344                            let keep = match self.exec_block_no_scope(&sub.body) {
16345                                Ok(val) => val.is_true(),
16346                                Err(_) => false,
16347                            };
16348                            self.scope_pop_hook();
16349                            if keep {
16350                                out.push(item);
16351                            }
16352                        }
16353                        v = out;
16354                    }
16355                }
16356                PipelineOp::Map(sub) => {
16357                    if par_stream {
16358                        v = self.pipeline_par_stream_map(v, &sub);
16359                    } else {
16360                        let mut out = Vec::new();
16361                        for item in v {
16362                            self.scope_push_hook();
16363                            self.scope.set_topic(item);
16364                            if let Some(ref env) = sub.closure_env {
16365                                self.scope.restore_capture(env);
16366                            }
16367                            let mapped = match self.exec_block_no_scope(&sub.body) {
16368                                Ok(val) => val,
16369                                Err(_) => PerlValue::UNDEF,
16370                            };
16371                            self.scope_pop_hook();
16372                            out.push(mapped);
16373                        }
16374                        v = out;
16375                    }
16376                }
16377                PipelineOp::Tap(sub) => {
16378                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
16379                        Ok(_) => {}
16380                        Err(FlowOrError::Error(e)) => return Err(e),
16381                        Err(FlowOrError::Flow(_)) => {
16382                            return Err(PerlError::runtime(
16383                                "tap: unsupported control flow in block",
16384                                line,
16385                            ));
16386                        }
16387                    }
16388                }
16389                PipelineOp::Take(n) => {
16390                    let n = n.max(0) as usize;
16391                    if v.len() > n {
16392                        v.truncate(n);
16393                    }
16394                }
16395                PipelineOp::PMap { sub, progress } => {
16396                    v = self.pipeline_parallel_map(v, &sub, progress);
16397                }
16398                PipelineOp::PGrep { sub, progress } => {
16399                    let subs = self.subs.clone();
16400                    let (scope_capture, atomic_arrays, atomic_hashes) =
16401                        self.scope.capture_with_atomics();
16402                    let pmap_progress = PmapProgress::new(progress, v.len());
16403                    v = v
16404                        .into_par_iter()
16405                        .filter_map(|item| {
16406                            let mut local_interp = Interpreter::new();
16407                            local_interp.subs = subs.clone();
16408                            local_interp.scope.restore_capture(&scope_capture);
16409                            local_interp
16410                                .scope
16411                                .restore_atomics(&atomic_arrays, &atomic_hashes);
16412                            local_interp.enable_parallel_guard();
16413                            local_interp.scope.set_topic(item.clone());
16414                            local_interp.scope_push_hook();
16415                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
16416                                Ok(val) => val.is_true(),
16417                                Err(_) => false,
16418                            };
16419                            local_interp.scope_pop_hook();
16420                            pmap_progress.tick();
16421                            if keep {
16422                                Some(item)
16423                            } else {
16424                                None
16425                            }
16426                        })
16427                        .collect();
16428                    pmap_progress.finish();
16429                }
16430                PipelineOp::PFor { sub, progress } => {
16431                    let subs = self.subs.clone();
16432                    let (scope_capture, atomic_arrays, atomic_hashes) =
16433                        self.scope.capture_with_atomics();
16434                    let pmap_progress = PmapProgress::new(progress, v.len());
16435                    let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
16436                    v.clone().into_par_iter().for_each(|item| {
16437                        if first_err.lock().is_some() {
16438                            return;
16439                        }
16440                        let mut local_interp = Interpreter::new();
16441                        local_interp.subs = subs.clone();
16442                        local_interp.scope.restore_capture(&scope_capture);
16443                        local_interp
16444                            .scope
16445                            .restore_atomics(&atomic_arrays, &atomic_hashes);
16446                        local_interp.enable_parallel_guard();
16447                        local_interp.scope.set_topic(item);
16448                        local_interp.scope_push_hook();
16449                        match local_interp.exec_block_no_scope(&sub.body) {
16450                            Ok(_) => {}
16451                            Err(e) => {
16452                                let stryke = match e {
16453                                    FlowOrError::Error(stryke) => stryke,
16454                                    FlowOrError::Flow(_) => PerlError::runtime(
16455                                        "return/last/next/redo not supported inside pipeline pfor block",
16456                                        line,
16457                                    ),
16458                                };
16459                                let mut g = first_err.lock();
16460                                if g.is_none() {
16461                                    *g = Some(stryke);
16462                                }
16463                            }
16464                        }
16465                        local_interp.scope_pop_hook();
16466                        pmap_progress.tick();
16467                    });
16468                    pmap_progress.finish();
16469                    let pfor_err = first_err.lock().take();
16470                    if let Some(e) = pfor_err {
16471                        return Err(e);
16472                    }
16473                }
16474                PipelineOp::PMapChunked {
16475                    chunk,
16476                    sub,
16477                    progress,
16478                } => {
16479                    let chunk_n = chunk.max(1) as usize;
16480                    let subs = self.subs.clone();
16481                    let (scope_capture, atomic_arrays, atomic_hashes) =
16482                        self.scope.capture_with_atomics();
16483                    let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = v
16484                        .chunks(chunk_n)
16485                        .enumerate()
16486                        .map(|(i, c)| (i, c.to_vec()))
16487                        .collect();
16488                    let n_chunks = indexed_chunks.len();
16489                    let pmap_progress = PmapProgress::new(progress, n_chunks);
16490                    let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
16491                        .into_par_iter()
16492                        .map(|(chunk_idx, chunk)| {
16493                            let mut local_interp = Interpreter::new();
16494                            local_interp.subs = subs.clone();
16495                            local_interp.scope.restore_capture(&scope_capture);
16496                            local_interp
16497                                .scope
16498                                .restore_atomics(&atomic_arrays, &atomic_hashes);
16499                            local_interp.enable_parallel_guard();
16500                            let mut out = Vec::with_capacity(chunk.len());
16501                            for item in chunk {
16502                                local_interp.scope.set_topic(item);
16503                                local_interp.scope_push_hook();
16504                                match local_interp.exec_block_no_scope(&sub.body) {
16505                                    Ok(val) => {
16506                                        local_interp.scope_pop_hook();
16507                                        out.push(val);
16508                                    }
16509                                    Err(_) => {
16510                                        local_interp.scope_pop_hook();
16511                                        out.push(PerlValue::UNDEF);
16512                                    }
16513                                }
16514                            }
16515                            pmap_progress.tick();
16516                            (chunk_idx, out)
16517                        })
16518                        .collect();
16519                    pmap_progress.finish();
16520                    chunk_results.sort_by_key(|(i, _)| *i);
16521                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
16522                }
16523                PipelineOp::PSort { cmp, progress } => {
16524                    let pmap_progress = PmapProgress::new(progress, 2);
16525                    pmap_progress.tick();
16526                    match cmp {
16527                        Some(cmp_block) => {
16528                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
16529                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
16530                            } else {
16531                                let subs = self.subs.clone();
16532                                let scope_capture = self.scope.capture();
16533                                v.par_sort_by(|a, b| {
16534                                    let mut local_interp = Interpreter::new();
16535                                    local_interp.subs = subs.clone();
16536                                    local_interp.scope.restore_capture(&scope_capture);
16537                                    local_interp.enable_parallel_guard();
16538                                    let _ = local_interp.scope.set_scalar("a", a.clone());
16539                                    let _ = local_interp.scope.set_scalar("b", b.clone());
16540                                    let _ = local_interp.scope.set_scalar("_0", a.clone());
16541                                    let _ = local_interp.scope.set_scalar("_1", b.clone());
16542                                    local_interp.scope_push_hook();
16543                                    let ord =
16544                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
16545                                            Ok(v) => {
16546                                                let n = v.to_int();
16547                                                if n < 0 {
16548                                                    std::cmp::Ordering::Less
16549                                                } else if n > 0 {
16550                                                    std::cmp::Ordering::Greater
16551                                                } else {
16552                                                    std::cmp::Ordering::Equal
16553                                                }
16554                                            }
16555                                            Err(_) => std::cmp::Ordering::Equal,
16556                                        };
16557                                    local_interp.scope_pop_hook();
16558                                    ord
16559                                });
16560                            }
16561                        }
16562                        None => {
16563                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
16564                        }
16565                    }
16566                    pmap_progress.tick();
16567                    pmap_progress.finish();
16568                }
16569                PipelineOp::PCache { sub, progress } => {
16570                    let subs = self.subs.clone();
16571                    let scope_capture = self.scope.capture();
16572                    let cache = &*crate::pcache::GLOBAL_PCACHE;
16573                    let pmap_progress = PmapProgress::new(progress, v.len());
16574                    v = v
16575                        .into_par_iter()
16576                        .map(|item| {
16577                            let k = crate::pcache::cache_key(&item);
16578                            if let Some(cached) = cache.get(&k) {
16579                                pmap_progress.tick();
16580                                return cached.clone();
16581                            }
16582                            let mut local_interp = Interpreter::new();
16583                            local_interp.subs = subs.clone();
16584                            local_interp.scope.restore_capture(&scope_capture);
16585                            local_interp.enable_parallel_guard();
16586                            local_interp.scope.set_topic(item.clone());
16587                            local_interp.scope_push_hook();
16588                            let val = match local_interp.exec_block_no_scope(&sub.body) {
16589                                Ok(v) => v,
16590                                Err(_) => PerlValue::UNDEF,
16591                            };
16592                            local_interp.scope_pop_hook();
16593                            cache.insert(k, val.clone());
16594                            pmap_progress.tick();
16595                            val
16596                        })
16597                        .collect();
16598                    pmap_progress.finish();
16599                }
16600                PipelineOp::PReduce { sub, progress } => {
16601                    if v.is_empty() {
16602                        return Ok(PerlValue::UNDEF);
16603                    }
16604                    if v.len() == 1 {
16605                        return Ok(v.into_iter().next().unwrap());
16606                    }
16607                    let block = sub.body.clone();
16608                    let subs = self.subs.clone();
16609                    let scope_capture = self.scope.capture();
16610                    let pmap_progress = PmapProgress::new(progress, v.len());
16611                    let result = v
16612                        .into_par_iter()
16613                        .map(|x| {
16614                            pmap_progress.tick();
16615                            x
16616                        })
16617                        .reduce_with(|a, b| {
16618                            let mut local_interp = Interpreter::new();
16619                            local_interp.subs = subs.clone();
16620                            local_interp.scope.restore_capture(&scope_capture);
16621                            local_interp.enable_parallel_guard();
16622                            let _ = local_interp.scope.set_scalar("a", a.clone());
16623                            let _ = local_interp.scope.set_scalar("b", b.clone());
16624                            let _ = local_interp.scope.set_scalar("_0", a);
16625                            let _ = local_interp.scope.set_scalar("_1", b);
16626                            match local_interp.exec_block(&block) {
16627                                Ok(val) => val,
16628                                Err(_) => PerlValue::UNDEF,
16629                            }
16630                        });
16631                    pmap_progress.finish();
16632                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16633                }
16634                PipelineOp::PReduceInit {
16635                    init,
16636                    sub,
16637                    progress,
16638                } => {
16639                    if v.is_empty() {
16640                        return Ok(init);
16641                    }
16642                    let block = sub.body.clone();
16643                    let subs = self.subs.clone();
16644                    let scope_capture = self.scope.capture();
16645                    let cap: &[(String, PerlValue)] = scope_capture.as_slice();
16646                    if v.len() == 1 {
16647                        return Ok(fold_preduce_init_step(
16648                            &subs,
16649                            cap,
16650                            &block,
16651                            preduce_init_fold_identity(&init),
16652                            v.into_iter().next().unwrap(),
16653                        ));
16654                    }
16655                    let pmap_progress = PmapProgress::new(progress, v.len());
16656                    let result = v
16657                        .into_par_iter()
16658                        .fold(
16659                            || preduce_init_fold_identity(&init),
16660                            |acc, item| {
16661                                pmap_progress.tick();
16662                                fold_preduce_init_step(&subs, cap, &block, acc, item)
16663                            },
16664                        )
16665                        .reduce(
16666                            || preduce_init_fold_identity(&init),
16667                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
16668                        );
16669                    pmap_progress.finish();
16670                    return Ok(result);
16671                }
16672                PipelineOp::PMapReduce {
16673                    map,
16674                    reduce,
16675                    progress,
16676                } => {
16677                    if v.is_empty() {
16678                        return Ok(PerlValue::UNDEF);
16679                    }
16680                    let map_block = map.body.clone();
16681                    let reduce_block = reduce.body.clone();
16682                    let subs = self.subs.clone();
16683                    let scope_capture = self.scope.capture();
16684                    if v.len() == 1 {
16685                        let mut local_interp = Interpreter::new();
16686                        local_interp.subs = subs.clone();
16687                        local_interp.scope.restore_capture(&scope_capture);
16688                        local_interp.scope.set_topic(v[0].clone());
16689                        return match local_interp.exec_block_no_scope(&map_block) {
16690                            Ok(val) => Ok(val),
16691                            Err(_) => Ok(PerlValue::UNDEF),
16692                        };
16693                    }
16694                    let pmap_progress = PmapProgress::new(progress, v.len());
16695                    let result = v
16696                        .into_par_iter()
16697                        .map(|item| {
16698                            let mut local_interp = Interpreter::new();
16699                            local_interp.subs = subs.clone();
16700                            local_interp.scope.restore_capture(&scope_capture);
16701                            local_interp.scope.set_topic(item);
16702                            let val = match local_interp.exec_block_no_scope(&map_block) {
16703                                Ok(val) => val,
16704                                Err(_) => PerlValue::UNDEF,
16705                            };
16706                            pmap_progress.tick();
16707                            val
16708                        })
16709                        .reduce_with(|a, b| {
16710                            let mut local_interp = Interpreter::new();
16711                            local_interp.subs = subs.clone();
16712                            local_interp.scope.restore_capture(&scope_capture);
16713                            let _ = local_interp.scope.set_scalar("a", a.clone());
16714                            let _ = local_interp.scope.set_scalar("b", b.clone());
16715                            let _ = local_interp.scope.set_scalar("_0", a);
16716                            let _ = local_interp.scope.set_scalar("_1", b);
16717                            match local_interp.exec_block_no_scope(&reduce_block) {
16718                                Ok(val) => val,
16719                                Err(_) => PerlValue::UNDEF,
16720                            }
16721                        });
16722                    pmap_progress.finish();
16723                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16724                }
16725            }
16726        }
16727        Ok(PerlValue::array(v))
16728    }
16729
16730    /// Streaming collect: wire pipeline ops through bounded channels so items flow
16731    /// between stages concurrently.  Order is **not** preserved.
16732    fn pipeline_collect_streaming(
16733        &mut self,
16734        source: Vec<PerlValue>,
16735        ops: &[PipelineOp],
16736        workers_per_stage: usize,
16737        buffer: usize,
16738        line: usize,
16739    ) -> PerlResult<PerlValue> {
16740        use crossbeam::channel::{bounded, Receiver, Sender};
16741
16742        // Validate: reject ops that require all items (can't stream).
16743        for op in ops {
16744            match op {
16745                PipelineOp::PSort { .. }
16746                | PipelineOp::PReduce { .. }
16747                | PipelineOp::PReduceInit { .. }
16748                | PipelineOp::PMapReduce { .. }
16749                | PipelineOp::PMapChunked { .. } => {
16750                    return Err(PerlError::runtime(
16751                        format!(
16752                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
16753                            std::mem::discriminant(op)
16754                        ),
16755                        line,
16756                    ));
16757                }
16758                _ => {}
16759            }
16760        }
16761
16762        // Filter out non-streamable ops and collect streamable ones.
16763        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
16764        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
16765        if streamable_ops.is_empty() {
16766            return Ok(PerlValue::array(source));
16767        }
16768
16769        let n_stages = streamable_ops.len();
16770        let wn = if workers_per_stage > 0 {
16771            workers_per_stage
16772        } else {
16773            self.parallel_thread_count()
16774        };
16775        let subs = self.subs.clone();
16776        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16777
16778        // Build channels: one between each pair of stages, plus one for output.
16779        // channel[0]: source → stage 0
16780        // channel[i]: stage i-1 → stage i
16781        // channel[n_stages]: stage n_stages-1 → collector
16782        let mut channels: Vec<(Sender<PerlValue>, Receiver<PerlValue>)> =
16783            (0..=n_stages).map(|_| bounded(buffer)).collect();
16784
16785        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
16786        let take_done: Arc<std::sync::atomic::AtomicBool> =
16787            Arc::new(std::sync::atomic::AtomicBool::new(false));
16788
16789        // Collect senders/receivers for each stage.
16790        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
16791        let source_tx = channels[0].0.clone();
16792        let result_rx = channels[n_stages].1.clone();
16793        let results: Arc<Mutex<Vec<PerlValue>>> = Arc::new(Mutex::new(Vec::new()));
16794
16795        std::thread::scope(|scope| {
16796            // Collector thread: drain results concurrently to avoid deadlock
16797            // when bounded channels fill up.
16798            let result_rx_c = result_rx.clone();
16799            let results_c = Arc::clone(&results);
16800            scope.spawn(move || {
16801                while let Ok(item) = result_rx_c.recv() {
16802                    results_c.lock().push(item);
16803                }
16804            });
16805
16806            // Source feeder thread.
16807            let err_s = Arc::clone(&err);
16808            let take_done_s = Arc::clone(&take_done);
16809            scope.spawn(move || {
16810                for item in source {
16811                    if err_s.lock().is_some()
16812                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
16813                    {
16814                        break;
16815                    }
16816                    if source_tx.send(item).is_err() {
16817                        break;
16818                    }
16819                }
16820            });
16821
16822            // Spawn workers for each stage.
16823            for (stage_idx, op) in streamable_ops.iter().enumerate() {
16824                let rx = channels[stage_idx].1.clone();
16825                let tx = channels[stage_idx + 1].0.clone();
16826
16827                for _ in 0..wn {
16828                    let rx = rx.clone();
16829                    let tx = tx.clone();
16830                    let subs = subs.clone();
16831                    let capture = capture.clone();
16832                    let atomic_arrays = atomic_arrays.clone();
16833                    let atomic_hashes = atomic_hashes.clone();
16834                    let err_w = Arc::clone(&err);
16835                    let take_done_w = Arc::clone(&take_done);
16836
16837                    match *op {
16838                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
16839                            let sub = Arc::clone(sub);
16840                            scope.spawn(move || {
16841                                while let Ok(item) = rx.recv() {
16842                                    if err_w.lock().is_some() {
16843                                        break;
16844                                    }
16845                                    let mut interp = Interpreter::new();
16846                                    interp.subs = subs.clone();
16847                                    interp.scope.restore_capture(&capture);
16848                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16849                                    interp.enable_parallel_guard();
16850                                    interp.scope.set_topic(item.clone());
16851                                    interp.scope_push_hook();
16852                                    let keep = match interp.exec_block_no_scope(&sub.body) {
16853                                        Ok(val) => val.is_true(),
16854                                        Err(_) => false,
16855                                    };
16856                                    interp.scope_pop_hook();
16857                                    if keep && tx.send(item).is_err() {
16858                                        break;
16859                                    }
16860                                }
16861                            });
16862                        }
16863                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
16864                            let sub = Arc::clone(sub);
16865                            scope.spawn(move || {
16866                                while let Ok(item) = rx.recv() {
16867                                    if err_w.lock().is_some() {
16868                                        break;
16869                                    }
16870                                    let mut interp = Interpreter::new();
16871                                    interp.subs = subs.clone();
16872                                    interp.scope.restore_capture(&capture);
16873                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16874                                    interp.enable_parallel_guard();
16875                                    interp.scope.set_topic(item);
16876                                    interp.scope_push_hook();
16877                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
16878                                        Ok(val) => val,
16879                                        Err(_) => PerlValue::UNDEF,
16880                                    };
16881                                    interp.scope_pop_hook();
16882                                    if tx.send(mapped).is_err() {
16883                                        break;
16884                                    }
16885                                }
16886                            });
16887                        }
16888                        PipelineOp::Take(n) => {
16889                            let limit = (*n).max(0) as usize;
16890                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
16891                            let count_w = Arc::clone(&count);
16892                            scope.spawn(move || {
16893                                while let Ok(item) = rx.recv() {
16894                                    let prev =
16895                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
16896                                    if prev >= limit {
16897                                        take_done_w
16898                                            .store(true, std::sync::atomic::Ordering::Relaxed);
16899                                        break;
16900                                    }
16901                                    if tx.send(item).is_err() {
16902                                        break;
16903                                    }
16904                                }
16905                            });
16906                            // Take only needs 1 worker; skip remaining worker spawns.
16907                            break;
16908                        }
16909                        PipelineOp::PFor { ref sub, .. } => {
16910                            let sub = Arc::clone(sub);
16911                            scope.spawn(move || {
16912                                while let Ok(item) = rx.recv() {
16913                                    if err_w.lock().is_some() {
16914                                        break;
16915                                    }
16916                                    let mut interp = Interpreter::new();
16917                                    interp.subs = subs.clone();
16918                                    interp.scope.restore_capture(&capture);
16919                                    interp
16920                                        .scope
16921                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16922                                    interp.enable_parallel_guard();
16923                                    interp.scope.set_topic(item.clone());
16924                                    interp.scope_push_hook();
16925                                    match interp.exec_block_no_scope(&sub.body) {
16926                                        Ok(_) => {}
16927                                        Err(e) => {
16928                                            let msg = match e {
16929                                                FlowOrError::Error(stryke) => stryke.to_string(),
16930                                                FlowOrError::Flow(_) => {
16931                                                    "unexpected control flow in par_pipeline_stream pfor".into()
16932                                                }
16933                                            };
16934                                            let mut g = err_w.lock();
16935                                            if g.is_none() {
16936                                                *g = Some(msg);
16937                                            }
16938                                            interp.scope_pop_hook();
16939                                            break;
16940                                        }
16941                                    }
16942                                    interp.scope_pop_hook();
16943                                    if tx.send(item).is_err() {
16944                                        break;
16945                                    }
16946                                }
16947                            });
16948                        }
16949                        PipelineOp::Tap(ref sub) => {
16950                            let sub = Arc::clone(sub);
16951                            scope.spawn(move || {
16952                                while let Ok(item) = rx.recv() {
16953                                    if err_w.lock().is_some() {
16954                                        break;
16955                                    }
16956                                    let mut interp = Interpreter::new();
16957                                    interp.subs = subs.clone();
16958                                    interp.scope.restore_capture(&capture);
16959                                    interp
16960                                        .scope
16961                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16962                                    interp.enable_parallel_guard();
16963                                    match interp.call_sub(
16964                                        &sub,
16965                                        vec![item.clone()],
16966                                        WantarrayCtx::Void,
16967                                        line,
16968                                    )
16969                                    {
16970                                        Ok(_) => {}
16971                                        Err(e) => {
16972                                            let msg = match e {
16973                                                FlowOrError::Error(stryke) => stryke.to_string(),
16974                                                FlowOrError::Flow(_) => {
16975                                                    "unexpected control flow in par_pipeline_stream tap"
16976                                                        .into()
16977                                                }
16978                                            };
16979                                            let mut g = err_w.lock();
16980                                            if g.is_none() {
16981                                                *g = Some(msg);
16982                                            }
16983                                            break;
16984                                        }
16985                                    }
16986                                    if tx.send(item).is_err() {
16987                                        break;
16988                                    }
16989                                }
16990                            });
16991                        }
16992                        PipelineOp::PCache { ref sub, .. } => {
16993                            let sub = Arc::clone(sub);
16994                            scope.spawn(move || {
16995                                while let Ok(item) = rx.recv() {
16996                                    if err_w.lock().is_some() {
16997                                        break;
16998                                    }
16999                                    let k = crate::pcache::cache_key(&item);
17000                                    let val = if let Some(cached) =
17001                                        crate::pcache::GLOBAL_PCACHE.get(&k)
17002                                    {
17003                                        cached.clone()
17004                                    } else {
17005                                        let mut interp = Interpreter::new();
17006                                        interp.subs = subs.clone();
17007                                        interp.scope.restore_capture(&capture);
17008                                        interp
17009                                            .scope
17010                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
17011                                        interp.enable_parallel_guard();
17012                                        interp.scope.set_topic(item);
17013                                        interp.scope_push_hook();
17014                                        let v = match interp.exec_block_no_scope(&sub.body) {
17015                                            Ok(v) => v,
17016                                            Err(_) => PerlValue::UNDEF,
17017                                        };
17018                                        interp.scope_pop_hook();
17019                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
17020                                        v
17021                                    };
17022                                    if tx.send(val).is_err() {
17023                                        break;
17024                                    }
17025                                }
17026                            });
17027                        }
17028                        // Non-streaming ops already rejected above.
17029                        _ => unreachable!(),
17030                    }
17031                }
17032            }
17033
17034            // Drop our copies of intermediate senders/receivers so channels disconnect
17035            // when workers finish.  Also drop result_rx so the collector thread exits
17036            // once all stage workers are done.
17037            channels.clear();
17038            drop(result_rx);
17039        });
17040
17041        if let Some(msg) = err.lock().take() {
17042            return Err(PerlError::runtime(msg, line));
17043        }
17044
17045        let results = std::mem::take(&mut *results.lock());
17046        Ok(PerlValue::array(results))
17047    }
17048
17049    fn heap_compare(&mut self, cmp: &Arc<PerlSub>, a: &PerlValue, b: &PerlValue) -> Ordering {
17050        self.scope_push_hook();
17051        if let Some(ref env) = cmp.closure_env {
17052            self.scope.restore_capture(env);
17053        }
17054        let _ = self.scope.set_scalar("a", a.clone());
17055        let _ = self.scope.set_scalar("b", b.clone());
17056        let _ = self.scope.set_scalar("_0", a.clone());
17057        let _ = self.scope.set_scalar("_1", b.clone());
17058        let ord = match self.exec_block_no_scope(&cmp.body) {
17059            Ok(v) => {
17060                let n = v.to_int();
17061                if n < 0 {
17062                    Ordering::Less
17063                } else if n > 0 {
17064                    Ordering::Greater
17065                } else {
17066                    Ordering::Equal
17067                }
17068            }
17069            Err(_) => Ordering::Equal,
17070        };
17071        self.scope_pop_hook();
17072        ord
17073    }
17074
17075    fn heap_sift_up(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
17076        while i > 0 {
17077            let p = (i - 1) / 2;
17078            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
17079                break;
17080            }
17081            items.swap(i, p);
17082            i = p;
17083        }
17084    }
17085
17086    fn heap_sift_down(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
17087        let n = items.len();
17088        loop {
17089            let mut sm = i;
17090            let l = 2 * i + 1;
17091            let r = 2 * i + 2;
17092            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
17093                sm = l;
17094            }
17095            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
17096                sm = r;
17097            }
17098            if sm == i {
17099                break;
17100            }
17101            items.swap(i, sm);
17102            i = sm;
17103        }
17104    }
17105
17106    fn hash_for_signature_destruct(
17107        &mut self,
17108        v: &PerlValue,
17109        line: usize,
17110    ) -> PerlResult<IndexMap<String, PerlValue>> {
17111        let Some(m) = self.match_subject_as_hash(v) else {
17112            return Err(PerlError::runtime(
17113                format!(
17114                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
17115                    v.ref_type()
17116                ),
17117                line,
17118            ));
17119        };
17120        Ok(m)
17121    }
17122
17123    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
17124    pub(crate) fn apply_sub_signature(
17125        &mut self,
17126        sub: &PerlSub,
17127        argv: &[PerlValue],
17128        line: usize,
17129    ) -> PerlResult<()> {
17130        if sub.params.is_empty() {
17131            return Ok(());
17132        }
17133        let mut i = 0usize;
17134        for p in &sub.params {
17135            match p {
17136                SubSigParam::Scalar(name, ty, default) => {
17137                    let val = if i < argv.len() {
17138                        argv[i].clone()
17139                    } else if let Some(default_expr) = default {
17140                        match self.eval_expr(default_expr) {
17141                            Ok(v) => v,
17142                            Err(FlowOrError::Error(e)) => return Err(e),
17143                            Err(FlowOrError::Flow(_)) => {
17144                                return Err(PerlError::runtime(
17145                                    "unexpected control flow in parameter default",
17146                                    line,
17147                                ))
17148                            }
17149                        }
17150                    } else {
17151                        PerlValue::UNDEF
17152                    };
17153                    i += 1;
17154                    if let Some(t) = ty {
17155                        if let Err(e) = t.check_value(&val) {
17156                            return Err(PerlError::runtime(
17157                                format!("sub parameter ${}: {}", name, e),
17158                                line,
17159                            ));
17160                        }
17161                    }
17162                    let n = self.english_scalar_name(name);
17163                    self.scope.declare_scalar(n, val);
17164                }
17165                SubSigParam::Array(name, default) => {
17166                    let rest: Vec<PerlValue> = if i < argv.len() {
17167                        let r = argv[i..].to_vec();
17168                        i = argv.len();
17169                        r
17170                    } else if let Some(default_expr) = default {
17171                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17172                            Ok(v) => v,
17173                            Err(FlowOrError::Error(e)) => return Err(e),
17174                            Err(FlowOrError::Flow(_)) => {
17175                                return Err(PerlError::runtime(
17176                                    "unexpected control flow in parameter default",
17177                                    line,
17178                                ))
17179                            }
17180                        };
17181                        val.to_list()
17182                    } else {
17183                        vec![]
17184                    };
17185                    let aname = self.stash_array_name_for_package(name);
17186                    self.scope.declare_array(&aname, rest);
17187                }
17188                SubSigParam::Hash(name, default) => {
17189                    let rest: Vec<PerlValue> = if i < argv.len() {
17190                        let r = argv[i..].to_vec();
17191                        i = argv.len();
17192                        r
17193                    } else if let Some(default_expr) = default {
17194                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17195                            Ok(v) => v,
17196                            Err(FlowOrError::Error(e)) => return Err(e),
17197                            Err(FlowOrError::Flow(_)) => {
17198                                return Err(PerlError::runtime(
17199                                    "unexpected control flow in parameter default",
17200                                    line,
17201                                ))
17202                            }
17203                        };
17204                        val.to_list()
17205                    } else {
17206                        vec![]
17207                    };
17208                    let mut map = IndexMap::new();
17209                    let mut j = 0;
17210                    while j + 1 < rest.len() {
17211                        map.insert(rest[j].to_string(), rest[j + 1].clone());
17212                        j += 2;
17213                    }
17214                    self.scope.declare_hash(name, map);
17215                }
17216                SubSigParam::ArrayDestruct(elems) => {
17217                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17218                    i += 1;
17219                    let Some(arr) = self.match_subject_as_array(&arg) else {
17220                        return Err(PerlError::runtime(
17221                            format!(
17222                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
17223                                arg.ref_type()
17224                            ),
17225                            line,
17226                        ));
17227                    };
17228                    let binds = self
17229                        .match_array_pattern_elems(&arr, elems, line)
17230                        .map_err(|e| match e {
17231                            FlowOrError::Error(stryke) => stryke,
17232                            FlowOrError::Flow(_) => PerlError::runtime(
17233                                "unexpected flow in sub signature array destruct",
17234                                line,
17235                            ),
17236                        })?;
17237                    let Some(binds) = binds else {
17238                        return Err(PerlError::runtime(
17239                            "sub signature array destruct: length or element mismatch",
17240                            line,
17241                        ));
17242                    };
17243                    for b in binds {
17244                        match b {
17245                            PatternBinding::Scalar(name, v) => {
17246                                let n = self.english_scalar_name(&name);
17247                                self.scope.declare_scalar(n, v);
17248                            }
17249                            PatternBinding::Array(name, elems) => {
17250                                self.scope.declare_array(&name, elems);
17251                            }
17252                        }
17253                    }
17254                }
17255                SubSigParam::HashDestruct(pairs) => {
17256                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17257                    i += 1;
17258                    let map = self.hash_for_signature_destruct(&arg, line)?;
17259                    for (key, varname) in pairs {
17260                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
17261                        let n = self.english_scalar_name(varname);
17262                        self.scope.declare_scalar(n, v);
17263                    }
17264                }
17265            }
17266        }
17267        Ok(())
17268    }
17269
17270    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
17271    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
17272    /// These are `PerlSub`s with empty bodies and magic keys in `closure_env`.
17273    pub(crate) fn try_hof_dispatch(
17274        &mut self,
17275        sub: &PerlSub,
17276        args: &[PerlValue],
17277        want: WantarrayCtx,
17278        line: usize,
17279    ) -> Option<ExecResult> {
17280        let env = sub.closure_env.as_ref()?;
17281        fn env_get<'a>(env: &'a [(String, PerlValue)], key: &str) -> Option<&'a PerlValue> {
17282            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
17283        }
17284
17285        match sub.name.as_str() {
17286            // ── compose: right-to-left function application ──
17287            "__comp__" => {
17288                let fns = env_get(env, "__comp_fns__")?.to_list();
17289                let mut val = args.first().cloned().unwrap_or(PerlValue::UNDEF);
17290                for f in fns.iter().rev() {
17291                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
17292                        Ok(v) => val = v,
17293                        Err(e) => return Some(Err(e)),
17294                    }
17295                }
17296                Some(Ok(val))
17297            }
17298            // ── constantly: always return the captured value ──
17299            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
17300            // ── juxt: call each fn with same args, collect results ──
17301            "__juxt__" => {
17302                let fns = env_get(env, "__juxt_fns__")?.to_list();
17303                let mut results = Vec::with_capacity(fns.len());
17304                for f in &fns {
17305                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
17306                        Ok(v) => results.push(v),
17307                        Err(e) => return Some(Err(e)),
17308                    }
17309                }
17310                Some(Ok(PerlValue::array(results)))
17311            }
17312            // ── partial: prepend bound args ──
17313            "__partial__" => {
17314                let fn_val = env_get(env, "__partial_fn__")?.clone();
17315                let bound = env_get(env, "__partial_args__")?.to_list();
17316                let mut all_args = bound;
17317                all_args.extend_from_slice(args);
17318                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
17319            }
17320            // ── complement: negate the result ──
17321            "__complement__" => {
17322                let fn_val = env_get(env, "__complement_fn__")?.clone();
17323                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17324                    Ok(v) => Some(Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }))),
17325                    Err(e) => Some(Err(e)),
17326                }
17327            }
17328            // ── fnil: replace undef args with defaults ──
17329            "__fnil__" => {
17330                let fn_val = env_get(env, "__fnil_fn__")?.clone();
17331                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
17332                let mut patched = args.to_vec();
17333                for (i, d) in defaults.iter().enumerate() {
17334                    if i < patched.len() {
17335                        if patched[i].is_undef() {
17336                            patched[i] = d.clone();
17337                        }
17338                    } else {
17339                        patched.push(d.clone());
17340                    }
17341                }
17342                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
17343            }
17344            // ── memoize: cache by stringified args ──
17345            "__memoize__" => {
17346                let fn_val = env_get(env, "__memoize_fn__")?.clone();
17347                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
17348                let key = args
17349                    .iter()
17350                    .map(|a| a.to_string())
17351                    .collect::<Vec<_>>()
17352                    .join("\x00");
17353                if let Some(href) = cache_ref.as_hash_ref() {
17354                    if let Some(cached) = href.read().get(&key) {
17355                        return Some(Ok(cached.clone()));
17356                    }
17357                }
17358                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17359                    Ok(v) => {
17360                        if let Some(href) = cache_ref.as_hash_ref() {
17361                            href.write().insert(key, v.clone());
17362                        }
17363                        Some(Ok(v))
17364                    }
17365                    Err(e) => Some(Err(e)),
17366                }
17367            }
17368            // ── curry: accumulate args until arity reached ──
17369            "__curry__" => {
17370                let fn_val = env_get(env, "__curry_fn__")?.clone();
17371                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
17372                let bound = env_get(env, "__curry_bound__")?.to_list();
17373                let mut all = bound;
17374                all.extend_from_slice(args);
17375                if all.len() >= arity {
17376                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
17377                } else {
17378                    let curry_sub = PerlSub {
17379                        name: "__curry__".to_string(),
17380                        params: vec![],
17381                        body: vec![],
17382                        closure_env: Some(vec![
17383                            ("__curry_fn__".to_string(), fn_val),
17384                            (
17385                                "__curry_arity__".to_string(),
17386                                PerlValue::integer(arity as i64),
17387                            ),
17388                            ("__curry_bound__".to_string(), PerlValue::array(all)),
17389                        ]),
17390                        prototype: None,
17391                        fib_like: None,
17392                    };
17393                    Some(Ok(PerlValue::code_ref(Arc::new(curry_sub))))
17394                }
17395            }
17396            // ── once: call once, cache forever ──
17397            "__once__" => {
17398                let cache_ref = env_get(env, "__once_cache__")?.clone();
17399                if let Some(href) = cache_ref.as_hash_ref() {
17400                    let r = href.read();
17401                    if r.contains_key("done") {
17402                        return Some(Ok(r.get("val").cloned().unwrap_or(PerlValue::UNDEF)));
17403                    }
17404                }
17405                let fn_val = env_get(env, "__once_fn__")?.clone();
17406                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17407                    Ok(v) => {
17408                        if let Some(href) = cache_ref.as_hash_ref() {
17409                            let mut w = href.write();
17410                            w.insert("done".to_string(), PerlValue::integer(1));
17411                            w.insert("val".to_string(), v.clone());
17412                        }
17413                        Some(Ok(v))
17414                    }
17415                    Err(e) => Some(Err(e)),
17416                }
17417            }
17418            _ => None,
17419        }
17420    }
17421
17422    pub(crate) fn call_sub(
17423        &mut self,
17424        sub: &PerlSub,
17425        args: Vec<PerlValue>,
17426        want: WantarrayCtx,
17427        _line: usize,
17428    ) -> ExecResult {
17429        // Push current sub for __SUB__ access
17430        self.current_sub_stack.push(Arc::new(sub.clone()));
17431
17432        // Single frame for both @_ and the block's local variables —
17433        // avoids the double push_frame/pop_frame overhead per call.
17434        self.scope_push_hook();
17435        self.scope.declare_array("_", args.clone());
17436        if let Some(ref env) = sub.closure_env {
17437            self.scope.restore_capture(env);
17438        }
17439        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
17440        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
17441        // Must be AFTER restore_capture so we don't get shadowed by captured $_
17442        self.scope.set_closure_args(&args);
17443        // Move `@_` out so `native_dispatch` / `fib_like` take `&[PerlValue]` without `get_array` cloning.
17444        let argv = self.scope.take_sub_underscore().unwrap_or_default();
17445        self.apply_sub_signature(sub, &argv, _line)?;
17446        let saved = self.wantarray_kind;
17447        self.wantarray_kind = want;
17448        if let Some(r) = crate::list_util::native_dispatch(self, sub, &argv, want) {
17449            self.wantarray_kind = saved;
17450            self.scope_pop_hook();
17451            self.current_sub_stack.pop();
17452            return match r {
17453                Ok(v) => Ok(v),
17454                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17455                Err(e) => Err(e),
17456            };
17457        }
17458        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
17459            self.wantarray_kind = saved;
17460            self.scope_pop_hook();
17461            self.current_sub_stack.pop();
17462            return match r {
17463                Ok(v) => Ok(v),
17464                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17465                Err(e) => Err(e),
17466            };
17467        }
17468        if let Some(pat) = sub.fib_like.as_ref() {
17469            if argv.len() == 1 {
17470                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
17471                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
17472                    if let Some(p) = &mut self.profiler {
17473                        p.enter_sub(&sub.name);
17474                    }
17475                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
17476                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
17477                        p.exit_sub(t0.elapsed());
17478                    }
17479                    self.wantarray_kind = saved;
17480                    self.scope_pop_hook();
17481                    self.current_sub_stack.pop();
17482                    return Ok(PerlValue::integer(n));
17483                }
17484            }
17485        }
17486        self.scope.declare_array("_", argv.clone());
17487        // Note: set_closure_args was already called at line 15077; don't call it again
17488        // as that would incorrectly shift the outer topic stack a second time.
17489        let t0 = self.profiler.is_some().then(std::time::Instant::now);
17490        if let Some(p) = &mut self.profiler {
17491            p.enter_sub(&sub.name);
17492        }
17493        // Always evaluate the function body's last expression in List context so
17494        // `@array` returns the array contents, not the count. The caller adapts the
17495        // return value to their own wantarray context after receiving it.
17496        let result = self.exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List);
17497        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
17498            p.exit_sub(t0.elapsed());
17499        }
17500        // For goto &sub, capture @_ before popping the frame
17501        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
17502            Some(self.scope.get_array("_"))
17503        } else {
17504            None
17505        };
17506        self.wantarray_kind = saved;
17507        self.scope_pop_hook();
17508        self.current_sub_stack.pop();
17509        match result {
17510            Ok(v) => Ok(v),
17511            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17512            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
17513                // goto &sub — tail call: look up target and call with same @_
17514                let goto_args = goto_args.unwrap_or_default();
17515                let fqn = if target_name.contains("::") {
17516                    target_name.clone()
17517                } else {
17518                    format!("{}::{}", self.current_package(), target_name)
17519                };
17520                if let Some(target_sub) = self
17521                    .subs
17522                    .get(&fqn)
17523                    .cloned()
17524                    .or_else(|| self.subs.get(&target_name).cloned())
17525                {
17526                    self.call_sub(&target_sub, goto_args, want, _line)
17527                } else {
17528                    Err(
17529                        PerlError::runtime(format!("Undefined subroutine &{}", target_name), _line)
17530                            .into(),
17531                    )
17532                }
17533            }
17534            Err(FlowOrError::Flow(Flow::Yield(_))) => {
17535                Err(PerlError::runtime("yield is only valid inside gen { }", 0).into())
17536            }
17537            Err(e) => Err(e),
17538        }
17539    }
17540
17541    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
17542    fn call_struct_method(
17543        &mut self,
17544        body: &Block,
17545        params: &[SubSigParam],
17546        args: Vec<PerlValue>,
17547        line: usize,
17548    ) -> ExecResult {
17549        self.scope_push_hook();
17550        self.scope.declare_array("_", args.clone());
17551        // Bind $self to first arg (the receiver)
17552        if let Some(self_val) = args.first() {
17553            self.scope.declare_scalar("self", self_val.clone());
17554        }
17555        // Set $_0, $_1, etc. for all args
17556        self.scope.set_closure_args(&args);
17557        // Apply signature if provided - skip the first arg ($self) for user params
17558        let user_args: Vec<PerlValue> = args.iter().skip(1).cloned().collect();
17559        self.apply_params_to_argv(params, &user_args, line)?;
17560        let result = self.exec_block_no_scope(body);
17561        self.scope_pop_hook();
17562        match result {
17563            Ok(v) => Ok(v),
17564            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17565            Err(e) => Err(e),
17566        }
17567    }
17568
17569    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
17570    pub(crate) fn call_class_method(
17571        &mut self,
17572        body: &Block,
17573        params: &[SubSigParam],
17574        args: Vec<PerlValue>,
17575        line: usize,
17576    ) -> ExecResult {
17577        self.call_class_method_inner(body, params, args, line, false)
17578    }
17579
17580    /// Call a static class method: `Math::add(...)`.
17581    pub(crate) fn call_static_class_method(
17582        &mut self,
17583        body: &Block,
17584        params: &[SubSigParam],
17585        args: Vec<PerlValue>,
17586        line: usize,
17587    ) -> ExecResult {
17588        self.call_class_method_inner(body, params, args, line, true)
17589    }
17590
17591    fn call_class_method_inner(
17592        &mut self,
17593        body: &Block,
17594        params: &[SubSigParam],
17595        args: Vec<PerlValue>,
17596        line: usize,
17597        is_static: bool,
17598    ) -> ExecResult {
17599        self.scope_push_hook();
17600        self.scope.declare_array("_", args.clone());
17601        if !is_static {
17602            // Bind $self to first arg (the receiver) for instance methods
17603            if let Some(self_val) = args.first() {
17604                self.scope.declare_scalar("self", self_val.clone());
17605            }
17606        }
17607        // Set $_0, $_1, etc. for all args
17608        self.scope.set_closure_args(&args);
17609        // Apply signature: skip first arg ($self) only for instance methods
17610        let user_args: Vec<PerlValue> = if is_static {
17611            args.clone()
17612        } else {
17613            args.iter().skip(1).cloned().collect()
17614        };
17615        self.apply_params_to_argv(params, &user_args, line)?;
17616        let result = self.exec_block_no_scope(body);
17617        self.scope_pop_hook();
17618        match result {
17619            Ok(v) => Ok(v),
17620            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17621            Err(e) => Err(e),
17622        }
17623    }
17624
17625    /// Apply SubSigParam bindings without the full PerlSub machinery.
17626    fn apply_params_to_argv(
17627        &mut self,
17628        params: &[SubSigParam],
17629        argv: &[PerlValue],
17630        line: usize,
17631    ) -> PerlResult<()> {
17632        let mut i = 0;
17633        for param in params {
17634            match param {
17635                SubSigParam::Scalar(name, ty_opt, default) => {
17636                    let v = if i < argv.len() {
17637                        argv[i].clone()
17638                    } else if let Some(default_expr) = default {
17639                        match self.eval_expr(default_expr) {
17640                            Ok(v) => v,
17641                            Err(FlowOrError::Error(e)) => return Err(e),
17642                            Err(FlowOrError::Flow(_)) => {
17643                                return Err(PerlError::runtime(
17644                                    "unexpected control flow in parameter default",
17645                                    line,
17646                                ))
17647                            }
17648                        }
17649                    } else {
17650                        PerlValue::UNDEF
17651                    };
17652                    i += 1;
17653                    if let Some(ty) = ty_opt {
17654                        ty.check_value(&v).map_err(|msg| {
17655                            PerlError::type_error(
17656                                format!("method parameter ${}: {}", name, msg),
17657                                line,
17658                            )
17659                        })?;
17660                    }
17661                    let n = self.english_scalar_name(name);
17662                    self.scope.declare_scalar(n, v);
17663                }
17664                SubSigParam::Array(name, default) => {
17665                    let rest: Vec<PerlValue> = if i < argv.len() {
17666                        let r = argv[i..].to_vec();
17667                        i = argv.len();
17668                        r
17669                    } else if let Some(default_expr) = default {
17670                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17671                            Ok(v) => v,
17672                            Err(FlowOrError::Error(e)) => return Err(e),
17673                            Err(FlowOrError::Flow(_)) => {
17674                                return Err(PerlError::runtime(
17675                                    "unexpected control flow in parameter default",
17676                                    line,
17677                                ))
17678                            }
17679                        };
17680                        val.to_list()
17681                    } else {
17682                        vec![]
17683                    };
17684                    let aname = self.stash_array_name_for_package(name);
17685                    self.scope.declare_array(&aname, rest);
17686                }
17687                SubSigParam::Hash(name, default) => {
17688                    let rest: Vec<PerlValue> = if i < argv.len() {
17689                        let r = argv[i..].to_vec();
17690                        i = argv.len();
17691                        r
17692                    } else if let Some(default_expr) = default {
17693                        let val = match self.eval_expr_ctx(default_expr, WantarrayCtx::List) {
17694                            Ok(v) => v,
17695                            Err(FlowOrError::Error(e)) => return Err(e),
17696                            Err(FlowOrError::Flow(_)) => {
17697                                return Err(PerlError::runtime(
17698                                    "unexpected control flow in parameter default",
17699                                    line,
17700                                ))
17701                            }
17702                        };
17703                        val.to_list()
17704                    } else {
17705                        vec![]
17706                    };
17707                    let mut map = IndexMap::new();
17708                    let mut j = 0;
17709                    while j + 1 < rest.len() {
17710                        map.insert(rest[j].to_string(), rest[j + 1].clone());
17711                        j += 2;
17712                    }
17713                    self.scope.declare_hash(name, map);
17714                }
17715                SubSigParam::ArrayDestruct(elems) => {
17716                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17717                    i += 1;
17718                    let Some(arr) = self.match_subject_as_array(&arg) else {
17719                        return Err(PerlError::runtime(
17720                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
17721                            line,
17722                        ));
17723                    };
17724                    let binds = self
17725                        .match_array_pattern_elems(&arr, elems, line)
17726                        .map_err(|e| match e {
17727                            FlowOrError::Error(stryke) => stryke,
17728                            FlowOrError::Flow(_) => {
17729                                PerlError::runtime("unexpected flow in method array destruct", line)
17730                            }
17731                        })?;
17732                    let Some(binds) = binds else {
17733                        return Err(PerlError::runtime(
17734                            format!(
17735                                "method parameter: array destructure failed at position {}",
17736                                i
17737                            ),
17738                            line,
17739                        ));
17740                    };
17741                    for b in binds {
17742                        match b {
17743                            PatternBinding::Scalar(name, v) => {
17744                                let n = self.english_scalar_name(&name);
17745                                self.scope.declare_scalar(n, v);
17746                            }
17747                            PatternBinding::Array(name, elems) => {
17748                                self.scope.declare_array(&name, elems);
17749                            }
17750                        }
17751                    }
17752                }
17753                SubSigParam::HashDestruct(pairs) => {
17754                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17755                    i += 1;
17756                    let map = self.hash_for_signature_destruct(&arg, line)?;
17757                    for (key, varname) in pairs {
17758                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
17759                        let n = self.english_scalar_name(varname);
17760                        self.scope.declare_scalar(n, v);
17761                    }
17762                }
17763            }
17764        }
17765        Ok(())
17766    }
17767
17768    fn builtin_new(&mut self, class: &str, args: Vec<PerlValue>, line: usize) -> ExecResult {
17769        if class == "Set" {
17770            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
17771        }
17772        if let Some(def) = self.struct_defs.get(class).cloned() {
17773            let mut provided = Vec::new();
17774            let mut i = 1;
17775            while i + 1 < args.len() {
17776                let k = args[i].to_string();
17777                let v = args[i + 1].clone();
17778                provided.push((k, v));
17779                i += 2;
17780            }
17781            let mut defaults = Vec::with_capacity(def.fields.len());
17782            for field in &def.fields {
17783                if let Some(ref expr) = field.default {
17784                    let val = self.eval_expr(expr)?;
17785                    defaults.push(Some(val));
17786                } else {
17787                    defaults.push(None);
17788                }
17789            }
17790            return Ok(crate::native_data::struct_new_with_defaults(
17791                &def, &provided, &defaults, line,
17792            )?);
17793        }
17794        // Default OO constructor: Class->new(%args) → bless {%args}, class
17795        let mut map = IndexMap::new();
17796        let mut i = 1; // skip $self (first arg is class name)
17797        while i + 1 < args.len() {
17798            let k = args[i].to_string();
17799            let v = args[i + 1].clone();
17800            map.insert(k, v);
17801            i += 2;
17802        }
17803        Ok(PerlValue::blessed(Arc::new(
17804            crate::value::BlessedRef::new_blessed(class.to_string(), PerlValue::hash(map)),
17805        )))
17806    }
17807
17808    fn exec_print(
17809        &mut self,
17810        handle: Option<&str>,
17811        args: &[Expr],
17812        newline: bool,
17813        line: usize,
17814    ) -> ExecResult {
17815        if newline && (self.feature_bits & FEAT_SAY) == 0 {
17816            return Err(PerlError::runtime(
17817                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
17818                line,
17819            )
17820            .into());
17821        }
17822        let mut output = String::new();
17823        if args.is_empty() {
17824            // Perl: print with no LIST prints $_ (same for say).
17825            let topic = self.scope.get_scalar("_").clone();
17826            let s = self.stringify_value(topic, line)?;
17827            output.push_str(&s);
17828        } else {
17829            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
17830            // between those top-level expressions only (not between elements of an expanded `@arr`).
17831            for (i, a) in args.iter().enumerate() {
17832                if i > 0 {
17833                    output.push_str(&self.ofs);
17834                }
17835                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17836                for item in val.to_list() {
17837                    let s = self.stringify_value(item, line)?;
17838                    output.push_str(&s);
17839                }
17840            }
17841        }
17842        if newline {
17843            output.push('\n');
17844        }
17845        output.push_str(&self.ors);
17846
17847        let handle_name =
17848            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17849        self.write_formatted_print(handle_name.as_str(), &output, line)?;
17850        Ok(PerlValue::integer(1))
17851    }
17852
17853    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
17854        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
17855            // Perl: printf with no args uses $_ as the format string.
17856            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
17857            (s, &[])
17858        } else {
17859            (self.eval_expr(&args[0])?.to_string(), &args[1..])
17860        };
17861        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
17862        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
17863        // ranges to flip-flop values, so go through list-context eval and splat.
17864        let mut arg_vals = Vec::new();
17865        for a in rest {
17866            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17867            if let Some(items) = v.as_array_vec() {
17868                arg_vals.extend(items);
17869            } else {
17870                arg_vals.push(v);
17871            }
17872        }
17873        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
17874        let handle_name =
17875            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17876        match handle_name.as_str() {
17877            "STDOUT" => {
17878                if !self.suppress_stdout {
17879                    print!("{}", output);
17880                    if self.output_autoflush {
17881                        let _ = io::stdout().flush();
17882                    }
17883                }
17884            }
17885            "STDERR" => {
17886                eprint!("{}", output);
17887                let _ = io::stderr().flush();
17888            }
17889            name => {
17890                if let Some(writer) = self.output_handles.get_mut(name) {
17891                    let _ = writer.write_all(output.as_bytes());
17892                    if self.output_autoflush {
17893                        let _ = writer.flush();
17894                    }
17895                }
17896            }
17897        }
17898        Ok(PerlValue::integer(1))
17899    }
17900
17901    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
17902    pub(crate) fn eval_substr_expr(
17903        &mut self,
17904        string: &Expr,
17905        offset: &Expr,
17906        length: Option<&Expr>,
17907        replacement: Option<&Expr>,
17908        _line: usize,
17909    ) -> Result<PerlValue, FlowOrError> {
17910        let s = self.eval_expr(string)?.to_string();
17911        let off = self.eval_expr(offset)?.to_int();
17912        let start = if off < 0 {
17913            (s.len() as i64 + off).max(0) as usize
17914        } else {
17915            off as usize
17916        };
17917        let len = if let Some(l) = length {
17918            let len_val = self.eval_expr(l)?.to_int();
17919            if len_val < 0 {
17920                // Negative length: count from end of string
17921                let remaining = s.len().saturating_sub(start) as i64;
17922                (remaining + len_val).max(0) as usize
17923            } else {
17924                len_val as usize
17925            }
17926        } else {
17927            s.len().saturating_sub(start)
17928        };
17929        let end = start.saturating_add(len).min(s.len());
17930        let result = s.get(start..end).unwrap_or("").to_string();
17931        if let Some(rep) = replacement {
17932            let rep_s = self.eval_expr(rep)?.to_string();
17933            let mut new_s = String::new();
17934            new_s.push_str(&s[..start]);
17935            new_s.push_str(&rep_s);
17936            new_s.push_str(&s[end..]);
17937            self.assign_value(string, PerlValue::string(new_s))?;
17938        }
17939        Ok(PerlValue::string(result))
17940    }
17941
17942    pub(crate) fn eval_push_expr(
17943        &mut self,
17944        array: &Expr,
17945        values: &[Expr],
17946        line: usize,
17947    ) -> Result<PerlValue, FlowOrError> {
17948        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17949            for v in values {
17950                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17951                self.push_array_deref_value(aref.clone(), val, line)?;
17952            }
17953            let len = self.array_deref_len(aref, line)?;
17954            return Ok(PerlValue::integer(len));
17955        }
17956        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17957        if self.scope.is_array_frozen(&arr_name) {
17958            return Err(PerlError::runtime(
17959                format!("Modification of a frozen value: @{}", arr_name),
17960                line,
17961            )
17962            .into());
17963        }
17964        for v in values {
17965            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17966            if let Some(items) = val.as_array_vec() {
17967                for item in items {
17968                    self.scope
17969                        .push_to_array(&arr_name, item)
17970                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17971                }
17972            } else {
17973                self.scope
17974                    .push_to_array(&arr_name, val)
17975                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17976            }
17977        }
17978        let len = self.scope.array_len(&arr_name);
17979        Ok(PerlValue::integer(len as i64))
17980    }
17981
17982    pub(crate) fn eval_pop_expr(
17983        &mut self,
17984        array: &Expr,
17985        line: usize,
17986    ) -> Result<PerlValue, FlowOrError> {
17987        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17988            return self.pop_array_deref(aref, line);
17989        }
17990        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17991        self.scope
17992            .pop_from_array(&arr_name)
17993            .map_err(|e| FlowOrError::Error(e.at_line(line)))
17994    }
17995
17996    pub(crate) fn eval_shift_expr(
17997        &mut self,
17998        array: &Expr,
17999        line: usize,
18000    ) -> Result<PerlValue, FlowOrError> {
18001        if let Some(aref) = self.try_eval_array_deref_container(array)? {
18002            return self.shift_array_deref(aref, line);
18003        }
18004        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
18005        self.scope
18006            .shift_from_array(&arr_name)
18007            .map_err(|e| FlowOrError::Error(e.at_line(line)))
18008    }
18009
18010    pub(crate) fn eval_unshift_expr(
18011        &mut self,
18012        array: &Expr,
18013        values: &[Expr],
18014        line: usize,
18015    ) -> Result<PerlValue, FlowOrError> {
18016        if let Some(aref) = self.try_eval_array_deref_container(array)? {
18017            let mut vals = Vec::new();
18018            for v in values {
18019                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
18020                if let Some(items) = val.as_array_vec() {
18021                    vals.extend(items);
18022                } else {
18023                    vals.push(val);
18024                }
18025            }
18026            let len = self.unshift_array_deref_multi(aref, vals, line)?;
18027            return Ok(PerlValue::integer(len));
18028        }
18029        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
18030        let mut vals = Vec::new();
18031        for v in values {
18032            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
18033            if let Some(items) = val.as_array_vec() {
18034                vals.extend(items);
18035            } else {
18036                vals.push(val);
18037            }
18038        }
18039        let arr = self
18040            .scope
18041            .get_array_mut(&arr_name)
18042            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18043        for (i, v) in vals.into_iter().enumerate() {
18044            arr.insert(i, v);
18045        }
18046        let len = arr.len();
18047        Ok(PerlValue::integer(len as i64))
18048    }
18049
18050    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
18051    pub(crate) fn push_array_deref_value(
18052        &mut self,
18053        arr_ref: PerlValue,
18054        val: PerlValue,
18055        line: usize,
18056    ) -> Result<(), FlowOrError> {
18057        if let Some(r) = arr_ref.as_array_ref() {
18058            let mut w = r.write();
18059            if let Some(items) = val.as_array_vec() {
18060                w.extend(items.iter().cloned());
18061            } else {
18062                w.push(val);
18063            }
18064            return Ok(());
18065        }
18066        if let Some(name) = arr_ref.as_array_binding_name() {
18067            if let Some(items) = val.as_array_vec() {
18068                for item in items {
18069                    self.scope
18070                        .push_to_array(&name, item)
18071                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18072                }
18073            } else {
18074                self.scope
18075                    .push_to_array(&name, val)
18076                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18077            }
18078            return Ok(());
18079        }
18080        if let Some(s) = arr_ref.as_str() {
18081            if self.strict_refs {
18082                return Err(PerlError::runtime(
18083                    format!(
18084                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18085                        s
18086                    ),
18087                    line,
18088                )
18089                .into());
18090            }
18091            let name = s.to_string();
18092            if let Some(items) = val.as_array_vec() {
18093                for item in items {
18094                    self.scope
18095                        .push_to_array(&name, item)
18096                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18097                }
18098            } else {
18099                self.scope
18100                    .push_to_array(&name, val)
18101                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18102            }
18103            return Ok(());
18104        }
18105        Err(PerlError::runtime("push argument is not an ARRAY reference", line).into())
18106    }
18107
18108    pub(crate) fn array_deref_len(
18109        &self,
18110        arr_ref: PerlValue,
18111        line: usize,
18112    ) -> Result<i64, FlowOrError> {
18113        if let Some(r) = arr_ref.as_array_ref() {
18114            return Ok(r.read().len() as i64);
18115        }
18116        if let Some(name) = arr_ref.as_array_binding_name() {
18117            return Ok(self.scope.array_len(&name) as i64);
18118        }
18119        if let Some(s) = arr_ref.as_str() {
18120            if self.strict_refs {
18121                return Err(PerlError::runtime(
18122                    format!(
18123                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18124                        s
18125                    ),
18126                    line,
18127                )
18128                .into());
18129            }
18130            return Ok(self.scope.array_len(&s) as i64);
18131        }
18132        Err(PerlError::runtime("argument is not an ARRAY reference", line).into())
18133    }
18134
18135    pub(crate) fn pop_array_deref(
18136        &mut self,
18137        arr_ref: PerlValue,
18138        line: usize,
18139    ) -> Result<PerlValue, FlowOrError> {
18140        if let Some(r) = arr_ref.as_array_ref() {
18141            let mut w = r.write();
18142            return Ok(w.pop().unwrap_or(PerlValue::UNDEF));
18143        }
18144        if let Some(name) = arr_ref.as_array_binding_name() {
18145            return self
18146                .scope
18147                .pop_from_array(&name)
18148                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18149        }
18150        if let Some(s) = arr_ref.as_str() {
18151            if self.strict_refs {
18152                return Err(PerlError::runtime(
18153                    format!(
18154                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18155                        s
18156                    ),
18157                    line,
18158                )
18159                .into());
18160            }
18161            return self
18162                .scope
18163                .pop_from_array(&s)
18164                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18165        }
18166        Err(PerlError::runtime("pop argument is not an ARRAY reference", line).into())
18167    }
18168
18169    pub(crate) fn shift_array_deref(
18170        &mut self,
18171        arr_ref: PerlValue,
18172        line: usize,
18173    ) -> Result<PerlValue, FlowOrError> {
18174        if let Some(r) = arr_ref.as_array_ref() {
18175            let mut w = r.write();
18176            return Ok(if w.is_empty() {
18177                PerlValue::UNDEF
18178            } else {
18179                w.remove(0)
18180            });
18181        }
18182        if let Some(name) = arr_ref.as_array_binding_name() {
18183            return self
18184                .scope
18185                .shift_from_array(&name)
18186                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18187        }
18188        if let Some(s) = arr_ref.as_str() {
18189            if self.strict_refs {
18190                return Err(PerlError::runtime(
18191                    format!(
18192                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18193                        s
18194                    ),
18195                    line,
18196                )
18197                .into());
18198            }
18199            return self
18200                .scope
18201                .shift_from_array(&s)
18202                .map_err(|e| FlowOrError::Error(e.at_line(line)));
18203        }
18204        Err(PerlError::runtime("shift argument is not an ARRAY reference", line).into())
18205    }
18206
18207    pub(crate) fn unshift_array_deref_multi(
18208        &mut self,
18209        arr_ref: PerlValue,
18210        vals: Vec<PerlValue>,
18211        line: usize,
18212    ) -> Result<i64, FlowOrError> {
18213        let mut flat: Vec<PerlValue> = Vec::new();
18214        for v in vals {
18215            if let Some(items) = v.as_array_vec() {
18216                flat.extend(items);
18217            } else {
18218                flat.push(v);
18219            }
18220        }
18221        if let Some(r) = arr_ref.as_array_ref() {
18222            let mut w = r.write();
18223            for (i, v) in flat.into_iter().enumerate() {
18224                w.insert(i, v);
18225            }
18226            return Ok(w.len() as i64);
18227        }
18228        if let Some(name) = arr_ref.as_array_binding_name() {
18229            let arr = self
18230                .scope
18231                .get_array_mut(&name)
18232                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18233            for (i, v) in flat.into_iter().enumerate() {
18234                arr.insert(i, v);
18235            }
18236            return Ok(arr.len() as i64);
18237        }
18238        if let Some(s) = arr_ref.as_str() {
18239            if self.strict_refs {
18240                return Err(PerlError::runtime(
18241                    format!(
18242                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18243                        s
18244                    ),
18245                    line,
18246                )
18247                .into());
18248            }
18249            let name = s.to_string();
18250            let arr = self
18251                .scope
18252                .get_array_mut(&name)
18253                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18254            for (i, v) in flat.into_iter().enumerate() {
18255                arr.insert(i, v);
18256            }
18257            return Ok(arr.len() as i64);
18258        }
18259        Err(PerlError::runtime("unshift argument is not an ARRAY reference", line).into())
18260    }
18261
18262    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
18263    /// / compiler wraps `splice` like other context-sensitive builtins).
18264    pub(crate) fn splice_array_deref(
18265        &mut self,
18266        aref: PerlValue,
18267        offset_val: PerlValue,
18268        length_val: PerlValue,
18269        rep_vals: Vec<PerlValue>,
18270        line: usize,
18271    ) -> Result<PerlValue, FlowOrError> {
18272        let ctx = self.wantarray_kind;
18273        if let Some(r) = aref.as_array_ref() {
18274            let arr_len = r.read().len();
18275            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18276            let mut w = r.write();
18277            let removed: Vec<PerlValue> = w.drain(off..end).collect();
18278            for (i, v) in rep_vals.into_iter().enumerate() {
18279                w.insert(off + i, v);
18280            }
18281            return Ok(match ctx {
18282                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18283                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18284            });
18285        }
18286        if let Some(name) = aref.as_array_binding_name() {
18287            let arr_len = self.scope.array_len(&name);
18288            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18289            let arr = self
18290                .scope
18291                .get_array_mut(&name)
18292                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18293            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
18294            for (i, v) in rep_vals.into_iter().enumerate() {
18295                arr.insert(off + i, v);
18296            }
18297            return Ok(match ctx {
18298                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18299                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18300            });
18301        }
18302        if let Some(s) = aref.as_str() {
18303            if self.strict_refs {
18304                return Err(PerlError::runtime(
18305                    format!(
18306                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
18307                        s
18308                    ),
18309                    line,
18310                )
18311                .into());
18312            }
18313            let arr_len = self.scope.array_len(&s);
18314            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18315            let arr = self
18316                .scope
18317                .get_array_mut(&s)
18318                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18319            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
18320            for (i, v) in rep_vals.into_iter().enumerate() {
18321                arr.insert(off + i, v);
18322            }
18323            return Ok(match ctx {
18324                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18325                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18326            });
18327        }
18328        Err(PerlError::runtime("splice argument is not an ARRAY reference", line).into())
18329    }
18330
18331    pub(crate) fn eval_splice_expr(
18332        &mut self,
18333        array: &Expr,
18334        offset: Option<&Expr>,
18335        length: Option<&Expr>,
18336        replacement: &[Expr],
18337        ctx: WantarrayCtx,
18338        line: usize,
18339    ) -> Result<PerlValue, FlowOrError> {
18340        if let Some(aref) = self.try_eval_array_deref_container(array)? {
18341            let offset_val = if let Some(o) = offset {
18342                self.eval_expr(o)?
18343            } else {
18344                PerlValue::integer(0)
18345            };
18346            let length_val = if let Some(l) = length {
18347                self.eval_expr(l)?
18348            } else {
18349                PerlValue::UNDEF
18350            };
18351            let mut rep_vals = Vec::new();
18352            for r in replacement {
18353                rep_vals.push(self.eval_expr(r)?);
18354            }
18355            let saved = self.wantarray_kind;
18356            self.wantarray_kind = ctx;
18357            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
18358            self.wantarray_kind = saved;
18359            return out;
18360        }
18361        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
18362        let arr_len = self.scope.array_len(&arr_name);
18363        let offset_val = if let Some(o) = offset {
18364            self.eval_expr(o)?
18365        } else {
18366            PerlValue::integer(0)
18367        };
18368        let length_val = if let Some(l) = length {
18369            self.eval_expr(l)?
18370        } else {
18371            PerlValue::UNDEF
18372        };
18373        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
18374        let mut rep_vals = Vec::new();
18375        for r in replacement {
18376            rep_vals.push(self.eval_expr(r)?);
18377        }
18378        let arr = self
18379            .scope
18380            .get_array_mut(&arr_name)
18381            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
18382        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
18383        for (i, v) in rep_vals.into_iter().enumerate() {
18384            arr.insert(off + i, v);
18385        }
18386        Ok(match ctx {
18387            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18388            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18389        })
18390    }
18391
18392    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
18393    pub(crate) fn keys_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
18394        if let Some(h) = val.as_hash_map() {
18395            Ok(PerlValue::array(
18396                h.keys().map(|k| PerlValue::string(k.clone())).collect(),
18397            ))
18398        } else if let Some(r) = val.as_hash_ref() {
18399            Ok(PerlValue::array(
18400                r.read()
18401                    .keys()
18402                    .map(|k| PerlValue::string(k.clone()))
18403                    .collect(),
18404            ))
18405        } else {
18406            Err(PerlError::runtime("keys requires hash", line).into())
18407        }
18408    }
18409
18410    pub(crate) fn eval_keys_expr(
18411        &mut self,
18412        expr: &Expr,
18413        line: usize,
18414    ) -> Result<PerlValue, FlowOrError> {
18415        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
18416        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
18417        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
18418        Self::keys_from_value(val, line)
18419    }
18420
18421    /// Result of `values EXPR` after `EXPR` has been evaluated.
18422    pub(crate) fn values_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
18423        if let Some(h) = val.as_hash_map() {
18424            Ok(PerlValue::array(h.values().cloned().collect()))
18425        } else if let Some(r) = val.as_hash_ref() {
18426            Ok(PerlValue::array(r.read().values().cloned().collect()))
18427        } else {
18428            Err(PerlError::runtime("values requires hash", line).into())
18429        }
18430    }
18431
18432    pub(crate) fn eval_values_expr(
18433        &mut self,
18434        expr: &Expr,
18435        line: usize,
18436    ) -> Result<PerlValue, FlowOrError> {
18437        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
18438        Self::values_from_value(val, line)
18439    }
18440
18441    pub(crate) fn eval_delete_operand(
18442        &mut self,
18443        expr: &Expr,
18444        line: usize,
18445    ) -> Result<PerlValue, FlowOrError> {
18446        match &expr.kind {
18447            ExprKind::HashElement { hash, key } => {
18448                let k = self.eval_expr(key)?.to_string();
18449                self.touch_env_hash(hash);
18450                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
18451                    let class = obj
18452                        .as_blessed_ref()
18453                        .map(|b| b.class.clone())
18454                        .unwrap_or_default();
18455                    let full = format!("{}::DELETE", class);
18456                    if let Some(sub) = self.subs.get(&full).cloned() {
18457                        return self.call_sub(
18458                            &sub,
18459                            vec![obj, PerlValue::string(k)],
18460                            WantarrayCtx::Scalar,
18461                            line,
18462                        );
18463                    }
18464                }
18465                self.scope
18466                    .delete_hash_element(hash, &k)
18467                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
18468            }
18469            ExprKind::ArrayElement { array, index } => {
18470                self.check_strict_array_var(array, line)?;
18471                let idx = self.eval_expr(index)?.to_int();
18472                let aname = self.stash_array_name_for_package(array);
18473                self.scope
18474                    .delete_array_element(&aname, idx)
18475                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
18476            }
18477            ExprKind::ArrowDeref {
18478                expr: inner,
18479                index,
18480                kind: DerefKind::Hash,
18481            } => {
18482                let k = self.eval_expr(index)?.to_string();
18483                let container = self.eval_expr(inner)?;
18484                self.delete_arrow_hash_element(container, &k, line)
18485                    .map_err(Into::into)
18486            }
18487            ExprKind::ArrowDeref {
18488                expr: inner,
18489                index,
18490                kind: DerefKind::Array,
18491            } => {
18492                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
18493                    return Err(PerlError::runtime(
18494                        "delete on array element needs scalar subscript",
18495                        line,
18496                    )
18497                    .into());
18498                }
18499                let container = self.eval_expr(inner)?;
18500                let idx = self.eval_expr(index)?.to_int();
18501                self.delete_arrow_array_element(container, idx, line)
18502                    .map_err(Into::into)
18503            }
18504            _ => Err(PerlError::runtime("delete requires hash or array element", line).into()),
18505        }
18506    }
18507
18508    pub(crate) fn eval_exists_operand(
18509        &mut self,
18510        expr: &Expr,
18511        line: usize,
18512    ) -> Result<PerlValue, FlowOrError> {
18513        match &expr.kind {
18514            ExprKind::HashElement { hash, key } => {
18515                let k = self.eval_expr(key)?.to_string();
18516                self.touch_env_hash(hash);
18517                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
18518                    let class = obj
18519                        .as_blessed_ref()
18520                        .map(|b| b.class.clone())
18521                        .unwrap_or_default();
18522                    let full = format!("{}::EXISTS", class);
18523                    if let Some(sub) = self.subs.get(&full).cloned() {
18524                        return self.call_sub(
18525                            &sub,
18526                            vec![obj, PerlValue::string(k)],
18527                            WantarrayCtx::Scalar,
18528                            line,
18529                        );
18530                    }
18531                }
18532                Ok(PerlValue::integer(
18533                    if self.scope.exists_hash_element(hash, &k) {
18534                        1
18535                    } else {
18536                        0
18537                    },
18538                ))
18539            }
18540            ExprKind::ArrayElement { array, index } => {
18541                self.check_strict_array_var(array, line)?;
18542                let idx = self.eval_expr(index)?.to_int();
18543                let aname = self.stash_array_name_for_package(array);
18544                Ok(PerlValue::integer(
18545                    if self.scope.exists_array_element(&aname, idx) {
18546                        1
18547                    } else {
18548                        0
18549                    },
18550                ))
18551            }
18552            ExprKind::ArrowDeref {
18553                expr: inner,
18554                index,
18555                kind: DerefKind::Hash,
18556            } => {
18557                let k = self.eval_expr(index)?.to_string();
18558                let container = self.eval_expr(inner)?;
18559                let yes = self.exists_arrow_hash_element(container, &k, line)?;
18560                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
18561            }
18562            ExprKind::ArrowDeref {
18563                expr: inner,
18564                index,
18565                kind: DerefKind::Array,
18566            } => {
18567                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
18568                    return Err(PerlError::runtime(
18569                        "exists on array element needs scalar subscript",
18570                        line,
18571                    )
18572                    .into());
18573                }
18574                let container = self.eval_expr(inner)?;
18575                let idx = self.eval_expr(index)?.to_int();
18576                let yes = self.exists_arrow_array_element(container, idx, line)?;
18577                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
18578            }
18579            _ => Err(PerlError::runtime("exists requires hash or array element", line).into()),
18580        }
18581    }
18582
18583    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
18584    ///
18585    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
18586    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
18587    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
18588    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
18589    /// the new path amortizes the handshake across the whole map.
18590    pub(crate) fn eval_pmap_remote(
18591        &mut self,
18592        cluster_pv: PerlValue,
18593        list_pv: PerlValue,
18594        show_progress: bool,
18595        block: &Block,
18596        flat_outputs: bool,
18597        line: usize,
18598    ) -> Result<PerlValue, FlowOrError> {
18599        let Some(cluster) = cluster_pv.as_remote_cluster() else {
18600            return Err(PerlError::runtime("pmap_on: expected cluster(...) value", line).into());
18601        };
18602        let items = list_pv.to_list();
18603        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18604        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
18605            return Err(PerlError::runtime(
18606                "pmap_on: mysync/atomic capture is not supported for remote workers",
18607                line,
18608            )
18609            .into());
18610        }
18611        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
18612            .map_err(|e| PerlError::runtime(e, line))?;
18613        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
18614        let block_src = crate::fmt::format_block(block);
18615        let item_jsons =
18616            crate::cluster::perl_items_to_json(&items).map_err(|e| PerlError::runtime(e, line))?;
18617
18618        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
18619        // synchronous from the caller's POV, so we drive the bar before/after the call.
18620        let pmap_progress = PmapProgress::new(show_progress, items.len());
18621        let result_values =
18622            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
18623                .map_err(|e| PerlError::runtime(format!("pmap_on remote: {e}"), line))?;
18624        for _ in 0..result_values.len() {
18625            pmap_progress.tick();
18626        }
18627        pmap_progress.finish();
18628
18629        if flat_outputs {
18630            let flattened: Vec<PerlValue> = result_values
18631                .into_iter()
18632                .flat_map(|v| v.map_flatten_outputs(true))
18633                .collect();
18634            Ok(PerlValue::array(flattened))
18635        } else {
18636            Ok(PerlValue::array(result_values))
18637        }
18638    }
18639
18640    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
18641    pub(crate) fn eval_par_lines_expr(
18642        &mut self,
18643        path: &Expr,
18644        callback: &Expr,
18645        progress: Option<&Expr>,
18646        line: usize,
18647    ) -> Result<PerlValue, FlowOrError> {
18648        let show_progress = progress
18649            .map(|p| self.eval_expr(p))
18650            .transpose()?
18651            .map(|v| v.is_true())
18652            .unwrap_or(false);
18653        let path_s = self.eval_expr(path)?.to_string();
18654        let cb_val = self.eval_expr(callback)?;
18655        let sub = if let Some(s) = cb_val.as_code_ref() {
18656            s
18657        } else {
18658            return Err(PerlError::runtime(
18659                "par_lines: second argument must be a code reference",
18660                line,
18661            )
18662            .into());
18663        };
18664        let subs = self.subs.clone();
18665        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18666        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
18667            FlowOrError::Error(PerlError::runtime(format!("par_lines: {}", e), line))
18668        })?;
18669        let mmap = unsafe {
18670            memmap2::Mmap::map(&file).map_err(|e| {
18671                FlowOrError::Error(PerlError::runtime(format!("par_lines: mmap: {}", e), line))
18672            })?
18673        };
18674        let data: &[u8] = &mmap;
18675        if data.is_empty() {
18676            return Ok(PerlValue::UNDEF);
18677        }
18678        let line_total = crate::par_lines::line_count_bytes(data);
18679        let pmap_progress = PmapProgress::new(show_progress, line_total);
18680        if self.num_threads == 0 {
18681            self.num_threads = rayon::current_num_threads();
18682        }
18683        let num_chunks = self.num_threads.saturating_mul(8).max(1);
18684        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
18685        chunks.into_par_iter().try_for_each(|(start, end)| {
18686            let slice = &data[start..end];
18687            let mut s = 0usize;
18688            while s < slice.len() {
18689                let e = slice[s..]
18690                    .iter()
18691                    .position(|&b| b == b'\n')
18692                    .map(|p| s + p)
18693                    .unwrap_or(slice.len());
18694                let line_bytes = &slice[s..e];
18695                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
18696                let mut local_interp = Interpreter::new();
18697                local_interp.subs = subs.clone();
18698                local_interp.scope.restore_capture(&scope_capture);
18699                local_interp
18700                    .scope
18701                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18702                local_interp.enable_parallel_guard();
18703                local_interp.scope.set_topic(PerlValue::string(line_str));
18704                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
18705                    Ok(_) => {}
18706                    Err(e) => return Err(e),
18707                }
18708                pmap_progress.tick();
18709                if e >= slice.len() {
18710                    break;
18711                }
18712                s = e + 1;
18713            }
18714            Ok(())
18715        })?;
18716        pmap_progress.finish();
18717        Ok(PerlValue::UNDEF)
18718    }
18719
18720    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
18721    pub(crate) fn eval_par_walk_expr(
18722        &mut self,
18723        path: &Expr,
18724        callback: &Expr,
18725        progress: Option<&Expr>,
18726        line: usize,
18727    ) -> Result<PerlValue, FlowOrError> {
18728        let show_progress = progress
18729            .map(|p| self.eval_expr(p))
18730            .transpose()?
18731            .map(|v| v.is_true())
18732            .unwrap_or(false);
18733        let path_val = self.eval_expr(path)?;
18734        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
18735            arr.into_iter()
18736                .map(|v| PathBuf::from(v.to_string()))
18737                .collect()
18738        } else {
18739            vec![PathBuf::from(path_val.to_string())]
18740        };
18741        let cb_val = self.eval_expr(callback)?;
18742        let sub = if let Some(s) = cb_val.as_code_ref() {
18743            s
18744        } else {
18745            return Err(PerlError::runtime(
18746                "par_walk: second argument must be a code reference",
18747                line,
18748            )
18749            .into());
18750        };
18751        let subs = self.subs.clone();
18752        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18753
18754        if show_progress {
18755            let paths = crate::par_walk::collect_paths(&roots);
18756            let pmap_progress = PmapProgress::new(true, paths.len());
18757            paths.into_par_iter().try_for_each(|p| {
18758                let s = p.to_string_lossy().into_owned();
18759                let mut local_interp = Interpreter::new();
18760                local_interp.subs = subs.clone();
18761                local_interp.scope.restore_capture(&scope_capture);
18762                local_interp
18763                    .scope
18764                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18765                local_interp.enable_parallel_guard();
18766                local_interp.scope.set_topic(PerlValue::string(s));
18767                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
18768                    Ok(_) => {}
18769                    Err(e) => return Err(e),
18770                }
18771                pmap_progress.tick();
18772                Ok(())
18773            })?;
18774            pmap_progress.finish();
18775        } else {
18776            for r in &roots {
18777                par_walk_recursive(
18778                    r.as_path(),
18779                    &sub,
18780                    &subs,
18781                    &scope_capture,
18782                    &atomic_arrays,
18783                    &atomic_hashes,
18784                    line,
18785                )?;
18786            }
18787        }
18788        Ok(PerlValue::UNDEF)
18789    }
18790
18791    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
18792    pub(crate) fn builtin_par_sed(
18793        &mut self,
18794        args: &[PerlValue],
18795        line: usize,
18796        has_progress: bool,
18797    ) -> PerlResult<PerlValue> {
18798        let show_progress = if has_progress {
18799            args.last().map(|v| v.is_true()).unwrap_or(false)
18800        } else {
18801            false
18802        };
18803        let slice = if has_progress {
18804            &args[..args.len().saturating_sub(1)]
18805        } else {
18806            args
18807        };
18808        if slice.len() < 3 {
18809            return Err(PerlError::runtime(
18810                "par_sed: need pattern, replacement, and at least one file path",
18811                line,
18812            ));
18813        }
18814        let pat_val = &slice[0];
18815        let repl = slice[1].to_string();
18816        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
18817
18818        let re = if let Some(rx) = pat_val.as_regex() {
18819            rx
18820        } else {
18821            let pattern = pat_val.to_string();
18822            match self.compile_regex(&pattern, "g", line) {
18823                Ok(r) => r,
18824                Err(FlowOrError::Error(e)) => return Err(e),
18825                Err(FlowOrError::Flow(f)) => {
18826                    return Err(PerlError::runtime(format!("par_sed: {:?}", f), line))
18827                }
18828            }
18829        };
18830
18831        let pmap = PmapProgress::new(show_progress, files.len());
18832        let touched = AtomicUsize::new(0);
18833        files.par_iter().try_for_each(|path| {
18834            let content = read_file_text_perl_compat(path)
18835                .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18836            let new_s = re.replace_all(&content, &repl);
18837            if new_s != content {
18838                std::fs::write(path, new_s.as_bytes())
18839                    .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18840                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
18841            }
18842            pmap.tick();
18843            Ok(())
18844        })?;
18845        pmap.finish();
18846        Ok(PerlValue::integer(
18847            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
18848        ))
18849    }
18850
18851    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
18852    pub(crate) fn eval_pwatch_expr(
18853        &mut self,
18854        path: &Expr,
18855        callback: &Expr,
18856        line: usize,
18857    ) -> Result<PerlValue, FlowOrError> {
18858        let pattern_s = self.eval_expr(path)?.to_string();
18859        let cb_val = self.eval_expr(callback)?;
18860        let sub = if let Some(s) = cb_val.as_code_ref() {
18861            s
18862        } else {
18863            return Err(PerlError::runtime(
18864                "pwatch: second argument must be a code reference",
18865                line,
18866            )
18867            .into());
18868        };
18869        let subs = self.subs.clone();
18870        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18871        crate::pwatch::run_pwatch(
18872            &pattern_s,
18873            sub,
18874            subs,
18875            scope_capture,
18876            atomic_arrays,
18877            atomic_hashes,
18878            line,
18879        )
18880        .map_err(FlowOrError::Error)
18881    }
18882
18883    /// Interpolate `$var` in s/// replacement strings, preserving numeric backrefs ($1, $2, etc.).
18884    fn interpolate_replacement_string(&self, replacement: &str) -> String {
18885        let mut out = String::with_capacity(replacement.len());
18886        let chars: Vec<char> = replacement.chars().collect();
18887        let mut i = 0;
18888        while i < chars.len() {
18889            if chars[i] == '\\' && i + 1 < chars.len() {
18890                out.push(chars[i]);
18891                out.push(chars[i + 1]);
18892                i += 2;
18893                continue;
18894            }
18895            if chars[i] == '$' && i + 1 < chars.len() {
18896                let start = i;
18897                i += 1;
18898                if chars[i].is_ascii_digit() {
18899                    out.push('$');
18900                    while i < chars.len() && chars[i].is_ascii_digit() {
18901                        out.push(chars[i]);
18902                        i += 1;
18903                    }
18904                    continue;
18905                }
18906                if chars[i] == '&' || chars[i] == '`' || chars[i] == '\'' {
18907                    out.push('$');
18908                    out.push(chars[i]);
18909                    i += 1;
18910                    continue;
18911                }
18912                if !chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{' {
18913                    out.push('$');
18914                    continue;
18915                }
18916                let mut name = String::new();
18917                if chars[i] == '{' {
18918                    i += 1;
18919                    while i < chars.len() && chars[i] != '}' {
18920                        name.push(chars[i]);
18921                        i += 1;
18922                    }
18923                    if i < chars.len() {
18924                        i += 1;
18925                    }
18926                } else {
18927                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18928                        name.push(chars[i]);
18929                        i += 1;
18930                    }
18931                }
18932                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
18933                    let val = self.scope.get_scalar(&name);
18934                    out.push_str(&val.to_string());
18935                } else if !name.is_empty() {
18936                    out.push_str(&replacement[start..i]);
18937                } else {
18938                    out.push('$');
18939                }
18940                continue;
18941            }
18942            out.push(chars[i]);
18943            i += 1;
18944        }
18945        out
18946    }
18947
18948    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
18949    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
18950        let mut out = String::with_capacity(pattern.len());
18951        let chars: Vec<char> = pattern.chars().collect();
18952        let mut i = 0;
18953        while i < chars.len() {
18954            if chars[i] == '\\' && i + 1 < chars.len() {
18955                // Preserve escape sequences (including \$ which is literal $)
18956                out.push(chars[i]);
18957                out.push(chars[i + 1]);
18958                i += 2;
18959                continue;
18960            }
18961            if chars[i] == '$' && i + 1 < chars.len() {
18962                i += 1;
18963                // `$` at end of pattern is an anchor, not a variable
18964                if i >= chars.len()
18965                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
18966                {
18967                    out.push('$');
18968                    continue;
18969                }
18970                let mut name = String::new();
18971                if chars[i] == '{' {
18972                    i += 1;
18973                    while i < chars.len() && chars[i] != '}' {
18974                        name.push(chars[i]);
18975                        i += 1;
18976                    }
18977                    if i < chars.len() {
18978                        i += 1;
18979                    } // skip }
18980                } else {
18981                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18982                        name.push(chars[i]);
18983                        i += 1;
18984                    }
18985                }
18986                if !name.is_empty() {
18987                    let val = self.scope.get_scalar(&name);
18988                    out.push_str(&val.to_string());
18989                } else {
18990                    out.push('$');
18991                }
18992                continue;
18993            }
18994            out.push(chars[i]);
18995            i += 1;
18996        }
18997        out
18998    }
18999
19000    pub(crate) fn compile_regex(
19001        &mut self,
19002        pattern: &str,
19003        flags: &str,
19004        line: usize,
19005    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
19006        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
19007        let pattern = if pattern.contains('$') || pattern.contains('@') {
19008            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
19009        } else {
19010            std::borrow::Cow::Borrowed(pattern)
19011        };
19012        let pattern = pattern.as_ref();
19013        // Fast path: same regex as last call (common in loops).
19014        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
19015        let multiline = self.multiline_match;
19016        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
19017            if lp == pattern && lf == flags && *lm == multiline {
19018                return Ok(lr.clone());
19019            }
19020        }
19021        // Slow path: HashMap lookup
19022        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
19023        if let Some(cached) = self.regex_cache.get(&key) {
19024            self.regex_last = Some((
19025                pattern.to_string(),
19026                flags.to_string(),
19027                multiline,
19028                cached.clone(),
19029            ));
19030            return Ok(cached.clone());
19031        }
19032        let expanded = expand_perl_regex_quotemeta(pattern);
19033        let expanded = expand_perl_regex_octal_escapes(&expanded);
19034        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
19035        let mut re_str = String::new();
19036        if flags.contains('i') {
19037            re_str.push_str("(?i)");
19038        }
19039        if flags.contains('s') {
19040            re_str.push_str("(?s)");
19041        }
19042        if flags.contains('m') {
19043            re_str.push_str("(?m)");
19044        }
19045        if flags.contains('x') {
19046            re_str.push_str("(?x)");
19047        }
19048        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
19049        if multiline {
19050            re_str.push_str("(?s)");
19051        }
19052        re_str.push_str(&expanded);
19053        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
19054            FlowOrError::Error(PerlError::runtime(
19055                format!("Invalid regex /{}/: {}", pattern, e),
19056                line,
19057            ))
19058        })?;
19059        let arc = re;
19060        self.regex_last = Some((
19061            pattern.to_string(),
19062            flags.to_string(),
19063            multiline,
19064            arc.clone(),
19065        ));
19066        self.regex_cache.insert(key, arc.clone());
19067        Ok(arc)
19068    }
19069
19070    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
19071    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
19072        if self.last_readline_handle.is_empty() {
19073            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
19074        }
19075        let n = *self
19076            .handle_line_numbers
19077            .get(&self.last_readline_handle)
19078            .unwrap_or(&0);
19079        if n <= 0 {
19080            return None;
19081        }
19082        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
19083        {
19084            return Some(("<>".to_string(), n));
19085        }
19086        if self.last_readline_handle == "STDIN" {
19087            return Some((self.last_stdin_die_bracket.clone(), n));
19088        }
19089        Some((format!("<{}>", self.last_readline_handle), n))
19090    }
19091
19092    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
19093    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
19094        let mut s = format!(" at {} line {}", self.file, source_line);
19095        if let Some((bracket, n)) = self.die_warn_io_annotation() {
19096            s.push_str(&format!(", {} line {}.", bracket, n));
19097        } else {
19098            s.push('.');
19099        }
19100        s
19101    }
19102
19103    /// Process a line in -n/-p mode.
19104    ///
19105    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
19106    /// file so `eof` with no arguments matches Perl behavior on that line.
19107    pub fn process_line(
19108        &mut self,
19109        line_str: &str,
19110        program: &Program,
19111        is_last_input_line: bool,
19112    ) -> PerlResult<Option<String>> {
19113        self.line_mode_eof_pending = is_last_input_line;
19114        let result: PerlResult<Option<String>> = (|| {
19115            self.line_number += 1;
19116            self.scope
19117                .set_topic(PerlValue::string(line_str.to_string()));
19118
19119            if self.auto_split {
19120                let sep = self.field_separator.as_deref().unwrap_or(" ");
19121                let re = regex::Regex::new(sep).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
19122                let fields: Vec<PerlValue> = re
19123                    .split(line_str)
19124                    .map(|s| PerlValue::string(s.to_string()))
19125                    .collect();
19126                self.scope.set_array("F", fields)?;
19127            }
19128
19129            for stmt in &program.statements {
19130                match &stmt.kind {
19131                    StmtKind::SubDecl { .. }
19132                    | StmtKind::Begin(_)
19133                    | StmtKind::UnitCheck(_)
19134                    | StmtKind::Check(_)
19135                    | StmtKind::Init(_)
19136                    | StmtKind::End(_) => continue,
19137                    _ => match self.exec_statement(stmt) {
19138                        Ok(_) => {}
19139                        Err(FlowOrError::Error(e)) => return Err(e),
19140                        Err(FlowOrError::Flow(_)) => {}
19141                    },
19142                }
19143            }
19144
19145            // `-p` implicit print matches `print $_` (appends `$\` / [`Self::ors`] — set by `-l`).
19146            let mut out = self.scope.get_scalar("_").to_string();
19147            out.push_str(&self.ors);
19148            Ok(Some(out))
19149        })();
19150        self.line_mode_eof_pending = false;
19151        result
19152    }
19153}
19154
19155fn par_walk_invoke_entry(
19156    path: &Path,
19157    sub: &Arc<PerlSub>,
19158    subs: &HashMap<String, Arc<PerlSub>>,
19159    scope_capture: &[(String, PerlValue)],
19160    atomic_arrays: &[(String, crate::scope::AtomicArray)],
19161    atomic_hashes: &[(String, crate::scope::AtomicHash)],
19162    line: usize,
19163) -> Result<(), FlowOrError> {
19164    let s = path.to_string_lossy().into_owned();
19165    let mut local_interp = Interpreter::new();
19166    local_interp.subs = subs.clone();
19167    local_interp.scope.restore_capture(scope_capture);
19168    local_interp
19169        .scope
19170        .restore_atomics(atomic_arrays, atomic_hashes);
19171    local_interp.enable_parallel_guard();
19172    local_interp.scope.set_topic(PerlValue::string(s));
19173    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
19174    Ok(())
19175}
19176
19177fn par_walk_recursive(
19178    path: &Path,
19179    sub: &Arc<PerlSub>,
19180    subs: &HashMap<String, Arc<PerlSub>>,
19181    scope_capture: &[(String, PerlValue)],
19182    atomic_arrays: &[(String, crate::scope::AtomicArray)],
19183    atomic_hashes: &[(String, crate::scope::AtomicHash)],
19184    line: usize,
19185) -> Result<(), FlowOrError> {
19186    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
19187        return par_walk_invoke_entry(
19188            path,
19189            sub,
19190            subs,
19191            scope_capture,
19192            atomic_arrays,
19193            atomic_hashes,
19194            line,
19195        );
19196    }
19197    if !path.is_dir() {
19198        return Ok(());
19199    }
19200    par_walk_invoke_entry(
19201        path,
19202        sub,
19203        subs,
19204        scope_capture,
19205        atomic_arrays,
19206        atomic_hashes,
19207        line,
19208    )?;
19209    let read = match std::fs::read_dir(path) {
19210        Ok(r) => r,
19211        Err(_) => return Ok(()),
19212    };
19213    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
19214    entries.par_iter().try_for_each(|e| {
19215        par_walk_recursive(
19216            &e.path(),
19217            sub,
19218            subs,
19219            scope_capture,
19220            atomic_arrays,
19221            atomic_hashes,
19222            line,
19223        )
19224    })?;
19225    Ok(())
19226}
19227
19228/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
19229pub(crate) fn perl_sprintf_format_with<F>(
19230    fmt: &str,
19231    args: &[PerlValue],
19232    mut string_for_s: F,
19233) -> Result<String, FlowOrError>
19234where
19235    F: FnMut(&PerlValue) -> Result<String, FlowOrError>,
19236{
19237    let mut result = String::new();
19238    let mut arg_idx = 0;
19239    let chars: Vec<char> = fmt.chars().collect();
19240    let mut i = 0;
19241
19242    while i < chars.len() {
19243        if chars[i] == '%' {
19244            i += 1;
19245            if i >= chars.len() {
19246                break;
19247            }
19248            if chars[i] == '%' {
19249                result.push('%');
19250                i += 1;
19251                continue;
19252            }
19253
19254            // Parse format specifier
19255            let mut flags = String::new();
19256            while i < chars.len() && "-+ #0".contains(chars[i]) {
19257                flags.push(chars[i]);
19258                i += 1;
19259            }
19260            let mut width = String::new();
19261            while i < chars.len() && chars[i].is_ascii_digit() {
19262                width.push(chars[i]);
19263                i += 1;
19264            }
19265            let mut precision = String::new();
19266            if i < chars.len() && chars[i] == '.' {
19267                i += 1;
19268                while i < chars.len() && chars[i].is_ascii_digit() {
19269                    precision.push(chars[i]);
19270                    i += 1;
19271                }
19272            }
19273            if i >= chars.len() {
19274                break;
19275            }
19276            let spec = chars[i];
19277            i += 1;
19278
19279            let arg = args.get(arg_idx).cloned().unwrap_or(PerlValue::UNDEF);
19280            arg_idx += 1;
19281
19282            let w: usize = width.parse().unwrap_or(0);
19283            let p: usize = precision.parse().unwrap_or(6);
19284
19285            let zero_pad = flags.contains('0') && !flags.contains('-');
19286            let left_align = flags.contains('-');
19287            let formatted = match spec {
19288                'd' | 'i' => {
19289                    if zero_pad {
19290                        format!("{:0width$}", arg.to_int(), width = w)
19291                    } else if left_align {
19292                        format!("{:<width$}", arg.to_int(), width = w)
19293                    } else {
19294                        format!("{:width$}", arg.to_int(), width = w)
19295                    }
19296                }
19297                'u' => {
19298                    if zero_pad {
19299                        format!("{:0width$}", arg.to_int() as u64, width = w)
19300                    } else {
19301                        format!("{:width$}", arg.to_int() as u64, width = w)
19302                    }
19303                }
19304                'f' => format!("{:width$.prec$}", arg.to_number(), width = w, prec = p),
19305                'e' => format!("{:width$.prec$e}", arg.to_number(), width = w, prec = p),
19306                'g' => {
19307                    let n = arg.to_number();
19308                    if n.abs() >= 1e-4 && n.abs() < 1e15 {
19309                        format!("{:width$.prec$}", n, width = w, prec = p)
19310                    } else {
19311                        format!("{:width$.prec$e}", n, width = w, prec = p)
19312                    }
19313                }
19314                's' => {
19315                    let s = string_for_s(&arg)?;
19316                    if !precision.is_empty() {
19317                        let truncated: String = s.chars().take(p).collect();
19318                        if flags.contains('-') {
19319                            format!("{:<width$}", truncated, width = w)
19320                        } else {
19321                            format!("{:>width$}", truncated, width = w)
19322                        }
19323                    } else if flags.contains('-') {
19324                        format!("{:<width$}", s, width = w)
19325                    } else {
19326                        format!("{:>width$}", s, width = w)
19327                    }
19328                }
19329                'x' => {
19330                    let v = arg.to_int();
19331                    if zero_pad && w > 0 {
19332                        format!("{:0width$x}", v, width = w)
19333                    } else if left_align {
19334                        format!("{:<width$x}", v, width = w)
19335                    } else if w > 0 {
19336                        format!("{:width$x}", v, width = w)
19337                    } else {
19338                        format!("{:x}", v)
19339                    }
19340                }
19341                'X' => {
19342                    let v = arg.to_int();
19343                    if zero_pad && w > 0 {
19344                        format!("{:0width$X}", v, width = w)
19345                    } else if left_align {
19346                        format!("{:<width$X}", v, width = w)
19347                    } else if w > 0 {
19348                        format!("{:width$X}", v, width = w)
19349                    } else {
19350                        format!("{:X}", v)
19351                    }
19352                }
19353                'o' => {
19354                    let v = arg.to_int();
19355                    if zero_pad && w > 0 {
19356                        format!("{:0width$o}", v, width = w)
19357                    } else if left_align {
19358                        format!("{:<width$o}", v, width = w)
19359                    } else if w > 0 {
19360                        format!("{:width$o}", v, width = w)
19361                    } else {
19362                        format!("{:o}", v)
19363                    }
19364                }
19365                'b' => {
19366                    let v = arg.to_int();
19367                    if zero_pad && w > 0 {
19368                        format!("{:0width$b}", v, width = w)
19369                    } else if left_align {
19370                        format!("{:<width$b}", v, width = w)
19371                    } else if w > 0 {
19372                        format!("{:width$b}", v, width = w)
19373                    } else {
19374                        format!("{:b}", v)
19375                    }
19376                }
19377                'c' => char::from_u32(arg.to_int() as u32)
19378                    .map(|c| c.to_string())
19379                    .unwrap_or_default(),
19380                _ => arg.to_string(),
19381            };
19382
19383            result.push_str(&formatted);
19384        } else {
19385            result.push(chars[i]);
19386            i += 1;
19387        }
19388    }
19389    Ok(result)
19390}
19391
19392#[cfg(test)]
19393mod regex_expand_tests {
19394    use super::Interpreter;
19395
19396    #[test]
19397    fn compile_regex_quotemeta_qe_matches_literal() {
19398        let mut i = Interpreter::new();
19399        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
19400        assert!(re.is_match("a.c"));
19401        assert!(!re.is_match("abc"));
19402    }
19403
19404    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
19405    /// stay literal (not rewritten to `(?:\n?\z)`).
19406    #[test]
19407    fn compile_regex_char_class_leading_close_bracket_is_literal() {
19408        let mut i = Interpreter::new();
19409        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
19410        assert!(re.is_match("$"));
19411        assert!(re.is_match("]"));
19412        assert!(!re.is_match("x"));
19413    }
19414}
19415
19416#[cfg(test)]
19417mod special_scalar_name_tests {
19418    use super::Interpreter;
19419
19420    #[test]
19421    fn special_scalar_name_for_get_matches_magic_globals() {
19422        assert!(Interpreter::is_special_scalar_name_for_get("0"));
19423        assert!(Interpreter::is_special_scalar_name_for_get("!"));
19424        assert!(Interpreter::is_special_scalar_name_for_get("^W"));
19425        assert!(Interpreter::is_special_scalar_name_for_get("^O"));
19426        assert!(Interpreter::is_special_scalar_name_for_get("^MATCH"));
19427        assert!(Interpreter::is_special_scalar_name_for_get("<"));
19428        assert!(Interpreter::is_special_scalar_name_for_get("?"));
19429        assert!(Interpreter::is_special_scalar_name_for_get("|"));
19430        assert!(Interpreter::is_special_scalar_name_for_get("^UNICODE"));
19431        assert!(Interpreter::is_special_scalar_name_for_get("\""));
19432        assert!(!Interpreter::is_special_scalar_name_for_get("foo"));
19433        assert!(!Interpreter::is_special_scalar_name_for_get("plainvar"));
19434    }
19435
19436    #[test]
19437    fn special_scalar_name_for_set_matches_set_special_var_arms() {
19438        assert!(Interpreter::is_special_scalar_name_for_set("0"));
19439        assert!(Interpreter::is_special_scalar_name_for_set("^D"));
19440        assert!(Interpreter::is_special_scalar_name_for_set("^H"));
19441        assert!(Interpreter::is_special_scalar_name_for_set("^WARNING_BITS"));
19442        assert!(Interpreter::is_special_scalar_name_for_set("ARGV"));
19443        assert!(Interpreter::is_special_scalar_name_for_set("|"));
19444        assert!(Interpreter::is_special_scalar_name_for_set("?"));
19445        assert!(Interpreter::is_special_scalar_name_for_set("^UNICODE"));
19446        assert!(Interpreter::is_special_scalar_name_for_set("."));
19447        assert!(!Interpreter::is_special_scalar_name_for_set("foo"));
19448        assert!(!Interpreter::is_special_scalar_name_for_set("__PACKAGE__"));
19449    }
19450
19451    #[test]
19452    fn caret_and_id_specials_roundtrip_get() {
19453        let i = Interpreter::new();
19454        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
19455        assert_eq!(
19456            i.get_special_var("^V").to_string(),
19457            format!("v{}", env!("CARGO_PKG_VERSION"))
19458        );
19459        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
19460        assert!(i.get_special_var("^T").to_int() >= 0);
19461        #[cfg(unix)]
19462        {
19463            assert!(i.get_special_var("<").to_int() >= 0);
19464        }
19465    }
19466
19467    #[test]
19468    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
19469        let mut i = Interpreter::new();
19470        i.last_readline_handle.clear();
19471        i.line_number = 3;
19472        i.prepare_flip_flop_vm_slots(1);
19473        assert_eq!(
19474            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
19475            1
19476        );
19477        assert!(i.flip_flop_active[0]);
19478        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
19479        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
19480        assert_eq!(
19481            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
19482            1
19483        );
19484        assert!(i.flip_flop_active[0]);
19485    }
19486
19487    #[test]
19488    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
19489        let mut i = Interpreter::new();
19490        i.last_readline_handle.clear();
19491        i.line_number = 2;
19492        i.prepare_flip_flop_vm_slots(1);
19493        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
19494        assert!(i.flip_flop_active[0]);
19495        i.line_number = 3;
19496        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
19497        assert!(!i.flip_flop_active[0]);
19498        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
19499    }
19500}